diff options
| author | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-06-25 15:40:48 -0400 |
| commit | abd612c15799f604e671e83dc7c410ed2b44185f (patch) | |
| tree | 0597ca92350de73aac2a7cf26b2f2d6595dac0cc /internal/validate/validate.go | |
| parent | 2725e2927a1595c7b100d942d1f14146252adeb7 (diff) | |
| download | thehouseoficarus-abd612c15799f604e671e83dc7c410ed2b44185f.tar.gz | |
slop refactor
Diffstat (limited to 'internal/validate/validate.go')
| -rw-r--r-- | internal/validate/validate.go | 162 |
1 files changed, 162 insertions, 0 deletions
diff --git a/internal/validate/validate.go b/internal/validate/validate.go new file mode 100644 index 0000000..5557f72 --- /dev/null +++ b/internal/validate/validate.go @@ -0,0 +1,162 @@ +// Package validate runs startup integrity checks across all data files: +// duplicate IDs, referential integrity (rooms, mobs, objects, items, drops, +// courses, techs), and world wiring (orphan rooms). It depends only on the +// data packages (world, object, item, behavior), never on the game +// orchestrator, so it can be reused and tested in isolation. +package validate + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/item" + "thehouseoficarus/internal/object" + "thehouseoficarus/internal/world" +) + +// Issue is a single validation finding. +type Issue struct { + Level string // "ERROR" or "WARN" + Type string // "duplicate", "reference", "integrity" + Message string +} + +// CourseView is the subset of a course definition needed for validation. +type CourseView struct { + ID string + StartRoom int + ObstacleRooms []int +} + +// TechView is the subset of a tech definition needed for validation. +type TechView struct { + ID string + Name string + Category string + DrainRate float64 +} + +// Source bundles everything the validator reads. The game package builds this +// from its stores and passes it to Run. +type Source struct { + DataDir string + Items *item.ItemStore + Objects *object.ObjectStore + Mobs *world.MobStore + World *world.World + Courses []CourseView + Techs []TechView + TechIDs map[string]bool + CheckSources []int + IgnoreUnreachable []int +} + +// Run executes all checks and returns the collected issues. +func Run(s Source) []Issue { + var issues []Issue + + for _, dir := range []struct{ sub, label string }{ + {"items", "item"}, {"rooms", "room"}, {"mobs", "mob"}, + {"objects", "object"}, {"drops", "drop table"}, {"courses", "course"}, + {"modules", "module"}, {"techs", "tech"}, {"help", "help topic"}, + } { + issues = append(issues, validateIDs(s.DataDir, dir.sub, dir.label)...) + } + + issues = append(issues, validateRooms(s)...) + issues = append(issues, validateMobs(s)...) + issues = append(issues, validateHazards(s)...) + issues = append(issues, validateObjects(s)...) + issues = append(issues, validateItems(s)...) + issues = append(issues, validateCraftBlocks(s)...) + issues = append(issues, validateDropTables(s)...) + issues = append(issues, validateCourses(s)...) + issues = append(issues, validateTechs(s)...) + issues = append(issues, validateRoomWiring(s)...) + + return issues +} + +func validateIDs(dataDir, subDir, label string) []Issue { + dir := filepath.Join(dataDir, subDir) + dups := behavior.CheckDuplicateIDs(dir) + var issues []Issue + for _, d := range dups { + short := make([]string, len(d.Paths)) + for i, p := range d.Paths { + short[i] = strings.TrimPrefix(p, dir+"/") + } + issues = append(issues, Issue{ + Level: "ERROR", + Type: "duplicate", + Message: fmt.Sprintf("Duplicate %s ID %q found in: %s", + label, d.ID, strings.Join(short, ", ")), + }) + } + return issues +} + +func dropTableIDSet(dataDir string) map[string]bool { + dir := filepath.Join(dataDir, "drops") + ids := make(map[string]bool) + behavior.WalkYAMLDir(dir, func(path, id string, data []byte) error { + ids[id] = true + return nil + }) + return ids +} + +// LogIssues prints validation results to stderr, sorted with errors first. +func LogIssues(issues []Issue) { + if len(issues) == 0 { + fmt.Fprintf(os.Stderr, "Startup validation: OK\n") + return + } + + errors := 0 + warns := 0 + for _, issue := range issues { + if issue.Level == "ERROR" { + errors++ + } else { + warns++ + } + } + + fmt.Fprintf(os.Stderr, "\n=== Startup Validation: %d error(s), %d warning(s) ===\n\n", errors, warns) + + sort.Slice(issues, func(i, j int) bool { + if issues[i].Level != issues[j].Level { + return issues[i].Level == "ERROR" + } + return issues[i].Message < issues[j].Message + }) + + for _, issue := range issues { + tag := fmt.Sprintf("[%s]", issue.Level) + fmt.Fprintf(os.Stderr, " %-7s %s\n", tag, issue.Message) + } + fmt.Fprintln(os.Stderr) + + if errors > 0 { + fmt.Fprintf(os.Stderr, "*** %d startup errors detected. The server may behave unexpectedly. ***\n", errors) + } +} + +var knownTechCategories = map[string]bool{ + "accuracy": true, "strength": true, "defense": true, "ranged": true, + "science": true, "protection": true, "utility": true, "combo": true, +} + +// reservedTechIDs are tech IDs whose semantics are coupled to code logic and +// must exist in YAML (damage protection mapping + death retribution). +var reservedTechIDs = map[string]bool{ + "protect_melee": true, + "protect_ranged": true, + "protect_science": true, + "retribution": true, +} |
