@driftengine/network 3.61.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +125 -0
- package/dist/authority.d.ts +170 -0
- package/dist/authority.js +235 -0
- package/dist/fingerprint.d.ts +64 -0
- package/dist/fingerprint.js +108 -0
- package/dist/fixture.d.ts +32 -0
- package/dist/fixture.js +136 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +32 -0
- package/dist/inputLog.d.ts +117 -0
- package/dist/inputLog.js +232 -0
- package/dist/lockstep.d.ts +181 -0
- package/dist/lockstep.js +291 -0
- package/dist/loopback.d.ts +115 -0
- package/dist/loopback.js +207 -0
- package/dist/rewind.d.ts +206 -0
- package/dist/rewind.js +212 -0
- package/dist/scriptSession.d.ts +77 -0
- package/dist/scriptSession.js +81 -0
- package/dist/simNumber.d.ts +103 -0
- package/dist/simNumber.js +156 -0
- package/dist/snapshotter.d.ts +43 -0
- package/dist/snapshotter.js +81 -0
- package/dist/transport.d.ts +63 -0
- package/dist/transport.js +40 -0
- package/dist/webrtc.d.ts +74 -0
- package/dist/webrtc.js +108 -0
- package/dist/websocket.d.ts +70 -0
- package/dist/websocket.js +105 -0
- package/dist/wire.d.ts +94 -0
- package/dist/wire.js +238 -0
- package/package.json +61 -0
- package/src/authority.ts +340 -0
- package/src/fingerprint.ts +143 -0
- package/src/fixture.ts +188 -0
- package/src/index.ts +95 -0
- package/src/inputLog.ts +262 -0
- package/src/lockstep.ts +374 -0
- package/src/loopback.ts +250 -0
- package/src/rewind.ts +337 -0
- package/src/scriptSession.ts +137 -0
- package/src/simNumber.ts +176 -0
- package/src/snapshotter.ts +98 -0
- package/src/transport.ts +69 -0
- package/src/webrtc.ts +141 -0
- package/src/websocket.ts +140 -0
- package/src/wire.ts +269 -0
package/src/wire.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framing, which is small on purpose.
|
|
3
|
+
*
|
|
4
|
+
* **An input payload is opaque bytes and this file never looks inside one.** `AGENTS.md`: engine
|
|
5
|
+
* APIs take positions, colours, sizes and time and never another game's noun. A wire format that
|
|
6
|
+
* knew a player had a throttle would be a racing engine's wire format.
|
|
7
|
+
*
|
|
8
|
+
* ---
|
|
9
|
+
*
|
|
10
|
+
* ## Fixed widths rather than varints
|
|
11
|
+
*
|
|
12
|
+
* A varint saves two bytes on a tick number and costs a branch per field and a length that cannot
|
|
13
|
+
* be known before encoding. At sixty inputs a second with a payload of a handful of bytes, the
|
|
14
|
+
* saving is under a kilobyte a minute and the cost is that nothing can be written into a
|
|
15
|
+
* preallocated buffer without measuring first. Fixed widths make an input frame a constant size,
|
|
16
|
+
* which is what lets a session own one buffer for the process.
|
|
17
|
+
*
|
|
18
|
+
* ## The widths, and what each one refuses
|
|
19
|
+
*
|
|
20
|
+
* - **Participant, one byte.** 255 participants in a session, and 255 is not a limit anybody is
|
|
21
|
+
* near: lockstep stops being viable in single figures, and an authoritative host with 255
|
|
22
|
+
* predicting clients has bandwidth problems long before it has an id problem.
|
|
23
|
+
* - **Tick, four bytes.** At 60 Hz an unsigned 32-bit tick counter is 2.2 years of continuous
|
|
24
|
+
* simulation. A session that reaches it has a different problem.
|
|
25
|
+
* - **Payload length, two bytes.** 65,535 bytes, which is far more than an input and far less than
|
|
26
|
+
* a full world. A state message that does not fit is a state message that should have been a
|
|
27
|
+
* delta.
|
|
28
|
+
*
|
|
29
|
+
* ## The version is in every `join`, not in every message
|
|
30
|
+
*
|
|
31
|
+
* A byte per message to say what the protocol was is a byte per message spent on something that
|
|
32
|
+
* cannot change mid-session. The handshake carries it and a mismatch is refused there, which is the
|
|
33
|
+
* one place a peer can still be told why.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export const PROTOCOL_VERSION = 1;
|
|
37
|
+
|
|
38
|
+
export const MESSAGE_JOIN = 1;
|
|
39
|
+
export const MESSAGE_WELCOME = 2;
|
|
40
|
+
export const MESSAGE_INPUT = 3;
|
|
41
|
+
export const MESSAGE_FINGERPRINT = 4;
|
|
42
|
+
export const MESSAGE_STATE = 5;
|
|
43
|
+
export const MESSAGE_ACK = 6;
|
|
44
|
+
export const MESSAGE_PING = 7;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Header bytes before an input run's payloads: kind, participant, first tick, count, stride.
|
|
48
|
+
*
|
|
49
|
+
* **An input message carries a *run* of consecutive ticks, and a run of one is the degenerate
|
|
50
|
+
* case.** That is not generality for its own sake — it is what makes lockstep survive a lossy link.
|
|
51
|
+
* An input is only useful for the tick it names, so retransmitting a lost one after a round trip
|
|
52
|
+
* delivers it too late to use; sending the last few inputs in *every* packet covers a loss before
|
|
53
|
+
* anybody notices, and costs a handful of bytes on a message that already has a header. A test at
|
|
54
|
+
* 15% loss is what turned this from a nicety into a requirement: without it a dropped input is lost
|
|
55
|
+
* for good and that peer's world is permanently wrong.
|
|
56
|
+
*/
|
|
57
|
+
export const INPUT_HEADER_BYTES = 1 + 1 + 4 + 1 + 2;
|
|
58
|
+
/** Header bytes before a state payload: kind, tick, length. */
|
|
59
|
+
export const STATE_HEADER_BYTES = 1 + 4 + 2;
|
|
60
|
+
/** A fingerprint message is fixed: kind, participant, tick, and eight bytes of digest. */
|
|
61
|
+
export const FINGERPRINT_BYTES = 1 + 1 + 4 + 8;
|
|
62
|
+
|
|
63
|
+
function writeU32(into: Uint8Array, at: number, value: number): void {
|
|
64
|
+
const v = value >>> 0;
|
|
65
|
+
into[at] = v & 0xff;
|
|
66
|
+
into[at + 1] = (v >>> 8) & 0xff;
|
|
67
|
+
into[at + 2] = (v >>> 16) & 0xff;
|
|
68
|
+
into[at + 3] = (v >>> 24) & 0xff;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readU32(from: Uint8Array, at: number): number {
|
|
72
|
+
return (
|
|
73
|
+
((from[at] as number) |
|
|
74
|
+
((from[at + 1] as number) << 8) |
|
|
75
|
+
((from[at + 2] as number) << 16) |
|
|
76
|
+
((from[at + 3] as number) << 24)) >>>
|
|
77
|
+
0
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** What a decoded message turned out to be. `kind` 0 means it did not decode. */
|
|
82
|
+
export interface DecodedMessage {
|
|
83
|
+
kind: number;
|
|
84
|
+
participant: number;
|
|
85
|
+
tick: number;
|
|
86
|
+
/** Where the payloads start, and how long **each one** is. Zero when there is none. */
|
|
87
|
+
payloadAt: number;
|
|
88
|
+
payloadBytes: number;
|
|
89
|
+
/** How many consecutive ticks this message carries, starting at `tick`. One for everything else. */
|
|
90
|
+
count: number;
|
|
91
|
+
/** The sixteen-hex-character digest, for a fingerprint message; empty otherwise. */
|
|
92
|
+
digest: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createDecoded(): DecodedMessage {
|
|
96
|
+
return { kind: 0, participant: 0, tick: 0, payloadAt: 0, payloadBytes: 0, count: 0, digest: '' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Write an input frame into `into`, answering how many bytes it used, or -1 when it does not fit.
|
|
101
|
+
*
|
|
102
|
+
* **-1 rather than a throw**, because the caller is a frame loop and `AGENTS.md` forbids throwing
|
|
103
|
+
* in one. A buffer too small is a caller's sizing bug and the honest report is a value they can
|
|
104
|
+
* branch on at the send site.
|
|
105
|
+
*/
|
|
106
|
+
export function encodeInput(
|
|
107
|
+
into: Uint8Array,
|
|
108
|
+
participant: number,
|
|
109
|
+
firstTick: number,
|
|
110
|
+
payloads: Uint8Array,
|
|
111
|
+
stride: number = payloads.length,
|
|
112
|
+
count = 1,
|
|
113
|
+
): number {
|
|
114
|
+
const total = INPUT_HEADER_BYTES + stride * count;
|
|
115
|
+
if (into.length < total || stride > 0xffff || count > 0xff || count < 1) return -1;
|
|
116
|
+
if (payloads.length < stride * count) return -1;
|
|
117
|
+
into[0] = MESSAGE_INPUT;
|
|
118
|
+
into[1] = participant & 0xff;
|
|
119
|
+
writeU32(into, 2, firstTick);
|
|
120
|
+
into[6] = count & 0xff;
|
|
121
|
+
into[7] = stride & 0xff;
|
|
122
|
+
into[8] = (stride >>> 8) & 0xff;
|
|
123
|
+
into.set(payloads.subarray(0, stride * count), INPUT_HEADER_BYTES);
|
|
124
|
+
return total;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function encodeState(into: Uint8Array, tick: number, payload: Uint8Array): number {
|
|
128
|
+
const total = STATE_HEADER_BYTES + payload.length;
|
|
129
|
+
if (into.length < total || payload.length > 0xffff) return -1;
|
|
130
|
+
into[0] = MESSAGE_STATE;
|
|
131
|
+
writeU32(into, 1, tick);
|
|
132
|
+
into[5] = payload.length & 0xff;
|
|
133
|
+
into[6] = (payload.length >>> 8) & 0xff;
|
|
134
|
+
into.set(payload, STATE_HEADER_BYTES);
|
|
135
|
+
return total;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function encodeFingerprint(
|
|
139
|
+
into: Uint8Array,
|
|
140
|
+
participant: number,
|
|
141
|
+
tick: number,
|
|
142
|
+
digest: string,
|
|
143
|
+
): number {
|
|
144
|
+
if (into.length < FINGERPRINT_BYTES || digest.length !== 16) return -1;
|
|
145
|
+
into[0] = MESSAGE_FINGERPRINT;
|
|
146
|
+
into[1] = participant & 0xff;
|
|
147
|
+
writeU32(into, 2, tick);
|
|
148
|
+
for (let i = 0; i < 8; i++) {
|
|
149
|
+
into[6 + i] = Number.parseInt(digest.slice(i * 2, i * 2 + 2), 16) & 0xff;
|
|
150
|
+
}
|
|
151
|
+
return FINGERPRINT_BYTES;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function encodeJoin(into: Uint8Array): number {
|
|
155
|
+
if (into.length < 3) return -1;
|
|
156
|
+
into[0] = MESSAGE_JOIN;
|
|
157
|
+
into[1] = PROTOCOL_VERSION & 0xff;
|
|
158
|
+
into[2] = (PROTOCOL_VERSION >>> 8) & 0xff;
|
|
159
|
+
return 3;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function encodeWelcome(
|
|
163
|
+
into: Uint8Array,
|
|
164
|
+
participant: number,
|
|
165
|
+
tick: number,
|
|
166
|
+
participants: number,
|
|
167
|
+
): number {
|
|
168
|
+
if (into.length < 7) return -1;
|
|
169
|
+
into[0] = MESSAGE_WELCOME;
|
|
170
|
+
into[1] = participant & 0xff;
|
|
171
|
+
writeU32(into, 2, tick);
|
|
172
|
+
into[6] = participants & 0xff;
|
|
173
|
+
return 7;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function encodeAck(into: Uint8Array, participant: number, tick: number): number {
|
|
177
|
+
if (into.length < 6) return -1;
|
|
178
|
+
into[0] = MESSAGE_ACK;
|
|
179
|
+
into[1] = participant & 0xff;
|
|
180
|
+
writeU32(into, 2, tick);
|
|
181
|
+
return 6;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const HEX = '0123456789abcdef';
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read a message's header into `out`. `false` when the bytes are not a message this version knows.
|
|
188
|
+
*
|
|
189
|
+
* **A payload is located rather than copied.** `payloadAt` and `payloadBytes` point into the buffer
|
|
190
|
+
* the caller was handed, which the transport may reuse the moment the sink returns — the same
|
|
191
|
+
* borrow `MessageSink` documents. A caller keeping a payload copies it, and one feeding it straight
|
|
192
|
+
* to `RewindLoop.supply` does not need to.
|
|
193
|
+
*/
|
|
194
|
+
export function decode(message: Uint8Array, out: DecodedMessage): boolean {
|
|
195
|
+
out.kind = 0;
|
|
196
|
+
out.participant = 0;
|
|
197
|
+
out.tick = 0;
|
|
198
|
+
out.payloadAt = 0;
|
|
199
|
+
out.payloadBytes = 0;
|
|
200
|
+
out.count = 0;
|
|
201
|
+
out.digest = '';
|
|
202
|
+
if (message.length < 1) return false;
|
|
203
|
+
|
|
204
|
+
const kind = message[0] as number;
|
|
205
|
+
switch (kind) {
|
|
206
|
+
case MESSAGE_INPUT: {
|
|
207
|
+
if (message.length < INPUT_HEADER_BYTES) return false;
|
|
208
|
+
const count = message[6] as number;
|
|
209
|
+
const stride = (message[7] as number) | ((message[8] as number) << 8);
|
|
210
|
+
if (count < 1 || stride < 1) return false;
|
|
211
|
+
if (message.length < INPUT_HEADER_BYTES + stride * count) return false;
|
|
212
|
+
out.kind = kind;
|
|
213
|
+
out.participant = message[1] as number;
|
|
214
|
+
out.tick = readU32(message, 2);
|
|
215
|
+
out.payloadAt = INPUT_HEADER_BYTES;
|
|
216
|
+
out.payloadBytes = stride;
|
|
217
|
+
out.count = count;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
case MESSAGE_STATE: {
|
|
221
|
+
if (message.length < STATE_HEADER_BYTES) return false;
|
|
222
|
+
const length = (message[5] as number) | ((message[6] as number) << 8);
|
|
223
|
+
if (message.length < STATE_HEADER_BYTES + length) return false;
|
|
224
|
+
out.kind = kind;
|
|
225
|
+
out.tick = readU32(message, 1);
|
|
226
|
+
out.payloadAt = STATE_HEADER_BYTES;
|
|
227
|
+
out.payloadBytes = length;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
case MESSAGE_FINGERPRINT: {
|
|
231
|
+
if (message.length < FINGERPRINT_BYTES) return false;
|
|
232
|
+
out.kind = kind;
|
|
233
|
+
out.participant = message[1] as number;
|
|
234
|
+
out.tick = readU32(message, 2);
|
|
235
|
+
let digest = '';
|
|
236
|
+
for (let i = 0; i < 8; i++) {
|
|
237
|
+
const byte = message[6 + i] as number;
|
|
238
|
+
digest += (HEX[(byte >>> 4) & 0xf] as string) + (HEX[byte & 0xf] as string);
|
|
239
|
+
}
|
|
240
|
+
out.digest = digest;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
case MESSAGE_JOIN: {
|
|
244
|
+
if (message.length < 3) return false;
|
|
245
|
+
out.kind = kind;
|
|
246
|
+
/* The version rides in `tick`, which is the only numeric field a join has. */
|
|
247
|
+
out.tick = (message[1] as number) | ((message[2] as number) << 8);
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
case MESSAGE_WELCOME: {
|
|
251
|
+
if (message.length < 7) return false;
|
|
252
|
+
out.kind = kind;
|
|
253
|
+
out.participant = message[1] as number;
|
|
254
|
+
out.tick = readU32(message, 2);
|
|
255
|
+
out.payloadBytes = message[6] as number;
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
case MESSAGE_ACK:
|
|
259
|
+
case MESSAGE_PING: {
|
|
260
|
+
if (message.length < 6) return false;
|
|
261
|
+
out.kind = kind;
|
|
262
|
+
out.participant = message[1] as number;
|
|
263
|
+
out.tick = readU32(message, 2);
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
default:
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
}
|