From 194164a77eb4f2479b94c19e6f0e1278ab608781 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 16:13:49 -0400 Subject: fix: more casino, economy, gameplay, security, and world data hardening gaps --- internal/validate/checks.go | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) (limited to 'internal/validate/checks.go') diff --git a/internal/validate/checks.go b/internal/validate/checks.go index a6eeee8..bfa8342 100644 --- a/internal/validate/checks.go +++ b/internal/validate/checks.go @@ -24,6 +24,7 @@ func validateRooms(s Source) []Issue { mobIDs := s.Mobs.AllDefIDs() objIDs := s.Objects.IDSet() hazardIDs := s.World.HazardIndex() + dropIDs := dropTableIDSet(s.DataDir) for id := range roomIndex { room, err := s.World.LoadRoom(id) @@ -69,7 +70,14 @@ func validateRooms(s Source) []Issue { }) continue } - if exit.Room > 0 { + if exit.Room <= 0 && !exit.AlwaysBlocked { + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: exit %q has room %d; a non-positive target is a permanently blocked exit (set always_blocked: true to express that intent)", + id, dir, exit.Room), + }) + } else if exit.Room > 0 { if _, ok := roomIndex[exit.Room]; !ok { issues = append(issues, Issue{ Level: "ERROR", @@ -81,7 +89,7 @@ func validateRooms(s Source) []Issue { } issues = append(issues, validateTriggerBlock( fmt.Sprintf("Room %d: exit %q on_traverse", id, dir), - exit.OnTraverse, itemIDs, roomIndex, mobIDs, nil)...) + exit.OnTraverse, itemIDs, roomIndex, mobIDs, dropIDs)...) } for _, rm := range room.Mobs { @@ -128,7 +136,7 @@ func validateRooms(s Source) []Issue { displayName: world.NormalizeObjectName(robj.Local.Name), effDefID: robj.ID, }) - issues = append(issues, validateLocalObject(id, robj, itemIDs, roomIndex, mobIDs)...) + issues = append(issues, validateLocalObject(id, robj, itemIDs, roomIndex, mobIDs, dropIDs)...) } else if robj.ID != "" && !objIDs[robj.ID] { issues = append(issues, Issue{ Level: "ERROR", @@ -144,6 +152,15 @@ func validateRooms(s Source) []Issue { effDefID: robj.ID, }) } + } else { + // Neither a local definition nor a file reference: the entry + // silently disappears at runtime, so surface it as an error. + issues = append(issues, Issue{ + Level: "ERROR", + Type: "reference", + Message: fmt.Sprintf("Room %d: object entry %d resolves to nothing (no local definition and no id)", + id, idx), + }) } for _, wr := range robj.WanderRooms { if _, ok := roomIndex[wr]; !ok { @@ -176,7 +193,7 @@ func validateRooms(s Source) []Issue { // are restricted to the passive subset (name/aliases/color/hidden/ // inroom_description/description/on_look); interactable or stateful behavior // must be defined as a standalone object file instead. -func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue { +func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue def := robj.Local prefix := fmt.Sprintf("Room %d: local object %q", roomID, robj.ID) @@ -233,7 +250,7 @@ func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]b } if len(def.OnLook) > 0 { - issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, itemIDs, roomIndex, mobIDs, nil)...) + issues = append(issues, validateTriggerBlock(prefix+": on_look", def.OnLook, itemIDs, roomIndex, mobIDs, dropIDs)...) } return issues @@ -678,7 +695,7 @@ func validateObjects(s Source) []Issue { } if obj.Talk != nil { - issues = append(issues, validateTalkConfig(fmt.Sprintf("Object %q: talk", id), obj.Talk, itemIDs, roomIndex)...) + issues = append(issues, validateTalkConfig(fmt.Sprintf("Object %q: talk", id), obj.Talk, itemIDs, roomIndex, mobIDs, dropIDs)...) } if len(obj.OnLook) > 0 { @@ -1112,6 +1129,7 @@ func validateRoomTriggers(s Source) []Issue { roomIndex := s.World.RoomIndex() itemIDs := s.Items.IDSet() mobIDs := s.Mobs.AllDefIDs() + dropIDs := dropTableIDSet(s.DataDir) for id := range roomIndex { room, err := s.World.LoadRoom(id) @@ -1120,11 +1138,11 @@ func validateRoomTriggers(s Source) []Issue { } if len(room.OnEnter) > 0 { issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, itemIDs, roomIndex, mobIDs, nil)...) + fmt.Sprintf("Room %d: on_enter", id), room.OnEnter, itemIDs, roomIndex, mobIDs, dropIDs)...) } if len(room.OnExit) > 0 { issues = append(issues, validateTriggerBlock( - fmt.Sprintf("Room %d: on_exit", id), room.OnExit, itemIDs, roomIndex, mobIDs, nil)...) + fmt.Sprintf("Room %d: on_exit", id), room.OnExit, itemIDs, roomIndex, mobIDs, dropIDs)...) } } @@ -1486,7 +1504,7 @@ func toolInList(list []string, target string) bool { return false } -func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[string]bool, roomIndex map[int]bool) []Issue { +func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue { var issues []Issue if cfg == nil { return issues @@ -1494,7 +1512,7 @@ func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[str for nodeID, node := range cfg.Nodes { nodePrefix := fmt.Sprintf("%s: node %q", prefix, nodeID) if node.Action != nil { - issues = append(issues, validateTalkStep(nodePrefix, node.Action, itemIDs, roomIndex)...) + issues = append(issues, validateTalkStep(nodePrefix, node.Action, itemIDs, roomIndex, mobIDs, dropIDs)...) } if node.Goto != "" { if _, ok := cfg.Nodes[node.Goto]; !ok { @@ -1508,7 +1526,7 @@ func validateTalkConfig(prefix string, cfg *behavior.TalkConfig, itemIDs map[str for i, opt := range node.Options { optPrefix := fmt.Sprintf("%s: option %d", nodePrefix, i+1) if opt.Action != nil { - issues = append(issues, validateTalkStep(optPrefix, opt.Action, itemIDs, roomIndex)...) + issues = append(issues, validateTalkStep(optPrefix, opt.Action, itemIDs, roomIndex, mobIDs, dropIDs)...) } } } @@ -1750,9 +1768,9 @@ func validateTriggerBlock(prefix string, block []behavior.Trigger, itemIDs map[s // validateTalkStep validates a talk node/option Action (a single Step, no mob // spawn/despawn expected — mobIDs nil so those are skipped). -func validateTalkStep(prefix string, step *behavior.Step, itemIDs map[string]bool, roomIndex map[int]bool) []Issue { +func validateTalkStep(prefix string, step *behavior.Step, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool, dropIDs map[string]bool) []Issue { if step == nil { return nil } - return validateStep(prefix, step, itemIDs, roomIndex, nil, nil) + return validateStep(prefix, step, itemIDs, roomIndex, mobIDs, dropIDs) } -- cgit v1.2.3