diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/rekey_objects_test.go | 28 | ||||
| -rw-r--r-- | internal/world/world.go | 55 |
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 } |
