@rpgjs/server 5.0.0-alpha.33 → 5.0.0-alpha.35
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/decorators/map.d.ts +22 -0
- package/dist/index.js +387 -324
- package/dist/index.js.map +1 -1
- package/dist/logs/log.d.ts +2 -3
- package/dist/rooms/map.d.ts +38 -3
- package/package.json +5 -5
- package/src/Player/ParameterManager.ts +36 -2
- package/src/Player/Player.ts +30 -8
- package/src/decorators/map.ts +26 -1
- package/src/logs/log.ts +10 -3
- package/src/module.ts +1 -0
- package/src/rooms/map.ts +296 -50
- package/tests/prediction-reconciliation.spec.ts +182 -0
- package/tests/world-maps.spec.ts +83 -1
package/dist/logs/log.d.ts
CHANGED
package/dist/rooms/map.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MockConnection, RoomMethods, RoomOnJoin } from '@signe/room';
|
|
2
|
-
import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WorldMapConfig } from '../../../common/src';
|
|
2
|
+
import { Hooks, RpgCommonMap, RpgShape, WorldMapsManager, WeatherState, WorldMapConfig } from '../../../common/src';
|
|
3
3
|
import { RpgPlayer, RpgEvent } from '../Player/Player';
|
|
4
4
|
import { BehaviorSubject } from 'rxjs';
|
|
5
5
|
import { MapOptions } from '../decorators/map';
|
|
@@ -17,6 +17,8 @@ export interface Controls {
|
|
|
17
17
|
minTimeBetweenInputs?: number;
|
|
18
18
|
/** Whether to enable anti-cheat validation */
|
|
19
19
|
enableAntiCheat?: boolean;
|
|
20
|
+
/** Maximum number of queued inputs processed per server tick */
|
|
21
|
+
maxInputsPerTick?: number;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Interface representing hook methods available for map events
|
|
@@ -58,6 +60,9 @@ export type EventPosOption = {
|
|
|
58
60
|
*/
|
|
59
61
|
event: EventConstructor | (EventHooks & Record<string, any>);
|
|
60
62
|
};
|
|
63
|
+
interface WeatherSetOptions {
|
|
64
|
+
sync?: boolean;
|
|
65
|
+
}
|
|
61
66
|
export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
|
|
62
67
|
/**
|
|
63
68
|
* Synchronized signal containing all players currently on the map
|
|
@@ -158,6 +163,7 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
158
163
|
* with custom formulas when the map is loaded.
|
|
159
164
|
*/
|
|
160
165
|
damageFormulas: any;
|
|
166
|
+
private _weatherState;
|
|
161
167
|
/** Internal: Map of shapes by name */
|
|
162
168
|
private _shapes;
|
|
163
169
|
/** Internal: Map of shape entity UUIDs to RpgShape instances */
|
|
@@ -169,6 +175,8 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
169
175
|
autoSync: boolean;
|
|
170
176
|
constructor(room: any);
|
|
171
177
|
onStart(): Promise<void>;
|
|
178
|
+
private isPositiveNumber;
|
|
179
|
+
private resolveTrustedMapDimensions;
|
|
172
180
|
/**
|
|
173
181
|
* Setup collision detection between players, events, and shapes
|
|
174
182
|
*
|
|
@@ -391,6 +399,10 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
391
399
|
* ```
|
|
392
400
|
*/
|
|
393
401
|
onInput(player: RpgPlayer, input: any): Promise<void>;
|
|
402
|
+
onPing(player: RpgPlayer, payload: {
|
|
403
|
+
clientTime?: number;
|
|
404
|
+
clientFrame?: number;
|
|
405
|
+
}): void;
|
|
394
406
|
saveSlot(player: RpgPlayer, value: {
|
|
395
407
|
requestId: string;
|
|
396
408
|
index: number;
|
|
@@ -463,10 +475,11 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
463
475
|
/**
|
|
464
476
|
* Process pending inputs for a player with anti-cheat validation
|
|
465
477
|
*
|
|
466
|
-
* This method processes
|
|
478
|
+
* This method processes pending inputs for a player while performing
|
|
467
479
|
* anti-cheat validation to prevent time manipulation and frame skipping.
|
|
468
480
|
* It validates the time deltas between inputs and ensures they are within
|
|
469
|
-
* acceptable ranges.
|
|
481
|
+
* acceptable ranges. To preserve movement itinerary under network bursts,
|
|
482
|
+
* the number of inputs processed per call is capped.
|
|
470
483
|
*
|
|
471
484
|
* ## Architecture
|
|
472
485
|
*
|
|
@@ -924,6 +937,27 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
924
937
|
x: number;
|
|
925
938
|
y: number;
|
|
926
939
|
}, graphic: string, animationName?: string): void;
|
|
940
|
+
private cloneWeatherState;
|
|
941
|
+
/**
|
|
942
|
+
* Get the current map weather state.
|
|
943
|
+
*/
|
|
944
|
+
getWeather(): WeatherState | null;
|
|
945
|
+
/**
|
|
946
|
+
* Set the full weather state for this map.
|
|
947
|
+
*
|
|
948
|
+
* When `sync` is true (default), all connected clients receive the new weather.
|
|
949
|
+
*/
|
|
950
|
+
setWeather(next: WeatherState | null, options?: WeatherSetOptions): WeatherState | null;
|
|
951
|
+
/**
|
|
952
|
+
* Patch the current weather state.
|
|
953
|
+
*
|
|
954
|
+
* Nested `params` values are merged.
|
|
955
|
+
*/
|
|
956
|
+
patchWeather(patch: Partial<WeatherState>, options?: WeatherSetOptions): WeatherState | null;
|
|
957
|
+
/**
|
|
958
|
+
* Clear weather for this map.
|
|
959
|
+
*/
|
|
960
|
+
clearWeather(options?: WeatherSetOptions): void;
|
|
927
961
|
/**
|
|
928
962
|
* Configure runtime synchronized properties on the map
|
|
929
963
|
*
|
|
@@ -1257,3 +1291,4 @@ export declare class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoi
|
|
|
1257
1291
|
}
|
|
1258
1292
|
export interface RpgMap extends RoomMethods {
|
|
1259
1293
|
}
|
|
1294
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/server",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.35",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"publishConfig": {
|
|
@@ -11,16 +11,16 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"description": "",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@rpgjs/common": "5.0.0-alpha.
|
|
15
|
-
"@rpgjs/physic": "5.0.0-alpha.
|
|
16
|
-
"@rpgjs/testing": "5.0.0-alpha.
|
|
14
|
+
"@rpgjs/common": "5.0.0-alpha.35",
|
|
15
|
+
"@rpgjs/physic": "5.0.0-alpha.35",
|
|
16
|
+
"@rpgjs/testing": "5.0.0-alpha.35",
|
|
17
17
|
"@rpgjs/database": "^4.3.0",
|
|
18
18
|
"@signe/di": "^2.8.3",
|
|
19
19
|
"@signe/reactive": "^2.8.3",
|
|
20
20
|
"@signe/room": "^2.8.3",
|
|
21
21
|
"@signe/sync": "^2.8.3",
|
|
22
22
|
"rxjs": "^7.8.2",
|
|
23
|
-
"zod": "^4.3.
|
|
23
|
+
"zod": "^4.3.6"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"vite": "^7.3.1",
|
|
@@ -10,6 +10,32 @@ export type ExpCurve = {
|
|
|
10
10
|
accelerationB: number;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
const DEFAULT_EXP_CURVE: ExpCurve = {
|
|
14
|
+
basis: 30,
|
|
15
|
+
extra: 20,
|
|
16
|
+
accelerationA: 30,
|
|
17
|
+
accelerationB: 30
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function isObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
return typeof value === "object" && value !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toValidNumber(value: unknown, fallback: number): number {
|
|
25
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeExpCurve(value: unknown): ExpCurve {
|
|
29
|
+
if (!isObject(value)) return DEFAULT_EXP_CURVE;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
basis: toValidNumber(value.basis, DEFAULT_EXP_CURVE.basis),
|
|
33
|
+
extra: toValidNumber(value.extra, DEFAULT_EXP_CURVE.extra),
|
|
34
|
+
accelerationA: toValidNumber(value.accelerationA, DEFAULT_EXP_CURVE.accelerationA),
|
|
35
|
+
accelerationB: toValidNumber(value.accelerationB, DEFAULT_EXP_CURVE.accelerationB)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
/**
|
|
14
40
|
* Interface for Parameter Manager functionality
|
|
15
41
|
*
|
|
@@ -549,10 +575,18 @@ export function WithParameterManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
549
575
|
* ```
|
|
550
576
|
* @memberof ParameterManager
|
|
551
577
|
* */
|
|
552
|
-
public _expCurveSignal = type(signal<string>(
|
|
578
|
+
public _expCurveSignal = type(signal<string>(JSON.stringify(DEFAULT_EXP_CURVE)) as any, '_expCurveSignal', { persist: true }, this as any)
|
|
553
579
|
|
|
554
580
|
get expCurve(): ExpCurve {
|
|
555
|
-
|
|
581
|
+
const raw = this._expCurveSignal()
|
|
582
|
+
if (!raw) return DEFAULT_EXP_CURVE
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
return normalizeExpCurve(JSON.parse(raw))
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
return DEFAULT_EXP_CURVE
|
|
589
|
+
}
|
|
556
590
|
}
|
|
557
591
|
|
|
558
592
|
set expCurve(val: ExpCurve) {
|
package/src/Player/Player.ts
CHANGED
|
@@ -187,7 +187,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
187
187
|
constructor() {
|
|
188
188
|
super();
|
|
189
189
|
|
|
190
|
-
|
|
190
|
+
const initialX = typeof this.x === "function" ? Number(this.x()) || 0 : 0;
|
|
191
|
+
const initialY = typeof this.y === "function" ? Number(this.y()) || 0 : 0;
|
|
192
|
+
let lastEmitted: { x: number; y: number } | null = { x: initialX, y: initialY };
|
|
191
193
|
let pendingUpdate: { x: number; y: number } | null = null;
|
|
192
194
|
let updateScheduled = false;
|
|
193
195
|
|
|
@@ -252,6 +254,8 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
252
254
|
|
|
253
255
|
setMap(map: RpgMap) {
|
|
254
256
|
this.map = map;
|
|
257
|
+
// Prevent immediate ping-pong map transfers when spawning near a border.
|
|
258
|
+
this.touchSide = true;
|
|
255
259
|
}
|
|
256
260
|
|
|
257
261
|
applyFrames() {
|
|
@@ -303,12 +307,13 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
303
307
|
if (canChange.some(v => v === false)) return false;
|
|
304
308
|
|
|
305
309
|
if (positions && typeof positions === 'object') {
|
|
306
|
-
this.teleport(positions)
|
|
310
|
+
await this.teleport(positions)
|
|
307
311
|
}
|
|
308
|
-
await room?.$sessionTransfer(this.conn, realMapId);
|
|
312
|
+
const transferToken = await room?.$sessionTransfer(this.conn, realMapId);
|
|
309
313
|
this.emit("changeMap", {
|
|
310
314
|
mapId: realMapId,
|
|
311
315
|
positions,
|
|
316
|
+
transferToken: typeof transferToken === 'string' ? transferToken : undefined,
|
|
312
317
|
});
|
|
313
318
|
return true;
|
|
314
319
|
}
|
|
@@ -321,17 +326,34 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
321
326
|
const direction = this.getDirection()
|
|
322
327
|
const marginLeftRight = map.tileWidth / 2
|
|
323
328
|
const marginTopDown = map.tileHeight / 2
|
|
329
|
+
const hitbox = this.hitbox()
|
|
330
|
+
const currentX = this.x()
|
|
331
|
+
const currentY = this.y()
|
|
332
|
+
const nearBorder =
|
|
333
|
+
currentX < marginLeftRight ||
|
|
334
|
+
currentX > map.widthPx - hitbox.w - marginLeftRight ||
|
|
335
|
+
currentY < marginTopDown ||
|
|
336
|
+
currentY > map.heightPx - hitbox.h - marginTopDown
|
|
337
|
+
|
|
338
|
+
if (this.touchSide) {
|
|
339
|
+
if (nearBorder) {
|
|
340
|
+
return false
|
|
341
|
+
}
|
|
342
|
+
this.touchSide = false
|
|
343
|
+
}
|
|
324
344
|
|
|
325
345
|
const changeMap = async (adjacent, to) => {
|
|
326
|
-
|
|
346
|
+
const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
|
|
347
|
+
if (!nextMap) {
|
|
327
348
|
return false
|
|
328
349
|
}
|
|
329
|
-
this.touchSide = true
|
|
330
|
-
const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
|
|
331
|
-
if (!nextMap) return false
|
|
332
350
|
const id = nextMap.id as string
|
|
333
351
|
const nextMapInfo = worldMaps.getMapInfo(id)
|
|
334
|
-
|
|
352
|
+
const changed = !!(await this.changeMap(id, to(nextMapInfo)))
|
|
353
|
+
if (changed) {
|
|
354
|
+
this.touchSide = true
|
|
355
|
+
}
|
|
356
|
+
return changed
|
|
335
357
|
}
|
|
336
358
|
|
|
337
359
|
if (nextPosition.x < marginLeftRight && direction == Direction.Left) {
|
package/src/decorators/map.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { WeatherState } from "@rpgjs/common";
|
|
2
|
+
|
|
1
3
|
export interface MapOptions {
|
|
2
4
|
/**
|
|
3
5
|
* Map identifier. Allows to go to the map (for example with player.changeMap())
|
|
@@ -97,6 +99,28 @@ export interface MapOptions {
|
|
|
97
99
|
* */
|
|
98
100
|
sounds?: string[]
|
|
99
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Initial weather state for this map.
|
|
104
|
+
*
|
|
105
|
+
* This value is applied when the map is loaded and can later be updated
|
|
106
|
+
* at runtime with `map.setWeather()` from server logic.
|
|
107
|
+
*
|
|
108
|
+
* ```ts
|
|
109
|
+
* @MapData({
|
|
110
|
+
* id: 'forest',
|
|
111
|
+
* file: require('./tmx/forest.tmx'),
|
|
112
|
+
* weather: {
|
|
113
|
+
* effect: 'fog',
|
|
114
|
+
* preset: 'rpgForestFog',
|
|
115
|
+
* params: { density: 1.2, height: 0.75 },
|
|
116
|
+
* transitionMs: 1200
|
|
117
|
+
* }
|
|
118
|
+
* })
|
|
119
|
+
* class ForestMap extends RpgMap {}
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
weather?: WeatherState | null
|
|
123
|
+
|
|
100
124
|
/**
|
|
101
125
|
* Whether to stop all sounds before playing the map sounds when a player joins.
|
|
102
126
|
*
|
|
@@ -278,6 +302,7 @@ export function MapData(options: MapOptions) {
|
|
|
278
302
|
target.prototype.file = options.file
|
|
279
303
|
target.prototype.id = options.id
|
|
280
304
|
target.prototype.sounds = options.sounds
|
|
305
|
+
target.prototype.weather = options.weather
|
|
281
306
|
target.prototype.lowMemory = options.lowMemory
|
|
282
307
|
target.prototype.stopAllSoundsBeforeJoin = options.stopAllSoundsBeforeJoin
|
|
283
308
|
|
|
@@ -299,4 +324,4 @@ export function MapData(options: MapOptions) {
|
|
|
299
324
|
target.prototype.onLeave = options.onLeave
|
|
300
325
|
}
|
|
301
326
|
}
|
|
302
|
-
}
|
|
327
|
+
}
|
package/src/logs/log.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
export class Log {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export class Log extends Error {
|
|
2
|
+
readonly id: string;
|
|
3
|
+
|
|
4
|
+
constructor(id: string, msg: string) {
|
|
5
|
+
super(`[${id}] ${msg}`);
|
|
6
|
+
this.name = "RpgLog";
|
|
7
|
+
this.id = id;
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
}
|
package/src/module.ts
CHANGED
|
@@ -97,6 +97,7 @@ export function provideServerModules(modules: RpgServerModule[]): FactoryProvide
|
|
|
97
97
|
type: MapClass.type,
|
|
98
98
|
name: MapClass.prototype?.name,
|
|
99
99
|
sounds: MapClass.prototype?.sounds,
|
|
100
|
+
weather: MapClass.prototype?.weather,
|
|
100
101
|
lowMemory: MapClass.prototype?.lowMemory,
|
|
101
102
|
stopAllSoundsBeforeJoin: MapClass.prototype?.stopAllSoundsBeforeJoin,
|
|
102
103
|
events: MapClass.prototype?._events,
|