@rpgjs/server 5.0.0-alpha.9 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Gui/DialogGui.d.ts +5 -0
- package/dist/Gui/GameoverGui.d.ts +23 -0
- package/dist/Gui/Gui.d.ts +6 -0
- package/dist/Gui/MenuGui.d.ts +22 -3
- package/dist/Gui/NotificationGui.d.ts +1 -2
- package/dist/Gui/SaveLoadGui.d.ts +13 -0
- package/dist/Gui/ShopGui.d.ts +28 -3
- package/dist/Gui/TitleGui.d.ts +23 -0
- package/dist/Gui/index.d.ts +10 -1
- package/dist/Player/BattleManager.d.ts +44 -32
- package/dist/Player/ClassManager.d.ts +24 -4
- package/dist/Player/ComponentManager.d.ts +95 -32
- package/dist/Player/Components.d.ts +345 -0
- package/dist/Player/EffectManager.d.ts +50 -4
- package/dist/Player/ElementManager.d.ts +77 -4
- package/dist/Player/GoldManager.d.ts +1 -1
- package/dist/Player/GuiManager.d.ts +87 -4
- package/dist/Player/ItemFixture.d.ts +1 -1
- package/dist/Player/ItemManager.d.ts +431 -4
- package/dist/Player/MoveManager.d.ts +301 -34
- package/dist/Player/ParameterManager.d.ts +364 -28
- package/dist/Player/Player.d.ts +558 -14
- package/dist/Player/SkillManager.d.ts +187 -13
- package/dist/Player/StateManager.d.ts +75 -4
- package/dist/Player/VariableManager.d.ts +62 -4
- package/dist/RpgServer.d.ts +278 -63
- package/dist/RpgServerEngine.d.ts +2 -1
- package/dist/decorators/event.d.ts +46 -0
- package/dist/decorators/map.d.ts +299 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +17920 -29711
- package/dist/index.js.map +1 -1
- package/dist/logs/log.d.ts +2 -3
- package/dist/module-CaCW1SDh.js +11018 -0
- package/dist/module-CaCW1SDh.js.map +1 -0
- package/dist/module.d.ts +43 -1
- package/dist/node/connection.d.ts +51 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +551 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/map.d.ts +16 -0
- package/dist/node/room.d.ts +21 -0
- package/dist/node/transport.d.ts +28 -0
- package/dist/node/types.d.ts +47 -0
- package/dist/presets/index.d.ts +0 -9
- package/dist/rooms/BaseRoom.d.ts +132 -0
- package/dist/rooms/lobby.d.ts +10 -2
- package/dist/rooms/map.d.ts +1359 -32
- package/dist/services/save.d.ts +43 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/localStorage.d.ts +23 -0
- package/package.json +25 -10
- package/src/Gui/DialogGui.ts +19 -4
- package/src/Gui/GameoverGui.ts +39 -0
- package/src/Gui/Gui.ts +23 -1
- package/src/Gui/MenuGui.ts +155 -6
- package/src/Gui/NotificationGui.ts +1 -2
- package/src/Gui/SaveLoadGui.ts +60 -0
- package/src/Gui/ShopGui.ts +146 -16
- package/src/Gui/TitleGui.ts +39 -0
- package/src/Gui/index.ts +15 -2
- package/src/Player/BattleManager.ts +39 -56
- package/src/Player/ClassManager.ts +82 -74
- package/src/Player/ComponentManager.ts +394 -32
- package/src/Player/Components.ts +380 -0
- package/src/Player/EffectManager.ts +50 -96
- package/src/Player/ElementManager.ts +74 -152
- package/src/Player/GuiManager.ts +125 -14
- package/src/Player/ItemManager.ts +747 -341
- package/src/Player/MoveManager.ts +1532 -750
- package/src/Player/ParameterManager.ts +636 -106
- package/src/Player/Player.ts +1273 -79
- package/src/Player/SkillManager.ts +558 -197
- package/src/Player/StateManager.ts +131 -258
- package/src/Player/VariableManager.ts +85 -157
- package/src/RpgServer.ts +293 -62
- package/src/decorators/event.ts +61 -0
- package/src/decorators/map.ts +343 -0
- package/src/index.ts +11 -1
- package/src/logs/log.ts +10 -3
- package/src/module.ts +126 -3
- package/src/node/connection.ts +254 -0
- package/src/node/index.ts +22 -0
- package/src/node/map.ts +328 -0
- package/src/node/room.ts +63 -0
- package/src/node/transport.ts +532 -0
- package/src/node/types.ts +61 -0
- package/src/presets/index.ts +1 -10
- package/src/rooms/BaseRoom.ts +232 -0
- package/src/rooms/lobby.ts +25 -7
- package/src/rooms/map.ts +2682 -206
- package/src/services/save.ts +147 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/localStorage.ts +76 -0
- package/tests/battle.spec.ts +375 -0
- package/tests/change-map.spec.ts +72 -0
- package/tests/class.spec.ts +274 -0
- package/tests/custom-websocket.spec.ts +127 -0
- package/tests/effect.spec.ts +219 -0
- package/tests/element.spec.ts +221 -0
- package/tests/event.spec.ts +80 -0
- package/tests/gold.spec.ts +99 -0
- package/tests/item.spec.ts +609 -0
- package/tests/module.spec.ts +38 -0
- package/tests/move.spec.ts +601 -0
- package/tests/node-transport.spec.ts +223 -0
- package/tests/player-param.spec.ts +45 -0
- package/tests/prediction-reconciliation.spec.ts +182 -0
- package/tests/random-move.spec.ts +65 -0
- package/tests/skill.spec.ts +658 -0
- package/tests/state.spec.ts +467 -0
- package/tests/variable.spec.ts +185 -0
- package/tests/world-maps.spec.ts +896 -0
- package/vite.config.ts +36 -3
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { RpgWebSocketConnection } from "./types";
|
|
2
|
+
|
|
3
|
+
type RuntimeProcess = {
|
|
4
|
+
env?: Record<string, string | undefined>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function readEnvVariable(name: string): string | undefined {
|
|
8
|
+
const value = (globalThis as { process?: RuntimeProcess }).process?.env?.[name];
|
|
9
|
+
return typeof value === "string" ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class PartyConnection {
|
|
13
|
+
public id: string;
|
|
14
|
+
public uri: string;
|
|
15
|
+
private _state: any = {};
|
|
16
|
+
private messageQueue: Array<{ message: string; timestamp: number; sequence: number }> = [];
|
|
17
|
+
private isProcessingQueue = false;
|
|
18
|
+
private sequenceCounter = 0;
|
|
19
|
+
private incomingQueue: Array<{
|
|
20
|
+
message: string;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
processor: (messages: string[]) => Promise<void>;
|
|
23
|
+
}> = [];
|
|
24
|
+
private isProcessingIncomingQueue = false;
|
|
25
|
+
|
|
26
|
+
public static packetLossRate = parseFloat(readEnvVariable("RPGJS_PACKET_LOSS_RATE") || "0.1");
|
|
27
|
+
public static packetLossEnabled = readEnvVariable("RPGJS_ENABLE_PACKET_LOSS") === "true";
|
|
28
|
+
public static packetLossFilter = readEnvVariable("RPGJS_PACKET_LOSS_FILTER") || "";
|
|
29
|
+
public static bandwidthEnabled = readEnvVariable("RPGJS_ENABLE_BANDWIDTH") === "true";
|
|
30
|
+
public static bandwidthKbps = parseInt(readEnvVariable("RPGJS_BANDWIDTH_KBPS") || "100");
|
|
31
|
+
public static bandwidthFilter = readEnvVariable("RPGJS_BANDWIDTH_FILTER") || "";
|
|
32
|
+
public static latencyEnabled = readEnvVariable("RPGJS_ENABLE_LATENCY") === "true";
|
|
33
|
+
public static latencyMs = parseInt(readEnvVariable("RPGJS_LATENCY_MS") || "50");
|
|
34
|
+
public static latencyFilter = readEnvVariable("RPGJS_LATENCY_FILTER") || "";
|
|
35
|
+
|
|
36
|
+
constructor(private ws: RpgWebSocketConnection, id?: string, uri?: string) {
|
|
37
|
+
this.id = id || this.generateId();
|
|
38
|
+
this.uri = uri || "";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private generateId(): string {
|
|
42
|
+
return `conn_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async send(data: any): Promise<void> {
|
|
46
|
+
if (this.ws.readyState !== 1) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const message = typeof data === "string" ? data : JSON.stringify(data);
|
|
51
|
+
const timestamp = Date.now();
|
|
52
|
+
const sequence = ++this.sequenceCounter;
|
|
53
|
+
|
|
54
|
+
this.messageQueue.push({ message, timestamp, sequence });
|
|
55
|
+
|
|
56
|
+
if (!this.isProcessingQueue) {
|
|
57
|
+
void this.processMessageQueue();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private async processMessageQueue(): Promise<void> {
|
|
62
|
+
if (this.isProcessingQueue) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.isProcessingQueue = true;
|
|
66
|
+
|
|
67
|
+
while (this.messageQueue.length > 0) {
|
|
68
|
+
const queueItem = this.messageQueue.shift()!;
|
|
69
|
+
|
|
70
|
+
if (this.shouldApplyLatency(queueItem.message)) {
|
|
71
|
+
await this.waitUntil(queueItem.timestamp + PartyConnection.latencyMs);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (PartyConnection.bandwidthEnabled && PartyConnection.bandwidthKbps > 0) {
|
|
75
|
+
if (!PartyConnection.bandwidthFilter || queueItem.message.includes(PartyConnection.bandwidthFilter)) {
|
|
76
|
+
const messageSizeBits = queueItem.message.length * 8;
|
|
77
|
+
const transmissionTimeMs = (messageSizeBits / (PartyConnection.bandwidthKbps * 1000)) * 1000;
|
|
78
|
+
const bandwidthDelayMs = Math.max(transmissionTimeMs, 10);
|
|
79
|
+
console.log(
|
|
80
|
+
`\x1b[34m[BANDWIDTH SIMULATION]\x1b[0m Connection ${this.id}: Message #${queueItem.sequence} transmission time: ${bandwidthDelayMs.toFixed(1)}ms`,
|
|
81
|
+
);
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, bandwidthDelayMs));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.ws.send(queueItem.message);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.isProcessingQueue = false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private shouldApplyLatency(message: string): boolean {
|
|
93
|
+
if (!PartyConnection.latencyEnabled || PartyConnection.latencyMs <= 0) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
if (!PartyConnection.latencyFilter) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return message.includes(PartyConnection.latencyFilter);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private async waitUntil(targetTimestamp: number): Promise<void> {
|
|
103
|
+
const delayMs = targetTimestamp - Date.now();
|
|
104
|
+
if (delayMs <= 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
close(): void {
|
|
111
|
+
if (this.ws.readyState === 1) {
|
|
112
|
+
this.ws.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setState(value: any): void {
|
|
117
|
+
this._state = value;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
get state(): any {
|
|
121
|
+
return this._state;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
bufferIncoming(message: string, processor: (messages: string[]) => Promise<void>): void {
|
|
125
|
+
this.incomingQueue.push({
|
|
126
|
+
message,
|
|
127
|
+
timestamp: Date.now(),
|
|
128
|
+
processor,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!this.isProcessingIncomingQueue) {
|
|
132
|
+
void this.processIncomingQueue();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async processIncomingQueue(): Promise<void> {
|
|
137
|
+
if (this.isProcessingIncomingQueue) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.isProcessingIncomingQueue = true;
|
|
141
|
+
|
|
142
|
+
while (this.incomingQueue.length > 0) {
|
|
143
|
+
const item = this.incomingQueue.shift()!;
|
|
144
|
+
if (this.shouldApplyLatency(item.message)) {
|
|
145
|
+
await this.waitUntil(item.timestamp + PartyConnection.latencyMs);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
await item.processor([item.message]);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
console.error("Error processing incoming message:", err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
this.isProcessingIncomingQueue = false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static configurePacketLoss(enabled: boolean, rate: number, filter?: string): void {
|
|
158
|
+
PartyConnection.packetLossEnabled = enabled;
|
|
159
|
+
PartyConnection.packetLossRate = Math.max(0, Math.min(1, rate));
|
|
160
|
+
PartyConnection.packetLossFilter = filter || "";
|
|
161
|
+
|
|
162
|
+
if (enabled && rate > 0) {
|
|
163
|
+
const filterInfo = filter ? ` (filtered: "${filter}")` : "";
|
|
164
|
+
console.log(`\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled with ${(rate * 100).toFixed(1)}% loss rate${filterInfo}`);
|
|
165
|
+
} else if (enabled) {
|
|
166
|
+
console.log("\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Enabled but rate is 0% (no messages will be dropped)");
|
|
167
|
+
} else {
|
|
168
|
+
console.log("\x1b[35m[PACKET LOSS SIMULATION]\x1b[0m Disabled");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static getPacketLossStatus(): { enabled: boolean; rate: number; filter: string } {
|
|
173
|
+
return {
|
|
174
|
+
enabled: PartyConnection.packetLossEnabled,
|
|
175
|
+
rate: PartyConnection.packetLossRate,
|
|
176
|
+
filter: PartyConnection.packetLossFilter,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
static configureBandwidth(enabled: boolean, kbps: number, filter?: string): void {
|
|
181
|
+
PartyConnection.bandwidthEnabled = enabled;
|
|
182
|
+
PartyConnection.bandwidthKbps = Math.max(1, kbps);
|
|
183
|
+
PartyConnection.bandwidthFilter = filter || "";
|
|
184
|
+
|
|
185
|
+
if (enabled && kbps > 0) {
|
|
186
|
+
const filterInfo = filter ? ` (filtered: "${filter}")` : "";
|
|
187
|
+
console.log(`\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled with ${kbps} kbps bandwidth${filterInfo}`);
|
|
188
|
+
} else if (enabled) {
|
|
189
|
+
console.log("\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Enabled but bandwidth is 0 kbps (no delay will be applied)");
|
|
190
|
+
} else {
|
|
191
|
+
console.log("\x1b[35m[BANDWIDTH SIMULATION]\x1b[0m Disabled");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
static getBandwidthStatus(): { enabled: boolean; kbps: number; filter: string } {
|
|
196
|
+
return {
|
|
197
|
+
enabled: PartyConnection.bandwidthEnabled,
|
|
198
|
+
kbps: PartyConnection.bandwidthKbps,
|
|
199
|
+
filter: PartyConnection.bandwidthFilter,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
static configureLatency(enabled: boolean, ms: number, filter?: string): void {
|
|
204
|
+
PartyConnection.latencyEnabled = enabled;
|
|
205
|
+
PartyConnection.latencyMs = Math.max(0, ms);
|
|
206
|
+
PartyConnection.latencyFilter = filter || "";
|
|
207
|
+
|
|
208
|
+
if (enabled && ms > 0) {
|
|
209
|
+
const filterInfo = filter ? ` (filtered: "${filter}")` : "";
|
|
210
|
+
console.log(`\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled with ${ms}ms fixed latency${filterInfo}`);
|
|
211
|
+
} else if (enabled) {
|
|
212
|
+
console.log("\x1b[35m[LATENCY SIMULATION]\x1b[0m Enabled but latency is 0ms (no delay will be applied)");
|
|
213
|
+
} else {
|
|
214
|
+
console.log("\x1b[35m[LATENCY SIMULATION]\x1b[0m Disabled");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static getLatencyStatus(): { enabled: boolean; ms: number; filter: string } {
|
|
219
|
+
return {
|
|
220
|
+
enabled: PartyConnection.latencyEnabled,
|
|
221
|
+
ms: PartyConnection.latencyMs,
|
|
222
|
+
filter: PartyConnection.latencyFilter,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function logNetworkSimulationStatus(): void {
|
|
228
|
+
const packetLossStatus = PartyConnection.getPacketLossStatus();
|
|
229
|
+
const bandwidthStatus = PartyConnection.getBandwidthStatus();
|
|
230
|
+
const latencyStatus = PartyConnection.getLatencyStatus();
|
|
231
|
+
|
|
232
|
+
if (packetLossStatus.enabled) {
|
|
233
|
+
const filterInfo = packetLossStatus.filter ? ` (filter: "${packetLossStatus.filter}")` : "";
|
|
234
|
+
console.log(
|
|
235
|
+
`\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: ${(packetLossStatus.rate * 100).toFixed(1)}% loss rate${filterInfo}`,
|
|
236
|
+
);
|
|
237
|
+
} else {
|
|
238
|
+
console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Packet loss simulation: disabled");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (bandwidthStatus.enabled) {
|
|
242
|
+
const filterInfo = bandwidthStatus.filter ? ` (filter: "${bandwidthStatus.filter}")` : "";
|
|
243
|
+
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: ${bandwidthStatus.kbps} kbps${filterInfo}`);
|
|
244
|
+
} else {
|
|
245
|
+
console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Bandwidth simulation: disabled");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (latencyStatus.enabled) {
|
|
249
|
+
const filterInfo = latencyStatus.filter ? ` (filter: "${latencyStatus.filter}")` : "";
|
|
250
|
+
console.log(`\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: ${latencyStatus.ms}ms ping${filterInfo}`);
|
|
251
|
+
} else {
|
|
252
|
+
console.log("\x1b[36m[NETWORK SIMULATION]\x1b[0m Latency simulation: disabled");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { PartyConnection, logNetworkSimulationStatus } from "./connection";
|
|
2
|
+
export {
|
|
3
|
+
createMapUpdateHeaders,
|
|
4
|
+
isMapUpdateAuthorized,
|
|
5
|
+
MAP_UPDATE_TOKEN_ENV,
|
|
6
|
+
MAP_UPDATE_TOKEN_HEADER,
|
|
7
|
+
readMapUpdateToken,
|
|
8
|
+
resolveMapUpdateToken,
|
|
9
|
+
} from "./map";
|
|
10
|
+
export { PartyRoom } from "./room";
|
|
11
|
+
export { createRpgServerTransport, RpgServerTransport } from "./transport";
|
|
12
|
+
export type {
|
|
13
|
+
CreateRpgServerTransportOptions,
|
|
14
|
+
HandleNodeRequestOptions,
|
|
15
|
+
RpgTransportRequestLike,
|
|
16
|
+
RpgTransportServer,
|
|
17
|
+
RpgTransportServerConstructor,
|
|
18
|
+
RpgWebSocketConnection,
|
|
19
|
+
RpgWebSocketRequestLike,
|
|
20
|
+
RpgWebSocketServer,
|
|
21
|
+
SendMapUpdateOptions,
|
|
22
|
+
} from "./types";
|
package/src/node/map.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { RpgTransportServer } from "./types";
|
|
2
|
+
|
|
3
|
+
interface ResolveMapOptions {
|
|
4
|
+
host?: string;
|
|
5
|
+
headers?: Headers;
|
|
6
|
+
mapUpdateToken?: string;
|
|
7
|
+
tiledBasePaths?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const MAP_UPDATE_TOKEN_HEADER = "x-rpgjs-map-update-token";
|
|
11
|
+
export const MAP_UPDATE_TOKEN_ENV = "RPGJS_MAP_UPDATE_TOKEN";
|
|
12
|
+
|
|
13
|
+
type RuntimeProcess = {
|
|
14
|
+
cwd?: () => string;
|
|
15
|
+
env?: Record<string, string | undefined>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function getRuntimeProcess(): RuntimeProcess | undefined {
|
|
19
|
+
return (globalThis as { process?: RuntimeProcess }).process;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readEnvVariable(name: string): string | undefined {
|
|
23
|
+
const value = getRuntimeProcess()?.env?.[name];
|
|
24
|
+
return typeof value === "string" ? value : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getWorkingDirectory(): string | undefined {
|
|
28
|
+
const cwd = getRuntimeProcess()?.cwd;
|
|
29
|
+
if (typeof cwd !== "function") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
return cwd();
|
|
35
|
+
} catch {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function normalizeRoomMapId(roomId: string): string {
|
|
41
|
+
return roomId.startsWith("map-") ? roomId.slice(4) : roomId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function toBasePathPrefix(basePath: string): string {
|
|
45
|
+
const trimmed = basePath.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractFileLikeMapDefinition(maps: any[], mapId: string): any | null {
|
|
53
|
+
for (const mapDef of maps) {
|
|
54
|
+
if (typeof mapDef === "object" && mapDef) {
|
|
55
|
+
const candidateId = typeof mapDef.id === "string" ? mapDef.id.replace(/^map-/, "") : "";
|
|
56
|
+
if (candidateId === mapId) {
|
|
57
|
+
return mapDef;
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof mapDef === "string") {
|
|
63
|
+
const fileName = mapDef.split("/").pop()?.replace(/\.tmx$/i, "");
|
|
64
|
+
if (fileName === mapId) {
|
|
65
|
+
return { id: mapId, file: mapDef };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function fetchTextByUrl(url: string): Promise<string | null> {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch(url);
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return await response.text();
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function readTextByFilePath(pathLike: string): Promise<string | null> {
|
|
86
|
+
try {
|
|
87
|
+
const { readFile } = await import("node:fs/promises");
|
|
88
|
+
const { isAbsolute, join } = await import("node:path");
|
|
89
|
+
|
|
90
|
+
const cwd = getWorkingDirectory();
|
|
91
|
+
const candidates = isAbsolute(pathLike) || !cwd ? [pathLike] : [pathLike, join(cwd, pathLike)];
|
|
92
|
+
|
|
93
|
+
for (const candidate of candidates) {
|
|
94
|
+
try {
|
|
95
|
+
return await readFile(candidate, "utf8");
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getTiledBasePaths(paths?: string[]): string[] {
|
|
108
|
+
const values = [
|
|
109
|
+
...(paths || []),
|
|
110
|
+
readEnvVariable("RPGJS_TILED_BASE_PATH"),
|
|
111
|
+
"map",
|
|
112
|
+
"data",
|
|
113
|
+
"assets/data",
|
|
114
|
+
"assets/map",
|
|
115
|
+
].filter((value): value is string => !!value);
|
|
116
|
+
|
|
117
|
+
return Array.from(new Set(values));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function resolveMapUpdateToken(explicitToken?: string): string {
|
|
121
|
+
return explicitToken ?? readEnvVariable(MAP_UPDATE_TOKEN_ENV) ?? "";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createMapUpdateHeaders(
|
|
125
|
+
token?: string,
|
|
126
|
+
init?: HeadersInit,
|
|
127
|
+
): Headers {
|
|
128
|
+
const headers = new Headers(init);
|
|
129
|
+
if (!headers.has("content-type")) {
|
|
130
|
+
headers.set("content-type", "application/json");
|
|
131
|
+
}
|
|
132
|
+
const resolvedToken = resolveMapUpdateToken(token);
|
|
133
|
+
if (resolvedToken) {
|
|
134
|
+
headers.set(MAP_UPDATE_TOKEN_HEADER, resolvedToken);
|
|
135
|
+
}
|
|
136
|
+
return headers;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function readMapUpdateToken(headers: Headers): string {
|
|
140
|
+
const directToken = headers.get(MAP_UPDATE_TOKEN_HEADER);
|
|
141
|
+
if (directToken) {
|
|
142
|
+
return directToken;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const authorization = headers.get("authorization");
|
|
146
|
+
if (!authorization) {
|
|
147
|
+
return "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const [scheme, value] = authorization.split(/\s+/, 2);
|
|
151
|
+
if (scheme?.toLowerCase() !== "bearer" || !value) {
|
|
152
|
+
return "";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return value.trim();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function isMapUpdateAuthorized(headers: Headers, expectedToken?: string): boolean {
|
|
159
|
+
const requiredToken = resolveMapUpdateToken(expectedToken);
|
|
160
|
+
if (!requiredToken) {
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return readMapUpdateToken(headers) === requiredToken;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function resolveMapDocument(
|
|
167
|
+
mapId: string,
|
|
168
|
+
mapDefinition: any,
|
|
169
|
+
options: ResolveMapOptions,
|
|
170
|
+
): Promise<{ xml: string; sourceUrl?: string }> {
|
|
171
|
+
if (typeof mapDefinition?.data === "string" && mapDefinition.data.includes("<map")) {
|
|
172
|
+
return { xml: mapDefinition.data };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof mapDefinition?.file === "string") {
|
|
176
|
+
const file = mapDefinition.file.trim();
|
|
177
|
+
if (file.includes("<map")) {
|
|
178
|
+
return { xml: file };
|
|
179
|
+
}
|
|
180
|
+
if (/^https?:\/\//i.test(file)) {
|
|
181
|
+
const xml = await fetchTextByUrl(file);
|
|
182
|
+
if (xml) {
|
|
183
|
+
return { xml, sourceUrl: file };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (file.startsWith("/") && options.host) {
|
|
187
|
+
const sourceUrl = `http://${options.host}${file}`;
|
|
188
|
+
const xml = await fetchTextByUrl(sourceUrl);
|
|
189
|
+
if (xml) {
|
|
190
|
+
return { xml, sourceUrl };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const xmlFromFile = await readTextByFilePath(file);
|
|
194
|
+
if (xmlFromFile) {
|
|
195
|
+
return { xml: xmlFromFile };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.host) {
|
|
200
|
+
for (const basePath of getTiledBasePaths(options.tiledBasePaths)) {
|
|
201
|
+
const prefix = toBasePathPrefix(basePath);
|
|
202
|
+
const sourceUrl = `http://${options.host}${prefix}/${mapId}.tmx`;
|
|
203
|
+
const xml = await fetchTextByUrl(sourceUrl);
|
|
204
|
+
if (xml) {
|
|
205
|
+
return { xml, sourceUrl };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { xml: "" };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function enrichMapWithParsedTiledData(payload: any, options: ResolveMapOptions = {}): Promise<void> {
|
|
214
|
+
if (payload?.parsedMap || typeof payload?.id !== "string") {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const maps = Array.isArray(payload.__maps) ? payload.__maps : [];
|
|
219
|
+
const mapDefinition = extractFileLikeMapDefinition(maps, payload.id);
|
|
220
|
+
const mapDoc = await resolveMapDocument(payload.id, mapDefinition, options);
|
|
221
|
+
if (!mapDoc.xml) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const tiledModuleName = "@canvasengine/tiled";
|
|
227
|
+
const tiledModule = await import(/* @vite-ignore */ tiledModuleName);
|
|
228
|
+
const TiledParser = tiledModule?.TiledParser;
|
|
229
|
+
if (!TiledParser) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const mapParser = new TiledParser(mapDoc.xml);
|
|
234
|
+
const parsedMap = mapParser.parseMap();
|
|
235
|
+
const tilesets = Array.isArray(parsedMap?.tilesets) ? parsedMap.tilesets : [];
|
|
236
|
+
const mergedTilesets: any[] = [];
|
|
237
|
+
|
|
238
|
+
for (const tileset of tilesets) {
|
|
239
|
+
if (!tileset?.source) {
|
|
240
|
+
mergedTilesets.push(tileset);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let tilesetUrl: string | undefined;
|
|
245
|
+
if (mapDoc.sourceUrl) {
|
|
246
|
+
try {
|
|
247
|
+
tilesetUrl = new URL(tileset.source, mapDoc.sourceUrl).toString();
|
|
248
|
+
} catch {
|
|
249
|
+
tilesetUrl = undefined;
|
|
250
|
+
}
|
|
251
|
+
} else if (options.host) {
|
|
252
|
+
const prefix = toBasePathPrefix(getTiledBasePaths(options.tiledBasePaths)[0] || "map");
|
|
253
|
+
const candidatePath = tileset.source.startsWith("/")
|
|
254
|
+
? tileset.source
|
|
255
|
+
: `${prefix}/${tileset.source}`.replace(/\/{2,}/g, "/");
|
|
256
|
+
tilesetUrl = `http://${options.host}${candidatePath.startsWith("/") ? candidatePath : `/${candidatePath}`}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const tilesetRaw = tilesetUrl
|
|
260
|
+
? await fetchTextByUrl(tilesetUrl)
|
|
261
|
+
: await readTextByFilePath(tileset.source);
|
|
262
|
+
|
|
263
|
+
if (!tilesetRaw) {
|
|
264
|
+
mergedTilesets.push(tileset);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const tilesetParser = new TiledParser(tilesetRaw);
|
|
270
|
+
const parsedTileset = tilesetParser.parseTileset();
|
|
271
|
+
mergedTilesets.push({
|
|
272
|
+
...tileset,
|
|
273
|
+
...parsedTileset,
|
|
274
|
+
});
|
|
275
|
+
} catch {
|
|
276
|
+
mergedTilesets.push(tileset);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
parsedMap.tilesets = mergedTilesets;
|
|
281
|
+
payload.data = mapDoc.xml;
|
|
282
|
+
payload.parsedMap = parsedMap;
|
|
283
|
+
|
|
284
|
+
if (typeof parsedMap?.width === "number" && typeof parsedMap?.tilewidth === "number") {
|
|
285
|
+
payload.width = parsedMap.width * parsedMap.tilewidth;
|
|
286
|
+
}
|
|
287
|
+
if (typeof parsedMap?.height === "number" && typeof parsedMap?.tileheight === "number") {
|
|
288
|
+
payload.height = parsedMap.height * parsedMap.tileheight;
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export async function updateMap(roomId: string, rpgServer: RpgTransportServer, options: ResolveMapOptions = {}): Promise<void> {
|
|
296
|
+
if (!roomId.startsWith("map-")) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const mapId = normalizeRoomMapId(roomId);
|
|
302
|
+
const serverMaps = Array.isArray(rpgServer.maps) ? rpgServer.maps : [];
|
|
303
|
+
const defaultMapPayload: any = {
|
|
304
|
+
id: mapId,
|
|
305
|
+
width: 0,
|
|
306
|
+
height: 0,
|
|
307
|
+
events: [],
|
|
308
|
+
__maps: serverMaps,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
await enrichMapWithParsedTiledData(defaultMapPayload, options);
|
|
312
|
+
delete defaultMapPayload.__maps;
|
|
313
|
+
|
|
314
|
+
const headers = createMapUpdateHeaders(options.mapUpdateToken, options.headers);
|
|
315
|
+
|
|
316
|
+
await rpgServer.onRequest?.({
|
|
317
|
+
url: `http://localhost/parties/main/${roomId}/map/update`,
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers,
|
|
320
|
+
json: async () => defaultMapPayload,
|
|
321
|
+
text: async () => JSON.stringify(defaultMapPayload),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
console.log(`Initialized map for room ${roomId} via POST /map/update`);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.warn(`Failed initializing map for room ${roomId}:`, error);
|
|
327
|
+
}
|
|
328
|
+
}
|
package/src/node/room.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { PartyConnection } from "./connection";
|
|
2
|
+
|
|
3
|
+
export class PartyRoom {
|
|
4
|
+
public id: string;
|
|
5
|
+
public internalID: string;
|
|
6
|
+
public env: Record<string, any> = {};
|
|
7
|
+
public context: any = {};
|
|
8
|
+
|
|
9
|
+
private connections = new Map<string, PartyConnection>();
|
|
10
|
+
private storageData = new Map<string, any>();
|
|
11
|
+
|
|
12
|
+
constructor(id: string) {
|
|
13
|
+
this.id = id;
|
|
14
|
+
this.internalID = `internal_${id}_${Date.now()}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async broadcast(message: any, except: string[] = []): Promise<void> {
|
|
18
|
+
const data = typeof message === "string" ? message : JSON.stringify(message);
|
|
19
|
+
const sendPromises: Promise<void>[] = [];
|
|
20
|
+
|
|
21
|
+
for (const [connectionId, connection] of this.connections) {
|
|
22
|
+
if (!except.includes(connectionId)) {
|
|
23
|
+
sendPromises.push(connection.send(data));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
await Promise.all(sendPromises);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
getConnection(id: string): PartyConnection | undefined {
|
|
31
|
+
return this.connections.get(id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getConnections(tag?: string): IterableIterator<PartyConnection> {
|
|
35
|
+
void tag;
|
|
36
|
+
return this.connections.values();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
addConnection(connection: PartyConnection): void {
|
|
40
|
+
this.connections.set(connection.id, connection);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
removeConnection(connectionId: string): void {
|
|
44
|
+
this.connections.delete(connectionId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get storage() {
|
|
48
|
+
return {
|
|
49
|
+
put: async (key: string, value: any) => {
|
|
50
|
+
this.storageData.set(key, value);
|
|
51
|
+
},
|
|
52
|
+
get: async <T = any>(key: string): Promise<T | undefined> => {
|
|
53
|
+
return this.storageData.get(key) as T;
|
|
54
|
+
},
|
|
55
|
+
delete: async (key: string) => {
|
|
56
|
+
this.storageData.delete(key);
|
|
57
|
+
},
|
|
58
|
+
list: async () => {
|
|
59
|
+
return Array.from(this.storageData.entries());
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|