aboutsummaryrefslogtreecommitdiff
path: root/internal/game/cmd_swapid.go
blob: bbcc79c9c21ecd82a6cb2ab3655ee4e248401696 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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 <id>  or  swapid <id1> <id2>")
		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<id>\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_<id>_<dir> 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))
}