diff options
Diffstat (limited to 'internal/validate/validate.go')
| -rw-r--r-- | internal/validate/validate.go | 79 |
1 files changed, 79 insertions, 0 deletions
diff --git a/internal/validate/validate.go b/internal/validate/validate.go index 735c9ef..8ca4555 100644 --- a/internal/validate/validate.go +++ b/internal/validate/validate.go @@ -6,13 +6,18 @@ package validate import ( + "bytes" "fmt" "os" "path/filepath" "sort" + "strconv" "strings" + "gopkg.in/yaml.v3" + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/cardart" "thehouseoficarus/internal/item" "thehouseoficarus/internal/object" "thehouseoficarus/internal/world" @@ -67,6 +72,9 @@ func Run(s Source) []Issue { issues = append(issues, validateIDs(s.DataDir, dir.sub, dir.label)...) } + issues = append(issues, validateKnownFields(s.DataDir)...) + issues = append(issues, validateRoomFilenames(s.DataDir)...) + issues = append(issues, validateRooms(s)...) issues = append(issues, validateMobs(s)...) issues = append(issues, validateHazards(s)...) @@ -84,6 +92,77 @@ func Run(s Source) []Issue { return issues } +// validateRoomFilenames flags room files whose name is not a plain integer: +// the room index only accepts numeric stems, so a file like +// "1001_copy.yaml" would otherwise be silently ignored (and, before the +// strconv fix, could shadow the real room 1001). +func validateRoomFilenames(dataDir string) []Issue { + dir := filepath.Join(dataDir, "rooms") + var issues []Issue + _ = behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { + if _, err := strconv.Atoi(id); err != nil { + rel := strings.TrimPrefix(path, dataDir+"/") + issues = append(issues, Issue{ + Level: "ERROR", + Type: "integrity", + Message: fmt.Sprintf("room %s: filename is not a numeric room ID and the file is ignored", rel), + }) + } + return nil + }) + return issues +} + +// validateKnownFields decodes every authored data file with KnownFields(true) +// so a misspelled or stale YAML key is a startup ERROR instead of being +// silently discarded by yaml.Unmarshal. Only package-owned content types are +// covered (world/object/item/behavior); game-owned definitions are checked by +// their own loaders. +func validateKnownFields(dataDir string) []Issue { + targets := []struct { + subDir string + label string + newDef func() any + }{ + {"rooms", "room", func() any { return new(world.Room) }}, + {"mobs", "mob", func() any { return new(world.MobDef) }}, + {"objects", "object", func() any { return new(object.ObjectDef) }}, + {"items", "item", func() any { return new(item.ItemDef) }}, + {"drops", "drop table", func() any { return new(behavior.DropTableDef) }}, + {"hazards", "hazard", func() any { return new(world.HazardDef) }}, + } + var issues []Issue + for _, t := range targets { + dir := filepath.Join(dataDir, t.subDir) + _ = behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { + dec := yaml.NewDecoder(bytes.NewReader(data)) + dec.KnownFields(true) + if err := dec.Decode(t.newDef()); err != nil { + rel := strings.TrimPrefix(path, dataDir+"/") + issues = append(issues, Issue{ + Level: "ERROR", + Type: "unknown-field", + Message: fmt.Sprintf("%s %s: %v", t.label, rel, err), + }) + } + return nil + }) + } + // cards.yaml lives at the data root rather than a subdirectory. + if data, err := os.ReadFile(filepath.Join(dataDir, "cards.yaml")); err == nil { + dec := yaml.NewDecoder(bytes.NewReader(data)) + dec.KnownFields(true) + if err := dec.Decode(new(cardart.Store)); err != nil { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "unknown-field", + Message: fmt.Sprintf("cards.yaml: %v", err), + }) + } + } + return issues +} + func validateIDs(dataDir, subDir, label string) []Issue { dir := filepath.Join(dataDir, subDir) dups := behavior.CheckDuplicateIDs(dir) |
