From 1091d0fae96d152c11aec1bb2dd949f51ed5aa1e Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Mon, 5 Oct 2026 19:20:17 -0400 Subject: feat: shared tls cert, validation fixes --- internal/validate/checks.go | 63 +++++++++++++++++++++++- internal/validate/correct_audit_fixes_test.go | 69 +++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 2 deletions(-) (limited to 'internal/validate') diff --git a/internal/validate/checks.go b/internal/validate/checks.go index 8fe8e72..c636b4f 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -15,6 +15,7 @@ import ( "thehouseoficarus/internal/color" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/item" + "thehouseoficarus/internal/object" "thehouseoficarus/internal/world" ) @@ -427,6 +428,13 @@ func validateMobCombatStats(id string, c *world.MobCombat) []Issue { Message: fmt.Sprintf("Mob %q: stats.speed must be > 0 (0 is clipped to 1 tick, so the mob would attack every tick)", id), }) } + if c.Size != "" && object.MobSizeRank[c.Size] == 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Mob %q: size %q is not a known size (small/medium/large/massive); safespot cover would misjudge it", id, c.Size), + }) + } for _, t := range c.AttackTypes { switch { case combat.IsMeleeType(t): @@ -759,6 +767,37 @@ func validateObjects(s Source) []Issue { issues = append(issues, validateGather(fmt.Sprintf("Object %q: gather", id), obj.Gather, itemIDs, dropIDs)...) } + if obj.Safespot != nil { + if obj.Safespot.MaxBlockSize != "" && object.MobSizeRank[obj.Safespot.MaxBlockSize] == 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Object %q: safespot.max_block_size %q is not a known size (small/medium/large/massive); cover would be disabled", id, obj.Safespot.MaxBlockSize), + }) + } + if obj.Safespot.MaxOccupants < 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Object %q: safespot.max_occupants must be >= 0", id), + }) + } + if obj.Safespot.UnsafeChance < 0 || obj.Safespot.UnsafeChance > 1 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Object %q: safespot.unsafe_chance must be between 0 and 1", id), + }) + } + if obj.Safespot.DecayChance < 0 || obj.Safespot.DecayChance > 1 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Object %q: safespot.decay_chance must be between 0 and 1", id), + }) + } + } + for i := range obj.Description { if c := obj.Description[i].Condition; c != nil { issues = append(issues, validateCondition(fmt.Sprintf("Object %q: description[%d]", id, i), c, roomIndex, itemIDs)...) @@ -851,6 +890,8 @@ func validateCraftBlocks(s Source) []Issue { // item id: every runtime consumer matches it against ItemDef.ToolType() via // hasToolType. Types such as "pickaxe" or "big_net" have no same-named item // file, so membership is checked against the types the item defs define. + // Capture the package-level set before the loop shadows `item`. + knownCraftKeys := item.KnownCraftKeys toolTypes := make(map[string]bool) for _, def := range allItems { if t := def.ToolType(); t != "" { @@ -870,6 +911,24 @@ func validateCraftBlocks(s Source) []Issue { craftLabel = c.Type + "/" + c.Subtype } + // The runtime selects the action by subtype (or type when no subtype + // is set), defaulting to combine. An unknown key leaves the action + // set but never advancing, so the player appears stuck. + cKey := c.Subtype + if cKey == "" { + cKey = c.Type + } + if cKey == "" { + cKey = "combine" + } + if !knownCraftKeys[cKey] { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "config", + Message: fmt.Sprintf("Item %q (craft: %s): unknown type/subtype %q; the action would never advance", item.ID, craftLabel, cKey), + }) + } + // SuccessChance clamps to Cap; an omitted cap makes the recipe // fail every time (see craftFailureReachable, which would then // mask it as an intentional always-fail recipe). @@ -1491,7 +1550,7 @@ func validateRoomGrid(s Source) []Issue { onConf := func(origin int) func(c world.GridConflict) { return func(c world.GridConflict) { switch c.Kind { - case "twist": + case world.GridConflictTwist: issues = append(issues, Issue{ Level: "ERROR", Type: "integrity", @@ -1499,7 +1558,7 @@ func validateRoomGrid(s Source) []Issue { "Grid twist: room %d (via %d %s) maps to (%d,%d,%d) but was already placed at (%d,%d,%d) [origin %d]", c.Target, c.From, c.Dir, c.Want[0], c.Want[1], c.Want[2], c.Existing[0], c.Existing[1], c.Existing[2], origin), }) - case "overlap": + case world.GridConflictOverlap: issues = append(issues, Issue{ Level: "ERROR", Type: "integrity", diff --git a/internal/validate/correct_audit_fixes_test.go b/internal/validate/correct_audit_fixes_test.go index f11dd3f..c07ffcc 100644 --- a/internal/validate/correct_audit_fixes_test.go +++ b/internal/validate/correct_audit_fixes_test.go @@ -1,10 +1,15 @@ package validate import ( + "os" + "path/filepath" "strings" "testing" "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/world" ) // Regression: a talk config with nodes but no "start" node is dead content at @@ -32,6 +37,70 @@ func TestValidateTalkConfigRequiresStartNode(t *testing.T) { } } +// Regression: an unknown craft type/subtype leaves the action set but never +// advancing, so the player appears stuck; it must be an ERROR. +func TestValidateCraftBlocksRejectsUnknownType(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "items"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "items", "thing.yaml"), + []byte("name: thing\ncraft:\n - type: smiting\n ingredients:\n - items: [ore]\n quantity: 1\n"), 0644); err != nil { + t.Fatal(err) + } + src := Source{ + DataDir: dir, + Items: item.NewItemStore(dir), + Objects: object.NewObjectStore(dir), + Mobs: world.NewMobStore(dir), + World: world.New(dir), + } + found := false + for _, issue := range validateCraftBlocks(src) { + if strings.Contains(issue.Message, "unknown type/subtype") { + found = true + if issue.Level != "ERROR" { + t.Fatalf("unknown craft type must be an ERROR: %+v", issue) + } + } + } + if !found { + t.Fatal("expected an ERROR for an unknown craft type/subtype") + } +} + +// Regression: an unknown safespot max_block_size silently disables cover; it +// must be an ERROR. +func TestValidateObjectsRejectsUnknownSafespotSize(t *testing.T) { + dir := t.TempDir() + if err := os.MkdirAll(filepath.Join(dir, "objects"), 0755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "objects", "rock.yaml"), + []byte("name: rock\nsafespot:\n max_block_size: huge\n"), 0644); err != nil { + t.Fatal(err) + } + src := Source{ + DataDir: dir, + Items: item.NewItemStore(dir), + Objects: object.NewObjectStore(dir), + Mobs: world.NewMobStore(dir), + World: world.New(dir), + } + found := false + for _, issue := range validateObjects(src) { + if strings.Contains(issue.Message, "max_block_size") { + found = true + if issue.Level != "ERROR" { + t.Fatalf("unknown safespot size must be an ERROR: %+v", issue) + } + } + } + if !found { + t.Fatal("expected an ERROR for an unknown safespot.max_block_size") + } +} + // Regression: a gather drop with weight <= 0 can never be selected (the // weighted roll has zero total); it must be flagged. func TestValidateGatherWarnsOnWeightlessDrop(t *testing.T) { -- cgit v1.2.3