archetype-ecs-net 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.
- package/README.md +287 -0
- package/dist/ComponentRegistry.d.ts +14 -0
- package/dist/ComponentRegistry.js +90 -0
- package/dist/DirtyTracker.d.ts +47 -0
- package/dist/DirtyTracker.js +348 -0
- package/dist/InterestManager.d.ts +25 -0
- package/dist/InterestManager.js +71 -0
- package/dist/NetClient.d.ts +14 -0
- package/dist/NetClient.js +123 -0
- package/dist/NetServer.d.ts +35 -0
- package/dist/NetServer.js +166 -0
- package/dist/Protocol.d.ts +58 -0
- package/dist/Protocol.js +356 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +4 -0
- package/dist/types.d.ts +40 -0
- package/dist/types.js +3 -0
- package/package.json +43 -0
- package/src/ComponentRegistry.ts +115 -0
- package/src/DirtyTracker.ts +459 -0
- package/src/InterestManager.ts +104 -0
- package/src/NetClient.ts +156 -0
- package/src/NetServer.ts +240 -0
- package/src/Protocol.ts +410 -0
- package/src/index.ts +18 -0
- package/src/types.ts +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<br>
|
|
3
|
+
<img src="https://em-content.zobj.net/source/apple/391/satellite-antenna_1f4e1.png" width="80" />
|
|
4
|
+
<br><br>
|
|
5
|
+
<strong>archetype-ecs-net</strong>
|
|
6
|
+
<br>
|
|
7
|
+
<sub>Binary delta sync over WebSocket for archetype-ecs.</sub>
|
|
8
|
+
<br><br>
|
|
9
|
+
<a href="https://www.npmjs.com/package/archetype-ecs-net"><img src="https://img.shields.io/npm/v/archetype-ecs-net.svg?style=flat-square&color=000" alt="npm" /></a>
|
|
10
|
+
<a href="https://github.com/RvRooijen/archetype-ecs-net/blob/master/LICENSE"><img src="https://img.shields.io/npm/l/archetype-ecs-net.svg?style=flat-square&color=000" alt="license" /></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Network layer for [archetype-ecs](https://github.com/RvRooijen/archetype-ecs). Tag entities with `Networked`, call `tick()` every frame — clients get binary deltas automatically.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm i archetype-ecs-net
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### The full picture in 30 lines
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { createEntityManager, component } from 'archetype-ecs'
|
|
27
|
+
import { createComponentRegistry, createNetServer, Networked } from 'archetype-ecs-net'
|
|
28
|
+
|
|
29
|
+
const Position = component('Position', { x: 'f32', y: 'f32' })
|
|
30
|
+
const Velocity = component('Velocity', { vx: 'f32', vy: 'f32' })
|
|
31
|
+
|
|
32
|
+
const registry = createComponentRegistry([
|
|
33
|
+
{ component: Position, name: 'Position' },
|
|
34
|
+
{ component: Velocity, name: 'Velocity' },
|
|
35
|
+
])
|
|
36
|
+
|
|
37
|
+
const em = createEntityManager()
|
|
38
|
+
|
|
39
|
+
// Spawn networked entities
|
|
40
|
+
for (let i = 0; i < 1000; i++) {
|
|
41
|
+
em.createEntityWith(
|
|
42
|
+
Position, { x: Math.random() * 800, y: Math.random() * 600 },
|
|
43
|
+
Velocity, { vx: 1, vy: 1 },
|
|
44
|
+
Networked,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const server = createNetServer(em, registry, { port: 9001 })
|
|
49
|
+
await server.start()
|
|
50
|
+
|
|
51
|
+
// Game loop
|
|
52
|
+
setInterval(() => {
|
|
53
|
+
em.forEach([Position, Velocity], (a) => {
|
|
54
|
+
const px = a.field(Position.x), py = a.field(Position.y)
|
|
55
|
+
const vx = a.field(Velocity.vx), vy = a.field(Velocity.vy)
|
|
56
|
+
for (let i = 0; i < a.count; i++) { px[i] += vx[i]; py[i] += vy[i] }
|
|
57
|
+
})
|
|
58
|
+
server.tick() // diff → encode → broadcast
|
|
59
|
+
}, 50)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Game systems don't need to change — `tick()` diffs the raw TypedArrays against a double-buffered snapshot.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
### How it works
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
Game systems write to TypedArrays (front buffer)
|
|
70
|
+
│
|
|
71
|
+
▼
|
|
72
|
+
server.tick()
|
|
73
|
+
│
|
|
74
|
+
┌──────┴──────┐
|
|
75
|
+
│ diff front │ Compare front[i] !== back[i] per field
|
|
76
|
+
│ vs back │ Only iterates Networked archetypes
|
|
77
|
+
└──────┬──────┘
|
|
78
|
+
│
|
|
79
|
+
┌──────┴──────┐
|
|
80
|
+
│ encode │ Binary protocol: u8 wire IDs, field bitmasks
|
|
81
|
+
└──────┬──────┘
|
|
82
|
+
│
|
|
83
|
+
┌──────┴──────┐
|
|
84
|
+
│ broadcast │ WebSocket binary frames
|
|
85
|
+
└──────┬──────┘
|
|
86
|
+
│
|
|
87
|
+
┌──────┴──────┐
|
|
88
|
+
│ flush │ .set() memcpy front → back per field
|
|
89
|
+
└─────────────┘
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Each tracked archetype keeps a back-buffer copy of its TypedArrays. `flushSnapshots()` copies front→back with `.set()` per field.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Client
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
import { createEntityManager, component } from 'archetype-ecs'
|
|
100
|
+
import { createComponentRegistry, createNetClient } from 'archetype-ecs-net'
|
|
101
|
+
|
|
102
|
+
// Same components, same registration order as server
|
|
103
|
+
const Position = component('Position', { x: 'f32', y: 'f32' })
|
|
104
|
+
const Velocity = component('Velocity', { vx: 'f32', vy: 'f32' })
|
|
105
|
+
|
|
106
|
+
const registry = createComponentRegistry([
|
|
107
|
+
{ component: Position, name: 'Position' },
|
|
108
|
+
{ component: Velocity, name: 'Velocity' },
|
|
109
|
+
])
|
|
110
|
+
|
|
111
|
+
const em = createEntityManager()
|
|
112
|
+
const client = createNetClient(em, registry)
|
|
113
|
+
|
|
114
|
+
client.onConnected = () => console.log('connected')
|
|
115
|
+
client.connect('ws://localhost:9001')
|
|
116
|
+
|
|
117
|
+
// On connect: receives full state snapshot
|
|
118
|
+
// Every tick: receives binary delta, auto-applied to local EM
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The client uses the browser `WebSocket` API — no server-side dependencies needed client-side.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
### Component registry
|
|
126
|
+
|
|
127
|
+
Both server and client must register the same components in the same order. This assigns stable `u8` wire IDs (0–255) used in the binary protocol. Each component supports up to 16 fields (u16 bitmask).
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const registry = createComponentRegistry([
|
|
131
|
+
{ component: Position, name: 'Position' }, // wireId 0
|
|
132
|
+
{ component: Velocity, name: 'Velocity' }, // wireId 1
|
|
133
|
+
{ component: Health, name: 'Health' }, // wireId 2
|
|
134
|
+
])
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Field types are introspected from the component schema — `f32`, `i32`, `string`, etc. — and encoded with their native byte size on the wire.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Networked tag
|
|
142
|
+
|
|
143
|
+
Only entities with the `Networked` tag component are tracked and synced:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import { Networked } from 'archetype-ecs-net'
|
|
147
|
+
|
|
148
|
+
// This entity is synced
|
|
149
|
+
em.createEntityWith(Position, { x: 0, y: 0 }, Networked)
|
|
150
|
+
|
|
151
|
+
// This entity is local-only
|
|
152
|
+
em.createEntityWith(Position, { x: 0, y: 0 })
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Removing the `Networked` tag triggers a destroy on all clients. Adding it triggers a create.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### Binary protocol
|
|
160
|
+
|
|
161
|
+
Binary format. Field values are written with their native byte size, no JSON encoding.
|
|
162
|
+
|
|
163
|
+
**Full state** (sent on client connect):
|
|
164
|
+
```
|
|
165
|
+
[u8 0x01] [u32 registryHash] [u16 entityCount]
|
|
166
|
+
for each: [varint netId] [u8 componentCount]
|
|
167
|
+
for each: [u8 wireId] [field values in schema order]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
**Delta** (sent every tick):
|
|
171
|
+
```
|
|
172
|
+
[u8 0x02]
|
|
173
|
+
[u16 createdCount] → varint netId + full component data per entity
|
|
174
|
+
[u16 destroyedCount] → varint netIds only
|
|
175
|
+
[u16 updatedEntityCount]
|
|
176
|
+
for each: [varint netId] [u8 compCount]
|
|
177
|
+
for each: [u8 wireId] [u16 fieldMask] [changed field values]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Network IDs (`netId`) are varint-encoded (LEB128) — 1 byte for IDs < 128, 2 bytes for < 16K. Updates are grouped per entity to avoid repeating the netId for each dirty component. The wire protocol uses stable netIds instead of raw entity IDs; the client maintains a `netId → localEntityId` mapping.
|
|
181
|
+
|
|
182
|
+
Only changed fields are sent per entity — if only `Position.x` changed, `Position.y` stays off the wire.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Pluggable transport
|
|
187
|
+
|
|
188
|
+
Default transport uses `ws`. You can provide your own `ServerTransport`:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { createNetServer, createWsTransport } from 'archetype-ecs-net'
|
|
192
|
+
|
|
193
|
+
// Default
|
|
194
|
+
const server = createNetServer(em, registry, { port: 9001 })
|
|
195
|
+
|
|
196
|
+
// Custom transport
|
|
197
|
+
const server = createNetServer(em, registry, { port: 9001 }, myTransport)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
interface ServerTransport {
|
|
202
|
+
start(port: number, handlers: TransportHandlers): Promise<void>
|
|
203
|
+
stop(): Promise<void>
|
|
204
|
+
send(clientId: ClientId, data: ArrayBuffer): void
|
|
205
|
+
broadcast(data: ArrayBuffer): void
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### Example
|
|
212
|
+
|
|
213
|
+
**[RPG demo](example/)** — Full RPG with interest management, pathfinding, chunk-based visibility
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npm run build # build dist/ for browser imports
|
|
217
|
+
npx tsx example/server/main.ts # start server
|
|
218
|
+
# open example/client.html in browser (serve from project root)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Benchmarks
|
|
224
|
+
|
|
225
|
+
1M entities across 4 archetypes (Players 50k/6c, Projectiles 250k/3c, NPCs 100k/5c, Static 600k/2c), 6 registered components, 3 game systems per tick. Termux/Android (aarch64):
|
|
226
|
+
|
|
227
|
+
| Test | ms/frame | overhead | wire |
|
|
228
|
+
|---|---:|---:|---:|
|
|
229
|
+
| Raw game tick (1M, 4 archetypes) | 3.51 | baseline | |
|
|
230
|
+
| + diffAndEncode (4k networked, 1%) | 5.59 | +2.08ms | 97 KB |
|
|
231
|
+
| + diffAndEncode (40k networked, 10%) | 23.96 | +20.45ms | 995 KB |
|
|
232
|
+
|
|
233
|
+
`diffAndEncode()` diffs and encodes in a single fused pass — no intermediate allocations, varint netIds, updates grouped per entity. At 1% networked (4k entities), the total overhead is 2.1ms — well within a 60fps budget.
|
|
234
|
+
|
|
235
|
+
Run them yourself:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
npx tsx bench/net-bench.ts
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## API reference
|
|
244
|
+
|
|
245
|
+
### `createComponentRegistry(registrations)`
|
|
246
|
+
|
|
247
|
+
Create a registry mapping components to wire IDs. Must be identical on server and client.
|
|
248
|
+
|
|
249
|
+
### `Networked`
|
|
250
|
+
|
|
251
|
+
Tag component. Add to any entity that should be synced over the network.
|
|
252
|
+
|
|
253
|
+
### `createNetServer(em, registry, config, transport?)`
|
|
254
|
+
|
|
255
|
+
Create a network server that diffs and broadcasts on every `tick()`.
|
|
256
|
+
|
|
257
|
+
| Method / Property | Description |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `start()` | Start listening on configured port |
|
|
260
|
+
| `stop()` | Stop server, disconnect all clients |
|
|
261
|
+
| `tick(filter?)` | Diff → encode → send. No filter = broadcast. With filter = per-client interest |
|
|
262
|
+
| `send(clientId, data)` | Send a custom message to a specific client |
|
|
263
|
+
| `clientCount` | Number of connected clients |
|
|
264
|
+
| `entityNetIds` | `ReadonlyMap<EntityId, number>` — entity → netId mapping |
|
|
265
|
+
| `onConnect` | Callback when client connects (receives full state) |
|
|
266
|
+
| `onDisconnect` | Callback when client disconnects |
|
|
267
|
+
| `onMessage` | Callback when client sends a message |
|
|
268
|
+
|
|
269
|
+
The `filter` parameter is an `InterestFilter: (clientId) => ReadonlySet<number>` that returns the set of netIds visible to that client. Entities entering/leaving a client's interest set are sent as creates/destroys.
|
|
270
|
+
|
|
271
|
+
### `createNetClient(em, registry)`
|
|
272
|
+
|
|
273
|
+
Create a client that connects via WebSocket and auto-applies received state.
|
|
274
|
+
|
|
275
|
+
| Method / Property | Description |
|
|
276
|
+
|---|---|
|
|
277
|
+
| `connect(url)` | Connect to server |
|
|
278
|
+
| `disconnect()` | Close connection |
|
|
279
|
+
| `connected` | Whether currently connected |
|
|
280
|
+
| `onConnected` | Callback on successful connection |
|
|
281
|
+
| `onDisconnected` | Callback on disconnect |
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentRegistration, RegisteredComponent } from './types.js';
|
|
2
|
+
export interface ComponentRegistry {
|
|
3
|
+
/** All registered components in wire ID order */
|
|
4
|
+
readonly components: readonly RegisteredComponent[];
|
|
5
|
+
/** Deterministic 32-bit hash of the registry schema (names + field names + types) */
|
|
6
|
+
readonly hash: number;
|
|
7
|
+
/** Look up by wire ID */
|
|
8
|
+
byWireId(id: number): RegisteredComponent | undefined;
|
|
9
|
+
/** Look up by component symbol */
|
|
10
|
+
bySymbol(sym: symbol): RegisteredComponent | undefined;
|
|
11
|
+
/** Look up by wire name */
|
|
12
|
+
byName(name: string): RegisteredComponent | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare function createComponentRegistry(registrations: ComponentRegistration[]): ComponentRegistry;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { componentSchemas } from 'archetype-ecs';
|
|
2
|
+
// Maps TypedArray constructors back to wire type strings
|
|
3
|
+
const CTOR_TO_TYPE = new Map([
|
|
4
|
+
[Float32Array, 'f32'],
|
|
5
|
+
[Float64Array, 'f64'],
|
|
6
|
+
[Int8Array, 'i8'],
|
|
7
|
+
[Int16Array, 'i16'],
|
|
8
|
+
[Int32Array, 'i32'],
|
|
9
|
+
[Uint8Array, 'u8'],
|
|
10
|
+
[Uint16Array, 'u16'],
|
|
11
|
+
[Uint32Array, 'u32'],
|
|
12
|
+
[Array, 'string'],
|
|
13
|
+
]);
|
|
14
|
+
const TYPE_BYTE_SIZE = {
|
|
15
|
+
f32: 4, f64: 8,
|
|
16
|
+
i8: 1, i16: 2, i32: 4,
|
|
17
|
+
u8: 1, u16: 2, u32: 4,
|
|
18
|
+
string: 0, // variable length
|
|
19
|
+
};
|
|
20
|
+
/** Simple FNV-1a 32-bit hash */
|
|
21
|
+
function fnv1a(str) {
|
|
22
|
+
let h = 0x811c9dc5;
|
|
23
|
+
for (let i = 0; i < str.length; i++) {
|
|
24
|
+
h ^= str.charCodeAt(i);
|
|
25
|
+
h = Math.imul(h, 0x01000193);
|
|
26
|
+
}
|
|
27
|
+
return h >>> 0;
|
|
28
|
+
}
|
|
29
|
+
export function createComponentRegistry(registrations) {
|
|
30
|
+
if (registrations.length > 255) {
|
|
31
|
+
throw new Error(`Max 255 networked components (got ${registrations.length})`);
|
|
32
|
+
}
|
|
33
|
+
const components = [];
|
|
34
|
+
const byWireIdMap = new Map();
|
|
35
|
+
const bySymbolMap = new Map();
|
|
36
|
+
const byNameMap = new Map();
|
|
37
|
+
for (let i = 0; i < registrations.length; i++) {
|
|
38
|
+
const reg = registrations[i];
|
|
39
|
+
const sym = reg.component._sym;
|
|
40
|
+
const schema = componentSchemas.get(sym);
|
|
41
|
+
const fields = [];
|
|
42
|
+
if (schema) {
|
|
43
|
+
for (const [fieldName, Ctor] of Object.entries(schema)) {
|
|
44
|
+
const wireType = CTOR_TO_TYPE.get(Ctor);
|
|
45
|
+
if (!wireType)
|
|
46
|
+
throw new Error(`Unknown constructor for field "${fieldName}" in "${reg.name}"`);
|
|
47
|
+
fields.push({
|
|
48
|
+
name: fieldName,
|
|
49
|
+
type: wireType,
|
|
50
|
+
byteSize: TYPE_BYTE_SIZE[wireType],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (fields.length > 16) {
|
|
55
|
+
throw new Error(`Component "${reg.name}" has ${fields.length} fields (max 16 for u16 field bitmask)`);
|
|
56
|
+
}
|
|
57
|
+
const entry = {
|
|
58
|
+
wireId: i,
|
|
59
|
+
name: reg.name,
|
|
60
|
+
component: reg.component,
|
|
61
|
+
fields,
|
|
62
|
+
};
|
|
63
|
+
components.push(entry);
|
|
64
|
+
byWireIdMap.set(i, entry);
|
|
65
|
+
bySymbolMap.set(sym, entry);
|
|
66
|
+
byNameMap.set(reg.name, entry);
|
|
67
|
+
}
|
|
68
|
+
// Validate wireIds are sequential (defensive against future refactoring)
|
|
69
|
+
for (let i = 0; i < components.length; i++) {
|
|
70
|
+
if (components[i].wireId !== i) {
|
|
71
|
+
throw new Error(`Internal error: wireId mismatch at index ${i} (expected ${i}, got ${components[i].wireId})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Build deterministic schema fingerprint: "name:field1:type1,field2:type2;..."
|
|
75
|
+
let schemaStr = '';
|
|
76
|
+
for (const c of components) {
|
|
77
|
+
schemaStr += c.name + ':';
|
|
78
|
+
for (const f of c.fields)
|
|
79
|
+
schemaStr += f.name + ':' + f.type + ',';
|
|
80
|
+
schemaStr += ';';
|
|
81
|
+
}
|
|
82
|
+
const hash = fnv1a(schemaStr);
|
|
83
|
+
return {
|
|
84
|
+
components,
|
|
85
|
+
hash,
|
|
86
|
+
byWireId: (id) => byWireIdMap.get(id),
|
|
87
|
+
bySymbol: (sym) => bySymbolMap.get(sym),
|
|
88
|
+
byName: (name) => byNameMap.get(name),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntityId, EntityManager, ComponentType } from 'archetype-ecs';
|
|
2
|
+
import type { ComponentRegistry } from './ComponentRegistry.js';
|
|
3
|
+
import { ProtocolEncoder } from './Protocol.js';
|
|
4
|
+
import type { ClientDelta } from './InterestManager.js';
|
|
5
|
+
/** Tag component — add to any entity that should be synced over the network */
|
|
6
|
+
export declare const Networked: ComponentType;
|
|
7
|
+
export interface CreatedEntry {
|
|
8
|
+
readonly netId: number;
|
|
9
|
+
readonly entityId: EntityId;
|
|
10
|
+
}
|
|
11
|
+
export interface DirtyEntry {
|
|
12
|
+
readonly netId: number;
|
|
13
|
+
readonly entityId: EntityId;
|
|
14
|
+
/** Per-wireId dirty field bitmask (copy, safe to read after compute) */
|
|
15
|
+
readonly dirtyMasks: Uint16Array;
|
|
16
|
+
}
|
|
17
|
+
export interface Changeset {
|
|
18
|
+
readonly created: readonly CreatedEntry[];
|
|
19
|
+
readonly destroyed: readonly number[];
|
|
20
|
+
readonly dirty: readonly DirtyEntry[];
|
|
21
|
+
/** Fast lookup sets for created/destroyed netIds */
|
|
22
|
+
readonly createdSet: ReadonlySet<number>;
|
|
23
|
+
readonly destroyedSet: ReadonlySet<number>;
|
|
24
|
+
}
|
|
25
|
+
export interface EntityCache {
|
|
26
|
+
/** Pre-encoded bytes for entity enters (varint netId + full component data) */
|
|
27
|
+
readonly enterSlices: ReadonlyMap<number, Uint8Array>;
|
|
28
|
+
/** Pre-encoded bytes for dirty entity updates (varint netId + dirty components) */
|
|
29
|
+
readonly updateSlices: ReadonlyMap<number, Uint8Array>;
|
|
30
|
+
}
|
|
31
|
+
export interface SnapshotDiffer {
|
|
32
|
+
/** Diff + encode in a single pass — avoids intermediate allocations (broadcast mode) */
|
|
33
|
+
diffAndEncode(encoder: ProtocolEncoder): ArrayBuffer;
|
|
34
|
+
/** Phase 1: compute what changed this tick (run once, then encode per client) */
|
|
35
|
+
computeChangeset(): Changeset;
|
|
36
|
+
/** Pre-encode all entities in a changeset into cached byte slices (1× per tick) */
|
|
37
|
+
preEncodeChangeset(encoder: ProtocolEncoder, changeset: Changeset, extraEnterNetIds: Iterable<number>): EntityCache;
|
|
38
|
+
/** Compose a per-client delta buffer from pre-encoded cache (fast memcpy path) */
|
|
39
|
+
composeFromCache(encoder: ProtocolEncoder, cache: EntityCache, clientDelta: ClientDelta): ArrayBuffer;
|
|
40
|
+
/** Flush snapshot buffers. Must call after all encoding is done when using computeChangeset(). */
|
|
41
|
+
flushSnapshots(): void;
|
|
42
|
+
/** Current mapping of entityId → netId for all tracked entities */
|
|
43
|
+
readonly entityNetIds: ReadonlyMap<EntityId, number>;
|
|
44
|
+
/** Reverse mapping of netId → entityId */
|
|
45
|
+
readonly netIdToEntity: ReadonlyMap<number, EntityId>;
|
|
46
|
+
}
|
|
47
|
+
export declare function createSnapshotDiffer(em: EntityManager, registry: ComponentRegistry): SnapshotDiffer;
|