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