archetype-ecs-net 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +331 -287
- package/dist/ComponentRegistry.d.ts +3 -1
- package/dist/ComponentRegistry.js +7 -1
- package/dist/NetClient.d.ts +11 -1
- package/dist/NetClient.js +144 -2
- package/dist/NetServer.d.ts +12 -2
- package/dist/NetServer.js +46 -2
- package/dist/Protocol.d.ts +1 -0
- package/dist/Protocol.js +34 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/types.d.ts +14 -1
- package/dist/types.js +2 -0
- package/package.json +45 -43
- package/src/ComponentRegistry.ts +122 -115
- package/src/DirtyTracker.ts +295 -8
- package/src/InterestManager.ts +29 -8
- package/src/NetClient.ts +521 -156
- package/src/NetServer.ts +369 -240
- package/src/Protocol.ts +502 -410
- package/src/index.ts +21 -18
- package/src/types.ts +68 -55
package/README.md
CHANGED
|
@@ -1,287 +1,331 @@
|
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
client.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
```ts
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
|
258
|
-
|---|---|
|
|
259
|
-
| `
|
|
260
|
-
| `
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
|
265
|
-
|
|
266
|
-
| `
|
|
267
|
-
| `
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
|
280
|
-
|
|
281
|
-
| `
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
+
## Shared
|
|
24
|
+
|
|
25
|
+
Both server and client register the same components in the same order. This assigns stable wire IDs used in the binary protocol.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { component } from 'archetype-ecs'
|
|
29
|
+
import { createComponentRegistry } from 'archetype-ecs-net'
|
|
30
|
+
|
|
31
|
+
const Position = component('Pos', 'f32', ['x', 'y'])
|
|
32
|
+
const Velocity = component('Vel', 'f32', ['vx', 'vy'])
|
|
33
|
+
const Attacking = component('Atk', 'i16', ['targetX', 'targetY'])
|
|
34
|
+
|
|
35
|
+
const registry = createComponentRegistry([
|
|
36
|
+
{ component: Position, name: 'Position', clientOwned: true }, // wireId 0
|
|
37
|
+
{ component: Velocity, name: 'Velocity' }, // wireId 1
|
|
38
|
+
{ component: Attacking, name: 'Attacking', clientOwned: true }, // wireId 2
|
|
39
|
+
])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Add the `Networked` tag to any entity that should sync. Entities without it stay local. Mark components as `clientOwned` to let clients write to them — field changes, attaches, and detaches auto-sync back to the server.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
import { Networked } from 'archetype-ecs-net'
|
|
46
|
+
|
|
47
|
+
em.createEntityWith(Position, { x: 0, y: 0 }, Networked) // synced
|
|
48
|
+
em.createEntityWith(Position, { x: 0, y: 0 }) // local-only
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Server
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { createEntityManager } from 'archetype-ecs'
|
|
57
|
+
import { createNetServer, Networked } from 'archetype-ecs-net'
|
|
58
|
+
|
|
59
|
+
const em = createEntityManager()
|
|
60
|
+
|
|
61
|
+
for (let i = 0; i < 1000; i++) {
|
|
62
|
+
em.createEntityWith(
|
|
63
|
+
Position, { x: Math.random() * 800, y: Math.random() * 600 },
|
|
64
|
+
Velocity, { vx: 1, vy: 1 },
|
|
65
|
+
Networked,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = createNetServer(em, registry, { port: 9001 })
|
|
70
|
+
server.onConnect = (clientId) => { /* spawn player, etc. */ }
|
|
71
|
+
await server.start()
|
|
72
|
+
|
|
73
|
+
setInterval(() => {
|
|
74
|
+
// game systems — write to ECS as usual
|
|
75
|
+
em.forEach([Position, Velocity], (a) => {
|
|
76
|
+
const px = a.field(Position.x), py = a.field(Position.y)
|
|
77
|
+
const vx = a.field(Velocity.vx), vy = a.field(Velocity.vy)
|
|
78
|
+
for (let i = 0; i < a.count; i++) { px[i] += vx[i]; py[i] += vy[i] }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
server.tick() // diff → encode → broadcast
|
|
82
|
+
}, 50)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Game systems don't need to change — `tick()` diffs the raw TypedArrays against a double-buffered snapshot.
|
|
86
|
+
|
|
87
|
+
#### How tick() works
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Game systems write to TypedArrays (front buffer)
|
|
91
|
+
│
|
|
92
|
+
▼
|
|
93
|
+
server.tick()
|
|
94
|
+
│
|
|
95
|
+
┌──────┴──────┐
|
|
96
|
+
│ diff front │ Compare front[i] !== back[i] per field
|
|
97
|
+
│ vs back │ Only iterates Networked archetypes
|
|
98
|
+
└──────┬──────┘
|
|
99
|
+
│
|
|
100
|
+
┌──────┴──────┐
|
|
101
|
+
│ encode │ Binary protocol: u8 wire IDs, field bitmasks
|
|
102
|
+
└──────┬──────┘
|
|
103
|
+
│
|
|
104
|
+
┌──────┴──────┐
|
|
105
|
+
│ broadcast │ WebSocket binary frames
|
|
106
|
+
└──────┬──────┘
|
|
107
|
+
│
|
|
108
|
+
┌──────┴──────┐
|
|
109
|
+
│ flush │ .set() memcpy front → back per field
|
|
110
|
+
└─────────────┘
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Interest management
|
|
114
|
+
|
|
115
|
+
Pass a filter to `tick()` to send per-client views. Entities entering/leaving a client's interest set are sent as creates/destroys.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
server.tick((clientId) => {
|
|
119
|
+
const visible = new Set<number>()
|
|
120
|
+
// ... add netIds of entities visible to this client
|
|
121
|
+
return visible
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Pluggable transport
|
|
126
|
+
|
|
127
|
+
Default transport uses `ws`. Provide your own `ServerTransport` to swap it out:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
const server = createNetServer(em, registry, { port: 9001 }, myTransport)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Client
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { createEntityManager } from 'archetype-ecs'
|
|
139
|
+
import { createNetClient } from 'archetype-ecs-net'
|
|
140
|
+
|
|
141
|
+
const em = createEntityManager()
|
|
142
|
+
const client = createNetClient(em, registry)
|
|
143
|
+
|
|
144
|
+
client.onConnected = () => console.log('connected')
|
|
145
|
+
client.connect('ws://localhost:9001')
|
|
146
|
+
|
|
147
|
+
// On connect: full state snapshot → entities created in local EM
|
|
148
|
+
// Every tick: binary delta → entities created/destroyed/updated automatically
|
|
149
|
+
|
|
150
|
+
// Read synced state via standard ECS queries
|
|
151
|
+
em.forEach([Position], (a) => {
|
|
152
|
+
const px = a.field(Position.x)
|
|
153
|
+
for (let i = 0; i < a.count; i++) { /* render px[i] ... */ }
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Find your own entity
|
|
157
|
+
for (const [netId, eid] of client.netToEntity) { /* ... */ }
|
|
158
|
+
|
|
159
|
+
// Write to client-owned components — synced automatically
|
|
160
|
+
em.set(myEid, Position.x, 10)
|
|
161
|
+
|
|
162
|
+
// Attach/detach client-owned components — synced as attach/detach deltas
|
|
163
|
+
em.addComponent(myEid, Attacking, { targetX: 5, targetY: 3 })
|
|
164
|
+
em.removeComponent(myEid, Attacking)
|
|
165
|
+
|
|
166
|
+
// Game loop: tick() diffs client-owned components and sends to server
|
|
167
|
+
function loop() {
|
|
168
|
+
client.tick() // diff → encode → send client-owned changes
|
|
169
|
+
render()
|
|
170
|
+
requestAnimationFrame(loop)
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The client uses the browser `WebSocket` API — works in any browser, no server-side dependencies needed.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Wire protocol
|
|
179
|
+
|
|
180
|
+
Binary format. Field values are written with their native byte size, no JSON.
|
|
181
|
+
|
|
182
|
+
**Full state** (sent on connect):
|
|
183
|
+
```
|
|
184
|
+
[u8 0x01] [u32 registryHash] [u16 entityCount]
|
|
185
|
+
for each: [varint netId] [u8 componentCount]
|
|
186
|
+
for each: [u8 wireId] [field values in schema order]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**Delta** (sent every tick):
|
|
190
|
+
```
|
|
191
|
+
[u8 0x02]
|
|
192
|
+
[u16 createdCount] → varint netId + full component data per entity
|
|
193
|
+
[u16 destroyedCount] → varint netIds only
|
|
194
|
+
[u16 updatedEntityCount]
|
|
195
|
+
for each: [varint netId] [u8 compCount]
|
|
196
|
+
for each: [u8 wireId] [u16 fieldMask] [changed field values]
|
|
197
|
+
[u16 attachedEntityCount]
|
|
198
|
+
for each: [varint netId] [u8 wireCount]
|
|
199
|
+
for each: [u8 wireId] [field values in schema order]
|
|
200
|
+
[u16 detachedEntityCount]
|
|
201
|
+
for each: [varint netId] [u8 wireCount]
|
|
202
|
+
for each: [u8 wireId]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Client delta** (sent by client for `clientOwned` components):
|
|
206
|
+
```
|
|
207
|
+
[u8 0x03]
|
|
208
|
+
[u16 updatedEntityCount]
|
|
209
|
+
for each: [varint netId] [u8 compCount]
|
|
210
|
+
for each: [u8 wireId] [u16 fieldMask] [changed field values]
|
|
211
|
+
[u16 attachedEntityCount]
|
|
212
|
+
for each: [varint netId] [u8 wireCount]
|
|
213
|
+
for each: [u8 wireId] [field values in schema order]
|
|
214
|
+
[u16 detachedEntityCount]
|
|
215
|
+
for each: [varint netId] [u8 wireCount]
|
|
216
|
+
for each: [u8 wireId]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
NetIds are varint-encoded (LEB128) — 1 byte for IDs < 128, 2 bytes for < 16K. Only changed fields are sent — if only `Position.x` changed, `Position.y` stays off the wire. Attached/detached sections track components added to or removed from existing entities — `addComponent()` and `removeComponent()` are detected automatically.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### Example
|
|
224
|
+
|
|
225
|
+
**[RPG demo](example/)** — Full multiplayer RPG with interest management, pathfinding, chunk-based visibility. Server and client share the same components and registry.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npm run dev # starts game server + vite, opens browser
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Benchmarks
|
|
234
|
+
|
|
235
|
+
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):
|
|
236
|
+
|
|
237
|
+
| Test | ms/frame | overhead | wire |
|
|
238
|
+
|---|---:|---:|---:|
|
|
239
|
+
| Raw game tick (1M, 4 archetypes) | 3.51 | baseline | |
|
|
240
|
+
| + diffAndEncode (4k networked, 1%) | 5.59 | +2.08ms | 97 KB |
|
|
241
|
+
| + diffAndEncode (40k networked, 10%) | 23.96 | +20.45ms | 995 KB |
|
|
242
|
+
|
|
243
|
+
`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.
|
|
244
|
+
|
|
245
|
+
Run them yourself:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
npx tsx bench/net-bench.ts
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## API reference
|
|
254
|
+
|
|
255
|
+
### Shared
|
|
256
|
+
|
|
257
|
+
| Export | Description |
|
|
258
|
+
|---|---|
|
|
259
|
+
| `createComponentRegistry(registrations)` | Create a registry mapping components to wire IDs. Set `clientOwned: true` per component for client→server sync. Must be identical on server and client. |
|
|
260
|
+
| `Networked` | Tag component. Add to any entity that should be synced. |
|
|
261
|
+
|
|
262
|
+
### Server — `createNetServer(em, registry, config, transport?, options?)`
|
|
263
|
+
|
|
264
|
+
| Method / Property | Description |
|
|
265
|
+
|---|---|
|
|
266
|
+
| `start()` | Start listening on configured port |
|
|
267
|
+
| `stop()` | Stop server, disconnect all clients |
|
|
268
|
+
| `tick(filter?)` | Diff → encode → send. No filter = broadcast. With filter = per-client interest |
|
|
269
|
+
| `send(clientId, data)` | Send a custom message to a specific client |
|
|
270
|
+
| `clientCount` | Number of connected clients |
|
|
271
|
+
| `entityNetIds` | `ReadonlyMap<EntityId, number>` — entity → netId mapping |
|
|
272
|
+
| `onConnect` | Callback when client connects |
|
|
273
|
+
| `onDisconnect` | Callback when client disconnects |
|
|
274
|
+
| `onMessage` | Callback when client sends a custom message |
|
|
275
|
+
| `validate(component, handlers)` | Register per-component validation. Returns `server` for chaining. See below. |
|
|
276
|
+
|
|
277
|
+
**Options:**
|
|
278
|
+
|
|
279
|
+
| Option | Description |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `ownerComponent` | `{ component, clientIdField }` — validates that client deltas only modify entities owned by the sender |
|
|
282
|
+
|
|
283
|
+
**Per-component validation** — `server.validate(component, handlers)`:
|
|
284
|
+
|
|
285
|
+
Register `delta`, `attach`, and/or `detach` handlers per component. Return `false` to reject.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
server.validate(Position, {
|
|
289
|
+
delta(clientId, entityId, data) {
|
|
290
|
+
if (!isWalkable(data.x, data.y)) return false
|
|
291
|
+
return true
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
server.validate(Attacking, {
|
|
296
|
+
attach(clientId, entityId, data) {
|
|
297
|
+
// check adjacency, cooldowns, etc.
|
|
298
|
+
return true
|
|
299
|
+
},
|
|
300
|
+
detach(clientId, entityId) {
|
|
301
|
+
return true
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Client — `createNetClient(em, registry, options?)`
|
|
307
|
+
|
|
308
|
+
| Method / Property | Description |
|
|
309
|
+
|---|---|
|
|
310
|
+
| `connect(url)` | Connect to server |
|
|
311
|
+
| `disconnect()` | Close connection |
|
|
312
|
+
| `tick()` | Diff client-owned components and send delta to server. Call once per frame. |
|
|
313
|
+
| `send(data)` | Send a binary message to the server |
|
|
314
|
+
| `connected` | Whether currently connected |
|
|
315
|
+
| `clientId` | Client ID assigned by the server on connect |
|
|
316
|
+
| `netToEntity` | `ReadonlyMap<number, EntityId>` — netId → local entity mapping |
|
|
317
|
+
| `onConnected` | Callback on successful connection |
|
|
318
|
+
| `onDisconnected` | Callback on disconnect |
|
|
319
|
+
| `onMessage` | Callback for unrecognized (custom) messages |
|
|
320
|
+
|
|
321
|
+
**Options:**
|
|
322
|
+
|
|
323
|
+
| Option | Description |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `ownerComponent` | `{ component, clientIdField }` — only diff and send entities where `clientIdField` matches this client's ID |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## License
|
|
330
|
+
|
|
331
|
+
MIT
|
|
@@ -2,8 +2,10 @@ import type { ComponentRegistration, RegisteredComponent } from './types.js';
|
|
|
2
2
|
export interface ComponentRegistry {
|
|
3
3
|
/** All registered components in wire ID order */
|
|
4
4
|
readonly components: readonly RegisteredComponent[];
|
|
5
|
-
/** Deterministic 32-bit hash of the registry schema (names + field names + types) */
|
|
5
|
+
/** Deterministic 32-bit hash of the registry schema (names + field names + types + ownership) */
|
|
6
6
|
readonly hash: number;
|
|
7
|
+
/** Wire IDs of client-owned components */
|
|
8
|
+
readonly clientOwnedWireIds: ReadonlySet<number>;
|
|
7
9
|
/** Look up by wire ID */
|
|
8
10
|
byWireId(id: number): RegisteredComponent | undefined;
|
|
9
11
|
/** Look up by component symbol */
|