diff options
Diffstat (limited to 'internal/world')
| -rw-r--r-- | internal/world/room_migrate.go | 71 | ||||
| -rw-r--r-- | internal/world/room_migrate_test.go | 111 |
2 files changed, 182 insertions, 0 deletions
diff --git a/internal/world/room_migrate.go b/internal/world/room_migrate.go new file mode 100644 index 0000000..6d79499 --- /dev/null +++ b/internal/world/room_migrate.go @@ -0,0 +1,71 @@ +package world + +// RewriteRoomIDs remaps every genuine room-ID reference in this room per idMap +// (oldID -> newID): exit targets, trigger Room/Teleport/DespawnRooms, on-enter +// Teleport/DespawnRooms, and mob WanderRooms. It does NOT touch author-chosen +// set_flags/set_player_flags values or condition `value:` fields — those are not +// structural room references (and historically the swapid regex rewrote them +// incorrectly as a side effect of matching any bare integer). +// +// idMap is assumed to be a bijection (each old ID maps to a distinct new ID); +// this holds for both swap (A<->B) and rename-to-unused (A->B). Returns whether +// any field was changed. +func (r *Room) RewriteRoomIDs(idMap map[int]int) bool { + if r == nil || len(idMap) == 0 { + return false + } + changed := false + for dir, exit := range r.Exits { + if remapInt(idMap, &exit.Room) { + r.Exits[dir] = exit + changed = true + } + } + for i := range r.Triggers { + if remapInt(idMap, &r.Triggers[i].Room) { + changed = true + } + for j := range r.Triggers[i].Steps { + s := &r.Triggers[i].Steps[j] + if remapInt(idMap, &s.Teleport) { + changed = true + } + if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) { + changed = true + } + } + } + for i := range r.OnEnter { + s := &r.OnEnter[i] + if remapInt(idMap, &s.Teleport) { + changed = true + } + if s.SpawnMob != nil && remapIntSlice(idMap, s.SpawnMob.DespawnRooms) { + changed = true + } + } + for i := range r.Mobs { + if remapIntSlice(idMap, r.Mobs[i].WanderRooms) { + changed = true + } + } + return changed +} + +func remapInt(idMap map[int]int, n *int) bool { + if newID, ok := idMap[*n]; ok && newID != *n { + *n = newID + return true + } + return false +} + +func remapIntSlice(idMap map[int]int, slice []int) bool { + changed := false + for i := range slice { + if remapInt(idMap, &slice[i]) { + changed = true + } + } + return changed +} diff --git a/internal/world/room_migrate_test.go b/internal/world/room_migrate_test.go new file mode 100644 index 0000000..b035172 --- /dev/null +++ b/internal/world/room_migrate_test.go @@ -0,0 +1,111 @@ +package world + +import "testing" + +func TestRoomRewriteRoomIDs(t *testing.T) { + r := &Room{ + Exits: map[ExitDir]ExitDef{ + North: {Room: 10, SetFlags: map[string]any{"last": 10}}, + South: {Room: 99}, + }, + Triggers: []TriggerDef{ + { + Room: 30, + Steps: []TriggerStep{ + {Teleport: 40, SpawnMob: &SpawnMobConfig{DespawnRooms: []int{50, 60}}}, + }, + }, + }, + OnEnter: []EnterStep{ + {Teleport: 70, SpawnMob: &SpawnMobConfig{DespawnRooms: []int{80}}}, + }, + Mobs: []RoomMob{ + {ID: "guard", WanderRooms: []int{90, 100}}, + }, + } + + idMap := map[int]int{ + 10: 11, 20: 21, 30: 31, 40: 41, 50: 51, 60: 61, + 70: 71, 80: 81, 90: 91, 100: 101, + } + if !r.RewriteRoomIDs(idMap) { + t.Fatal("expected changed=true") + } + + checkExit := func(dir ExitDir, want int) { + t.Helper() + if got := r.Exits[dir].Room; got != want { + t.Errorf("exit %s room = %d, want %d", dir, got, want) + } + } + checkExit(North, 11) + checkExit(South, 99) // unchanged (not in idMap) + + // Author set_flags values are NOT structural room references and must not + // be remapped (the old swapid regex wrongly rewrote these). + if v := r.Exits[North].SetFlags["last"]; v != 10 { + t.Errorf("set_flags value remapped: got %v, want 10", v) + } + + if r.Triggers[0].Room != 31 { + t.Errorf("trigger room = %d, want 31", r.Triggers[0].Room) + } + if r.Triggers[0].Steps[0].Teleport != 41 { + t.Errorf("trigger teleport = %d, want 41", r.Triggers[0].Steps[0].Teleport) + } + if got := r.Triggers[0].Steps[0].SpawnMob.DespawnRooms; len(got) != 2 || got[0] != 51 || got[1] != 61 { + t.Errorf("trigger despawn_rooms = %v, want [51 61]", got) + } + if r.OnEnter[0].Teleport != 71 { + t.Errorf("on_enter teleport = %d, want 71", r.OnEnter[0].Teleport) + } + if got := r.OnEnter[0].SpawnMob.DespawnRooms; len(got) != 1 || got[0] != 81 { + t.Errorf("on_enter despawn_rooms = %v, want [81]", got) + } + if got := r.Mobs[0].WanderRooms; len(got) != 2 || got[0] != 91 || got[1] != 101 { + t.Errorf("mob wander_rooms = %v, want [91 101]", got) + } +} + +func TestRoomRewriteRoomIDsNoChange(t *testing.T) { + r := &Room{ + Exits: map[ExitDir]ExitDef{North: {Room: 5}}, + } + // idMap does not mention 5 -> nothing changes. + if r.RewriteRoomIDs(map[int]int{99: 100}) { + t.Error("expected changed=false when no references match") + } + if r.Exits[North].Room != 5 { + t.Errorf("exit room = %d, want 5 (untouched)", r.Exits[North].Room) + } +} + +func TestRoomRewriteRoomIDsSwapRoundTrips(t *testing.T) { + r := &Room{Exits: map[ExitDir]ExitDef{ + North: {Room: 10}, + South: {Room: 20}, + }} + swap := map[int]int{10: 20, 20: 10} + r.RewriteRoomIDs(swap) + if r.Exits[North].Room != 20 || r.Exits[South].Room != 10 { + t.Errorf("after swap: north=%d south=%d, want north=20 south=10", r.Exits[North].Room, r.Exits[South].Room) + } + // Applying the same swap again reverts (a swap is its own inverse). + r.RewriteRoomIDs(swap) + if r.Exits[North].Room != 10 || r.Exits[South].Room != 20 { + t.Errorf("after double swap: north=%d south=%d, want north=10 south=20", r.Exits[North].Room, r.Exits[South].Room) + } +} + +func TestRoomRewriteRoomIDsIdempotentRename(t *testing.T) { + r := &Room{Exits: map[ExitDir]ExitDef{North: {Room: 10}}} + rename := map[int]int{10: 11} + r.RewriteRoomIDs(rename) + if r.Exits[North].Room != 11 { + t.Fatalf("after rename: room=%d, want 11", r.Exits[North].Room) + } + // Second pass: 11 is not a key in idMap, so nothing changes. + if r.RewriteRoomIDs(rename) { + t.Error("expected changed=false on second pass of a rename idMap") + } +} |
