diff options
Diffstat (limited to 'internal/validate')
| -rw-r--r-- | internal/validate/checks.go | 239 | ||||
| -rw-r--r-- | internal/validate/local_test.go | 8 |
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) } |
