@aethervtt/shard-net 0.0.0-stage → 0.3.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/dist/client.js ADDED
@@ -0,0 +1,527 @@
1
+ import {
2
+ ByteReader,
3
+ ByteWriter,
4
+ createRowCodec,
5
+ onRemove,
6
+ ShardError
7
+ } from "@aethervtt/shard-core";
8
+ import { FixedTime } from "@aethervtt/shard-runtime";
9
+ import { Connected, Disconnected, NetClock, NetId, NetRole } from "./components.js";
10
+ import {
11
+ badMessage,
12
+ describeManifest,
13
+ Fragmenter,
14
+ header,
15
+ Msg,
16
+ PROTOCOL_VERSION,
17
+ Section
18
+ } from "./wire.js";
19
+ class PendingCommand {
20
+ def;
21
+ seq;
22
+ status = "pending";
23
+ error;
24
+ constructor(def, seq) {
25
+ this.def = def;
26
+ this.seq = seq;
27
+ }
28
+ }
29
+ class InterpBuffer {
30
+ static SIZE = 8;
31
+ ticks = new Float64Array(InterpBuffer.SIZE);
32
+ values;
33
+ count = 0;
34
+ head = 0;
35
+ constructor(width) {
36
+ this.values = new Float64Array(InterpBuffer.SIZE * width);
37
+ }
38
+ }
39
+ const SAMPLE_WINDOW = 16;
40
+ const PING_MS = 500;
41
+ class NetClient {
42
+ world;
43
+ config;
44
+ byNetId = /* @__PURE__ */ new Map();
45
+ connection;
46
+ /** Why the server refused or dropped this client, if it did. */
47
+ lastError;
48
+ pending = /* @__PURE__ */ new Map();
49
+ fragmenter = new Fragmenter();
50
+ inbox = [];
51
+ applied = /* @__PURE__ */ new Map();
52
+ interp = /* @__PURE__ */ new Map();
53
+ codecs;
54
+ commandCodecs;
55
+ eventCodecs;
56
+ floatFields;
57
+ widths;
58
+ commandIndex = /* @__PURE__ */ new Map();
59
+ reader = new ByteReader();
60
+ out = new ByteWriter(1024);
61
+ latestSeq = 0;
62
+ seqBits = 0;
63
+ ackDirty = false;
64
+ lastPing = Number.NEGATIVE_INFINITY;
65
+ lastNetTick = -1;
66
+ nextCommand = 1;
67
+ rtts = [];
68
+ offsets = [];
69
+ sortScratch = [];
70
+ closedReason;
71
+ unsubscribe;
72
+ constructor(world, config) {
73
+ this.world = world;
74
+ this.config = config;
75
+ const mapping = {
76
+ toWire: (e) => this.toWire(e),
77
+ fromWire: (id) => id === 0 ? null : this.byNetId.get(id)
78
+ };
79
+ const m = config.manifest;
80
+ this.codecs = m.components.map(
81
+ (c) => createRowCodec(c.def, { quantize: c.quantize, entities: mapping })
82
+ );
83
+ this.commandCodecs = m.commands.map((c) => createRowCodec(c.schema, { entities: mapping }));
84
+ this.eventCodecs = m.events.map((e) => createRowCodec(e.schema, { entities: mapping }));
85
+ m.commands.forEach((c, i) => {
86
+ this.commandIndex.set(c.name, i);
87
+ });
88
+ this.floatFields = [];
89
+ this.widths = [];
90
+ for (const entry of m.components) {
91
+ const fields = [];
92
+ let offset = 0;
93
+ if (entry.mode === "interpolate") {
94
+ entry.def.layout.forEach((c, column) => {
95
+ if (c.storage !== "f32" && c.storage !== "f64") return;
96
+ fields.push({ column, stride: c.stride, quat: c.field.kind === "quat", offset });
97
+ offset += c.stride;
98
+ });
99
+ }
100
+ this.floatFields.push(fields);
101
+ this.widths.push(offset);
102
+ }
103
+ world.insertResource(NetClock, { serverTick: 0, offsetTicks: 0, rtt: 0, jitter: 0 });
104
+ this.unsubscribe = world.observe(onRemove(NetId), (event) => {
105
+ const table = world.entityTable(event.entity);
106
+ const id = table.column(NetId, "id")[world.entityRow(event.entity)];
107
+ if (this.byNetId.get(id) === event.entity) this.forget(id);
108
+ });
109
+ }
110
+ attach(connection) {
111
+ this.connection = connection;
112
+ connection.onMessage((channel, bytes) => this.inbox.push({ channel, bytes }));
113
+ connection.onClose((reason) => {
114
+ this.closedReason = reason?.message ?? "disconnected";
115
+ if (reason && !this.lastError) this.lastError = reason;
116
+ });
117
+ header(this.out, Msg.hello, this.world.resource(FixedTime).tick);
118
+ this.out.u16(PROTOCOL_VERSION);
119
+ this.out.u32(this.config.manifest.hash);
120
+ this.out.string(this.config.token);
121
+ this.out.string(JSON.stringify(describeManifest(this.config.manifest)));
122
+ this.fragmenter.send(connection, "reliable", this.out.finish(), this.config.maxMessage);
123
+ }
124
+ dispose() {
125
+ this.unsubscribe?.();
126
+ const connection = this.connection;
127
+ if (!connection) return;
128
+ header(this.out, Msg.disconnect, this.world.resource(FixedTime).tick);
129
+ this.out.string("left");
130
+ connection.send("reliable", this.out.finish());
131
+ connection.close("left");
132
+ }
133
+ toWire(entity) {
134
+ const world = this.world;
135
+ if (!world.isAlive(entity) || !world.has(entity, NetId)) return 0;
136
+ return world.entityTable(entity).column(NetId, "id")[world.entityRow(entity)];
137
+ }
138
+ forget(id) {
139
+ this.byNetId.delete(id);
140
+ this.applied.delete(id);
141
+ this.interp.delete(id);
142
+ }
143
+ // --- receiving -----------------------------------------------------------
144
+ receive() {
145
+ const connection = this.connection;
146
+ const world = this.world;
147
+ const fixed = world.resource(FixedTime);
148
+ const clock = world.resource(NetClock);
149
+ if (connection) {
150
+ connection.pump?.();
151
+ const inbox = this.inbox;
152
+ for (let i = 0; i < inbox.length; i++) {
153
+ try {
154
+ this.handle(inbox[i].bytes);
155
+ } catch (err) {
156
+ this.lastError = err instanceof ShardError ? err : badMessage(String(err));
157
+ connection.close(this.lastError.message);
158
+ this.closedReason = this.lastError.message;
159
+ break;
160
+ }
161
+ }
162
+ inbox.length = 0;
163
+ }
164
+ if (this.closedReason !== void 0) {
165
+ const role = world.resource(NetRole);
166
+ const error = this.lastError;
167
+ world.send(Disconnected, {
168
+ code: error?.code ?? "net/transport-closed",
169
+ message: error?.message ?? this.closedReason
170
+ });
171
+ role.connected = false;
172
+ this.connection = void 0;
173
+ this.closedReason = void 0;
174
+ }
175
+ clock.serverTick = fixed.tick + fixed.alpha + clock.offsetTicks;
176
+ }
177
+ handle(bytes) {
178
+ const r = this.reader;
179
+ r.reset(bytes);
180
+ const kind = r.u8();
181
+ const tick = r.u32();
182
+ switch (kind) {
183
+ case Msg.fragment: {
184
+ const whole = this.fragmenter.receive(r);
185
+ if (whole) this.handle(whole);
186
+ return;
187
+ }
188
+ case Msg.welcome:
189
+ this.welcome(r, tick);
190
+ return;
191
+ case Msg.refused: {
192
+ const code = r.string();
193
+ const message = r.string();
194
+ const hint = r.string();
195
+ this.lastError = new ShardError(code, message, hint ? { hint } : {});
196
+ this.closedReason = message;
197
+ return;
198
+ }
199
+ case Msg.disconnect:
200
+ this.lastError = new ShardError("net/disconnected", r.string());
201
+ this.closedReason = this.lastError.message;
202
+ return;
203
+ case Msg.pong:
204
+ this.pong(r, tick);
205
+ return;
206
+ case Msg.reliable:
207
+ this.reliable(r, tick);
208
+ return;
209
+ case Msg.state:
210
+ this.state(r, tick);
211
+ return;
212
+ default:
213
+ throw badMessage(`unknown kind ${kind}`);
214
+ }
215
+ }
216
+ welcome(r, serverTick) {
217
+ const world = this.world;
218
+ const peer = r.varuint();
219
+ r.u16();
220
+ const role = world.resource(NetRole);
221
+ role.peer = peer;
222
+ role.connected = true;
223
+ const clock = world.resource(NetClock);
224
+ clock.offsetTicks = serverTick - world.resource(FixedTime).tick;
225
+ world.send(Connected, { peer });
226
+ }
227
+ pong(r, serverTick) {
228
+ const sentAt = r.f64();
229
+ const fixed = this.world.resource(FixedTime);
230
+ const clock = this.world.resource(NetClock);
231
+ const rtt = Math.max(0, this.config.now() - sentAt);
232
+ pushSample(this.rtts, rtt);
233
+ const stepMs = fixed.step * 1e3;
234
+ pushSample(this.offsets, serverTick + rtt / 2 / stepMs - (fixed.tick + fixed.alpha));
235
+ clock.rtt = median(this.rtts, this.sortScratch);
236
+ clock.offsetTicks = median(this.offsets, this.sortScratch);
237
+ let spread = 0;
238
+ for (const v of this.rtts) spread += Math.abs(v - clock.rtt);
239
+ clock.jitter = spread / this.rtts.length;
240
+ }
241
+ stale(netId, comp, tick) {
242
+ let ticks = this.applied.get(netId);
243
+ if (!ticks) {
244
+ ticks = new Float64Array(this.codecs.length).fill(-1);
245
+ this.applied.set(netId, ticks);
246
+ }
247
+ if (tick < ticks[comp]) return true;
248
+ ticks[comp] = tick;
249
+ return false;
250
+ }
251
+ /** Decodes one component update into its entity. False if it named an unknown entity. */
252
+ apply(r, netId, comp, tick) {
253
+ const codec = this.codecs[comp];
254
+ if (!codec) throw badMessage(`component #${comp}`);
255
+ const entity = this.byNetId.get(netId);
256
+ if (entity === void 0 || !this.world.isAlive(entity)) {
257
+ codec.decode(r);
258
+ return false;
259
+ }
260
+ if (this.stale(netId, comp, tick)) {
261
+ codec.decode(r);
262
+ return true;
263
+ }
264
+ const world = this.world;
265
+ if (!world.has(entity, codec.def)) world.add(entity, codec.def);
266
+ const ok = codec.decodeRow(r, world.entityTable(entity), world.entityRow(entity));
267
+ if (this.widths[comp] > 0) this.sample(netId, comp, entity, tick);
268
+ return ok;
269
+ }
270
+ reliable(r, tick) {
271
+ const world = this.world;
272
+ for (; ; ) {
273
+ const section = r.u8();
274
+ if (section === Section.end) return;
275
+ const count = r.varuint();
276
+ for (let i = 0; i < count; i++) {
277
+ switch (section) {
278
+ case Section.spawn: {
279
+ const netId = r.varuint();
280
+ const prefab = r.string();
281
+ const old = this.byNetId.get(netId);
282
+ if (old !== void 0 && world.isAlive(old)) world.despawn(old);
283
+ const spawn = this.config.spawnPrefab;
284
+ const entity = prefab && spawn ? spawn(world, prefab) : world.spawn();
285
+ world.add(entity, NetId, { id: netId });
286
+ this.byNetId.set(netId, entity);
287
+ break;
288
+ }
289
+ case Section.set:
290
+ this.apply(r, r.varuint(), r.varuint(), tick);
291
+ break;
292
+ case Section.remove: {
293
+ const entity = this.byNetId.get(r.varuint());
294
+ const def = this.codecs[r.varuint()]?.def;
295
+ if (entity !== void 0 && def && world.isAlive(entity)) world.remove(entity, def);
296
+ break;
297
+ }
298
+ case Section.despawn: {
299
+ const netId = r.varuint();
300
+ const entity = this.byNetId.get(netId);
301
+ this.forget(netId);
302
+ if (entity !== void 0 && world.isAlive(entity)) world.despawn(entity);
303
+ break;
304
+ }
305
+ case Section.answer: {
306
+ const seq = r.varuint();
307
+ const code = r.string();
308
+ const message = r.string();
309
+ const hint = r.string();
310
+ const pending = this.pending.get(seq);
311
+ if (!pending) break;
312
+ this.pending.delete(seq);
313
+ if (code) {
314
+ pending.status = "rejected";
315
+ pending.error = new ShardError(code, message, hint ? { hint } : {});
316
+ } else pending.status = "accepted";
317
+ break;
318
+ }
319
+ case Section.event: {
320
+ const index = r.varuint();
321
+ const def = this.config.manifest.events[index];
322
+ const codec = this.eventCodecs[index];
323
+ if (!def || !codec) throw badMessage(`event #${index}`);
324
+ const data = codec.decode(r);
325
+ if (data) world.send(def.event, { data, tick });
326
+ break;
327
+ }
328
+ default:
329
+ throw badMessage(`section ${section}`);
330
+ }
331
+ }
332
+ }
333
+ }
334
+ state(r, tick) {
335
+ const seq = r.u32();
336
+ const count = r.u32();
337
+ let complete = true;
338
+ for (let i = 0; i < count; i++) {
339
+ if (!this.apply(r, r.varuint(), r.varuint(), tick)) complete = false;
340
+ }
341
+ if (complete) this.received(seq);
342
+ }
343
+ received(seq) {
344
+ if (seq > this.latestSeq) {
345
+ const shift = seq - this.latestSeq;
346
+ this.seqBits = shift > 32 ? 0 : shift === 32 ? 1 << 31 : (this.seqBits << shift | 1 << shift - 1) >>> 0;
347
+ if (this.latestSeq === 0) this.seqBits = 0;
348
+ this.latestSeq = seq;
349
+ } else if (seq < this.latestSeq) {
350
+ const diff = this.latestSeq - seq;
351
+ if (diff <= 32) this.seqBits = (this.seqBits | 1 << diff - 1) >>> 0;
352
+ }
353
+ this.ackDirty = true;
354
+ }
355
+ // --- interpolation -------------------------------------------------------
356
+ sample(netId, comp, entity, tick) {
357
+ let buffers = this.interp.get(netId);
358
+ if (!buffers) {
359
+ buffers = new Array(this.codecs.length);
360
+ this.interp.set(netId, buffers);
361
+ }
362
+ let buffer = buffers[comp];
363
+ if (!buffer) {
364
+ buffer = new InterpBuffer(this.widths[comp]);
365
+ buffers[comp] = buffer;
366
+ }
367
+ const width = this.widths[comp];
368
+ const newest = buffer.count > 0 ? buffer.ticks[(buffer.head + SIZE - 1) % SIZE] : -1;
369
+ if (tick <= newest) return;
370
+ const slot = buffer.head;
371
+ buffer.ticks[slot] = tick;
372
+ const world = this.world;
373
+ const storage = world.entityTable(entity).storage(this.codecs[comp].def);
374
+ const row = world.entityRow(entity);
375
+ for (const f of this.floatFields[comp]) {
376
+ const column = storage.columns[f.column];
377
+ for (let i = 0; i < f.stride; i++) {
378
+ buffer.values[slot * width + f.offset + i] = column[row * f.stride + i];
379
+ }
380
+ }
381
+ buffer.head = (slot + 1) % SIZE;
382
+ if (buffer.count < SIZE) buffer.count++;
383
+ }
384
+ /** Writes interpolated values for every remote entity (PreUpdate). */
385
+ interpolate() {
386
+ if (this.interp.size === 0) return;
387
+ const world = this.world;
388
+ const fixed = world.resource(FixedTime);
389
+ const clock = world.resource(NetClock);
390
+ const step = fixed.step;
391
+ const delay = Math.max(2 * this.config.ticksPerNet, 2 * clock.jitter / 1e3 / step);
392
+ this.renderTick = clock.serverTick - clock.rtt / 2 / 1e3 / step - delay;
393
+ this.interp.forEach(this.interpolateEntity);
394
+ }
395
+ renderTick = 0;
396
+ interpolateEntity = (buffers, netId) => {
397
+ const world = this.world;
398
+ const entity = this.byNetId.get(netId);
399
+ if (entity === void 0 || !world.isAlive(entity)) return;
400
+ for (let c = 0; c < buffers.length; c++) {
401
+ const buffer = buffers[c];
402
+ if (!buffer || buffer.count === 0) continue;
403
+ if (!world.has(entity, this.codecs[c].def)) continue;
404
+ this.write(buffer, c, entity, this.renderTick);
405
+ }
406
+ };
407
+ write(buffer, comp, entity, renderTick) {
408
+ const width = this.widths[comp];
409
+ const oldest = (buffer.head + SIZE - buffer.count) % SIZE;
410
+ const newestSlot = (buffer.head + SIZE - 1) % SIZE;
411
+ let a = oldest;
412
+ let b = oldest;
413
+ let t = 0;
414
+ if (renderTick <= buffer.ticks[oldest]) {
415
+ a = b = oldest;
416
+ } else if (renderTick >= buffer.ticks[newestSlot]) {
417
+ a = b = newestSlot;
418
+ } else {
419
+ for (let k = 0; k < buffer.count - 1; k++) {
420
+ const i = (oldest + k) % SIZE;
421
+ const j = (i + 1) % SIZE;
422
+ if (renderTick >= buffer.ticks[i] && renderTick <= buffer.ticks[j]) {
423
+ a = i;
424
+ b = j;
425
+ const span = buffer.ticks[j] - buffer.ticks[i];
426
+ t = span > 0 ? (renderTick - buffer.ticks[i]) / span : 0;
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ const world = this.world;
432
+ const table = world.entityTable(entity);
433
+ const storage = table.storage(this.codecs[comp].def);
434
+ const row = world.entityRow(entity);
435
+ const va = buffer.values;
436
+ for (const f of this.floatFields[comp]) {
437
+ const column = storage.columns[f.column];
438
+ const base = row * f.stride;
439
+ const oa = a * width + f.offset;
440
+ const ob = b * width + f.offset;
441
+ if (f.quat) {
442
+ const dot = va[oa] * va[ob] + va[oa + 1] * va[ob + 1] + va[oa + 2] * va[ob + 2] + va[oa + 3] * va[ob + 3];
443
+ const sign = dot < 0 ? -1 : 1;
444
+ const u = Math.min(t, 1);
445
+ let len = 0;
446
+ for (let i = 0; i < 4; i++) {
447
+ const v = va[oa + i] * (1 - u) + va[ob + i] * sign * u;
448
+ column[base + i] = v;
449
+ len += v * v;
450
+ }
451
+ len = Math.sqrt(len) || 1;
452
+ for (let i = 0; i < 4; i++) column[base + i] = column[base + i] / len;
453
+ continue;
454
+ }
455
+ for (let i = 0; i < f.stride; i++) {
456
+ column[base + i] = va[oa + i] + (va[ob + i] - va[oa + i]) * t;
457
+ }
458
+ }
459
+ table.markChanged(this.codecs[comp].def, row);
460
+ }
461
+ // --- sending -------------------------------------------------------------
462
+ /** Runs in Last: acks and pings on network ticks. */
463
+ send() {
464
+ const connection = this.connection;
465
+ const world = this.world;
466
+ if (!connection || !world.resource(NetRole).connected) return;
467
+ const fixed = world.resource(FixedTime);
468
+ const netTick = Math.floor(fixed.tick / this.config.ticksPerNet);
469
+ if (netTick === this.lastNetTick) return;
470
+ this.lastNetTick = netTick;
471
+ world.resource(NetRole).tick = Math.floor(world.resource(NetClock).serverTick);
472
+ if (this.ackDirty) {
473
+ header(this.out, Msg.ack, fixed.tick);
474
+ this.out.varuint(this.latestSeq);
475
+ this.out.u32(this.seqBits);
476
+ this.out.f64(world.resource(NetClock).rtt);
477
+ connection.send("unreliable", this.out.finish());
478
+ this.ackDirty = false;
479
+ }
480
+ const now = this.config.now();
481
+ if (now - this.lastPing >= PING_MS) {
482
+ this.lastPing = now;
483
+ header(this.out, Msg.ping, fixed.tick);
484
+ this.out.f64(now);
485
+ connection.send("unreliable", this.out.finish());
486
+ }
487
+ }
488
+ sendCommand(def, data) {
489
+ const index = this.commandIndex.get(def.name);
490
+ if (index === void 0) {
491
+ throw new ShardError("net/unknown-command", `${def.name} isn't in netPlugin({ commands })`, {
492
+ hint: "List every net command in the plugin options on both sides."
493
+ });
494
+ }
495
+ const pending = new PendingCommand(def, this.nextCommand++);
496
+ const connection = this.connection;
497
+ if (!connection || !this.world.resource(NetRole).connected) {
498
+ pending.status = "rejected";
499
+ pending.error = new ShardError("net/not-connected", `Can't send ${def.name}: not connected`);
500
+ return pending;
501
+ }
502
+ this.pending.set(pending.seq, pending);
503
+ header(this.out, Msg.command, this.world.resource(FixedTime).tick);
504
+ this.out.varuint(pending.seq);
505
+ this.out.varuint(index);
506
+ this.commandCodecs[index].encode(data, this.out);
507
+ this.fragmenter.send(connection, "reliable", this.out.finish(), this.config.maxMessage);
508
+ return pending;
509
+ }
510
+ }
511
+ const SIZE = InterpBuffer.SIZE;
512
+ function pushSample(list, v) {
513
+ list.push(v);
514
+ if (list.length > SAMPLE_WINDOW) list.shift();
515
+ }
516
+ function median(list, scratch) {
517
+ scratch.length = 0;
518
+ for (const v of list) scratch.push(v);
519
+ scratch.sort((a, b) => a - b);
520
+ const n = scratch.length;
521
+ if (n === 0) return 0;
522
+ return n % 2 === 1 ? scratch[(n - 1) / 2] : (scratch[n / 2 - 1] + scratch[n / 2]) / 2;
523
+ }
524
+ export {
525
+ NetClient,
526
+ PendingCommand
527
+ };
@@ -0,0 +1,72 @@
1
+ import type { PeerId } from './transport.js';
2
+ export declare const NetId: import("@aethervtt/shard-core").ComponentDef<{
3
+ readonly id: import("@aethervtt/shard-core").FieldType<number, "u32">;
4
+ }>;
5
+ export declare const Replicated: import("@aethervtt/shard-core").ComponentDef<{
6
+ readonly prefab: import("@aethervtt/shard-core").FieldType<string, "object">;
7
+ readonly owner: import("@aethervtt/shard-core").FieldType<number, "u32">;
8
+ }>;
9
+ export declare const Relevance: import("@aethervtt/shard-core").ComponentDef<{
10
+ readonly always: import("@aethervtt/shard-core").FieldType<boolean, "u8">;
11
+ readonly priority: import("@aethervtt/shard-core").FieldType<number, "f32">;
12
+ }>;
13
+ export declare const Focus: import("@aethervtt/shard-core").ComponentDef<{
14
+ readonly peer: import("@aethervtt/shard-core").FieldType<number, "u32">;
15
+ }>;
16
+ export type NetRoleName = 'server' | 'client';
17
+ export interface NetRoleData {
18
+ role: NetRoleName;
19
+ /** This app's peer id: 0 on the server, assigned by the server on a client. */
20
+ peer: PeerId;
21
+ /** Client: handshake done. Server: listening (or running locally with no transport). */
22
+ connected: boolean;
23
+ /** The last network tick sent (server) or received (client). */
24
+ tick: number;
25
+ }
26
+ export declare const NetRole: import("@aethervtt/shard-core").ResourceDef<NetRoleData>;
27
+ export interface NetClockData {
28
+ /** The server tick now, estimated (fractional). Equals FixedTime.tick on the server. */
29
+ serverTick: number;
30
+ /** serverTick minus this app's FixedTime.tick, smoothed. */
31
+ offsetTicks: number;
32
+ /** Round trip in ms. */
33
+ rtt: number;
34
+ /** Spread of recent round trips in ms. */
35
+ jitter: number;
36
+ }
37
+ export declare const NetClock: import("@aethervtt/shard-core").ResourceDef<NetClockData>;
38
+ export interface PeerInfo {
39
+ readonly id: PeerId;
40
+ /** `joining`: accept or the player load is still running (0090). */
41
+ state: 'handshake' | 'joining' | 'connected';
42
+ rtt: number;
43
+ /** The token the client sent with hello. */
44
+ token: string;
45
+ /** What `accept` returned as `user`: the peer's account (0090). */
46
+ user: unknown;
47
+ joinedTick: number;
48
+ }
49
+ export declare const Peers: import("@aethervtt/shard-core").ResourceDef<Map<number, PeerInfo>>;
50
+ export declare const PeerJoined: import("@aethervtt/shard-core").EventDef<{
51
+ peer: PeerId;
52
+ user: unknown;
53
+ }>;
54
+ export declare const PeerLeft: import("@aethervtt/shard-core").EventDef<{
55
+ peer: PeerId;
56
+ reason: string;
57
+ user: unknown;
58
+ }>;
59
+ /** Server: a player's `store` failed (0090). The server keeps running. */
60
+ export declare const PlayerSaveFailed: import("@aethervtt/shard-core").EventDef<{
61
+ peer: PeerId;
62
+ user: unknown;
63
+ code: string;
64
+ message: string;
65
+ }>;
66
+ export declare const Connected: import("@aethervtt/shard-core").EventDef<{
67
+ peer: PeerId;
68
+ }>;
69
+ export declare const Disconnected: import("@aethervtt/shard-core").EventDef<{
70
+ code: string;
71
+ message: string;
72
+ }>;
@@ -0,0 +1,71 @@
1
+ import { defineComponent, defineEvent, defineResource, t } from "@aethervtt/shard-core";
2
+ const NetId = defineComponent(
3
+ "net/NetId",
4
+ {
5
+ id: t.u32({
6
+ readonly: true,
7
+ description: "Session-wide id the server assigned. Never reused."
8
+ })
9
+ },
10
+ {
11
+ description: "The id an entity has on every machine in a session (0087).",
12
+ serialize: false
13
+ }
14
+ );
15
+ const Replicated = defineComponent(
16
+ "net/Replicated",
17
+ {
18
+ prefab: t.string({
19
+ description: "Prefab clients spawn this entity from (path or guid); empty for a bare entity."
20
+ }),
21
+ owner: t.u32({ description: "Peer that owns it; 0 for the server." })
22
+ },
23
+ {
24
+ description: "Marks a server entity for replication (0087). Add it on the server; clients never author it.",
25
+ serialize: false
26
+ }
27
+ );
28
+ const Relevance = defineComponent(
29
+ "net/Relevance",
30
+ {
31
+ always: t.bool({ description: "Every client receives it, whatever the interest policy says." }),
32
+ priority: t.f32({ default: 1, min: 0, description: "Weight when bandwidth is short." })
33
+ },
34
+ { description: "Pins or raises an entity in interest management (0087).", serialize: false }
35
+ );
36
+ const Focus = defineComponent(
37
+ "net/Focus",
38
+ { peer: t.u32({ description: "Peer whose interest centres here (a spectator camera)." }) },
39
+ {
40
+ description: "An extra point of interest for a peer, besides the entities it owns (0087).",
41
+ serialize: false
42
+ }
43
+ );
44
+ const NetRole = defineResource("net/NetRole", {
45
+ description: "Which side of a session this app is, and whether it is connected (0086)."
46
+ });
47
+ const NetClock = defineResource("net/NetClock", {
48
+ description: "Clock estimate against the server: tick offset, RTT and jitter (0086)."
49
+ });
50
+ const Peers = defineResource("net/Peers", {
51
+ description: "Server: connected peers by id (0086)."
52
+ });
53
+ const PeerJoined = defineEvent("net/PeerJoined");
54
+ const PeerLeft = defineEvent("net/PeerLeft");
55
+ const PlayerSaveFailed = defineEvent("net/PlayerSaveFailed");
56
+ const Connected = defineEvent("net/Connected");
57
+ const Disconnected = defineEvent("net/Disconnected");
58
+ export {
59
+ Connected,
60
+ Disconnected,
61
+ Focus,
62
+ NetClock,
63
+ NetId,
64
+ NetRole,
65
+ PeerJoined,
66
+ PeerLeft,
67
+ Peers,
68
+ PlayerSaveFailed,
69
+ Relevance,
70
+ Replicated
71
+ };
@@ -0,0 +1,29 @@
1
+ import type { NetTransport } from './transport.js';
2
+ export interface NetConditions {
3
+ /** One-way delay in seconds, each direction. RTT is twice this. */
4
+ latency: number;
5
+ /** Up to this many extra seconds per message, uniformly. */
6
+ jitter: number;
7
+ /** Probability an unreliable message is dropped, each direction. */
8
+ loss: number;
9
+ /** Probability an unreliable message arrives twice. */
10
+ duplicate: number;
11
+ /** Unreliable messages may overtake each other under jitter. Reliable ones never do. */
12
+ reorder: boolean;
13
+ }
14
+ export interface ConditionsOptions extends Partial<NetConditions> {
15
+ seed?: number;
16
+ /** The clock in ms. Tests pass the fake clock every app shares. */
17
+ now?: () => number;
18
+ }
19
+ /** A transport whose conditions can change at runtime (`net.conditions`). */
20
+ export interface ConditionedTransport extends NetTransport {
21
+ readonly conditions: NetConditions;
22
+ setConditions(conditions: Partial<NetConditions>): void;
23
+ }
24
+ export declare function isConditioned(transport: NetTransport): transport is ConditionedTransport;
25
+ /**
26
+ * Wraps any transport with seeded latency, jitter, loss and duplication (0086). Connections it
27
+ * makes or accepts deliver both directions through the delay, so one wrapped side is enough.
28
+ */
29
+ export declare function simulateConditions(transport: NetTransport, options?: ConditionsOptions): ConditionedTransport;