diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/grid_test.go | 2 | ||||
| -rw-r--r-- | internal/world/mob.go | 25 | ||||
| -rw-r--r-- | internal/world/mob_rekey_test.go | 23 | ||||
| -rw-r--r-- | internal/world/objects.go | 7 | ||||
| -rw-r--r-- | internal/world/room_migrate.go | 42 |
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 { |
