@antha/multiplayer-p2p-lock-step 0.17.0 → 0.19.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.
@@ -1,6 +1,7 @@
1
- import { type ApiAndRoomConnectionState } from '@antha/multiplayer-core';
2
- import { type JsonCompatibleValue, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
3
- import { P2pLockStepMultiplayerController, type P2pLockStepMultiplayerControllerParams } from './p2p-lock-step-multiplayer-controller.js';
1
+ import { type ModExecuteParams } from '@antha/engine';
2
+ import { type ApiAndRoomConnectionState, MultiplayerControllerClientStatusEvent } from '@antha/multiplayer-core';
3
+ import { type JsonCompatibleValue, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
4
+ import { MultiplayerControllerFrameEvent, type MultiplayerFramePacket, P2pLockStepMultiplayerController, type P2pLockStepMultiplayerControllerParams } from './p2p-lock-step-multiplayer-controller.js';
4
5
  /**
5
6
  * Engine state added by the p2p-lock-step multiplayer mod.
6
7
  *
@@ -9,6 +10,8 @@ import { P2pLockStepMultiplayerController, type P2pLockStepMultiplayerController
9
10
  export type AnthaMultiplayerP2pLockStepState<MultiplayerPacket extends JsonCompatibleValue = any> = {
10
11
  /** Enables verbose multiplayer debug logs. */
11
12
  debugMultiplayer?: boolean | undefined;
13
+ /** Number of completed lock-step frame updates. */
14
+ multiplayerLockstepTick: number;
12
15
  /** P2p-lock-step controller state. */
13
16
  multiplayerP2pLockStep: {
14
17
  /** Multiplayer controller used to drive singleplayer or multiplayer frame sync. */
@@ -30,14 +33,34 @@ export declare function isMultiplayerRoomConnected({ multiplayerP2pLockStep, }:
30
33
  *
31
34
  * @category Internal
32
35
  */
33
- export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCompatibleValue = any> = PartialWithUndefined<SelectFrom<P2pLockStepMultiplayerControllerParams<MultiplayerPacket>, {
36
+ export type AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket>> = PartialWithUndefined<SelectFrom<P2pLockStepMultiplayerControllerParams<MultiplayerPacket>, {
34
37
  acceptConnection: true;
35
38
  debugMultiplayer: true;
39
+ frameDuration: true;
36
40
  gameId: true;
37
- }>>;
41
+ }>> & PartialWithUndefined<{
42
+ /** Applies an individual action from within a frame event. */
43
+ handlePacket: (params: Readonly<{
44
+ packet: Readonly<MultiplayerFramePacket<MultiplayerPacket>>;
45
+ multiplayerController: P2pLockStepMultiplayerController<MultiplayerPacket>;
46
+ state: Partial<State>;
47
+ }>) => MaybePromise<void>;
48
+ /** Called after all actions in a frame have been applied, in singleplayer and multiplayer. */
49
+ runFrameUpdate: (params: Readonly<{
50
+ multiplayerFrameEvent: MultiplayerControllerFrameEvent<MultiplayerPacket>;
51
+ } & ModExecuteParams<State>>) => MaybePromise<void>;
52
+ /** Handles a client or host status change. */
53
+ handleClientStatus: (params: Readonly<{
54
+ event: Readonly<MultiplayerControllerClientStatusEvent>;
55
+ multiplayerController: P2pLockStepMultiplayerController<MultiplayerPacket>;
56
+ state: Partial<State>;
57
+ }>) => MaybePromise<void>;
58
+ }>;
38
59
  /**
39
- * Create the engine mod that owns p2p-lock-step multiplayer state.
60
+ * Create the engine mod that owns p2p-lock-step multiplayer state. When `handlePacket` and
61
+ * `runFrameUpdate` are provided, the mod also applies every lock-step frame serially, preserving
62
+ * every frame when the engine batches multiple frame events into one tick.
40
63
  *
41
64
  * @category Main
42
65
  */
43
- export declare function createAnthaMultiplayerP2pLockStepMod<const MultiplayerPacket extends JsonCompatibleValue = any>(options?: Readonly<AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket>>): import("@antha/engine").AnthaMod<NoInfer<AnthaMultiplayerP2pLockStepState<NoInfer<MultiplayerPacket>>>>;
66
+ export declare function createAnthaMultiplayerP2pLockStepMod<const MultiplayerPacket extends JsonCompatibleValue = any, State extends AnthaMultiplayerP2pLockStepState<MultiplayerPacket> = AnthaMultiplayerP2pLockStepState<MultiplayerPacket>>(options?: Readonly<AnthaMultiplayerP2pLockStepOptions<MultiplayerPacket, NoInfer<State>>>): import("@antha/engine").AnthaMod<NoInfer<State>>;
@@ -1,6 +1,6 @@
1
- import { defineAnthaMod } from '@antha/engine';
2
- import { emptyApiAndRoomConnectionState, MultiplayerControllerConnectionEvent, } from '@antha/multiplayer-core';
3
- import { log, } from '@augment-vir/common';
1
+ import { defineAnthaMod, ModExecutionTriggerType } from '@antha/engine';
2
+ import { emptyApiAndRoomConnectionState, MultiplayerControllerClientStatusEvent, MultiplayerControllerConnectionEvent, } from '@antha/multiplayer-core';
3
+ import { awaitedBlockingMap, log, } from '@augment-vir/common';
4
4
  import { MultiplayerControllerFrameEvent, P2pLockStepMultiplayerController, } from './p2p-lock-step-multiplayer-controller.js';
5
5
  /**
6
6
  * Indicates whether a p2p-lock-step multiplayer room is currently connected.
@@ -11,17 +11,38 @@ export function isMultiplayerRoomConnected({ multiplayerP2pLockStep, }) {
11
11
  return !!multiplayerP2pLockStep?.multiplayerController.roomId;
12
12
  }
13
13
  /**
14
- * Create the engine mod that owns p2p-lock-step multiplayer state.
14
+ * Create the engine mod that owns p2p-lock-step multiplayer state. When `handlePacket` and
15
+ * `runFrameUpdate` are provided, the mod also applies every lock-step frame serially, preserving
16
+ * every frame when the engine batches multiple frame events into one tick.
15
17
  *
16
18
  * @category Main
17
19
  */
18
20
  export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
19
21
  return defineAnthaMod({
20
22
  modName: 'antha-multiplayer-p2p-lock-step',
21
- execute({ engine, state }) {
22
- if (options.debugMultiplayer == undefined) {
23
- state.debugMultiplayer = options.debugMultiplayer;
23
+ initState: {
24
+ debugMultiplayer: options.debugMultiplayer,
25
+ multiplayerLockstepTick: 0,
26
+ },
27
+ trigger: options.handlePacket || options.runFrameUpdate || options.handleClientStatus
28
+ ? {
29
+ event: [
30
+ ...(options.handlePacket || options.runFrameUpdate
31
+ ? [MultiplayerControllerFrameEvent]
32
+ : []),
33
+ ...(options.handleClientStatus
34
+ ? [MultiplayerControllerClientStatusEvent]
35
+ : []),
36
+ ],
37
+ executeImmediately: true,
24
38
  }
39
+ : undefined,
40
+ cleanup({ state }) {
41
+ log.if(!!state.debugMultiplayer).faint('[multiplayer] cleaning up p2p-lock-step mod');
42
+ state.multiplayerP2pLockStep?.multiplayerController.destroy();
43
+ },
44
+ async execute(executeParams) {
45
+ const { engine, state } = executeParams;
25
46
  if (!state.multiplayerP2pLockStep) {
26
47
  log.if(!!state.debugMultiplayer).faint('[multiplayer] creating p2p-lock-step mod state');
27
48
  state.multiplayerP2pLockStep = {
@@ -29,7 +50,7 @@ export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
29
50
  gameId: options.gameId || 'antha',
30
51
  acceptConnection: options.acceptConnection,
31
52
  debugMultiplayer: state.debugMultiplayer,
32
- frameDuration: undefined,
53
+ frameDuration: options.frameDuration,
33
54
  }),
34
55
  connectionState: emptyApiAndRoomConnectionState,
35
56
  };
@@ -40,16 +61,58 @@ export function createAnthaMultiplayerP2pLockStepMod(options = {}) {
40
61
  log.if(!!state.debugMultiplayer).faint(`[multiplayer] mod connection state updated: api=${String(newConnectionState.api)} room=${String(newConnectionState.room)}`);
41
62
  state.multiplayerP2pLockStep.connectionState = newConnectionState;
42
63
  });
43
- state.multiplayerP2pLockStep.multiplayerController.listen(MultiplayerControllerFrameEvent, ({ detail }) => {
44
- engine.dispatch(new MultiplayerControllerFrameEvent({
45
- detail,
46
- }));
64
+ state.multiplayerP2pLockStep.multiplayerController.listenToAll((event) => {
65
+ engine.dispatch(event);
66
+ });
67
+ }
68
+ if (executeParams.executionTrigger.type === ModExecutionTriggerType.Event) {
69
+ await awaitedBlockingMap(executeParams.executionTrigger.events, async (event) => {
70
+ if (!state.multiplayerP2pLockStep?.multiplayerController) {
71
+ return;
72
+ }
73
+ else if (event instanceof MultiplayerControllerClientStatusEvent) {
74
+ await options.handleClientStatus?.({
75
+ event,
76
+ multiplayerController: state.multiplayerP2pLockStep.multiplayerController,
77
+ state,
78
+ });
79
+ return;
80
+ }
81
+ else if (event instanceof MultiplayerControllerFrameEvent &&
82
+ (options.handlePacket || options.runFrameUpdate)) {
83
+ if (options.handlePacket) {
84
+ for (const detail of event.detail) {
85
+ await options.handlePacket({
86
+ packet: detail,
87
+ multiplayerController: state.multiplayerP2pLockStep.multiplayerController,
88
+ state,
89
+ });
90
+ }
91
+ }
92
+ state.multiplayerLockstepTick = (state.multiplayerLockstepTick || 0) + 1;
93
+ await options.runFrameUpdate?.({
94
+ ...executeParams,
95
+ currentTick: state.multiplayerLockstepTick,
96
+ executionTrigger: {
97
+ events: [
98
+ event,
99
+ ],
100
+ type: ModExecutionTriggerType.Event,
101
+ },
102
+ /**
103
+ * This is a fixed duration so that clients don't get out of sync with
104
+ * each other. If a client is ever executing multiple events per
105
+ * execution, that means they got behind somehow, so we should still
106
+ * keep this duration fixed so that they run the same calculations as
107
+ * everyone else while catching up.
108
+ */
109
+ msSinceLastExecute: state.multiplayerP2pLockStep.multiplayerController.frameMs,
110
+ multiplayerFrameEvent: event,
111
+ ticksSinceLastExecute: 1,
112
+ });
113
+ }
47
114
  });
48
115
  }
49
- },
50
- cleanup({ state }) {
51
- log.if(!!state.debugMultiplayer).faint('[multiplayer] cleaning up p2p-lock-step mod');
52
- state.multiplayerP2pLockStep?.multiplayerController.destroy();
53
116
  },
54
117
  });
55
118
  }
@@ -1,4 +1,4 @@
1
- import { type ApiAndRoomConnectionState, type ClientId, MultiplayerControllerClientEvent, MultiplayerControllerConnectionEvent, MultiplayerControllerRoomListEvent, type MultiplayerControllerRoomListListener, type MultiplayerInitParams, type MultiplayerRoomConnection, MultiplayerRoomController, type RoomInput, RoomRejectionError } from '@antha/multiplayer-core';
1
+ import { type ApiAndRoomConnectionState, type ClientId, MultiplayerControllerClientStatusEvent, MultiplayerControllerConnectionEvent, MultiplayerControllerRoomListEvent, type MultiplayerControllerRoomListListener, type MultiplayerInitParams, type MultiplayerRoomConnection, MultiplayerRoomController, type RoomInput, RoomRejectionError } from '@antha/multiplayer-core';
2
2
  import { type JsonCompatibleValue, type MaybePromise, type PartialWithUndefined } from '@augment-vir/common';
3
3
  import { type AnyDuration } from 'date-vir';
4
4
  import { ListenTarget, type RemoveListenerCallback, type TypedCustomEventInit } from 'typed-event-target';
@@ -16,7 +16,7 @@ export declare enum P2pLockStepMessageType {
16
16
  *
17
17
  * @category Internal
18
18
  */
19
- export type FrameEventDetail<MultiplayerPacket extends JsonCompatibleValue> = {
19
+ export type MultiplayerFramePacket<MultiplayerPacket extends JsonCompatibleValue> = {
20
20
  packet: MultiplayerPacket;
21
21
  sourceClientId: ClientId;
22
22
  };
@@ -35,7 +35,7 @@ export type P2pLockStepMessage<MultiplayerPacket extends JsonCompatibleValue> =
35
35
  /** Sent from the host to clients when a frame is ready. */
36
36
  | ({
37
37
  type: P2pLockStepMessageType.Frame;
38
- actions: FrameEventDetail<MultiplayerPacket>[];
38
+ packets: MultiplayerFramePacket<MultiplayerPacket>[];
39
39
  } & PartialWithUndefined<{
40
40
  /** Whether this frame is meant for syncing a new client. */
41
41
  isSynchronizationFrame: boolean;
@@ -89,15 +89,15 @@ declare const MultiplayerControllerFrameEvent_base: (new (eventInitDict: {
89
89
  * @category Events
90
90
  */
91
91
  export declare class MultiplayerControllerFrameEvent<MultiplayerPacket extends JsonCompatibleValue> extends MultiplayerControllerFrameEvent_base {
92
- detail: ReadonlyArray<FrameEventDetail<MultiplayerPacket>>;
93
- constructor(eventInitDict: TypedCustomEventInit<ReadonlyArray<FrameEventDetail<MultiplayerPacket>>>);
92
+ detail: ReadonlyArray<MultiplayerFramePacket<MultiplayerPacket>>;
93
+ constructor(eventInitDict: TypedCustomEventInit<ReadonlyArray<MultiplayerFramePacket<MultiplayerPacket>>>);
94
94
  }
95
95
  /**
96
96
  * All events emitted by this controller.
97
97
  *
98
98
  * @category Internal
99
99
  */
100
- export type AllP2pLockStepMultiplayerControllerEvents<MultiplayerPacket extends JsonCompatibleValue> = MultiplayerControllerFrameEvent<MultiplayerPacket> | MultiplayerControllerRoomListEvent | MultiplayerControllerClientEvent | MultiplayerControllerConnectionEvent;
100
+ export type AllP2pLockStepMultiplayerControllerEvents<MultiplayerPacket extends JsonCompatibleValue> = MultiplayerControllerFrameEvent<MultiplayerPacket> | MultiplayerControllerRoomListEvent | MultiplayerControllerClientStatusEvent | MultiplayerControllerConnectionEvent;
101
101
  /**
102
102
  * Listener callback for p2p-lock-step frame events.
103
103
  *
@@ -132,10 +132,10 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
132
132
  protected readonly localClientId: ClientId;
133
133
  protected roomConnection: MultiplayerRoomConnection<P2pLockStepMessage<MultiplayerPacket>> | undefined;
134
134
  protected clientsResponded: Record<ClientId, boolean>;
135
- protected frameActions: FrameEventDetail<MultiplayerPacket>[];
135
+ protected frameActions: MultiplayerFramePacket<MultiplayerPacket>[];
136
136
  protected timeoutId: ReturnType<typeof globalThis.setTimeout> | undefined;
137
137
  protected frameTickReady: boolean;
138
- protected frameMs: number | undefined;
138
+ frameMs: number;
139
139
  protected joiningRoom: boolean;
140
140
  protected lastFpsCalculation: {
141
141
  timestamp: number;
@@ -1,4 +1,4 @@
1
- import { createMultiplayerId, emptyApiAndRoomConnectionState, MultiplayerConnectionState, MultiplayerControllerClientEvent, MultiplayerControllerConnectionEvent, MultiplayerControllerMessageEvent, MultiplayerControllerRoomListEvent, MultiplayerRoomController, RoomRejectionError, } from '@antha/multiplayer-core';
1
+ import { createMultiplayerId, emptyApiAndRoomConnectionState, MultiplayerConnectionState, MultiplayerControllerClientStatusEvent, MultiplayerControllerConnectionEvent, MultiplayerControllerMessageEvent, MultiplayerControllerRoomListEvent, MultiplayerRoomController, RoomRejectionError, } from '@antha/multiplayer-core';
2
2
  import { ensureArray, log, makeWritable, } from '@augment-vir/common';
3
3
  import { convertDuration } from 'date-vir';
4
4
  import { defineTypedCustomEvent, ListenTarget, } from 'typed-event-target';
@@ -174,9 +174,6 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
174
174
  */
175
175
  runFrame(actions) {
176
176
  this.debugLog(`runFrame called with ${actions?.length || 0} actions`);
177
- if (this.frameMs != undefined) {
178
- throw new Error('Cannot manually run frame when frameDuration has been set.');
179
- }
180
177
  if (actions) {
181
178
  this.act(actions);
182
179
  }
@@ -289,7 +286,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
289
286
  this.debugLog(`connection event received: api=${String(event.detail.api)} room=${String(event.detail.room)}`);
290
287
  this.dispatch(event);
291
288
  });
292
- this.roomController.listen(MultiplayerControllerClientEvent, (event) => {
289
+ this.roomController.listen(MultiplayerControllerClientStatusEvent, (event) => {
293
290
  this.debugLog(`client event received: ${JSON.stringify(event.detail)}`);
294
291
  if ('newMember' in event.detail) {
295
292
  this.syncNewMember(event.detail.newMember);
@@ -339,7 +336,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
339
336
  if (this.roomConnection && this.isHost()) {
340
337
  this.roomConnection.sendToOnlyOneClient(clientId, {
341
338
  type: P2pLockStepMessageType.Frame,
342
- actions: [],
339
+ packets: [],
343
340
  isSynchronizationFrame: true,
344
341
  });
345
342
  }
@@ -369,7 +366,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
369
366
  this.maybeFinishFrame();
370
367
  }
371
368
  else if (!this.isHost() && message.type === P2pLockStepMessageType.Frame) {
372
- this.debugLog(`member received frame with ${message.actions.length} actions; sending ${this.frameActions.length} local actions back to host`);
369
+ this.debugLog(`member received frame with ${message.packets.length} actions; sending ${this.frameActions.length} local actions back to host`);
373
370
  const currentFrameActions = this.frameActions;
374
371
  this.frameActions = [];
375
372
  this.roomConnection.sendMessage({
@@ -382,7 +379,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
382
379
  if (!message.isSynchronizationFrame) {
383
380
  this.calculateFps();
384
381
  this.dispatch(new MultiplayerControllerFrameEvent({
385
- detail: message.actions,
382
+ detail: message.packets,
386
383
  }));
387
384
  }
388
385
  }
@@ -411,7 +408,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
411
408
  this.frameActions = [];
412
409
  this.roomConnection?.sendMessage({
413
410
  type: P2pLockStepMessageType.Frame,
414
- actions: currentFrameActions,
411
+ packets: currentFrameActions,
415
412
  });
416
413
  this.dispatch(new MultiplayerControllerFrameEvent({
417
414
  detail: currentFrameActions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antha/multiplayer-p2p-lock-step",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Multiplayer mod for the Antha engine.",
5
5
  "keywords": [
6
6
  "vir",
@@ -42,6 +42,7 @@
42
42
  "typed-event-target": "^4.3.3"
43
43
  },
44
44
  "devDependencies": {
45
+ "@antha/multiplayer-core": "^0.19.0",
45
46
  "@augment-vir/test": "^32.3.0",
46
47
  "@web/dev-server-esbuild": "^2.0.0",
47
48
  "@web/test-runner": "^1.0.0",
@@ -49,7 +50,7 @@
49
50
  "istanbul-smart-text-reporter": "^1.1.5"
50
51
  },
51
52
  "peerDependencies": {
52
- "@antha/multiplayer-core": "^0.17.0"
53
+ "@antha/multiplayer-core": "^0.19.0"
53
54
  },
54
55
  "engines": {
55
56
  "node": ">=22"