modd-network 1.0.1 → 1.0.3
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 +18 -19
- package/dist/binary-input.d.ts +53 -0
- package/dist/binary-input.js +168 -0
- package/dist/game-input-codec.d.ts +55 -0
- package/dist/game-input-codec.js +263 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -1
- package/dist/input-codec.d.ts +33 -0
- package/dist/input-codec.js +192 -0
- package/dist/modd-engine.d.ts +77 -56
- package/dist/modd-engine.js +479 -244
- package/dist/modd-engine.test.d.ts +1 -0
- package/dist/modd-engine.test.js +190 -0
- package/dist/modd-network.d.ts +55 -22
- package/dist/modd-network.js +497 -352
- package/dist/state-codec.d.ts +81 -0
- package/dist/state-codec.js +337 -0
- package/package.json +5 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const modd_engine_1 = require("./modd-engine");
|
|
5
|
+
// Mock physics adapter
|
|
6
|
+
function createMockAdapter(initialState) {
|
|
7
|
+
let state = { ...initialState };
|
|
8
|
+
return {
|
|
9
|
+
getState: () => ({ ...state }),
|
|
10
|
+
setState: (newState) => { state = { ...newState }; }
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
(0, vitest_1.describe)('createStateSync (Game)', () => {
|
|
14
|
+
let game;
|
|
15
|
+
(0, vitest_1.beforeEach)(() => {
|
|
16
|
+
game = (0, modd_engine_1.createStateSync)({ lerpSpeed: 0.3 });
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.describe)('player management', () => {
|
|
19
|
+
(0, vitest_1.it)('should add and retrieve players', () => {
|
|
20
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
21
|
+
const player = game.getPlayer('player1');
|
|
22
|
+
(0, vitest_1.expect)(player).toBeDefined();
|
|
23
|
+
(0, vitest_1.expect)(player?.id).toBe('player1');
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.it)('should remove players', () => {
|
|
26
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
27
|
+
game.removePlayer('player1');
|
|
28
|
+
(0, vitest_1.expect)(game.getPlayer('player1')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.it)('should get all players', () => {
|
|
31
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
32
|
+
game.addPlayer('player2', { x: 10, y: 0, z: 10 });
|
|
33
|
+
const players = game.getPlayers();
|
|
34
|
+
(0, vitest_1.expect)(players.length).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
(0, vitest_1.it)('should set local player', () => {
|
|
37
|
+
game.setLocalPlayer('player1');
|
|
38
|
+
(0, vitest_1.expect)(game.localPlayerId).toBe('player1');
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)('should update local player state', () => {
|
|
41
|
+
game.setLocalPlayer('player1');
|
|
42
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
43
|
+
game.updateLocalPlayerState({ x: 10, y: 5, z: 10 });
|
|
44
|
+
const player = game.getPlayer('player1');
|
|
45
|
+
(0, vitest_1.expect)(player?.state.x).toBe(10);
|
|
46
|
+
(0, vitest_1.expect)(player?.state.y).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)('entity management', () => {
|
|
50
|
+
(0, vitest_1.it)('should register and retrieve entities', () => {
|
|
51
|
+
const adapter = createMockAdapter({ x: 0, y: 0, z: 0 });
|
|
52
|
+
game.registerEntity('box1', adapter);
|
|
53
|
+
const entity = game.getEntity('box1');
|
|
54
|
+
(0, vitest_1.expect)(entity).toBeDefined();
|
|
55
|
+
(0, vitest_1.expect)(entity?.id).toBe('box1');
|
|
56
|
+
});
|
|
57
|
+
(0, vitest_1.it)('should unregister entities', () => {
|
|
58
|
+
const adapter = createMockAdapter({ x: 0, y: 0, z: 0 });
|
|
59
|
+
game.registerEntity('box1', adapter);
|
|
60
|
+
game.unregisterEntity('box1');
|
|
61
|
+
(0, vitest_1.expect)(game.getEntity('box1')).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
(0, vitest_1.it)('should get all entities', () => {
|
|
64
|
+
game.registerEntity('box1', createMockAdapter({ x: 0, y: 0, z: 0 }));
|
|
65
|
+
game.registerEntity('box2', createMockAdapter({ x: 10, y: 0, z: 10 }));
|
|
66
|
+
(0, vitest_1.expect)(game.getEntities().length).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.describe)('authority', () => {
|
|
70
|
+
(0, vitest_1.beforeEach)(() => {
|
|
71
|
+
const adapter = createMockAdapter({ x: 10, y: 0, z: 10 });
|
|
72
|
+
game.registerEntity('box1', adapter);
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)('should return false when no local player', () => {
|
|
75
|
+
(0, vitest_1.expect)(game.isAuthority('box1')).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
(0, vitest_1.it)('should assign authority to closest player after tick', () => {
|
|
78
|
+
game.setLocalPlayer('player1');
|
|
79
|
+
game.addPlayer('player1', { x: 10, y: 0, z: 10 }); // close
|
|
80
|
+
game.addPlayer('player2', { x: 100, y: 0, z: 100 }); // far
|
|
81
|
+
game.tick();
|
|
82
|
+
(0, vitest_1.expect)(game.isAuthority('box1')).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('should not have authority if another player is closer', () => {
|
|
85
|
+
game.setLocalPlayer('player1');
|
|
86
|
+
game.addPlayer('player1', { x: 100, y: 0, z: 100 }); // far
|
|
87
|
+
game.addPlayer('player2', { x: 10, y: 0, z: 10 }); // close
|
|
88
|
+
game.tick();
|
|
89
|
+
(0, vitest_1.expect)(game.isAuthority('box1')).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
(0, vitest_1.describe)('state receiving', () => {
|
|
93
|
+
(0, vitest_1.it)('should receive player state via processInput', () => {
|
|
94
|
+
game.setLocalPlayer('player1');
|
|
95
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
96
|
+
game.processInput({ type: 'playerState', id: 'player2', x: 50, y: 0, z: 50 });
|
|
97
|
+
const player2 = game.getPlayer('player2');
|
|
98
|
+
(0, vitest_1.expect)(player2).toBeDefined();
|
|
99
|
+
(0, vitest_1.expect)(player2?.targetState.x).toBe(50);
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.it)('should not apply own state back', () => {
|
|
102
|
+
game.setLocalPlayer('player1');
|
|
103
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
104
|
+
game.processInput({ type: 'playerState', id: 'player1', x: 100, y: 0, z: 100 });
|
|
105
|
+
const player1 = game.getPlayer('player1');
|
|
106
|
+
(0, vitest_1.expect)(player1?.state.x).toBe(0);
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)('should receive entity state when not authority', () => {
|
|
109
|
+
const adapter = createMockAdapter({ x: 0, y: 0, z: 0 });
|
|
110
|
+
game.registerEntity('box1', adapter);
|
|
111
|
+
game.processInput({ type: 'entityState', id: 'box1', x: 50, y: 10, z: 50 });
|
|
112
|
+
const entity = game.getEntity('box1');
|
|
113
|
+
(0, vitest_1.expect)(entity?.targetState.x).toBe(50);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
(0, vitest_1.describe)('tick', () => {
|
|
117
|
+
(0, vitest_1.it)('should increment frame', () => {
|
|
118
|
+
const initialFrame = game.frame;
|
|
119
|
+
game.tick();
|
|
120
|
+
(0, vitest_1.expect)(game.frame).toBe(initialFrame + 1);
|
|
121
|
+
});
|
|
122
|
+
(0, vitest_1.it)('should interpolate other players toward target', () => {
|
|
123
|
+
game.setLocalPlayer('player1');
|
|
124
|
+
game.addPlayer('player1', { x: 0, y: 0, z: 0 });
|
|
125
|
+
game.addPlayer('player2', { x: 0, y: 0, z: 0 });
|
|
126
|
+
// Set target for player2
|
|
127
|
+
const player2 = game.getPlayer('player2');
|
|
128
|
+
player2.targetState = { x: 100, y: 0, z: 0 };
|
|
129
|
+
game.tick();
|
|
130
|
+
// Should have moved toward target
|
|
131
|
+
(0, vitest_1.expect)(player2.state.x).toBeGreaterThan(0);
|
|
132
|
+
(0, vitest_1.expect)(player2.state.x).toBeLessThan(100);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
(0, vitest_1.describe)('configuration', () => {
|
|
136
|
+
(0, vitest_1.it)('should use default config values', () => {
|
|
137
|
+
const defaultGame = (0, modd_engine_1.createStateSync)();
|
|
138
|
+
(0, vitest_1.expect)(defaultGame.config.lerpSpeed).toBe(0.3);
|
|
139
|
+
(0, vitest_1.expect)(defaultGame.config.positionThreshold).toBe(0.01);
|
|
140
|
+
(0, vitest_1.expect)(defaultGame.config.restFramesRequired).toBe(30);
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.it)('should accept custom config', () => {
|
|
143
|
+
const customGame = (0, modd_engine_1.createStateSync)({
|
|
144
|
+
lerpSpeed: 0.5,
|
|
145
|
+
positionThreshold: 0.1,
|
|
146
|
+
restFramesRequired: 60
|
|
147
|
+
});
|
|
148
|
+
(0, vitest_1.expect)(customGame.config.lerpSpeed).toBe(0.5);
|
|
149
|
+
(0, vitest_1.expect)(customGame.config.positionThreshold).toBe(0.1);
|
|
150
|
+
(0, vitest_1.expect)(customGame.config.restFramesRequired).toBe(60);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
(0, vitest_1.describe)('rapierAdapter', () => {
|
|
155
|
+
(0, vitest_1.it)('should throw on invalid body', () => {
|
|
156
|
+
(0, vitest_1.expect)(() => (0, modd_engine_1.rapierAdapter)(null)).toThrow();
|
|
157
|
+
(0, vitest_1.expect)(() => (0, modd_engine_1.rapierAdapter)(undefined)).toThrow();
|
|
158
|
+
(0, vitest_1.expect)(() => (0, modd_engine_1.rapierAdapter)({})).toThrow();
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.it)('should work with valid body', () => {
|
|
161
|
+
const mockBody = {
|
|
162
|
+
translation: () => ({ x: 1, y: 2, z: 3 }),
|
|
163
|
+
linvel: () => ({ x: 0.1, y: 0.2, z: 0.3 }),
|
|
164
|
+
rotation: () => ({ x: 0, y: 0, z: 0, w: 1 }),
|
|
165
|
+
angvel: () => ({ x: 0, y: 0, z: 0 }),
|
|
166
|
+
setTranslation: () => { },
|
|
167
|
+
setLinvel: () => { },
|
|
168
|
+
setRotation: () => { },
|
|
169
|
+
setAngvel: () => { }
|
|
170
|
+
};
|
|
171
|
+
const adapter = (0, modd_engine_1.rapierAdapter)(mockBody);
|
|
172
|
+
const state = adapter.getState();
|
|
173
|
+
(0, vitest_1.expect)(state.x).toBe(1);
|
|
174
|
+
(0, vitest_1.expect)(state.y).toBe(2);
|
|
175
|
+
(0, vitest_1.expect)(state.z).toBe(3);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
(0, vitest_1.describe)('generic2DAdapter', () => {
|
|
179
|
+
(0, vitest_1.it)('should get and set state', () => {
|
|
180
|
+
let body = { x: 10, y: 20, vx: 1, vy: 2 };
|
|
181
|
+
const adapter = (0, modd_engine_1.generic2DAdapter)(() => body, (state) => { body = { ...body, ...state }; });
|
|
182
|
+
const state = adapter.getState();
|
|
183
|
+
(0, vitest_1.expect)(state.x).toBe(10);
|
|
184
|
+
(0, vitest_1.expect)(state.y).toBe(20);
|
|
185
|
+
(0, vitest_1.expect)(state.z).toBe(0);
|
|
186
|
+
adapter.setState({ x: 50, y: 60, z: 0, vx: 5, vy: 6 });
|
|
187
|
+
(0, vitest_1.expect)(body.x).toBe(50);
|
|
188
|
+
(0, vitest_1.expect)(body.y).toBe(60);
|
|
189
|
+
});
|
|
190
|
+
});
|
package/dist/modd-network.d.ts
CHANGED
|
@@ -1,40 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Works with any app type: games, chat, collaboration, etc.
|
|
2
|
+
* Hash a client ID to a 4-byte identifier (matches server-side hash)
|
|
3
|
+
* Uses FNV-1a hash for speed and consistency
|
|
5
4
|
*/
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
export declare function hashClientId(clientId: string): number;
|
|
6
|
+
export declare const hashPlayerId: typeof hashClientId;
|
|
7
|
+
/**
|
|
8
|
+
* Register a client ID so we can decode their hash in TICK messages
|
|
9
|
+
* Call this when a client joins (from the join event's clientId)
|
|
10
|
+
*/
|
|
11
|
+
export declare function registerClientId(clientId: string): void;
|
|
12
|
+
export declare const registerPlayerId: typeof registerClientId;
|
|
13
|
+
/**
|
|
14
|
+
* Unregister a client ID (call on leave)
|
|
15
|
+
*/
|
|
16
|
+
export declare function unregisterClientId(clientId: string): void;
|
|
17
|
+
export declare const unregisterPlayerId: typeof unregisterClientId;
|
|
18
|
+
export interface GameEvent {
|
|
19
|
+
id: string;
|
|
20
|
+
clientId: string;
|
|
21
|
+
type: string;
|
|
22
22
|
data: any;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
timestamp: number;
|
|
24
|
+
seq: number;
|
|
25
25
|
}
|
|
26
|
+
export type Input = GameEvent;
|
|
26
27
|
export interface Connection {
|
|
27
28
|
send(data: any): void;
|
|
28
29
|
sendSnapshot(snapshot: any, hash: string): void;
|
|
29
30
|
leaveRoom(): void;
|
|
30
31
|
close(): void;
|
|
31
32
|
readonly connected: boolean;
|
|
33
|
+
readonly clientId: string | null;
|
|
32
34
|
readonly node: string | null;
|
|
33
35
|
readonly bandwidthIn: number;
|
|
34
36
|
readonly bandwidthOut: number;
|
|
35
37
|
readonly totalBytesIn: number;
|
|
36
38
|
readonly totalBytesOut: number;
|
|
37
39
|
readonly frame: number;
|
|
40
|
+
getLatency(): string;
|
|
41
|
+
getClients(): void;
|
|
42
|
+
}
|
|
43
|
+
export interface ConnectOptions {
|
|
44
|
+
snapshot?: any;
|
|
45
|
+
user?: any;
|
|
46
|
+
centralServiceUrl?: string;
|
|
47
|
+
nodeUrl?: string;
|
|
48
|
+
joinToken?: string;
|
|
49
|
+
getStateHash?: () => string;
|
|
50
|
+
onConnect?: (snapshot: any, events: GameEvent[], frame: number, node: string | null) => void;
|
|
51
|
+
onDisconnect?: () => void;
|
|
52
|
+
onError?: (error: string) => void;
|
|
53
|
+
onMessage?: (data: any, seq: number) => void;
|
|
54
|
+
onTick?: (frame: number, events: GameEvent[], isCatchUp?: boolean) => void;
|
|
55
|
+
onSnapshot?: (snapshot: any, hash: string) => void;
|
|
56
|
+
onClientsUpdate?: (clients: any[]) => void;
|
|
57
|
+
}
|
|
58
|
+
export interface DecodedMessage {
|
|
59
|
+
type: string;
|
|
60
|
+
roomId?: string;
|
|
61
|
+
clientId?: string;
|
|
62
|
+
frame?: number;
|
|
63
|
+
inputs?: any[];
|
|
64
|
+
snapshot?: any;
|
|
65
|
+
snapshotHash?: string;
|
|
66
|
+
events?: GameEvent[];
|
|
67
|
+
message?: string;
|
|
68
|
+
clients?: any[];
|
|
38
69
|
}
|
|
39
|
-
export declare function
|
|
70
|
+
export declare function encodeSyncHash(roomId: string, hash: string, seq: number, frame: number): Uint8Array;
|
|
71
|
+
export declare function decodeBinaryMessage(buffer: ArrayBuffer): DecodedMessage | null;
|
|
72
|
+
export declare function connect(appId: string, roomId: string, options?: ConnectOptions): Promise<Connection>;
|
|
40
73
|
export declare const modd: typeof connect;
|