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/admin/api_room_courses.go | 4 +-- internal/admin/api_rooms.go | 50 ++++++++++++++++++++-------------- internal/admin/server.go | 56 +++++++++++++++++++++----------------- internal/admin/static/map.js | 2 +- 4 files changed, 63 insertions(+), 49 deletions(-) (limited to 'internal/admin') diff --git a/internal/admin/api_room_courses.go b/internal/admin/api_room_courses.go index 86c7cdf..740a102 100644 --- a/internal/admin/api_room_courses.go +++ b/internal/admin/api_room_courses.go @@ -574,8 +574,8 @@ func roomExitsMap(m map[string]any) map[string]any { } // setCourseBlockedExit adds or replaces an always-blocked exit in direction dir -// targeting targetRoomID. Always-blocked exits are impassable but render on the map; -// used to lay out course rooms in visual sequence. +// targeting targetRoomID. Always-blocked exits are blocked to players but +// render on the map; used to lay out course rooms in visual sequence. func setCourseBlockedExit(m map[string]any, dir string, targetRoomID int) { ex := roomExitsMap(m) ex[dir] = map[string]any{"room": targetRoomID, "always_blocked": true} diff --git a/internal/admin/api_rooms.go b/internal/admin/api_rooms.go index 75f4779..124b093 100644 --- a/internal/admin/api_rooms.go +++ b/internal/admin/api_rooms.go @@ -821,37 +821,40 @@ func (s *AdminServer) handleRoomRename(w http.ResponseWriter, r *http.Request) { s.world.RebuildRoomIndex(s.dataDir) s.world.ClearRoomState(body.ID) + // Remap every genuine room-ID reference (exit targets, trigger/on-enter + // room+teleport+despawn_rooms, mob wander_rooms) from the old ID to the + // new one across all rooms — including the renamed room itself, so a + // self-loop exit is corrected too. Only rooms whose references actually + // moved are re-marshaled (preserving untouched files' formatting). + idMap := map[int]int{body.ID: body.NewID} allIDs, _ := listRoomIDs(s.dataDir) - var exitUpdates int + var refUpdates int for _, otherID := range allIDs { - if otherID == body.NewID { - continue - } otherRoom, loadErr := s.world.LoadRoom(otherID) if loadErr != nil { continue } - changed := false - for _, dir := range world.ExitOrder { - exit, exists := otherRoom.Exits[dir] - if !exists || exit.Room != body.ID { - continue - } - exit.Room = body.NewID - otherRoom.Exits[dir] = exit - changed = true - } - if changed { - otherPath, pathOk := s.world.GetRoomPath(otherID) - if pathOk { - writeYAMLFile(otherPath, otherRoom) - exitUpdates++ - } + if !otherRoom.RewriteRoomIDs(idMap) { + continue + } + otherPath, pathOk := s.world.GetRoomPath(otherID) + if !pathOk { + continue } + writeYAMLFile(otherPath, otherRoom) + refUpdates++ + } + + // Migrate room-ID-embedded state across all characters (discovered-exit + // player flags, RoomID, EnterSeqRoom, MapSymbols, RoomsVisited) so a + // rename does not strand players or leave stale hidden_exit__ + // flags pointing at the old room ID. + if s.rewriteRoomIDs != nil { + s.rewriteRoomIDs(idMap) } s.undoStack.Push(ChangeDesc{ - Description: fmt.Sprintf("rename room %d to %d (updated %d exits)", body.ID, body.NewID, exitUpdates), + Description: fmt.Sprintf("rename room %d to %d (updated %d refs)", body.ID, body.NewID, refUpdates), FilePath: oldPath, NewFilePath: newPath, OldContent: data, @@ -941,6 +944,11 @@ func (s *AdminServer) handleResolveDuplicate(w http.ResponseWriter, r *http.Requ writeJSON(w, map[string]any{"error": "yaml parse failed: " + err.Error()}) return } + // A duplicate file was never in the room index (only one of the pair + // is indexed), so no player has visited it and no other room points at + // it. Renaming it to an unused ID therefore needs no exit/trigger/wander + // reference rewrite and no player-state migration — only its own id + // field and filename change. m["id"] = body.NewID newContent, err := writeMapAsYAML(newRel, m) diff --git a/internal/admin/server.go b/internal/admin/server.go index f0fa58e..6ad20f5 100644 --- a/internal/admin/server.go +++ b/internal/admin/server.go @@ -36,22 +36,27 @@ import ( var embedded embed.FS type AdminServer struct { - cfg *config.Config - useTLS bool - accountStore *player.AccountStore - world *world.World - itemStore *item.ItemStore - objectStore *object.ObjectStore - mobStore *world.MobStore - dataDir string - httpServer *http.Server - undoStack *UndoStack - tmpl *template.Template - cookieSecret []byte + cfg *config.Config + useTLS bool + accountStore *player.AccountStore + world *world.World + itemStore *item.ItemStore + objectStore *object.ObjectStore + mobStore *world.MobStore + dataDir string + httpServer *http.Server + undoStack *UndoStack + tmpl *template.Template + cookieSecret []byte reloadCourses func() + // rewriteRoomIDs migrates room-ID-embedded state across all characters + // (online + offline) when a room ID changes. Wired from the Game so the + // admin web GUI can trigger the same migration as the swapid command + // without the admin package depending on game. + rewriteRoomIDs func(idMap map[int]int) } -func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string, reloadCourses func()) (*AdminServer, error) { +func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStore, w *world.World, is *item.ItemStore, os *object.ObjectStore, ms *world.MobStore, dataDir string, reloadCourses func(), rewriteRoomIDs func(map[int]int)) (*AdminServer, error) { secret := make([]byte, 32) if _, err := rand.Read(secret); err != nil { return nil, fmt.Errorf("cookie secret: %w", err) @@ -83,18 +88,19 @@ func NewServer(cfg *config.Config, useTLS bool, accountStore *player.AccountStor } s := &AdminServer{ - cfg: cfg, - useTLS: useTLS, - accountStore: accountStore, - world: w, - itemStore: is, - objectStore: os, - mobStore: ms, - dataDir: dataDir, - undoStack: NewUndoStack(dataDir), - tmpl: tmpl, - cookieSecret: secret, - reloadCourses: reloadCourses, + cfg: cfg, + useTLS: useTLS, + accountStore: accountStore, + world: w, + itemStore: is, + objectStore: os, + mobStore: ms, + dataDir: dataDir, + undoStack: NewUndoStack(dataDir), + tmpl: tmpl, + cookieSecret: secret, + reloadCourses: reloadCourses, + rewriteRoomIDs: rewriteRoomIDs, } mux := http.NewServeMux() diff --git a/internal/admin/static/map.js b/internal/admin/static/map.js index 8080451..3fdd543 100644 --- a/internal/admin/static/map.js +++ b/internal/admin/static/map.js @@ -1296,7 +1296,7 @@ function editExitCondition(dir) { h += '
'; h += '
'; h += '
'; - h += '
'; + h += '
'; h += '
'; h += ''; -- cgit v1.2.3