// 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"}, {"triggers", "trigger"}, } { 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, validateRoomTriggers(s)...) issues = append(issues, validateRoomEnterSteps(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, }