diff options
Diffstat (limited to 'internal/validate')
| -rw-r--r-- | internal/validate/checks.go | 44 | ||||
| -rw-r--r-- | internal/validate/local_test.go | 8 | ||||
| -rw-r--r-- | internal/validate/p2_hardening_test.go | 50 |
3 files changed, 85 insertions, 17 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) } diff --git a/internal/validate/local_test.go b/internal/validate/local_test.go index f0bc0a1..f696b7b 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, nil) + issues := validateLocalObject(1001, robj, nil, 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, nil) + issues := validateLocalObject(1001, robj, nil, 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, nil) + issues := validateLocalObject(1001, robj, nil, 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, nil) + issues := validateLocalObject(1001, robj, nil, nil, nil, nil) if !containsMsg(issues, "ignored on local objects") { t.Errorf("expected stray-id warning, got: %+v", issues) } diff --git a/internal/validate/p2_hardening_test.go b/internal/validate/p2_hardening_test.go new file mode 100644 index 0000000..68ae48f --- /dev/null +++ b/internal/validate/p2_hardening_test.go @@ -0,0 +1,50 @@ +package validate + +import ( + "path/filepath" + "testing" +) + +// TestValidateRejectsNonPositiveExit: an exit with room <= 0 renders as a +// blocked direction but passes every other check, so it must be reported +// unless always_blocked: true expresses the intent. +func TestValidateRejectsNonPositiveExit(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), "name: Blocked\nexits:\n north:\n room: 0\n") + issues := validateRooms(newSource(dir)) + if !containsMsg(issues, "non-positive") { + t.Fatalf("non-positive exit not reported: %+v", issues) + } +} + +// TestValidateRejectsEmptyObjectEntry: an objects: entry that is neither a +// local definition nor a file reference disappears at runtime, so it must be +// an error instead of a silent no-op. +func TestValidateRejectsEmptyObjectEntry(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), + "name: Empty\nobjects:\n - aliases: []\n description: []\n hidden: false\n name: \"\"\n") + issues := validateRooms(newSource(dir)) + if !containsMsg(issues, "resolves to nothing") { + t.Fatalf("empty object entry not reported: %+v", issues) + } +} + +// TestValidateRejectsDropTableInRoomTrigger: room on_enter/on_exit and +// on_traverse steps consume drop_table like every other trigger block, so a +// typo'd table id must be reported rather than silently awarding nothing. +func TestValidateRejectsDropTableInRoomTrigger(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "rooms", "1.yaml"), + "name: Loot\nexits:\n north:\n room: 1\n on_traverse:\n - steps:\n - drop_table:\n - table: no_such_table\n weight: 100\n") + if issues := validateRooms(newSource(dir)); !containsMsg(issues, "no_such_table") { + t.Fatalf("unknown drop table in an exit trigger not reported: %+v", issues) + } + + // on_enter is validated by validateRoomTriggers. + writeFile(t, filepath.Join(dir, "rooms", "2.yaml"), + "name: Loot2\non_enter:\n - steps:\n - drop_table:\n - table: no_such_table\n weight: 100\n") + if issues := validateRoomTriggers(newSource(dir)); !containsMsg(issues, "no_such_table") { + t.Fatalf("unknown drop table in on_enter not reported: %+v", issues) + } +} |
