aboutsummaryrefslogtreecommitdiff
path: root/internal/validate/validate.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/validate/validate.go')
-rw-r--r--internal/validate/validate.go162
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,
+}