@antha/multiplayer-p2p-lock-step 0.0.8 → 0.1.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/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
1
|
# @antha/multiplayer-p2p-lock-step
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This package provides a pre-built [Antha game engine](https://www.npmjs.com/package/@antha/engine) mod for peer-to-peer lock-step multiplayer state synchronization. Players submit actions, and the host distributes them as ordered frames so every client advances game state in the same order.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm i @antha/multiplayer-p2p-lock-step
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
<!-- example-link: src/readme-examples/creating-lock-step-game.example.ts -->
|
|
14
|
+
|
|
15
|
+
```TypeScript
|
|
16
|
+
import {AnthaEngine, defineAnthaMod} from '@antha/engine';
|
|
17
|
+
import {
|
|
18
|
+
type AnthaMultiplayerP2pLockStepState,
|
|
19
|
+
createAnthaMultiplayerP2pLockStepMod,
|
|
20
|
+
} from '@antha/multiplayer-p2p-lock-step';
|
|
21
|
+
|
|
22
|
+
type GameState = AnthaMultiplayerP2pLockStepState<string>;
|
|
23
|
+
|
|
24
|
+
const engine = new AnthaEngine<GameState>({
|
|
25
|
+
mods: [
|
|
26
|
+
createAnthaMultiplayerP2pLockStepMod<string>({
|
|
27
|
+
gameId: 'my-game',
|
|
28
|
+
}),
|
|
29
|
+
defineAnthaMod<GameState>({
|
|
30
|
+
modName: 'game-logic',
|
|
31
|
+
execute({state}) {
|
|
32
|
+
const controller = state.multiplayerP2pLockStep?.multiplayerController;
|
|
33
|
+
|
|
34
|
+
if (!controller) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (!controller.isConnected()) {
|
|
38
|
+
controller.startSingleplayer();
|
|
39
|
+
controller.act('move-left');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return `Network FPS: ${controller.getFps()}`;
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
engine.startLoop();
|
|
49
|
+
```
|
|
@@ -133,6 +133,7 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
133
133
|
protected timeoutId: ReturnType<typeof globalThis.setTimeout> | undefined;
|
|
134
134
|
protected frameTickReady: boolean;
|
|
135
135
|
protected frameMs: number | undefined;
|
|
136
|
+
protected joiningRoom: boolean;
|
|
136
137
|
protected lastFpsCalculation: {
|
|
137
138
|
timestamp: number;
|
|
138
139
|
frameCount: number;
|
|
@@ -186,9 +187,9 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
186
187
|
* - For non-host clients, this includes the member client and the host client once connected.
|
|
187
188
|
*/
|
|
188
189
|
getAllClientIds(): ClientId[];
|
|
189
|
-
/**
|
|
190
|
+
/** Initialize multiplayer API access without opening a room or starting host pings. */
|
|
190
191
|
initMultiplayer(params: Readonly<MultiplayerInitParams>): Promise<void>;
|
|
191
|
-
/** Start
|
|
192
|
+
/** Start local play without contacting the multiplayer API. This can later open into a room. */
|
|
192
193
|
startSingleplayer(): void;
|
|
193
194
|
/**
|
|
194
195
|
* Manually run the next frame.
|
|
@@ -219,6 +220,8 @@ export declare class P2pLockStepMultiplayerController<MultiplayerPacket extends
|
|
|
219
220
|
protected listenToRoomController(): void;
|
|
220
221
|
/** Attach an established room transport and publish initial frame readiness. */
|
|
221
222
|
protected attachMultiplayerRoomConnection(roomConnection: Readonly<MultiplayerRoomConnection<P2pLockStepMessage<MultiplayerPacket>>>): void;
|
|
223
|
+
/** Restart frame production if this client is promoted after losing its previous host. */
|
|
224
|
+
protected handleNewHost(clientId: ClientId): void;
|
|
222
225
|
/** Send an empty frame to a newly connected member so it can join the frame flow. */
|
|
223
226
|
protected syncNewMember(clientId: ClientId): void;
|
|
224
227
|
/** Apply received member actions on the host or received frames on member clients. */
|
|
@@ -53,6 +53,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
53
53
|
timeoutId;
|
|
54
54
|
frameTickReady = true;
|
|
55
55
|
frameMs;
|
|
56
|
+
joiningRoom = false;
|
|
56
57
|
lastFpsCalculation = {
|
|
57
58
|
timestamp: 0,
|
|
58
59
|
frameCount: 0,
|
|
@@ -68,6 +69,7 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
68
69
|
}).milliseconds;
|
|
69
70
|
this.roomController = new MultiplayerRoomController({
|
|
70
71
|
gameId: params.gameId,
|
|
72
|
+
clientId: this.localClientId,
|
|
71
73
|
acceptConnection: params.acceptConnection
|
|
72
74
|
? (connectingClientId) => {
|
|
73
75
|
this.debugLog(`checking incoming connection from ${connectingClientId}`);
|
|
@@ -143,13 +145,13 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
143
145
|
}
|
|
144
146
|
return this.roomConnection?.getAllClientIds() || [];
|
|
145
147
|
}
|
|
146
|
-
/**
|
|
148
|
+
/** Initialize multiplayer API access without opening a room or starting host pings. */
|
|
147
149
|
async initMultiplayer(params) {
|
|
148
150
|
this.debugLog(`initializing multiplayer with backend ${params.backendOrigin}`);
|
|
149
151
|
await this.roomController.initMultiplayer(params);
|
|
150
152
|
this.debugLog('multiplayer API initialized');
|
|
151
153
|
}
|
|
152
|
-
/** Start
|
|
154
|
+
/** Start local play without contacting the multiplayer API. This can later open into a room. */
|
|
153
155
|
startSingleplayer() {
|
|
154
156
|
if (this.currentConnection) {
|
|
155
157
|
throw new Error('Cannot start singleplayer with a connection already present.');
|
|
@@ -223,23 +225,32 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
223
225
|
}
|
|
224
226
|
/** Join or create a room. */
|
|
225
227
|
async joinOrCreateRoom(room) {
|
|
226
|
-
if (this.singleplayer) {
|
|
227
|
-
throw new Error('Cannot join room: connection already established.');
|
|
228
|
-
}
|
|
229
228
|
const previousRoomConnection = this.roomConnection;
|
|
229
|
+
const wasSingleplayer = this.singleplayer;
|
|
230
230
|
this.debugLog(`joining or creating room '${room.roomName}' (${room.roomId})`);
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
this.joiningRoom = true;
|
|
232
|
+
try {
|
|
233
|
+
const roomConnection = await this.joinRoom({
|
|
234
|
+
previousRoomConnection,
|
|
235
|
+
room,
|
|
236
|
+
});
|
|
237
|
+
if (previousRoomConnection) {
|
|
238
|
+
globalThis.clearTimeout(this.timeoutId);
|
|
239
|
+
this.clientsResponded = {};
|
|
240
|
+
this.frameActions = [];
|
|
241
|
+
this.frameTickReady = true;
|
|
242
|
+
}
|
|
243
|
+
else if (wasSingleplayer) {
|
|
244
|
+
globalThis.clearTimeout(this.timeoutId);
|
|
245
|
+
this.frameTickReady = true;
|
|
246
|
+
}
|
|
247
|
+
this.singleplayer = false;
|
|
248
|
+
this.attachMultiplayerRoomConnection(roomConnection);
|
|
249
|
+
this.debugLog(`attached p2p-lock-step connection; client=${this.getClientId() || 'unknown'} host=${this.isHost()} connected=${this.isConnected()}`);
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
this.joiningRoom = false;
|
|
240
253
|
}
|
|
241
|
-
this.attachMultiplayerRoomConnection(roomConnection);
|
|
242
|
-
this.debugLog(`attached p2p-lock-step connection; client=${this.getClientId() || 'unknown'} host=${this.isHost()} connected=${this.isConnected()}`);
|
|
243
254
|
}
|
|
244
255
|
/** Join through the core room controller while preserving the wrapper connection on failure. */
|
|
245
256
|
async joinRoom({ previousRoomConnection, room, }) {
|
|
@@ -283,6 +294,9 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
283
294
|
if ('newMember' in event.detail) {
|
|
284
295
|
this.syncNewMember(event.detail.newMember);
|
|
285
296
|
}
|
|
297
|
+
else if ('newHost' in event.detail) {
|
|
298
|
+
this.handleNewHost(event.detail.newHost);
|
|
299
|
+
}
|
|
286
300
|
this.dispatch(event);
|
|
287
301
|
});
|
|
288
302
|
this.roomController.listen((ControllerMessageEvent), (event) => {
|
|
@@ -306,6 +320,19 @@ export class P2pLockStepMultiplayerController extends ListenTarget {
|
|
|
306
320
|
});
|
|
307
321
|
}
|
|
308
322
|
}
|
|
323
|
+
/** Restart frame production if this client is promoted after losing its previous host. */
|
|
324
|
+
handleNewHost(clientId) {
|
|
325
|
+
if (this.joiningRoom ||
|
|
326
|
+
clientId !== this.localClientId ||
|
|
327
|
+
!this.roomConnection ||
|
|
328
|
+
!this.roomConnection.isHost()) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
globalThis.clearTimeout(this.timeoutId);
|
|
332
|
+
this.clientsResponded = {};
|
|
333
|
+
this.frameTickReady = true;
|
|
334
|
+
this.finishFrame();
|
|
335
|
+
}
|
|
309
336
|
/** Send an empty frame to a newly connected member so it can join the frame flow. */
|
|
310
337
|
syncNewMember(clientId) {
|
|
311
338
|
this.debugLog(`syncNewMember called for ${clientId}; host=${this.isHost()}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antha/multiplayer-p2p-lock-step",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Multiplayer mod for the Antha engine.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vir",
|
|
@@ -36,20 +36,20 @@
|
|
|
36
36
|
"test:docs": "virmator docs check"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@augment-vir/assert": "^
|
|
40
|
-
"@augment-vir/common": "^
|
|
41
|
-
"date-vir": "^
|
|
42
|
-
"typed-event-target": "^4.3.
|
|
39
|
+
"@augment-vir/assert": "^32.2.2",
|
|
40
|
+
"@augment-vir/common": "^32.2.2",
|
|
41
|
+
"date-vir": "^9.0.0",
|
|
42
|
+
"typed-event-target": "^4.3.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@augment-vir/test": "^
|
|
46
|
-
"@web/dev-server-esbuild": "^
|
|
47
|
-
"@web/test-runner": "^0.
|
|
48
|
-
"@web/test-runner-playwright": "^0.
|
|
45
|
+
"@augment-vir/test": "^32.2.2",
|
|
46
|
+
"@web/dev-server-esbuild": "^2.0.0",
|
|
47
|
+
"@web/test-runner": "^1.0.0",
|
|
48
|
+
"@web/test-runner-playwright": "^1.0.0",
|
|
49
49
|
"istanbul-smart-text-reporter": "^1.1.5"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@antha/multiplayer-core": "^0.
|
|
52
|
+
"@antha/multiplayer-core": "^0.1.1"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
55
|
"node": ">=22"
|