@antha/multiplayer-p2p-lock-step 0.21.0 → 0.22.0
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.
|
@@ -34,7 +34,7 @@ export declare function isMultiplayerRoomConnected({ multiplayerP2pLockStep, }:
|
|
|
34
34
|
*
|
|
35
35
|
* @category Internal
|
|
36
36
|
*/
|
|
37
|
-
export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket
|
|
37
|
+
export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket>, StateSync extends JsonCompatibleValue = JsonCompatibleValue> = PartialWithUndefined<SelectFrom<P2pLockStepMultiplayerControllerParams<MultiplayerPacket>, {
|
|
38
38
|
acceptConnection: true;
|
|
39
39
|
debugMultiplayer: true;
|
|
40
40
|
frameDuration: true;
|
|
@@ -60,6 +60,32 @@ export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCom
|
|
|
60
60
|
state: Partial<State>;
|
|
61
61
|
}>) => MaybePromise<number | undefined>;
|
|
62
62
|
};
|
|
63
|
+
/**
|
|
64
|
+
* Sends the host's state to each peer that joins, so games don't need their own sync
|
|
65
|
+
* packets. A joining peer skips every frame until its state is loaded, and desync checks
|
|
66
|
+
* skip it until then too. The host pauses frames while `createStateSync` runs.
|
|
67
|
+
*
|
|
68
|
+
* @default joining peers receive no state
|
|
69
|
+
*/
|
|
70
|
+
stateSync: {
|
|
71
|
+
/** Called on the host, right after applying a frame, to capture the state to send. */
|
|
72
|
+
createStateSync: (params: Readonly<{
|
|
73
|
+
state: Partial<State>;
|
|
74
|
+
}>) => MaybePromise<StateSync>;
|
|
75
|
+
/** Called on a joining (or resyncing) peer to replace its state with the host's. */
|
|
76
|
+
loadStateSync: (params: Readonly<{
|
|
77
|
+
stateSync: StateSync;
|
|
78
|
+
multiplayerController: P2pLockStepMultiplayerController<MultiplayerPacket>;
|
|
79
|
+
state: Partial<State>;
|
|
80
|
+
}>) => MaybePromise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* When `desyncCheck` is also set, a peer that detects a desync reloads the host's state
|
|
83
|
+
* (after `MultiplayerControllerDesyncEvent` is emitted).
|
|
84
|
+
*
|
|
85
|
+
* @default desyncs are only reported
|
|
86
|
+
*/
|
|
87
|
+
resyncOnDesync?: boolean | undefined;
|
|
88
|
+
};
|
|
63
89
|
/** Applies an individual action from within a frame event. */
|
|
64
90
|
handlePacket: (params: Readonly<{
|
|
65
91
|
packet: Readonly<MultiplayerFramePacket<MultiplayerPacket>>;
|
|
@@ -84,4 +110,4 @@ export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCom
|
|
|
84
110
|
*
|
|
85
111
|
* @category Main
|
|
86
112
|
*/
|
|
87
|
-
export declare function createAnthaMultiplayerP2pLockStepMod<const MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket
|
|
113
|
+
export declare function createAnthaMultiplayerP2pLockStepMod<const MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket>, StateSync extends JsonCompatibleValue = JsonCompatibleValue>(options?: Readonly<AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket, NoInfer<State>, NoInfer<StateSync>>>): import("@antha/engine").AnthaMod<NoInfer<State>>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineAnthaMod, ModExecutionTriggerType } from '@antha/engine';
|
|
2
2
|
import { emptyApiAndRoomConnectionState, MultiplayerControllerClientStatusEvent, MultiplayerControllerConnectionEvent, } from '@antha/multiplayer-core';
|
|
3
3
|
import { awaitedBlockingMap, log, } from '@augment-vir/common';
|
|
4
|
-
import { MultiplayerControllerFrameEvent, P2pLockStepMultiplayerController, } from './p2p-lock-step-multiplayer-controller.js';
|
|
4
|
+
import { MultiplayerControllerFrameEvent, MultiplayerControllerStateSyncEvent, P2pLockStepMultiplayerController, } from './p2p-lock-step-multiplayer-controller.js';
|
|
5
5
|
/**
|
|
6
6
|
* Indicates whether a p2p-lock-step multiplayer room is currently connected.
|
|
7
7
|
*
|
|
@@ -18,20 +18,28 @@ export function isMultiplayerRoomConnected({ multiplayerP2pLockStep, }) {
|
|
|
18
18
|
* @category Main
|
|
19
19
|
*/
|
|
20
20
|
export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
|
|
21
|
+
const shouldHandleFrames = !!(options.handlePacket ||
|
|
22
|
+
options.runFrameUpdate ||
|
|
23
|
+
options.desyncCheck ||
|
|
24
|
+
options.stateSync);
|
|
21
25
|
return defineAnthaMod({
|
|
22
26
|
modName: 'antha-multiplayer-p2p-lock-step',
|
|
23
27
|
initState: {
|
|
24
28
|
debugMultiplayer: options.debugMultiplayer,
|
|
25
29
|
multiplayerLockstepTick: 0,
|
|
26
30
|
},
|
|
27
|
-
trigger: options.
|
|
28
|
-
options.runFrameUpdate ||
|
|
29
|
-
options.desyncCheck ||
|
|
30
|
-
options.handleClientStatus
|
|
31
|
+
trigger: shouldHandleFrames || options.handleClientStatus
|
|
31
32
|
? {
|
|
32
33
|
event: [
|
|
33
|
-
...(
|
|
34
|
-
? [
|
|
34
|
+
...(shouldHandleFrames
|
|
35
|
+
? [
|
|
36
|
+
MultiplayerControllerFrameEvent,
|
|
37
|
+
]
|
|
38
|
+
: []),
|
|
39
|
+
...(options.stateSync
|
|
40
|
+
? [
|
|
41
|
+
MultiplayerControllerStateSyncEvent,
|
|
42
|
+
]
|
|
35
43
|
: []),
|
|
36
44
|
...(options.handleClientStatus
|
|
37
45
|
? [MultiplayerControllerClientStatusEvent]
|
|
@@ -54,7 +62,9 @@ export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
|
|
|
54
62
|
acceptConnection: options.acceptConnection,
|
|
55
63
|
debugMultiplayer: state.debugMultiplayer,
|
|
56
64
|
desyncCheckInterval: options.desyncCheck?.interval,
|
|
65
|
+
enableStateSync: !!options.stateSync,
|
|
57
66
|
frameDuration: options.frameDuration,
|
|
67
|
+
resyncOnDesync: options.stateSync?.resyncOnDesync,
|
|
58
68
|
}),
|
|
59
69
|
connectionState: emptyApiAndRoomConnectionState,
|
|
60
70
|
};
|
|
@@ -82,8 +92,19 @@ export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
|
|
|
82
92
|
});
|
|
83
93
|
return;
|
|
84
94
|
}
|
|
95
|
+
else if (event instanceof MultiplayerControllerStateSyncEvent) {
|
|
96
|
+
await options.stateSync?.loadStateSync({
|
|
97
|
+
stateSync: event.detail
|
|
98
|
+
.stateSync,
|
|
99
|
+
multiplayerController: state.multiplayerP2pLockStep.multiplayerController,
|
|
100
|
+
state,
|
|
101
|
+
});
|
|
102
|
+
state.multiplayerP2pLockStep.multiplayerController.finishStateSync();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
85
105
|
else if (event instanceof MultiplayerControllerFrameEvent &&
|
|
86
|
-
|
|
106
|
+
shouldHandleFrames &&
|
|
107
|
+
!state.multiplayerP2pLockStep.multiplayerController.awaitingStateSync) {
|
|
87
108
|
state.multiplayerP2pLockStep.multiplayerController.checkStateHash(event);
|
|
88
109
|
if (options.handlePacket) {
|
|
89
110
|
for (const detail of event.detail.packets) {
|
|
@@ -123,6 +144,11 @@ export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
|
|
|
123
144
|
}),
|
|
124
145
|
});
|
|
125
146
|
}
|
|
147
|
+
if (event.detail.shouldSendStateSync && options.stateSync) {
|
|
148
|
+
state.multiplayerP2pLockStep.multiplayerController.sendStateSync(await options.stateSync.createStateSync({
|
|
149
|
+
state,
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
126
152
|
}
|
|
127
153
|
});
|
|
128
154
|
}
|
|
@@ -9,7 +9,8 @@ import { ListenTarget, type RemoveListenerCallback, type TypedCustomEventInit }
|
|
|
9
9
|
*/
|
|
10
10
|
export declare enum P2pLockStepMessageType {
|
|
11
11
|
Actions = "actions",
|
|
12
|
-
Frame = "frame"
|
|
12
|
+
Frame = "frame",
|
|
13
|
+
StateSyncRequest = "state-sync-request"
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* A single action within a {@link MultiplayerFrame}.
|
|
@@ -38,6 +39,12 @@ export type MultiplayerFrame<MultiplayerPacket extends JsonCompatibleValue> = {
|
|
|
38
39
|
* {@link P2pLockStepMultiplayerController.reportStateHash} right after applying it.
|
|
39
40
|
*/
|
|
40
41
|
shouldReportNextFrameHash: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* On the host, whether to pass the state from right after applying this frame to
|
|
44
|
+
* {@link P2pLockStepMultiplayerController.sendStateSync}. The host produces no more frames until
|
|
45
|
+
* it does.
|
|
46
|
+
*/
|
|
47
|
+
shouldSendStateSync: boolean;
|
|
41
48
|
}>;
|
|
42
49
|
/**
|
|
43
50
|
* Message exchanged by p2p-lock-step clients.
|
|
@@ -65,7 +72,13 @@ export type P2pLockStepMessage<MultiplayerPacket extends JsonCompatibleValue> =
|
|
|
65
72
|
shouldReportNextFrameHash: boolean;
|
|
66
73
|
/** The host's state hash from right after the most recent desync check frame. */
|
|
67
74
|
stateHash: number;
|
|
68
|
-
|
|
75
|
+
/** On synchronization frames, the host's state for the receiving client to load. */
|
|
76
|
+
stateSync: JsonCompatibleValue;
|
|
77
|
+
}>)
|
|
78
|
+
/** Sent from a child client to the host to ask for the host's current state. */
|
|
79
|
+
| {
|
|
80
|
+
type: P2pLockStepMessageType.StateSyncRequest;
|
|
81
|
+
};
|
|
69
82
|
/**
|
|
70
83
|
* Each {@link P2pLockStepMessage} variant, keyed by its message type.
|
|
71
84
|
*
|
|
@@ -107,6 +120,23 @@ export type P2pLockStepMultiplayerControllerParams<Action extends JsonCompatible
|
|
|
107
120
|
acceptConnection?: ((connectingClientId: ClientId, multiplayerController: P2pLockStepMultiplayerController<Action>) => MaybePromise<boolean>) | undefined;
|
|
108
121
|
/** Enables verbose multiplayer debug logs. */
|
|
109
122
|
debugMultiplayer?: boolean | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Sends the host's state to each client that joins. When a client joins, the host's next frame
|
|
125
|
+
* event has `shouldSendStateSync` set, and the host produces no more frames until that state is
|
|
126
|
+
* passed to {@link P2pLockStepMultiplayerController.sendStateSync}. The joining client emits
|
|
127
|
+
* {@link MultiplayerControllerStateSyncEvent} with that state, and ignores every frame before it
|
|
128
|
+
* (see {@link P2pLockStepMultiplayerController.awaitingStateSync}).
|
|
129
|
+
*
|
|
130
|
+
* @default joining clients receive no state
|
|
131
|
+
*/
|
|
132
|
+
enableStateSync?: boolean | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* When `enableStateSync` is also set, a client that detects a desync asks the host for its
|
|
135
|
+
* state with {@link P2pLockStepMultiplayerController.requestStateSync}.
|
|
136
|
+
*
|
|
137
|
+
* @default desyncs are only reported
|
|
138
|
+
*/
|
|
139
|
+
resyncOnDesync?: boolean | undefined;
|
|
110
140
|
/**
|
|
111
141
|
* The duration between desync check frames, rounded to a whole number of frames. Ignored when
|
|
112
142
|
* `frameDuration` is zero, because then frames only run manually. Every peer's frame event for
|
|
@@ -172,12 +202,40 @@ declare const MultiplayerControllerDesyncEvent_base: (new (eventInitDict: {
|
|
|
172
202
|
*/
|
|
173
203
|
export declare class MultiplayerControllerDesyncEvent extends MultiplayerControllerDesyncEvent_base {
|
|
174
204
|
}
|
|
205
|
+
declare const MultiplayerControllerStateSyncEvent_base: (new (eventInitDict: {
|
|
206
|
+
bubbles?: boolean;
|
|
207
|
+
cancelable?: boolean;
|
|
208
|
+
composed?: boolean;
|
|
209
|
+
detail: Readonly<{
|
|
210
|
+
stateSync: JsonCompatibleValue;
|
|
211
|
+
}>;
|
|
212
|
+
}) => import("typed-event-target").TypedCustomEvent<Readonly<{
|
|
213
|
+
stateSync: JsonCompatibleValue;
|
|
214
|
+
}>, "multiplayer-controller-state-sync">) & Pick<{
|
|
215
|
+
new (type: string, eventInitDict?: EventInit): Event;
|
|
216
|
+
prototype: Event;
|
|
217
|
+
readonly NONE: 0;
|
|
218
|
+
readonly CAPTURING_PHASE: 1;
|
|
219
|
+
readonly AT_TARGET: 2;
|
|
220
|
+
readonly BUBBLING_PHASE: 3;
|
|
221
|
+
}, "prototype" | "NONE" | "CAPTURING_PHASE" | "AT_TARGET" | "BUBBLING_PHASE"> & Pick<import("typed-event-target").TypedCustomEvent<Readonly<{
|
|
222
|
+
stateSync: JsonCompatibleValue;
|
|
223
|
+
}>, "multiplayer-controller-state-sync">, "type">;
|
|
224
|
+
/**
|
|
225
|
+
* This is fired on a client when it receives the host's state, when it joins or after it calls
|
|
226
|
+
* {@link P2pLockStepMultiplayerController.requestStateSync}. Load the state, then call
|
|
227
|
+
* {@link P2pLockStepMultiplayerController.finishStateSync} before applying any later frame.
|
|
228
|
+
*
|
|
229
|
+
* @category Events
|
|
230
|
+
*/
|
|
231
|
+
export declare class MultiplayerControllerStateSyncEvent extends MultiplayerControllerStateSyncEvent_base {
|
|
232
|
+
}
|
|
175
233
|
/**
|
|
176
234
|
* All events emitted by this controller.
|
|
177
235
|
*
|
|
178
236
|
* @category Internal
|
|
179
237
|
*/
|
|
180
|
-
export type AllP2pLockStepMultiplayerControllerEvents<MultiplayerPacket extends JsonCompatibleValue> = MultiplayerControllerFrameEvent<MultiplayerPacket> | MultiplayerControllerDesyncEvent | MultiplayerControllerRoomListEvent | MultiplayerControllerClientStatusEvent | MultiplayerControllerConnectionEvent;
|
|
238
|
+
export type AllP2pLockStepMultiplayerControllerEvents<MultiplayerPacket extends JsonCompatibleValue> = MultiplayerControllerFrameEvent<MultiplayerPacket> | MultiplayerControllerDesyncEvent | MultiplayerControllerStateSyncEvent | MultiplayerControllerRoomListEvent | MultiplayerControllerClientStatusEvent | MultiplayerControllerConnectionEvent;
|
|
181
239
|
/**
|
|
182
240
|
* Listener callback for p2p-lock-step frame events.
|
|
183
241
|
*
|
|
@@ -197,11 +255,13 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
197
255
|
static readonly events: {
|
|
198
256
|
MultiplayerControllerDesyncEvent: typeof MultiplayerControllerDesyncEvent;
|
|
199
257
|
MultiplayerControllerFrameEvent: typeof MultiplayerControllerFrameEvent;
|
|
258
|
+
MultiplayerControllerStateSyncEvent: typeof MultiplayerControllerStateSyncEvent;
|
|
200
259
|
};
|
|
201
260
|
/** All events emitted by this controller. */
|
|
202
261
|
readonly events: {
|
|
203
262
|
MultiplayerControllerDesyncEvent: typeof MultiplayerControllerDesyncEvent;
|
|
204
263
|
MultiplayerControllerFrameEvent: typeof MultiplayerControllerFrameEvent;
|
|
264
|
+
MultiplayerControllerStateSyncEvent: typeof MultiplayerControllerStateSyncEvent;
|
|
205
265
|
};
|
|
206
266
|
static readonly knownErrors: {
|
|
207
267
|
RoomRejectionError: typeof RoomRejectionError;
|
|
@@ -228,6 +288,17 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
228
288
|
protected nextFrameStateHash: number | undefined;
|
|
229
289
|
/** On clients, this client's state hash from the most recent check frame. */
|
|
230
290
|
protected localStateHash: number | undefined;
|
|
291
|
+
/** On the host, clients whose state sync will be requested by the next frame. */
|
|
292
|
+
protected stateSyncRequestClientIds: ClientId[];
|
|
293
|
+
/** On the host, clients waiting for {@link P2pLockStepMultiplayerController.sendStateSync}. */
|
|
294
|
+
protected stateSyncFrameClientIds: ClientId[];
|
|
295
|
+
/**
|
|
296
|
+
* Whether this client is waiting for the host's state, after joining a room with
|
|
297
|
+
* `enableStateSync` set or after {@link P2pLockStepMultiplayerController.requestStateSync}.
|
|
298
|
+
* Frame events received while waiting should not be applied: the host's state already includes
|
|
299
|
+
* them. Desync checks are skipped while waiting.
|
|
300
|
+
*/
|
|
301
|
+
awaitingStateSync: boolean;
|
|
231
302
|
protected joiningRoom: boolean;
|
|
232
303
|
protected lastFpsCalculation: {
|
|
233
304
|
timestamp: number;
|
|
@@ -312,6 +383,22 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
312
383
|
* emits {@link MultiplayerControllerDesyncEvent} if they differ.
|
|
313
384
|
*/
|
|
314
385
|
checkStateHash(frameEvent: Readonly<MultiplayerControllerFrameEvent<MultiplayerPacket>>): void;
|
|
386
|
+
/**
|
|
387
|
+
* On clients, asks the host for its current state, which arrives as a
|
|
388
|
+
* {@link MultiplayerControllerStateSyncEvent}. Requires `enableStateSync`. Does nothing on the
|
|
389
|
+
* host or while already waiting.
|
|
390
|
+
*/
|
|
391
|
+
requestStateSync(): void;
|
|
392
|
+
/**
|
|
393
|
+
* Call on clients right after loading the state from a
|
|
394
|
+
* {@link MultiplayerControllerStateSyncEvent}, so that later frames are applied again.
|
|
395
|
+
*/
|
|
396
|
+
finishStateSync(): void;
|
|
397
|
+
/**
|
|
398
|
+
* Call on the host right after applying a frame event whose `shouldSendStateSync` is set. Sends
|
|
399
|
+
* the state to every client waiting for it, then resumes frame production.
|
|
400
|
+
*/
|
|
401
|
+
sendStateSync(stateSync: JsonCompatibleValue): void;
|
|
315
402
|
/** Detects if this controller is the room host or not. */
|
|
316
403
|
isHost(): boolean;
|
|
317
404
|
/** Detects if this controller is connected to a room or not. */
|
|
@@ -333,7 +420,10 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
333
420
|
protected attachMultiplayerRoomConnection(roomConnection: Readonly<MultiplayerRoomConnection<P2pLockStepMessage<MultiplayerPacket>>>): void;
|
|
334
421
|
/** Restart frame production if this client is promoted after losing its previous host. */
|
|
335
422
|
protected handleNewHost(clientId: ClientId): void;
|
|
336
|
-
/**
|
|
423
|
+
/**
|
|
424
|
+
* Send an empty frame to a newly connected member so it can join the frame flow, or queue a
|
|
425
|
+
* state sync for it when `enableStateSync` is set.
|
|
426
|
+
*/
|
|
337
427
|
protected syncNewMember(clientId: ClientId): void;
|
|
338
428
|
/**
|
|
339
429
|
* Per message type, whether only the host or only member clients handle it and how. Messages
|
|
@@ -355,6 +445,10 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
355
445
|
* {@link P2pLockStepMultiplayerController.messageHandlers}.
|
|
356
446
|
*/
|
|
357
447
|
protected handleReceivedMessage<Type extends P2pLockStepMessageType>(sourceClientId: ClientId, message: P2pLockStepMessageByType<MultiplayerPacket>[Type]): void;
|
|
448
|
+
/** On the host, queue a state sync for a client if it isn't already queued. */
|
|
449
|
+
protected queueStateSync(clientId: ClientId): void;
|
|
450
|
+
/** Forget pending state syncs, such as when frames restart under a new host or room. */
|
|
451
|
+
protected resetStateSync(): void;
|
|
358
452
|
/** Forget pending state hashes, such as when frames restart under a new host or room. */
|
|
359
453
|
protected resetDesyncCheck(): void;
|
|
360
454
|
/** Recalculate the current data-flow FPS from completed frames. */
|
|
@@ -11,6 +11,7 @@ export var P2pLockStepMessageType;
|
|
|
11
11
|
(function (P2pLockStepMessageType) {
|
|
12
12
|
P2pLockStepMessageType["Actions"] = "actions";
|
|
13
13
|
P2pLockStepMessageType["Frame"] = "frame";
|
|
14
|
+
P2pLockStepMessageType["StateSyncRequest"] = "state-sync-request";
|
|
14
15
|
})(P2pLockStepMessageType || (P2pLockStepMessageType = {}));
|
|
15
16
|
/**
|
|
16
17
|
* This is fired whenever a new p2p-lock-step frame is received from the host client.
|
|
@@ -31,6 +32,15 @@ export class MultiplayerControllerFrameEvent extends defineTypedCustomEvent()('m
|
|
|
31
32
|
*/
|
|
32
33
|
export class MultiplayerControllerDesyncEvent extends defineTypedCustomEvent()('multiplayer-controller-desync') {
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* This is fired on a client when it receives the host's state, when it joins or after it calls
|
|
37
|
+
* {@link P2pLockStepMultiplayerController.requestStateSync}. Load the state, then call
|
|
38
|
+
* {@link P2pLockStepMultiplayerController.finishStateSync} before applying any later frame.
|
|
39
|
+
*
|
|
40
|
+
* @category Events
|
|
41
|
+
*/
|
|
42
|
+
export class MultiplayerControllerStateSyncEvent extends defineTypedCustomEvent()('multiplayer-controller-state-sync') {
|
|
43
|
+
}
|
|
34
44
|
const defaultFrameDuration = {
|
|
35
45
|
milliseconds: 10,
|
|
36
46
|
};
|
|
@@ -47,6 +57,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
47
57
|
static events = {
|
|
48
58
|
MultiplayerControllerDesyncEvent,
|
|
49
59
|
MultiplayerControllerFrameEvent,
|
|
60
|
+
MultiplayerControllerStateSyncEvent,
|
|
50
61
|
};
|
|
51
62
|
/** All events emitted by this controller. */
|
|
52
63
|
events = P2pLockStepMultiplayerController.events;
|
|
@@ -73,6 +84,17 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
73
84
|
nextFrameStateHash;
|
|
74
85
|
/** On clients, this client's state hash from the most recent check frame. */
|
|
75
86
|
localStateHash;
|
|
87
|
+
/** On the host, clients whose state sync will be requested by the next frame. */
|
|
88
|
+
stateSyncRequestClientIds = [];
|
|
89
|
+
/** On the host, clients waiting for {@link P2pLockStepMultiplayerController.sendStateSync}. */
|
|
90
|
+
stateSyncFrameClientIds = [];
|
|
91
|
+
/**
|
|
92
|
+
* Whether this client is waiting for the host's state, after joining a room with
|
|
93
|
+
* `enableStateSync` set or after {@link P2pLockStepMultiplayerController.requestStateSync}.
|
|
94
|
+
* Frame events received while waiting should not be applied: the host's state already includes
|
|
95
|
+
* them. Desync checks are skipped while waiting.
|
|
96
|
+
*/
|
|
97
|
+
awaitingStateSync = false;
|
|
76
98
|
joiningRoom = false;
|
|
77
99
|
lastFpsCalculation = {
|
|
78
100
|
timestamp: 0,
|
|
@@ -183,6 +205,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
183
205
|
throw new Error('Cannot start singleplayer with a connection already present.');
|
|
184
206
|
}
|
|
185
207
|
this.debugLog('starting singleplayer connection');
|
|
208
|
+
this.resetStateSync();
|
|
186
209
|
this.singleplayer = true;
|
|
187
210
|
this.finishFrame();
|
|
188
211
|
this.dispatch(new MultiplayerControllerConnectionEvent({
|
|
@@ -236,7 +259,10 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
236
259
|
* `undefined` when there's no state to hash yet, which skips this check.
|
|
237
260
|
*/
|
|
238
261
|
reportStateHash({ frameEvent, stateHash, }) {
|
|
239
|
-
if (
|
|
262
|
+
if (this.awaitingStateSync) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
else if (!this.isHost()) {
|
|
240
266
|
this.localStateHash = stateHash;
|
|
241
267
|
}
|
|
242
268
|
else if (frameEvent === this.latestCheckFrameEvent) {
|
|
@@ -250,7 +276,8 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
250
276
|
* emits {@link MultiplayerControllerDesyncEvent} if they differ.
|
|
251
277
|
*/
|
|
252
278
|
checkStateHash(frameEvent) {
|
|
253
|
-
if (
|
|
279
|
+
if (this.awaitingStateSync ||
|
|
280
|
+
frameEvent.detail.hostStateHash == undefined ||
|
|
254
281
|
this.localStateHash == undefined ||
|
|
255
282
|
frameEvent.detail.hostStateHash === this.localStateHash) {
|
|
256
283
|
return;
|
|
@@ -263,6 +290,55 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
263
290
|
this.dispatch(new MultiplayerControllerDesyncEvent({
|
|
264
291
|
detail: desync,
|
|
265
292
|
}));
|
|
293
|
+
if (this.params.resyncOnDesync) {
|
|
294
|
+
this.requestStateSync();
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* On clients, asks the host for its current state, which arrives as a
|
|
299
|
+
* {@link MultiplayerControllerStateSyncEvent}. Requires `enableStateSync`. Does nothing on the
|
|
300
|
+
* host or while already waiting.
|
|
301
|
+
*/
|
|
302
|
+
requestStateSync() {
|
|
303
|
+
if (!this.params.enableStateSync || this.isHost() || this.awaitingStateSync) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
this.debugLog('requesting state sync from host');
|
|
307
|
+
this.awaitingStateSync = true;
|
|
308
|
+
this.localStateHash = undefined;
|
|
309
|
+
this.roomConnection?.sendMessage({
|
|
310
|
+
type: P2pLockStepMessageType.StateSyncRequest,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Call on clients right after loading the state from a
|
|
315
|
+
* {@link MultiplayerControllerStateSyncEvent}, so that later frames are applied again.
|
|
316
|
+
*/
|
|
317
|
+
finishStateSync() {
|
|
318
|
+
this.awaitingStateSync = false;
|
|
319
|
+
this.localStateHash = undefined;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Call on the host right after applying a frame event whose `shouldSendStateSync` is set. Sends
|
|
323
|
+
* the state to every client waiting for it, then resumes frame production.
|
|
324
|
+
*/
|
|
325
|
+
sendStateSync(stateSync) {
|
|
326
|
+
const connectedClientIds = this.getConnectedClientIds();
|
|
327
|
+
this.stateSyncFrameClientIds
|
|
328
|
+
.filter((clientId) => {
|
|
329
|
+
return connectedClientIds.includes(clientId);
|
|
330
|
+
})
|
|
331
|
+
.forEach((clientId) => {
|
|
332
|
+
this.debugLog(`sending state sync to ${clientId}`);
|
|
333
|
+
this.roomConnection?.sendToOnlyOneClient(clientId, {
|
|
334
|
+
type: P2pLockStepMessageType.Frame,
|
|
335
|
+
packets: [],
|
|
336
|
+
isSynchronizationFrame: true,
|
|
337
|
+
stateSync,
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
this.stateSyncFrameClientIds = [];
|
|
341
|
+
this.maybeFinishFrame();
|
|
266
342
|
}
|
|
267
343
|
/** Detects if this controller is the room host or not. */
|
|
268
344
|
isHost() {
|
|
@@ -293,6 +369,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
293
369
|
room,
|
|
294
370
|
});
|
|
295
371
|
this.resetDesyncCheck();
|
|
372
|
+
this.resetStateSync();
|
|
296
373
|
if (previousRoomConnection) {
|
|
297
374
|
globalThis.clearTimeout(this.timeoutId);
|
|
298
375
|
this.clientsResponded = {};
|
|
@@ -304,6 +381,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
304
381
|
this.frameTickReady = true;
|
|
305
382
|
}
|
|
306
383
|
this.singleplayer = false;
|
|
384
|
+
this.awaitingStateSync = !!this.params.enableStateSync && !roomConnection.isHost();
|
|
307
385
|
this.attachMultiplayerRoomConnection(roomConnection);
|
|
308
386
|
this.debugLog(`attached p2p-lock-step connection; client=${this.getClientId() || 'unknown'} host=${this.isHost()} connected=${this.isConnected()}`);
|
|
309
387
|
}
|
|
@@ -335,6 +413,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
335
413
|
}
|
|
336
414
|
this.debugLog(`leaving room '${this.roomId || 'unknown'}'`);
|
|
337
415
|
globalThis.clearTimeout(this.timeoutId);
|
|
416
|
+
this.resetStateSync();
|
|
338
417
|
this.roomConnection = undefined;
|
|
339
418
|
this.singleplayer = false;
|
|
340
419
|
this.roomController.leaveRoom();
|
|
@@ -390,13 +469,20 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
390
469
|
globalThis.clearTimeout(this.timeoutId);
|
|
391
470
|
this.clientsResponded = {};
|
|
392
471
|
this.resetDesyncCheck();
|
|
472
|
+
this.resetStateSync();
|
|
393
473
|
this.frameTickReady = true;
|
|
394
474
|
this.finishFrame();
|
|
395
475
|
}
|
|
396
|
-
/**
|
|
476
|
+
/**
|
|
477
|
+
* Send an empty frame to a newly connected member so it can join the frame flow, or queue a
|
|
478
|
+
* state sync for it when `enableStateSync` is set.
|
|
479
|
+
*/
|
|
397
480
|
syncNewMember(clientId) {
|
|
398
481
|
this.debugLog(`syncNewMember called for ${clientId}; host=${this.isHost()}`);
|
|
399
|
-
if (this.
|
|
482
|
+
if (this.params.enableStateSync) {
|
|
483
|
+
this.queueStateSync(clientId);
|
|
484
|
+
}
|
|
485
|
+
else if (this.roomConnection && this.isHost()) {
|
|
400
486
|
this.roomConnection.sendToOnlyOneClient(clientId, {
|
|
401
487
|
type: P2pLockStepMessageType.Frame,
|
|
402
488
|
packets: [],
|
|
@@ -442,7 +528,14 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
442
528
|
sourceClientId: this.clientId,
|
|
443
529
|
type: P2pLockStepMessageType.Actions,
|
|
444
530
|
});
|
|
445
|
-
if (
|
|
531
|
+
if (message.stateSync !== undefined) {
|
|
532
|
+
this.dispatch(new MultiplayerControllerStateSyncEvent({
|
|
533
|
+
detail: {
|
|
534
|
+
stateSync: message.stateSync,
|
|
535
|
+
},
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
else if (!message.isSynchronizationFrame) {
|
|
446
539
|
this.calculateFps();
|
|
447
540
|
this.dispatch(new MultiplayerControllerFrameEvent({
|
|
448
541
|
detail: {
|
|
@@ -454,6 +547,12 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
454
547
|
}
|
|
455
548
|
},
|
|
456
549
|
},
|
|
550
|
+
[P2pLockStepMessageType.StateSyncRequest]: {
|
|
551
|
+
isForHost: true,
|
|
552
|
+
handle: ({ sourceClientId }) => {
|
|
553
|
+
this.queueStateSync(sourceClientId);
|
|
554
|
+
},
|
|
555
|
+
},
|
|
457
556
|
};
|
|
458
557
|
/**
|
|
459
558
|
* Route a received message to its handler in
|
|
@@ -474,6 +573,25 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
474
573
|
});
|
|
475
574
|
}
|
|
476
575
|
}
|
|
576
|
+
/** On the host, queue a state sync for a client if it isn't already queued. */
|
|
577
|
+
queueStateSync(clientId) {
|
|
578
|
+
if (!this.isHost() ||
|
|
579
|
+
this.stateSyncRequestClientIds.includes(clientId) ||
|
|
580
|
+
this.stateSyncFrameClientIds.includes(clientId)) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
this.debugLog(`queueing state sync for ${clientId}`);
|
|
584
|
+
this.stateSyncRequestClientIds = [
|
|
585
|
+
...this.stateSyncRequestClientIds,
|
|
586
|
+
clientId,
|
|
587
|
+
];
|
|
588
|
+
}
|
|
589
|
+
/** Forget pending state syncs, such as when frames restart under a new host or room. */
|
|
590
|
+
resetStateSync() {
|
|
591
|
+
this.stateSyncRequestClientIds = [];
|
|
592
|
+
this.stateSyncFrameClientIds = [];
|
|
593
|
+
this.awaitingStateSync = false;
|
|
594
|
+
}
|
|
477
595
|
/** Forget pending state hashes, such as when frames restart under a new host or room. */
|
|
478
596
|
resetDesyncCheck() {
|
|
479
597
|
this.latestCheckFrameEvent = undefined;
|
|
@@ -508,6 +626,12 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
508
626
|
const shouldReportNextFrameHash = !!this.desyncCheckFrameInterval &&
|
|
509
627
|
!(this.producedFrameCount % this.desyncCheckFrameInterval) &&
|
|
510
628
|
this.getAllClientIds().length > 1;
|
|
629
|
+
const shouldSendStateSync = !!this.stateSyncRequestClientIds.length;
|
|
630
|
+
this.stateSyncFrameClientIds = [
|
|
631
|
+
...this.stateSyncFrameClientIds,
|
|
632
|
+
...this.stateSyncRequestClientIds,
|
|
633
|
+
];
|
|
634
|
+
this.stateSyncRequestClientIds = [];
|
|
511
635
|
this.roomConnection?.sendMessage({
|
|
512
636
|
type: P2pLockStepMessageType.Frame,
|
|
513
637
|
packets: currentFrameActions,
|
|
@@ -522,6 +646,9 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
522
646
|
detail: {
|
|
523
647
|
packets: currentFrameActions,
|
|
524
648
|
shouldReportNextFrameHash,
|
|
649
|
+
...(shouldSendStateSync && {
|
|
650
|
+
shouldSendStateSync,
|
|
651
|
+
}),
|
|
525
652
|
},
|
|
526
653
|
});
|
|
527
654
|
if (shouldReportNextFrameHash) {
|
|
@@ -547,8 +674,8 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
547
674
|
this.roomConnection?.getConnectedClientIds().every((clientId) => {
|
|
548
675
|
return this.clientsResponded[clientId];
|
|
549
676
|
});
|
|
550
|
-
if (!this.frameTickReady || !clientsReady) {
|
|
551
|
-
this.debugLog(`maybeFinishFrame waiting: frameTickReady=${this.frameTickReady} clientsReady=${!!clientsReady}`);
|
|
677
|
+
if (!this.frameTickReady || !clientsReady || this.stateSyncFrameClientIds.length) {
|
|
678
|
+
this.debugLog(`maybeFinishFrame waiting: frameTickReady=${this.frameTickReady} clientsReady=${!!clientsReady} stateSyncs=${this.stateSyncFrameClientIds.length}`);
|
|
552
679
|
return;
|
|
553
680
|
}
|
|
554
681
|
this.finishFrame();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antha/multiplayer-p2p-lock-step",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Multiplayer mod for the Antha engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vir",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"typed-event-target": "^4.3.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@antha/multiplayer-core": "^0.
|
|
45
|
+
"@antha/multiplayer-core": "^0.22.0",
|
|
46
46
|
"@augment-vir/test": "^32.3.0",
|
|
47
47
|
"@web/dev-server-esbuild": "^2.0.0",
|
|
48
48
|
"@web/test-runner": "^1.0.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"istanbul-smart-text-reporter": "^1.1.5"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@antha/multiplayer-core": "^0.
|
|
53
|
+
"@antha/multiplayer-core": "^0.22.0"
|
|
54
54
|
},
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=22"
|