murow 0.0.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 +61 -0
- package/dist/core/binary-codec/binary-codec.d.ts +159 -0
- package/dist/core/binary-codec/binary-codec.js +336 -0
- package/dist/core/binary-codec/index.d.ts +1 -0
- package/dist/core/binary-codec/index.js +1 -0
- package/dist/core/events/event-system.d.ts +71 -0
- package/dist/core/events/event-system.js +88 -0
- package/dist/core/events/index.d.ts +1 -0
- package/dist/core/events/index.js +1 -0
- package/dist/core/fixed-ticker/fixed-ticker.d.ts +105 -0
- package/dist/core/fixed-ticker/fixed-ticker.js +91 -0
- package/dist/core/fixed-ticker/index.d.ts +1 -0
- package/dist/core/fixed-ticker/index.js +1 -0
- package/dist/core/generate-id/generate-id.d.ts +21 -0
- package/dist/core/generate-id/generate-id.js +25 -0
- package/dist/core/generate-id/index.d.ts +1 -0
- package/dist/core/generate-id/index.js +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.js +8 -0
- package/dist/core/lerp/index.d.ts +1 -0
- package/dist/core/lerp/index.js +1 -0
- package/dist/core/lerp/lerp.d.ts +40 -0
- package/dist/core/lerp/lerp.js +42 -0
- package/dist/core/navmesh/index.d.ts +1 -0
- package/dist/core/navmesh/index.js +1 -0
- package/dist/core/navmesh/navmesh.d.ts +116 -0
- package/dist/core/navmesh/navmesh.js +666 -0
- package/dist/core/pooled-codec/index.d.ts +1 -0
- package/dist/core/pooled-codec/index.js +1 -0
- package/dist/core/pooled-codec/pooled-codec.d.ts +140 -0
- package/dist/core/pooled-codec/pooled-codec.js +213 -0
- package/dist/core/prediction/index.d.ts +1 -0
- package/dist/core/prediction/index.js +1 -0
- package/dist/core/prediction/prediction.d.ts +64 -0
- package/dist/core/prediction/prediction.js +90 -0
- package/dist/core.esm.js +1 -0
- package/dist/core.js +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +18 -0
- package/dist/protocol/index.d.ts +43 -0
- package/dist/protocol/index.js +43 -0
- package/dist/protocol/intent/index.d.ts +39 -0
- package/dist/protocol/intent/index.js +38 -0
- package/dist/protocol/intent/intent-registry.d.ts +54 -0
- package/dist/protocol/intent/intent-registry.js +73 -0
- package/dist/protocol/intent/intent.d.ts +12 -0
- package/dist/protocol/intent/intent.js +1 -0
- package/dist/protocol/snapshot/index.d.ts +44 -0
- package/dist/protocol/snapshot/index.js +43 -0
- package/dist/protocol/snapshot/snapshot-codec.d.ts +48 -0
- package/dist/protocol/snapshot/snapshot-codec.js +56 -0
- package/dist/protocol/snapshot/snapshot-registry.d.ts +100 -0
- package/dist/protocol/snapshot/snapshot-registry.js +136 -0
- package/dist/protocol/snapshot/snapshot.d.ts +19 -0
- package/dist/protocol/snapshot/snapshot.js +30 -0
- package/package.json +54 -0
- package/src/core/binary-codec/README.md +60 -0
- package/src/core/binary-codec/binary-codec.test.ts +300 -0
- package/src/core/binary-codec/binary-codec.ts +430 -0
- package/src/core/binary-codec/index.ts +1 -0
- package/src/core/events/README.md +47 -0
- package/src/core/events/event-system.test.ts +243 -0
- package/src/core/events/event-system.ts +140 -0
- package/src/core/events/index.ts +1 -0
- package/src/core/fixed-ticker/README.md +77 -0
- package/src/core/fixed-ticker/fixed-ticker.test.ts +151 -0
- package/src/core/fixed-ticker/fixed-ticker.ts +158 -0
- package/src/core/fixed-ticker/index.ts +1 -0
- package/src/core/generate-id/README.md +18 -0
- package/src/core/generate-id/generate-id.test.ts +79 -0
- package/src/core/generate-id/generate-id.ts +37 -0
- package/src/core/generate-id/index.ts +1 -0
- package/src/core/index.ts +8 -0
- package/src/core/lerp/README.md +79 -0
- package/src/core/lerp/index.ts +1 -0
- package/src/core/lerp/lerp.test.ts +90 -0
- package/src/core/lerp/lerp.ts +42 -0
- package/src/core/navmesh/README.md +124 -0
- package/src/core/navmesh/index.ts +1 -0
- package/src/core/navmesh/navmesh.test.ts +344 -0
- package/src/core/navmesh/navmesh.ts +850 -0
- package/src/core/pooled-codec/README.md +70 -0
- package/src/core/pooled-codec/index.ts +1 -0
- package/src/core/pooled-codec/pooled-codec.test.ts +349 -0
- package/src/core/pooled-codec/pooled-codec.ts +239 -0
- package/src/core/prediction/README.md +64 -0
- package/src/core/prediction/index.ts +1 -0
- package/src/core/prediction/prediction.test.ts +422 -0
- package/src/core/prediction/prediction.ts +101 -0
- package/src/index.ts +20 -0
- package/src/protocol/README.md +310 -0
- package/src/protocol/index.ts +44 -0
- package/src/protocol/intent/index.ts +40 -0
- package/src/protocol/intent/intent-registry.test.ts +237 -0
- package/src/protocol/intent/intent-registry.ts +88 -0
- package/src/protocol/intent/intent.ts +12 -0
- package/src/protocol/snapshot/index.ts +45 -0
- package/src/protocol/snapshot/snapshot-codec.test.ts +138 -0
- package/src/protocol/snapshot/snapshot-codec.ts +71 -0
- package/src/protocol/snapshot/snapshot-registry.test.ts +302 -0
- package/src/protocol/snapshot/snapshot-registry.ts +162 -0
- package/src/protocol/snapshot/snapshot.test.ts +76 -0
- package/src/protocol/snapshot/snapshot.ts +41 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protocol Layer - Type-safe networking primitives
|
|
3
|
+
*
|
|
4
|
+
* This layer enforces:
|
|
5
|
+
* - Type-safe intent definitions (extend Intent interface)
|
|
6
|
+
* - Type-safe snapshot definitions (Snapshot<YourState>)
|
|
7
|
+
* - Memory-efficient encoding (use PooledCodec from core)
|
|
8
|
+
*
|
|
9
|
+
* You provide:
|
|
10
|
+
* - Your intent types
|
|
11
|
+
* - Your state types
|
|
12
|
+
* - Your schemas (for binary encoding)
|
|
13
|
+
* - Codec instances (instantiate once, reuse)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // Define your types
|
|
18
|
+
* interface MoveIntent extends Intent {
|
|
19
|
+
* kind: 1;
|
|
20
|
+
* tick: number;
|
|
21
|
+
* dx: number;
|
|
22
|
+
* dy: number;
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* interface GameState {
|
|
26
|
+
* players: Record<number, { x: number; y: number }>;
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* // Create codecs once (reuse these!)
|
|
30
|
+
* const intentRegistry = new IntentRegistry();
|
|
31
|
+
* intentRegistry.register(1, new PooledCodec(moveSchema));
|
|
32
|
+
*
|
|
33
|
+
* const snapshotCodec = new SnapshotCodec<GameState>(
|
|
34
|
+
* new PooledCodec(stateSchema)
|
|
35
|
+
* );
|
|
36
|
+
*
|
|
37
|
+
* // Use them
|
|
38
|
+
* const buf = intentRegistry.encode(intent);
|
|
39
|
+
* const snapshot = snapshotCodec.decode(buf);
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export * from "./intent";
|
|
43
|
+
export * from "./snapshot";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent system for client-to-server actions.
|
|
3
|
+
*
|
|
4
|
+
* Intents are player/AI actions that need to be:
|
|
5
|
+
* 1. Encoded efficiently (binary)
|
|
6
|
+
* 2. Sent over network
|
|
7
|
+
* 3. Decoded on the other end
|
|
8
|
+
* 4. Processed deterministically
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Intent, IntentRegistry } from './protocol/intent';
|
|
13
|
+
* import { PooledCodec } from '../core/pooled-codec';
|
|
14
|
+
* import { BinaryCodec } from '../core/binary-codec';
|
|
15
|
+
*
|
|
16
|
+
* // 1. Define your intent type
|
|
17
|
+
* interface MoveIntent extends Intent {
|
|
18
|
+
* kind: 1;
|
|
19
|
+
* tick: number;
|
|
20
|
+
* dx: number;
|
|
21
|
+
* dy: number;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // 2. Create registry and register once (reuse this instance)
|
|
25
|
+
* const registry = new IntentRegistry();
|
|
26
|
+
* registry.register(1, new PooledCodec({
|
|
27
|
+
* kind: BinaryCodec.u8,
|
|
28
|
+
* tick: BinaryCodec.u32,
|
|
29
|
+
* dx: BinaryCodec.f32,
|
|
30
|
+
* dy: BinaryCodec.f32,
|
|
31
|
+
* }));
|
|
32
|
+
*
|
|
33
|
+
* // 3. Encode/decode
|
|
34
|
+
* const buf = registry.encode(intent);
|
|
35
|
+
* const decoded = registry.decode(1, buf);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export type { Intent } from "./intent";
|
|
39
|
+
export { IntentRegistry } from "./intent-registry";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent system for client-to-server actions.
|
|
3
|
+
*
|
|
4
|
+
* Intents are player/AI actions that need to be:
|
|
5
|
+
* 1. Encoded efficiently (binary)
|
|
6
|
+
* 2. Sent over network
|
|
7
|
+
* 3. Decoded on the other end
|
|
8
|
+
* 4. Processed deterministically
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { Intent, IntentRegistry } from './protocol/intent';
|
|
13
|
+
* import { PooledCodec } from '../core/pooled-codec';
|
|
14
|
+
* import { BinaryCodec } from '../core/binary-codec';
|
|
15
|
+
*
|
|
16
|
+
* // 1. Define your intent type
|
|
17
|
+
* interface MoveIntent extends Intent {
|
|
18
|
+
* kind: 1;
|
|
19
|
+
* tick: number;
|
|
20
|
+
* dx: number;
|
|
21
|
+
* dy: number;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // 2. Create registry and register once (reuse this instance)
|
|
25
|
+
* const registry = new IntentRegistry();
|
|
26
|
+
* registry.register(1, new PooledCodec({
|
|
27
|
+
* kind: BinaryCodec.u8,
|
|
28
|
+
* tick: BinaryCodec.u32,
|
|
29
|
+
* dx: BinaryCodec.f32,
|
|
30
|
+
* dy: BinaryCodec.f32,
|
|
31
|
+
* }));
|
|
32
|
+
*
|
|
33
|
+
* // 3. Encode/decode
|
|
34
|
+
* const buf = registry.encode(intent);
|
|
35
|
+
* const decoded = registry.decode(1, buf);
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export { IntentRegistry } from "./intent-registry";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Intent } from "./intent";
|
|
2
|
+
/**
|
|
3
|
+
* Generic codec interface (users import from core/pooled-codec)
|
|
4
|
+
*/
|
|
5
|
+
export interface Codec<T> {
|
|
6
|
+
encode(value: T): Uint8Array;
|
|
7
|
+
decode(buf: Uint8Array): T;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Registry for mapping intent kinds to their codecs.
|
|
11
|
+
*
|
|
12
|
+
* Users instantiate this once and register their intent types with
|
|
13
|
+
* PooledCodec instances (from core/pooled-codec).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { IntentRegistry } from './protocol/intent';
|
|
18
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
19
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
20
|
+
*
|
|
21
|
+
* const registry = new IntentRegistry();
|
|
22
|
+
*
|
|
23
|
+
* registry.register(1, new PooledCodec({
|
|
24
|
+
* kind: BinaryCodec.u8,
|
|
25
|
+
* tick: BinaryCodec.u32,
|
|
26
|
+
* dx: BinaryCodec.f32,
|
|
27
|
+
* dy: BinaryCodec.f32,
|
|
28
|
+
* }));
|
|
29
|
+
*
|
|
30
|
+
* // Encode/decode
|
|
31
|
+
* const buf = registry.encode(intent);
|
|
32
|
+
* const decoded = registry.decode(1, buf);
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare class IntentRegistry {
|
|
36
|
+
private codecs;
|
|
37
|
+
/**
|
|
38
|
+
* Register a codec for a specific intent kind.
|
|
39
|
+
* Call this once per intent type at startup.
|
|
40
|
+
*/
|
|
41
|
+
register<T extends Intent>(kind: number, codec: Codec<T>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Encode an intent into binary format.
|
|
44
|
+
*/
|
|
45
|
+
encode<T extends Intent>(intent: T): Uint8Array;
|
|
46
|
+
/**
|
|
47
|
+
* Decode binary data into an intent.
|
|
48
|
+
*/
|
|
49
|
+
decode(kind: number, buf: Uint8Array): Intent;
|
|
50
|
+
has(kind: number): boolean;
|
|
51
|
+
unregister(kind: number): boolean;
|
|
52
|
+
clear(): void;
|
|
53
|
+
getKinds(): number[];
|
|
54
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for mapping intent kinds to their codecs.
|
|
3
|
+
*
|
|
4
|
+
* Users instantiate this once and register their intent types with
|
|
5
|
+
* PooledCodec instances (from core/pooled-codec).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { IntentRegistry } from './protocol/intent';
|
|
10
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
11
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
12
|
+
*
|
|
13
|
+
* const registry = new IntentRegistry();
|
|
14
|
+
*
|
|
15
|
+
* registry.register(1, new PooledCodec({
|
|
16
|
+
* kind: BinaryCodec.u8,
|
|
17
|
+
* tick: BinaryCodec.u32,
|
|
18
|
+
* dx: BinaryCodec.f32,
|
|
19
|
+
* dy: BinaryCodec.f32,
|
|
20
|
+
* }));
|
|
21
|
+
*
|
|
22
|
+
* // Encode/decode
|
|
23
|
+
* const buf = registry.encode(intent);
|
|
24
|
+
* const decoded = registry.decode(1, buf);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export class IntentRegistry {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.codecs = new Map();
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Register a codec for a specific intent kind.
|
|
33
|
+
* Call this once per intent type at startup.
|
|
34
|
+
*/
|
|
35
|
+
register(kind, codec) {
|
|
36
|
+
if (this.codecs.has(kind)) {
|
|
37
|
+
throw new Error(`Intent kind ${kind} is already registered`);
|
|
38
|
+
}
|
|
39
|
+
this.codecs.set(kind, codec);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Encode an intent into binary format.
|
|
43
|
+
*/
|
|
44
|
+
encode(intent) {
|
|
45
|
+
const codec = this.codecs.get(intent.kind);
|
|
46
|
+
if (!codec) {
|
|
47
|
+
throw new Error(`No codec registered for intent kind ${intent.kind}`);
|
|
48
|
+
}
|
|
49
|
+
return codec.encode(intent);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decode binary data into an intent.
|
|
53
|
+
*/
|
|
54
|
+
decode(kind, buf) {
|
|
55
|
+
const codec = this.codecs.get(kind);
|
|
56
|
+
if (!codec) {
|
|
57
|
+
throw new Error(`No codec registered for intent kind ${kind}`);
|
|
58
|
+
}
|
|
59
|
+
return codec.decode(buf);
|
|
60
|
+
}
|
|
61
|
+
has(kind) {
|
|
62
|
+
return this.codecs.has(kind);
|
|
63
|
+
}
|
|
64
|
+
unregister(kind) {
|
|
65
|
+
return this.codecs.delete(kind);
|
|
66
|
+
}
|
|
67
|
+
clear() {
|
|
68
|
+
this.codecs.clear();
|
|
69
|
+
}
|
|
70
|
+
getKinds() {
|
|
71
|
+
return Array.from(this.codecs.keys());
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base interface for all game intents.
|
|
3
|
+
*
|
|
4
|
+
* Intents represent player or AI actions that need to be processed by the game simulation.
|
|
5
|
+
* They are timestamped with a tick number for deterministic replay and synchronization.
|
|
6
|
+
*/
|
|
7
|
+
export interface Intent {
|
|
8
|
+
/** The game tick at which this intent should be processed */
|
|
9
|
+
tick: number;
|
|
10
|
+
/** Numeric identifier for the intent type (used for codec lookup) */
|
|
11
|
+
kind: number;
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot system for server-to-client state updates.
|
|
3
|
+
*
|
|
4
|
+
* Snapshots are delta updates that contain:
|
|
5
|
+
* 1. Server tick number
|
|
6
|
+
* 2. Partial state updates (only what changed)
|
|
7
|
+
*
|
|
8
|
+
* They need to be:
|
|
9
|
+
* 1. Encoded efficiently (binary)
|
|
10
|
+
* 2. Sent over network
|
|
11
|
+
* 3. Decoded on the client
|
|
12
|
+
* 4. Merged into client state
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { Snapshot, SnapshotCodec, applySnapshot } from './protocol/snapshot';
|
|
17
|
+
* import { PooledCodec } from '../core/pooled-codec';
|
|
18
|
+
* import { BinaryCodec } from '../core/binary-codec';
|
|
19
|
+
*
|
|
20
|
+
* interface GameState {
|
|
21
|
+
* players: Record<number, { x: number; y: number }>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // 1. Create codec once (reuse this instance)
|
|
25
|
+
* const snapshotCodec = new SnapshotCodec<GameState>(
|
|
26
|
+
* new PooledCodec({ players: // ... your schema })
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* // 2. Server: Encode snapshot
|
|
30
|
+
* const snapshot: Snapshot<GameState> = {
|
|
31
|
+
* tick: 100,
|
|
32
|
+
* updates: { players: { 1: { x: 5, y: 10 } } }
|
|
33
|
+
* };
|
|
34
|
+
* const buf = snapshotCodec.encode(snapshot);
|
|
35
|
+
*
|
|
36
|
+
* // 3. Client: Decode and apply
|
|
37
|
+
* const snapshot = snapshotCodec.decode(buf);
|
|
38
|
+
* applySnapshot(clientState, snapshot);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export type { Snapshot } from "./snapshot";
|
|
42
|
+
export { applySnapshot } from "./snapshot";
|
|
43
|
+
export { SnapshotCodec } from "./snapshot-codec";
|
|
44
|
+
export { SnapshotRegistry } from "./snapshot-registry";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot system for server-to-client state updates.
|
|
3
|
+
*
|
|
4
|
+
* Snapshots are delta updates that contain:
|
|
5
|
+
* 1. Server tick number
|
|
6
|
+
* 2. Partial state updates (only what changed)
|
|
7
|
+
*
|
|
8
|
+
* They need to be:
|
|
9
|
+
* 1. Encoded efficiently (binary)
|
|
10
|
+
* 2. Sent over network
|
|
11
|
+
* 3. Decoded on the client
|
|
12
|
+
* 4. Merged into client state
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { Snapshot, SnapshotCodec, applySnapshot } from './protocol/snapshot';
|
|
17
|
+
* import { PooledCodec } from '../core/pooled-codec';
|
|
18
|
+
* import { BinaryCodec } from '../core/binary-codec';
|
|
19
|
+
*
|
|
20
|
+
* interface GameState {
|
|
21
|
+
* players: Record<number, { x: number; y: number }>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* // 1. Create codec once (reuse this instance)
|
|
25
|
+
* const snapshotCodec = new SnapshotCodec<GameState>(
|
|
26
|
+
* new PooledCodec({ players: // ... your schema })
|
|
27
|
+
* );
|
|
28
|
+
*
|
|
29
|
+
* // 2. Server: Encode snapshot
|
|
30
|
+
* const snapshot: Snapshot<GameState> = {
|
|
31
|
+
* tick: 100,
|
|
32
|
+
* updates: { players: { 1: { x: 5, y: 10 } } }
|
|
33
|
+
* };
|
|
34
|
+
* const buf = snapshotCodec.encode(snapshot);
|
|
35
|
+
*
|
|
36
|
+
* // 3. Client: Decode and apply
|
|
37
|
+
* const snapshot = snapshotCodec.decode(buf);
|
|
38
|
+
* applySnapshot(clientState, snapshot);
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export { applySnapshot } from "./snapshot";
|
|
42
|
+
export { SnapshotCodec } from "./snapshot-codec";
|
|
43
|
+
export { SnapshotRegistry } from "./snapshot-registry";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Snapshot } from "./snapshot";
|
|
2
|
+
/**
|
|
3
|
+
* Generic codec interface (users import from core/pooled-codec)
|
|
4
|
+
*/
|
|
5
|
+
export interface Codec<T> {
|
|
6
|
+
encode(value: T): Uint8Array;
|
|
7
|
+
decode(buf: Uint8Array): T;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Codec for encoding/decoding snapshots with binary serialization.
|
|
11
|
+
*
|
|
12
|
+
* Users instantiate this once with a PooledCodec for their state schema.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { SnapshotCodec } from './protocol/snapshot';
|
|
17
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
18
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
19
|
+
*
|
|
20
|
+
* interface GameState {
|
|
21
|
+
* players: Record<number, { x: number; y: number }>;
|
|
22
|
+
* }
|
|
23
|
+
*
|
|
24
|
+
* const snapshotCodec = new SnapshotCodec<GameState>(
|
|
25
|
+
* new PooledCodec({ players: // schema })
|
|
26
|
+
* );
|
|
27
|
+
*
|
|
28
|
+
* // Encode/decode
|
|
29
|
+
* const buf = snapshotCodec.encode(snapshot);
|
|
30
|
+
* const decoded = snapshotCodec.decode(buf);
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare class SnapshotCodec<T> {
|
|
34
|
+
private updatesCodec;
|
|
35
|
+
/**
|
|
36
|
+
* @param updatesCodec Codec for encoding/decoding the state updates
|
|
37
|
+
*/
|
|
38
|
+
constructor(updatesCodec: Codec<Partial<T>>);
|
|
39
|
+
/**
|
|
40
|
+
* Encode a snapshot into binary format.
|
|
41
|
+
* Format: [tick: u32][updates: encoded by updatesCodec]
|
|
42
|
+
*/
|
|
43
|
+
encode(snapshot: Snapshot<T>): Uint8Array;
|
|
44
|
+
/**
|
|
45
|
+
* Decode binary data into a snapshot.
|
|
46
|
+
*/
|
|
47
|
+
decode(buf: Uint8Array): Snapshot<T>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codec for encoding/decoding snapshots with binary serialization.
|
|
3
|
+
*
|
|
4
|
+
* Users instantiate this once with a PooledCodec for their state schema.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { SnapshotCodec } from './protocol/snapshot';
|
|
9
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
10
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
11
|
+
*
|
|
12
|
+
* interface GameState {
|
|
13
|
+
* players: Record<number, { x: number; y: number }>;
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* const snapshotCodec = new SnapshotCodec<GameState>(
|
|
17
|
+
* new PooledCodec({ players: // schema })
|
|
18
|
+
* );
|
|
19
|
+
*
|
|
20
|
+
* // Encode/decode
|
|
21
|
+
* const buf = snapshotCodec.encode(snapshot);
|
|
22
|
+
* const decoded = snapshotCodec.decode(buf);
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class SnapshotCodec {
|
|
26
|
+
/**
|
|
27
|
+
* @param updatesCodec Codec for encoding/decoding the state updates
|
|
28
|
+
*/
|
|
29
|
+
constructor(updatesCodec) {
|
|
30
|
+
this.updatesCodec = updatesCodec;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Encode a snapshot into binary format.
|
|
34
|
+
* Format: [tick: u32][updates: encoded by updatesCodec]
|
|
35
|
+
*/
|
|
36
|
+
encode(snapshot) {
|
|
37
|
+
const updatesBytes = this.updatesCodec.encode(snapshot.updates);
|
|
38
|
+
const buf = new Uint8Array(4 + updatesBytes.length);
|
|
39
|
+
// Encode tick (4 bytes, little-endian)
|
|
40
|
+
new DataView(buf.buffer).setUint32(0, snapshot.tick, true);
|
|
41
|
+
// Encode updates
|
|
42
|
+
buf.set(updatesBytes, 4);
|
|
43
|
+
return buf;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Decode binary data into a snapshot.
|
|
47
|
+
*/
|
|
48
|
+
decode(buf) {
|
|
49
|
+
// Decode tick (first 4 bytes)
|
|
50
|
+
const tick = new DataView(buf.buffer, buf.byteOffset).getUint32(0, true);
|
|
51
|
+
// Decode updates (remaining bytes)
|
|
52
|
+
const updatesBytes = buf.subarray(4);
|
|
53
|
+
const updates = this.updatesCodec.decode(updatesBytes);
|
|
54
|
+
return { tick, updates };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Snapshot } from "./snapshot";
|
|
2
|
+
/**
|
|
3
|
+
* Generic codec interface (users import from core/pooled-codec)
|
|
4
|
+
*/
|
|
5
|
+
interface Codec<T> {
|
|
6
|
+
encode(value: T): Uint8Array;
|
|
7
|
+
decode(buf: Uint8Array): T;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Registry for multiple snapshot types with different update schemas.
|
|
11
|
+
*
|
|
12
|
+
* This allows efficient delta encoding by only sending specific update types
|
|
13
|
+
* instead of encoding all fields (including empty/nil ones).
|
|
14
|
+
*
|
|
15
|
+
* ## Memory Efficiency
|
|
16
|
+
*
|
|
17
|
+
* The encoding path minimizes allocations:
|
|
18
|
+
* 1. PooledCodec acquires a buffer from its pool (reused across calls)
|
|
19
|
+
* 2. PooledCodec writes data directly to the buffer at sequential offsets
|
|
20
|
+
* 3. SnapshotRegistry creates ONE final buffer: [typeId(1) + tick(4) + updatesBytes]
|
|
21
|
+
* 4. Total allocations per encode: 1 pooled buffer + 1 final buffer
|
|
22
|
+
*
|
|
23
|
+
* Buffer Layout:
|
|
24
|
+
* ```
|
|
25
|
+
* ┌─────────┬────────────┬──────────────────┐
|
|
26
|
+
* │ Type ID │ Tick │ Updates (codec) │
|
|
27
|
+
* │ (u8) │ (u32) │ (variable) │
|
|
28
|
+
* │ 1 byte │ 4 bytes │ N bytes │
|
|
29
|
+
* └─────────┴────────────┴──────────────────┘
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { SnapshotRegistry } from './protocol/snapshot';
|
|
35
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
36
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
37
|
+
*
|
|
38
|
+
* // Define different update types
|
|
39
|
+
* interface PlayerUpdate {
|
|
40
|
+
* players: Array<{ entityId: number; x: number; y: number }>;
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* interface ScoreUpdate {
|
|
44
|
+
* score: number;
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* // Create registry
|
|
48
|
+
* const registry = new SnapshotRegistry<PlayerUpdate | ScoreUpdate>();
|
|
49
|
+
*
|
|
50
|
+
* // Register codecs for each update type
|
|
51
|
+
* registry.register('players', new PooledCodec({
|
|
52
|
+
* players: // schema
|
|
53
|
+
* }));
|
|
54
|
+
*
|
|
55
|
+
* registry.register('score', new PooledCodec({
|
|
56
|
+
* score: BinaryCodec.u32
|
|
57
|
+
* }));
|
|
58
|
+
*
|
|
59
|
+
* // Server: Encode specific update type
|
|
60
|
+
* const buf = registry.encode('players', {
|
|
61
|
+
* tick: 100,
|
|
62
|
+
* updates: { players: [{ entityId: 1, x: 5, y: 10 }] }
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Client: Decode (type is embedded in message)
|
|
66
|
+
* const { type, snapshot } = registry.decode(buf);
|
|
67
|
+
* applySnapshot(state, snapshot);
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export declare class SnapshotRegistry<T> {
|
|
71
|
+
private codecs;
|
|
72
|
+
private typeIds;
|
|
73
|
+
private idToType;
|
|
74
|
+
private nextId;
|
|
75
|
+
/**
|
|
76
|
+
* Register a codec for a specific update type.
|
|
77
|
+
* Call this once per update type at startup.
|
|
78
|
+
*/
|
|
79
|
+
register<U extends Partial<T>>(type: string, codec: Codec<U>): void;
|
|
80
|
+
/**
|
|
81
|
+
* Encode a snapshot with a specific update type.
|
|
82
|
+
* Format: [typeId: u8][tick: u32][updates: encoded by codec]
|
|
83
|
+
*
|
|
84
|
+
* Efficiently writes to a single buffer:
|
|
85
|
+
* - PooledCodec writes updates to its pooled buffer
|
|
86
|
+
* - We allocate ONE final buffer for [typeId + tick + updates]
|
|
87
|
+
* - Direct memory copy, no intermediate allocations
|
|
88
|
+
*/
|
|
89
|
+
encode<U extends Partial<T>>(type: string, snapshot: Snapshot<U>): Uint8Array;
|
|
90
|
+
/**
|
|
91
|
+
* Decode a snapshot and return both the type and the snapshot.
|
|
92
|
+
*/
|
|
93
|
+
decode(buf: Uint8Array): {
|
|
94
|
+
type: string;
|
|
95
|
+
snapshot: Snapshot<Partial<T>>;
|
|
96
|
+
};
|
|
97
|
+
has(type: string): boolean;
|
|
98
|
+
getTypes(): string[];
|
|
99
|
+
}
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry for multiple snapshot types with different update schemas.
|
|
3
|
+
*
|
|
4
|
+
* This allows efficient delta encoding by only sending specific update types
|
|
5
|
+
* instead of encoding all fields (including empty/nil ones).
|
|
6
|
+
*
|
|
7
|
+
* ## Memory Efficiency
|
|
8
|
+
*
|
|
9
|
+
* The encoding path minimizes allocations:
|
|
10
|
+
* 1. PooledCodec acquires a buffer from its pool (reused across calls)
|
|
11
|
+
* 2. PooledCodec writes data directly to the buffer at sequential offsets
|
|
12
|
+
* 3. SnapshotRegistry creates ONE final buffer: [typeId(1) + tick(4) + updatesBytes]
|
|
13
|
+
* 4. Total allocations per encode: 1 pooled buffer + 1 final buffer
|
|
14
|
+
*
|
|
15
|
+
* Buffer Layout:
|
|
16
|
+
* ```
|
|
17
|
+
* ┌─────────┬────────────┬──────────────────┐
|
|
18
|
+
* │ Type ID │ Tick │ Updates (codec) │
|
|
19
|
+
* │ (u8) │ (u32) │ (variable) │
|
|
20
|
+
* │ 1 byte │ 4 bytes │ N bytes │
|
|
21
|
+
* └─────────┴────────────┴──────────────────┘
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { SnapshotRegistry } from './protocol/snapshot';
|
|
27
|
+
* import { PooledCodec } from './core/pooled-codec';
|
|
28
|
+
* import { BinaryCodec } from './core/binary-codec';
|
|
29
|
+
*
|
|
30
|
+
* // Define different update types
|
|
31
|
+
* interface PlayerUpdate {
|
|
32
|
+
* players: Array<{ entityId: number; x: number; y: number }>;
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* interface ScoreUpdate {
|
|
36
|
+
* score: number;
|
|
37
|
+
* }
|
|
38
|
+
*
|
|
39
|
+
* // Create registry
|
|
40
|
+
* const registry = new SnapshotRegistry<PlayerUpdate | ScoreUpdate>();
|
|
41
|
+
*
|
|
42
|
+
* // Register codecs for each update type
|
|
43
|
+
* registry.register('players', new PooledCodec({
|
|
44
|
+
* players: // schema
|
|
45
|
+
* }));
|
|
46
|
+
*
|
|
47
|
+
* registry.register('score', new PooledCodec({
|
|
48
|
+
* score: BinaryCodec.u32
|
|
49
|
+
* }));
|
|
50
|
+
*
|
|
51
|
+
* // Server: Encode specific update type
|
|
52
|
+
* const buf = registry.encode('players', {
|
|
53
|
+
* tick: 100,
|
|
54
|
+
* updates: { players: [{ entityId: 1, x: 5, y: 10 }] }
|
|
55
|
+
* });
|
|
56
|
+
*
|
|
57
|
+
* // Client: Decode (type is embedded in message)
|
|
58
|
+
* const { type, snapshot } = registry.decode(buf);
|
|
59
|
+
* applySnapshot(state, snapshot);
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export class SnapshotRegistry {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.codecs = new Map();
|
|
65
|
+
this.typeIds = new Map();
|
|
66
|
+
this.idToType = new Map();
|
|
67
|
+
this.nextId = 0;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Register a codec for a specific update type.
|
|
71
|
+
* Call this once per update type at startup.
|
|
72
|
+
*/
|
|
73
|
+
register(type, codec) {
|
|
74
|
+
if (this.codecs.has(type)) {
|
|
75
|
+
throw new Error(`Snapshot type "${type}" is already registered`);
|
|
76
|
+
}
|
|
77
|
+
const typeId = this.nextId++;
|
|
78
|
+
this.codecs.set(type, codec);
|
|
79
|
+
this.typeIds.set(type, typeId);
|
|
80
|
+
this.idToType.set(typeId, type);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Encode a snapshot with a specific update type.
|
|
84
|
+
* Format: [typeId: u8][tick: u32][updates: encoded by codec]
|
|
85
|
+
*
|
|
86
|
+
* Efficiently writes to a single buffer:
|
|
87
|
+
* - PooledCodec writes updates to its pooled buffer
|
|
88
|
+
* - We allocate ONE final buffer for [typeId + tick + updates]
|
|
89
|
+
* - Direct memory copy, no intermediate allocations
|
|
90
|
+
*/
|
|
91
|
+
encode(type, snapshot) {
|
|
92
|
+
const codec = this.codecs.get(type);
|
|
93
|
+
const typeId = this.typeIds.get(type);
|
|
94
|
+
if (!codec || typeId === undefined) {
|
|
95
|
+
throw new Error(`No codec registered for snapshot type "${type}"`);
|
|
96
|
+
}
|
|
97
|
+
// Encode updates using the specific codec (acquires from pool)
|
|
98
|
+
const updatesBytes = codec.encode(snapshot.updates);
|
|
99
|
+
// Allocate single buffer for complete message
|
|
100
|
+
const buf = new Uint8Array(1 + 4 + updatesBytes.length);
|
|
101
|
+
// Write type ID (1 byte)
|
|
102
|
+
buf[0] = typeId;
|
|
103
|
+
// Write tick (4 bytes, little-endian)
|
|
104
|
+
new DataView(buf.buffer).setUint32(1, snapshot.tick, true);
|
|
105
|
+
// Write updates (direct copy)
|
|
106
|
+
buf.set(updatesBytes, 5);
|
|
107
|
+
return buf;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Decode a snapshot and return both the type and the snapshot.
|
|
111
|
+
*/
|
|
112
|
+
decode(buf) {
|
|
113
|
+
// Decode type ID (first byte)
|
|
114
|
+
const typeId = buf[0];
|
|
115
|
+
const type = this.idToType.get(typeId);
|
|
116
|
+
if (!type) {
|
|
117
|
+
throw new Error(`Unknown snapshot type ID: ${typeId}`);
|
|
118
|
+
}
|
|
119
|
+
const codec = this.codecs.get(type);
|
|
120
|
+
if (!codec) {
|
|
121
|
+
throw new Error(`No codec registered for snapshot type "${type}"`);
|
|
122
|
+
}
|
|
123
|
+
// Decode tick (bytes 1-4)
|
|
124
|
+
const tick = new DataView(buf.buffer, buf.byteOffset + 1).getUint32(0, true);
|
|
125
|
+
// Decode updates (remaining bytes)
|
|
126
|
+
const updatesBytes = buf.subarray(5);
|
|
127
|
+
const updates = codec.decode(updatesBytes);
|
|
128
|
+
return { type, snapshot: { tick, updates } };
|
|
129
|
+
}
|
|
130
|
+
has(type) {
|
|
131
|
+
return this.codecs.has(type);
|
|
132
|
+
}
|
|
133
|
+
getTypes() {
|
|
134
|
+
return Array.from(this.codecs.keys());
|
|
135
|
+
}
|
|
136
|
+
}
|