@rpgjs/client 5.0.0-alpha.32 → 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/Game/Map.d.ts +11 -1
- package/dist/Game/Map.js +31 -1
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +1 -1
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.js +5 -5
- package/dist/RpgClientEngine.d.ts +15 -0
- package/dist/RpgClientEngine.js +241 -19
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/gui/box.ce.js +3 -3
- package/dist/components/scenes/canvas.ce.js +1 -1
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +34 -2
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/core/inject.js +1 -1
- package/dist/core/setup.js +1 -1
- package/dist/index.js +1 -1
- package/dist/module.js +1 -1
- package/dist/node_modules/.pnpm/{@signe_di@2.8.2 → @signe_di@2.8.3}/node_modules/@signe/di/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_reactive@2.8.2 → @signe_reactive@2.8.3}/node_modules/@signe/reactive/dist/index.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_room@2.8.2 → @signe_room@2.8.3}/node_modules/@signe/room/dist/index.js +4 -4
- package/dist/node_modules/.pnpm/@signe_room@2.8.3/node_modules/@signe/room/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/client/index.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/index.js +1 -1
- package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/index.js.map +1 -1
- package/dist/services/AbstractSocket.d.ts +9 -5
- package/dist/services/AbstractSocket.js.map +1 -1
- package/dist/services/loadMap.js +1 -1
- package/dist/services/mmorpg.d.ts +12 -8
- package/dist/services/mmorpg.js +68 -13
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/standalone.d.ts +2 -4
- package/dist/services/standalone.js +2 -2
- package/dist/services/standalone.js.map +1 -1
- package/package.json +10 -10
- package/src/Game/Map.ts +38 -2
- package/src/Game/Object.ts +1 -1
- package/src/RpgClientEngine.ts +300 -20
- package/src/components/scenes/canvas.ce +1 -1
- package/src/components/scenes/draw-map.ce +37 -1
- package/src/services/AbstractSocket.ts +10 -2
- package/src/services/mmorpg.ts +84 -14
- package/src/services/standalone.ts +3 -3
- package/dist/node_modules/.pnpm/@signe_room@2.8.2/node_modules/@signe/room/dist/index.js.map +0 -1
- /package/dist/node_modules/.pnpm/{@signe_di@2.8.2 → @signe_di@2.8.3}/node_modules/@signe/di/dist/index.js +0 -0
- /package/dist/node_modules/.pnpm/{@signe_reactive@2.8.2 → @signe_reactive@2.8.3}/node_modules/@signe/reactive/dist/index.js +0 -0
- /package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -0
- /package/dist/node_modules/.pnpm/{@signe_sync@2.8.2 → @signe_sync@2.8.3}/node_modules/@signe/sync/dist/client/index.js +0 -0
package/dist/RpgClientEngine.js
CHANGED
|
@@ -6,7 +6,7 @@ import { LoadMapToken } from './services/loadMap.js';
|
|
|
6
6
|
import { RpgSound } from './Sound.js';
|
|
7
7
|
import { RpgResource } from './Resource.js';
|
|
8
8
|
import { Direction, PredictionController, ModulesToken } from '@rpgjs/common';
|
|
9
|
-
import { load } from './node_modules/.pnpm/@signe_sync@2.8.
|
|
9
|
+
import { load } from './node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/index.js';
|
|
10
10
|
import { RpgClientMap } from './Game/Map.js';
|
|
11
11
|
import { RpgGui } from './Gui/Gui.js';
|
|
12
12
|
import { AnimationManager } from './Game/AnimationManager.js';
|
|
@@ -41,6 +41,8 @@ class RpgClientEngine {
|
|
|
41
41
|
this.predictionEnabled = false;
|
|
42
42
|
this.SERVER_CORRECTION_THRESHOLD = 30;
|
|
43
43
|
this.inputFrameCounter = 0;
|
|
44
|
+
this.pendingPredictionFrames = [];
|
|
45
|
+
this.lastClientPhysicsStepAt = 0;
|
|
44
46
|
this.frameOffset = 0;
|
|
45
47
|
// Ping/Pong for RTT measurement
|
|
46
48
|
this.rtt = 0;
|
|
@@ -49,6 +51,10 @@ class RpgClientEngine {
|
|
|
49
51
|
this.PING_INTERVAL_MS = 5e3;
|
|
50
52
|
// Send ping every 5 seconds
|
|
51
53
|
this.lastInputTime = 0;
|
|
54
|
+
this.MOVE_PATH_RESEND_INTERVAL_MS = 120;
|
|
55
|
+
this.MAX_MOVE_TRAJECTORY_POINTS = 240;
|
|
56
|
+
this.lastMovePathSentAt = 0;
|
|
57
|
+
this.lastMovePathSentFrame = 0;
|
|
52
58
|
// Track map loading state for onAfterLoading hook using RxJS
|
|
53
59
|
this.mapLoadCompleted$ = new BehaviorSubject(false);
|
|
54
60
|
this.playerIdReceived$ = new BehaviorSubject(false);
|
|
@@ -125,6 +131,8 @@ class RpgClientEngine {
|
|
|
125
131
|
}
|
|
126
132
|
async start() {
|
|
127
133
|
this.sceneMap = new RpgClientMap();
|
|
134
|
+
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
135
|
+
this.sceneMap.loadPhysic();
|
|
128
136
|
this.selector = document.body.querySelector("#rpg");
|
|
129
137
|
const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
|
|
130
138
|
const { app, canvasElement } = await bootstrapCanvas(
|
|
@@ -160,6 +168,9 @@ class RpgClientEngine {
|
|
|
160
168
|
};
|
|
161
169
|
window.addEventListener("resize", this.resizeHandler);
|
|
162
170
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
171
|
+
this.stepClientPhysicsTick();
|
|
172
|
+
this.flushPendingPredictedStates();
|
|
173
|
+
this.flushPendingMovePath();
|
|
163
174
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
164
175
|
if (tick % 60 === 0) {
|
|
165
176
|
const now = Date.now();
|
|
@@ -173,8 +184,45 @@ class RpgClientEngine {
|
|
|
173
184
|
saveClient.initialize(this.webSocket);
|
|
174
185
|
this.initListeners();
|
|
175
186
|
this.guiService._initialize();
|
|
187
|
+
this.startPingPong();
|
|
176
188
|
});
|
|
177
189
|
}
|
|
190
|
+
prepareSyncPayload(data) {
|
|
191
|
+
const payload = { ...data ?? {} };
|
|
192
|
+
delete payload.ack;
|
|
193
|
+
delete payload.timestamp;
|
|
194
|
+
const myId = this.playerIdSignal();
|
|
195
|
+
const players = payload.players;
|
|
196
|
+
const shouldMaskLocalPosition = this.predictionEnabled && !!this.prediction?.hasPendingInputs();
|
|
197
|
+
if (shouldMaskLocalPosition && myId && players && players[myId]) {
|
|
198
|
+
const localPatch = { ...players[myId] };
|
|
199
|
+
delete localPatch.x;
|
|
200
|
+
delete localPatch.y;
|
|
201
|
+
delete localPatch.direction;
|
|
202
|
+
delete localPatch._frames;
|
|
203
|
+
payload.players = {
|
|
204
|
+
...players,
|
|
205
|
+
[myId]: localPatch
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return payload;
|
|
209
|
+
}
|
|
210
|
+
normalizeAckWithSyncState(ack, syncData) {
|
|
211
|
+
const myId = this.playerIdSignal();
|
|
212
|
+
if (!myId) {
|
|
213
|
+
return ack;
|
|
214
|
+
}
|
|
215
|
+
const localPatch = syncData?.players?.[myId];
|
|
216
|
+
if (typeof localPatch?.x !== "number" || typeof localPatch?.y !== "number") {
|
|
217
|
+
return ack;
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
...ack,
|
|
221
|
+
x: localPatch.x,
|
|
222
|
+
y: localPatch.y,
|
|
223
|
+
direction: localPatch.direction ?? ack.direction
|
|
224
|
+
};
|
|
225
|
+
}
|
|
178
226
|
initListeners() {
|
|
179
227
|
this.webSocket.on("sync", (data) => {
|
|
180
228
|
if (data.pId) {
|
|
@@ -183,22 +231,29 @@ class RpgClientEngine {
|
|
|
183
231
|
}
|
|
184
232
|
if (this.sceneResetQueued) {
|
|
185
233
|
this.sceneMap.reset();
|
|
234
|
+
this.sceneMap.loadPhysic();
|
|
186
235
|
this.sceneResetQueued = false;
|
|
187
236
|
}
|
|
188
237
|
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
const ack = data?.ack;
|
|
239
|
+
const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
|
|
240
|
+
const payload = this.prepareSyncPayload(data);
|
|
241
|
+
load(this.sceneMap, payload, true);
|
|
242
|
+
if (normalizedAck) {
|
|
243
|
+
this.applyServerAck(normalizedAck);
|
|
244
|
+
}
|
|
245
|
+
for (const playerId in payload.players ?? {}) {
|
|
246
|
+
const player = payload.players[playerId];
|
|
192
247
|
if (!player._param) continue;
|
|
193
248
|
for (const param in player._param) {
|
|
194
249
|
this.sceneMap.players()[playerId]._param()[param] = player._param[param];
|
|
195
250
|
}
|
|
196
251
|
}
|
|
197
|
-
const players =
|
|
252
|
+
const players = payload.players || this.sceneMap.players();
|
|
198
253
|
if (players && Object.keys(players).length > 0) {
|
|
199
254
|
this.playersReceived$.next(true);
|
|
200
255
|
}
|
|
201
|
-
const events =
|
|
256
|
+
const events = payload.events || this.sceneMap.events();
|
|
202
257
|
if (events !== void 0) {
|
|
203
258
|
this.eventsReceived$.next(true);
|
|
204
259
|
}
|
|
@@ -216,7 +271,8 @@ class RpgClientEngine {
|
|
|
216
271
|
this.webSocket.on("changeMap", (data) => {
|
|
217
272
|
this.sceneResetQueued = true;
|
|
218
273
|
this.cameraFollowTargetId.set(null);
|
|
219
|
-
|
|
274
|
+
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
|
|
275
|
+
this.loadScene(data.mapId, transferToken);
|
|
220
276
|
});
|
|
221
277
|
this.webSocket.on("showComponentAnimation", (data) => {
|
|
222
278
|
const { params, object, position, id } = data;
|
|
@@ -269,8 +325,29 @@ class RpgClientEngine {
|
|
|
269
325
|
direction
|
|
270
326
|
});
|
|
271
327
|
});
|
|
328
|
+
this.webSocket.on("weatherState", (data) => {
|
|
329
|
+
const raw = data && typeof data === "object" && "value" in data ? data.value : data;
|
|
330
|
+
if (raw === null) {
|
|
331
|
+
this.sceneMap.weatherState.set(null);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const validEffects = ["rain", "snow", "fog", "cloud"];
|
|
335
|
+
if (!raw || !validEffects.includes(raw.effect)) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.sceneMap.weatherState.set({
|
|
339
|
+
effect: raw.effect,
|
|
340
|
+
preset: raw.preset,
|
|
341
|
+
params: raw.params,
|
|
342
|
+
transitionMs: raw.transitionMs,
|
|
343
|
+
durationMs: raw.durationMs,
|
|
344
|
+
startedAt: raw.startedAt,
|
|
345
|
+
seed: raw.seed
|
|
346
|
+
});
|
|
347
|
+
});
|
|
272
348
|
this.webSocket.on("open", () => {
|
|
273
349
|
this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
|
|
350
|
+
this.startPingPong();
|
|
274
351
|
});
|
|
275
352
|
this.webSocket.on("close", () => {
|
|
276
353
|
this.hooks.callHooks("client-engine-onDisconnected", this, this.socket).subscribe();
|
|
@@ -343,7 +420,7 @@ class RpgClientEngine {
|
|
|
343
420
|
clientFrame
|
|
344
421
|
});
|
|
345
422
|
}
|
|
346
|
-
async loadScene(mapId) {
|
|
423
|
+
async loadScene(mapId, transferToken) {
|
|
347
424
|
await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
|
|
348
425
|
this.clearClientPredictionStates();
|
|
349
426
|
this.mapLoadCompleted$.next(false);
|
|
@@ -354,7 +431,10 @@ class RpgClientEngine {
|
|
|
354
431
|
this.onAfterLoadingSubscription.unsubscribe();
|
|
355
432
|
}
|
|
356
433
|
this.setupOnAfterLoadingObserver();
|
|
357
|
-
this.webSocket.updateProperties({
|
|
434
|
+
this.webSocket.updateProperties({
|
|
435
|
+
room: mapId,
|
|
436
|
+
query: transferToken ? { transferToken } : void 0
|
|
437
|
+
});
|
|
358
438
|
await this.webSocket.reconnect(() => {
|
|
359
439
|
const saveClient = inject(SaveClientService);
|
|
360
440
|
saveClient.initialize(this.webSocket);
|
|
@@ -375,6 +455,7 @@ class RpgClientEngine {
|
|
|
375
455
|
this.eventsReceived$.next(true);
|
|
376
456
|
}
|
|
377
457
|
this.mapLoadCompleted$.next(true);
|
|
458
|
+
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
378
459
|
this.sceneMap.loadPhysic();
|
|
379
460
|
}
|
|
380
461
|
addSpriteSheet(spritesheetClass, id) {
|
|
@@ -899,19 +980,22 @@ class RpgClientEngine {
|
|
|
899
980
|
frame = ++this.inputFrameCounter;
|
|
900
981
|
tick = this.getPhysicsTick();
|
|
901
982
|
}
|
|
983
|
+
this.inputFrameCounter = frame;
|
|
902
984
|
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
903
|
-
this.webSocket.emit("move", {
|
|
904
|
-
input,
|
|
905
|
-
timestamp,
|
|
906
|
-
frame,
|
|
907
|
-
tick
|
|
908
|
-
});
|
|
909
985
|
const currentPlayer = this.sceneMap.getCurrentPlayer();
|
|
910
|
-
|
|
986
|
+
const bodyReady = this.ensureCurrentPlayerBody();
|
|
987
|
+
if (currentPlayer && bodyReady) {
|
|
988
|
+
currentPlayer.changeDirection(input);
|
|
911
989
|
this.sceneMap.moveBody(currentPlayer, input);
|
|
990
|
+
if (this.predictionEnabled && this.prediction) {
|
|
991
|
+
this.pendingPredictionFrames.push(frame);
|
|
992
|
+
if (this.pendingPredictionFrames.length > 240) {
|
|
993
|
+
this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
912
996
|
}
|
|
997
|
+
this.emitMovePacket(input, frame, tick, timestamp, true);
|
|
913
998
|
this.lastInputTime = Date.now();
|
|
914
|
-
this.playerIdSignal();
|
|
915
999
|
}
|
|
916
1000
|
processAction({ action }) {
|
|
917
1001
|
if (this.stopProcessingInput) return;
|
|
@@ -933,6 +1017,110 @@ class RpgClientEngine {
|
|
|
933
1017
|
getPhysicsTick() {
|
|
934
1018
|
return this.sceneMap?.getTick?.() ?? 0;
|
|
935
1019
|
}
|
|
1020
|
+
ensureCurrentPlayerBody() {
|
|
1021
|
+
const player = this.sceneMap?.getCurrentPlayer();
|
|
1022
|
+
const myId = this.playerIdSignal();
|
|
1023
|
+
if (!player || !myId) {
|
|
1024
|
+
return false;
|
|
1025
|
+
}
|
|
1026
|
+
if (!player.id) {
|
|
1027
|
+
player.id = myId;
|
|
1028
|
+
}
|
|
1029
|
+
if (this.sceneMap.getBody(myId)) {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
try {
|
|
1033
|
+
this.sceneMap.loadPhysic();
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
console.error("[RPGJS] Unable to initialize client physics before input:", error);
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
return !!this.sceneMap.getBody(myId);
|
|
1039
|
+
}
|
|
1040
|
+
stepClientPhysicsTick() {
|
|
1041
|
+
if (!this.predictionEnabled || !this.sceneMap) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const now = Date.now();
|
|
1045
|
+
if (this.lastClientPhysicsStepAt === 0) {
|
|
1046
|
+
this.lastClientPhysicsStepAt = now;
|
|
1047
|
+
}
|
|
1048
|
+
const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));
|
|
1049
|
+
this.lastClientPhysicsStepAt = now;
|
|
1050
|
+
this.sceneMap.stepClientPhysics(deltaMs);
|
|
1051
|
+
}
|
|
1052
|
+
flushPendingPredictedStates() {
|
|
1053
|
+
if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const state = this.getLocalPlayerState();
|
|
1057
|
+
while (this.pendingPredictionFrames.length > 0) {
|
|
1058
|
+
const frame = this.pendingPredictionFrames.shift();
|
|
1059
|
+
if (typeof frame === "number") {
|
|
1060
|
+
this.prediction.attachPredictedState(frame, state);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
buildPendingMoveTrajectory() {
|
|
1065
|
+
if (!this.predictionEnabled || !this.prediction) {
|
|
1066
|
+
return [];
|
|
1067
|
+
}
|
|
1068
|
+
const pendingInputs = this.prediction.getPendingInputs();
|
|
1069
|
+
const trajectory = [];
|
|
1070
|
+
for (const entry of pendingInputs) {
|
|
1071
|
+
const state = entry.state;
|
|
1072
|
+
if (!state) continue;
|
|
1073
|
+
if (typeof state.x !== "number" || typeof state.y !== "number") continue;
|
|
1074
|
+
trajectory.push({
|
|
1075
|
+
frame: entry.frame,
|
|
1076
|
+
tick: entry.tick,
|
|
1077
|
+
timestamp: entry.timestamp,
|
|
1078
|
+
input: entry.direction,
|
|
1079
|
+
x: state.x,
|
|
1080
|
+
y: state.y,
|
|
1081
|
+
direction: state.direction ?? entry.direction
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
|
|
1085
|
+
return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
|
|
1086
|
+
}
|
|
1087
|
+
return trajectory;
|
|
1088
|
+
}
|
|
1089
|
+
emitMovePacket(input, frame, tick, timestamp, force = false) {
|
|
1090
|
+
const trajectory = this.buildPendingMoveTrajectory();
|
|
1091
|
+
const latestTrajectoryFrame = trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;
|
|
1092
|
+
const shouldThrottle = !force && latestTrajectoryFrame <= this.lastMovePathSentFrame && timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;
|
|
1093
|
+
if (shouldThrottle) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
this.webSocket.emit("move", {
|
|
1097
|
+
input,
|
|
1098
|
+
timestamp,
|
|
1099
|
+
frame,
|
|
1100
|
+
tick,
|
|
1101
|
+
trajectory
|
|
1102
|
+
});
|
|
1103
|
+
this.lastMovePathSentAt = timestamp;
|
|
1104
|
+
this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);
|
|
1105
|
+
}
|
|
1106
|
+
flushPendingMovePath() {
|
|
1107
|
+
if (!this.predictionEnabled || !this.prediction) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
const pendingInputs = this.prediction.getPendingInputs();
|
|
1111
|
+
if (pendingInputs.length === 0) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const latest = pendingInputs[pendingInputs.length - 1];
|
|
1115
|
+
if (!latest) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const now = Date.now();
|
|
1119
|
+
if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
|
|
1123
|
+
}
|
|
936
1124
|
getLocalPlayerState() {
|
|
937
1125
|
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
938
1126
|
if (!currentPlayer) {
|
|
@@ -963,11 +1151,18 @@ class RpgClientEngine {
|
|
|
963
1151
|
initializePredictionController() {
|
|
964
1152
|
if (!this.predictionEnabled) {
|
|
965
1153
|
this.prediction = void 0;
|
|
1154
|
+
this.sceneMap?.configureClientPrediction?.(false);
|
|
966
1155
|
return;
|
|
967
1156
|
}
|
|
1157
|
+
const configuredTtl = this.globalConfig?.prediction?.historyTtlMs;
|
|
1158
|
+
const historyTtlMs = typeof configuredTtl === "number" ? configuredTtl : 1e4;
|
|
1159
|
+
const configuredMaxEntries = this.globalConfig?.prediction?.maxHistoryEntries;
|
|
1160
|
+
const maxHistoryEntries = typeof configuredMaxEntries === "number" ? configuredMaxEntries : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
|
|
1161
|
+
this.sceneMap?.configureClientPrediction?.(true);
|
|
968
1162
|
this.prediction = new PredictionController({
|
|
969
1163
|
correctionThreshold: this.globalConfig?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
970
|
-
historyTtlMs
|
|
1164
|
+
historyTtlMs,
|
|
1165
|
+
maxHistoryEntries,
|
|
971
1166
|
getPhysicsTick: () => this.getPhysicsTick(),
|
|
972
1167
|
getCurrentState: () => this.getLocalPlayerState(),
|
|
973
1168
|
setAuthoritativeState: (state) => this.applyAuthoritativeState(state)
|
|
@@ -1031,6 +1226,10 @@ class RpgClientEngine {
|
|
|
1031
1226
|
this.initializePredictionController();
|
|
1032
1227
|
this.frameOffset = 0;
|
|
1033
1228
|
this.inputFrameCounter = 0;
|
|
1229
|
+
this.pendingPredictionFrames = [];
|
|
1230
|
+
this.lastClientPhysicsStepAt = 0;
|
|
1231
|
+
this.lastMovePathSentAt = 0;
|
|
1232
|
+
this.lastMovePathSentFrame = 0;
|
|
1034
1233
|
}
|
|
1035
1234
|
/**
|
|
1036
1235
|
* Trigger a flash animation on a sprite
|
|
@@ -1089,11 +1288,14 @@ class RpgClientEngine {
|
|
|
1089
1288
|
}
|
|
1090
1289
|
applyServerAck(ack) {
|
|
1091
1290
|
if (this.predictionEnabled && this.prediction) {
|
|
1092
|
-
this.prediction.applyServerAck({
|
|
1291
|
+
const result = this.prediction.applyServerAck({
|
|
1093
1292
|
frame: ack.frame,
|
|
1094
1293
|
serverTick: ack.serverTick,
|
|
1095
1294
|
state: typeof ack.x === "number" && typeof ack.y === "number" ? { x: ack.x, y: ack.y, direction: ack.direction } : void 0
|
|
1096
1295
|
});
|
|
1296
|
+
if (result.state && result.needsReconciliation) {
|
|
1297
|
+
this.reconcilePrediction(result.state, result.pendingInputs);
|
|
1298
|
+
}
|
|
1097
1299
|
return;
|
|
1098
1300
|
}
|
|
1099
1301
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
@@ -1117,6 +1319,24 @@ class RpgClientEngine {
|
|
|
1117
1319
|
player.changeDirection(ack.direction);
|
|
1118
1320
|
}
|
|
1119
1321
|
}
|
|
1322
|
+
reconcilePrediction(authoritativeState, pendingInputs) {
|
|
1323
|
+
const player = this.getCurrentPlayer();
|
|
1324
|
+
if (!player) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
this.sceneMap.stopMovement(player);
|
|
1328
|
+
this.applyAuthoritativeState(authoritativeState);
|
|
1329
|
+
if (!pendingInputs.length) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const replayInputs = pendingInputs.slice(-600);
|
|
1333
|
+
for (const entry of replayInputs) {
|
|
1334
|
+
if (!entry?.direction) continue;
|
|
1335
|
+
this.sceneMap.moveBody(player, entry.direction);
|
|
1336
|
+
this.sceneMap.stepPredictionTick();
|
|
1337
|
+
this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1120
1340
|
/**
|
|
1121
1341
|
* Replay unacknowledged inputs from a given frame to resimulate client prediction
|
|
1122
1342
|
* after applying server authority at a certain frame.
|
|
@@ -1248,6 +1468,8 @@ class RpgClientEngine {
|
|
|
1248
1468
|
this.inputFrameCounter = 0;
|
|
1249
1469
|
this.frameOffset = 0;
|
|
1250
1470
|
this.rtt = 0;
|
|
1471
|
+
this.lastMovePathSentAt = 0;
|
|
1472
|
+
this.lastMovePathSentFrame = 0;
|
|
1251
1473
|
this.mapLoadCompleted$.next(false);
|
|
1252
1474
|
this.playerIdReceived$.next(false);
|
|
1253
1475
|
this.playersReceived$.next(false);
|