aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/rekey_objects_test.go28
-rw-r--r--internal/world/world.go55
2 files changed, 45 insertions, 38 deletions
diff --git a/internal/world/rekey_objects_test.go b/internal/world/rekey_objects_test.go
index 7ee2de3..908e30b 100644
--- a/internal/world/rekey_objects_test.go
+++ b/internal/world/rekey_objects_test.go
@@ -35,3 +35,31 @@ func TestRekeyRoomsUpdatesObjStateRoomID(t *testing.T) {
t.Fatalf("AllObjInstances(1) = %d entries, want 0 (stale old room)", len(got))
}
}
+
+// Regression: an object away from its home room is keyed by its current room,
+// so the old key-driven walk left its HomeRoomID/WanderRooms stale and a later
+// entry to the renamed home created a duplicate instance.
+func TestRekeyRoomsUpdatesAwayFromHomeObject(t *testing.T) {
+ w := New(t.TempDir())
+ // Declared in home room 1, currently wandering in room 5.
+ w.objStates[w.ObjStateKey(5, "fishing_spot", 0)] = &ObjState{
+ RoomID: 5,
+ HomeRoomID: 1,
+ DefID: "fishing_spot",
+ Index: 0,
+ WanderRooms: []int{1, 5},
+ }
+
+ w.RekeyRooms(map[int]int{1: 20})
+
+ inst := w.GetObjState(5, "fishing_spot", 0)
+ if inst == nil {
+ t.Fatal("away object state lost after rekey")
+ }
+ if inst.HomeRoomID != 20 {
+ t.Fatalf("HomeRoomID = %d, want 20", inst.HomeRoomID)
+ }
+ if got := inst.WanderRooms; len(got) != 2 || got[0] != 20 || got[1] != 5 {
+ t.Fatalf("WanderRooms = %v, want [20 5]", got)
+ }
+}
diff --git a/internal/world/world.go b/internal/world/world.go
index 10ceb08..c9e1559 100644
--- a/internal/world/world.go
+++ b/internal/world/world.go
@@ -6,7 +6,6 @@ import (
"math"
"os"
"path/filepath"
- "strconv"
"strings"
"sync"
@@ -252,46 +251,26 @@ func (w *World) RekeyRooms(idMap map[int]int) {
for newID := range movedSeeded {
w.seeded[newID] = true
}
- type objKeyMove struct {
- oldKey, newKey string
- st *ObjState
- newID int
- }
- var moves []objKeyMove
- for k, st := range w.objStates {
- roomStr, _, ok := strings.Cut(k, ":")
- if !ok {
- continue
- }
- roomID, err := strconv.Atoi(roomStr)
- if err != nil {
- continue
- }
- newID, ok := idMap[roomID]
- if !ok || newID == roomID {
- continue
+ // Rebuild the object-state map from every state, not only states whose
+ // current-room key is in idMap. An object away from its home room is keyed
+ // by the current room, so keying the walk on the map key leaves its
+ // HomeRoomID and WanderRooms stale; a later entry to the renamed home then
+ // fails homeObjStateLocked and creates a duplicate instance. Rebuilding the
+ // map also preserves the invariant mapKey == ObjStateKey(st.RoomID, ...).
+ rekeyed := make(map[string]*ObjState, len(w.objStates))
+ for _, st := range w.objStates {
+ if newID, ok := idMap[st.RoomID]; ok && newID != st.RoomID {
+ st.RoomID = newID
}
- newKey := fmt.Sprintf("%d:%s", newID, k[len(roomStr)+1:])
- moves = append(moves, objKeyMove{k, newKey, st, newID})
- }
- for _, m := range moves {
- delete(w.objStates, m.oldKey)
- }
- for _, m := range moves {
- // Keep the package invariant mapKey == ObjStateKey(st.RoomID, ...):
- // object lookups filter on st.RoomID while GetObjState looks up the
- // key, so a rekey that leaves RoomID stale makes the object vanish
- // from the renamed room (and TickObjWander then creates a second
- // entry for the same state). Remap wander rooms like MobStore does.
- m.st.RoomID = m.newID
- if mapped, ok := idMap[m.st.HomeRoomID]; ok && mapped != m.st.HomeRoomID {
- m.st.HomeRoomID = mapped
+ if newID, ok := idMap[st.HomeRoomID]; ok && newID != st.HomeRoomID {
+ st.HomeRoomID = newID
}
- for i, room := range m.st.WanderRooms {
- if mapped, ok := idMap[room]; ok && mapped != room {
- m.st.WanderRooms[i] = mapped
+ for i, room := range st.WanderRooms {
+ if newID, ok := idMap[room]; ok && newID != room {
+ st.WanderRooms[i] = newID
}
}
- w.objStates[m.newKey] = m.st
+ rekeyed[w.ObjStateKey(st.RoomID, st.DefID, st.Index)] = st
}
+ w.objStates = rekeyed
}