package game import ( "fmt" "os" "strconv" "gopkg.in/yaml.v3" "thehouseoficarus/internal/net" ) func (g *Game) executeSwapID(sess *net.Session, args []string, rawInput string) { if !g.checkAdmin(sess) { sess.WriteLine("Unknown command.") return } p := sess.Player if p == nil { return } var id1, id2 int switch len(args) { case 1: id1 = p.RoomID var err error id2, err = strconv.Atoi(args[0]) if err != nil { sess.WriteLine("Invalid room ID.") return } case 2: var err error id1, err = strconv.Atoi(args[0]) if err != nil { sess.WriteLine("Invalid room ID.") return } id2, err = strconv.Atoi(args[1]) if err != nil { sess.WriteLine("Invalid room ID.") return } default: sess.WriteLine("Usage: swapid or swapid ") return } if id1 == id2 { sess.WriteLine("Cannot swap a room with itself.") return } path1, ok1 := g.World.GetRoomPath(id1) path2, ok2 := g.World.GetRoomPath(id2) if !ok1 { sess.WriteLine(fmt.Sprintf("Room %d not found.", id1)) return } if !ok2 { sess.WriteLine(fmt.Sprintf("Room %d not found.", id2)) return } // Refuse while non-admin players are online: swapping room IDs relocates // every character whose state embeds those IDs, which is disruptive to // normal play. Admins may stay online (their own state is migrated too). for _, other := range g.Hub.AllSessions() { if other == sess || other.Player == nil { continue } if !g.checkAdmin(other) { sess.WriteLine("Cannot swap room IDs while non-admin players are online.") return } } // Swap the two files first. After this, path1 holds the old room id2's // content and path2 holds the old room id1's content. Rebuilding the index // then makes LoadRoom(id1)/LoadRoom(id2) return the swapped content, so the // reference rewrite below operates on the correctly-positioned rooms and // re-marshaling writes an `id:` field matching the new filename (keeping // the file's id field consistent with its name, as the old regex did). tempPath := path1 + ".swapid-tmp" if rerr := os.Rename(path1, tempPath); rerr != nil { sess.WriteLine(fmt.Sprintf("Error renaming room %d: %v", id1, rerr)) return } if rerr := os.Rename(path2, path1); rerr != nil { os.Rename(tempPath, path1) sess.WriteLine(fmt.Sprintf("Error renaming room %d: %v", id2, rerr)) return } if rerr := os.Rename(tempPath, path2); rerr != nil { os.Rename(path2, path1) os.Rename(tempPath, path1) sess.WriteLine(fmt.Sprintf("Error finishing rename: %v", rerr)) return } g.World.RebuildRoomIndex(g.DataDir) // Remap every genuine room-ID reference (exit targets, trigger/on-enter // room+teleport+despawn_rooms, mob wander_rooms) across all rooms. This // replaces the old \b\b regex, which also clobbered unrelated bare // integers (delays, quantities, color indices) and author flag values. // The two swapped rooms are always re-marshaled to correct their `id:` // field; other rooms are re-marshaled only if a reference actually moved. idMap := map[int]int{id1: id2, id2: id1} var updated int for id := range g.World.RoomIndex() { room, err := g.World.LoadRoom(id) if err != nil { continue } changed := room.RewriteRoomIDs(idMap) if !changed && id != id1 && id != id2 { continue } path, ok := g.World.GetRoomPath(id) if !ok { continue } data, merr := yaml.Marshal(room) if merr != nil { continue } if werr := os.WriteFile(path, data, 0644); werr != nil { continue } updated++ } // Migrate room-ID-embedded state across all characters (online + offline): // discovered-exit player flags, current RoomID, EnterSeqRoom, MapSymbols, // and RoomsVisited. This is what the player asked for — without it, // swapid leaves every character's hidden_exit__ flags (and more) // pointing at stale room IDs. g.RewriteRoomIDs(idMap) sess.WriteLine(fmt.Sprintf("Swapped room IDs %d and %d (%d files updated).", id1, id2, updated)) }