@rpgjs/server 5.0.0-alpha.8 → 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 +100 -7
- 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 +233 -5
- 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 -29866
- 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 +401 -37
- 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 +284 -149
- 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
- package/dist/Player/Event.d.ts +0 -0
- package/src/Player/Event.ts +0 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import { injector } from "@signe/di";
|
|
4
|
+
import { context as serverContext } from "../core/context";
|
|
5
|
+
import { setInject } from "../core/inject";
|
|
6
|
+
import { provideServerModules } from "../module";
|
|
7
|
+
import { PartyConnection } from "./connection";
|
|
8
|
+
import { createMapUpdateHeaders, resolveMapUpdateToken, updateMap } from "./map";
|
|
9
|
+
import { PartyRoom } from "./room";
|
|
10
|
+
import type {
|
|
11
|
+
CreateRpgServerTransportOptions,
|
|
12
|
+
HandleNodeRequestOptions,
|
|
13
|
+
RpgTransportRequestLike,
|
|
14
|
+
RpgTransportServer,
|
|
15
|
+
RpgTransportServerConstructor,
|
|
16
|
+
RpgWebSocketConnection,
|
|
17
|
+
RpgWebSocketRequestLike,
|
|
18
|
+
RpgWebSocketServer,
|
|
19
|
+
SendMapUpdateOptions,
|
|
20
|
+
} from "./types";
|
|
21
|
+
|
|
22
|
+
type PartiesFetchInit = {
|
|
23
|
+
body?: any;
|
|
24
|
+
headers?: HeadersInit | IncomingHttpHeaders | Map<string, string | undefined>;
|
|
25
|
+
method?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function normalizePathPrefix(path: string, fallback: string): string {
|
|
29
|
+
const trimmed = (path || fallback).trim();
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
34
|
+
return prefixed !== "/" ? prefixed.replace(/\/+$/, "") : prefixed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasPathPrefix(pathname: string, prefix: string): boolean {
|
|
38
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function prependMountedPath(pathname: string, mountedPath?: string): string {
|
|
42
|
+
if (!mountedPath) {
|
|
43
|
+
return pathname;
|
|
44
|
+
}
|
|
45
|
+
const normalizedMountedPath = normalizePathPrefix(mountedPath, "/");
|
|
46
|
+
if (hasPathPrefix(pathname, normalizedMountedPath)) {
|
|
47
|
+
return pathname;
|
|
48
|
+
}
|
|
49
|
+
if (pathname === "/") {
|
|
50
|
+
return normalizedMountedPath;
|
|
51
|
+
}
|
|
52
|
+
return `${normalizedMountedPath}${pathname.startsWith("/") ? pathname : `/${pathname}`}`.replace(/\/{2,}/g, "/");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseHttpRoute(pathname: string, partiesPath: string): { roomId: string; requestPath: string } | null {
|
|
56
|
+
if (!hasPathPrefix(pathname, partiesPath)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const remainder = pathname.slice(partiesPath.length);
|
|
61
|
+
const segments = remainder.split("/").filter(Boolean);
|
|
62
|
+
if (segments.length < 2) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
roomId: segments[0],
|
|
68
|
+
requestPath: `/${segments.slice(1).join("/")}`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseSocketRoute(pathname: string, partiesPath: string): { roomId: string } | null {
|
|
73
|
+
if (!hasPathPrefix(pathname, partiesPath)) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const remainder = pathname.slice(partiesPath.length);
|
|
78
|
+
const segments = remainder.split("/").filter(Boolean);
|
|
79
|
+
if (segments.length < 1) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { roomId: segments[0] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toHeaders(
|
|
87
|
+
input?: Headers | HeadersInit | IncomingHttpHeaders | Map<string, string | undefined>,
|
|
88
|
+
): Headers {
|
|
89
|
+
if (!input) {
|
|
90
|
+
return new Headers();
|
|
91
|
+
}
|
|
92
|
+
if (input instanceof Headers) {
|
|
93
|
+
return new Headers(input);
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(input)) {
|
|
96
|
+
return new Headers(input);
|
|
97
|
+
}
|
|
98
|
+
if (input instanceof Map) {
|
|
99
|
+
const headers = new Headers();
|
|
100
|
+
for (const [key, value] of input) {
|
|
101
|
+
if (typeof value !== "undefined") {
|
|
102
|
+
headers.set(key, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return headers;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const headers = new Headers();
|
|
109
|
+
Object.entries(input).forEach(([key, value]) => {
|
|
110
|
+
if (Array.isArray(value)) {
|
|
111
|
+
if (typeof value[0] !== "undefined") {
|
|
112
|
+
headers.set(key, value[0]);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (typeof value !== "undefined") {
|
|
117
|
+
headers.set(key, String(value));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return headers;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function createRequestLike(url: string, method: string, headers: Headers, bodyText: string): RpgTransportRequestLike {
|
|
124
|
+
return {
|
|
125
|
+
url,
|
|
126
|
+
method,
|
|
127
|
+
headers,
|
|
128
|
+
json: async () => {
|
|
129
|
+
if (!bodyText) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return JSON.parse(bodyText);
|
|
133
|
+
},
|
|
134
|
+
text: async () => bodyText,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function normalizeEngineResponse(result: any): Promise<Response> {
|
|
139
|
+
if (result instanceof Response) {
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
if (typeof result === "string") {
|
|
143
|
+
return new Response(result, {
|
|
144
|
+
status: 200,
|
|
145
|
+
headers: {
|
|
146
|
+
"Content-Type": "text/plain",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return new Response(JSON.stringify(result ?? {}), {
|
|
152
|
+
status: 200,
|
|
153
|
+
headers: {
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function sendNodeResponse(res: ServerResponse, response: Response): Promise<void> {
|
|
160
|
+
res.statusCode = response.status;
|
|
161
|
+
response.headers.forEach((value, key) => {
|
|
162
|
+
res.setHeader(key, value);
|
|
163
|
+
});
|
|
164
|
+
res.end(await response.text());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function readNodeBody(req: IncomingMessage): Promise<string> {
|
|
168
|
+
return await new Promise<string>((resolve, reject) => {
|
|
169
|
+
const chunks: Buffer[] = [];
|
|
170
|
+
req.on("data", (chunk: Buffer | string) => {
|
|
171
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
172
|
+
});
|
|
173
|
+
req.on("end", () => {
|
|
174
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
175
|
+
});
|
|
176
|
+
req.on("error", reject);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveUrlFromSocketRequest(request: RpgWebSocketRequestLike): { headers: Headers; method?: string; rawUrl: string; url: URL } {
|
|
181
|
+
const headers = toHeaders(request.headers);
|
|
182
|
+
const host = headers.get("host") || "localhost";
|
|
183
|
+
const rawUrl = request.url || "/";
|
|
184
|
+
const url = new URL(rawUrl, `http://${host}`);
|
|
185
|
+
return {
|
|
186
|
+
headers,
|
|
187
|
+
method: request.method,
|
|
188
|
+
rawUrl,
|
|
189
|
+
url,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function createConnectionContext(url: URL, headers: Headers, method?: string): any {
|
|
194
|
+
const normalizedHeaders = new Map<string, string>();
|
|
195
|
+
headers.forEach((value, key) => {
|
|
196
|
+
normalizedHeaders.set(key.toLowerCase(), value);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
request: {
|
|
201
|
+
headers: {
|
|
202
|
+
has: (name: string) => normalizedHeaders.has(name.toLowerCase()),
|
|
203
|
+
get: (name: string) => normalizedHeaders.get(name.toLowerCase()),
|
|
204
|
+
entries: () => normalizedHeaders.entries(),
|
|
205
|
+
keys: () => normalizedHeaders.keys(),
|
|
206
|
+
values: () => normalizedHeaders.values(),
|
|
207
|
+
},
|
|
208
|
+
method,
|
|
209
|
+
url: url.toString(),
|
|
210
|
+
},
|
|
211
|
+
url,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export class RpgServerTransport {
|
|
216
|
+
private serverContextInitialized = false;
|
|
217
|
+
private partiesPath: string;
|
|
218
|
+
private readonly initializeMaps: boolean;
|
|
219
|
+
private readonly mapUpdateToken: string;
|
|
220
|
+
private readonly tiledBasePaths?: string[];
|
|
221
|
+
private readonly rooms = new Map<string, PartyRoom>();
|
|
222
|
+
private readonly servers = new Map<string, RpgTransportServer>();
|
|
223
|
+
private lastKnownHost = "";
|
|
224
|
+
|
|
225
|
+
constructor(
|
|
226
|
+
private readonly serverModule: RpgTransportServerConstructor,
|
|
227
|
+
options: CreateRpgServerTransportOptions = {},
|
|
228
|
+
) {
|
|
229
|
+
this.initializeMaps = options.initializeMaps ?? true;
|
|
230
|
+
this.mapUpdateToken = resolveMapUpdateToken(options.mapUpdateToken);
|
|
231
|
+
this.partiesPath = normalizePathPrefix(options.partiesPath || "/parties/main", "/parties/main");
|
|
232
|
+
this.tiledBasePaths = options.tiledBasePaths;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private async ensureServerContext(): Promise<void> {
|
|
236
|
+
if (this.serverContextInitialized) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
setInject(serverContext);
|
|
241
|
+
await injector(serverContext, [provideServerModules([])]);
|
|
242
|
+
this.serverContextInitialized = true;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
getRoom(roomId: string): PartyRoom | undefined {
|
|
246
|
+
return this.rooms.get(roomId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getServer(roomId: string): RpgTransportServer | undefined {
|
|
250
|
+
return this.servers.get(roomId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async ensureRoomAndServer(roomId: string, host?: string): Promise<{ room: PartyRoom; rpgServer: RpgTransportServer }> {
|
|
254
|
+
if (host) {
|
|
255
|
+
this.lastKnownHost = host;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let room = this.rooms.get(roomId);
|
|
259
|
+
if (!room) {
|
|
260
|
+
room = new PartyRoom(roomId);
|
|
261
|
+
this.rooms.set(roomId, room);
|
|
262
|
+
console.log(`Created new room: ${roomId}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let rpgServer = this.servers.get(roomId);
|
|
266
|
+
if (!rpgServer) {
|
|
267
|
+
await this.ensureServerContext();
|
|
268
|
+
rpgServer = new this.serverModule(room);
|
|
269
|
+
this.servers.set(roomId, rpgServer);
|
|
270
|
+
console.log(`Created new server instance for room: ${roomId}`);
|
|
271
|
+
|
|
272
|
+
if (typeof rpgServer.onStart === "function") {
|
|
273
|
+
try {
|
|
274
|
+
await rpgServer.onStart();
|
|
275
|
+
console.log(`Server started for room: ${roomId}`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error(`Error starting server for room ${roomId}:`, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (this.initializeMaps) {
|
|
282
|
+
await updateMap(roomId, rpgServer, {
|
|
283
|
+
host: host || this.lastKnownHost,
|
|
284
|
+
mapUpdateToken: this.mapUpdateToken,
|
|
285
|
+
tiledBasePaths: this.tiledBasePaths,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
room.context.parties = this.buildPartiesContext();
|
|
291
|
+
return { room, rpgServer };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private buildPartiesContext() {
|
|
295
|
+
return {
|
|
296
|
+
main: {
|
|
297
|
+
get: async (targetRoomId: string) => {
|
|
298
|
+
return {
|
|
299
|
+
fetch: async (path: string, init?: PartiesFetchInit) => {
|
|
300
|
+
const method = (init?.method || "GET").toUpperCase();
|
|
301
|
+
const headers = toHeaders(init?.headers);
|
|
302
|
+
const requestPath = path.startsWith("/") ? path : `/${path}`;
|
|
303
|
+
let bodyText = "";
|
|
304
|
+
|
|
305
|
+
if (typeof init?.body === "string") {
|
|
306
|
+
bodyText = init.body;
|
|
307
|
+
} else if (typeof init?.body !== "undefined") {
|
|
308
|
+
bodyText = JSON.stringify(init.body);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return this.dispatchRoomRequest(
|
|
312
|
+
targetRoomId,
|
|
313
|
+
createRequestLike(
|
|
314
|
+
`http://localhost${this.partiesPath}/${targetRoomId}${requestPath}`,
|
|
315
|
+
method,
|
|
316
|
+
headers,
|
|
317
|
+
bodyText,
|
|
318
|
+
),
|
|
319
|
+
this.lastKnownHost,
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
} as any;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async dispatchRoomRequest(roomId: string, requestLike: RpgTransportRequestLike, host?: string): Promise<Response> {
|
|
329
|
+
const { room, rpgServer } = await this.ensureRoomAndServer(roomId, host);
|
|
330
|
+
room.context.parties = this.buildPartiesContext();
|
|
331
|
+
const result = await rpgServer.onRequest?.(requestLike);
|
|
332
|
+
return normalizeEngineResponse(result);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async fetch(request: Request | string | URL, init?: RequestInit): Promise<Response> {
|
|
336
|
+
const webRequest = request instanceof Request ? request : new Request(String(request), init);
|
|
337
|
+
const url = new URL(webRequest.url);
|
|
338
|
+
const route = parseHttpRoute(url.pathname, this.partiesPath);
|
|
339
|
+
if (!route) {
|
|
340
|
+
return new Response(JSON.stringify({ error: "Not found" }), {
|
|
341
|
+
status: 404,
|
|
342
|
+
headers: {
|
|
343
|
+
"Content-Type": "application/json",
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const bodyText = await webRequest.text();
|
|
349
|
+
return this.dispatchRoomRequest(
|
|
350
|
+
route.roomId,
|
|
351
|
+
createRequestLike(webRequest.url, webRequest.method.toUpperCase(), toHeaders(webRequest.headers), bodyText),
|
|
352
|
+
url.host,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async updateMap(mapId: string, payload: any, options: SendMapUpdateOptions = {}): Promise<Response> {
|
|
357
|
+
const roomId = mapId.startsWith("map-") ? mapId : `map-${mapId}`;
|
|
358
|
+
const headers = createMapUpdateHeaders(this.mapUpdateToken, options.headers);
|
|
359
|
+
if (!headers.has("content-type")) {
|
|
360
|
+
headers.set("content-type", "application/json");
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return this.dispatchRoomRequest(
|
|
364
|
+
roomId,
|
|
365
|
+
createRequestLike(
|
|
366
|
+
`http://localhost${this.partiesPath}/${roomId}/map/update`,
|
|
367
|
+
"POST",
|
|
368
|
+
headers,
|
|
369
|
+
JSON.stringify(payload),
|
|
370
|
+
),
|
|
371
|
+
options.host ?? this.lastKnownHost,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async handleNodeRequest(
|
|
376
|
+
req: IncomingMessage,
|
|
377
|
+
res: ServerResponse,
|
|
378
|
+
next?: () => void,
|
|
379
|
+
options: HandleNodeRequestOptions = {},
|
|
380
|
+
): Promise<boolean> {
|
|
381
|
+
try {
|
|
382
|
+
const headers = toHeaders(req.headers);
|
|
383
|
+
const host = headers.get("host") || "localhost";
|
|
384
|
+
const url = new URL(req.url || "/", `http://${host}`);
|
|
385
|
+
const normalizedPathname = prependMountedPath(url.pathname, options.mountedPath);
|
|
386
|
+
const normalizedUrl = new URL(url.toString());
|
|
387
|
+
normalizedUrl.pathname = normalizedPathname;
|
|
388
|
+
|
|
389
|
+
const route = parseHttpRoute(normalizedUrl.pathname, this.partiesPath);
|
|
390
|
+
if (!route) {
|
|
391
|
+
next?.();
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const bodyText = await readNodeBody(req);
|
|
396
|
+
const response = await this.dispatchRoomRequest(
|
|
397
|
+
route.roomId,
|
|
398
|
+
createRequestLike(normalizedUrl.toString(), (req.method || "GET").toUpperCase(), headers, bodyText),
|
|
399
|
+
host,
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
await sendNodeResponse(res, response);
|
|
403
|
+
return true;
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error("Error handling RPG-JS request:", error);
|
|
406
|
+
res.statusCode = 500;
|
|
407
|
+
res.setHeader("Content-Type", "application/json");
|
|
408
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async acceptWebSocket(ws: RpgWebSocketConnection, request: RpgWebSocketRequestLike): Promise<boolean> {
|
|
414
|
+
const normalizedRequest = resolveUrlFromSocketRequest(request);
|
|
415
|
+
const route = parseSocketRoute(normalizedRequest.url.pathname, this.partiesPath);
|
|
416
|
+
if (!route) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
console.log(`WebSocket upgrade request: ${normalizedRequest.url.pathname}`);
|
|
422
|
+
|
|
423
|
+
const queryParams = Object.fromEntries(normalizedRequest.url.searchParams.entries());
|
|
424
|
+
console.log(`Room: ${route.roomId}, Query params:`, queryParams);
|
|
425
|
+
|
|
426
|
+
const { room, rpgServer } = await this.ensureRoomAndServer(route.roomId, normalizedRequest.url.host);
|
|
427
|
+
room.context.parties = this.buildPartiesContext();
|
|
428
|
+
|
|
429
|
+
const connection = new PartyConnection(ws, queryParams._pk, normalizedRequest.rawUrl);
|
|
430
|
+
room.addConnection(connection);
|
|
431
|
+
|
|
432
|
+
console.log(`WebSocket connection established: ${connection.id} in room: ${route.roomId}`);
|
|
433
|
+
|
|
434
|
+
let isClosed = false;
|
|
435
|
+
const cleanup = async (logMessage?: string, error?: Error) => {
|
|
436
|
+
if (isClosed) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
isClosed = true;
|
|
440
|
+
if (logMessage) {
|
|
441
|
+
console.log(logMessage);
|
|
442
|
+
}
|
|
443
|
+
if (error) {
|
|
444
|
+
console.error("WebSocket error:", error);
|
|
445
|
+
}
|
|
446
|
+
room.removeConnection(connection.id);
|
|
447
|
+
await rpgServer.onClose?.(connection as any);
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
ws.on("message", async (data: Buffer | string) => {
|
|
451
|
+
try {
|
|
452
|
+
const rawMessage = typeof data === "string" ? data : data.toString();
|
|
453
|
+
|
|
454
|
+
if (PartyConnection.packetLossEnabled && PartyConnection.packetLossRate > 0) {
|
|
455
|
+
if (!PartyConnection.packetLossFilter || rawMessage.includes(PartyConnection.packetLossFilter)) {
|
|
456
|
+
const random = Math.random();
|
|
457
|
+
if (random < PartyConnection.packetLossRate) {
|
|
458
|
+
console.log(
|
|
459
|
+
`\x1b[31m[PACKET LOSS]\x1b[0m Connection ${connection.id}: Server dropped an incoming packet (${(PartyConnection.packetLossRate * 100).toFixed(1)}% loss rate)`,
|
|
460
|
+
);
|
|
461
|
+
console.log(`\x1b[33m[PACKET DATA]\x1b[0m ${rawMessage.slice(0, 100)}${rawMessage.length > 100 ? "..." : ""}`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
connection.bufferIncoming(rawMessage, async (batch: string[]) => {
|
|
468
|
+
for (const message of batch) {
|
|
469
|
+
await rpgServer.onMessage?.(message, connection as any);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
} catch (error) {
|
|
473
|
+
console.error("Error processing WebSocket message:", error);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
ws.on("close", () => {
|
|
478
|
+
void cleanup(`WebSocket connection closed: ${connection.id} from room: ${route.roomId}`);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
ws.on("error", (error: Error) => {
|
|
482
|
+
void cleanup(undefined, error);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (typeof rpgServer.onConnect === "function") {
|
|
486
|
+
await rpgServer.onConnect(
|
|
487
|
+
connection as any,
|
|
488
|
+
createConnectionContext(normalizedRequest.url, normalizedRequest.headers, normalizedRequest.method) as any,
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
await connection.send({
|
|
493
|
+
type: "connected",
|
|
494
|
+
id: connection.id,
|
|
495
|
+
message: "Connected to RPG-JS server",
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
return true;
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error("Error establishing WebSocket connection:", error);
|
|
501
|
+
ws.close();
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async handleUpgrade(
|
|
507
|
+
wsServer: RpgWebSocketServer,
|
|
508
|
+
request: IncomingMessage,
|
|
509
|
+
socket: Duplex,
|
|
510
|
+
head: Buffer,
|
|
511
|
+
): Promise<boolean> {
|
|
512
|
+
const headers = toHeaders(request.headers);
|
|
513
|
+
const host = headers.get("host") || "localhost";
|
|
514
|
+
const url = new URL(request.url || "/", `http://${host}`);
|
|
515
|
+
if (!parseSocketRoute(url.pathname, this.partiesPath)) {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
520
|
+
void this.acceptWebSocket(ws, request);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function createRpgServerTransport(
|
|
528
|
+
serverModule: RpgTransportServerConstructor,
|
|
529
|
+
options?: CreateRpgServerTransportOptions,
|
|
530
|
+
): RpgServerTransport {
|
|
531
|
+
return new RpgServerTransport(serverModule, options);
|
|
532
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders, IncomingMessage } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
import type { RpgServerEngine } from "../RpgServerEngine";
|
|
4
|
+
|
|
5
|
+
export interface RpgWebSocketConnection {
|
|
6
|
+
readyState: number;
|
|
7
|
+
send(data: string): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
on(event: string, callback: (...args: any[]) => void): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RpgWebSocketServer {
|
|
13
|
+
handleUpgrade(
|
|
14
|
+
request: IncomingMessage,
|
|
15
|
+
socket: Duplex,
|
|
16
|
+
head: Buffer,
|
|
17
|
+
callback: (ws: RpgWebSocketConnection) => void,
|
|
18
|
+
): void;
|
|
19
|
+
close(): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RpgTransportRequestLike {
|
|
23
|
+
url: string;
|
|
24
|
+
method?: string;
|
|
25
|
+
headers?: Headers | HeadersInit | IncomingHttpHeaders | Map<string, string | undefined>;
|
|
26
|
+
text(): Promise<string>;
|
|
27
|
+
json(): Promise<any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RpgWebSocketRequestLike {
|
|
31
|
+
url?: string;
|
|
32
|
+
method?: string;
|
|
33
|
+
headers?: Headers | HeadersInit | IncomingHttpHeaders | Map<string, string | undefined>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type RpgTransportServer = RpgServerEngine & {
|
|
37
|
+
onStart?(): void | Promise<void>;
|
|
38
|
+
onRequest?(req: RpgTransportRequestLike): any | Promise<any>;
|
|
39
|
+
onMessage?(message: string, connection: any): void | Promise<void>;
|
|
40
|
+
onClose?(connection: any): void | Promise<void>;
|
|
41
|
+
onConnect?(connection: any, context: any): void | Promise<void>;
|
|
42
|
+
maps?: any[];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type RpgTransportServerConstructor = new (room: any) => RpgTransportServer;
|
|
46
|
+
|
|
47
|
+
export interface CreateRpgServerTransportOptions {
|
|
48
|
+
initializeMaps?: boolean;
|
|
49
|
+
mapUpdateToken?: string;
|
|
50
|
+
partiesPath?: string;
|
|
51
|
+
tiledBasePaths?: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HandleNodeRequestOptions {
|
|
55
|
+
mountedPath?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SendMapUpdateOptions {
|
|
59
|
+
headers?: HeadersInit | IncomingHttpHeaders | Map<string, string | undefined>;
|
|
60
|
+
host?: string;
|
|
61
|
+
}
|
package/src/presets/index.ts
CHANGED
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { random } from "@rpgjs/common"
|
|
2
|
-
|
|
3
|
-
export const MAXHP: string = 'maxHp'
|
|
4
|
-
export const MAXSP: string = 'maxSp'
|
|
5
|
-
export const ATK: string = 'atk'
|
|
6
|
-
export const PDEF: string = 'pdef'
|
|
7
|
-
export const SDEF: string = 'sdef'
|
|
8
|
-
export const STR: string = 'str'
|
|
9
|
-
export const AGI: string = 'agi'
|
|
10
|
-
export const INT: string = 'int'
|
|
11
|
-
export const DEX: string = 'dex'
|
|
2
|
+
import { DEX, AGI, ATK, PDEF, SDEF, STR, INT } from "@rpgjs/common"
|
|
12
3
|
|
|
13
4
|
export const MAXHP_CURVE = {
|
|
14
5
|
start: 741,
|