@woosh/meep-engine 3.14.3 → 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/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 +96 -7
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +392 -68
- 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 +105 -13
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +306 -59
- package/src/engine/network/replication/Replicator.d.ts +43 -7
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +309 -111
- package/src/engine/network/sim/ActionLog.d.ts +23 -3
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +26 -3
- 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/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 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +74 -7
- package/src/engine/network/transport/Channel.d.ts +1 -1
- package/src/engine/network/transport/Channel.js +1 -1
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- 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 +553 -0
- package/src/shade/playground/skinned_mesh_soup/README.md +143 -0
- package/src/shade/playground/skinned_mesh_soup/index.html +202 -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 +567 -0
- package/src/shade/playground/skinned_mesh_soup/verify_clone_vertices.d.ts +29 -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 +428 -0
- 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/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/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/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/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/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/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
- 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
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
2
3
|
import Signal from "../../../core/events/signal/Signal.js";
|
|
3
4
|
import { MalformedPacketError } from "../core/MalformedPacketError.js";
|
|
5
|
+
import { ActionRecordCursor } from "../sim/ActionRecordCursor.js";
|
|
4
6
|
|
|
5
7
|
// Bytes of fixed per-action framing that always precede the variable payload:
|
|
6
8
|
// uint8 action_type_id + uint32 action_payload_len.
|
|
7
9
|
const ACTION_HEADER_BYTES = 5;
|
|
8
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Most slices {@link Replicator#unpack_from_peer} holds per peer while waiting
|
|
13
|
+
* for an earlier slice to land. A sender bursts at most `max_packets_per_tick`
|
|
14
|
+
* slices a tick and re-sends the missing one every tick, so a legitimate
|
|
15
|
+
* backlog is a burst or two; this is a backstop against a sender that never
|
|
16
|
+
* fills the gap. Beyond it the farthest slice is dropped — those frames were
|
|
17
|
+
* acked at the channel level, so the sender will credit them and not re-send
|
|
18
|
+
* them, which is a loss, and the reason the cap is generous.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_HELD_SLICES = 256;
|
|
21
|
+
|
|
9
22
|
/**
|
|
10
23
|
* Forwards action records between peers.
|
|
11
24
|
*
|
|
@@ -33,33 +46,52 @@ const ACTION_HEADER_BYTES = 5;
|
|
|
33
46
|
* uint32: action_payload_len
|
|
34
47
|
* bytes: action_payload
|
|
35
48
|
* ```
|
|
49
|
+
* {@link NetworkPeer} prefixes each packet of this with a slice header — the
|
|
50
|
+
* frame range the packet covers and whether it is the first of the sender's
|
|
51
|
+
* tick — which it parses itself and hands to {@link unpack_from_peer} as
|
|
52
|
+
* `slice`, so the receiver can apply the sender's packets in frame order.
|
|
36
53
|
*
|
|
37
54
|
* @author Alex Goldring
|
|
38
55
|
* @copyright Company Named Limited (c) 2026
|
|
39
56
|
*/
|
|
40
57
|
export class Replicator {
|
|
41
58
|
/**
|
|
42
|
-
* Pre-allocated scratch for
|
|
43
|
-
*
|
|
44
|
-
* Grown on demand.
|
|
59
|
+
* Pre-allocated scratch for the in-scope actions of the current pack
|
|
60
|
+
* pass: `(action_type_id, payload_start, payload_len)` per action,
|
|
61
|
+
* interleaved. Grown on demand.
|
|
45
62
|
* @type {Int32Array}
|
|
46
63
|
* @private
|
|
47
64
|
*/
|
|
48
|
-
#
|
|
65
|
+
#in_scope_actions = new Int32Array(64 * 3);
|
|
49
66
|
/**
|
|
50
|
-
* Per-peer
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* frame
|
|
58
|
-
*
|
|
59
|
-
*
|
|
67
|
+
* Per-peer state of {@link unpack_from_peer}'s execute-on-arrival path:
|
|
68
|
+
* `next_frame`, the frame after the newest applied, and `held`, later
|
|
69
|
+
* slices of a sender's tick waiting for an earlier slice still in flight,
|
|
70
|
+
* sorted by `frame_start`.
|
|
71
|
+
*
|
|
72
|
+
* `next_frame` is what keeps each frame applied exactly once. The action
|
|
73
|
+
* stream re-sends every unconfirmed frame each tick (back-fill for loss
|
|
74
|
+
* tolerance), so a frame arrives several times before its ack round-trips,
|
|
75
|
+
* and re-running it would recycle its action-log slot and re-capture prior
|
|
76
|
+
* state from the already-mutated world — corrupting the rewind baseline.
|
|
77
|
+
* Frames below `next_frame` are skipped.
|
|
78
|
+
*
|
|
79
|
+
* One watermark is sound only because frames are applied in order. The
|
|
80
|
+
* sender may split a tick's owed range across several packets
|
|
81
|
+
* ({@link NetworkPeer#flush_outbound}), and when the first is lost the
|
|
82
|
+
* later ones land first. Applied on arrival they would put their frames
|
|
83
|
+
* ahead of the re-sent earlier ones — an absolute-state action from the
|
|
84
|
+
* older frame would then overwrite the newer, a despawn could precede its
|
|
85
|
+
* spawn — and the watermark would take them as proof of everything before,
|
|
86
|
+
* so the re-send would be skipped for good while the sender, seeing it
|
|
87
|
+
* acked, credited those frames as delivered. So a later slice is held
|
|
88
|
+
* until the slices before it have landed. Only the execute-on-arrival path
|
|
89
|
+
* keeps this; the deferred path ({@link on_pending_action}) orders and
|
|
90
|
+
* dedups downstream.
|
|
91
|
+
* @type {Map<number, {next_frame: number, held: Array<{frame_start: number, frame_end: number, buffer: BinaryBuffer, length: number}>}>}
|
|
60
92
|
* @private
|
|
61
93
|
*/
|
|
62
|
-
#
|
|
94
|
+
#inbound = new Map();
|
|
63
95
|
|
|
64
96
|
/**
|
|
65
97
|
* Fired after each per-frame action group is fully applied via
|
|
@@ -69,8 +101,9 @@ export class Replicator {
|
|
|
69
101
|
* or anything else that needs to know "frame N from peer P has just
|
|
70
102
|
* been applied to the local world."
|
|
71
103
|
*
|
|
72
|
-
* Fires in frame
|
|
73
|
-
*
|
|
104
|
+
* Fires in frame order — within a packet, which encodes its frames
|
|
105
|
+
* ascending, and across packets, because a later slice of a sender's tick
|
|
106
|
+
* is held until the slices before it have landed.
|
|
74
107
|
*
|
|
75
108
|
* @type {Signal}
|
|
76
109
|
*/
|
|
@@ -96,6 +129,13 @@ export class Replicator {
|
|
|
96
129
|
*/
|
|
97
130
|
on_pending_action = null;
|
|
98
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Walks a frame's records for {@link pack_for_peer}.
|
|
134
|
+
* @type {ActionRecordCursor}
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
#cursor = new ActionRecordCursor();
|
|
138
|
+
|
|
99
139
|
/**
|
|
100
140
|
* @param {{
|
|
101
141
|
* action_log: ActionLog,
|
|
@@ -140,51 +180,95 @@ export class Replicator {
|
|
|
140
180
|
*
|
|
141
181
|
* Writes nothing if no in-scope actions are found.
|
|
142
182
|
*
|
|
183
|
+
* `max_bytes` caps how many bytes this call may add. Frame groups are
|
|
184
|
+
* written whole or not at all: when the next group would cross the cap, the
|
|
185
|
+
* pack stops before it and the return value reports how far it got, so the
|
|
186
|
+
* caller can credit exactly what it sent and pick the rest up on a later
|
|
187
|
+
* pass. The one exception is a first group that does not fit by itself —
|
|
188
|
+
* that one is written anyway, because no smaller pack could ever carry the
|
|
189
|
+
* frame and dropping it would strand those actions forever.
|
|
190
|
+
*
|
|
143
191
|
* @param {number} peer_id
|
|
144
192
|
* @param {number} start_frame inclusive; if no frame in range is in the log, that frame is skipped
|
|
145
193
|
* @param {number} end_frame inclusive
|
|
146
194
|
* @param {BinaryBuffer} out_buffer
|
|
195
|
+
* @param {number} [max_bytes] byte budget for this call; unbounded by default
|
|
196
|
+
* @returns {number} the last frame this pack covers, or `start_frame - 1` if
|
|
197
|
+
* it covers nothing. A frame that was skipped — absent from the log, or
|
|
198
|
+
* holding nothing in scope — counts as covered: the receiver has nothing
|
|
199
|
+
* to apply for it, so the caller may credit it.
|
|
147
200
|
*/
|
|
148
|
-
pack_for_peer(peer_id, start_frame, end_frame, out_buffer) {
|
|
201
|
+
pack_for_peer(peer_id, start_frame, end_frame, out_buffer, max_bytes = Number.MAX_SAFE_INTEGER) {
|
|
149
202
|
assert.isNonNegativeInteger(peer_id, 'peer_id');
|
|
150
203
|
assert.isNonNegativeInteger(start_frame, 'start_frame');
|
|
151
204
|
assert.isNonNegativeInteger(end_frame, 'end_frame');
|
|
152
205
|
|
|
153
|
-
|
|
154
|
-
|
|
206
|
+
// Budget is measured from wherever the caller had already written to,
|
|
207
|
+
// so a caller that prefixed a packet-type byte gets it counted.
|
|
208
|
+
const budget_end = out_buffer.position + max_bytes;
|
|
209
|
+
let last_packed = start_frame - 1;
|
|
210
|
+
let wrote_any_group = false;
|
|
155
211
|
|
|
156
|
-
|
|
157
|
-
|
|
212
|
+
for (let frame = start_frame; frame <= end_frame; frame++) {
|
|
213
|
+
if (!this.action_log.has_frame(frame)) {
|
|
214
|
+
last_packed = frame;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
158
217
|
|
|
159
|
-
// First pass: walk records,
|
|
218
|
+
// First pass: walk the records, remembering where the action of
|
|
219
|
+
// each in-scope one sits.
|
|
220
|
+
const cursor = this.#cursor;
|
|
221
|
+
cursor.open(this.action_log, frame);
|
|
160
222
|
let in_scope_count = 0;
|
|
161
|
-
while (
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (in_scope) {
|
|
167
|
-
if ((in_scope_count + 1) * 2 > this.#record_bounds.length) {
|
|
168
|
-
this.#grow_record_bounds();
|
|
169
|
-
}
|
|
170
|
-
this.#record_bounds[in_scope_count * 2] = record_start;
|
|
171
|
-
this.#record_bounds[in_scope_count * 2 + 1] = record_end;
|
|
172
|
-
in_scope_count++;
|
|
223
|
+
while (cursor.next()) {
|
|
224
|
+
if (!this.#record_in_scope(cursor, peer_id)) continue;
|
|
225
|
+
if ((in_scope_count + 1) * 3 > this.#in_scope_actions.length) {
|
|
226
|
+
this.#grow_in_scope_actions();
|
|
173
227
|
}
|
|
228
|
+
this.#in_scope_actions[in_scope_count * 3] = cursor.action_type_id;
|
|
229
|
+
this.#in_scope_actions[in_scope_count * 3 + 1] = cursor.payload_start;
|
|
230
|
+
this.#in_scope_actions[in_scope_count * 3 + 2] = cursor.payload_len;
|
|
231
|
+
in_scope_count++;
|
|
174
232
|
}
|
|
175
233
|
|
|
176
|
-
if (in_scope_count === 0)
|
|
234
|
+
if (in_scope_count === 0) {
|
|
235
|
+
last_packed = frame;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
177
238
|
|
|
178
239
|
// Second pass: write the frame header + each in-scope action's payload only.
|
|
240
|
+
// The group's encoded size is only known once written (varint frame
|
|
241
|
+
// numbers and counts), so write it and roll back if it overran.
|
|
242
|
+
const group_start = out_buffer.position;
|
|
179
243
|
out_buffer.writeUintVar(frame);
|
|
180
244
|
out_buffer.writeUintVar(in_scope_count);
|
|
181
245
|
|
|
246
|
+
// Wire format per action is (type_id, len, payload). The record's
|
|
247
|
+
// local-only sender_id byte is NOT copied: the receiver derives the
|
|
248
|
+
// sender from the packet's peer_id, and carrying it on the wire
|
|
249
|
+
// would let a hostile peer impersonate other peers' actions.
|
|
250
|
+
const raw_bytes = cursor.buffer.raw_bytes;
|
|
182
251
|
for (let i = 0; i < in_scope_count; i++) {
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
this.#
|
|
252
|
+
const payload_start = this.#in_scope_actions[i * 3 + 1];
|
|
253
|
+
const payload_len = this.#in_scope_actions[i * 3 + 2];
|
|
254
|
+
out_buffer.writeUint8(this.#in_scope_actions[i * 3]);
|
|
255
|
+
out_buffer.writeUint32(payload_len);
|
|
256
|
+
out_buffer.writeBytes(raw_bytes, payload_start, payload_len);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (wrote_any_group && out_buffer.position > budget_end) {
|
|
260
|
+
// Doesn't fit alongside what we already have. Un-write it and
|
|
261
|
+
// stop — the caller credits only up to the previous frame and
|
|
262
|
+
// this one leads the next pack.
|
|
263
|
+
out_buffer.position = group_start;
|
|
264
|
+
break;
|
|
186
265
|
}
|
|
266
|
+
|
|
267
|
+
wrote_any_group = true;
|
|
268
|
+
last_packed = frame;
|
|
187
269
|
}
|
|
270
|
+
|
|
271
|
+
return last_packed;
|
|
188
272
|
}
|
|
189
273
|
|
|
190
274
|
/**
|
|
@@ -193,11 +277,23 @@ export class Replicator {
|
|
|
193
277
|
* sender's frame number, ensuring the rewind/replay machinery sees consistent
|
|
194
278
|
* frame indexing across peers.
|
|
195
279
|
*
|
|
280
|
+
* `slice` is the sender's description of the packet, from the
|
|
281
|
+
* {@link NetworkPeer} slice header: the frame range it covers and whether
|
|
282
|
+
* it is the first packet of the sender's tick. Frames are applied in frame
|
|
283
|
+
* order. A slice that starts past the frames applied so far and is not a
|
|
284
|
+
* head is a later slice of a burst whose earlier slice has not landed; it
|
|
285
|
+
* is held and applied once the gap before it fills. A head is applied as
|
|
286
|
+
* soon as it arrives, because nothing the sender can still send lies
|
|
287
|
+
* before it — that is also how the receiver moves past frames lost for
|
|
288
|
+
* good. Without `slice` (callers that pack and unpack directly) the packet
|
|
289
|
+
* is taken as a head covering the frames it carries.
|
|
290
|
+
*
|
|
196
291
|
* @param {number} peer_id
|
|
197
292
|
* @param {BinaryBuffer} in_buffer
|
|
198
293
|
* @param {number} in_buffer_end byte position to stop reading at
|
|
294
|
+
* @param {{frame_start: number, frame_end: number, head: boolean}|null} [slice]
|
|
199
295
|
*/
|
|
200
|
-
unpack_from_peer(peer_id, in_buffer, in_buffer_end) {
|
|
296
|
+
unpack_from_peer(peer_id, in_buffer, in_buffer_end, slice = null) {
|
|
201
297
|
assert.isNonNegativeInteger(peer_id, 'peer_id');
|
|
202
298
|
assert.isNonNegativeInteger(in_buffer_end, 'in_buffer_end');
|
|
203
299
|
|
|
@@ -234,19 +330,72 @@ export class Replicator {
|
|
|
234
330
|
return;
|
|
235
331
|
}
|
|
236
332
|
|
|
237
|
-
|
|
333
|
+
let state = this.#inbound.get(peer_id);
|
|
334
|
+
if (state === undefined) {
|
|
335
|
+
state = { next_frame: 0, held: [] };
|
|
336
|
+
this.#inbound.set(peer_id, state);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (slice !== null && !slice.head && slice.frame_start > state.next_frame) {
|
|
340
|
+
this.#hold_slice(peer_id, state, slice, in_buffer, in_buffer_end);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (slice !== null && slice.head) {
|
|
345
|
+
// Nothing before a head will be sent again. Whatever was held from
|
|
346
|
+
// before it is as complete as it will ever be, so apply it first,
|
|
347
|
+
// in order, rather than letting the head jump past it.
|
|
348
|
+
this.#apply_held_before(peer_id, state, slice.frame_start);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const last_frame = this.#apply_groups(
|
|
352
|
+
peer_id, in_buffer, in_buffer_end, state.next_frame,
|
|
353
|
+
slice !== null ? slice.frame_start : 0,
|
|
354
|
+
slice !== null ? slice.frame_end : Infinity,
|
|
355
|
+
);
|
|
356
|
+
const covered_through = slice !== null ? slice.frame_end : last_frame;
|
|
357
|
+
if (covered_through + 1 > state.next_frame) {
|
|
358
|
+
state.next_frame = covered_through + 1;
|
|
359
|
+
}
|
|
360
|
+
this.#apply_held_contiguous(peer_id, state);
|
|
361
|
+
}
|
|
238
362
|
|
|
239
|
-
|
|
363
|
+
/**
|
|
364
|
+
* Apply the frame groups in `[in_buffer.position, in_buffer_end)`, skipping
|
|
365
|
+
* any frame below `min_frame` — already applied, or, when `min_frame` is
|
|
366
|
+
* `Infinity`, every frame, which is how a packet is validated before it is
|
|
367
|
+
* held. A skipped group is still walked, so a malformed one throws here
|
|
368
|
+
* rather than later. A group outside `[frame_start, frame_end]`, the range
|
|
369
|
+
* the slice header declared, is malformed too: the watermark is raised to
|
|
370
|
+
* the header's end, so a frame beyond it would be applied again by the
|
|
371
|
+
* slice that genuinely carries it.
|
|
372
|
+
*
|
|
373
|
+
* @param {number} peer_id
|
|
374
|
+
* @param {BinaryBuffer} in_buffer
|
|
375
|
+
* @param {number} in_buffer_end
|
|
376
|
+
* @param {number} min_frame
|
|
377
|
+
* @param {number} frame_start
|
|
378
|
+
* @param {number} frame_end
|
|
379
|
+
* @returns {number} the highest frame number the packet carries, or -1 if none
|
|
380
|
+
* @private
|
|
381
|
+
*/
|
|
382
|
+
#apply_groups(peer_id, in_buffer, in_buffer_end, min_frame, frame_start, frame_end) {
|
|
383
|
+
const registry = this.action_registry;
|
|
384
|
+
let last_frame = -1;
|
|
240
385
|
|
|
241
386
|
while (in_buffer.position < in_buffer_end) {
|
|
242
387
|
const frame_number = in_buffer.readUintVar();
|
|
243
388
|
const action_count = in_buffer.readUintVar();
|
|
389
|
+
if (frame_number < frame_start || frame_number > frame_end) {
|
|
390
|
+
throw new MalformedPacketError(`Replicator.unpack_from_peer: frame ${frame_number} lies outside the slice [${frame_start}, ${frame_end}]`);
|
|
391
|
+
}
|
|
392
|
+
if (frame_number > last_frame) last_frame = frame_number;
|
|
244
393
|
|
|
245
394
|
// Skip frames already applied from this peer — the action stream
|
|
246
|
-
// re-sends every
|
|
247
|
-
// corrupt the action-log prior-state capture (see #
|
|
395
|
+
// re-sends every unconfirmed frame each tick, and re-applying would
|
|
396
|
+
// corrupt the action-log prior-state capture (see #inbound).
|
|
248
397
|
// The read cursor must still advance past this frame's actions.
|
|
249
|
-
if (
|
|
398
|
+
if (frame_number < min_frame) {
|
|
250
399
|
for (let i = 0; i < action_count; i++) {
|
|
251
400
|
if (in_buffer.position + ACTION_HEADER_BYTES > in_buffer_end) {
|
|
252
401
|
throw new MalformedPacketError(`Replicator.unpack_from_peer: action header overruns packet (skip; frame ${frame_number}, action ${i}/${action_count})`);
|
|
@@ -294,44 +443,135 @@ export class Replicator {
|
|
|
294
443
|
this.action_log.end_frame();
|
|
295
444
|
}
|
|
296
445
|
|
|
297
|
-
// Advance the per-peer watermark so subsequent back-fill resends of
|
|
298
|
-
// this frame are skipped. Frames within a packet are strictly
|
|
299
|
-
// ascending, so this only ever moves forward.
|
|
300
|
-
this.#applied_through.set(peer_id, frame_number);
|
|
301
|
-
|
|
302
446
|
// Fire AFTER end_frame so handlers see consistent state and can
|
|
303
447
|
// safely query the action log for the just-applied frame.
|
|
304
448
|
this.onFrameApplied.send2(peer_id, frame_number);
|
|
305
449
|
}
|
|
450
|
+
|
|
451
|
+
return last_frame;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Keep a slice that arrived ahead of the slices before it, to be applied
|
|
456
|
+
* once they land. The bytes are copied — the inbound buffer is shared
|
|
457
|
+
* scratch — after a validation walk, so a malformed slice is rejected now,
|
|
458
|
+
* by the packet that carried it, and not during a later drain inside some
|
|
459
|
+
* other packet's handling.
|
|
460
|
+
*
|
|
461
|
+
* @param {number} peer_id
|
|
462
|
+
* @param {{next_frame: number, held: Array<{frame_start: number, frame_end: number, buffer: BinaryBuffer, length: number}>}} state
|
|
463
|
+
* @param {{frame_start: number, frame_end: number}} slice
|
|
464
|
+
* @param {BinaryBuffer} in_buffer
|
|
465
|
+
* @param {number} in_buffer_end
|
|
466
|
+
* @private
|
|
467
|
+
*/
|
|
468
|
+
#hold_slice(peer_id, state, slice, in_buffer, in_buffer_end) {
|
|
469
|
+
const start = in_buffer.position;
|
|
470
|
+
this.#apply_groups(peer_id, in_buffer, in_buffer_end, Infinity, slice.frame_start, slice.frame_end);
|
|
471
|
+
|
|
472
|
+
const held = state.held;
|
|
473
|
+
let i = 0;
|
|
474
|
+
while (i < held.length && held[i].frame_start < slice.frame_start) i++;
|
|
475
|
+
|
|
476
|
+
// Slice boundaries move between ticks: the last slice of a burst grows
|
|
477
|
+
// by a frame a tick until it fills a packet, a frame that packs to
|
|
478
|
+
// nothing lets the next one in, a rollback that grows a frame pushes
|
|
479
|
+
// the next one out. So a re-send with the same start is not the same
|
|
480
|
+
// slice. Every copy was acked, and the sender credits every frame any
|
|
481
|
+
// copy covered, so a frame only one copy covers is never sent again:
|
|
482
|
+
// keep the copy that reaches further, and between equals the newer,
|
|
483
|
+
// which carries the sender's latest content for those frames.
|
|
484
|
+
const same_start = i < held.length && held[i].frame_start === slice.frame_start;
|
|
485
|
+
if (same_start && held[i].frame_end > slice.frame_end) return;
|
|
486
|
+
if (!same_start && held.length >= MAX_HELD_SLICES) {
|
|
487
|
+
// Keep the slices nearest the gap — the ones the next head is most
|
|
488
|
+
// likely to release — and drop the farthest.
|
|
489
|
+
if (i === held.length) return;
|
|
490
|
+
held.pop();
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const length = in_buffer_end - start;
|
|
494
|
+
const buffer = new BinaryBuffer();
|
|
495
|
+
buffer.setCapacity(length);
|
|
496
|
+
buffer.writeBytes(in_buffer.raw_bytes, start, length);
|
|
497
|
+
const entry = { frame_start: slice.frame_start, frame_end: slice.frame_end, buffer, length };
|
|
498
|
+
if (same_start) {
|
|
499
|
+
held[i] = entry;
|
|
500
|
+
} else {
|
|
501
|
+
held.splice(i, 0, entry);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Apply, in order, every held slice that starts before `frame` — the start
|
|
507
|
+
* of a head, before which nothing will be sent again.
|
|
508
|
+
* @private
|
|
509
|
+
*/
|
|
510
|
+
#apply_held_before(peer_id, state, frame) {
|
|
511
|
+
const held = state.held;
|
|
512
|
+
while (held.length > 0 && held[0].frame_start < frame) {
|
|
513
|
+
const h = held.shift();
|
|
514
|
+
if (h.frame_end >= state.next_frame) this.#apply_held(peer_id, state, h);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Apply held slices for as long as the next one starts at or before the
|
|
520
|
+
* watermark, dropping any the watermark has already passed.
|
|
521
|
+
* @private
|
|
522
|
+
*/
|
|
523
|
+
#apply_held_contiguous(peer_id, state) {
|
|
524
|
+
const held = state.held;
|
|
525
|
+
while (held.length > 0) {
|
|
526
|
+
const h = held[0];
|
|
527
|
+
if (h.frame_end < state.next_frame) {
|
|
528
|
+
held.shift();
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
if (h.frame_start > state.next_frame) break;
|
|
532
|
+
held.shift();
|
|
533
|
+
this.#apply_held(peer_id, state, h);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** @private */
|
|
538
|
+
#apply_held(peer_id, state, h) {
|
|
539
|
+
h.buffer.position = 0;
|
|
540
|
+
this.#apply_groups(peer_id, h.buffer, h.length, state.next_frame, h.frame_start, h.frame_end);
|
|
541
|
+
if (h.frame_end + 1 > state.next_frame) {
|
|
542
|
+
state.next_frame = h.frame_end + 1;
|
|
543
|
+
}
|
|
306
544
|
}
|
|
307
545
|
|
|
308
546
|
/**
|
|
309
|
-
* Forget
|
|
310
|
-
*
|
|
311
|
-
*
|
|
547
|
+
* Forget a peer's inbound state — its apply watermark and any slices held
|
|
548
|
+
* for it. Call when a peer disconnects so a later peer reusing the same id
|
|
549
|
+
* (or the same peer resuming a session whose frame numbering restarts) is
|
|
550
|
+
* not wrongly deduped against stale state.
|
|
312
551
|
*
|
|
313
552
|
* @param {number} peer_id
|
|
314
553
|
*/
|
|
315
554
|
forget_peer(peer_id) {
|
|
316
555
|
assert.isNonNegativeInteger(peer_id, 'peer_id');
|
|
317
|
-
this.#
|
|
556
|
+
this.#inbound.delete(peer_id);
|
|
318
557
|
}
|
|
319
558
|
|
|
320
559
|
/**
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
560
|
+
* Is the record under `cursor` in scope for `peer_id` — does it touch an
|
|
561
|
+
* entity the peer should see? A record with no affected components (an
|
|
562
|
+
* event-style action) is always in scope.
|
|
324
563
|
*
|
|
325
|
-
* @param {
|
|
564
|
+
* @param {ActionRecordCursor} cursor
|
|
326
565
|
* @param {number} peer_id
|
|
327
566
|
* @returns {boolean}
|
|
328
567
|
* @private
|
|
329
568
|
*/
|
|
330
|
-
#
|
|
331
|
-
|
|
569
|
+
#record_in_scope(cursor, peer_id) {
|
|
570
|
+
if (cursor.prior_count === 0) return true;
|
|
332
571
|
|
|
333
|
-
|
|
334
|
-
|
|
572
|
+
const buffer = cursor.buffer;
|
|
573
|
+
buffer.position = cursor.prior_start;
|
|
574
|
+
for (let i = 0; i < cursor.prior_count; i++) {
|
|
335
575
|
const entity_id = buffer.readUintVar();
|
|
336
576
|
// Translate local entity_id to network_id for the scope filter, which
|
|
337
577
|
// operates on peer-shared identifiers. If `network_for` returns -1,
|
|
@@ -342,64 +582,22 @@ export class Replicator {
|
|
|
342
582
|
// safely apply.
|
|
343
583
|
const network_id = this.slot_table.network_for(entity_id);
|
|
344
584
|
if (network_id >= 0 && this.scope_filter.is_entity_in_scope(peer_id, network_id)) {
|
|
345
|
-
|
|
585
|
+
return true;
|
|
346
586
|
}
|
|
347
587
|
buffer.readUint8(); // component_type_id
|
|
348
|
-
const
|
|
349
|
-
buffer.position +=
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Skip the action payload too.
|
|
353
|
-
buffer.readUint8(); // action_type_id
|
|
354
|
-
buffer.readUint8(); // sender_id (local-only metadata, not sent)
|
|
355
|
-
const action_payload_len = buffer.readUint32();
|
|
356
|
-
buffer.position += action_payload_len;
|
|
357
|
-
|
|
358
|
-
// Event-style actions (no affected components) are always in scope.
|
|
359
|
-
return prior_count === 0 ? true : any_in_scope;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Buffer's current position is the start of an action record. Skip the
|
|
364
|
-
* prior_state section, then copy the action portion (type_id + len + payload)
|
|
365
|
-
* to `out_buffer`. Buffer position advances to the end of the record.
|
|
366
|
-
*
|
|
367
|
-
* @param {BinaryBuffer} buffer source action-log frame
|
|
368
|
-
* @param {BinaryBuffer} out_buffer destination packet
|
|
369
|
-
* @private
|
|
370
|
-
*/
|
|
371
|
-
#copy_action_to(buffer, out_buffer) {
|
|
372
|
-
// Skip prior_state.
|
|
373
|
-
const prior_count = buffer.readUintVar();
|
|
374
|
-
for (let i = 0; i < prior_count; i++) {
|
|
375
|
-
buffer.readUintVar(); // entity_id
|
|
376
|
-
buffer.readUint8(); // component_type_id
|
|
377
|
-
const len = buffer.readUint32();
|
|
378
|
-
buffer.position += len; // skip prior payload
|
|
588
|
+
const prior_len = buffer.readUint32();
|
|
589
|
+
buffer.position += prior_len; // skip prior payload
|
|
379
590
|
}
|
|
380
|
-
|
|
381
|
-
// Copy action portion: type_id + len + payload. The local-only
|
|
382
|
-
// sender_id byte is STRIPPED here — over-the-wire format stays
|
|
383
|
-
// (type_id, len, payload) and the receiver derives sender from
|
|
384
|
-
// the packet's peer_id. Including sender_id on the wire would
|
|
385
|
-
// let a hostile peer impersonate other peers' actions.
|
|
386
|
-
const action_type_id = buffer.readUint8();
|
|
387
|
-
buffer.readUint8(); // sender_id — discarded, not sent on the wire
|
|
388
|
-
const action_payload_len = buffer.readUint32();
|
|
389
|
-
|
|
390
|
-
out_buffer.writeUint8(action_type_id);
|
|
391
|
-
out_buffer.writeUint32(action_payload_len);
|
|
392
|
-
out_buffer.writeBytes(buffer.raw_bytes, buffer.position, action_payload_len);
|
|
393
|
-
buffer.position += action_payload_len;
|
|
591
|
+
return false;
|
|
394
592
|
}
|
|
395
593
|
|
|
396
594
|
/**
|
|
397
595
|
* @private
|
|
398
596
|
*/
|
|
399
|
-
#
|
|
400
|
-
const old = this.#
|
|
597
|
+
#grow_in_scope_actions() {
|
|
598
|
+
const old = this.#in_scope_actions;
|
|
401
599
|
const next = new Int32Array(old.length * 2);
|
|
402
600
|
next.set(old);
|
|
403
|
-
this.#
|
|
601
|
+
this.#in_scope_actions = next;
|
|
404
602
|
}
|
|
405
603
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* uint32: prior_payload_len
|
|
13
13
|
* bytes: prior_payload (adapter.serialize of the component's prior state)
|
|
14
14
|
* uint8: action_type_id
|
|
15
|
-
* uint8: sender_id (
|
|
15
|
+
* uint8: sender_id (provenance of the record; local-only,
|
|
16
16
|
* STRIPPED by Replicator before send)
|
|
17
17
|
* uint32: action_payload_len
|
|
18
18
|
* bytes: action_payload (the action's own serialize output)
|
|
@@ -21,9 +21,21 @@
|
|
|
21
21
|
* Self-describing: records can be skipped without consulting the action registry,
|
|
22
22
|
* which means rewind code can walk records forward to find boundaries, then
|
|
23
23
|
* iterate backward to apply prior states — no need to instantiate any actions.
|
|
24
|
+
* {@link ActionRecordCursor} is that walk; readers use it rather than parsing
|
|
25
|
+
* the layout themselves.
|
|
24
26
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
+
* `sender_id` says where the record came from, which is what a rollback replay
|
|
28
|
+
* needs in order to decide what to do with it:
|
|
29
|
+
*
|
|
30
|
+
* - a peer id `[0, 253]` — arrived from that peer;
|
|
31
|
+
* - `SENDER_LOCAL` (255) — authored on this peer;
|
|
32
|
+
* - `SENDER_DERIVED` (254) — computed by this peer's simulation from the
|
|
33
|
+
* frame's own state. A replay of the frame recomputes these rather than
|
|
34
|
+
* reapplying them (see `ServerAuthoritativeServer.__read_historical`), so
|
|
35
|
+
* the sim's output does not accumulate a copy per replay.
|
|
36
|
+
*
|
|
37
|
+
* It also gives the replay a stable sort key, so multi-client action order is
|
|
38
|
+
* deterministic across peers. It never crosses the wire —
|
|
27
39
|
* {@link Replicator#pack_for_peer} strips it and the receiver derives sender
|
|
28
40
|
* from the inbound packet's peer_id, so a hostile peer cannot impersonate.
|
|
29
41
|
*
|
|
@@ -60,6 +72,14 @@ export class ActionLog {
|
|
|
60
72
|
* Close the currently-open frame.
|
|
61
73
|
*/
|
|
62
74
|
end_frame(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Is a frame open for writing? {@link current_buffer} — and therefore
|
|
77
|
+
* `SimActionExecutor.execute` — throws when it is not, so a caller that can
|
|
78
|
+
* run either inside or outside a frame checks this to pick its path.
|
|
79
|
+
*
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
has_open_frame(): boolean;
|
|
63
83
|
/**
|
|
64
84
|
* Get the buffer for the currently-open frame for direct writing.
|
|
65
85
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionLog.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/ActionLog.js"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"ActionLog.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/sim/ActionLog.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH;IAoBI;;OAEG;IACH,qDAFW;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE,EAIlE;IAED;;;;OAIG;IACH,sCAEC;IAED;;;;OAIG;IACH,mBAFW,MAAM,QAShB;IAED;;OAEG;IACH,kBASC;IAED;;;;;;OAMG;IACH,kBAFa,OAAO,CAInB;IAED;;;;OAIG;IACH,+BAMC;IAED;;;;;OAKG;IACH,iBAHW,MAAM,GACJ,OAAO,CAInB;IAED;;;;;;;;;;;OAWG;IACH,kBAHW,MAAM,gBAKhB;IAED;;;;OAIG;IACH,qBAHW,MAAM,GACJ,MAAM,CAIlB;;CACJ"}
|