aboutsummaryrefslogtreecommitdiff
path: root/internal/world
diff options
context:
space:
mode:
Diffstat (limited to 'internal/world')
-rw-r--r--internal/world/room_migrate.go71
-rw-r--r--internal/world/room_migrate_test.go111
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")
+ }
+}