aboutsummaryrefslogtreecommitdiff
path: root/internal/validate
diff options
context:
space:
mode:
Diffstat (limited to 'internal/validate')
-rw-r--r--internal/validate/checks.go239
-rw-r--r--internal/validate/local_test.go8
2 files changed, 133 insertions, 114 deletions
diff --git a/internal/validate/checks.go b/internal/validate/checks.go
index 64b1df4..47bbd1a 100644
--- a/internal/validate/checks.go
+++ b/internal/validate/checks.go
@@ -57,16 +57,20 @@ func validateRooms(s Source) []Issue {
})
continue
}
- if exit.Room <= 0 {
- continue
+ if exit.Room > 0 {
+ if _, ok := roomIndex[exit.Room]; !ok {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("Room %d: exit %q targets nonexistent room %d",
+ id, dir, exit.Room),
+ })
+ }
}
- if _, ok := roomIndex[exit.Room]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("Room %d: exit %q targets nonexistent room %d",
- id, dir, exit.Room),
- })
+ for i, it := range exit.OnTraverse {
+ issues = append(issues, validateStepAction(
+ fmt.Sprintf("Room %d: exit %q on_traverse[%d]", id, dir, i),
+ it.Action, itemIDs, roomIndex, mobIDs)...)
}
}
@@ -114,7 +118,7 @@ func validateRooms(s Source) []Issue {
displayName: world.NormalizeObjectName(robj.Local.Name),
effDefID: robj.ID,
})
- issues = append(issues, validateLocalObject(id, robj, itemIDs, roomIndex)...)
+ issues = append(issues, validateLocalObject(id, robj, itemIDs, roomIndex, mobIDs)...)
} else if robj.ID != "" && !objIDs[robj.ID] {
issues = append(issues, Issue{
Level: "ERROR",
@@ -162,7 +166,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) []Issue {
+func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue {
var issues []Issue
def := robj.Local
prefix := fmt.Sprintf("Room %d: local object %q", roomID, robj.ID)
@@ -192,8 +196,8 @@ func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]b
if def.Safespot != nil {
bad = append(bad, "safespot")
}
- if len(def.UseInteractions) > 0 {
- bad = append(bad, "use_interactions")
+ if len(def.OnUse) > 0 {
+ bad = append(bad, "on_use")
}
if def.Steal != nil {
bad = append(bad, "steal")
@@ -218,8 +222,8 @@ func validateLocalObject(roomID int, robj world.RoomObject, itemIDs map[string]b
})
}
- if def.OnLook != nil {
- issues = append(issues, validateNodeAction(prefix+": on_look", def.OnLook, itemIDs, roomIndex)...)
+ if len(def.OnLook) > 0 {
+ issues = append(issues, validateOnLook(prefix+": on_look", def.OnLook, itemIDs, roomIndex, mobIDs)...)
}
return issues
@@ -343,6 +347,7 @@ func validateMobs(s Source) []Issue {
itemIDs := s.Items.IDSet()
dropIDs := dropTableIDSet(s.DataDir)
mobIDs := s.Mobs.AllDefIDs()
+ roomIndex := s.World.RoomIndex()
for id := range mobIDs {
def, err := s.Mobs.LoadDef(id)
@@ -481,6 +486,11 @@ func validateMobs(s Source) []Issue {
}
}
}
+
+ for i, it := range def.OnKill {
+ issues = append(issues, validateStepAction(
+ fmt.Sprintf("Mob %q: on_kill[%d]", id, i), it.Action, itemIDs, roomIndex, mobIDs)...)
+ }
}
return issues
@@ -584,17 +594,17 @@ func validateObjects(s Source) []Issue {
}
}
- for _, ui := range obj.UseInteractions {
+ for _, ui := range obj.OnUse {
if ui.Item != "" && !itemIDs[ui.Item] {
issues = append(issues, Issue{
Level: "ERROR",
Type: "reference",
- Message: fmt.Sprintf("Object %q: use_interaction item %q does not exist",
+ Message: fmt.Sprintf("Object %q: on_use item %q does not exist",
id, ui.Item),
})
}
if ui.Action != nil {
- issues = append(issues, validateNodeAction(fmt.Sprintf("Object %q: use_interaction", id), ui.Action, itemIDs, roomIndex)...)
+ issues = append(issues, validateStepAction(fmt.Sprintf("Object %q: on_use", id), ui.Action, itemIDs, roomIndex, mobIDs)...)
}
}
@@ -606,9 +616,8 @@ func validateObjects(s Source) []Issue {
issues = append(issues, validateTalkConfig(fmt.Sprintf("Object %q: talk", id), obj.Talk, itemIDs, roomIndex)...)
}
-
- if obj.OnLook != nil {
- issues = append(issues, validateNodeAction(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, itemIDs, roomIndex)...)
+ if len(obj.OnLook) > 0 {
+ issues = append(issues, validateOnLook(fmt.Sprintf("Object %q: on_look", id), obj.OnLook, itemIDs, roomIndex, mobIDs)...)
}
}
@@ -804,49 +813,9 @@ func validateRoomEnterSteps(s Source) []Issue {
if err != nil || len(room.OnEnter) == 0 {
continue
}
- for si, step := range room.OnEnter {
+ for si := range room.OnEnter {
stepPrefix := fmt.Sprintf("Room %d: on_enter[%d]", id, si)
- if step.SpawnMob != nil && step.SpawnMob.ID != "" {
- if _, ok := mobIDs[step.SpawnMob.ID]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: spawn_mob %q does not exist", stepPrefix, step.SpawnMob.ID),
- })
- }
- for _, wr := range step.SpawnMob.DespawnRooms {
- if _, ok := roomIndex[wr]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: spawn_mob despawn_rooms references nonexistent room %d", stepPrefix, wr),
- })
- }
- }
- }
- if step.GiveItem != "" && !itemIDs[step.GiveItem] {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: give_item %q does not exist", stepPrefix, step.GiveItem),
- })
- }
- if step.TakeItem != "" && !itemIDs[step.TakeItem] {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: take_item %q does not exist", stepPrefix, step.TakeItem),
- })
- }
- if step.Teleport > 0 {
- if _, ok := roomIndex[step.Teleport]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: teleport to nonexistent room %d", stepPrefix, step.Teleport),
- })
- }
- }
+ issues = append(issues, validateStepAction(stepPrefix, &room.OnEnter[si], itemIDs, roomIndex, mobIDs)...)
}
}
@@ -866,54 +835,9 @@ func validateRoomTriggers(s Source) []Issue {
}
for ti, trigger := range room.Triggers {
prefix := fmt.Sprintf("Room %d: trigger[%d]", id, ti)
- for si, step := range trigger.Steps {
+ for si := range trigger.Steps {
stepPrefix := fmt.Sprintf("%s: step[%d]", prefix, si)
- if step.SpawnMob != nil && step.SpawnMob.ID != "" {
- if _, ok := mobIDs[step.SpawnMob.ID]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: spawn_mob %q does not exist",
- stepPrefix, step.SpawnMob.ID),
- })
- }
- for _, wr := range step.SpawnMob.DespawnRooms {
- if _, ok := roomIndex[wr]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: spawn_mob despawn_rooms references nonexistent room %d",
- stepPrefix, wr),
- })
- }
- }
- }
- if step.GiveItem != "" && !itemIDs[step.GiveItem] {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: give_item %q does not exist",
- stepPrefix, step.GiveItem),
- })
- }
- if step.TakeItem != "" && !itemIDs[step.TakeItem] {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: take_item %q does not exist",
- stepPrefix, step.TakeItem),
- })
- }
- if step.Teleport > 0 {
- if _, ok := roomIndex[step.Teleport]; !ok {
- issues = append(issues, Issue{
- Level: "ERROR",
- Type: "reference",
- Message: fmt.Sprintf("%s: teleport to nonexistent room %d",
- stepPrefix, step.Teleport),
- })
- }
- }
+ issues = append(issues, validateStepAction(stepPrefix, &trigger.Steps[si], itemIDs, roomIndex, mobIDs)...)
}
}
}
@@ -1309,3 +1233,98 @@ func validateNodeAction(prefix string, na *behavior.NodeAction, itemIDs map[stri
return issues
}
+
+// validateStepAction validates the universal effect superset used by on_use,
+// on_look, on_kill, on_enter steps, trigger steps, and exit
+// traversal. It covers the inline NodeAction fields plus spawn_mob/despawn_mob
+// (when mobIDs is non-nil). Returns the list of issues found.
+func validateStepAction(prefix string, step *behavior.StepAction, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue {
+ var issues []Issue
+ if step == nil {
+ return issues
+ }
+
+ // Validate the embedded NodeAction subset.
+ if step.GiveItem != "" && !itemIDs[step.GiveItem] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: give_item %q does not exist",
+ prefix, step.GiveItem),
+ })
+ }
+ if step.TakeItem != "" && !itemIDs[step.TakeItem] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: take_item %q does not exist",
+ prefix, step.TakeItem),
+ })
+ }
+ if step.Teleport > 0 {
+ if _, ok := roomIndex[step.Teleport]; !ok {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: teleport to nonexistent room %d",
+ prefix, step.Teleport),
+ })
+ }
+ }
+
+ // Spawn mob: validate mob def + despawn_rooms (only if mobIDs populated —
+ // a nil map means the caller doesn't track mob ids, e.g. local objects).
+ if step.SpawnMob != nil && step.SpawnMob.ID != "" {
+ if mobIDs != nil && !mobIDs[step.SpawnMob.ID] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: spawn_mob %q does not exist",
+ prefix, step.SpawnMob.ID),
+ })
+ }
+ for _, wr := range step.SpawnMob.DespawnRooms {
+ if _, ok := roomIndex[wr]; !ok {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: spawn_mob despawn_rooms references nonexistent room %d",
+ prefix, wr),
+ })
+ }
+ }
+ }
+
+ // Despawn mob.
+ if step.DespawnMob != "" && mobIDs != nil && !mobIDs[step.DespawnMob] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: despawn_mob %q does not exist",
+ prefix, step.DespawnMob),
+ })
+ }
+
+ return issues
+}
+
+// validateOnLook validates a list of on_look interactions (now a list, not a
+// single NodeAction). For each entry, validates the optional item_id (unused
+// on on_look but permitted), the action's effects, and skips (the entry's
+// condition is a runtime gate, not a static referential ref).
+func validateOnLook(prefix string, list []behavior.Interaction, itemIDs map[string]bool, roomIndex map[int]bool, mobIDs map[string]bool) []Issue {
+ var issues []Issue
+ for i, it := range list {
+ entryPrefix := fmt.Sprintf("%s[%d]", prefix, i)
+ if it.Item != "" && !itemIDs[it.Item] {
+ issues = append(issues, Issue{
+ Level: "ERROR",
+ Type: "reference",
+ Message: fmt.Sprintf("%s: item_id %q does not exist",
+ entryPrefix, it.Item),
+ })
+ }
+ issues = append(issues, validateStepAction(entryPrefix, it.Action, itemIDs, roomIndex, mobIDs)...)
+ }
+ return issues
+}
diff --git a/internal/validate/local_test.go b/internal/validate/local_test.go
index d37838e..337f3d2 100644
--- a/internal/validate/local_test.go
+++ b/internal/validate/local_test.go
@@ -17,7 +17,7 @@ func TestValidateLocalObjectPassiveOK(t *testing.T) {
Hidden: true,
Description: behavior.DescList{{Text: "A small window."}},
}}
- issues := validateLocalObject(1001, robj, nil, nil)
+ issues := validateLocalObject(1001, robj, nil, nil, nil)
if len(issues) != 0 {
t.Errorf("expected no issues for passive local object, got: %+v", issues)
}
@@ -28,7 +28,7 @@ func TestValidateLocalObjectRejectsInteractable(t *testing.T) {
Name: "rock",
Gather: &behavior.GatherConfig{},
}}
- issues := validateLocalObject(1001, robj, nil, nil)
+ issues := validateLocalObject(1001, robj, nil, nil, nil)
if !containsMsg(issues, "interactable behavior") {
t.Errorf("expected interactable-behavior error, got: %+v", issues)
}
@@ -38,7 +38,7 @@ func TestValidateLocalObjectRequiresName(t *testing.T) {
robj := world.RoomObject{ID: "x", Local: &object.ObjectDef{
Description: behavior.DescList{{Text: "no name"}},
}}
- issues := validateLocalObject(1001, robj, nil, nil)
+ issues := validateLocalObject(1001, robj, nil, nil, nil)
if !containsMsg(issues, "has no name") {
t.Errorf("expected has-no-name error, got: %+v", issues)
}
@@ -46,7 +46,7 @@ func TestValidateLocalObjectRequiresName(t *testing.T) {
func TestValidateLocalObjectStrayIDWarns(t *testing.T) {
robj := world.RoomObject{ID: "anvil", Local: &object.ObjectDef{Name: "anvil", ID: "anvil"}}
- issues := validateLocalObject(1001, robj, nil, nil)
+ issues := validateLocalObject(1001, robj, nil, nil, nil)
if !containsMsg(issues, "ignored on local objects") {
t.Errorf("expected stray-id warning, got: %+v", issues)
}