From a51f7a53aa2c487ebf94ba0363038574ae8bb590 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Tue, 7 Jul 2026 00:22:32 -0400 Subject: feat: hidden exits (and related flags) work with commands that change room ids. exit code simplified and unified between impassable and blocked exits. --- internal/game/exit_migrate.go | 150 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 internal/game/exit_migrate.go (limited to 'internal/game/exit_migrate.go') diff --git a/internal/game/exit_migrate.go b/internal/game/exit_migrate.go new file mode 100644 index 0000000..845a6b2 --- /dev/null +++ b/internal/game/exit_migrate.go @@ -0,0 +1,150 @@ +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 keys. 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 + } + if len(p.Stats.RoomsVisited) > 0 && rekeyIntMap(p.Stats.RoomsVisited, idMap) { + 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) + } + } +} -- cgit v1.2.3