From 7eab582d8a8ee2af1a6baae7baf0696c7005de2e Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 15:16:44 -0400 Subject: fix: harden triggers, economy, casino, world data, and admin/net paths --- internal/validate/checks.go | 106 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) (limited to 'internal/validate/checks.go') diff --git a/internal/validate/checks.go b/internal/validate/checks.go index eb24721..a6eeee8 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -427,6 +427,16 @@ func validateMobs(s Source) []Issue { }) } + // A weakness that matches no module element is dead data: the +weakness + // bonus can never trigger against any science module. + if s.ModuleElements != nil && def.Combat != nil && def.Combat.Stats.Defenses.Weakness != "" && !s.ModuleElements[def.Combat.Stats.Defenses.Weakness] { + issues = append(issues, Issue{ + Level: "WARN", + Type: "reference", + Message: fmt.Sprintf("Mob %q: weakness %q matches no science module element (the weakness can never trigger)", id, def.Combat.Stats.Defenses.Weakness), + }) + } + if !def.Protected && (def.Combat == nil || def.Combat.Stats.HP <= 0) { hp := 0 if def.Combat != nil { @@ -761,6 +771,15 @@ func validateCraftBlocks(s Source) []Issue { } for _, e := range c.Ingredients { + if len(e.Items) == 0 { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Item %q (craft: %s): ingredient entry has no items; the recipe can never be satisfied", + item.ID, craftLabel), + }) + continue + } // Byproducts pair with the alternatives by index // (collectByproducts reads Byproducts[j] for Items[j]), so a // short or reordered list silently returns the wrong @@ -1165,6 +1184,93 @@ func validateRoomWiring(s Source) []Issue { }) } + // Reachability: an orphan check only catches rooms with zero inbound + // edges, so an entire disconnected component (every member linked only + // within the component) passes silently. BFS the directed exits from the + // configured root rooms and warn about anything with no path in, honouring + // ignore_unreachable. + reachable := make(map[int]bool) + var queue []int + for _, src := range s.RootRooms { + if roomIndex[src] && !reachable[src] { + reachable[src] = true + queue = append(queue, src) + } + } + // Transport module destinations are also legitimate entry points: a + // player can reach them with a module even when no exit path leads there, + // so they must not be reported as unreachable. + for _, dest := range s.ModuleDestinations { + if dest > 0 && roomIndex[dest] && !reachable[dest] { + reachable[dest] = true + queue = append(queue, dest) + } + } + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + room, err := s.World.LoadRoom(id) + if err != nil { + continue + } + for _, exit := range room.Exits { + next := exit.Room + if next <= 0 || reachable[next] || !roomIndex[next] { + continue + } + reachable[next] = true + queue = append(queue, next) + } + } + var unreachable []int + for id := range roomIndex { + if reachable[id] || ignoreTo[id] || sourceSet[id] { + continue + } + unreachable = append(unreachable, id) + } + if len(unreachable) > 0 { + sort.Ints(unreachable) + var strs []string + for _, id := range unreachable { + strs = append(strs, fmt.Sprintf("%d", id)) + } + issues = append(issues, Issue{ + Level: "WARN", + Type: "integrity", + Message: fmt.Sprintf("Unreachable rooms (no exit path from a configured root room): %s", + strings.Join(strs, ", ")), + }) + } + + return issues +} + +// validateModuleDestinations flags a transport module whose destination room +// does not exist: the game assigns the id unconditionally, so the player is +// saved into a void with no exits. +func validateModuleDestinations(s Source) []Issue { + if len(s.ModuleDestinations) == 0 { + return nil + } + roomIndex := s.World.RoomIndex() + ids := make([]string, 0, len(s.ModuleDestinations)) + for id := range s.ModuleDestinations { + ids = append(ids, id) + } + sort.Strings(ids) + var issues []Issue + for _, id := range ids { + dest := s.ModuleDestinations[id] + if dest <= 0 || roomIndex[dest] { + continue + } + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Module %q: destination room %d does not exist", id, dest), + }) + } return issues } -- cgit v1.2.3