murow 0.0.73 → 0.1.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.
Files changed (99) hide show
  1. package/README.md +15 -1
  2. package/dist/cjs/core/binary-codec/binary-codec.js +1 -1
  3. package/dist/cjs/core/driver/driver.js +1 -1
  4. package/dist/cjs/core/driver/drivers/immediate.js +1 -1
  5. package/dist/cjs/core/driver/drivers/raf.js +1 -1
  6. package/dist/cjs/core/driver/drivers/timeout.js +1 -1
  7. package/dist/cjs/core/input/index.js +1 -1
  8. package/dist/cjs/core/input/mouse-look/index.js +1 -0
  9. package/dist/cjs/core/input/mouse-look/mouse-look.js +1 -0
  10. package/dist/cjs/core/input/scroll-zoom/index.js +1 -0
  11. package/dist/cjs/core/input/scroll-zoom/scroll-zoom.js +1 -0
  12. package/dist/cjs/ecs/component.js +1 -1
  13. package/dist/cjs/ecs/system-builder.js +1 -1
  14. package/dist/cjs/ecs/world.js +1 -1
  15. package/dist/cjs/game/loop/loop.js +1 -1
  16. package/dist/cjs/net/adapters/bun-websocket.js +1 -1
  17. package/dist/esm/core/binary-codec/binary-codec.js +1 -1
  18. package/dist/esm/core/driver/drivers/immediate.js +1 -1
  19. package/dist/esm/core/driver/drivers/raf.js +1 -1
  20. package/dist/esm/core/driver/drivers/timeout.js +1 -1
  21. package/dist/esm/core/input/index.js +1 -1
  22. package/dist/esm/core/input/mouse-look/index.js +1 -0
  23. package/dist/esm/core/input/mouse-look/mouse-look.js +1 -0
  24. package/dist/esm/core/input/scroll-zoom/index.js +1 -0
  25. package/dist/esm/core/input/scroll-zoom/scroll-zoom.js +1 -0
  26. package/dist/esm/ecs/component.js +1 -1
  27. package/dist/esm/ecs/system-builder.js +1 -1
  28. package/dist/esm/ecs/world.js +1 -1
  29. package/dist/esm/game/loop/loop.js +1 -1
  30. package/dist/esm/net/adapters/bun-websocket.js +1 -1
  31. package/dist/netcode/cjs/index.js +1552 -0
  32. package/dist/netcode/esm/index.js +1530 -0
  33. package/dist/netcode/types/client/game-client.d.ts +125 -0
  34. package/dist/netcode/types/client/index.d.ts +1 -0
  35. package/dist/netcode/types/client/interpolation-buffer.d.ts +37 -0
  36. package/dist/netcode/types/client/interpolation-buffer.test.d.ts +1 -0
  37. package/dist/netcode/types/codec/delta-codec.d.ts +17 -0
  38. package/dist/netcode/types/codec/delta-codec.test.d.ts +1 -0
  39. package/dist/netcode/types/codec/index.d.ts +1 -0
  40. package/dist/netcode/types/components/index.d.ts +1 -0
  41. package/dist/netcode/types/components/sync-spec.d.ts +43 -0
  42. package/dist/netcode/types/components/sync-spec.test.d.ts +1 -0
  43. package/dist/netcode/types/ctx.d.ts +105 -0
  44. package/dist/netcode/types/ctx.test.d.ts +1 -0
  45. package/dist/netcode/types/handlers/define-handlers.d.ts +47 -0
  46. package/dist/netcode/types/handlers/index.d.ts +1 -0
  47. package/dist/netcode/types/index.d.ts +11 -0
  48. package/dist/netcode/types/integration.test.d.ts +1 -0
  49. package/dist/netcode/types/intents/define-intents.d.ts +53 -0
  50. package/dist/netcode/types/intents/define-intents.test.d.ts +1 -0
  51. package/dist/netcode/types/intents/index.d.ts +1 -0
  52. package/dist/netcode/types/network/base.d.ts +120 -0
  53. package/dist/netcode/types/network/index.d.ts +2 -0
  54. package/dist/netcode/types/network/transport.d.ts +1 -0
  55. package/dist/netcode/types/packets/convergence.test.d.ts +1 -0
  56. package/dist/netcode/types/packets/harness.d.ts +103 -0
  57. package/dist/netcode/types/packets/index.d.ts +2 -0
  58. package/dist/netcode/types/packets/intermittent-intents.test.d.ts +1 -0
  59. package/dist/netcode/types/packets/pathological.test.d.ts +1 -0
  60. package/dist/netcode/types/packets/peer-interpolation.test.d.ts +1 -0
  61. package/dist/netcode/types/packets/reordering.test.d.ts +1 -0
  62. package/dist/netcode/types/packets/virtual-network.d.ts +65 -0
  63. package/dist/netcode/types/predictions/define-predictions.d.ts +45 -0
  64. package/dist/netcode/types/predictions/define-predictions.test.d.ts +1 -0
  65. package/dist/netcode/types/predictions/index.d.ts +1 -0
  66. package/dist/netcode/types/reconciliation.test.d.ts +1 -0
  67. package/dist/netcode/types/rpcs/define-rpcs.d.ts +44 -0
  68. package/dist/netcode/types/rpcs/define-rpcs.test.d.ts +1 -0
  69. package/dist/netcode/types/rpcs/index.d.ts +1 -0
  70. package/dist/netcode/types/server/game-server.d.ts +77 -0
  71. package/dist/netcode/types/server/index.d.ts +2 -0
  72. package/dist/netcode/types/server/plugins/aoi-grid.d.ts +34 -0
  73. package/dist/netcode/types/server/plugins/index.d.ts +3 -0
  74. package/dist/netcode/types/server/plugins/lag-compensation.d.ts +34 -0
  75. package/dist/netcode/types/server/plugins/plugin.d.ts +24 -0
  76. package/dist/netcode/types/tick-rate.test.d.ts +1 -0
  77. package/dist/netcode/types/transports/index.d.ts +1 -0
  78. package/dist/netcode/types/transports/memory-transport.d.ts +51 -0
  79. package/dist/netcode/types/types.test.d.ts +1 -0
  80. package/dist/types/core/binary-codec/binary-codec.d.ts +89 -31
  81. package/dist/types/core/driver/driver.d.ts +8 -8
  82. package/dist/types/core/driver/drivers/immediate.d.ts +4 -4
  83. package/dist/types/core/driver/drivers/raf.d.ts +6 -6
  84. package/dist/types/core/driver/drivers/timeout.d.ts +4 -4
  85. package/dist/types/core/input/index.d.ts +2 -0
  86. package/dist/types/core/input/mouse-look/index.d.ts +1 -0
  87. package/dist/types/core/input/mouse-look/mouse-look.d.ts +139 -0
  88. package/dist/types/core/input/scroll-zoom/index.d.ts +1 -0
  89. package/dist/types/core/input/scroll-zoom/scroll-zoom.d.ts +38 -0
  90. package/dist/types/ecs/component.d.ts +67 -11
  91. package/dist/types/ecs/entity-handle.d.ts +5 -5
  92. package/dist/types/ecs/system-builder.d.ts +13 -0
  93. package/dist/types/ecs/world.d.ts +72 -4
  94. package/dist/types/game/loop/loop.d.ts +21 -2
  95. package/dist/types/net/adapters/bun-websocket.d.ts +19 -3
  96. package/dist/webgpu/cjs/index.js +120 -5
  97. package/dist/webgpu/esm/index.js +120 -5
  98. package/dist/webgpu/types/3d/renderer.d.ts +38 -0
  99. package/package.json +6 -1
@@ -0,0 +1,1552 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/index.ts
20
+ var src_exports = {};
21
+ __export(src_exports, {
22
+ AoiGrid: () => AoiGrid,
23
+ GameClient: () => GameClient,
24
+ GameServer: () => GameServer,
25
+ LagCompensation: () => LagCompensation,
26
+ MemoryPeerTransport: () => MemoryPeerTransport,
27
+ MemoryServerTransport: () => MemoryServerTransport,
28
+ Network: () => Network,
29
+ decodeDelta: () => decodeDelta,
30
+ defineHandlers: () => defineHandlers,
31
+ defineIntents: () => defineIntents,
32
+ definePredictions: () => definePredictions,
33
+ defineRpcs: () => defineRpcs,
34
+ encodeDelta: () => encodeDelta,
35
+ makeFieldsAccessor: () => makeFieldsAccessor,
36
+ makeMarkDirty: () => makeMarkDirty,
37
+ networked: () => networked
38
+ });
39
+ module.exports = __toCommonJS(src_exports);
40
+
41
+ // src/intents/define-intents.ts
42
+ var import_protocol = require("murow/protocol");
43
+ function defineIntents(intents) {
44
+ const defs = {};
45
+ const kindByName = {};
46
+ const nameByKind = {};
47
+ const registry = new import_protocol.IntentRegistry();
48
+ let nextKind = 1;
49
+ for (const name of Object.keys(intents)) {
50
+ const kind = nextKind++;
51
+ const def = (0, import_protocol.defineIntent)({
52
+ kind,
53
+ schema: intents[name]
54
+ });
55
+ defs[name] = def;
56
+ kindByName[name] = kind;
57
+ nameByKind[kind] = name;
58
+ registry.register(def);
59
+ }
60
+ return {
61
+ defs,
62
+ kindByName,
63
+ nameByKind,
64
+ registry,
65
+ __payloads: void 0
66
+ };
67
+ }
68
+
69
+ // src/rpcs/define-rpcs.ts
70
+ var import_protocol2 = require("murow/protocol");
71
+ function defineRpcs(rpcs) {
72
+ const defs = {};
73
+ const registry = new import_protocol2.RpcRegistry();
74
+ for (const method of Object.keys(rpcs)) {
75
+ const def = (0, import_protocol2.defineRPC)({ method, schema: rpcs[method] });
76
+ defs[method] = def;
77
+ registry.register(def);
78
+ }
79
+ return {
80
+ defs,
81
+ registry,
82
+ __payloads: void 0
83
+ };
84
+ }
85
+
86
+ // src/predictions/define-predictions.ts
87
+ function definePredictions(intents, map) {
88
+ return { __kind: "predictions", intents, map };
89
+ }
90
+
91
+ // src/handlers/define-handlers.ts
92
+ function defineHandlers(intents, map) {
93
+ return { __kind: "handlers", intents, map };
94
+ }
95
+
96
+ // src/network/base.ts
97
+ var import_events = require("murow/core/events");
98
+ var Network = class extends import_events.EventSystem {
99
+ constructor(events) {
100
+ super({ events });
101
+ }
102
+ };
103
+
104
+ // src/server/game-server.ts
105
+ var import_simple_rng = require("murow/core/simple-rng");
106
+ var import_binary_codec = require("murow/core/binary-codec");
107
+ var import_protocol3 = require("murow/protocol");
108
+
109
+ // src/codec/delta-codec.ts
110
+ var HEADER_BYTES = 4 + 4 + 2 + 2;
111
+ function encodeDelta(world, tick, entities, components, numMaskWords, despawned = [], clientAckTick = 0) {
112
+ const perEntityMasks = new Array(entities.length);
113
+ const perEntityBitmaskBytes = numMaskWords * 4;
114
+ let bodyBytes = 0;
115
+ for (let i = 0; i < entities.length; i++) {
116
+ const eid = entities[i];
117
+ const mask = new Uint32Array(numMaskWords);
118
+ let componentBytes = 0;
119
+ for (let ci = 0; ci < components.length; ci++) {
120
+ const c = components[ci];
121
+ if (!world.has(eid, c))
122
+ continue;
123
+ const wordIndex = ci >>> 5;
124
+ const bitIndex = ci & 31;
125
+ mask[wordIndex] |= 1 << bitIndex;
126
+ componentBytes += c.size;
127
+ }
128
+ perEntityMasks[i] = mask;
129
+ bodyBytes += 4 + perEntityBitmaskBytes + componentBytes;
130
+ }
131
+ const despawnBytes = despawned.length * 4;
132
+ const buf = new Uint8Array(HEADER_BYTES + bodyBytes + despawnBytes);
133
+ const dv = new DataView(buf.buffer);
134
+ let off = 0;
135
+ dv.setUint32(off, tick >>> 0, true);
136
+ off += 4;
137
+ dv.setUint32(off, clientAckTick >>> 0, true);
138
+ off += 4;
139
+ dv.setUint16(off, entities.length, true);
140
+ off += 2;
141
+ dv.setUint16(off, despawned.length, true);
142
+ off += 2;
143
+ const componentFieldArrays = new Array(components.length);
144
+ const componentSchemas = new Array(components.length);
145
+ const componentAllScalar = new Array(components.length);
146
+ const maxEntities = world.getMaxEntities();
147
+ for (let ci = 0; ci < components.length; ci++) {
148
+ const c = components[ci];
149
+ const fieldArrays = world.fields(c);
150
+ componentFieldArrays[ci] = fieldArrays;
151
+ componentSchemas[ci] = c.schema;
152
+ let allScalar = true;
153
+ for (let fi = 0; fi < c.fieldNames.length; fi++) {
154
+ const fname = c.fieldNames[fi];
155
+ if (fieldArrays[fname].length !== maxEntities) {
156
+ allScalar = false;
157
+ break;
158
+ }
159
+ }
160
+ componentAllScalar[ci] = allScalar;
161
+ }
162
+ for (let i = 0; i < entities.length; i++) {
163
+ const eid = entities[i];
164
+ const mask = perEntityMasks[i];
165
+ dv.setUint32(off, eid >>> 0, true);
166
+ off += 4;
167
+ for (let w = 0; w < numMaskWords; w++) {
168
+ dv.setUint32(off, mask[w], true);
169
+ off += 4;
170
+ }
171
+ for (let ci = 0; ci < components.length; ci++) {
172
+ const wordIndex = ci >>> 5;
173
+ const bitIndex = ci & 31;
174
+ if ((mask[wordIndex] & 1 << bitIndex) === 0)
175
+ continue;
176
+ const c = components[ci];
177
+ const fieldArrays = componentFieldArrays[ci];
178
+ const schema = componentSchemas[ci];
179
+ const fieldNames = c.fieldNames;
180
+ if (componentAllScalar[ci]) {
181
+ for (let fi = 0; fi < fieldNames.length; fi++) {
182
+ const fieldName = fieldNames[fi];
183
+ const field = schema[fieldName];
184
+ field.write(dv, off, fieldArrays[fieldName][eid]);
185
+ off += field.size;
186
+ }
187
+ } else {
188
+ const data = world.get(eid, c);
189
+ for (let fi = 0; fi < fieldNames.length; fi++) {
190
+ const fieldName = fieldNames[fi];
191
+ const field = schema[fieldName];
192
+ field.write(dv, off, data[fieldName]);
193
+ off += field.size;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ for (let i = 0; i < despawned.length; i++) {
199
+ dv.setUint32(off, despawned[i] >>> 0, true);
200
+ off += 4;
201
+ }
202
+ return buf;
203
+ }
204
+ function decodeDelta(world, buf, components, numMaskWords, ensureEntity, shouldApply = () => true) {
205
+ const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
206
+ let off = 0;
207
+ const tick = dv.getUint32(off, true);
208
+ off += 4;
209
+ const clientAckTick = dv.getUint32(off, true);
210
+ off += 4;
211
+ const entityCount = dv.getUint16(off, true);
212
+ off += 2;
213
+ const despawnCount = dv.getUint16(off, true);
214
+ off += 2;
215
+ const localEntityIds = [];
216
+ const serverEntityIds = [];
217
+ const valuesByServerEntity = /* @__PURE__ */ new Map();
218
+ for (let i = 0; i < entityCount; i++) {
219
+ const serverEid = dv.getUint32(off, true);
220
+ off += 4;
221
+ serverEntityIds.push(serverEid);
222
+ const mask = new Uint32Array(numMaskWords);
223
+ for (let w = 0; w < numMaskWords; w++) {
224
+ mask[w] = dv.getUint32(off, true);
225
+ off += 4;
226
+ }
227
+ const present = [];
228
+ for (let ci = 0; ci < components.length; ci++) {
229
+ const wordIndex = ci >>> 5;
230
+ const bitIndex = ci & 31;
231
+ if ((mask[wordIndex] & 1 << bitIndex) !== 0) {
232
+ present.push(components[ci]);
233
+ }
234
+ }
235
+ const localEid = ensureEntity(serverEid, present);
236
+ localEntityIds.push(localEid);
237
+ const applyToWorld = shouldApply(localEid);
238
+ const compMap = /* @__PURE__ */ new Map();
239
+ for (const c of present) {
240
+ const update = {};
241
+ for (const fieldName of c.fieldNames) {
242
+ const field = c.schema[fieldName];
243
+ update[fieldName] = field.read(dv, off);
244
+ off += field.size;
245
+ }
246
+ compMap.set(c, update);
247
+ if (applyToWorld) {
248
+ if (!world.has(localEid, c)) {
249
+ world.add(localEid, c, update);
250
+ } else {
251
+ world.update(localEid, c, update);
252
+ }
253
+ } else if (!world.has(localEid, c)) {
254
+ world.add(localEid, c, update);
255
+ }
256
+ }
257
+ valuesByServerEntity.set(serverEid, compMap);
258
+ }
259
+ const despawnedServerIds = [];
260
+ for (let i = 0; i < despawnCount; i++) {
261
+ despawnedServerIds.push(dv.getUint32(off, true));
262
+ off += 4;
263
+ }
264
+ return {
265
+ tick,
266
+ clientAckTick,
267
+ entityIds: localEntityIds,
268
+ serverEntityIds,
269
+ despawnedServerIds,
270
+ valuesByServerEntity
271
+ };
272
+ }
273
+
274
+ // src/ctx.ts
275
+ function makeFieldsAccessor(world, entity) {
276
+ return function fields(component) {
277
+ if (component.__sync !== void 0 && component.__worldIndex !== void 0) {
278
+ world.markDirty(entity, component.__worldIndex);
279
+ }
280
+ return world.fields(component);
281
+ };
282
+ }
283
+ function makeMarkDirty(world, ctxEntity) {
284
+ function markDirty(arg, entity) {
285
+ const target = entity ?? ctxEntity;
286
+ if (Array.isArray(arg)) {
287
+ for (let i = 0; i < arg.length; i++) {
288
+ const c = arg[i];
289
+ if (c.__sync === void 0 || c.__worldIndex === void 0)
290
+ continue;
291
+ world.markDirty(target, c.__worldIndex);
292
+ }
293
+ } else {
294
+ const c = arg;
295
+ if (c.__sync === void 0 || c.__worldIndex === void 0)
296
+ return;
297
+ world.markDirty(target, c.__worldIndex);
298
+ }
299
+ }
300
+ return markDirty;
301
+ }
302
+
303
+ // src/server/game-server.ts
304
+ var PING_RPC = (0, import_protocol3.defineRPC)({ method: "__murow_ping", schema: { ts: import_binary_codec.u16 } });
305
+ var PONG_RPC = (0, import_protocol3.defineRPC)({ method: "__murow_pong", schema: { ts: import_binary_codec.u16 } });
306
+ var MSG_SNAPSHOT = 128;
307
+ var MSG_RPC = 129;
308
+ var MSG_KICK = 130;
309
+ var MSG_ASSIGN_ENTITY = 132;
310
+ var CMSG_INTENT = 1;
311
+ var CMSG_RPC = 2;
312
+ var CMSG_KICK_ACK = 3;
313
+ var GameServer = class extends Network {
314
+ constructor(opts) {
315
+ super([
316
+ "connection",
317
+ "disconnection",
318
+ "intent",
319
+ "intent-failed",
320
+ "rpc",
321
+ "snapshot",
322
+ "error"
323
+ ]);
324
+ this.peers = /* @__PURE__ */ new Map();
325
+ this.predictionMap = null;
326
+ this.handlerMap = null;
327
+ this.plugins = [];
328
+ this.lagComp = null;
329
+ this.syncedComponents = [];
330
+ this.syncedNumMaskWords = 1;
331
+ this.tickCounter = 0;
332
+ this.scratch = [];
333
+ this.world = opts.world;
334
+ this.loop = opts.loop;
335
+ this.transport = opts.transport;
336
+ this.intents = opts.protocol.intents;
337
+ this.rpcs = opts.protocol.rpcs;
338
+ this.rng = new import_simple_rng.SimpleRNG(1);
339
+ const reg = this.rpcs.registry;
340
+ if (!reg.has(PING_RPC.method))
341
+ reg.register(PING_RPC);
342
+ if (!reg.has(PONG_RPC.method))
343
+ reg.register(PONG_RPC);
344
+ const tickRate = opts.loop.ticker.rate;
345
+ const requestedSnapshotRate = opts.snapshot?.rate ?? Math.min(20, tickRate);
346
+ const effectiveSnapshotRate = Math.min(requestedSnapshotRate, tickRate);
347
+ this.snapshotEvery = Math.max(1, Math.round(tickRate / effectiveSnapshotRate));
348
+ this.kickAckTimeout = opts.kick?.ackTimeout ?? 2e3;
349
+ this.lastDt = opts.loop.ticker.intervalMs / 1e3;
350
+ this.discoverSyncedComponents();
351
+ this.wireTransport();
352
+ this.wireLoop();
353
+ }
354
+ discoverSyncedComponents() {
355
+ const all = this.world.components;
356
+ for (const c of all) {
357
+ if (c.__sync !== void 0)
358
+ this.syncedComponents.push(c);
359
+ }
360
+ this.syncedNumMaskWords = Math.max(1, Math.ceil(this.syncedComponents.length / 32));
361
+ }
362
+ wireTransport() {
363
+ this.transport.onConnection((peerTransport, peerId) => {
364
+ const peer = {
365
+ peerId,
366
+ entity: -1,
367
+ transport: peerTransport,
368
+ kicking: null,
369
+ lastAckedClientTick: 0,
370
+ pendingBySequence: /* @__PURE__ */ new Map(),
371
+ needsBaseline: true,
372
+ intentQueue: []
373
+ };
374
+ this.peers.set(peerId, peer);
375
+ peerTransport.onMessage((data) => this.handleIncoming(peer, data));
376
+ peerTransport.onClose?.(() => this.handleDisconnect(peer, "transport-closed"));
377
+ this.emit("connection", { peer });
378
+ });
379
+ this.transport.onDisconnection((peerId) => {
380
+ const peer = this.peers.get(peerId);
381
+ if (peer === void 0)
382
+ return;
383
+ this.handleDisconnect(peer, "transport-closed");
384
+ });
385
+ }
386
+ handleDisconnect(peer, reason) {
387
+ if (!this.peers.has(peer.peerId))
388
+ return;
389
+ if (peer.kicking !== null)
390
+ clearTimeout(peer.kicking.timer);
391
+ this.peers.delete(peer.peerId);
392
+ for (const plugin of this.plugins)
393
+ plugin.onDisconnect?.(peer);
394
+ this.emit("disconnection", { peer, reason });
395
+ }
396
+ wireLoop() {
397
+ const events = this.loop.events;
398
+ events.on("pre-tick", () => {
399
+ this.drainIntentQueues();
400
+ });
401
+ events.on("post-tick", ({ deltaTime }) => {
402
+ this.tickCounter++;
403
+ this.lastDt = deltaTime;
404
+ for (const plugin of this.plugins)
405
+ plugin.onTick?.(this.world, deltaTime);
406
+ if (this.tickCounter % this.snapshotEvery !== 0)
407
+ return;
408
+ this.sendSnapshots();
409
+ });
410
+ }
411
+ drainIntentQueues() {
412
+ this.peers.forEach((peer) => {
413
+ if (peer.kicking !== null) {
414
+ peer.intentQueue.length = 0;
415
+ peer.pendingBySequence.clear();
416
+ return;
417
+ }
418
+ for (let i = 0; i < peer.intentQueue.length; i++) {
419
+ const entry = peer.intentQueue[i];
420
+ if (entry.sequence <= peer.lastAckedClientTick)
421
+ continue;
422
+ if (peer.pendingBySequence.has(entry.sequence))
423
+ continue;
424
+ peer.pendingBySequence.set(entry.sequence, entry.payload);
425
+ }
426
+ peer.intentQueue.length = 0;
427
+ while (peer.pendingBySequence.has(peer.lastAckedClientTick + 1)) {
428
+ const nextSequence = peer.lastAckedClientTick + 1;
429
+ const payload = peer.pendingBySequence.get(nextSequence);
430
+ peer.pendingBySequence.delete(nextSequence);
431
+ this.handleIntent(peer, nextSequence, payload);
432
+ }
433
+ if (peer.pendingBySequence.size > 0) {
434
+ const STALL_LIMIT = 16;
435
+ let lowestPending = Infinity;
436
+ for (const sequence of peer.pendingBySequence.keys()) {
437
+ if (sequence < lowestPending)
438
+ lowestPending = sequence;
439
+ }
440
+ if (lowestPending - peer.lastAckedClientTick > STALL_LIMIT) {
441
+ peer.lastAckedClientTick = lowestPending - 1;
442
+ while (peer.pendingBySequence.has(peer.lastAckedClientTick + 1)) {
443
+ const nextSequence = peer.lastAckedClientTick + 1;
444
+ const payload = peer.pendingBySequence.get(nextSequence);
445
+ peer.pendingBySequence.delete(nextSequence);
446
+ this.handleIntent(peer, nextSequence, payload);
447
+ }
448
+ }
449
+ }
450
+ });
451
+ }
452
+ sendSnapshots() {
453
+ if (this.syncedComponents.length === 0)
454
+ return;
455
+ const seen = /* @__PURE__ */ new Set();
456
+ this.scratch.length = 0;
457
+ for (const c of this.syncedComponents) {
458
+ this.world.forEachDirty(c, (eid) => {
459
+ if (!seen.has(eid)) {
460
+ seen.add(eid);
461
+ this.scratch.push(eid);
462
+ }
463
+ });
464
+ }
465
+ const despawnedView = this.world.getDespawned();
466
+ const despawned = despawnedView.length > 0 ? Array.from(despawnedView) : [];
467
+ if (despawned.length > 0)
468
+ this.world.flushDespawned();
469
+ if (this.scratch.length === 0 && despawned.length === 0 && !this.anyNeedsBaseline()) {
470
+ return;
471
+ }
472
+ let baseline = null;
473
+ const computeBaseline = () => {
474
+ if (baseline !== null)
475
+ return baseline;
476
+ const result = [];
477
+ const baselineSeen = /* @__PURE__ */ new Set();
478
+ for (const c of this.syncedComponents) {
479
+ for (const eid of this.world.query(c)) {
480
+ if (!baselineSeen.has(eid)) {
481
+ baselineSeen.add(eid);
482
+ result.push(eid);
483
+ }
484
+ }
485
+ }
486
+ baseline = result;
487
+ return result;
488
+ };
489
+ const peerOut = [];
490
+ for (const peer of this.peers.values()) {
491
+ if (peer.kicking !== null)
492
+ continue;
493
+ const source = peer.needsBaseline ? computeBaseline() : this.scratch;
494
+ let current = source;
495
+ for (const plugin of this.plugins) {
496
+ if (plugin.filterSnapshot === void 0)
497
+ continue;
498
+ peerOut.length = 0;
499
+ plugin.filterSnapshot(peer, this.world, current, peerOut);
500
+ current = peerOut.slice();
501
+ }
502
+ if (current.length === 0 && despawned.length === 0 && !peer.needsBaseline)
503
+ continue;
504
+ const buf = encodeDelta(
505
+ this.world,
506
+ this.tickCounter,
507
+ current,
508
+ this.syncedComponents,
509
+ this.syncedNumMaskWords,
510
+ despawned,
511
+ peer.lastAckedClientTick
512
+ );
513
+ const framed = new Uint8Array(buf.length + 1);
514
+ framed[0] = MSG_SNAPSHOT;
515
+ framed.set(buf, 1);
516
+ peer.transport.send(framed);
517
+ peer.needsBaseline = false;
518
+ this.emit("snapshot", { peer, tick: this.tickCounter, byteSize: framed.length });
519
+ }
520
+ this.world.clearAllDirty();
521
+ }
522
+ anyNeedsBaseline() {
523
+ for (const peer of this.peers.values()) {
524
+ if (peer.kicking === null && peer.needsBaseline)
525
+ return true;
526
+ }
527
+ return false;
528
+ }
529
+ handleIncoming(peer, data) {
530
+ if (data.length === 0)
531
+ return;
532
+ const type = data[0];
533
+ if (peer.kicking !== null) {
534
+ if (type === CMSG_KICK_ACK)
535
+ this.completeKick(peer);
536
+ return;
537
+ }
538
+ if (type === CMSG_INTENT) {
539
+ if (data.length < 5) {
540
+ this.emit("intent-failed", { peer, kind: -1, reason: "decode-error" });
541
+ return;
542
+ }
543
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
544
+ const sequence = dv.getUint32(1, true);
545
+ peer.intentQueue.push({ sequence, payload: data.subarray(5) });
546
+ return;
547
+ }
548
+ if (type === CMSG_RPC) {
549
+ this.handleRpc(peer, data.subarray(1));
550
+ return;
551
+ }
552
+ this.emit("error", {
553
+ error: new Error(`Unknown client message type 0x${type.toString(16)}`),
554
+ context: "handleIncoming"
555
+ });
556
+ }
557
+ handleIntent(peer, sequence, payload) {
558
+ let intent;
559
+ try {
560
+ intent = this.intents.registry.decode(payload);
561
+ } catch (err) {
562
+ this.emit("intent-failed", {
563
+ peer,
564
+ kind: payload[0] ?? -1,
565
+ reason: "decode-error"
566
+ });
567
+ return;
568
+ }
569
+ const name = this.intents.nameByKind[intent.kind];
570
+ if (name === void 0) {
571
+ this.emit("intent-failed", {
572
+ peer,
573
+ kind: intent.kind,
574
+ reason: "unknown-kind"
575
+ });
576
+ return;
577
+ }
578
+ const ctx = {
579
+ world: this.world,
580
+ entity: peer.entity,
581
+ tick: this.tickCounter,
582
+ deltaTime: this.lastDt,
583
+ rng: this.rng,
584
+ fields: makeFieldsAccessor(this.world, peer.entity),
585
+ markDirty: makeMarkDirty(this.world, peer.entity),
586
+ peer,
587
+ clientTick: intent.tick,
588
+ lagCompensated: (fn) => {
589
+ if (this.lagComp === null)
590
+ return fn();
591
+ return this.lagComp.rewind(intent.tick, fn);
592
+ }
593
+ };
594
+ for (const plugin of this.plugins) {
595
+ plugin.onIntent?.(peer, intent.kind, name, intent, ctx);
596
+ }
597
+ const predFn = this.predictionMap?.[name];
598
+ if (predFn)
599
+ predFn(intent, ctx);
600
+ const handlerFn = this.handlerMap?.[name];
601
+ if (handlerFn)
602
+ handlerFn(intent, ctx);
603
+ peer.lastAckedClientTick = sequence;
604
+ this.emit("intent", {
605
+ peer,
606
+ kind: intent.kind,
607
+ name,
608
+ payload: intent,
609
+ tick: intent.tick
610
+ });
611
+ }
612
+ handleRpc(peer, payload) {
613
+ try {
614
+ const decoded = this.rpcs.registry.decode(payload);
615
+ if (decoded.method === "__murow_ping") {
616
+ const encoded = this.rpcs.registry.encode(PONG_RPC, { ts: decoded.data.ts });
617
+ const framed = new Uint8Array(encoded.length + 1);
618
+ framed[0] = MSG_RPC;
619
+ framed.set(encoded, 1);
620
+ peer.transport.send(framed);
621
+ return;
622
+ }
623
+ this.emit("rpc", { peer, name: decoded.method, payload: decoded.data });
624
+ } catch (err) {
625
+ this.emit("error", { error: err, context: "handleRpc" });
626
+ }
627
+ }
628
+ use(bundle) {
629
+ if (bundle && bundle.__kind === "predictions") {
630
+ this.predictionMap = bundle.map;
631
+ return this;
632
+ }
633
+ if (bundle && bundle.__kind === "handlers") {
634
+ this.handlerMap = bundle.map;
635
+ return this;
636
+ }
637
+ const plugin = bundle;
638
+ this.plugins.push(plugin);
639
+ plugin.onMount?.(this);
640
+ if (plugin.name === "lag-compensation" || plugin.rewind !== void 0) {
641
+ this.lagComp = plugin;
642
+ }
643
+ return this;
644
+ }
645
+ /**
646
+ * Bind an entity to a peer. Fills `ctx.entity` for the peer's intents
647
+ * and pushes a MSG_ASSIGN_ENTITY frame so the client knows which
648
+ * entity it owns.
649
+ */
650
+ assignEntity(peer, entityId) {
651
+ const internal = this.peers.get(peer.peerId);
652
+ if (internal === void 0)
653
+ return;
654
+ internal.entity = entityId;
655
+ const frame = new Uint8Array(1 + 4);
656
+ frame[0] = MSG_ASSIGN_ENTITY;
657
+ new DataView(frame.buffer).setUint32(1, entityId >>> 0, true);
658
+ internal.transport.send(frame);
659
+ }
660
+ sendRpc(peer, name, payload) {
661
+ const internal = this.peers.get(peer.peerId);
662
+ if (internal === void 0)
663
+ return;
664
+ if (internal.kicking !== null)
665
+ return;
666
+ const def = this.rpcs.defs[name];
667
+ if (def === void 0) {
668
+ this.emit("error", {
669
+ error: new Error(`Unknown RPC "${String(name)}"`),
670
+ context: "sendRpc"
671
+ });
672
+ return;
673
+ }
674
+ const encoded = this.rpcs.registry.encode(def, payload);
675
+ const framed = new Uint8Array(encoded.length + 1);
676
+ framed[0] = MSG_RPC;
677
+ framed.set(encoded, 1);
678
+ internal.transport.send(framed);
679
+ }
680
+ broadcastRpc(name, payload) {
681
+ for (const peer of this.peers.values()) {
682
+ if (peer.kicking !== null)
683
+ continue;
684
+ this.sendRpc(peer, name, payload);
685
+ }
686
+ }
687
+ /**
688
+ * Soft-kick: send KICK frame, wait `kick.ackTimeout` for the client's
689
+ * ack, then force the transport closed.
690
+ */
691
+ kick(peer, reason) {
692
+ const internal = this.peers.get(peer.peerId);
693
+ if (internal === void 0)
694
+ return;
695
+ if (internal.kicking !== null)
696
+ return;
697
+ const reasonBytes = new TextEncoder().encode(reason);
698
+ const framed = new Uint8Array(reasonBytes.length + 1);
699
+ framed[0] = MSG_KICK;
700
+ framed.set(reasonBytes, 1);
701
+ internal.transport.send(framed);
702
+ const timer = setTimeout(() => this.completeKick(internal), this.kickAckTimeout);
703
+ internal.kicking = { reason, sentAt: Date.now(), timer };
704
+ }
705
+ completeKick(peer) {
706
+ if (peer.kicking === null)
707
+ return;
708
+ clearTimeout(peer.kicking.timer);
709
+ this.handleDisconnect(peer, "kicked");
710
+ peer.transport.close();
711
+ }
712
+ };
713
+
714
+ // src/server/plugins/aoi-grid.ts
715
+ var AoiGrid = class {
716
+ constructor(opts) {
717
+ this.name = opts.name ?? "aoi";
718
+ this.cellSize = opts.cellSize;
719
+ this.radius = opts.radius;
720
+ this.hysteresisRadius = opts.hysteresisRadius ?? 0;
721
+ this.positionComponent = opts.positionComponent;
722
+ }
723
+ filterSnapshot(peer, world, dirtyEntities, out) {
724
+ if (peer.entity === -1 || !world.has(peer.entity, this.positionComponent)) {
725
+ for (let i = 0; i < dirtyEntities.length; i++)
726
+ out.push(dirtyEntities[i]);
727
+ return;
728
+ }
729
+ const fields = world.fields(this.positionComponent);
730
+ const xs = fields.x;
731
+ const ys = fields.y;
732
+ const px = xs[peer.entity];
733
+ const py = ys[peer.entity];
734
+ const maxR = this.radius + this.hysteresisRadius;
735
+ const maxR2 = maxR * maxR;
736
+ for (let i = 0; i < dirtyEntities.length; i++) {
737
+ const eid = dirtyEntities[i];
738
+ if (!world.has(eid, this.positionComponent)) {
739
+ out.push(eid);
740
+ continue;
741
+ }
742
+ const dx = xs[eid] - px;
743
+ const dy = ys[eid] - py;
744
+ if (dx * dx + dy * dy <= maxR2)
745
+ out.push(eid);
746
+ }
747
+ }
748
+ };
749
+
750
+ // src/server/plugins/lag-compensation.ts
751
+ var LagCompensation = class {
752
+ constructor(opts) {
753
+ this.componentIndices = [];
754
+ this.ringBuffer = [];
755
+ this.ringHead = 0;
756
+ this.currentTick = 0;
757
+ this.world = null;
758
+ this.name = opts.name ?? "lag-compensation";
759
+ this.historyMs = opts.historyMs ?? 500;
760
+ this.tickRate = opts.tickRate;
761
+ this.components = opts.components;
762
+ this.ringSize = Math.ceil(this.historyMs / 1e3 * this.tickRate) + 1;
763
+ for (let i = 0; i < this.ringSize; i++) {
764
+ this.ringBuffer.push({ tick: -1, snapshots: [] });
765
+ }
766
+ }
767
+ onMount(server) {
768
+ this.world = server.world;
769
+ for (const c of this.components) {
770
+ if (c.__worldIndex === void 0) {
771
+ throw new Error(
772
+ `LagCompensation: component "${c.name}" is not registered in the world.`
773
+ );
774
+ }
775
+ this.componentIndices.push(c.__worldIndex);
776
+ }
777
+ }
778
+ onTick(_world, _dt) {
779
+ const world = this.world;
780
+ if (world === null)
781
+ return;
782
+ this.currentTick++;
783
+ const frame = this.ringBuffer[this.ringHead];
784
+ frame.tick = this.currentTick;
785
+ frame.snapshots = [];
786
+ for (let i = 0; i < this.components.length; i++) {
787
+ const c = this.components[i];
788
+ const snap = /* @__PURE__ */ new Map();
789
+ world.forEachDirty(c, (eid) => {
790
+ snap.set(eid, { ...world.get(eid, c) });
791
+ });
792
+ if (snap.size === 0) {
793
+ for (const eid of world.query(c)) {
794
+ snap.set(eid, { ...world.get(eid, c) });
795
+ }
796
+ }
797
+ frame.snapshots[i] = snap;
798
+ }
799
+ this.ringHead = (this.ringHead + 1) % this.ringSize;
800
+ }
801
+ rewind(clientTick, fn) {
802
+ const world = this.world;
803
+ if (world === null)
804
+ return fn();
805
+ let chosen = null;
806
+ let bestDelta = Number.MAX_SAFE_INTEGER;
807
+ for (const frame of this.ringBuffer) {
808
+ if (frame.tick < 0)
809
+ continue;
810
+ const d = Math.abs(frame.tick - clientTick);
811
+ if (d < bestDelta) {
812
+ bestDelta = d;
813
+ chosen = frame;
814
+ }
815
+ }
816
+ if (chosen === null)
817
+ return fn();
818
+ const saved = [];
819
+ for (let i = 0; i < this.components.length; i++) {
820
+ const c = this.components[i];
821
+ const snap = chosen.snapshots[i];
822
+ if (!snap) {
823
+ saved[i] = /* @__PURE__ */ new Map();
824
+ continue;
825
+ }
826
+ const savedFrame = /* @__PURE__ */ new Map();
827
+ for (const [eid, value] of snap) {
828
+ if (world.has(eid, c)) {
829
+ savedFrame.set(eid, { ...world.get(eid, c) });
830
+ world.set(eid, c, value);
831
+ }
832
+ }
833
+ saved[i] = savedFrame;
834
+ }
835
+ try {
836
+ return fn();
837
+ } finally {
838
+ for (let i = 0; i < this.components.length; i++) {
839
+ const c = this.components[i];
840
+ const savedFrame = saved[i];
841
+ for (const [eid, value] of savedFrame) {
842
+ if (world.has(eid, c))
843
+ world.set(eid, c, value);
844
+ }
845
+ }
846
+ }
847
+ }
848
+ };
849
+
850
+ // src/client/game-client.ts
851
+ var import_binary_codec2 = require("murow/core/binary-codec");
852
+ var import_simple_rng2 = require("murow/core/simple-rng");
853
+ var import_protocol4 = require("murow/protocol");
854
+
855
+ // src/client/interpolation-buffer.ts
856
+ var import_lerp = require("murow/core/lerp");
857
+ function modeFor(c) {
858
+ const sync = c.__sync;
859
+ return sync?.interp ?? "lerp";
860
+ }
861
+ var InterpolationBuffer = class {
862
+ constructor(serverToLocal, capacity, delayMs, staleWindowMs) {
863
+ this.buffer = [];
864
+ this.renderTick = -Infinity;
865
+ this.latestReceivedAt = -Infinity;
866
+ this.smoothedTickRateMs = 0;
867
+ this.serverToLocal = serverToLocal;
868
+ this.capacity = capacity;
869
+ this.delay = delayMs;
870
+ this.staleWindow = staleWindowMs;
871
+ }
872
+ setDelay(delayMs) {
873
+ this.delay = delayMs;
874
+ }
875
+ setStaleWindow(staleWindowMs) {
876
+ this.staleWindow = staleWindowMs;
877
+ }
878
+ record(snapshot) {
879
+ const gap = snapshot.receivedAt - this.latestReceivedAt;
880
+ if (this.buffer.length > 0 && gap > this.staleWindow) {
881
+ this.buffer.length = 0;
882
+ this.renderTick = -Infinity;
883
+ this.latestReceivedAt = -Infinity;
884
+ this.smoothedTickRateMs = 0;
885
+ }
886
+ if (snapshot.receivedAt > this.latestReceivedAt) {
887
+ this.latestReceivedAt = snapshot.receivedAt;
888
+ }
889
+ let insertAt = this.buffer.length;
890
+ while (insertAt > 0 && this.buffer[insertAt - 1].serverTick >= snapshot.serverTick) {
891
+ if (this.buffer[insertAt - 1].serverTick === snapshot.serverTick)
892
+ return;
893
+ insertAt--;
894
+ }
895
+ this.buffer.splice(insertAt, 0, snapshot);
896
+ while (this.buffer.length > this.capacity)
897
+ this.buffer.shift();
898
+ }
899
+ clear() {
900
+ this.buffer.length = 0;
901
+ this.renderTick = -Infinity;
902
+ this.latestReceivedAt = -Infinity;
903
+ this.smoothedTickRateMs = 0;
904
+ }
905
+ apply(world, now, components, shouldSkip) {
906
+ if (this.buffer.length === 0)
907
+ return;
908
+ const newest = this.buffer[this.buffer.length - 1];
909
+ const oldest = this.buffer[0];
910
+ let tickRateMs = 0;
911
+ if (this.buffer.length >= 2) {
912
+ const tickSpan = newest.serverTick - oldest.serverTick;
913
+ const wallSpan = this.latestReceivedAt - oldest.receivedAt;
914
+ const rawTickRateMs = tickSpan > 0 && wallSpan > 0 ? wallSpan / tickSpan : 0;
915
+ if (rawTickRateMs > 0) {
916
+ if (this.smoothedTickRateMs === 0)
917
+ this.smoothedTickRateMs = rawTickRateMs;
918
+ else
919
+ this.smoothedTickRateMs = this.smoothedTickRateMs * 0.9 + rawTickRateMs * 0.1;
920
+ }
921
+ tickRateMs = this.smoothedTickRateMs;
922
+ }
923
+ if (tickRateMs === 0) {
924
+ if (now - newest.receivedAt < this.delay)
925
+ return;
926
+ this.writeSnapshot(world, newest, components, shouldSkip);
927
+ return;
928
+ }
929
+ const ageBeyondDelay = now - newest.receivedAt - this.delay;
930
+ const targetTick = newest.serverTick + ageBeyondDelay / tickRateMs;
931
+ if (this.renderTick === -Infinity) {
932
+ this.renderTick = targetTick;
933
+ } else {
934
+ const drift = targetTick - this.renderTick;
935
+ if (drift > 2) {
936
+ this.renderTick = targetTick;
937
+ } else {
938
+ const warp = Math.max(0.9, Math.min(1.1, 1 + drift * 0.05));
939
+ this.renderTick += warp;
940
+ }
941
+ }
942
+ const renderTick = this.renderTick;
943
+ let a = null;
944
+ let b = null;
945
+ for (let i = 0; i < this.buffer.length - 1; i++) {
946
+ const s0 = this.buffer[i];
947
+ const s1 = this.buffer[i + 1];
948
+ if (s0.serverTick <= renderTick && renderTick <= s1.serverTick) {
949
+ a = s0;
950
+ b = s1;
951
+ break;
952
+ }
953
+ }
954
+ if (a === null || b === null) {
955
+ if (renderTick < oldest.serverTick)
956
+ return;
957
+ this.writeSnapshot(world, newest, components, shouldSkip);
958
+ return;
959
+ }
960
+ const seen = /* @__PURE__ */ new Set();
961
+ for (const eid of a.entityIds)
962
+ seen.add(eid);
963
+ for (const eid of b.entityIds)
964
+ seen.add(eid);
965
+ const aIndex = this.buffer.indexOf(a);
966
+ const bIndex = this.buffer.indexOf(b);
967
+ for (const serverEid of seen) {
968
+ const localEid = this.serverToLocal.get(serverEid);
969
+ if (localEid === void 0)
970
+ continue;
971
+ if (shouldSkip(localEid))
972
+ continue;
973
+ for (const c of components) {
974
+ let aIdx = aIndex;
975
+ let va = a.componentValuesByEntity.get(serverEid)?.get(c);
976
+ while (va === void 0 && aIdx > 0) {
977
+ aIdx--;
978
+ va = this.buffer[aIdx].componentValuesByEntity.get(serverEid)?.get(c);
979
+ }
980
+ let bIdx = bIndex;
981
+ let vb = b.componentValuesByEntity.get(serverEid)?.get(c);
982
+ while (vb === void 0 && bIdx < this.buffer.length - 1) {
983
+ bIdx++;
984
+ vb = this.buffer[bIdx].componentValuesByEntity.get(serverEid)?.get(c);
985
+ }
986
+ if (va === void 0 && vb === void 0)
987
+ continue;
988
+ let toWrite;
989
+ if (vb === void 0) {
990
+ toWrite = va;
991
+ } else if (va === void 0) {
992
+ toWrite = vb;
993
+ } else {
994
+ const mode = modeFor(c);
995
+ if (mode === "none") {
996
+ toWrite = vb;
997
+ } else {
998
+ const aTick = this.buffer[aIdx].serverTick;
999
+ const bTick = this.buffer[bIdx].serverTick;
1000
+ const wideSpan = bTick - aTick;
1001
+ const wideT = wideSpan > 0 ? Math.min(1, Math.max(0, (renderTick - aTick) / wideSpan)) : 0;
1002
+ if (mode === "step") {
1003
+ toWrite = wideT < 0.5 ? va : vb;
1004
+ } else {
1005
+ const out = {};
1006
+ for (const fieldName of c.fieldNames) {
1007
+ out[fieldName] = (0, import_lerp.lerp)(
1008
+ va[fieldName],
1009
+ vb[fieldName],
1010
+ wideT
1011
+ );
1012
+ }
1013
+ toWrite = out;
1014
+ }
1015
+ }
1016
+ }
1017
+ if (toWrite === void 0)
1018
+ continue;
1019
+ if (world.has(localEid, c)) {
1020
+ world.update(localEid, c, toWrite);
1021
+ } else {
1022
+ world.add(localEid, c, toWrite);
1023
+ }
1024
+ }
1025
+ }
1026
+ }
1027
+ writeSnapshot(world, snap, components, shouldSkip) {
1028
+ for (const serverEid of snap.entityIds) {
1029
+ const localEid = this.serverToLocal.get(serverEid);
1030
+ if (localEid === void 0)
1031
+ continue;
1032
+ if (shouldSkip(localEid))
1033
+ continue;
1034
+ const comps = snap.componentValuesByEntity.get(serverEid);
1035
+ if (comps === void 0)
1036
+ continue;
1037
+ for (const c of components) {
1038
+ const v = comps.get(c);
1039
+ if (v === void 0)
1040
+ continue;
1041
+ if (world.has(localEid, c))
1042
+ world.update(localEid, c, v);
1043
+ else
1044
+ world.add(localEid, c, v);
1045
+ }
1046
+ }
1047
+ }
1048
+ };
1049
+
1050
+ // src/client/game-client.ts
1051
+ var MSG_SNAPSHOT2 = 128;
1052
+ var MSG_RPC2 = 129;
1053
+ var MSG_KICK2 = 130;
1054
+ var MSG_ASSIGN_ENTITY2 = 132;
1055
+ var CMSG_INTENT2 = 1;
1056
+ var CMSG_RPC2 = 2;
1057
+ var CMSG_KICK_ACK2 = 3;
1058
+ var PING_RPC2 = (0, import_protocol4.defineRPC)({ method: "__murow_ping", schema: { ts: import_binary_codec2.u16 } });
1059
+ var PONG_RPC2 = (0, import_protocol4.defineRPC)({ method: "__murow_pong", schema: { ts: import_binary_codec2.u16 } });
1060
+ var GameClient = class extends Network {
1061
+ constructor(opts) {
1062
+ super([
1063
+ "connected",
1064
+ "disconnected",
1065
+ "kicked",
1066
+ "snapshot",
1067
+ "rpc",
1068
+ "spawn",
1069
+ "despawn",
1070
+ "reconciled",
1071
+ "assigned",
1072
+ "pong",
1073
+ "error"
1074
+ ]);
1075
+ this.predictionMap = null;
1076
+ this.predictionHistory = [];
1077
+ this.localTick = 0;
1078
+ this.intentSequence = 0;
1079
+ this.lastServerTick = 0;
1080
+ this.lastReconciledServerTick = 0;
1081
+ this.serverToLocal = /* @__PURE__ */ new Map();
1082
+ this.syncedComponents = [];
1083
+ this.syncedNumMaskWords = 1;
1084
+ this.predictedEntities = /* @__PURE__ */ new Set();
1085
+ /** Set when MSG_ASSIGN_ENTITY lands before the matching spawn. */
1086
+ this.pendingAssignedServerEid = null;
1087
+ /** Resolved local entity for the server's assignment. Default for sendIntent. */
1088
+ this._assignedEntity = null;
1089
+ this._rttMs = null;
1090
+ this.world = opts.world;
1091
+ this.loop = opts.loop;
1092
+ this.transport = opts.transport;
1093
+ this.intents = opts.protocol.intents;
1094
+ this.rpcs = opts.protocol.rpcs;
1095
+ const reg = this.rpcs.registry;
1096
+ if (!reg.has(PING_RPC2.method))
1097
+ reg.register(PING_RPC2);
1098
+ if (!reg.has(PONG_RPC2.method))
1099
+ reg.register(PONG_RPC2);
1100
+ const strategy = opts.strategy ?? { kind: "snapshot-interpolation" };
1101
+ const interpolationDelay = strategy.delay ?? 100;
1102
+ const staleWindowMs = strategy.staleWindow ?? interpolationDelay * 2 + 100;
1103
+ this.predictionBufferSize = opts.prediction?.bufferSize ?? 64;
1104
+ this.rng = new import_simple_rng2.SimpleRNG(1);
1105
+ this.lastDt = opts.loop.ticker.intervalMs / 1e3;
1106
+ this.now = opts.now ?? (() => performance.now());
1107
+ this.interpBuffer = new InterpolationBuffer(
1108
+ this.serverToLocal,
1109
+ 16,
1110
+ interpolationDelay,
1111
+ staleWindowMs
1112
+ );
1113
+ this.discoverSyncedComponents();
1114
+ this.wireTransport();
1115
+ this.wireLoop();
1116
+ }
1117
+ /**
1118
+ * Local entity the server has assigned to this peer, or `null` if no
1119
+ * assignment has been received yet. `sendIntent` is a no-op (emits an
1120
+ * `'error'` event) until this is set.
1121
+ */
1122
+ get assignedEntity() {
1123
+ return this._assignedEntity;
1124
+ }
1125
+ /**
1126
+ * Last measured round-trip time in milliseconds, or `null` if no pong
1127
+ * has come back yet. Updated whenever a `'pong'` event fires.
1128
+ */
1129
+ get rttMs() {
1130
+ return this._rttMs;
1131
+ }
1132
+ discoverSyncedComponents() {
1133
+ const all = this.world.components;
1134
+ for (const c of all) {
1135
+ if (c.__sync !== void 0)
1136
+ this.syncedComponents.push(c);
1137
+ }
1138
+ this.syncedNumMaskWords = Math.max(1, Math.ceil(this.syncedComponents.length / 32));
1139
+ }
1140
+ wireTransport() {
1141
+ this.transport.onOpen?.(() => this.emit("connected", {}));
1142
+ this.transport.onMessage((data) => this.handleIncoming(data));
1143
+ this.transport.onClose(() => this.emit("disconnected", { reason: "transport-closed" }));
1144
+ this.transport.onError?.((error) => this.emit("error", { error, context: "transport" }));
1145
+ }
1146
+ wireLoop() {
1147
+ const events = this.loop.events;
1148
+ events.on("sync", () => {
1149
+ this.interpBuffer.apply(
1150
+ this.world,
1151
+ this.now(),
1152
+ this.syncedComponents,
1153
+ (e) => this.predictedEntities.has(e)
1154
+ );
1155
+ });
1156
+ events.on("tick", ({ deltaTime }) => {
1157
+ this.localTick++;
1158
+ this.lastDt = deltaTime;
1159
+ });
1160
+ }
1161
+ handleIncoming(data) {
1162
+ if (data.length === 0)
1163
+ return;
1164
+ const type = data[0];
1165
+ if (type === MSG_SNAPSHOT2) {
1166
+ this.handleSnapshot(data.subarray(1));
1167
+ return;
1168
+ }
1169
+ if (type === MSG_RPC2) {
1170
+ this.handleRpc(data.subarray(1));
1171
+ return;
1172
+ }
1173
+ if (type === MSG_KICK2) {
1174
+ const reason = new TextDecoder().decode(data.subarray(1));
1175
+ this.emit("kicked", { reason });
1176
+ const ack = new Uint8Array([CMSG_KICK_ACK2]);
1177
+ this.transport.send(ack);
1178
+ return;
1179
+ }
1180
+ if (type === MSG_ASSIGN_ENTITY2) {
1181
+ if (data.length < 5) {
1182
+ this.emit("error", {
1183
+ error: new Error("MSG_ASSIGN_ENTITY frame too short"),
1184
+ context: "handleIncoming"
1185
+ });
1186
+ return;
1187
+ }
1188
+ const view = new DataView(data.buffer, data.byteOffset + 1, 4);
1189
+ this.applyAssignment(view.getUint32(0, true));
1190
+ return;
1191
+ }
1192
+ this.emit("error", {
1193
+ error: new Error(`Unknown server message type 0x${type.toString(16)}`),
1194
+ context: "handleIncoming"
1195
+ });
1196
+ }
1197
+ handleSnapshot(payload) {
1198
+ try {
1199
+ const decoded = decodeDelta(
1200
+ this.world,
1201
+ payload,
1202
+ this.syncedComponents,
1203
+ this.syncedNumMaskWords,
1204
+ (serverEid, present) => this.ensureEntity(serverEid, present),
1205
+ // Never overwrite live state from the snapshot path.
1206
+ // Predicted entities go through reconcile; peer entities
1207
+ // go through the buffer's `sync` apply. decodeDelta still
1208
+ // archetype-inits first-appearance components.
1209
+ () => false
1210
+ );
1211
+ this.lastServerTick = decoded.tick;
1212
+ this.emit("snapshot", { tick: decoded.tick, byteSize: payload.length + 1 });
1213
+ this.interpBuffer.record({
1214
+ receivedAt: this.now(),
1215
+ serverTick: decoded.tick,
1216
+ entityIds: decoded.serverEntityIds,
1217
+ componentValuesByEntity: decoded.valuesByServerEntity
1218
+ });
1219
+ for (const serverEid of decoded.despawnedServerIds) {
1220
+ const localEid = this.serverToLocal.get(serverEid);
1221
+ if (localEid === void 0)
1222
+ continue;
1223
+ this.serverToLocal.delete(serverEid);
1224
+ if (this.world.isAlive(localEid))
1225
+ this.world.despawn(localEid);
1226
+ this.emit("despawn", { entity: localEid });
1227
+ }
1228
+ if (decoded.tick <= this.lastReconciledServerTick)
1229
+ return;
1230
+ this.lastReconciledServerTick = decoded.tick;
1231
+ this.reconcile(decoded);
1232
+ } catch (err) {
1233
+ this.emit("error", { error: err, context: "handleSnapshot" });
1234
+ }
1235
+ }
1236
+ ensureEntity(serverEid, present) {
1237
+ let localEid = this.serverToLocal.get(serverEid);
1238
+ if (localEid === void 0) {
1239
+ localEid = this.world.spawn();
1240
+ this.serverToLocal.set(serverEid, localEid);
1241
+ const components = {};
1242
+ for (const c of present)
1243
+ components[c.name] = true;
1244
+ this.emit("spawn", { entity: localEid, components });
1245
+ if (this.pendingAssignedServerEid === serverEid) {
1246
+ this.pendingAssignedServerEid = null;
1247
+ this.predictedEntities.add(localEid);
1248
+ this._assignedEntity = localEid;
1249
+ this.emit("assigned", { entity: localEid });
1250
+ }
1251
+ }
1252
+ return localEid;
1253
+ }
1254
+ applyAssignment(serverEid) {
1255
+ const localEid = this.serverToLocal.get(serverEid);
1256
+ if (localEid === void 0) {
1257
+ this.pendingAssignedServerEid = serverEid;
1258
+ return;
1259
+ }
1260
+ this.predictedEntities.add(localEid);
1261
+ this._assignedEntity = localEid;
1262
+ this.emit("assigned", { entity: localEid });
1263
+ }
1264
+ /**
1265
+ * Rewind predicted entities to authoritative state, drop predictions
1266
+ * the server has acked, replay the rest on top.
1267
+ */
1268
+ reconcile(decoded) {
1269
+ const resetEntities = /* @__PURE__ */ new Set();
1270
+ for (const serverEid of decoded.serverEntityIds) {
1271
+ const localEid = this.serverToLocal.get(serverEid);
1272
+ if (localEid === void 0)
1273
+ continue;
1274
+ if (!this.predictedEntities.has(localEid))
1275
+ continue;
1276
+ const comps = decoded.valuesByServerEntity.get(serverEid);
1277
+ if (comps === void 0)
1278
+ continue;
1279
+ for (const [c, value] of comps) {
1280
+ if (this.world.has(localEid, c)) {
1281
+ this.world.update(localEid, c, value);
1282
+ } else {
1283
+ this.world.add(localEid, c, value);
1284
+ }
1285
+ }
1286
+ resetEntities.add(localEid);
1287
+ }
1288
+ const ackSequence = decoded.clientAckTick;
1289
+ let cut = 0;
1290
+ while (cut < this.predictionHistory.length && this.predictionHistory[cut].sequence <= ackSequence) {
1291
+ cut++;
1292
+ }
1293
+ if (cut > 0)
1294
+ this.predictionHistory.splice(0, cut);
1295
+ const remaining = this.predictionHistory.length;
1296
+ if (remaining === 0) {
1297
+ this.emit("reconciled", { rewindTick: ackSequence, replayed: 0 });
1298
+ return;
1299
+ }
1300
+ let replayedCount = 0;
1301
+ for (let i = 0; i < remaining; i++) {
1302
+ const pred = this.predictionHistory[i];
1303
+ if (!resetEntities.has(pred.entity))
1304
+ continue;
1305
+ const predFn = this.predictionMap?.[pred.name];
1306
+ if (predFn === void 0)
1307
+ continue;
1308
+ const ctx = {
1309
+ world: this.world,
1310
+ entity: pred.entity,
1311
+ tick: pred.tick,
1312
+ deltaTime: pred.deltaTime,
1313
+ rng: this.rng,
1314
+ fields: makeFieldsAccessor(this.world, pred.entity),
1315
+ markDirty: makeMarkDirty(this.world, pred.entity)
1316
+ };
1317
+ predFn(pred.payload, ctx);
1318
+ replayedCount++;
1319
+ }
1320
+ this.emit("reconciled", {
1321
+ rewindTick: ackSequence,
1322
+ replayed: replayedCount
1323
+ });
1324
+ }
1325
+ handleRpc(payload) {
1326
+ try {
1327
+ const { method, data } = this.rpcs.registry.decode(payload);
1328
+ if (method === "__murow_pong") {
1329
+ const rtt = Date.now() - data.ts & 65535;
1330
+ this._rttMs = rtt;
1331
+ this.emit("pong", { rtt });
1332
+ }
1333
+ this.emit("rpc", { name: method, payload: data });
1334
+ } catch (err) {
1335
+ this.emit("error", { error: err, context: "handleRpc" });
1336
+ }
1337
+ }
1338
+ use(bundle) {
1339
+ if (bundle && bundle.__kind === "predictions") {
1340
+ this.predictionMap = bundle.map;
1341
+ return this;
1342
+ }
1343
+ return this;
1344
+ }
1345
+ /**
1346
+ * Send an intent to the server. `ctx.entity` in the prediction is the
1347
+ * server-assigned entity. Target entities (chestId, targetId, etc.)
1348
+ * belong in the payload itself.
1349
+ *
1350
+ * Returns `true` when the intent was sent, `false` when blocked. Both
1351
+ * blocked paths emit an `'error'` event so calling code can react:
1352
+ * - Unknown intent name.
1353
+ * - No entity assigned yet (server hasn't called `assignEntity` for
1354
+ * this peer). Check `client.assignedEntity` if you want to know
1355
+ * without sending.
1356
+ */
1357
+ sendIntent(name, payload) {
1358
+ const def = this.intents.defs[name];
1359
+ if (def === void 0) {
1360
+ this.emit("error", {
1361
+ error: new Error(`Unknown intent "${String(name)}"`),
1362
+ context: "sendIntent"
1363
+ });
1364
+ return false;
1365
+ }
1366
+ const entity = this._assignedEntity;
1367
+ if (entity === null) {
1368
+ this.emit("error", {
1369
+ error: new Error(`Cannot send intent "${String(name)}": no entity assigned yet`),
1370
+ context: "sendIntent"
1371
+ });
1372
+ return false;
1373
+ }
1374
+ const intentObj = { ...payload, kind: def.kind, tick: this.localTick };
1375
+ const encoded = this.intents.registry.encode(intentObj);
1376
+ const sequence = ++this.intentSequence;
1377
+ const framed = new Uint8Array(encoded.length + 5);
1378
+ framed[0] = CMSG_INTENT2;
1379
+ const dv = new DataView(framed.buffer, framed.byteOffset, framed.byteLength);
1380
+ dv.setUint32(1, sequence >>> 0, true);
1381
+ framed.set(encoded, 5);
1382
+ this.transport.send(framed);
1383
+ const predFn = this.predictionMap?.[name];
1384
+ if (predFn !== void 0) {
1385
+ const deltaTime = this.lastDt;
1386
+ const ctx = {
1387
+ world: this.world,
1388
+ entity,
1389
+ tick: this.localTick,
1390
+ deltaTime,
1391
+ rng: this.rng,
1392
+ fields: makeFieldsAccessor(this.world, entity),
1393
+ markDirty: makeMarkDirty(this.world, entity)
1394
+ };
1395
+ predFn(payload, ctx);
1396
+ this.predictionHistory.push({
1397
+ tick: this.localTick,
1398
+ sequence,
1399
+ name,
1400
+ payload,
1401
+ entity,
1402
+ deltaTime
1403
+ });
1404
+ this.predictedEntities.add(entity);
1405
+ while (this.predictionHistory.length > this.predictionBufferSize) {
1406
+ this.predictionHistory.shift();
1407
+ }
1408
+ }
1409
+ return true;
1410
+ }
1411
+ sendRpc(name, payload) {
1412
+ const def = this.rpcs.defs[name];
1413
+ if (def === void 0) {
1414
+ this.emit("error", {
1415
+ error: new Error(`Unknown RPC "${String(name)}"`),
1416
+ context: "sendRpc"
1417
+ });
1418
+ return;
1419
+ }
1420
+ const encoded = this.rpcs.registry.encode(def, payload);
1421
+ const framed = new Uint8Array(encoded.length + 1);
1422
+ framed[0] = CMSG_RPC2;
1423
+ framed.set(encoded, 1);
1424
+ this.transport.send(framed);
1425
+ }
1426
+ ping() {
1427
+ const ts = Date.now() & 65535;
1428
+ const encoded = this.rpcs.registry.encode(PING_RPC2, { ts });
1429
+ const framed = new Uint8Array(encoded.length + 1);
1430
+ framed[0] = CMSG_RPC2;
1431
+ framed.set(encoded, 1);
1432
+ this.transport.send(framed);
1433
+ }
1434
+ getLocalTick() {
1435
+ return this.localTick;
1436
+ }
1437
+ getPredictionDepth() {
1438
+ return this.predictionHistory.length;
1439
+ }
1440
+ get interpolationDelay() {
1441
+ return this.interpBuffer.delay;
1442
+ }
1443
+ setInterpolationDelay(ms) {
1444
+ this.interpBuffer.setDelay(ms);
1445
+ }
1446
+ };
1447
+
1448
+ // src/transports/memory-transport.ts
1449
+ var MemoryServerTransport = class {
1450
+ constructor() {
1451
+ this.peers = /* @__PURE__ */ new Map();
1452
+ this.connectionHandler = null;
1453
+ this.disconnectionHandler = null;
1454
+ this.nextPeerId = 1;
1455
+ }
1456
+ onConnection(handler) {
1457
+ this.connectionHandler = handler;
1458
+ }
1459
+ onDisconnection(handler) {
1460
+ this.disconnectionHandler = handler;
1461
+ }
1462
+ getPeer(peerId) {
1463
+ return this.peers.get(peerId);
1464
+ }
1465
+ getPeerIds() {
1466
+ return [...this.peers.keys()];
1467
+ }
1468
+ /**
1469
+ * Simulate a client connection. Returns the client-side adapter and
1470
+ * the server-assigned peer id. Fires the server's connection handler
1471
+ * synchronously; the client's `onOpen` fires on the next microtask.
1472
+ */
1473
+ connectClient() {
1474
+ const peerId = `peer_${this.nextPeerId++}`;
1475
+ const peer = new MemoryPeerTransport(peerId, this);
1476
+ this.peers.set(peerId, peer);
1477
+ this.connectionHandler?.(peer, peerId);
1478
+ peer._openClient();
1479
+ return { client: peer.clientView(), peerId };
1480
+ }
1481
+ /** Called by a peer when it disconnects. */
1482
+ _peerClosed(peerId) {
1483
+ this.peers.delete(peerId);
1484
+ this.disconnectionHandler?.(peerId);
1485
+ }
1486
+ close() {
1487
+ for (const peerId of [...this.peers.keys()]) {
1488
+ this._peerClosed(peerId);
1489
+ }
1490
+ }
1491
+ };
1492
+ var MemoryPeerTransport = class {
1493
+ constructor(peerId, server) {
1494
+ this.peerId = peerId;
1495
+ this.server = server;
1496
+ this.serverMessageHandler = null;
1497
+ this.serverCloseHandler = null;
1498
+ this.clientMessageHandler = null;
1499
+ this.clientCloseHandler = null;
1500
+ this.clientOpenHandler = null;
1501
+ }
1502
+ // Server-side TransportAdapter
1503
+ send(data) {
1504
+ const copy = new Uint8Array(data);
1505
+ queueMicrotask(() => this.clientMessageHandler?.(copy));
1506
+ }
1507
+ onOpen(_handler) {
1508
+ }
1509
+ onMessage(handler) {
1510
+ this.serverMessageHandler = handler;
1511
+ }
1512
+ onClose(handler) {
1513
+ this.serverCloseHandler = handler;
1514
+ }
1515
+ close() {
1516
+ this.serverCloseHandler?.();
1517
+ this.clientCloseHandler?.();
1518
+ this.server._peerClosed(this.peerId);
1519
+ }
1520
+ /** Client-side adapter view: sends to the server, observes server messages. */
1521
+ clientView() {
1522
+ const self = this;
1523
+ return {
1524
+ send(data) {
1525
+ const copy = new Uint8Array(data);
1526
+ queueMicrotask(() => self.serverMessageHandler?.(copy));
1527
+ },
1528
+ onOpen(handler) {
1529
+ self.clientOpenHandler = handler;
1530
+ },
1531
+ onMessage(handler) {
1532
+ self.clientMessageHandler = handler;
1533
+ },
1534
+ onClose(handler) {
1535
+ self.clientCloseHandler = handler;
1536
+ },
1537
+ close() {
1538
+ self.clientCloseHandler?.();
1539
+ self.serverCloseHandler?.();
1540
+ self.server._peerClosed(self.peerId);
1541
+ }
1542
+ };
1543
+ }
1544
+ _openClient() {
1545
+ queueMicrotask(() => this.clientOpenHandler?.());
1546
+ }
1547
+ };
1548
+
1549
+ // src/components/sync-spec.ts
1550
+ function networked(spec) {
1551
+ return spec;
1552
+ }