@net-mesh/browser 0.37.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.
- package/CHANGELOG.md +517 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-MIT +21 -0
- package/README.md +666 -0
- package/dist/async-queue.d.ts +26 -0
- package/dist/async-queue.js +62 -0
- package/dist/credential.d.ts +70 -0
- package/dist/credential.js +95 -0
- package/dist/errors.d.ts +400 -0
- package/dist/errors.js +726 -0
- package/dist/events.d.ts +232 -0
- package/dist/events.js +298 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +78 -0
- package/dist/index.bundle.js +2 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +46 -0
- package/dist/leader/events.d.ts +111 -0
- package/dist/leader/events.js +116 -0
- package/dist/leader/index.d.ts +14 -0
- package/dist/leader/index.js +11 -0
- package/dist/leader/session.d.ts +539 -0
- package/dist/leader/session.js +770 -0
- package/dist/leader/wasm.d.ts +123 -0
- package/dist/leader/wasm.js +39 -0
- package/dist/lobby.d.ts +183 -0
- package/dist/lobby.js +504 -0
- package/dist/local.d.ts +80 -0
- package/dist/local.js +160 -0
- package/dist/net_leaf.d.ts +1494 -0
- package/dist/net_leaf.js +3753 -0
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +137 -0
- package/dist/netcode/client.d.ts +102 -0
- package/dist/netcode/client.js +296 -0
- package/dist/netcode/clock.d.ts +45 -0
- package/dist/netcode/clock.js +54 -0
- package/dist/netcode/host.d.ts +118 -0
- package/dist/netcode/host.js +276 -0
- package/dist/netcode/index.d.ts +19 -0
- package/dist/netcode/index.js +14 -0
- package/dist/netcode/interpolate.d.ts +50 -0
- package/dist/netcode/interpolate.js +100 -0
- package/dist/netcode/wire.d.ts +125 -0
- package/dist/netcode/wire.js +117 -0
- package/dist/node.d.ts +908 -0
- package/dist/node.js +1085 -0
- package/dist/org.d.ts +412 -0
- package/dist/org.js +567 -0
- package/dist/peer-driver.d.ts +117 -0
- package/dist/peer-driver.js +224 -0
- package/dist/store/assembly.d.ts +176 -0
- package/dist/store/assembly.js +325 -0
- package/dist/store/chunker.d.ts +83 -0
- package/dist/store/chunker.js +190 -0
- package/dist/store/core.d.ts +136 -0
- package/dist/store/core.js +477 -0
- package/dist/store/definition.d.ts +23 -0
- package/dist/store/definition.js +83 -0
- package/dist/store/errors.d.ts +77 -0
- package/dist/store/errors.js +94 -0
- package/dist/store/host.d.ts +247 -0
- package/dist/store/host.js +675 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.js +40 -0
- package/dist/store/interest.d.ts +40 -0
- package/dist/store/interest.js +77 -0
- package/dist/store/inventory.d.ts +81 -0
- package/dist/store/inventory.js +140 -0
- package/dist/store/join.d.ts +118 -0
- package/dist/store/join.js +724 -0
- package/dist/store/json.d.ts +50 -0
- package/dist/store/json.js +283 -0
- package/dist/store/ledger.d.ts +187 -0
- package/dist/store/ledger.js +292 -0
- package/dist/store/owner.d.ts +616 -0
- package/dist/store/owner.js +1711 -0
- package/dist/store/patch.d.ts +70 -0
- package/dist/store/patch.js +166 -0
- package/dist/store/player.d.ts +65 -0
- package/dist/store/player.js +261 -0
- package/dist/store/replica.d.ts +187 -0
- package/dist/store/replica.js +740 -0
- package/dist/store/state.d.ts +42 -0
- package/dist/store/state.js +165 -0
- package/dist/store/types.d.ts +200 -0
- package/dist/store/types.js +25 -0
- package/dist/store/visibility.d.ts +118 -0
- package/dist/store/visibility.js +282 -0
- package/dist/store/wire.d.ts +309 -0
- package/dist/store/wire.js +834 -0
- package/dist/stream.d.ts +174 -0
- package/dist/stream.js +238 -0
- package/dist/three/index.d.ts +109 -0
- package/dist/three/index.js +130 -0
- package/dist/udp-probe.d.ts +159 -0
- package/dist/udp-probe.js +245 -0
- package/dist/wasm.d.ts +642 -0
- package/dist/wasm.js +98 -0
- package/dist/world/border.d.ts +84 -0
- package/dist/world/border.js +99 -0
- package/dist/world/handoff.d.ts +187 -0
- package/dist/world/handoff.js +222 -0
- package/dist/world/index.d.ts +13 -0
- package/dist/world/index.js +9 -0
- package/dist/world/link.d.ts +179 -0
- package/dist/world/link.js +476 -0
- package/dist/world/regions.d.ts +154 -0
- package/dist/world/regions.js +314 -0
- package/package.json +72 -0
|
Binary file
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
export const memory: WebAssembly.Memory;
|
|
4
|
+
export const __wbg_leafnode_free: (a: number, b: number) => void;
|
|
5
|
+
export const __wbg_leafstream_free: (a: number, b: number) => void;
|
|
6
|
+
export const __wbg_meshsession_free: (a: number, b: number) => void;
|
|
7
|
+
export const __wbg_orgbytestreamhandle_free: (a: number, b: number) => void;
|
|
8
|
+
export const __wbg_orgduplexcallhandle_free: (a: number, b: number) => void;
|
|
9
|
+
export const __wbg_orgrequeststreamhandle_free: (a: number, b: number) => void;
|
|
10
|
+
export const __wbg_orgresponsesinkhandle_free: (a: number, b: number) => void;
|
|
11
|
+
export const __wbg_orgservehandle_free: (a: number, b: number) => void;
|
|
12
|
+
export const __wbg_orguploadcallhandle_free: (a: number, b: number) => void;
|
|
13
|
+
export const __wbg_proxystream_free: (a: number, b: number) => void;
|
|
14
|
+
export const leafnode_anchor_id_hex: (a: number, b: number) => void;
|
|
15
|
+
export const leafnode_announce: (a: number, b: number, c: number) => number;
|
|
16
|
+
export const leafnode_apply_org_revocation_bundle: (a: number, b: number, c: number, d: number) => void;
|
|
17
|
+
export const leafnode_arm_network_retry: (a: number, b: number) => void;
|
|
18
|
+
export const leafnode_call: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
|
19
|
+
export const leafnode_call_org: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
20
|
+
export const leafnode_call_org_client_stream: (a: number, b: number, c: number, d: number) => number;
|
|
21
|
+
export const leafnode_call_org_duplex: (a: number, b: number, c: number, d: number) => number;
|
|
22
|
+
export const leafnode_call_org_streaming: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
23
|
+
export const leafnode_close: (a: number) => void;
|
|
24
|
+
export const leafnode_connect: (a: number) => number;
|
|
25
|
+
export const leafnode_counters_json: (a: number, b: number) => void;
|
|
26
|
+
export const leafnode_effective_ice_servers: (a: number, b: number) => void;
|
|
27
|
+
export const leafnode_effective_stream_options: (a: number, b: number) => void;
|
|
28
|
+
export const leafnode_enroll: (a: number) => number;
|
|
29
|
+
export const leafnode_is_enrolled: (a: number) => number;
|
|
30
|
+
export const leafnode_listener_count: (a: number) => number;
|
|
31
|
+
export const leafnode_maxEventBytes: () => number;
|
|
32
|
+
export const leafnode_node_id_hex: (a: number, b: number) => void;
|
|
33
|
+
export const leafnode_on_event: (a: number, b: number, c: number) => void;
|
|
34
|
+
export const leafnode_open_stream: (a: number, b: number, c: number) => void;
|
|
35
|
+
export const leafnode_origin_hash_hex: (a: number, b: number) => void;
|
|
36
|
+
export const leafnode_peer_accept_offer: (a: number, b: number, c: number) => number;
|
|
37
|
+
export const leafnode_peer_candidate: (a: number, b: number, c: number) => number;
|
|
38
|
+
export const leafnode_peer_candidate_in: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
39
|
+
export const leafnode_peer_direct_dialog: (a: number, b: number, c: number, d: number) => void;
|
|
40
|
+
export const leafnode_peer_handshake: (a: number, b: number, c: number) => number;
|
|
41
|
+
export const leafnode_peer_handshake_in: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
42
|
+
export const leafnode_peer_offer: (a: number, b: number, c: number) => number;
|
|
43
|
+
export const leafnode_publish: (a: number, b: number, c: number, d: number) => number;
|
|
44
|
+
export const leafnode_query: (a: number, b: number, c: number) => number;
|
|
45
|
+
export const leafnode_remove_listener: (a: number, b: number, c: number) => number;
|
|
46
|
+
export const leafnode_retire: (a: number, b: bigint) => number;
|
|
47
|
+
export const leafnode_retry_report: (a: number, b: number) => void;
|
|
48
|
+
export const leafnode_rtc_stats_json: (a: number, b: number) => void;
|
|
49
|
+
export const leafnode_serve_org: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
50
|
+
export const leafnode_serve_org_client_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
51
|
+
export const leafnode_serve_org_duplex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
52
|
+
export const leafnode_serve_org_streaming: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
53
|
+
export const leafnode_signal: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
|
|
54
|
+
export const leafnode_subscribe: (a: number, b: number, c: number) => number;
|
|
55
|
+
export const leafnode_unsubscribe: (a: number, b: number, c: number) => number;
|
|
56
|
+
export const leafstream_close: (a: number) => void;
|
|
57
|
+
export const leafstream_incarnation: (a: number, b: number) => void;
|
|
58
|
+
export const leafstream_is_reliable: (a: number) => number;
|
|
59
|
+
export const leafstream_on_message: (a: number, b: number, c: number) => void;
|
|
60
|
+
export const leafstream_peer_node_hex: (a: number, b: number) => void;
|
|
61
|
+
export const leafstream_remove_listener: (a: number, b: number, c: number) => number;
|
|
62
|
+
export const leafstream_send: (a: number, b: number, c: number) => void;
|
|
63
|
+
export const leafstream_stream_id_hex: (a: number, b: number) => void;
|
|
64
|
+
export const meshsession_announce: (a: number, b: number, c: number) => number;
|
|
65
|
+
export const meshsession_call: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
|
|
66
|
+
export const meshsession_call_org: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
67
|
+
export const meshsession_call_org_client_stream: (a: number, b: number, c: number, d: number) => number;
|
|
68
|
+
export const meshsession_call_org_duplex: (a: number, b: number, c: number, d: number) => number;
|
|
69
|
+
export const meshsession_call_org_streaming: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
70
|
+
export const meshsession_close: (a: number) => void;
|
|
71
|
+
export const meshsession_counters_json: (a: number) => number;
|
|
72
|
+
export const meshsession_enroll: (a: number) => number;
|
|
73
|
+
export const meshsession_fingerprint: (a: number, b: number) => void;
|
|
74
|
+
export const meshsession_generation: (a: number, b: number) => void;
|
|
75
|
+
export const meshsession_interruption_ms: (a: number, b: number) => void;
|
|
76
|
+
export const meshsession_is_enrolled: (a: number) => number;
|
|
77
|
+
export const meshsession_node_id_hex: (a: number, b: number) => void;
|
|
78
|
+
export const meshsession_on_event: (a: number, b: number) => void;
|
|
79
|
+
export const meshsession_open: (a: number) => number;
|
|
80
|
+
export const meshsession_open_stream: (a: number, b: number) => number;
|
|
81
|
+
export const meshsession_peer_accept_offer: (a: number, b: number, c: number) => number;
|
|
82
|
+
export const meshsession_peer_candidate: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
83
|
+
export const meshsession_peer_direct_dialog: (a: number, b: number, c: number) => number;
|
|
84
|
+
export const meshsession_peer_handshake: (a: number, b: number, c: number, d: number, e: number) => number;
|
|
85
|
+
export const meshsession_peer_offer: (a: number, b: number, c: number) => number;
|
|
86
|
+
export const meshsession_publish: (a: number, b: number, c: number, d: number) => number;
|
|
87
|
+
export const meshsession_query: (a: number, b: number, c: number) => number;
|
|
88
|
+
export const meshsession_role: (a: number, b: number) => void;
|
|
89
|
+
export const meshsession_scope: (a: number, b: number) => void;
|
|
90
|
+
export const meshsession_serve_org: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
91
|
+
export const meshsession_serve_org_client_stream: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
92
|
+
export const meshsession_serve_org_duplex: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
93
|
+
export const meshsession_serve_org_streaming: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
|
|
94
|
+
export const meshsession_signal: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
|
|
95
|
+
export const meshsession_subscribe: (a: number, b: number, c: number) => number;
|
|
96
|
+
export const meshsession_unsubscribe: (a: number, b: number, c: number) => number;
|
|
97
|
+
export const orgbytestreamhandle_cancel: (a: number) => void;
|
|
98
|
+
export const orgbytestreamhandle_next: (a: number) => number;
|
|
99
|
+
export const orgduplexcallhandle_cancel: (a: number) => void;
|
|
100
|
+
export const orgduplexcallhandle_finish_sending: (a: number) => number;
|
|
101
|
+
export const orgduplexcallhandle_send: (a: number, b: number) => number;
|
|
102
|
+
export const orgduplexcallhandle_stream: (a: number) => number;
|
|
103
|
+
export const orgrequeststreamhandle_next: (a: number) => number;
|
|
104
|
+
export const orgrequeststreamhandle_retired: (a: number) => number;
|
|
105
|
+
export const orgresponsesinkhandle_close: (a: number) => number;
|
|
106
|
+
export const orgresponsesinkhandle_retired: (a: number) => number;
|
|
107
|
+
export const orgresponsesinkhandle_send: (a: number, b: number) => number;
|
|
108
|
+
export const orgservehandle_close: (a: number) => void;
|
|
109
|
+
export const orgservehandle_service: (a: number, b: number) => void;
|
|
110
|
+
export const orguploadcallhandle_cancel: (a: number) => void;
|
|
111
|
+
export const orguploadcallhandle_finish: (a: number) => number;
|
|
112
|
+
export const orguploadcallhandle_send: (a: number, b: number) => number;
|
|
113
|
+
export const proxystream_close: (a: number) => void;
|
|
114
|
+
export const proxystream_generation: (a: number, b: number) => void;
|
|
115
|
+
export const proxystream_incarnation: (a: number, b: number) => void;
|
|
116
|
+
export const proxystream_is_reliable: (a: number) => number;
|
|
117
|
+
export const proxystream_on_message: (a: number, b: number) => void;
|
|
118
|
+
export const proxystream_peer_node_hex: (a: number, b: number) => void;
|
|
119
|
+
export const proxystream_send: (a: number, b: number) => number;
|
|
120
|
+
export const proxystream_stream_id_hex: (a: number, b: number) => void;
|
|
121
|
+
export const start: () => void;
|
|
122
|
+
export const __wasm_bindgen_func_elem_6504: (a: number, b: number, c: number, d: number) => void;
|
|
123
|
+
export const __wasm_bindgen_func_elem_6506: (a: number, b: number, c: number, d: number) => void;
|
|
124
|
+
export const __wasm_bindgen_func_elem_2355: (a: number, b: number, c: number) => number;
|
|
125
|
+
export const __wasm_bindgen_func_elem_2356: (a: number, b: number, c: number) => void;
|
|
126
|
+
export const __wasm_bindgen_func_elem_2356_112: (a: number, b: number, c: number) => void;
|
|
127
|
+
export const __wasm_bindgen_func_elem_2356_113: (a: number, b: number, c: number) => void;
|
|
128
|
+
export const __wasm_bindgen_func_elem_2356_114: (a: number, b: number, c: number) => void;
|
|
129
|
+
export const __wasm_bindgen_func_elem_2356_115: (a: number, b: number, c: number) => void;
|
|
130
|
+
export const __wasm_bindgen_func_elem_2356_116: (a: number, b: number, c: number) => void;
|
|
131
|
+
export const __wbindgen_export: (a: number, b: number) => number;
|
|
132
|
+
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
133
|
+
export const __wbindgen_export3: (a: number) => void;
|
|
134
|
+
export const __wbindgen_export4: (a: number, b: number) => void;
|
|
135
|
+
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
|
|
136
|
+
export const __wbindgen_export5: (a: number, b: number, c: number) => void;
|
|
137
|
+
export const __wbindgen_start: () => void;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `joinNetcode` — a player's side of netcode model 2: clock sync with the
|
|
3
|
+
* host, **snapshot interpolation** for everyone else, **local prediction
|
|
4
|
+
* with reconciliation** for yourself.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const net = joinNetcode({
|
|
8
|
+
* transport: node,
|
|
9
|
+
* host: lobbyHostId,
|
|
10
|
+
* label: 'my-game.movement',
|
|
11
|
+
* local: { id: node.nodeIdHex()!, predict: (ship, input) => move(ship, input) },
|
|
12
|
+
* });
|
|
13
|
+
* onKey(input => net.input(input)); // applied locally at once, sent to the host
|
|
14
|
+
* renderLoop(() => draw(net.view())); // remote ships interpolated, yours predicted
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { type ClockEstimate } from './clock.js';
|
|
18
|
+
import { type Interpolator } from './interpolate.js';
|
|
19
|
+
import { type NetcodeTransport, type Now } from './wire.js';
|
|
20
|
+
/** Your own entity, predicted locally. */
|
|
21
|
+
export interface LocalEntity<E, I> {
|
|
22
|
+
/** Its id in the host's snapshots (usually your node id). */
|
|
23
|
+
readonly id: string;
|
|
24
|
+
/** Apply one input to a state — the same rule the host applies. */
|
|
25
|
+
predict(state: E, input: I): E;
|
|
26
|
+
}
|
|
27
|
+
/** {@link joinNetcode}'s argument. */
|
|
28
|
+
export interface JoinNetcodeOptions<E, I> {
|
|
29
|
+
readonly transport: NetcodeTransport;
|
|
30
|
+
/** The host's node id, 16 hex. */
|
|
31
|
+
readonly host: string;
|
|
32
|
+
/** The same label the host uses. */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
/** How far behind the host clock remote entities render. Default 100 ms
|
|
35
|
+
* — at least two snapshot intervals, so one lost snapshot does not
|
|
36
|
+
* stall them. */
|
|
37
|
+
readonly interpolationDelayMs?: number;
|
|
38
|
+
/** Blend two states of an entity. Default {@link lerpNumbers}. */
|
|
39
|
+
readonly interpolate?: Interpolator<E>;
|
|
40
|
+
/**
|
|
41
|
+
* When the newest snapshot is older than the render time (a late or lost
|
|
42
|
+
* snapshot), carry remote entities on along their last motion for at most
|
|
43
|
+
* this long, ms, instead of freezing them. Default 0: hold. Extrapolation
|
|
44
|
+
* guesses; a wrong guess is corrected by the next snapshot, visibly. Your
|
|
45
|
+
* `interpolate`, if you supply one, is then called with `alpha > 1`.
|
|
46
|
+
*/
|
|
47
|
+
readonly extrapolateMs?: number;
|
|
48
|
+
/** Predict and reconcile this entity. Omit for a spectator. */
|
|
49
|
+
readonly local?: LocalEntity<E, I>;
|
|
50
|
+
/** Unacknowledged inputs repeated in each input frame. Default 8. */
|
|
51
|
+
readonly inputRedundancy?: number;
|
|
52
|
+
/** Clock pings per second. Default 4. */
|
|
53
|
+
readonly pingRate?: number;
|
|
54
|
+
/**
|
|
55
|
+
* The interest keys to receive (the host's `interest` keys entities; an
|
|
56
|
+
* entity keyed `null` always arrives). Omit to receive everything the host
|
|
57
|
+
* shows you. Change it with {@link NetcodeClient.setInterest}.
|
|
58
|
+
*/
|
|
59
|
+
readonly interest?: readonly string[];
|
|
60
|
+
/**
|
|
61
|
+
* How long a correction takes to show, ms. Default 100; `0` snaps.
|
|
62
|
+
*
|
|
63
|
+
* When reconciliation moves your entity, the view blends from where it
|
|
64
|
+
* was drawn to where the host says it is, instead of jumping. Inputs
|
|
65
|
+
* keep applying to both during the blend, so it never lags your
|
|
66
|
+
* controls; it only closes the gap.
|
|
67
|
+
*/
|
|
68
|
+
readonly correctionSmoothingMs?: number;
|
|
69
|
+
readonly now?: Now;
|
|
70
|
+
}
|
|
71
|
+
/** What {@link NetcodeClient.stats} reports. */
|
|
72
|
+
export interface NetcodeStats {
|
|
73
|
+
readonly clock: ClockEstimate | null;
|
|
74
|
+
readonly snapshots: number;
|
|
75
|
+
/** Snapshots that arrived after a newer one. */
|
|
76
|
+
readonly lateSnapshots: number;
|
|
77
|
+
/** Inputs sent and not yet acknowledged by the host. */
|
|
78
|
+
readonly pendingInputs: number;
|
|
79
|
+
/** Times reconciliation moved the local entity away from its prediction. */
|
|
80
|
+
readonly corrections: number;
|
|
81
|
+
/** Chunked snapshots finished with chunks missing (their entities carried over). */
|
|
82
|
+
readonly partialSnapshots: number;
|
|
83
|
+
}
|
|
84
|
+
/** The running client. */
|
|
85
|
+
export interface NetcodeClient<E, I> {
|
|
86
|
+
/** Apply `input` locally now (prediction) and send it to the host. */
|
|
87
|
+
input(data: I): void;
|
|
88
|
+
/** The world to draw now: remote entities interpolated, yours predicted. */
|
|
89
|
+
view(): Readonly<Record<string, E>>;
|
|
90
|
+
/**
|
|
91
|
+
* Receive only entities under these interest keys from now on. Entities
|
|
92
|
+
* that leave the set disappear from later snapshots, and so from `view()`.
|
|
93
|
+
* Throws `RangeError` over 256 keys or for a key over 64 characters.
|
|
94
|
+
*/
|
|
95
|
+
setInterest(keys: readonly string[]): void;
|
|
96
|
+
/** The host clock, as estimated. `null` before the first pong. */
|
|
97
|
+
hostNow(): number | null;
|
|
98
|
+
stats(): NetcodeStats;
|
|
99
|
+
close(): void;
|
|
100
|
+
}
|
|
101
|
+
/** Join a host's netcode. */
|
|
102
|
+
export declare function joinNetcode<E, I>(options: JoinNetcodeOptions<E, I>): NetcodeClient<E, I>;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `joinNetcode` — a player's side of netcode model 2: clock sync with the
|
|
3
|
+
* host, **snapshot interpolation** for everyone else, **local prediction
|
|
4
|
+
* with reconciliation** for yourself.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* const net = joinNetcode({
|
|
8
|
+
* transport: node,
|
|
9
|
+
* host: lobbyHostId,
|
|
10
|
+
* label: 'my-game.movement',
|
|
11
|
+
* local: { id: node.nodeIdHex()!, predict: (ship, input) => move(ship, input) },
|
|
12
|
+
* });
|
|
13
|
+
* onKey(input => net.input(input)); // applied locally at once, sent to the host
|
|
14
|
+
* renderLoop(() => draw(net.view())); // remote ships interpolated, yours predicted
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
import { ClockEstimator } from './clock.js';
|
|
18
|
+
import { lerpNumbers, SnapshotBuffer } from './interpolate.js';
|
|
19
|
+
import { MAX_NETCODE_INTEREST_KEYS, MAX_NETCODE_INTEREST_KEY_LENGTH, chunkOf, defaultNow, decodeFrame, encodeFrame, eventPeer, peerHex, } from './wire.js';
|
|
20
|
+
/** Join a host's netcode. */
|
|
21
|
+
export function joinNetcode(options) {
|
|
22
|
+
const now = options.now ?? defaultNow;
|
|
23
|
+
const host = peerHex(options.host);
|
|
24
|
+
if (host === null)
|
|
25
|
+
throw new TypeError(`host is not a node id: ${JSON.stringify(options.host)}`);
|
|
26
|
+
const label = options.label;
|
|
27
|
+
const delay = options.interpolationDelayMs ?? 100;
|
|
28
|
+
const blend = options.interpolate ?? lerpNumbers;
|
|
29
|
+
const redundancy = Math.max(1, options.inputRedundancy ?? 8);
|
|
30
|
+
const clock = new ClockEstimator();
|
|
31
|
+
const snapshots = new SnapshotBuffer();
|
|
32
|
+
let stream = null;
|
|
33
|
+
let closed = false;
|
|
34
|
+
let seq = 0;
|
|
35
|
+
let pending = [];
|
|
36
|
+
let predicted = null;
|
|
37
|
+
// A correction being blended in: the stale prediction (still advanced
|
|
38
|
+
// by every input, so it keeps pace with the controls) and when it began.
|
|
39
|
+
let correcting = null;
|
|
40
|
+
const smoothing = Math.max(0, options.correctionSmoothingMs ?? 100);
|
|
41
|
+
/** The local entity as drawn now, and ends a finished blend. */
|
|
42
|
+
const drawnLocal = () => {
|
|
43
|
+
if (predicted === null || correcting === null)
|
|
44
|
+
return predicted;
|
|
45
|
+
const alpha = smoothing === 0 ? 1 : (now() - correcting.at) / smoothing;
|
|
46
|
+
if (alpha >= 1) {
|
|
47
|
+
correcting = null;
|
|
48
|
+
return predicted;
|
|
49
|
+
}
|
|
50
|
+
return blend(correcting.from, predicted, Math.max(0, alpha));
|
|
51
|
+
};
|
|
52
|
+
let snapshotsSeen = 0;
|
|
53
|
+
// The interest set: what was asked, its version, and the newest version a
|
|
54
|
+
// snapshot has echoed (the `w` frame repeats until they meet).
|
|
55
|
+
let interest = null;
|
|
56
|
+
let interestVersion = 0;
|
|
57
|
+
let interestAcked = 0;
|
|
58
|
+
const checkInterest = (keys) => {
|
|
59
|
+
if (keys.length > MAX_NETCODE_INTEREST_KEYS) {
|
|
60
|
+
throw new RangeError(`at most ${MAX_NETCODE_INTEREST_KEYS} interest keys, got ${keys.length}`);
|
|
61
|
+
}
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
if (typeof key !== 'string' || key.length > MAX_NETCODE_INTEREST_KEY_LENGTH) {
|
|
64
|
+
throw new RangeError(`an interest key is a string of at most ${MAX_NETCODE_INTEREST_KEY_LENGTH} characters`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...keys];
|
|
68
|
+
};
|
|
69
|
+
if (options.interest !== undefined) {
|
|
70
|
+
interest = checkInterest(options.interest);
|
|
71
|
+
interestVersion = 1;
|
|
72
|
+
}
|
|
73
|
+
const sendInterest = () => {
|
|
74
|
+
if (interest !== null && interestAcked < interestVersion) {
|
|
75
|
+
send({ n: label, k: 'w', v: interestVersion, w: interest });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
// Chunked snapshots being assembled, by tick; the newest tick accepted;
|
|
79
|
+
// snapshots finished with chunks missing.
|
|
80
|
+
const assemblies = new Map();
|
|
81
|
+
let newestTick = null;
|
|
82
|
+
let partialSnapshots = 0;
|
|
83
|
+
let corrections = 0;
|
|
84
|
+
let joined = false;
|
|
85
|
+
const offset = () => clock.estimate()?.offsetMs ?? null;
|
|
86
|
+
const hostNow = () => {
|
|
87
|
+
const o = offset();
|
|
88
|
+
return o === null ? null : now() + o;
|
|
89
|
+
};
|
|
90
|
+
let opening = null;
|
|
91
|
+
const open = () => {
|
|
92
|
+
opening ??= (async () => {
|
|
93
|
+
await options.transport.connectPeer?.(host).catch(() => { });
|
|
94
|
+
const opened = await options.transport.openStream({ reliability: 'fireAndForget', peer: host, label, lossy: true });
|
|
95
|
+
if (closed) {
|
|
96
|
+
opened.close();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
stream = opened;
|
|
100
|
+
})()
|
|
101
|
+
.catch(() => { })
|
|
102
|
+
.finally(() => {
|
|
103
|
+
opening = null;
|
|
104
|
+
});
|
|
105
|
+
return opening;
|
|
106
|
+
};
|
|
107
|
+
// A failed send retires the stream: the session under it may have been
|
|
108
|
+
// replaced (a reconnect, a relayed → direct upgrade) and a stream handle
|
|
109
|
+
// is fenced to the session it was opened on. The protocol repeats what
|
|
110
|
+
// was lost, so reopening is all recovery needs.
|
|
111
|
+
const send = (frame) => {
|
|
112
|
+
const current = stream;
|
|
113
|
+
if (current === null) {
|
|
114
|
+
void open();
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const retire = () => {
|
|
118
|
+
if (stream === current) {
|
|
119
|
+
stream = null;
|
|
120
|
+
try {
|
|
121
|
+
current.close();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Already unusable.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
try {
|
|
129
|
+
void Promise.resolve(current.send(encodeFrame(frame))).catch(retire);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
retire();
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const stopEvents = options.transport.onEvent(event => {
|
|
136
|
+
if (closed || event.type !== 'stream_data')
|
|
137
|
+
return;
|
|
138
|
+
if (eventPeer(event.peerNode) !== host)
|
|
139
|
+
return;
|
|
140
|
+
const frame = decodeFrame(event.payload, label);
|
|
141
|
+
if (frame === null)
|
|
142
|
+
return;
|
|
143
|
+
if (frame.k === 'q') {
|
|
144
|
+
clock.add(frame.t0, frame.t1, frame.t2, now());
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (frame.k !== 's')
|
|
148
|
+
return;
|
|
149
|
+
joined = true;
|
|
150
|
+
if (frame.iv !== undefined && frame.iv > interestAcked)
|
|
151
|
+
interestAcked = frame.iv;
|
|
152
|
+
if (frame.of === undefined || frame.c === undefined) {
|
|
153
|
+
accept(frame.tick, frame.t, frame.ack, frame.e, frame.e);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
// A chunk. Older assemblies still open when a newer tick shows up are
|
|
157
|
+
// finished as they are: their missing chunks were lost.
|
|
158
|
+
for (const [tick, open] of assemblies) {
|
|
159
|
+
if (tick < frame.tick)
|
|
160
|
+
finish(tick, open);
|
|
161
|
+
}
|
|
162
|
+
let assembly = assemblies.get(frame.tick);
|
|
163
|
+
if (assembly === undefined) {
|
|
164
|
+
if (newestTick !== null && frame.tick <= newestTick)
|
|
165
|
+
return; // already finished
|
|
166
|
+
assembly = { t: frame.t, ack: frame.ack, of: frame.of, parts: new Map() };
|
|
167
|
+
assemblies.set(frame.tick, assembly);
|
|
168
|
+
}
|
|
169
|
+
if (assembly.of !== frame.of)
|
|
170
|
+
return;
|
|
171
|
+
assembly.parts.set(frame.c, frame.e);
|
|
172
|
+
assembly.ack = Math.max(assembly.ack, frame.ack);
|
|
173
|
+
if (assembly.parts.size === assembly.of)
|
|
174
|
+
finish(frame.tick, assembly);
|
|
175
|
+
});
|
|
176
|
+
/**
|
|
177
|
+
* A chunked snapshot, done: every chunk, or the chunks that came, with
|
|
178
|
+
* each missing chunk's entities carried over from the previous snapshot
|
|
179
|
+
* ({@link chunkOf} is stable, so which entities a chunk carries is known
|
|
180
|
+
* whatever the previous snapshot's cut).
|
|
181
|
+
*/
|
|
182
|
+
function finish(tick, assembly) {
|
|
183
|
+
assemblies.delete(tick);
|
|
184
|
+
const received = {};
|
|
185
|
+
for (const part of assembly.parts.values())
|
|
186
|
+
Object.assign(received, part);
|
|
187
|
+
let entities = received;
|
|
188
|
+
if (assembly.parts.size < assembly.of) {
|
|
189
|
+
partialSnapshots += 1;
|
|
190
|
+
const previous = snapshots.latest();
|
|
191
|
+
if (previous !== null && previous.tick < tick) {
|
|
192
|
+
entities = { ...received };
|
|
193
|
+
for (const [id, entity] of Object.entries(previous.entities)) {
|
|
194
|
+
if (!assembly.parts.has(chunkOf(id, assembly.of)) && !(id in entities))
|
|
195
|
+
entities[id] = entity;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// Reconciled against what the host actually sent this tick: a local
|
|
200
|
+
// entity carried over from a lost chunk is not the host's word.
|
|
201
|
+
accept(tick, assembly.t, assembly.ack, entities, received);
|
|
202
|
+
}
|
|
203
|
+
/** One snapshot, whole or assembled: buffered, and reconciled against. */
|
|
204
|
+
function accept(tick, time, ack, entities, sent) {
|
|
205
|
+
snapshotsSeen += 1;
|
|
206
|
+
const newest = snapshots.latest();
|
|
207
|
+
snapshots.add({ time, tick, entities });
|
|
208
|
+
if (newestTick === null || tick > newestTick)
|
|
209
|
+
newestTick = tick;
|
|
210
|
+
// Reconcile only on a snapshot newer than any seen: an older one
|
|
211
|
+
// (reordered by the carrier) must not roll the local entity back.
|
|
212
|
+
if (options.local && (newest === null || tick > newest.tick)) {
|
|
213
|
+
pending = pending.filter(input => input.seq > ack);
|
|
214
|
+
const authoritative = Object.prototype.hasOwnProperty.call(sent, options.local.id)
|
|
215
|
+
? sent[options.local.id]
|
|
216
|
+
: undefined;
|
|
217
|
+
if (authoritative !== undefined) {
|
|
218
|
+
let replayed = authoritative;
|
|
219
|
+
for (const input of pending)
|
|
220
|
+
replayed = options.local.predict(replayed, input.data);
|
|
221
|
+
if (predicted !== null && JSON.stringify(replayed) !== JSON.stringify(predicted)) {
|
|
222
|
+
corrections += 1;
|
|
223
|
+
// Blend from what is on screen now, so a correction that lands
|
|
224
|
+
// mid-blend continues from there rather than jumping back.
|
|
225
|
+
if (smoothing > 0)
|
|
226
|
+
correcting = { from: drawnLocal() ?? predicted, at: now() };
|
|
227
|
+
}
|
|
228
|
+
predicted = replayed;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Join, and keep the clock honest.
|
|
233
|
+
const pingEvery = 1000 / Math.max(0.5, options.pingRate ?? 4);
|
|
234
|
+
const timer = setInterval(() => {
|
|
235
|
+
if (!joined)
|
|
236
|
+
send({ n: label, k: 'h' });
|
|
237
|
+
send({ n: label, k: 'p', t0: now() });
|
|
238
|
+
sendInterest();
|
|
239
|
+
// Unacknowledged inputs are repeated even with nothing new to send, so
|
|
240
|
+
// a lost input frame is repaired without waiting for the next input.
|
|
241
|
+
if (pending.length > 0)
|
|
242
|
+
send({ n: label, k: 'i', i: pending.slice(-redundancy) });
|
|
243
|
+
}, pingEvery);
|
|
244
|
+
void open().then(() => {
|
|
245
|
+
send({ n: label, k: 'h' });
|
|
246
|
+
send({ n: label, k: 'p', t0: now() });
|
|
247
|
+
sendInterest();
|
|
248
|
+
});
|
|
249
|
+
return {
|
|
250
|
+
input(data) {
|
|
251
|
+
if (closed)
|
|
252
|
+
return;
|
|
253
|
+
seq += 1;
|
|
254
|
+
const seen = (hostNow() ?? now()) - delay;
|
|
255
|
+
pending.push({ seq, seen, data });
|
|
256
|
+
if (options.local && predicted !== null) {
|
|
257
|
+
predicted = options.local.predict(predicted, data);
|
|
258
|
+
if (correcting !== null)
|
|
259
|
+
correcting = { ...correcting, from: options.local.predict(correcting.from, data) };
|
|
260
|
+
}
|
|
261
|
+
send({ n: label, k: 'i', i: pending.slice(-redundancy) });
|
|
262
|
+
},
|
|
263
|
+
view() {
|
|
264
|
+
const at = hostNow();
|
|
265
|
+
const latest = snapshots.latest();
|
|
266
|
+
const entities = at === null ? (latest?.entities ?? {}) : snapshots.at(at - delay, blend, options.extrapolateMs ?? 0);
|
|
267
|
+
const local = drawnLocal();
|
|
268
|
+
if (!options.local || local === null)
|
|
269
|
+
return entities;
|
|
270
|
+
return { ...entities, [options.local.id]: local };
|
|
271
|
+
},
|
|
272
|
+
setInterest(keys) {
|
|
273
|
+
interest = checkInterest(keys);
|
|
274
|
+
interestVersion += 1;
|
|
275
|
+
if (!closed)
|
|
276
|
+
sendInterest();
|
|
277
|
+
},
|
|
278
|
+
hostNow,
|
|
279
|
+
stats: () => ({
|
|
280
|
+
clock: clock.estimate(),
|
|
281
|
+
snapshots: snapshotsSeen,
|
|
282
|
+
lateSnapshots: snapshots.late,
|
|
283
|
+
pendingInputs: pending.length,
|
|
284
|
+
corrections,
|
|
285
|
+
partialSnapshots,
|
|
286
|
+
}),
|
|
287
|
+
close() {
|
|
288
|
+
if (closed)
|
|
289
|
+
return;
|
|
290
|
+
closed = true;
|
|
291
|
+
clearInterval(timer);
|
|
292
|
+
stopEvents();
|
|
293
|
+
stream?.close();
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock sync: one timeline for the host and its players.
|
|
3
|
+
*
|
|
4
|
+
* NTP-style. A player stamps a ping with its own clock (`t0`); the host
|
|
5
|
+
* stamps receipt (`t1`) and reply (`t2`) with its clock; the player stamps
|
|
6
|
+
* arrival (`t3`). Then
|
|
7
|
+
*
|
|
8
|
+
* ```text
|
|
9
|
+
* rtt = (t3 - t0) - (t2 - t1)
|
|
10
|
+
* offset = ((t1 - t0) + (t2 - t3)) / 2 // host clock − player clock
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* One sample is noisy — queueing on either path skews it — so the
|
|
14
|
+
* estimator keeps a window and takes the offset from the **lowest-RTT**
|
|
15
|
+
* samples, which are the ones least distorted by queueing. Jitter is the
|
|
16
|
+
* mean absolute deviation of RTT across the window. Pure arithmetic; the
|
|
17
|
+
* transport and timers live in the netcode client.
|
|
18
|
+
*/
|
|
19
|
+
/** A clock reading in milliseconds (e.g. `performance.now()`). */
|
|
20
|
+
export type Millis = number;
|
|
21
|
+
/** What the estimator currently believes. */
|
|
22
|
+
export interface ClockEstimate {
|
|
23
|
+
/** Host clock minus this clock, in ms. `hostNow = now + offset`. */
|
|
24
|
+
readonly offsetMs: number;
|
|
25
|
+
/** Smoothed round-trip time (median of the window), in ms. */
|
|
26
|
+
readonly rttMs: number;
|
|
27
|
+
/** Mean absolute deviation of round-trip time, in ms. */
|
|
28
|
+
readonly jitterMs: number;
|
|
29
|
+
/** Samples the estimate rests on. */
|
|
30
|
+
readonly samples: number;
|
|
31
|
+
}
|
|
32
|
+
/** Samples kept. */
|
|
33
|
+
export declare const CLOCK_WINDOW = 16;
|
|
34
|
+
/** Accumulates ping/pong samples into a clock estimate. */
|
|
35
|
+
export declare class ClockEstimator {
|
|
36
|
+
#private;
|
|
37
|
+
/**
|
|
38
|
+
* Add one exchange. Samples with a negative or non-finite RTT (clock
|
|
39
|
+
* weirdness, a reply to a ping from before a reload) are ignored.
|
|
40
|
+
* Returns whether the sample was used.
|
|
41
|
+
*/
|
|
42
|
+
add(t0: Millis, t1: Millis, t2: Millis, t3: Millis): boolean;
|
|
43
|
+
/** The current estimate, or `null` before the first sample. */
|
|
44
|
+
estimate(): ClockEstimate | null;
|
|
45
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clock sync: one timeline for the host and its players.
|
|
3
|
+
*
|
|
4
|
+
* NTP-style. A player stamps a ping with its own clock (`t0`); the host
|
|
5
|
+
* stamps receipt (`t1`) and reply (`t2`) with its clock; the player stamps
|
|
6
|
+
* arrival (`t3`). Then
|
|
7
|
+
*
|
|
8
|
+
* ```text
|
|
9
|
+
* rtt = (t3 - t0) - (t2 - t1)
|
|
10
|
+
* offset = ((t1 - t0) + (t2 - t3)) / 2 // host clock − player clock
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* One sample is noisy — queueing on either path skews it — so the
|
|
14
|
+
* estimator keeps a window and takes the offset from the **lowest-RTT**
|
|
15
|
+
* samples, which are the ones least distorted by queueing. Jitter is the
|
|
16
|
+
* mean absolute deviation of RTT across the window. Pure arithmetic; the
|
|
17
|
+
* transport and timers live in the netcode client.
|
|
18
|
+
*/
|
|
19
|
+
/** Samples kept. */
|
|
20
|
+
export const CLOCK_WINDOW = 16;
|
|
21
|
+
/** Of the window, how many lowest-RTT samples the offset averages. */
|
|
22
|
+
const BEST = 4;
|
|
23
|
+
/** Accumulates ping/pong samples into a clock estimate. */
|
|
24
|
+
export class ClockEstimator {
|
|
25
|
+
#window = [];
|
|
26
|
+
/**
|
|
27
|
+
* Add one exchange. Samples with a negative or non-finite RTT (clock
|
|
28
|
+
* weirdness, a reply to a ping from before a reload) are ignored.
|
|
29
|
+
* Returns whether the sample was used.
|
|
30
|
+
*/
|
|
31
|
+
add(t0, t1, t2, t3) {
|
|
32
|
+
const rtt = t3 - t0 - (t2 - t1);
|
|
33
|
+
const offset = (t1 - t0 + (t2 - t3)) / 2;
|
|
34
|
+
if (!Number.isFinite(rtt) || !Number.isFinite(offset) || rtt < 0)
|
|
35
|
+
return false;
|
|
36
|
+
this.#window.push({ rtt, offset });
|
|
37
|
+
if (this.#window.length > CLOCK_WINDOW)
|
|
38
|
+
this.#window.shift();
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** The current estimate, or `null` before the first sample. */
|
|
42
|
+
estimate() {
|
|
43
|
+
const window = this.#window;
|
|
44
|
+
if (window.length === 0)
|
|
45
|
+
return null;
|
|
46
|
+
const byRtt = [...window].sort((a, b) => a.rtt - b.rtt);
|
|
47
|
+
const best = byRtt.slice(0, Math.min(BEST, byRtt.length));
|
|
48
|
+
const offsetMs = best.reduce((sum, s) => sum + s.offset, 0) / best.length;
|
|
49
|
+
const rttMs = byRtt[Math.floor((byRtt.length - 1) / 2)].rtt;
|
|
50
|
+
const mean = window.reduce((sum, s) => sum + s.rtt, 0) / window.length;
|
|
51
|
+
const jitterMs = window.reduce((sum, s) => sum + Math.abs(s.rtt - mean), 0) / window.length;
|
|
52
|
+
return { offsetMs, rttMs, jitterMs, samples: window.length };
|
|
53
|
+
}
|
|
54
|
+
}
|