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__), current RoomID, EnterSeqRoom, 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.EnterSeqRoom != 0 && remapScalar(idMap, &p.EnterSeqRoom) { 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__ 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):] // "_" 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. It follows // the same cross-goroutine mutation precedent as cmd_undig/cmd_summon. 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) } } }