aboutsummaryrefslogtreecommitdiff
path: root/internal/validate/checks.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 16:13:49 -0400
committerhistoria <[not public]>2026-09-18 16:13:49 -0400
commit194164a77eb4f2479b94c19e6f0e1278ab608781 (patch)
tree53413add8ed6f32a34d1733397e380cc98ef70ef /internal/validate/checks.go
parent67e01aab85607b9e601185c7d62dff576af3bb43 (diff)
downloadthehouseoficarus-194164a77eb4f2479b94c19e6f0e1278ab608781.tar.gz
fix: more casino, economy, gameplay, security, and world data hardening gaps
Diffstat (limited to 'internal/validate/checks.go')
-rw-r--r--internal/validate/checks.go44
1 files changed, 31 insertions, 13 deletions
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)
}