@woosh/meep-engine 3.14.3 → 3.14.4
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/package.json +1 -1
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +733 -726
- package/src/engine/network/NetworkSession.d.ts +24 -4
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +28 -4
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +47 -6
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +184 -56
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +97 -11
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +304 -22
- package/src/engine/network/replication/Replicator.d.ts +14 -1
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +44 -3
- package/src/engine/network/sim/ActionLog.d.ts +21 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +24 -3
- package/src/engine/network/sim/SimAction.d.ts +15 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -1
- package/src/engine/network/sim/SimAction.js +15 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +51 -1
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +73 -5
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +4 -1
- package/src/engine/ui/GUIEngine.d.ts.map +1 -1
- package/src/engine/ui/GUIEngine.js +6 -0
- package/src/shade/playground/particle_system/README.md +98 -0
- package/src/shade/playground/particle_system/index.html +124 -0
- package/src/shade/playground/particle_system/main.d.ts +2 -0
- package/src/shade/playground/particle_system/main.d.ts.map +1 -0
- package/src/shade/playground/particle_system/main.js +180 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts +52 -0
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -0
- package/src/shade/playground/particle_system/particle_scene.js +552 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +122 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +199 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts +7 -0
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/main.js +564 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +23 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +409 -0
- package/src/shade/renderer/particles/cull/chunk_particle_cull.d.ts.map +1 -1
- package/src/shade/renderer/particles/cull/chunk_particle_cull.js +3 -1
- package/src/shade/renderer/particles/data/chunk_particle_emitter.js +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts +89 -0
- package/src/shade/renderer/particles/isa/InstructionStream.d.ts.map +1 -0
- package/src/shade/renderer/particles/isa/InstructionStream.js +162 -0
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts +1 -48
- package/src/shade/renderer/particles/isa/ParticleAssembler.d.ts.map +1 -1
- package/src/shade/renderer/particles/isa/ParticleAssembler.js +25 -114
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_curve_animation.js +4 -2
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts +4 -4
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_lighting.js +7 -5
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/chunk_particle_render_math.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_emit.js +3 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_finalize.js +5 -2
- package/src/shade/renderer/particles/shaders/shader_particle_render.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_render.js +9 -2
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.d.ts.map +1 -1
- package/src/shade/renderer/particles/shaders/shader_particle_simulate.js +3 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts +0 -2
- package/src/shade/renderer/particles/prototypeParticleSystem.d.ts.map +0 -1
- package/src/shade/renderer/particles/prototypeParticleSystem.js +0 -448
|
@@ -367,10 +367,30 @@ export class NetworkSession {
|
|
|
367
367
|
*/
|
|
368
368
|
private normalize_if_dirty;
|
|
369
369
|
/**
|
|
370
|
-
* Dispatch one user-defined action
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
370
|
+
* Dispatch one user-defined action, without needing a reference to the
|
|
371
|
+
* executor or knowledge of the local peer id.
|
|
372
|
+
*
|
|
373
|
+
* Called with a frame open — from an `onLocalSim` handler on a host, or an
|
|
374
|
+
* `onPredict` handler on a client — the action executes immediately, into
|
|
375
|
+
* that frame. Its provenance follows its context: on a host, actions a
|
|
376
|
+
* local-sim handler executes are the frame's DERIVED output, recomputed on
|
|
377
|
+
* every replay of the frame rather than replayed from the log. That is what
|
|
378
|
+
* a handler that sends every tick wants, and it is why such a handler does
|
|
379
|
+
* not accumulate copies under rollback.
|
|
380
|
+
*
|
|
381
|
+
* Called with NO frame open — between ticks, from a lobby event, a timer, a
|
|
382
|
+
* console command — a host queues the action via
|
|
383
|
+
* {@link ServerAuthoritativeServer#enqueue_action}: it applies once, on the
|
|
384
|
+
* next frame the server simulates, and is recorded as INPUT, so replays
|
|
385
|
+
* take it from the log. This is the path for a one-shot. Do NOT reach for a
|
|
386
|
+
* latched local-sim handler (`if (already_sent) return`) instead: the
|
|
387
|
+
* handler's output is derived, so the replay expects it to be re-emitted,
|
|
388
|
+
* and a latch that stops it re-emitting loses the action the first time a
|
|
389
|
+
* rollback crosses its frame.
|
|
390
|
+
*
|
|
391
|
+
* (A client outside a prediction frame has nowhere to put the action and
|
|
392
|
+
* still throws — client actions are re-derived from the InputRing during
|
|
393
|
+
* reconciliation, so they belong in an `onPredict` handler.)
|
|
374
394
|
*
|
|
375
395
|
* @param {SimAction} action_instance
|
|
376
396
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkSession.d.ts","sourceRoot":"","sources":["../../../../src/engine/network/NetworkSession.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;iCAsCoB,MAAM;AAhB1B;;;;;;;;;;;;;;;;;GAiBG;AACH;;;GAGG;AAiFH;;;;;;;;;;;;;;;;;;GAkBG;AACH;IAgPI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDG;IACH,0QAnDW;QACV,cAAkB,gBAAgB;QAClC,SAAa,CAAC,EAAE,MAAM,CAAC;QACvB,iBAAqB,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,CAAC;QACvC,IAAQ,CAAC,EAAE,QAAQ,GAAC,MAAM,CAAC;QAC3B,aAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,sBAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,YAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,eAAmB,CAAC,EAAE,2BAA2B,CAAC;QAClD,YAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,aAAiB,CAAC,EAAE,YAAY,CAAC;QACjC,qBAAyB,CAAC,EAAE,mBAAmB,CAAC;QAChD,sBAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,qBAAyB,CAAC,EAAE,MAAM,CAAC;QACnC,SAAa,CAAC,EAAE;YAChB,OAAa,CAAC,EAAE,OAAO,CAAC;YACxB,YAAkB,CAAC,EAAE,MAAM,CAAC;YAC5B,aAAmB,CAAC,EAAE,MAAM,CAAC;YAC7B,YAAkB,CAAC,EAAE,MAAM,CAAC;YAC5B,kBAAwB,CAAC,EAAE,MAAM,CAAC;YAClC,gBAAsB,CAAC,EAAE,MAAM,CAAC;YAChC,mBAAyB,CAAC,EAAE,OAAO,CAAC;SAC/B,CAAC;KACH,EAgJH;IAlVD;;;;;;OAMG;IACH,qBAAqB;IA0DrB;;;;;;OAMG;IACH,eAFU,IAAI,GAAC,IAAI,CAEE;IA6BrB;;;;OAIG;IACH,YAFU,MAAM,CAEU;IAE1B;;;;;OAKG;IACH,0BAFU,MAAM,CAEwB;IAExC;;;;OAIG;IACH,kBAFU,MAAM,CAEgB;IAEhC;;;;OAIG;IACH,oBAFU,MAAM,CAEkB;IAElC;;;;;;OAMG;IACH,eAFU,MAAM,CAEa;IAE7B;;;;;OAKG;IACH,6BAFU,MAAM,CAE2B;IAwGvC;;;OAGG;IACH,uCAAoC;IAEpC;;;OAGG;IACH,uCAAmC;IAEnC;;;OAGG;IACH,eAFU,kBAAkB,GAAC,MAAM,CAEnB;IAEhB;;;OAGG;IACH,yBAFU,MAAM,CAEyB;IAEzC;;;OAGG;IACH,iCAFU,MAAM,CAIT;IAEP;;;;;;OAMG;IACH,yBAFU,MAAM,CAEoB;IAEpC;;;OAGG;IACH,wBAFU,MAAM,CAIgC;IAEhD;;;OAGG;IACH,0BAFU,2BAA2B,CAEsC;IA0C/E;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,mDAFW,0BAA0B,QA4BpC;IAED;;;;;;;;OAQG;IACH,6CAIC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,uCAFmB,MAAM,KAAK,MAAM,SAAS,CAAC,GAAC,IAAI,QAWlD;IAMD;;;;;;;;;OASG;IACH,uBAyMC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,wBAHW,MAAM,aACN,MAAM,QA8DhB;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,mBAHW,MAAM,WACN,MAAM,QA8BhB;IAED;;;;;;OAMG;IACH,oBAFW,MAAM,QAkChB;IAED;;OAEG;IACH,aAcC;IAMD;;;;;;OAMG;IACH,iBAFW,MAAM,QAyChB;IAED;;;;;;;;;;OAUG;IACH,2BA8BC;IAED
|
|
1
|
+
{"version":3,"file":"NetworkSession.d.ts","sourceRoot":"","sources":["../../../../src/engine/network/NetworkSession.js"],"names":[],"mappings":"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"}
|
|
@@ -1151,10 +1151,30 @@ export class NetworkSession {
|
|
|
1151
1151
|
}
|
|
1152
1152
|
|
|
1153
1153
|
/**
|
|
1154
|
-
* Dispatch one user-defined action
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1154
|
+
* Dispatch one user-defined action, without needing a reference to the
|
|
1155
|
+
* executor or knowledge of the local peer id.
|
|
1156
|
+
*
|
|
1157
|
+
* Called with a frame open — from an `onLocalSim` handler on a host, or an
|
|
1158
|
+
* `onPredict` handler on a client — the action executes immediately, into
|
|
1159
|
+
* that frame. Its provenance follows its context: on a host, actions a
|
|
1160
|
+
* local-sim handler executes are the frame's DERIVED output, recomputed on
|
|
1161
|
+
* every replay of the frame rather than replayed from the log. That is what
|
|
1162
|
+
* a handler that sends every tick wants, and it is why such a handler does
|
|
1163
|
+
* not accumulate copies under rollback.
|
|
1164
|
+
*
|
|
1165
|
+
* Called with NO frame open — between ticks, from a lobby event, a timer, a
|
|
1166
|
+
* console command — a host queues the action via
|
|
1167
|
+
* {@link ServerAuthoritativeServer#enqueue_action}: it applies once, on the
|
|
1168
|
+
* next frame the server simulates, and is recorded as INPUT, so replays
|
|
1169
|
+
* take it from the log. This is the path for a one-shot. Do NOT reach for a
|
|
1170
|
+
* latched local-sim handler (`if (already_sent) return`) instead: the
|
|
1171
|
+
* handler's output is derived, so the replay expects it to be re-emitted,
|
|
1172
|
+
* and a latch that stops it re-emitting loses the action the first time a
|
|
1173
|
+
* rollback crosses its frame.
|
|
1174
|
+
*
|
|
1175
|
+
* (A client outside a prediction frame has nowhere to put the action and
|
|
1176
|
+
* still throws — client actions are re-derived from the InputRing during
|
|
1177
|
+
* reconciliation, so they belong in an `onPredict` handler.)
|
|
1158
1178
|
*
|
|
1159
1179
|
* @param {SimAction} action_instance
|
|
1160
1180
|
*/
|
|
@@ -1163,6 +1183,10 @@ export class NetworkSession {
|
|
|
1163
1183
|
if (!action_instance || action_instance.isSimAction !== true) {
|
|
1164
1184
|
throw new Error("NetworkSession.send: argument must be a SimAction instance");
|
|
1165
1185
|
}
|
|
1186
|
+
if (this.#server !== null && !this.#peer.action_log.has_open_frame()) {
|
|
1187
|
+
this.#server.enqueue_action(action_instance);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1166
1190
|
this.#peer.executor.execute(action_instance, this.local_peer_id);
|
|
1167
1191
|
}
|
|
1168
1192
|
|
|
@@ -50,14 +50,29 @@ export const ResumeRejectReason: Readonly<{
|
|
|
50
50
|
*/
|
|
51
51
|
NoSessionToken: 5;
|
|
52
52
|
}>;
|
|
53
|
+
/**
|
|
54
|
+
* One outbound action-stream send, awaiting confirmation.
|
|
55
|
+
*
|
|
56
|
+
* A send occupies one transport seq when it fits the MTU and N consecutive
|
|
57
|
+
* seqs when it has to be fragmented. All of those seqs map to the SAME record:
|
|
58
|
+
* the receiver reassembles a fragmented message all-or-nothing, so the frame
|
|
59
|
+
* range is confirmed only when `unacked` reaches zero without `abandoned`
|
|
60
|
+
* having been set by a loss along the way.
|
|
61
|
+
*
|
|
62
|
+
* @typedef {{frame_end: number, unacked: number, abandoned: boolean}} PendingAck
|
|
63
|
+
* @property {number} frame_end last frame in the packed range `[start, frame_end]`
|
|
64
|
+
* @property {number} unacked seqs of this send not yet acked or declared lost
|
|
65
|
+
* @property {boolean} abandoned at least one seq was declared lost, so the
|
|
66
|
+
* message is forfeit and must never advance the baseline
|
|
67
|
+
*/
|
|
53
68
|
/**
|
|
54
69
|
* Per-tick orchestrator that wires the action infrastructure to one or more
|
|
55
70
|
* peers over a Transport.
|
|
56
71
|
*
|
|
57
72
|
* Holds a single world (`EntityComponentDataset`) plus the action machinery
|
|
58
73
|
* (executor, replicator, action log). For each connected peer it owns a
|
|
59
|
-
* `Channel` and a `seq →
|
|
60
|
-
* `Baseline.last_acked(peer_id)` watermark.
|
|
74
|
+
* `Channel` and a `seq` → {@link PendingAck} map so that ack notifications
|
|
75
|
+
* advance the `Baseline.last_acked(peer_id)` watermark.
|
|
61
76
|
*
|
|
62
77
|
* Use as both server (many connected peers) and client (one peer = the server).
|
|
63
78
|
* The asymmetry is in *who* connects — the orchestrator itself is symmetric.
|
|
@@ -262,6 +277,17 @@ export class NetworkPeer {
|
|
|
262
277
|
* @returns {number}
|
|
263
278
|
*/
|
|
264
279
|
inbound_count(peer_id: number): number;
|
|
280
|
+
/**
|
|
281
|
+
* Number of outbound action-stream seqs for `peer_id` still awaiting an
|
|
282
|
+
* ack-or-lost verdict. Returns 0 for an unknown peer.
|
|
283
|
+
*
|
|
284
|
+
* Diagnostics: this is bounded by the channel's in-flight window and must
|
|
285
|
+
* fall back to 0 once a stream of acks has resolved everything sent. A
|
|
286
|
+
* value that only grows is a leak in the ack accounting.
|
|
287
|
+
* @param {number} peer_id
|
|
288
|
+
* @returns {number}
|
|
289
|
+
*/
|
|
290
|
+
pending_ack_count(peer_id: number): number;
|
|
265
291
|
/**
|
|
266
292
|
* Direct access to a peer's channel (for sending acks-only packets, raw payloads, etc.).
|
|
267
293
|
* @param {number} peer_id
|
|
@@ -280,8 +306,8 @@ export class NetworkPeer {
|
|
|
280
306
|
/**
|
|
281
307
|
* Close the current tick and send pending action records to every peer.
|
|
282
308
|
* For each peer, packs frames `[Baseline.last_acked(peer) + 1, current_frame]`
|
|
283
|
-
* and sends via the channel; records the
|
|
284
|
-
*
|
|
309
|
+
* and sends via the channel; records the seqs the send went out under so
|
|
310
|
+
* that acks for all of them advance the baseline.
|
|
285
311
|
*
|
|
286
312
|
* If a {@link MutationLedger} was supplied at construction, this tick's
|
|
287
313
|
* accumulated changed-entity set is also compacted into the ledger.
|
|
@@ -299,8 +325,9 @@ export class NetworkPeer {
|
|
|
299
325
|
* by the caller. Records the tick's accumulated mutations into the
|
|
300
326
|
* MutationLedger (if configured), then packs frames
|
|
301
327
|
* `[last_acked+1, current_frame]` for each peer via
|
|
302
|
-
* `Replicator.pack_for_peer
|
|
303
|
-
* not sent. Clears the per-tick changed-entity
|
|
328
|
+
* `Replicator.pack_for_peer`, bounded to one channel packet. Empty packs
|
|
329
|
+
* (no in-scope actions) are not sent. Clears the per-tick changed-entity
|
|
330
|
+
* set after recording.
|
|
304
331
|
*
|
|
305
332
|
* @param {number} current_frame
|
|
306
333
|
*/
|
|
@@ -421,6 +448,20 @@ export class NetworkPeer {
|
|
|
421
448
|
send_reliable_command(peer_id: number, payload: Uint8Array, length: number): number;
|
|
422
449
|
#private;
|
|
423
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* One outbound action-stream send, awaiting confirmation.
|
|
453
|
+
*
|
|
454
|
+
* A send occupies one transport seq when it fits the MTU and N consecutive
|
|
455
|
+
* seqs when it has to be fragmented. All of those seqs map to the SAME record:
|
|
456
|
+
* the receiver reassembles a fragmented message all-or-nothing, so the frame
|
|
457
|
+
* range is confirmed only when `unacked` reaches zero without `abandoned`
|
|
458
|
+
* having been set by a loss along the way.
|
|
459
|
+
*/
|
|
460
|
+
export type PendingAck = {
|
|
461
|
+
frame_end: number;
|
|
462
|
+
unacked: number;
|
|
463
|
+
abandoned: boolean;
|
|
464
|
+
};
|
|
424
465
|
import { ReplicatedComponentRegistry } from "../sim/ReplicatedComponentRegistry.js";
|
|
425
466
|
import { SimActionRegistry } from "../sim/SimActionRegistry.js";
|
|
426
467
|
import { ReplicationSlotTable } from "../state/ReplicationSlotTable.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NetworkPeer.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/orchestrator/NetworkPeer.js"],"names":[],"mappings":";;;;;gCAmCU,MAAM;;;;;;;;;;;;;;;;;;;iCA6GI,MAAM;AAF1B;;;GAGG;AACH;;;;;IAKI;;;;;OAKG;;IAEH;;;;;;;;OAQG;;GAEJ;AAEH;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH;
|
|
1
|
+
{"version":3,"file":"NetworkPeer.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/orchestrator/NetworkPeer.js"],"names":[],"mappings":"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"}
|
|
@@ -7,7 +7,7 @@ import { Replicator } from "../replication/Replicator.js";
|
|
|
7
7
|
import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
|
|
8
8
|
import { ActionLog } from "../sim/ActionLog.js";
|
|
9
9
|
import { ReplicatedComponentRegistry } from "../sim/ReplicatedComponentRegistry.js";
|
|
10
|
-
import { SimActionExecutor } from "../sim/SimActionExecutor.js";
|
|
10
|
+
import { MAX_PEER_ID, SimActionExecutor } from "../sim/SimActionExecutor.js";
|
|
11
11
|
import { SimActionRegistry } from "../sim/SimActionRegistry.js";
|
|
12
12
|
import { snapshotter_emit } from "../sim/Snapshotter.js";
|
|
13
13
|
import { Baseline } from "../state/Baseline.js";
|
|
@@ -168,14 +168,30 @@ export const ResumeRejectReason = Object.freeze({
|
|
|
168
168
|
NoSessionToken: 5,
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
/**
|
|
172
|
+
* One outbound action-stream send, awaiting confirmation.
|
|
173
|
+
*
|
|
174
|
+
* A send occupies one transport seq when it fits the MTU and N consecutive
|
|
175
|
+
* seqs when it has to be fragmented. All of those seqs map to the SAME record:
|
|
176
|
+
* the receiver reassembles a fragmented message all-or-nothing, so the frame
|
|
177
|
+
* range is confirmed only when `unacked` reaches zero without `abandoned`
|
|
178
|
+
* having been set by a loss along the way.
|
|
179
|
+
*
|
|
180
|
+
* @typedef {{frame_end: number, unacked: number, abandoned: boolean}} PendingAck
|
|
181
|
+
* @property {number} frame_end last frame in the packed range `[start, frame_end]`
|
|
182
|
+
* @property {number} unacked seqs of this send not yet acked or declared lost
|
|
183
|
+
* @property {boolean} abandoned at least one seq was declared lost, so the
|
|
184
|
+
* message is forfeit and must never advance the baseline
|
|
185
|
+
*/
|
|
186
|
+
|
|
171
187
|
/**
|
|
172
188
|
* Per-tick orchestrator that wires the action infrastructure to one or more
|
|
173
189
|
* peers over a Transport.
|
|
174
190
|
*
|
|
175
191
|
* Holds a single world (`EntityComponentDataset`) plus the action machinery
|
|
176
192
|
* (executor, replicator, action log). For each connected peer it owns a
|
|
177
|
-
* `Channel` and a `seq →
|
|
178
|
-
* `Baseline.last_acked(peer_id)` watermark.
|
|
193
|
+
* `Channel` and a `seq` → {@link PendingAck} map so that ack notifications
|
|
194
|
+
* advance the `Baseline.last_acked(peer_id)` watermark.
|
|
179
195
|
*
|
|
180
196
|
* Use as both server (many connected peers) and client (one peer = the server).
|
|
181
197
|
* The asymmetry is in *who* connects — the orchestrator itself is symmetric.
|
|
@@ -222,6 +238,16 @@ export class NetworkPeer {
|
|
|
222
238
|
*/
|
|
223
239
|
#malformed_packets_dropped = 0;
|
|
224
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Scratch list of the transport seqs one send produced — one entry for a
|
|
243
|
+
* single-packet send, one per fragment for a fragmented one. Reused across
|
|
244
|
+
* sends so the common path allocates nothing; only valid until the next
|
|
245
|
+
* `__send_to_peer` call.
|
|
246
|
+
* @type {number[]}
|
|
247
|
+
* @private
|
|
248
|
+
*/
|
|
249
|
+
#sent_seqs = [];
|
|
250
|
+
|
|
225
251
|
/**
|
|
226
252
|
* @param {{
|
|
227
253
|
* world: EntityComponentDataset,
|
|
@@ -317,8 +343,8 @@ export class NetworkPeer {
|
|
|
317
343
|
this.baseline = new Baseline();
|
|
318
344
|
|
|
319
345
|
/**
|
|
320
|
-
* peer_id → { channel, transport,
|
|
321
|
-
* @type {Map<number, { channel: Channel, transport: Object,
|
|
346
|
+
* peer_id → { channel, transport, seq_to_pending, on_payload, on_acked }
|
|
347
|
+
* @type {Map<number, { channel: Channel, transport: Object, seq_to_pending: Map<number, PendingAck>, on_payload: Function, on_acked: Function }>}
|
|
322
348
|
* @private
|
|
323
349
|
*/
|
|
324
350
|
this.#peers = new Map();
|
|
@@ -463,12 +489,21 @@ export class NetworkPeer {
|
|
|
463
489
|
*/
|
|
464
490
|
connect_peer(peer_id, transport) {
|
|
465
491
|
assert.isNonNegativeInteger(peer_id, 'peer_id');
|
|
492
|
+
// The action log records a peer's id as the provenance of its actions,
|
|
493
|
+
// in one byte whose top two values are reserved for locally-authored
|
|
494
|
+
// and sim-derived records. A peer holding one of those would have its
|
|
495
|
+
// input read back as the local sim's own output and dropped by the
|
|
496
|
+
// first rollback replay of its frame, so refuse the connection rather
|
|
497
|
+
// than lose its input later. Real runtime check — asserts strip in prod.
|
|
498
|
+
if (peer_id > MAX_PEER_ID) {
|
|
499
|
+
throw new Error(`NetworkPeer.connect_peer: peer id ${peer_id} is out of range; must be in [0, ${MAX_PEER_ID}] — 254 and 255 are reserved provenance markers in the action log`);
|
|
500
|
+
}
|
|
466
501
|
if (this.#peers.has(peer_id)) {
|
|
467
502
|
throw new Error(`NetworkPeer.connect_peer: peer ${peer_id} already connected`);
|
|
468
503
|
}
|
|
469
504
|
|
|
470
505
|
const channel = new Channel({ transport });
|
|
471
|
-
const
|
|
506
|
+
const seq_to_pending = new Map();
|
|
472
507
|
const fragment_assembler = new FragmentAssembler();
|
|
473
508
|
const fragment_retention = new FragmentRetention();
|
|
474
509
|
const reliable_pipeline = new ReliableCommandPipeline({
|
|
@@ -639,22 +674,42 @@ export class NetworkPeer {
|
|
|
639
674
|
}
|
|
640
675
|
};
|
|
641
676
|
|
|
642
|
-
// Ack: advance Baseline to the
|
|
677
|
+
// Ack: advance Baseline to the record's frame_end — but only once every
|
|
678
|
+
// seq that send went out under has been acked. For a single-packet send
|
|
679
|
+
// that is the one seq. For a fragmented send it is all N fragments,
|
|
680
|
+
// because the receiver applies nothing until it has reassembled the
|
|
681
|
+
// whole message; advancing on the first fragment's ack would retire a
|
|
682
|
+
// frame range the receiver never saw, from the only mechanism that
|
|
683
|
+
// could still re-send it.
|
|
643
684
|
const on_acked = (seq) => {
|
|
644
|
-
const
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
685
|
+
const pending = seq_to_pending.get(seq);
|
|
686
|
+
if (pending === undefined) return;
|
|
687
|
+
seq_to_pending.delete(seq);
|
|
688
|
+
pending.unacked--;
|
|
689
|
+
if (pending.unacked === 0 && !pending.abandoned) {
|
|
690
|
+
this.baseline.set_acked(peer_id, pending.frame_end);
|
|
648
691
|
}
|
|
649
692
|
};
|
|
650
693
|
|
|
651
694
|
// Loss: a packet that aged out of the ack window without being acked
|
|
652
|
-
// leaves a dead seq→
|
|
653
|
-
// frame was never confirmed; back-fill
|
|
654
|
-
//
|
|
655
|
-
//
|
|
695
|
+
// leaves a dead seq→record entry. The baseline must NOT advance (the
|
|
696
|
+
// frame range was never confirmed; the back-fill re-sends it under a
|
|
697
|
+
// fresh seq), and the record is marked abandoned so that siblings
|
|
698
|
+
// acking afterwards cannot complete it — one lost fragment forfeits the
|
|
699
|
+
// whole message. The mapping is reclaimed either way, otherwise
|
|
700
|
+
// lost-packet entries accumulate across the session until the seq value
|
|
701
|
+
// is reused on wrap.
|
|
702
|
+
//
|
|
703
|
+
// Every outstanding seq is resolved by Channel as exactly one of acked
|
|
704
|
+
// or lost — via the ack bitfield, the 33-seq window age-out, or the
|
|
705
|
+
// max_in_flight eviction — so no record can be stranded by a seq that
|
|
706
|
+
// never reports.
|
|
656
707
|
const on_lost = (seq) => {
|
|
657
|
-
|
|
708
|
+
const pending = seq_to_pending.get(seq);
|
|
709
|
+
if (pending === undefined) return;
|
|
710
|
+
seq_to_pending.delete(seq);
|
|
711
|
+
pending.unacked--;
|
|
712
|
+
pending.abandoned = true;
|
|
658
713
|
};
|
|
659
714
|
|
|
660
715
|
// Untrusted-input boundary. A structurally-malformed or truncated
|
|
@@ -688,7 +743,7 @@ export class NetworkPeer {
|
|
|
688
743
|
channel.onPacketLost.add(on_lost);
|
|
689
744
|
|
|
690
745
|
this.#peers.set(peer_id, {
|
|
691
|
-
channel, transport,
|
|
746
|
+
channel, transport, seq_to_pending,
|
|
692
747
|
on_payload: payload_handler, on_acked, on_lost, on_nack,
|
|
693
748
|
fragment_assembler, fragment_retention,
|
|
694
749
|
reliable_pipeline,
|
|
@@ -764,6 +819,21 @@ export class NetworkPeer {
|
|
|
764
819
|
return conn === undefined ? 0 : conn.inbound_count;
|
|
765
820
|
}
|
|
766
821
|
|
|
822
|
+
/**
|
|
823
|
+
* Number of outbound action-stream seqs for `peer_id` still awaiting an
|
|
824
|
+
* ack-or-lost verdict. Returns 0 for an unknown peer.
|
|
825
|
+
*
|
|
826
|
+
* Diagnostics: this is bounded by the channel's in-flight window and must
|
|
827
|
+
* fall back to 0 once a stream of acks has resolved everything sent. A
|
|
828
|
+
* value that only grows is a leak in the ack accounting.
|
|
829
|
+
* @param {number} peer_id
|
|
830
|
+
* @returns {number}
|
|
831
|
+
*/
|
|
832
|
+
pending_ack_count(peer_id) {
|
|
833
|
+
const conn = this.#peers.get(peer_id);
|
|
834
|
+
return conn === undefined ? 0 : conn.seq_to_pending.size;
|
|
835
|
+
}
|
|
836
|
+
|
|
767
837
|
/**
|
|
768
838
|
* Direct access to a peer's channel (for sending acks-only packets, raw payloads, etc.).
|
|
769
839
|
* @param {number} peer_id
|
|
@@ -797,8 +867,8 @@ export class NetworkPeer {
|
|
|
797
867
|
/**
|
|
798
868
|
* Close the current tick and send pending action records to every peer.
|
|
799
869
|
* For each peer, packs frames `[Baseline.last_acked(peer) + 1, current_frame]`
|
|
800
|
-
* and sends via the channel; records the
|
|
801
|
-
*
|
|
870
|
+
* and sends via the channel; records the seqs the send went out under so
|
|
871
|
+
* that acks for all of them advance the baseline.
|
|
802
872
|
*
|
|
803
873
|
* If a {@link MutationLedger} was supplied at construction, this tick's
|
|
804
874
|
* accumulated changed-entity set is also compacted into the ledger.
|
|
@@ -835,8 +905,9 @@ export class NetworkPeer {
|
|
|
835
905
|
* by the caller. Records the tick's accumulated mutations into the
|
|
836
906
|
* MutationLedger (if configured), then packs frames
|
|
837
907
|
* `[last_acked+1, current_frame]` for each peer via
|
|
838
|
-
* `Replicator.pack_for_peer
|
|
839
|
-
* not sent. Clears the per-tick changed-entity
|
|
908
|
+
* `Replicator.pack_for_peer`, bounded to one channel packet. Empty packs
|
|
909
|
+
* (no in-scope actions) are not sent. Clears the per-tick changed-entity
|
|
910
|
+
* set after recording.
|
|
840
911
|
*
|
|
841
912
|
* @param {number} current_frame
|
|
842
913
|
*/
|
|
@@ -853,15 +924,14 @@ export class NetworkPeer {
|
|
|
853
924
|
|
|
854
925
|
for (const [peer_id, conn] of this.#peers) {
|
|
855
926
|
const last_acked = this.baseline.last_acked(peer_id);
|
|
856
|
-
// Pack-range start
|
|
927
|
+
// Pack-range start: whichever is newer of "catch up since you
|
|
928
|
+
// last confirmed receipt" and "the oldest frame the log still
|
|
929
|
+
// holds."
|
|
857
930
|
//
|
|
858
|
-
//
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
// For freshly-connected peers (no ack yet), we cover the
|
|
863
|
-
// action_log's full ring: max(0, current_frame -
|
|
864
|
-
// frame_capacity + 1). Two competing constraints:
|
|
931
|
+
// last_acked+1 is the ordinary catch-up point. A freshly-
|
|
932
|
+
// connected peer has no ack, so its -1+1 falls through to the
|
|
933
|
+
// ring floor and it covers the log's full content. Two competing
|
|
934
|
+
// constraints shaped that floor:
|
|
865
935
|
//
|
|
866
936
|
// - Don't pack `current_frame` only: under any non-zero
|
|
867
937
|
// latency, A's actions tagged at frame X land on the
|
|
@@ -877,28 +947,81 @@ export class NetworkPeer {
|
|
|
877
947
|
//
|
|
878
948
|
// `current_frame - frame_capacity + 1` is the oldest frame
|
|
879
949
|
// the ring could still hold, so we naturally cap at the
|
|
880
|
-
// ring's actual content.
|
|
881
|
-
//
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
950
|
+
// ring's actual content.
|
|
951
|
+
//
|
|
952
|
+
// The floor binds acked peers too, not just fresh ones: a peer
|
|
953
|
+
// that acked once and then stopped leaves last_acked pinned while
|
|
954
|
+
// current_frame runs on, and without the floor the loop walks
|
|
955
|
+
// every frame since that ack to emit at most a ring's worth —
|
|
956
|
+
// work that grows without bound the longer the peer stays quiet.
|
|
957
|
+
// Skipping those frames costs nothing, because they are already
|
|
958
|
+
// gone from the log and can never be sent whatever the ack says;
|
|
959
|
+
// `pack_for_peer` credits them as covered the moment it reaches
|
|
960
|
+
// them anyway, so the frame this pack confirms is the same
|
|
961
|
+
// either way.
|
|
962
|
+
//
|
|
963
|
+
// Actions referencing entities the peer's slot_table doesn't yet
|
|
964
|
+
// know about silently no-op (Replicator translates via
|
|
965
|
+
// network_for → -1; SimAction apply hits the `if (local < 0)
|
|
966
|
+
// return` guard). For first-connect scenarios the app is still
|
|
967
|
+
// expected to send a state-burst seed; this just makes the
|
|
968
|
+
// action-stream bootstrap work in addition to that.
|
|
969
|
+
const start = Math.max(
|
|
970
|
+
last_acked + 1,
|
|
971
|
+
current_frame - this.action_log.frame_capacity + 1,
|
|
972
|
+
0,
|
|
973
|
+
);
|
|
890
974
|
if (start > current_frame) continue;
|
|
891
975
|
|
|
892
976
|
this.#send_buffer.position = 0;
|
|
893
977
|
// Packet-type prefix.
|
|
894
978
|
this.#send_buffer.writeUint8(NetworkPacketType.ACTION_STREAM);
|
|
895
|
-
|
|
979
|
+
// Bound the pack to a single channel packet.
|
|
980
|
+
//
|
|
981
|
+
// A fragmented action stream is confirmed only if EVERY fragment
|
|
982
|
+
// survives, so letting the pack grow with the owed range is
|
|
983
|
+
// self-defeating under loss: one lost fragment pins the baseline,
|
|
984
|
+
// which widens the next pack, which raises the fragment count N,
|
|
985
|
+
// which drops P(all arrive) = (1-p)^N geometrically — the range
|
|
986
|
+
// then widens faster than it drains and delivery collapses.
|
|
987
|
+
//
|
|
988
|
+
// Bounded, the back-fill degrades into "catch up by a packet each
|
|
989
|
+
// tick": every send occupies one seq, so any packet that lands
|
|
990
|
+
// advances the baseline, and a peer behind by more than one packet
|
|
991
|
+
// converges instead of spiralling. The pack is oldest-owed-first,
|
|
992
|
+
// which is what the range order already gives.
|
|
993
|
+
//
|
|
994
|
+
// The budget is what is left of one payload after the prefix
|
|
995
|
+
// above, read from the buffer rather than written as a constant:
|
|
996
|
+
// `pack_for_peer` measures its budget from the current position,
|
|
997
|
+
// so a hard-coded allowance would have to be edited in step with
|
|
998
|
+
// every byte added here, and getting that wrong puts each full
|
|
999
|
+
// pack one byte over the limit and quietly fragments it again.
|
|
1000
|
+
const packed_through = this.replicator.pack_for_peer(
|
|
1001
|
+
peer_id, start, current_frame, this.#send_buffer,
|
|
1002
|
+
MAX_CHANNEL_PAYLOAD_BYTES - this.#send_buffer.position,
|
|
1003
|
+
);
|
|
896
1004
|
const length = this.#send_buffer.position;
|
|
897
1005
|
// Just the prefix → no in-scope actions to send.
|
|
898
1006
|
if (length === 1) continue;
|
|
899
1007
|
|
|
900
|
-
|
|
901
|
-
|
|
1008
|
+
this.#send_to_peer(conn, this.#send_buffer.raw_bytes, length, this.#sent_seqs);
|
|
1009
|
+
|
|
1010
|
+
// One record shared by every seq the send produced. Frames
|
|
1011
|
+
// [start, packed_through] are confirmed only when all of those seqs
|
|
1012
|
+
// ack; a loss on any of them abandons the record and leaves the
|
|
1013
|
+
// range owed, so the next tick's pack picks it up again.
|
|
1014
|
+
//
|
|
1015
|
+
// frame_end is what the pack actually held, NOT current_frame: the
|
|
1016
|
+
// budget may have stopped it short, and crediting frames that were
|
|
1017
|
+
// left out of the packet would retire them from the back-fill
|
|
1018
|
+
// unsent — the same defect as crediting a whole message to its
|
|
1019
|
+
// first fragment, one layer up.
|
|
1020
|
+
const seq_count = this.#sent_seqs.length;
|
|
1021
|
+
const pending = { frame_end: packed_through, unacked: seq_count, abandoned: false };
|
|
1022
|
+
for (let i = 0; i < seq_count; i++) {
|
|
1023
|
+
conn.seq_to_pending.set(this.#sent_seqs[i], pending);
|
|
1024
|
+
}
|
|
902
1025
|
}
|
|
903
1026
|
|
|
904
1027
|
// Reset the per-tick changed-entity set so the next tick starts clean.
|
|
@@ -910,24 +1033,31 @@ export class NetworkPeer {
|
|
|
910
1033
|
* Send a logical payload to a connected peer, fragmenting transparently
|
|
911
1034
|
* if it exceeds the channel's single-packet capacity.
|
|
912
1035
|
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
916
|
-
*
|
|
917
|
-
*
|
|
918
|
-
*
|
|
919
|
-
* back-fill
|
|
920
|
-
*
|
|
1036
|
+
* Fragments either all arrive (and the receiver reassembles) or the
|
|
1037
|
+
* message is lost — there is no partial delivery, and the receiver never
|
|
1038
|
+
* applies a message it could not reassemble.
|
|
1039
|
+
*
|
|
1040
|
+
* A caller that ties delivery state to the ack must therefore wait on
|
|
1041
|
+
* EVERY seq the send produced, not just the first. The action stream's
|
|
1042
|
+
* back-fill does recover a lost message, but only for as long as the
|
|
1043
|
+
* frames stay owed: crediting the range to the first fragment's ack
|
|
1044
|
+
* retires it from the back-fill, and a message whose later fragments were
|
|
1045
|
+
* dropped is then never re-sent and never applied. Pass `seqs_out` to
|
|
1046
|
+
* collect the seqs; {@link NetworkPeer#flush_outbound} does exactly that.
|
|
921
1047
|
*
|
|
922
1048
|
* @param {object} conn entry from __peers
|
|
923
1049
|
* @param {Uint8Array} payload starts with the packet-type byte
|
|
924
1050
|
* @param {number} length
|
|
925
|
-
* @
|
|
1051
|
+
* @param {number[]} [seqs_out] when given, cleared and then filled with the
|
|
1052
|
+
* seq of every transport packet emitted, in send order
|
|
926
1053
|
* @private
|
|
927
1054
|
*/
|
|
928
|
-
#send_to_peer(conn, payload, length) {
|
|
1055
|
+
#send_to_peer(conn, payload, length, seqs_out = null) {
|
|
1056
|
+
if (seqs_out !== null) seqs_out.length = 0;
|
|
929
1057
|
if (length <= MAX_CHANNEL_PAYLOAD_BYTES) {
|
|
930
|
-
|
|
1058
|
+
const seq = conn.channel.send(payload, length);
|
|
1059
|
+
if (seqs_out !== null) seqs_out.push(seq);
|
|
1060
|
+
return;
|
|
931
1061
|
}
|
|
932
1062
|
const message_id = this.#next_fragment_message_id;
|
|
933
1063
|
this.#next_fragment_message_id = seq16_advance(this.#next_fragment_message_id);
|
|
@@ -935,16 +1065,14 @@ export class NetworkPeer {
|
|
|
935
1065
|
// Copy happens inside retain(); `payload` is the orchestrator's
|
|
936
1066
|
// shared send buffer and will be overwritten by the next call.
|
|
937
1067
|
conn.fragment_retention.retain(message_id, payload, length, performance.now());
|
|
938
|
-
let first_seq = -1;
|
|
939
1068
|
send_fragmented(
|
|
940
1069
|
payload, length, message_id,
|
|
941
1070
|
NetworkPacketType.FRAGMENT, this.#fragment_scratch,
|
|
942
1071
|
(chunk_bytes, chunk_length) => {
|
|
943
1072
|
const seq = conn.channel.send(chunk_bytes, chunk_length);
|
|
944
|
-
if (
|
|
1073
|
+
if (seqs_out !== null) seqs_out.push(seq);
|
|
945
1074
|
},
|
|
946
1075
|
);
|
|
947
|
-
return first_seq;
|
|
948
1076
|
}
|
|
949
1077
|
|
|
950
1078
|
/**
|