aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 20:03:13 -0400
committerhistoria <[not public]>2026-09-18 20:03:13 -0400
commit79764732406d6484f67d3dec045483dcb5d08c28 (patch)
tree7a64e8581a540b97ed554f5723e2d587424ab1c6 /internal/world
parentf1e461ab6aec6b9bf056bbe2daa88a251e0f21da (diff)
downloadthehouseoficarus-79764732406d6484f67d3dec045483dcb5d08c28.tar.gz
feat: better card ascii art
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/grid_test.go2
-rw-r--r--internal/world/mob.go25
-rw-r--r--internal/world/mob_rekey_test.go23
-rw-r--r--internal/world/objects.go7
-rw-r--r--internal/world/room_migrate.go42
5 files changed, 98 insertions, 1 deletions
diff --git a/internal/world/grid_test.go b/internal/world/grid_test.go
index 005f60f..6cf53d0 100644
--- a/internal/world/grid_test.go
+++ b/internal/world/grid_test.go
@@ -82,7 +82,7 @@ func TestBuildGridConflictsTwist(t *testing.T) {
func TestBuildGridConflictsDiagonalClean(t *testing.T) {
dir := t.TempDir()
writeGridTestRoom(t, dir, 1, "exits:\n northeast: {room: 2}\n")
- writeGridTestRoom(t, dir, 2, "name: two\n exits:\n southwest: {room: 1}\n")
+ writeGridTestRoom(t, dir, 2, "name: two\nexits:\n southwest: {room: 1}\n")
w := New(dir)
if conflicts := BuildGridConflicts(1, loadGridRoom(w)); len(conflicts) != 0 {
diff --git a/internal/world/mob.go b/internal/world/mob.go
index afd9024..4441108 100644
--- a/internal/world/mob.go
+++ b/internal/world/mob.go
@@ -747,7 +747,14 @@ func (s *MobStore) RekeyRooms(idMap map[int]int) {
}
s.mu.Lock()
defer s.mu.Unlock()
+ // A seeded instance's key embeds its home room (SeedMobs builds
+ // "<def>_<room>_<i>"). Leaving the old key in place would make SeedMobs
+ // create a duplicate instance for the renamed room and would let a
+ // recycled ID skip seeding entirely, so the map is rebuilt with the
+ // rewritten keys.
+ rekeyed := make(map[string]*MobInstance, len(s.instances))
for _, inst := range s.instances {
+ oldHome := inst.HomeRoomID
if newID, ok := idMap[inst.RoomID]; ok && newID != inst.RoomID {
inst.RoomID = newID
}
@@ -764,5 +771,23 @@ func (s *MobStore) RekeyRooms(idMap map[int]int) {
inst.DespawnRooms[i] = newID
}
}
+ if inst.HomeRoomID != oldHome {
+ inst.InstanceID = rekeySeededInstanceID(inst.InstanceID, inst.DefID, oldHome, inst.HomeRoomID)
+ }
+ rekeyed[inst.InstanceID] = inst
+ }
+ s.instances = rekeyed
+}
+
+// rekeySeededInstanceID rewrites the room component of a seeded instance ID
+// ("<def>_<oldRoom>_<suffix>"). IDs that do not match the seeded shape (a
+// transient or test instance) are returned unchanged. The DefID prefix is
+// stripped by string match rather than parsed because a mob def ID may itself
+// contain underscores.
+func rekeySeededInstanceID(instanceID, defID string, oldRoom, newRoom int) string {
+ prefix := fmt.Sprintf("%s_%d_", defID, oldRoom)
+ if !strings.HasPrefix(instanceID, prefix) {
+ return instanceID
}
+ return fmt.Sprintf("%s_%d_%s", defID, newRoom, strings.TrimPrefix(instanceID, prefix))
}
diff --git a/internal/world/mob_rekey_test.go b/internal/world/mob_rekey_test.go
index 3a0eb85..8118059 100644
--- a/internal/world/mob_rekey_test.go
+++ b/internal/world/mob_rekey_test.go
@@ -2,6 +2,29 @@ package world
import "testing"
+// TestRekeyRoomsRewritesSeededInstanceID: a seeded instance's map key embeds
+// its home room ("<def>_<room>_<i>"). Renaming the room must rewrite the key,
+// or SeedMobs creates a duplicate instance for the renamed room and a later
+// recycled ID skips seeding entirely.
+func TestRekeyRoomsRewritesSeededInstanceID(t *testing.T) {
+ s := &MobStore{instances: map[string]*MobInstance{
+ "rat_2_0": {InstanceID: "rat_2_0", DefID: "rat", RoomID: 2, HomeRoomID: 2},
+ "bat_5_1": {InstanceID: "bat_5_1", DefID: "bat", RoomID: 5, HomeRoomID: 5},
+ }}
+ s.RekeyRooms(map[int]int{2: 4})
+
+ if _, stale := s.instances["rat_2_0"]; stale {
+ t.Fatal("stale seeded key survived the rekey")
+ }
+ inst, ok := s.instances["rat_4_0"]
+ if !ok || inst.RoomID != 4 || inst.HomeRoomID != 4 {
+ t.Fatalf("rekeyed instance = %+v (present=%v), want rat_4_0 in room 4", inst, ok)
+ }
+ if _, ok := s.instances["bat_5_1"]; !ok {
+ t.Fatal("an unrelated instance was dropped")
+ }
+}
+
// TestRekeyRoomsUpdatesMobDespawnRooms: DespawnRooms is a live room reference
// (sys_triggers compares it against the owner's current room). A room rename
// must remap it alongside RoomID/HomeRoomID/WanderRooms, or a transient mob
diff --git a/internal/world/objects.go b/internal/world/objects.go
index 849c47a..fa17f08 100644
--- a/internal/world/objects.go
+++ b/internal/world/objects.go
@@ -308,6 +308,13 @@ func (w *World) TickObjWander() {
if toRoom == st.RoomID {
continue
}
+ // A wander_rooms entry may point at a room that was since deleted.
+ // Moving there would strand the object in a nonexistent room, so
+ // leave it where it is. (roomPathIndex is read under w.mu, which
+ // this method already holds; LoadRoom would deadlock here.)
+ if _, ok := w.roomPathIndex[toRoom]; !ok {
+ continue
+ }
moves = append(moves, struct {
st *ObjState
toRoom int
diff --git a/internal/world/room_migrate.go b/internal/world/room_migrate.go
index b7840bc..42cd1ae 100644
--- a/internal/world/room_migrate.go
+++ b/internal/world/room_migrate.go
@@ -101,6 +101,48 @@ func (r *Room) RewriteRoomIDs(idMap map[int]int) bool {
return changed
}
+// RemoveRoomRefs drops references to a deleted room from this room's
+// per-instance mob and object wander lists. Exit targets are removed by the
+// deletion caller. Teleport steps and conditions that name the deleted room
+// are left in place and neutralized at runtime (teleportPlayer refuses a
+// missing destination, and a Condition.Room that no longer exists simply
+// never matches), so this only removes the references the validator would
+// otherwise report as dangling. Returns whether anything changed.
+func (r *Room) RemoveRoomRefs(removedID int) bool {
+ if r == nil || removedID <= 0 {
+ return false
+ }
+ changed := false
+ for i := range r.Mobs {
+ if removeRoomRef(&r.Mobs[i].WanderRooms, removedID) {
+ changed = true
+ }
+ }
+ for i := range r.Objects {
+ if removeRoomRef(&r.Objects[i].WanderRooms, removedID) {
+ changed = true
+ }
+ }
+ return changed
+}
+
+// removeRoomRef removes target from list in place and reports whether it was
+// present.
+func removeRoomRef(list *[]int, target int) bool {
+ src := *list
+ out := src[:0]
+ removed := false
+ for _, v := range src {
+ if v == target {
+ removed = true
+ continue
+ }
+ out = append(out, v)
+ }
+ *list = out
+ return removed
+}
+
// remapTriggers remaps step fields across a []behavior.Trigger (each trigger's
// Steps + per-step Condition + trigger-level Condition).
func remapTriggers(idMap map[int]int, list []behavior.Trigger) bool {