aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/mob.go5
-rw-r--r--internal/world/p2_hardening_test.go33
-rw-r--r--internal/world/room.go23
-rw-r--r--internal/world/world.go10
4 files changed, 70 insertions, 1 deletions
diff --git a/internal/world/mob.go b/internal/world/mob.go
index b004643..631e3a3 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -2,6 +2,7 @@ package world
import (
"fmt"
+ "log"
"math/rand"
"os"
"path/filepath"
@@ -441,6 +442,10 @@ func (s *MobStore) LoadDef(id string) (*MobDef, error) {
if err := yaml.Unmarshal(data, &def); err != nil {
return nil, fmt.Errorf("parse mob %s: %w", id, err)
}
+ if def.ID != "" && def.ID != id {
+ log.Printf("mob %s: file %s declares id %q but is loaded as %q; the filename is authoritative and the declared id is ignored",
+ id, path, def.ID, id)
+ }
def.ID = id
s.mu.Lock()
diff --git a/internal/world/p2_hardening_test.go b/internal/world/p2_hardening_test.go
new file mode 100644
index 0000000..01f8a04
--- /dev/null
+++ b/internal/world/p2_hardening_test.go
@@ -0,0 +1,33 @@
+package world
+
+import (
+ "testing"
+
+ "gopkg.in/yaml.v3"
+)
+
+// TestRoomObjectRejectsUnknownField: RoomObject.UnmarshalYAML uses
+// Node.Decode, which does not inherit KnownFields(true), so without an
+// explicit check a misspelled field inside a room's objects: entry is
+// silently discarded even though the top-level room file is strict.
+func TestRoomObjectRejectsUnknownField(t *testing.T) {
+ var ro RoomObject
+ if err := yaml.Unmarshal([]byte("name: anvil\nin_room_description: typo\n"), &ro); err == nil {
+ t.Fatal("accepted an unknown room object field")
+ }
+ // A legitimate local object still parses.
+ if err := yaml.Unmarshal([]byte("name: anvil\ninroom_description: A heavy anvil.\n"), &ro); err != nil {
+ t.Fatalf("rejected a valid local object: %v", err)
+ }
+}
+
+// TestSeedGroundItemsDoesNotMarkUnloadedRoom: a room that fails to load must
+// not be marked seeded, or its item_spawns stay disabled for the rest of the
+// process even after the room file is fixed/created.
+func TestSeedGroundItemsDoesNotMarkUnloadedRoom(t *testing.T) {
+ w := New(t.TempDir())
+ w.SeedGroundItems(999)
+ if w.seeded[999] {
+ t.Fatal("a room that failed to load was marked seeded; its spawns would never appear")
+ }
+}
diff --git a/internal/world/room.go b/internal/world/room.go
index f4390da..3783a73 100644
--- a/internal/world/room.go
+++ b/internal/world/room.go
@@ -1,6 +1,7 @@
package world
import (
+ "fmt"
"strings"
"gopkg.in/yaml.v3"
@@ -122,6 +123,28 @@ type RoomObject struct {
}
func (ro *RoomObject) UnmarshalYAML(value *yaml.Node) error {
+ // yaml.v3's Node.Decode builds a fresh decoder that does NOT inherit
+ // KnownFields(true), so a misspelled key inside a room's objects: entry
+ // (inroom_description, onlook, ...) would be silently discarded even
+ // though the top-level room file is decoded strictly. Reject unknown
+ // keys here so the startup validator's "unknown field is an ERROR"
+ // guarantee extends to the region authors edit most.
+ if value.Kind != yaml.MappingNode {
+ return fmt.Errorf("room object must be a mapping")
+ }
+ allowed := map[string]bool{
+ "id": true, "wander_rooms": true, "wander_interval": true, "hidden": true,
+ "name": true, "aliases": true, "color": true, "inroom_description": true,
+ "removal_item": true, "description": true, "on_use": true, "steal": true,
+ "gather": true, "talk": true, "safespot": true, "on_look": true,
+ }
+ for i := 0; i+1 < len(value.Content); i += 2 {
+ key := value.Content[i].Value
+ if !allowed[key] {
+ return fmt.Errorf("unknown room object field %q", key)
+ }
+ }
+
type rawRef struct {
ID string `yaml:"id"`
WanderRooms []int `yaml:"wander_rooms"`
diff --git a/internal/world/world.go b/internal/world/world.go
index 7475ce0..a084f63 100644
--- a/internal/world/world.go
+++ b/internal/world/world.go
@@ -91,16 +91,24 @@ func (w *World) SeedGroundItems(roomID int) {
w.mu.Unlock()
return
}
- w.seeded[roomID] = true
w.mu.Unlock()
room, err := w.LoadRoom(roomID)
if err != nil {
+ // Do NOT mark a room seeded before it loads: a dangling exit (or a
+ // not-yet-created room) would otherwise permanently disable its item
+ // spawns even after the room is fixed/created.
return
}
w.mu.Lock()
defer w.mu.Unlock()
+ // Re-check under the lock: a concurrent seed may have won the race while
+ // this call was loading.
+ if w.seeded[roomID] {
+ return
+ }
+ w.seeded[roomID] = true
for _, s := range room.ItemSpawns {
merged := false