@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.
Files changed (49) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +125 -0
  4. package/dist/authority.d.ts +170 -0
  5. package/dist/authority.js +235 -0
  6. package/dist/fingerprint.d.ts +64 -0
  7. package/dist/fingerprint.js +108 -0
  8. package/dist/fixture.d.ts +32 -0
  9. package/dist/fixture.js +136 -0
  10. package/dist/index.d.ts +45 -0
  11. package/dist/index.js +32 -0
  12. package/dist/inputLog.d.ts +117 -0
  13. package/dist/inputLog.js +232 -0
  14. package/dist/lockstep.d.ts +181 -0
  15. package/dist/lockstep.js +291 -0
  16. package/dist/loopback.d.ts +115 -0
  17. package/dist/loopback.js +207 -0
  18. package/dist/rewind.d.ts +206 -0
  19. package/dist/rewind.js +212 -0
  20. package/dist/scriptSession.d.ts +77 -0
  21. package/dist/scriptSession.js +81 -0
  22. package/dist/simNumber.d.ts +103 -0
  23. package/dist/simNumber.js +156 -0
  24. package/dist/snapshotter.d.ts +43 -0
  25. package/dist/snapshotter.js +81 -0
  26. package/dist/transport.d.ts +63 -0
  27. package/dist/transport.js +40 -0
  28. package/dist/webrtc.d.ts +74 -0
  29. package/dist/webrtc.js +108 -0
  30. package/dist/websocket.d.ts +70 -0
  31. package/dist/websocket.js +105 -0
  32. package/dist/wire.d.ts +94 -0
  33. package/dist/wire.js +238 -0
  34. package/package.json +61 -0
  35. package/src/authority.ts +340 -0
  36. package/src/fingerprint.ts +143 -0
  37. package/src/fixture.ts +188 -0
  38. package/src/index.ts +95 -0
  39. package/src/inputLog.ts +262 -0
  40. package/src/lockstep.ts +374 -0
  41. package/src/loopback.ts +250 -0
  42. package/src/rewind.ts +337 -0
  43. package/src/scriptSession.ts +137 -0
  44. package/src/simNumber.ts +176 -0
  45. package/src/snapshotter.ts +98 -0
  46. package/src/transport.ts +69 -0
  47. package/src/webrtc.ts +141 -0
  48. package/src/websocket.ts +140 -0
  49. package/src/wire.ts +269 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * `Transport` over a WebSocket, which is the browser default.
3
+ *
4
+ * **Ordered and reliable, which is the wrong guarantee for inputs and the right one for this
5
+ * topology.** A dropped input frame blocks the frames behind it while TCP retransmits, and by the
6
+ * time it arrives the tick it belonged to is usually past — so a lockstep peer waits for news it no
7
+ * longer needs. The unordered answer is `WebRtcTransport` beside this.
8
+ *
9
+ * It is the default anyway, for a reason outside the protocol: WebSocket and WebTransport are what
10
+ * a relay service realistically speaks, whatever language it is written in. A transport nothing can
11
+ * connect to is not a default.
12
+ *
13
+ * ---
14
+ *
15
+ * ## The socket is supplied and its type is declared structurally
16
+ *
17
+ * No `WebSocket` type is named. `@driftengine/network` imports `@driftengine/entities` and nothing
18
+ * else, and that is the property that lets an authoritative host run it — a package that named a
19
+ * DOM type would compile only where a DOM does. The shape below is what a browser `WebSocket` and
20
+ * a Node `ws` socket both satisfy, which is also what makes the tests possible.
21
+ *
22
+ * The socket is **supplied rather than constructed**, so a consumer owns the URL, the protocols,
23
+ * the credentials and the reconnection policy. A transport that opened its own connection would be
24
+ * making four decisions that belong to whoever is paying for the server.
25
+ */
26
+ import { BROADCAST, } from './transport.js';
27
+ /** The `readyState` values, which are the same three numbers everywhere. */
28
+ const OPEN = 1;
29
+ const CLOSED = 3;
30
+ export class WebSocketTransport {
31
+ self;
32
+ socket;
33
+ maxQueued;
34
+ arrived = [];
35
+ closedByUs = false;
36
+ droppedCount = 0;
37
+ constructor(options) {
38
+ this.socket = options.socket;
39
+ this.self = options.self ?? -1;
40
+ this.maxQueued = Math.max(1, options.maxQueued ?? 1024);
41
+ /* Binary frames, or every message arrives as a string and every payload is corrupted by the
42
+ round trip through UTF-8. Set before any listener, because a frame can arrive immediately. */
43
+ if ('binaryType' in this.socket)
44
+ this.socket.binaryType = 'arraybuffer';
45
+ this.socket.addEventListener('message', (event) => {
46
+ const data = event.data;
47
+ const bytes = toBytes(data);
48
+ if (bytes === null)
49
+ return;
50
+ if (this.arrived.length >= this.maxQueued) {
51
+ this.arrived.shift();
52
+ this.droppedCount += 1;
53
+ }
54
+ this.arrived.push(bytes);
55
+ });
56
+ }
57
+ get state() {
58
+ if (this.closedByUs || this.socket.readyState === CLOSED)
59
+ return 'closed';
60
+ return this.socket.readyState === OPEN ? 'open' : 'connecting';
61
+ }
62
+ /** Messages dropped because nothing drained. A consumer watching for a stalled tab reads this. */
63
+ get dropped() {
64
+ return this.droppedCount;
65
+ }
66
+ /**
67
+ * Send, or do nothing if the socket is not open yet.
68
+ *
69
+ * **Nothing is queued while connecting, deliberately.** Every message this package sends is
70
+ * addressed to a tick, and a tick that has passed by the time the socket opens is a tick nobody
71
+ * can use. Flushing a backlog of stale inputs on connect is worse than having sent none.
72
+ */
73
+ send(_to, message) {
74
+ if (this.state !== 'open')
75
+ return;
76
+ this.socket.send(message);
77
+ }
78
+ drain(into) {
79
+ for (const message of this.arrived)
80
+ into(BROADCAST, message);
81
+ this.arrived.length = 0;
82
+ }
83
+ close() {
84
+ this.closedByUs = true;
85
+ this.arrived.length = 0;
86
+ this.socket.close();
87
+ }
88
+ }
89
+ /**
90
+ * A message's bytes, whatever the socket handed over, or null.
91
+ *
92
+ * Browsers deliver an `ArrayBuffer` when `binaryType` is set, `ws` under Node delivers a `Buffer`,
93
+ * and a socket left on its default delivers a string. The first two are usable and the third is
94
+ * not: a payload that has been through UTF-8 has had every byte above 0x7f replaced.
95
+ */
96
+ function toBytes(data) {
97
+ if (data instanceof Uint8Array)
98
+ return data;
99
+ if (data instanceof ArrayBuffer)
100
+ return new Uint8Array(data);
101
+ if (ArrayBuffer.isView(data)) {
102
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
103
+ }
104
+ return null;
105
+ }
package/dist/wire.d.ts ADDED
@@ -0,0 +1,94 @@
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
+ export declare const PROTOCOL_VERSION = 1;
36
+ export declare const MESSAGE_JOIN = 1;
37
+ export declare const MESSAGE_WELCOME = 2;
38
+ export declare const MESSAGE_INPUT = 3;
39
+ export declare const MESSAGE_FINGERPRINT = 4;
40
+ export declare const MESSAGE_STATE = 5;
41
+ export declare const MESSAGE_ACK = 6;
42
+ export declare const MESSAGE_PING = 7;
43
+ /**
44
+ * Header bytes before an input run's payloads: kind, participant, first tick, count, stride.
45
+ *
46
+ * **An input message carries a *run* of consecutive ticks, and a run of one is the degenerate
47
+ * case.** That is not generality for its own sake — it is what makes lockstep survive a lossy link.
48
+ * An input is only useful for the tick it names, so retransmitting a lost one after a round trip
49
+ * delivers it too late to use; sending the last few inputs in *every* packet covers a loss before
50
+ * anybody notices, and costs a handful of bytes on a message that already has a header. A test at
51
+ * 15% loss is what turned this from a nicety into a requirement: without it a dropped input is lost
52
+ * for good and that peer's world is permanently wrong.
53
+ */
54
+ export declare const INPUT_HEADER_BYTES: number;
55
+ /** Header bytes before a state payload: kind, tick, length. */
56
+ export declare const STATE_HEADER_BYTES: number;
57
+ /** A fingerprint message is fixed: kind, participant, tick, and eight bytes of digest. */
58
+ export declare const FINGERPRINT_BYTES: number;
59
+ /** What a decoded message turned out to be. `kind` 0 means it did not decode. */
60
+ export interface DecodedMessage {
61
+ kind: number;
62
+ participant: number;
63
+ tick: number;
64
+ /** Where the payloads start, and how long **each one** is. Zero when there is none. */
65
+ payloadAt: number;
66
+ payloadBytes: number;
67
+ /** How many consecutive ticks this message carries, starting at `tick`. One for everything else. */
68
+ count: number;
69
+ /** The sixteen-hex-character digest, for a fingerprint message; empty otherwise. */
70
+ digest: string;
71
+ }
72
+ export declare function createDecoded(): DecodedMessage;
73
+ /**
74
+ * Write an input frame into `into`, answering how many bytes it used, or -1 when it does not fit.
75
+ *
76
+ * **-1 rather than a throw**, because the caller is a frame loop and `AGENTS.md` forbids throwing
77
+ * in one. A buffer too small is a caller's sizing bug and the honest report is a value they can
78
+ * branch on at the send site.
79
+ */
80
+ export declare function encodeInput(into: Uint8Array, participant: number, firstTick: number, payloads: Uint8Array, stride?: number, count?: number): number;
81
+ export declare function encodeState(into: Uint8Array, tick: number, payload: Uint8Array): number;
82
+ export declare function encodeFingerprint(into: Uint8Array, participant: number, tick: number, digest: string): number;
83
+ export declare function encodeJoin(into: Uint8Array): number;
84
+ export declare function encodeWelcome(into: Uint8Array, participant: number, tick: number, participants: number): number;
85
+ export declare function encodeAck(into: Uint8Array, participant: number, tick: number): number;
86
+ /**
87
+ * Read a message's header into `out`. `false` when the bytes are not a message this version knows.
88
+ *
89
+ * **A payload is located rather than copied.** `payloadAt` and `payloadBytes` point into the buffer
90
+ * the caller was handed, which the transport may reuse the moment the sink returns — the same
91
+ * borrow `MessageSink` documents. A caller keeping a payload copies it, and one feeding it straight
92
+ * to `RewindLoop.supply` does not need to.
93
+ */
94
+ export declare function decode(message: Uint8Array, out: DecodedMessage): boolean;
package/dist/wire.js ADDED
@@ -0,0 +1,238 @@
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
+ export const PROTOCOL_VERSION = 1;
36
+ export const MESSAGE_JOIN = 1;
37
+ export const MESSAGE_WELCOME = 2;
38
+ export const MESSAGE_INPUT = 3;
39
+ export const MESSAGE_FINGERPRINT = 4;
40
+ export const MESSAGE_STATE = 5;
41
+ export const MESSAGE_ACK = 6;
42
+ export const MESSAGE_PING = 7;
43
+ /**
44
+ * Header bytes before an input run's payloads: kind, participant, first tick, count, stride.
45
+ *
46
+ * **An input message carries a *run* of consecutive ticks, and a run of one is the degenerate
47
+ * case.** That is not generality for its own sake — it is what makes lockstep survive a lossy link.
48
+ * An input is only useful for the tick it names, so retransmitting a lost one after a round trip
49
+ * delivers it too late to use; sending the last few inputs in *every* packet covers a loss before
50
+ * anybody notices, and costs a handful of bytes on a message that already has a header. A test at
51
+ * 15% loss is what turned this from a nicety into a requirement: without it a dropped input is lost
52
+ * for good and that peer's world is permanently wrong.
53
+ */
54
+ export const INPUT_HEADER_BYTES = 1 + 1 + 4 + 1 + 2;
55
+ /** Header bytes before a state payload: kind, tick, length. */
56
+ export const STATE_HEADER_BYTES = 1 + 4 + 2;
57
+ /** A fingerprint message is fixed: kind, participant, tick, and eight bytes of digest. */
58
+ export const FINGERPRINT_BYTES = 1 + 1 + 4 + 8;
59
+ function writeU32(into, at, value) {
60
+ const v = value >>> 0;
61
+ into[at] = v & 0xff;
62
+ into[at + 1] = (v >>> 8) & 0xff;
63
+ into[at + 2] = (v >>> 16) & 0xff;
64
+ into[at + 3] = (v >>> 24) & 0xff;
65
+ }
66
+ function readU32(from, at) {
67
+ return ((from[at] |
68
+ (from[at + 1] << 8) |
69
+ (from[at + 2] << 16) |
70
+ (from[at + 3] << 24)) >>>
71
+ 0);
72
+ }
73
+ export function createDecoded() {
74
+ return { kind: 0, participant: 0, tick: 0, payloadAt: 0, payloadBytes: 0, count: 0, digest: '' };
75
+ }
76
+ /**
77
+ * Write an input frame into `into`, answering how many bytes it used, or -1 when it does not fit.
78
+ *
79
+ * **-1 rather than a throw**, because the caller is a frame loop and `AGENTS.md` forbids throwing
80
+ * in one. A buffer too small is a caller's sizing bug and the honest report is a value they can
81
+ * branch on at the send site.
82
+ */
83
+ export function encodeInput(into, participant, firstTick, payloads, stride = payloads.length, count = 1) {
84
+ const total = INPUT_HEADER_BYTES + stride * count;
85
+ if (into.length < total || stride > 0xffff || count > 0xff || count < 1)
86
+ return -1;
87
+ if (payloads.length < stride * count)
88
+ return -1;
89
+ into[0] = MESSAGE_INPUT;
90
+ into[1] = participant & 0xff;
91
+ writeU32(into, 2, firstTick);
92
+ into[6] = count & 0xff;
93
+ into[7] = stride & 0xff;
94
+ into[8] = (stride >>> 8) & 0xff;
95
+ into.set(payloads.subarray(0, stride * count), INPUT_HEADER_BYTES);
96
+ return total;
97
+ }
98
+ export function encodeState(into, tick, payload) {
99
+ const total = STATE_HEADER_BYTES + payload.length;
100
+ if (into.length < total || payload.length > 0xffff)
101
+ return -1;
102
+ into[0] = MESSAGE_STATE;
103
+ writeU32(into, 1, tick);
104
+ into[5] = payload.length & 0xff;
105
+ into[6] = (payload.length >>> 8) & 0xff;
106
+ into.set(payload, STATE_HEADER_BYTES);
107
+ return total;
108
+ }
109
+ export function encodeFingerprint(into, participant, tick, digest) {
110
+ if (into.length < FINGERPRINT_BYTES || digest.length !== 16)
111
+ return -1;
112
+ into[0] = MESSAGE_FINGERPRINT;
113
+ into[1] = participant & 0xff;
114
+ writeU32(into, 2, tick);
115
+ for (let i = 0; i < 8; i++) {
116
+ into[6 + i] = Number.parseInt(digest.slice(i * 2, i * 2 + 2), 16) & 0xff;
117
+ }
118
+ return FINGERPRINT_BYTES;
119
+ }
120
+ export function encodeJoin(into) {
121
+ if (into.length < 3)
122
+ return -1;
123
+ into[0] = MESSAGE_JOIN;
124
+ into[1] = PROTOCOL_VERSION & 0xff;
125
+ into[2] = (PROTOCOL_VERSION >>> 8) & 0xff;
126
+ return 3;
127
+ }
128
+ export function encodeWelcome(into, participant, tick, participants) {
129
+ if (into.length < 7)
130
+ return -1;
131
+ into[0] = MESSAGE_WELCOME;
132
+ into[1] = participant & 0xff;
133
+ writeU32(into, 2, tick);
134
+ into[6] = participants & 0xff;
135
+ return 7;
136
+ }
137
+ export function encodeAck(into, participant, tick) {
138
+ if (into.length < 6)
139
+ return -1;
140
+ into[0] = MESSAGE_ACK;
141
+ into[1] = participant & 0xff;
142
+ writeU32(into, 2, tick);
143
+ return 6;
144
+ }
145
+ const HEX = '0123456789abcdef';
146
+ /**
147
+ * Read a message's header into `out`. `false` when the bytes are not a message this version knows.
148
+ *
149
+ * **A payload is located rather than copied.** `payloadAt` and `payloadBytes` point into the buffer
150
+ * the caller was handed, which the transport may reuse the moment the sink returns — the same
151
+ * borrow `MessageSink` documents. A caller keeping a payload copies it, and one feeding it straight
152
+ * to `RewindLoop.supply` does not need to.
153
+ */
154
+ export function decode(message, out) {
155
+ out.kind = 0;
156
+ out.participant = 0;
157
+ out.tick = 0;
158
+ out.payloadAt = 0;
159
+ out.payloadBytes = 0;
160
+ out.count = 0;
161
+ out.digest = '';
162
+ if (message.length < 1)
163
+ return false;
164
+ const kind = message[0];
165
+ switch (kind) {
166
+ case MESSAGE_INPUT: {
167
+ if (message.length < INPUT_HEADER_BYTES)
168
+ return false;
169
+ const count = message[6];
170
+ const stride = message[7] | (message[8] << 8);
171
+ if (count < 1 || stride < 1)
172
+ return false;
173
+ if (message.length < INPUT_HEADER_BYTES + stride * count)
174
+ return false;
175
+ out.kind = kind;
176
+ out.participant = message[1];
177
+ out.tick = readU32(message, 2);
178
+ out.payloadAt = INPUT_HEADER_BYTES;
179
+ out.payloadBytes = stride;
180
+ out.count = count;
181
+ return true;
182
+ }
183
+ case MESSAGE_STATE: {
184
+ if (message.length < STATE_HEADER_BYTES)
185
+ return false;
186
+ const length = message[5] | (message[6] << 8);
187
+ if (message.length < STATE_HEADER_BYTES + length)
188
+ return false;
189
+ out.kind = kind;
190
+ out.tick = readU32(message, 1);
191
+ out.payloadAt = STATE_HEADER_BYTES;
192
+ out.payloadBytes = length;
193
+ return true;
194
+ }
195
+ case MESSAGE_FINGERPRINT: {
196
+ if (message.length < FINGERPRINT_BYTES)
197
+ return false;
198
+ out.kind = kind;
199
+ out.participant = message[1];
200
+ out.tick = readU32(message, 2);
201
+ let digest = '';
202
+ for (let i = 0; i < 8; i++) {
203
+ const byte = message[6 + i];
204
+ digest += HEX[(byte >>> 4) & 0xf] + HEX[byte & 0xf];
205
+ }
206
+ out.digest = digest;
207
+ return true;
208
+ }
209
+ case MESSAGE_JOIN: {
210
+ if (message.length < 3)
211
+ return false;
212
+ out.kind = kind;
213
+ /* The version rides in `tick`, which is the only numeric field a join has. */
214
+ out.tick = message[1] | (message[2] << 8);
215
+ return true;
216
+ }
217
+ case MESSAGE_WELCOME: {
218
+ if (message.length < 7)
219
+ return false;
220
+ out.kind = kind;
221
+ out.participant = message[1];
222
+ out.tick = readU32(message, 2);
223
+ out.payloadBytes = message[6];
224
+ return true;
225
+ }
226
+ case MESSAGE_ACK:
227
+ case MESSAGE_PING: {
228
+ if (message.length < 6)
229
+ return false;
230
+ out.kind = kind;
231
+ out.participant = message[1];
232
+ out.tick = readU32(message, 2);
233
+ return true;
234
+ }
235
+ default:
236
+ return false;
237
+ }
238
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@driftengine/network",
3
+ "version": "3.61.0",
4
+ "description": "A transport seam, rewind and replay, lockstep and an authoritative host, on the fixed-step loop",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "drift-source": "./src/index.ts",
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json",
16
+ "./*": "./*"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "src",
21
+ "!src/**/*.test.ts",
22
+ "!src/**/*.test.mjs",
23
+ "!src/**/__snapshots__",
24
+ "README.md",
25
+ "LICENSE",
26
+ "NOTICE"
27
+ ],
28
+ "sideEffects": false,
29
+ "peerDependencies": {
30
+ "@driftengine/entities": "3.61.0"
31
+ },
32
+ "devDependencies": {
33
+ "@driftengine/core": "3.61.0"
34
+ },
35
+ "author": "Drift Technologies",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/drftrun/driftengine.git",
39
+ "directory": "packages/network"
40
+ },
41
+ "homepage": "https://github.com/drftrun/driftengine#readme",
42
+ "bugs": "https://github.com/drftrun/driftengine/issues",
43
+ "keywords": [
44
+ "driftengine",
45
+ "3d",
46
+ "webgl",
47
+ "webgpu",
48
+ "typescript",
49
+ "netcode",
50
+ "rollback",
51
+ "lockstep",
52
+ "prediction",
53
+ "multiplayer"
54
+ ],
55
+ "engines": {
56
+ "node": ">=22.12.0"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ }
61
+ }