@woosh/meep-engine 3.14.4 → 3.14.5
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/core/geom/3d/mat4/m4_rigidity_defect.d.ts +30 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.d.ts.map +1 -0
- package/src/core/geom/3d/mat4/m4_rigidity_defect.js +71 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +56 -8
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +263 -67
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +6 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +6 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +13 -7
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +103 -138
- package/src/engine/network/replication/Replicator.d.ts +29 -6
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +265 -108
- package/src/engine/network/sim/ActionLog.d.ts +2 -0
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +2 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts +82 -0
- package/src/engine/network/sim/ActionRecordCursor.d.ts.map +1 -0
- package/src/engine/network/sim/ActionRecordCursor.js +120 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -1
- package/src/engine/network/sim/RewindEngine.js +13 -29
- package/src/engine/network/sim/SimActionExecutor.d.ts +2 -3
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +10 -11
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/shade/playground/particle_system/particle_scene.d.ts.map +1 -1
- package/src/shade/playground/particle_system/particle_scene.js +38 -37
- package/src/shade/playground/skinned_mesh_soup/README.md +39 -18
- package/src/shade/playground/skinned_mesh_soup/index.html +9 -6
- package/src/shade/playground/skinned_mesh_soup/main.d.ts.map +1 -1
- package/src/shade/playground/skinned_mesh_soup/main.js +16 -13
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +8 -2
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts.map +1 -1
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.js +23 -4
- package/src/shade/renderer/animation/GPUAnimationManager.d.ts.map +1 -1
- package/src/shade/renderer/animation/GPUAnimationManager.js +1958 -1796
- package/src/shade/renderer/animation/SKINNING_FRAME_PLAN.md +357 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts +25 -0
- package/src/shade/renderer/animation/skin_bind_transform.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_bind_transform.js +33 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts +67 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_clip_scale_conflicts.js +199 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts +34 -0
- package/src/shade/renderer/animation/skin_frame_defect.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_frame_defect.js +132 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts +24 -0
- package/src/shade/renderer/animation/skin_joint_matrix.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_joint_matrix.js +46 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts +70 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_normalize_mesh_frame.js +216 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts +86 -0
- package/src/shade/renderer/animation/skin_test_fixtures.d.ts.map +1 -0
- package/src/shade/renderer/animation/skin_test_fixtures.js +250 -0
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts +18 -4
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.d.ts.map +1 -1
- package/src/shade/renderer/animation/skinning/chunk_skin_blend_mesh_local.js +18 -4
- package/src/shade/renderer/particles/DESIGN.md +5 -5
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts +43 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/ParticleNodeDescription.js +44 -0
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts +33 -59
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/ParticleNodeRegistry.js +185 -118
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts +15 -10
- package/src/shade/renderer/particles/graph/compile_particle_graph.d.ts.map +1 -1
- package/src/shade/renderer/particles/graph/compile_particle_graph.js +205 -170
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts +44 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.d.ts.map +1 -0
- package/src/shade/renderer/particles/graph/particle_graph_authoring.js +108 -0
- package/src/shade/renderer/particles/particle_prototype.d.ts.map +1 -1
- package/src/shade/renderer/particles/particle_prototype.js +181 -180
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts +4 -4
- package/src/shade/renderer/particles/runtime/create_particle_effect.d.ts.map +1 -1
- package/src/shade/renderer/particles/runtime/create_particle_effect.js +19 -19
- package/src/shade/renderer/particles/vm/chunk_particle_vm.js +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.d.ts +11 -0
- package/src/shade/renderer/scene/SkinnedMesh.d.ts.map +1 -1
- package/src/shade/renderer/scene/SkinnedMesh.js +11 -0
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts +33 -3
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.d.ts.map +1 -1
- package/src/shade/renderer/shader/chunk/geometry/quaternion/dual/chunk_dual_quat_from_m4.js +58 -37
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts +0 -54
- package/src/shade/renderer/particles/graph/ParticleGraph.d.ts.map +0 -1
- package/src/shade/renderer/particles/graph/ParticleGraph.js +0 -99
|
@@ -8,6 +8,7 @@ import { NetworkIdentity } from "../ecs/components/NetworkIdentity.js";
|
|
|
8
8
|
import { make_owner_authorization } from "../ecs/owner_authorization.js";
|
|
9
9
|
import { Replicator } from "../replication/Replicator.js";
|
|
10
10
|
import { AlwaysRelevantScope } from "../replication/ScopeFilter.js";
|
|
11
|
+
import { ActionRecordCursor } from "../sim/ActionRecordCursor.js";
|
|
11
12
|
import { RewindEngine } from "../sim/RewindEngine.js";
|
|
12
13
|
import { SENDER_DERIVED, SENDER_LOCAL } from "../sim/SimActionExecutor.js";
|
|
13
14
|
import { NetworkPeer } from "./NetworkPeer.js";
|
|
@@ -20,14 +21,15 @@ import { NetworkPeer } from "./NetworkPeer.js";
|
|
|
20
21
|
*
|
|
21
22
|
* 1. Inbound action packets do NOT execute on arrival. Instead, a deferral
|
|
22
23
|
* hook on the {@link Replicator} drops them into a per-tick pending log
|
|
23
|
-
* tagged with `(client_frame, sender_id, type_id, payload_bytes)
|
|
24
|
+
* tagged with `(client_frame, sender_id, type_id, payload_bytes)` —
|
|
25
|
+
* unless the action log already holds that record. The action stream
|
|
26
|
+
* re-sends every unconfirmed frame each tick, so most arrivals at any
|
|
27
|
+
* real latency are copies of applied records; those are dropped where
|
|
28
|
+
* they arrive, and pending holds only input the log does not.
|
|
24
29
|
* 2. Once per server tick, `tick(current_frame)` consumes the pending log:
|
|
25
30
|
* - Rejects entries older than the action_log's rewindable window.
|
|
26
|
-
* - Determines `replay_start`: the oldest pending frame
|
|
27
|
-
*
|
|
28
|
-
* every unacked frame each tick, so most of pending is copies of
|
|
29
|
-
* already-applied records; rewinding for those would replay the
|
|
30
|
-
* world into the state it was already in.
|
|
31
|
+
* - Determines `replay_start`: the oldest pending frame, which by the
|
|
32
|
+
* above is the oldest frame whose outcome can change.
|
|
31
33
|
* - Rewinds the server world back to end-of-(replay_start - 1) via the
|
|
32
34
|
* action_log's prior-state captures (see {@link RewindEngine}).
|
|
33
35
|
* - Replays forward `[replay_start, current_frame]`. For each frame `f`:
|
|
@@ -92,6 +94,8 @@ export class ServerAuthoritativeServer {
|
|
|
92
94
|
#historical_scratch = /** @type {Array<{type_id:number, sender_id:number, payload:Uint8Array}>} */ ([]);
|
|
93
95
|
#sort_scratch = /** @type {Array<{sender_id:number, type_id:number, payload:Uint8Array}>} */ ([]);
|
|
94
96
|
#payload_buf;
|
|
97
|
+
// Walks a frame's records, for the history read and the arrival check.
|
|
98
|
+
#cursor = new ActionRecordCursor();
|
|
95
99
|
// Locally-authored actions waiting for the next fresh frame. Parallel
|
|
96
100
|
// arrays, serialized at enqueue time so the caller's instance is not
|
|
97
101
|
// retained (and so a bad action class is rejected at the call site rather
|
|
@@ -99,10 +103,6 @@ export class ServerAuthoritativeServer {
|
|
|
99
103
|
#local_type_ids = /** @type {number[]} */ ([]);
|
|
100
104
|
#local_payloads = /** @type {Uint8Array[]} */ ([]);
|
|
101
105
|
#local_buf;
|
|
102
|
-
// The frame this tick is simulating for the first time. Only it may take
|
|
103
|
-
// the locally-authored queue; every earlier frame in the replay window
|
|
104
|
-
// already holds, in its history, the actions it was given the first time.
|
|
105
|
-
#fresh_frame = -1;
|
|
106
106
|
/**
|
|
107
107
|
* Count of inbound pending entries dropped by the future-lead / total
|
|
108
108
|
* bounds. See {@link onPendingActionDropped}.
|
|
@@ -128,8 +128,12 @@ export class ServerAuthoritativeServer {
|
|
|
128
128
|
* frame_capacity?: number,
|
|
129
129
|
* initial_buffer_size?: number,
|
|
130
130
|
* simulation_delay_ticks?: number,
|
|
131
|
+
* max_packets_per_tick?: number,
|
|
131
132
|
* }} options
|
|
132
133
|
*
|
|
134
|
+
* `max_packets_per_tick`: forwarded to {@link NetworkPeer}; how many
|
|
135
|
+
* action-stream packets a tick may send one peer.
|
|
136
|
+
*
|
|
133
137
|
* `simulation_delay_ticks`: server-side input buffer (Overwatch-style).
|
|
134
138
|
* When > 0, {@link tick}(wall_frame) simulates
|
|
135
139
|
* `sim_frame = wall_frame - simulation_delay_ticks`. Pending actions
|
|
@@ -152,6 +156,7 @@ export class ServerAuthoritativeServer {
|
|
|
152
156
|
max_pending_total = 4096,
|
|
153
157
|
identity_class = NetworkIdentity,
|
|
154
158
|
server_peer_id = -1,
|
|
159
|
+
max_packets_per_tick,
|
|
155
160
|
}) {
|
|
156
161
|
assert.isNonNegativeInteger(simulation_delay_ticks, 'simulation_delay_ticks');
|
|
157
162
|
assert.isPositiveInteger(max_pending_future_lead, 'max_pending_future_lead');
|
|
@@ -165,6 +170,7 @@ export class ServerAuthoritativeServer {
|
|
|
165
170
|
scope_filter,
|
|
166
171
|
frame_capacity,
|
|
167
172
|
initial_buffer_size,
|
|
173
|
+
max_packets_per_tick,
|
|
168
174
|
});
|
|
169
175
|
|
|
170
176
|
/** @type {EntityComponentDataset} */
|
|
@@ -496,31 +502,17 @@ export class ServerAuthoritativeServer {
|
|
|
496
502
|
this.#pending_trim_to_window(window_oldest);
|
|
497
503
|
}
|
|
498
504
|
|
|
499
|
-
// 2. Pick the replay window: the OLDEST pending frame
|
|
500
|
-
// input the
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// action arrives once per tick until its ack round-trips, and the
|
|
507
|
-
// deferral hook logs every copy. Taking `refs[0]` verbatim would
|
|
508
|
-
// therefore rewind by the ACK round trip on every tick — and
|
|
509
|
-
// `__replay_frame`'s dedup would then discard the very entries
|
|
510
|
-
// that caused the rewind, leaving the world exactly as it was.
|
|
511
|
-
// So run the same comparison here, before the window is chosen:
|
|
512
|
-
// a frame whose every pending entry is already in the log needs
|
|
513
|
-
// no rewind. What's left is genuinely late input, which is what
|
|
514
|
-
// the rollback exists for.
|
|
505
|
+
// 2. Pick the replay window: the OLDEST pending frame at or below
|
|
506
|
+
// `sim_frame`. Pending holds only input the log does not already
|
|
507
|
+
// hold — a retransmission of an applied record is dropped on
|
|
508
|
+
// arrival, see __pending_add — so the oldest pending frame is the
|
|
509
|
+
// oldest frame whose outcome can change, and a rewind to it is
|
|
510
|
+
// never wasted. `__pending_referenced_frames` is sorted
|
|
511
|
+
// ascending; entries `> sim_frame` stay buffered.
|
|
515
512
|
let replay_start = sim_frame;
|
|
516
513
|
const refs = this.#pending_referenced_frames;
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
if (f > sim_frame) break;
|
|
520
|
-
if (this.#frame_has_unapplied_input(f)) {
|
|
521
|
-
replay_start = f;
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
514
|
+
if (refs.length > 0 && refs[0] <= sim_frame) {
|
|
515
|
+
replay_start = refs[0];
|
|
524
516
|
}
|
|
525
517
|
|
|
526
518
|
// 3. Rewind only if the replay window covers committed frames.
|
|
@@ -533,9 +525,8 @@ export class ServerAuthoritativeServer {
|
|
|
533
525
|
// 4. Replay [replay_start, sim_frame]. Only `sim_frame` is being
|
|
534
526
|
// simulated for the first time; the rest are re-executions of
|
|
535
527
|
// frames the log already holds.
|
|
536
|
-
this.#fresh_frame = sim_frame;
|
|
537
528
|
for (let f = replay_start; f <= sim_frame; f++) {
|
|
538
|
-
this.#replay_frame(f);
|
|
529
|
+
this.#replay_frame(f, f === sim_frame);
|
|
539
530
|
}
|
|
540
531
|
|
|
541
532
|
// 5. Keep only `frame > sim_frame` pending (future buffer).
|
|
@@ -564,48 +555,20 @@ export class ServerAuthoritativeServer {
|
|
|
564
555
|
return max - this.current_sim_frame;
|
|
565
556
|
}
|
|
566
557
|
|
|
567
|
-
/**
|
|
568
|
-
* Does frame `f` carry at least one pending entry the action log does not
|
|
569
|
-
* already hold? Only such a frame can change the world when replayed, so
|
|
570
|
-
* only such a frame is worth rewinding to.
|
|
571
|
-
*
|
|
572
|
-
* Same key as {@link __replay_frame}'s dedup — `(sender_id, type_id,
|
|
573
|
-
* payload bytes)` — asked one step earlier, against the log rather than
|
|
574
|
-
* against a scratch copy of it. The two must stay in step: a frame this
|
|
575
|
-
* call reports clean is never replayed, so an entry it treats as a
|
|
576
|
-
* duplicate that `__replay_frame` would have treated as new would be
|
|
577
|
-
* dropped from pending without ever being applied.
|
|
578
|
-
*
|
|
579
|
-
* Note the key can only recognise records the log actually holds. An
|
|
580
|
-
* action the executor REJECTED (owner authorization) writes nothing, so
|
|
581
|
-
* each of its retransmissions still reads as new input and still costs a
|
|
582
|
-
* rewind — unchanged from before this check existed, and self-limiting,
|
|
583
|
-
* because the sender stops re-sending the frame once it is acked.
|
|
584
|
-
*
|
|
585
|
-
* @param {number} f
|
|
586
|
-
* @returns {boolean}
|
|
587
|
-
* @private
|
|
588
|
-
*/
|
|
589
|
-
#frame_has_unapplied_input(f) {
|
|
590
|
-
const has_history = this.action_log.has_frame(f);
|
|
591
|
-
for (let i = 0; i < this.#pending_frames.length; i++) {
|
|
592
|
-
if (this.#pending_frames[i] !== f) continue;
|
|
593
|
-
// Nothing recorded for this frame yet: every entry is new.
|
|
594
|
-
if (!has_history) return true;
|
|
595
|
-
if (!this.#log_holds_record(f, this.#pending_senders[i], this.#pending_type_ids[i], this.#pending_payloads[i])) {
|
|
596
|
-
return true;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
558
|
/**
|
|
603
559
|
* Walk `action_log[frame_number]` looking for a record identical to
|
|
604
560
|
* `(sender_id, type_id, payload)`. Compares the payload in place against
|
|
605
561
|
* the log's own bytes — no copy, unlike {@link __read_historical}, because
|
|
606
|
-
* this runs per
|
|
562
|
+
* this runs once per arriving action.
|
|
607
563
|
*
|
|
608
|
-
*
|
|
564
|
+
* The key can only recognise records the log actually holds. An action
|
|
565
|
+
* the executor REJECTED (owner authorization) writes nothing, so each of
|
|
566
|
+
* its retransmissions still reads as new input and still costs a rewind —
|
|
567
|
+
* self-limiting, because the sender stops re-sending the frame once it is
|
|
568
|
+
* acked.
|
|
569
|
+
*
|
|
570
|
+
* Caller must have checked `action_log.has_frame(frame_number)`, and that
|
|
571
|
+
* the frame is closed.
|
|
609
572
|
*
|
|
610
573
|
* @param {number} frame_number
|
|
611
574
|
* @param {number} sender_id
|
|
@@ -615,38 +578,34 @@ export class ServerAuthoritativeServer {
|
|
|
615
578
|
* @private
|
|
616
579
|
*/
|
|
617
580
|
#log_holds_record(frame_number, sender_id, type_id, payload) {
|
|
618
|
-
const
|
|
619
|
-
|
|
620
|
-
const bytes =
|
|
621
|
-
while (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
buf.position += plen;
|
|
628
|
-
}
|
|
629
|
-
const r_type_id = buf.readUint8();
|
|
630
|
-
const r_sender_id = buf.readUint8();
|
|
631
|
-
const r_payload_len = buf.readUint32();
|
|
632
|
-
const r_payload_start = buf.position;
|
|
633
|
-
buf.position += r_payload_len;
|
|
634
|
-
|
|
635
|
-
if (r_type_id !== type_id) continue;
|
|
636
|
-
if (r_sender_id !== sender_id) continue;
|
|
637
|
-
if (r_payload_len !== payload.length) continue;
|
|
638
|
-
|
|
581
|
+
const cursor = this.#cursor;
|
|
582
|
+
cursor.open(this.action_log, frame_number);
|
|
583
|
+
const bytes = cursor.buffer.raw_bytes;
|
|
584
|
+
while (cursor.next()) {
|
|
585
|
+
if (cursor.action_type_id !== type_id) continue;
|
|
586
|
+
if (cursor.sender_id !== sender_id) continue;
|
|
587
|
+
if (cursor.payload_len !== payload.length) continue;
|
|
588
|
+
|
|
589
|
+
const payload_start = cursor.payload_start;
|
|
639
590
|
let same = true;
|
|
640
|
-
for (let i = 0; i <
|
|
641
|
-
if (bytes[
|
|
591
|
+
for (let i = 0; i < payload.length; i++) {
|
|
592
|
+
if (bytes[payload_start + i] !== payload[i]) { same = false; break; }
|
|
642
593
|
}
|
|
643
594
|
if (same) return true;
|
|
644
595
|
}
|
|
645
596
|
return false;
|
|
646
597
|
}
|
|
647
598
|
|
|
648
|
-
/**
|
|
649
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Execute frame `f`: its history, its pending input and — when `fresh`,
|
|
601
|
+
* meaning this is the first time the frame is simulated — the
|
|
602
|
+
* locally-authored queue, merged and sorted, then the local sim.
|
|
603
|
+
*
|
|
604
|
+
* @param {number} f
|
|
605
|
+
* @param {boolean} fresh
|
|
606
|
+
* @private
|
|
607
|
+
*/
|
|
608
|
+
#replay_frame(f, fresh) {
|
|
650
609
|
this.#sort_scratch.length = 0;
|
|
651
610
|
|
|
652
611
|
// Read historical actions out of action_log[f] BEFORE begin_frame(f)
|
|
@@ -659,15 +618,13 @@ export class ServerAuthoritativeServer {
|
|
|
659
618
|
this.#sort_scratch.push({ sender_id: h.sender_id, type_id: h.type_id, payload: h.payload });
|
|
660
619
|
}
|
|
661
620
|
|
|
662
|
-
// Append
|
|
663
|
-
//
|
|
664
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
//
|
|
668
|
-
//
|
|
669
|
-
// record left over from the previous apply and executes the action
|
|
670
|
-
// twice (and three times, four times… until the ack arrives).
|
|
621
|
+
// Append the pending actions for this frame. Retransmissions of
|
|
622
|
+
// records the log already holds are dropped on arrival (see
|
|
623
|
+
// __pending_add), so in the supported call pattern nothing here
|
|
624
|
+
// duplicates history. The check stays as a safety net: a duplicate
|
|
625
|
+
// that reached this point would merge with the historical record and
|
|
626
|
+
// execute the action twice, diverging the world, and the cost of the
|
|
627
|
+
// check is a scan of a small frame's copied history.
|
|
671
628
|
//
|
|
672
629
|
// Dedup key: (sender_id, type_id, payload bytes). Two actions are
|
|
673
630
|
// "the same arrival" if all three match. (Same sender_id and
|
|
@@ -696,7 +653,7 @@ export class ServerAuthoritativeServer {
|
|
|
696
653
|
// depend on whether it is being simulated fresh or replayed: on replay
|
|
697
654
|
// these come back through __read_historical and are sorted with
|
|
698
655
|
// everything else, so the fresh pass has to sort them the same way.
|
|
699
|
-
if (
|
|
656
|
+
if (fresh && this.#local_type_ids.length > 0) {
|
|
700
657
|
const sender = this.server_peer_id >= 0 ? this.server_peer_id : SENDER_LOCAL;
|
|
701
658
|
for (let i = 0; i < this.#local_type_ids.length; i++) {
|
|
702
659
|
this.#sort_scratch.push({
|
|
@@ -764,32 +721,20 @@ export class ServerAuthoritativeServer {
|
|
|
764
721
|
#read_historical(frame_number, out_array) {
|
|
765
722
|
out_array.length = 0;
|
|
766
723
|
if (!this.action_log.has_frame(frame_number)) return;
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
for (let i = 0; i < prior_count; i++) {
|
|
772
|
-
buf.readUintVar(); // entity_id
|
|
773
|
-
buf.readUint8(); // component_type_id
|
|
774
|
-
const plen = buf.readUint32();
|
|
775
|
-
buf.position += plen;
|
|
776
|
-
}
|
|
777
|
-
const type_id = buf.readUint8();
|
|
778
|
-
const sender_id = buf.readUint8();
|
|
779
|
-
const payload_len = buf.readUint32();
|
|
780
|
-
const payload_start = buf.position;
|
|
781
|
-
buf.position += payload_len;
|
|
782
|
-
|
|
724
|
+
const cursor = this.#cursor;
|
|
725
|
+
cursor.open(this.action_log, frame_number);
|
|
726
|
+
const bytes = cursor.buffer.raw_bytes;
|
|
727
|
+
while (cursor.next()) {
|
|
783
728
|
// Derived records are the previous pass's local-sim output. The
|
|
784
729
|
// caller re-runs onLocalSim for this frame, which recomputes them
|
|
785
730
|
// against a world that now includes the late input that forced the
|
|
786
731
|
// replay; carrying the stale copy forward as well would apply the
|
|
787
732
|
// effect twice, and the copies would accumulate one per replay.
|
|
788
|
-
if (sender_id === SENDER_DERIVED) continue;
|
|
733
|
+
if (cursor.sender_id === SENDER_DERIVED) continue;
|
|
789
734
|
|
|
790
|
-
const payload = new Uint8Array(payload_len);
|
|
791
|
-
payload.set(
|
|
792
|
-
out_array.push({ type_id, sender_id, payload });
|
|
735
|
+
const payload = new Uint8Array(cursor.payload_len);
|
|
736
|
+
payload.set(bytes.subarray(cursor.payload_start, cursor.payload_start + cursor.payload_len));
|
|
737
|
+
out_array.push({ type_id: cursor.action_type_id, sender_id: cursor.sender_id, payload });
|
|
793
738
|
}
|
|
794
739
|
}
|
|
795
740
|
|
|
@@ -824,17 +769,17 @@ export class ServerAuthoritativeServer {
|
|
|
824
769
|
return;
|
|
825
770
|
}
|
|
826
771
|
|
|
827
|
-
//
|
|
828
|
-
//
|
|
829
|
-
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
836
|
-
//
|
|
837
|
-
//
|
|
772
|
+
// Retransmissions are routine on the wire: `Replicator.pack_for_peer`
|
|
773
|
+
// packs every frame in [last_acked + 1, current_frame] each tick, so
|
|
774
|
+
// an action arrives once per tick until its ack round-trips, and
|
|
775
|
+
// several copies can land in the same server tick when the round trip
|
|
776
|
+
// exceeds the tick period. A copy is recognised by its whole identity,
|
|
777
|
+
// (sender_id, type_id, payload bytes) — distinct payloads from the
|
|
778
|
+
// same sender at the same frame are legitimately different actions
|
|
779
|
+
// (move + fire) and both flow through.
|
|
780
|
+
//
|
|
781
|
+
// First against everything already pending for the frame, which
|
|
782
|
+
// catches the copies that arrive before the frame is simulated.
|
|
838
783
|
for (let i = 0; i < this.#pending_frames.length; i++) {
|
|
839
784
|
if (this.#pending_frames[i] !== frame_number) continue;
|
|
840
785
|
if (this.#pending_senders[i] !== sender_id) continue;
|
|
@@ -842,6 +787,26 @@ export class ServerAuthoritativeServer {
|
|
|
842
787
|
if (fastArrayEquals(this.#pending_payloads[i], payload)) return; // duplicate already in pending; drop
|
|
843
788
|
}
|
|
844
789
|
|
|
790
|
+
// Then against the action log, which catches the copies that arrive
|
|
791
|
+
// after it. A record the log already holds is not input — applying it
|
|
792
|
+
// again would apply the action twice, and rewinding for it would
|
|
793
|
+
// replay the world into the state it is already in — so it is dropped
|
|
794
|
+
// here, and pending holds only input the log does not. That is what
|
|
795
|
+
// lets `tick` take the oldest pending frame as its replay window
|
|
796
|
+
// without a second look.
|
|
797
|
+
//
|
|
798
|
+
// Frames are only open inside `tick`, and packets arrive between
|
|
799
|
+
// ticks, so an open frame here means a call pattern this orchestrator
|
|
800
|
+
// does not support (a packet delivered from inside a local-sim
|
|
801
|
+
// handler). The check is skipped rather than reading an open frame's
|
|
802
|
+
// stale length; the copy then costs one spurious rewind, and
|
|
803
|
+
// `__replay_frame`'s own dedup keeps it from applying twice.
|
|
804
|
+
if (this.action_log.has_frame(frame_number)
|
|
805
|
+
&& !this.action_log.has_open_frame()
|
|
806
|
+
&& this.#log_holds_record(frame_number, sender_id, type_id, payload)) {
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
845
810
|
this.#pending_frames.push(frame_number);
|
|
846
811
|
this.#pending_senders.push(sender_id);
|
|
847
812
|
this.#pending_type_ids.push(type_id);
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
* uint32: action_payload_len
|
|
26
26
|
* bytes: action_payload
|
|
27
27
|
* ```
|
|
28
|
+
* {@link NetworkPeer} prefixes each packet of this with a slice header — the
|
|
29
|
+
* frame range the packet covers and whether it is the first of the sender's
|
|
30
|
+
* tick — which it parses itself and hands to {@link unpack_from_peer} as
|
|
31
|
+
* `slice`, so the receiver can apply the sender's packets in frame order.
|
|
28
32
|
*
|
|
29
33
|
* @author Alex Goldring
|
|
30
34
|
* @copyright Company Named Limited (c) 2026
|
|
@@ -56,8 +60,9 @@ export class Replicator {
|
|
|
56
60
|
* or anything else that needs to know "frame N from peer P has just
|
|
57
61
|
* been applied to the local world."
|
|
58
62
|
*
|
|
59
|
-
* Fires in frame
|
|
60
|
-
*
|
|
63
|
+
* Fires in frame order — within a packet, which encodes its frames
|
|
64
|
+
* ascending, and across packets, because a later slice of a sender's tick
|
|
65
|
+
* is held until the slices before it have landed.
|
|
61
66
|
*
|
|
62
67
|
* @type {Signal}
|
|
63
68
|
*/
|
|
@@ -136,15 +141,32 @@ export class Replicator {
|
|
|
136
141
|
* sender's frame number, ensuring the rewind/replay machinery sees consistent
|
|
137
142
|
* frame indexing across peers.
|
|
138
143
|
*
|
|
144
|
+
* `slice` is the sender's description of the packet, from the
|
|
145
|
+
* {@link NetworkPeer} slice header: the frame range it covers and whether
|
|
146
|
+
* it is the first packet of the sender's tick. Frames are applied in frame
|
|
147
|
+
* order. A slice that starts past the frames applied so far and is not a
|
|
148
|
+
* head is a later slice of a burst whose earlier slice has not landed; it
|
|
149
|
+
* is held and applied once the gap before it fills. A head is applied as
|
|
150
|
+
* soon as it arrives, because nothing the sender can still send lies
|
|
151
|
+
* before it — that is also how the receiver moves past frames lost for
|
|
152
|
+
* good. Without `slice` (callers that pack and unpack directly) the packet
|
|
153
|
+
* is taken as a head covering the frames it carries.
|
|
154
|
+
*
|
|
139
155
|
* @param {number} peer_id
|
|
140
156
|
* @param {BinaryBuffer} in_buffer
|
|
141
157
|
* @param {number} in_buffer_end byte position to stop reading at
|
|
158
|
+
* @param {{frame_start: number, frame_end: number, head: boolean}|null} [slice]
|
|
142
159
|
*/
|
|
143
|
-
unpack_from_peer(peer_id: number, in_buffer: BinaryBuffer, in_buffer_end: number
|
|
160
|
+
unpack_from_peer(peer_id: number, in_buffer: BinaryBuffer, in_buffer_end: number, slice?: {
|
|
161
|
+
frame_start: number;
|
|
162
|
+
frame_end: number;
|
|
163
|
+
head: boolean;
|
|
164
|
+
} | null): void;
|
|
144
165
|
/**
|
|
145
|
-
* Forget
|
|
146
|
-
*
|
|
147
|
-
*
|
|
166
|
+
* Forget a peer's inbound state — its apply watermark and any slices held
|
|
167
|
+
* for it. Call when a peer disconnects so a later peer reusing the same id
|
|
168
|
+
* (or the same peer resuming a session whose frame numbering restarts) is
|
|
169
|
+
* not wrongly deduped against stale state.
|
|
148
170
|
*
|
|
149
171
|
* @param {number} peer_id
|
|
150
172
|
*/
|
|
@@ -152,4 +174,5 @@ export class Replicator {
|
|
|
152
174
|
#private;
|
|
153
175
|
}
|
|
154
176
|
import Signal from "../../../core/events/signal/Signal.js";
|
|
177
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
155
178
|
//# sourceMappingURL=Replicator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Replicator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/replication/Replicator.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Replicator.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/replication/Replicator.js"],"names":[],"mappings":"AAqBA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH;IAkFI;;;;;;;;OAQG;IACH;;;;;;wCAHiD,MAAM,cAAc,MAAM,GAAG,OAAO;;OA8BpF;IA/ED;;;;;;;;;;;;;OAaG;IACH,gBAFU,MAAM,CAEc;IAE9B;;;;;;;;;;;;;;;;;OAiBG;IACH,6BAFqB,MAAM,gBAAgB,MAAM,kBAAkB,MAAM,aAAa,YAAY,kBAAkB,MAAM,eAAe,MAAM,KAAK,IAAI,CAE/H;IAmBrB;;OAEG;IACH,sBAA4B;IAE5B;;OAEG;IACH,mCAAsC;IAEtC;;OAEG;IACH,4BAAwB;IAExB;;OAEG;IACH,iCAA4B;IAE5B;;;;OAIG;IACH;oCAFwC,MAAM,cAAc,MAAM,GAAG,OAAO;MAE5C;IAGpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,uBAVW,MAAM,eACN,MAAM,aACN,MAAM,cACN,YAAY,cACZ,MAAM,GACJ,MAAM,CA4ElB;IAED;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,0BALW,MAAM,aACN,YAAY,iBACZ,MAAM,UACN;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAC,GAAC,IAAI,QAmEtE;IAyLD;;;;;;;OAOG;IACH,qBAFW,MAAM,QAKhB;;CA8CJ;mBAxlBkB,uCAAuC;6BAD7B,sCAAsC"}
|