1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
|
package game
import (
"strconv"
"strings"
"thehouseoficarus/internal/player"
)
// rewritePlayerRoomIDs remaps room-ID-embedded state in a single player per
// idMap (oldID -> newID): discovered-exit player flag keys
// (hidden_exit_<id>_<dir>), current RoomID, PendingSequence.RoomID, MapSymbols
// keys, and Stats.RoomsVisited bitset. Returns whether any field was changed.
//
// idMap is assumed to be a bijection (see Room.RewriteRoomIDs); the flag-key
// and int-map rekeys use delete-all-then-set-all so a swap (A<->B) cannot
// clobber an entry mid-rewrite.
func rewritePlayerRoomIDs(p *player.Player, idMap map[int]int) bool {
if p == nil || len(idMap) == 0 {
return false
}
changed := false
if remapScalar(idMap, &p.RoomID) {
changed = true
}
if p.PendingSequence != nil && p.PendingSequence.RoomID != 0 && remapScalar(idMap, &p.PendingSequence.RoomID) {
changed = true
}
if len(p.Flags) > 0 && rewriteDiscoveredExitFlags(p.Flags, idMap) {
changed = true
}
if len(p.MapSymbols) > 0 && rekeyIntMap(p.MapSymbols, idMap) {
changed = true
}
next, moved := p.Stats.RoomsVisited.Rekey(idMap)
if moved {
p.Stats.RoomsVisited = next
changed = true
}
return changed
}
func remapScalar(idMap map[int]int, n *int) bool {
if newID, ok := idMap[*n]; ok && newID != *n {
*n = newID
return true
}
return false
}
// rewriteDiscoveredExitFlags rewrites hidden_exit_<roomID>_<dir> flag keys,
// remapping the embedded roomID per idMap. Flag values are preserved.
func rewriteDiscoveredExitFlags(flags map[string]any, idMap map[int]int) bool {
type move struct {
oldKey, newKey string
val any
}
var moves []move
for k, v := range flags {
if !strings.HasPrefix(k, hiddenExitFlagPrefix) {
continue
}
rest := k[len(hiddenExitFlagPrefix):] // "<roomID>_<dir>"
idx := strings.IndexByte(rest, '_')
if idx <= 0 {
continue
}
n, err := strconv.Atoi(rest[:idx])
if err != nil {
continue
}
newID, ok := idMap[n]
if !ok || newID == n {
continue
}
moves = append(moves, move{k, hiddenExitFlagPrefix + strconv.Itoa(newID) + "_" + rest[idx+1:], v})
}
if len(moves) == 0 {
return false
}
for _, m := range moves {
delete(flags, m.oldKey)
}
for _, m := range moves {
flags[m.newKey] = m.val
}
return true
}
// rekeyIntMap rekeys a map[int]V per idMap (oldID -> newID), preserving values.
// Uses delete-all-then-set-all so a swap cannot clobber an entry.
func rekeyIntMap[V any](m map[int]V, idMap map[int]int) bool {
type entry struct {
key int
val V
}
var saved []entry
for k, v := range m {
if newID, ok := idMap[k]; ok && newID != k {
saved = append(saved, entry{k, v})
}
}
if len(saved) == 0 {
return false
}
for _, e := range saved {
delete(m, e.key)
}
for _, e := range saved {
m[idMap[e.key]] = e.val
}
return true
}
// RewriteRoomIDs is the shared entry point called by every room-ID change
// path (the swapid command and the admin web GUI rename) to migrate room-ID-embedded
// state across all characters. Online players are updated in memory (and
// re-saved); offline characters are loaded, rewritten, and saved. Callers
// must be on the owner goroutine (use MigrateRoomIDs from other goroutines).
func (g *Game) RewriteRoomIDs(idMap map[int]int) {
if len(idMap) == 0 {
return
}
online := make(map[string]bool)
if g.Hub != nil {
for _, sess := range g.Hub.AllSessions() {
if sess.Player == nil {
continue
}
if rewritePlayerRoomIDs(sess.Player, idMap) {
g.AccountStore.SaveCharacter(sess.Player)
}
online[sess.Player.Name] = true
}
}
names, err := g.AccountStore.ListCharacterNames()
if err != nil {
return
}
for _, name := range names {
if online[name] {
continue
}
p, err := g.AccountStore.LoadCharacter(name)
if err != nil {
continue
}
if rewritePlayerRoomIDs(p, idMap) {
g.AccountStore.SaveCharacter(p)
}
}
}
// MigrateRoomIDs is the complete room-ID migration entry point for other
// goroutines (the admin API and swapid command): player state, Hub room
// buckets, casino room keys, world room-keyed runtime state, and live mob
// instances all move together under one owner task. This is what makes a
// live rename leave NO gameplay or broadcast attached to the old location.
func (g *Game) MigrateRoomIDs(idMap map[int]int) {
if len(idMap) == 0 {
return
}
g.RunOnOwner(func() {
g.migrateRoomIDsLocked(idMap)
})
}
// migrateRoomIDsLocked performs the full runtime migration; caller must
// already be on the owner goroutine.
func (g *Game) migrateRoomIDsLocked(idMap map[int]int) {
if g.Hub != nil {
g.Hub.RekeyRooms(idMap)
}
if g.Casino != nil {
g.Casino.RekeyRooms(idMap)
}
g.World.RekeyRooms(idMap)
g.MobStore.RekeyRooms(idMap)
// The file paths changed under the world: rebuild its room index so
// lookups agree with the post-migration layout.
g.World.RebuildRoomIndex(g.DataDir)
g.RewriteRoomIDs(idMap)
}
|