@woosh/meep-engine 2.138.0 → 2.138.1
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/build/bundle-worker-image-decoder.js +1 -1
- package/build/bundle-worker-terrain.js +1 -1
- package/package.json +1 -1
- package/src/core/assert.d.ts +6 -0
- package/src/core/assert.d.ts.map +1 -1
- package/src/core/assert.js +16 -3
- package/src/core/binary/half_to_float_uint16.js +1 -1
- package/src/core/binary/to_half_float_uint16.d.ts.map +1 -1
- package/src/core/binary/to_half_float_uint16.js +9 -4
- package/src/core/collection/table/RowFirstTableSpec.js +1 -1
- package/src/core/events/signal/Signal.d.ts.map +1 -1
- package/src/core/events/signal/Signal.js +53 -0
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.d.ts +3 -3
- package/src/core/math/spline/spline3_hermite_intersection_spline3_hermite_2d.js +3 -3
- package/src/engine/Clock.d.ts +2 -2
- package/src/engine/Clock.js +2 -2
- package/src/engine/graphics/ecs/highlight/system/RenderableHighlightSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/system/RenderableHighlightSystem.js +17 -29
- package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/highlight/system/ShadedGeometryHighlightSystem.js +18 -31
- package/src/engine/network/NetworkSession.d.ts +386 -0
- package/src/engine/network/NetworkSession.d.ts.map +1 -0
- package/src/engine/network/NetworkSession.js +1841 -0
- package/src/engine/network/PriorityFetch.d.ts.map +1 -1
- package/src/engine/network/PriorityFetch.js +3 -2
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts +14 -0
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/network/adapters/QuaternionInterpolationAdapter.js +44 -0
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts +18 -0
- package/src/engine/network/adapters/TransformInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/network/adapters/TransformInterpolationAdapter.js +79 -0
- package/src/engine/network/adapters/TransformReplicationAdapter.d.ts +37 -0
- package/src/engine/network/adapters/TransformReplicationAdapter.d.ts.map +1 -0
- package/src/engine/network/adapters/TransformReplicationAdapter.js +87 -0
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts +18 -0
- package/src/engine/network/adapters/Vector3InterpolationAdapter.d.ts.map +1 -0
- package/src/engine/network/adapters/Vector3InterpolationAdapter.js +46 -0
- package/src/engine/network/convertPathToURL.js +107 -107
- package/src/engine/network/core/quantize/quantize_float.d.ts +54 -0
- package/src/engine/network/core/quantize/quantize_float.d.ts.map +1 -0
- package/src/engine/network/core/quantize/quantize_float.js +66 -0
- package/src/engine/network/core/quantize/quantize_position.d.ts +44 -0
- package/src/engine/network/core/quantize/quantize_position.d.ts.map +1 -0
- package/src/engine/network/core/quantize/quantize_position.js +54 -0
- package/src/engine/network/core/sequence/ack_bitfield.d.ts +47 -0
- package/src/engine/network/core/sequence/ack_bitfield.d.ts.map +1 -0
- package/src/engine/network/core/sequence/ack_bitfield.js +77 -0
- package/src/engine/network/core/sequence/seq16.d.ts +53 -0
- package/src/engine/network/core/sequence/seq16.d.ts.map +1 -0
- package/src/engine/network/core/sequence/seq16.js +69 -0
- package/src/engine/network/core/sequence/seq32.d.ts +55 -0
- package/src/engine/network/core/sequence/seq32.d.ts.map +1 -0
- package/src/engine/network/core/sequence/seq32.js +73 -0
- package/src/engine/network/diagnostics/BandwidthMeter.d.ts +76 -0
- package/src/engine/network/diagnostics/BandwidthMeter.d.ts.map +1 -0
- package/src/engine/network/diagnostics/BandwidthMeter.js +155 -0
- package/src/engine/network/diagnostics/ReplayLog.d.ts +74 -0
- package/src/engine/network/diagnostics/ReplayLog.d.ts.map +1 -0
- package/src/engine/network/diagnostics/ReplayLog.js +137 -0
- package/src/engine/network/diagnostics/SyncTest.d.ts +74 -0
- package/src/engine/network/diagnostics/SyncTest.d.ts.map +1 -0
- package/src/engine/network/diagnostics/SyncTest.js +151 -0
- package/src/engine/network/ecs/NetworkSystem.d.ts +57 -0
- package/src/engine/network/ecs/NetworkSystem.d.ts.map +1 -0
- package/src/engine/network/ecs/NetworkSystem.js +84 -0
- package/src/engine/network/ecs/components/NetworkIdentity.d.ts +58 -0
- package/src/engine/network/ecs/components/NetworkIdentity.d.ts.map +1 -0
- package/src/engine/network/ecs/components/NetworkIdentity.js +73 -0
- package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.d.ts +40 -0
- package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.d.ts.map +1 -0
- package/src/engine/network/ecs/serialization/NetworkIdentitySerializationAdapter.js +64 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +389 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -0
- package/src/engine/network/orchestrator/NetworkPeer.js +1107 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +260 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +425 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +217 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +562 -0
- package/src/engine/network/replication/Replicator.d.ts +134 -0
- package/src/engine/network/replication/Replicator.d.ts.map +1 -0
- package/src/engine/network/replication/Replicator.js +334 -0
- package/src/engine/network/replication/ScopeFilter.d.ts +64 -0
- package/src/engine/network/replication/ScopeFilter.d.ts.map +1 -0
- package/src/engine/network/replication/ScopeFilter.js +71 -0
- package/src/engine/network/sim/ActionLog.d.ts +94 -0
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -0
- package/src/engine/network/sim/ActionLog.js +189 -0
- package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts +58 -0
- package/src/engine/network/sim/BinaryInterpolationAdapter.d.ts.map +1 -0
- package/src/engine/network/sim/BinaryInterpolationAdapter.js +56 -0
- package/src/engine/network/sim/InterpolationLog.d.ts +165 -0
- package/src/engine/network/sim/InterpolationLog.d.ts.map +1 -0
- package/src/engine/network/sim/InterpolationLog.js +583 -0
- package/src/engine/network/sim/ReplicatedComponentRegistry.d.ts +59 -0
- package/src/engine/network/sim/ReplicatedComponentRegistry.d.ts.map +1 -0
- package/src/engine/network/sim/ReplicatedComponentRegistry.js +140 -0
- package/src/engine/network/sim/RewindEngine.d.ts +66 -0
- package/src/engine/network/sim/RewindEngine.d.ts.map +1 -0
- package/src/engine/network/sim/RewindEngine.js +182 -0
- package/src/engine/network/sim/SimAction.d.ts +133 -0
- package/src/engine/network/sim/SimAction.d.ts.map +1 -0
- package/src/engine/network/sim/SimAction.js +273 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts +109 -0
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -0
- package/src/engine/network/sim/SimActionExecutor.js +238 -0
- package/src/engine/network/sim/SimActionRegistry.d.ts +60 -0
- package/src/engine/network/sim/SimActionRegistry.d.ts.map +1 -0
- package/src/engine/network/sim/SimActionRegistry.js +128 -0
- package/src/engine/network/sim/SmoothingState.d.ts +87 -0
- package/src/engine/network/sim/SmoothingState.d.ts.map +1 -0
- package/src/engine/network/sim/SmoothingState.js +223 -0
- package/src/engine/network/sim/Snapshotter.d.ts +98 -0
- package/src/engine/network/sim/Snapshotter.d.ts.map +1 -0
- package/src/engine/network/sim/Snapshotter.js +206 -0
- package/src/engine/network/sim/SpeculationLog.d.ts +53 -0
- package/src/engine/network/sim/SpeculationLog.d.ts.map +1 -0
- package/src/engine/network/sim/SpeculationLog.js +84 -0
- package/src/engine/network/state/Baseline.d.ts +48 -0
- package/src/engine/network/state/Baseline.d.ts.map +1 -0
- package/src/engine/network/state/Baseline.js +83 -0
- package/src/engine/network/state/ChangedEntitySet.d.ts +94 -0
- package/src/engine/network/state/ChangedEntitySet.d.ts.map +1 -0
- package/src/engine/network/state/ChangedEntitySet.js +256 -0
- package/src/engine/network/state/InputRing.d.ts +90 -0
- package/src/engine/network/state/InputRing.d.ts.map +1 -0
- package/src/engine/network/state/InputRing.js +173 -0
- package/src/engine/network/state/MutationLedger.d.ts +82 -0
- package/src/engine/network/state/MutationLedger.d.ts.map +1 -0
- package/src/engine/network/state/MutationLedger.js +182 -0
- package/src/engine/network/state/PriorityAccumulator.d.ts +104 -0
- package/src/engine/network/state/PriorityAccumulator.d.ts.map +1 -0
- package/src/engine/network/state/PriorityAccumulator.js +180 -0
- package/src/engine/network/state/ReplicationSlotTable.d.ts +78 -0
- package/src/engine/network/state/ReplicationSlotTable.d.ts.map +1 -0
- package/src/engine/network/state/ReplicationSlotTable.js +211 -0
- package/src/engine/network/time/AdaptiveRenderDelay.d.ts +128 -0
- package/src/engine/network/time/AdaptiveRenderDelay.d.ts.map +1 -0
- package/src/engine/network/time/AdaptiveRenderDelay.js +258 -0
- package/src/engine/network/time/JitterBuffer.d.ts +58 -0
- package/src/engine/network/time/JitterBuffer.d.ts.map +1 -0
- package/src/engine/network/time/JitterBuffer.js +116 -0
- package/src/engine/network/time/TimeDilation.d.ts +49 -0
- package/src/engine/network/time/TimeDilation.d.ts.map +1 -0
- package/src/engine/network/time/TimeDilation.js +62 -0
- package/src/engine/network/time/TimeSync.d.ts +68 -0
- package/src/engine/network/time/TimeSync.d.ts.map +1 -0
- package/src/engine/network/time/TimeSync.js +153 -0
- package/src/engine/network/transport/Channel.d.ts +74 -0
- package/src/engine/network/transport/Channel.d.ts.map +1 -0
- package/src/engine/network/transport/Channel.js +272 -0
- package/src/engine/network/transport/LoopbackTransport.d.ts +59 -0
- package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -0
- package/src/engine/network/transport/LoopbackTransport.js +194 -0
- package/src/engine/network/transport/ReliableCommandPipeline.d.ts +139 -0
- package/src/engine/network/transport/ReliableCommandPipeline.d.ts.map +1 -0
- package/src/engine/network/transport/ReliableCommandPipeline.js +291 -0
- package/src/engine/network/transport/Transport.d.ts +109 -0
- package/src/engine/network/transport/Transport.d.ts.map +1 -0
- package/src/engine/network/transport/Transport.js +119 -0
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts +60 -0
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -0
- package/src/engine/network/transport/adapters/NodeUDPTransport.js +206 -0
- package/src/engine/network/transport/adapters/SimulatedTransport.d.ts +110 -0
- package/src/engine/network/transport/adapters/SimulatedTransport.d.ts.map +1 -0
- package/src/engine/network/transport/adapters/SimulatedTransport.js +252 -0
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts +33 -0
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -0
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +131 -0
- package/src/engine/network/transport/adapters/WebSocketTransport.d.ts +49 -0
- package/src/engine/network/transport/adapters/WebSocketTransport.d.ts.map +1 -0
- package/src/engine/network/transport/adapters/WebSocketTransport.js +180 -0
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts +73 -0
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -0
- package/src/engine/network/transport/adapters/WebTransportTransport.js +210 -0
- package/src/engine/network/transport/fragments/FragmentAssembler.d.ts +104 -0
- package/src/engine/network/transport/fragments/FragmentAssembler.d.ts.map +1 -0
- package/src/engine/network/transport/fragments/FragmentAssembler.js +291 -0
- package/src/engine/network/transport/fragments/FragmentRetention.d.ts +103 -0
- package/src/engine/network/transport/fragments/FragmentRetention.d.ts.map +1 -0
- package/src/engine/network/transport/fragments/FragmentRetention.js +194 -0
- package/src/engine/network/transport/fragments/fragment_send.d.ts +53 -0
- package/src/engine/network/transport/fragments/fragment_send.d.ts.map +1 -0
- package/src/engine/network/transport/fragments/fragment_send.js +147 -0
- package/src/engine/network/transport/fragments/packet_size.d.ts +93 -0
- package/src/engine/network/transport/fragments/packet_size.d.ts.map +1 -0
- package/src/engine/network/transport/fragments/packet_size.js +101 -0
- package/src/engine/network/xhr.js +23 -23
- package/src/engine/simulation/Ticker.d.ts +7 -0
- package/src/engine/simulation/Ticker.d.ts.map +1 -1
- package/src/engine/simulation/Ticker.js +15 -4
- package/src/engine/network/DataChannel.js +0 -1210
- package/src/engine/network/RemoteController.d.ts +0 -23
- package/src/engine/network/RemoteController.d.ts.map +0 -1
- package/src/engine/network/RemoteController.js +0 -114
- package/src/engine/network/remoteEditor.d.ts +0 -2
- package/src/engine/network/remoteEditor.d.ts.map +0 -1
- package/src/engine/network/remoteEditor.js +0 -142
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Aggregates `getStats()` snapshots from one or more named sources (typically
|
|
5
|
+
* a `Transport`, `Channel`, or `NetworkPeer.channel_for(peer)`) and reports
|
|
6
|
+
* recent throughput rates.
|
|
7
|
+
*
|
|
8
|
+
* Per-source stats are queried via the source's `getStats()` method. A "sample"
|
|
9
|
+
* captures the current totals at a wall-clock moment; the meter retains a
|
|
10
|
+
* sliding window of samples and computes bytes-per-second / packets-per-second
|
|
11
|
+
* over the window.
|
|
12
|
+
*
|
|
13
|
+
* No timer of its own — caller invokes {@link sample} per tick (or whenever).
|
|
14
|
+
*
|
|
15
|
+
* @author Alex Goldring
|
|
16
|
+
* @copyright Company Named Limited (c) 2025
|
|
17
|
+
*/
|
|
18
|
+
export class BandwidthMeter {
|
|
19
|
+
#sources;
|
|
20
|
+
#samples;
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {{ window_seconds?: number }} [options]
|
|
25
|
+
*/
|
|
26
|
+
constructor({ window_seconds = 5 } = {}) {
|
|
27
|
+
assert.isNumber(window_seconds, 'window_seconds');
|
|
28
|
+
if (window_seconds <= 0) throw new Error('BandwidthMeter: window_seconds must be > 0');
|
|
29
|
+
|
|
30
|
+
/** @readonly */
|
|
31
|
+
this.window_seconds = window_seconds;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Registered sources. Each is { name: string, source: { getStats() } }.
|
|
35
|
+
* @type {{name: string, source: any}[]}
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
this.#sources = [];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Per-window samples (oldest first). Each entry: { time_ms, totals }.
|
|
42
|
+
* @type {{time_ms: number, totals: {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number}}[]}
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
this.#samples = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} name human-readable label
|
|
50
|
+
* @param {{ getStats(): {bytes_in: number, bytes_out: number, packets_in: number, packets_out: number} }} source
|
|
51
|
+
*/
|
|
52
|
+
add_source(name, source) {
|
|
53
|
+
assert.isString(name, 'name');
|
|
54
|
+
assert.ok(source && typeof source.getStats === 'function', 'source must implement getStats()');
|
|
55
|
+
this.#sources.push({ name, source });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Capture a fresh sample at the given wall-clock time (caller-supplied so
|
|
60
|
+
* tests can use a deterministic clock; in production typically `Date.now()`).
|
|
61
|
+
*
|
|
62
|
+
* @param {number} now_ms
|
|
63
|
+
*/
|
|
64
|
+
sample(now_ms) {
|
|
65
|
+
assert.isNumber(now_ms, 'now_ms');
|
|
66
|
+
const totals = this.#sum_totals();
|
|
67
|
+
this.#samples.push({ time_ms: now_ms, totals });
|
|
68
|
+
|
|
69
|
+
// Drop samples older than the window, but keep one straddling expired
|
|
70
|
+
// sample if it's the only thing standing between us and "only the
|
|
71
|
+
// just-pushed sample is left" (which would silently make __rate return
|
|
72
|
+
// 0 after a long quiet stretch). Once we have 2+ in-window samples the
|
|
73
|
+
// straddle is no longer needed and would bias the rate, so drop it.
|
|
74
|
+
const cutoff = now_ms - this.window_seconds * 1000;
|
|
75
|
+
while (this.#samples.length >= 2 && this.#samples[1].time_ms < cutoff) {
|
|
76
|
+
this.#samples.shift();
|
|
77
|
+
}
|
|
78
|
+
if (this.#samples.length >= 3 && this.#samples[0].time_ms < cutoff) {
|
|
79
|
+
this.#samples.shift();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cumulative totals across all sources, right now (re-queries getStats()).
|
|
85
|
+
* Independent of the sample window.
|
|
86
|
+
*/
|
|
87
|
+
cumulative() {
|
|
88
|
+
return this.#sum_totals();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Bytes-per-second received, averaged over the sample window.
|
|
93
|
+
* Returns 0 if fewer than 2 samples have been taken.
|
|
94
|
+
*/
|
|
95
|
+
rate_bytes_in() {
|
|
96
|
+
return this.#rate('bytes_in');
|
|
97
|
+
}
|
|
98
|
+
rate_bytes_out() {
|
|
99
|
+
return this.#rate('bytes_out');
|
|
100
|
+
}
|
|
101
|
+
rate_packets_in() {
|
|
102
|
+
return this.#rate('packets_in');
|
|
103
|
+
}
|
|
104
|
+
rate_packets_out() {
|
|
105
|
+
return this.#rate('packets_out');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Per-source breakdown of cumulative totals (re-queries getStats()).
|
|
110
|
+
* @returns {Array<{name: string, totals: object}>}
|
|
111
|
+
*/
|
|
112
|
+
per_source() {
|
|
113
|
+
const result = [];
|
|
114
|
+
for (let i = 0; i < this.#sources.length; i++) {
|
|
115
|
+
const s = this.#sources[i];
|
|
116
|
+
result.push({ name: s.name, totals: s.source.getStats() });
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Drop all retained samples. Source registrations are preserved.
|
|
123
|
+
*/
|
|
124
|
+
reset_samples() {
|
|
125
|
+
this.#samples.length = 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @private
|
|
130
|
+
*/
|
|
131
|
+
#sum_totals() {
|
|
132
|
+
let bytes_in = 0, bytes_out = 0, packets_in = 0, packets_out = 0;
|
|
133
|
+
for (let i = 0; i < this.#sources.length; i++) {
|
|
134
|
+
const s = this.#sources[i].source.getStats();
|
|
135
|
+
bytes_in += s.bytes_in;
|
|
136
|
+
bytes_out += s.bytes_out;
|
|
137
|
+
packets_in += s.packets_in;
|
|
138
|
+
packets_out += s.packets_out;
|
|
139
|
+
}
|
|
140
|
+
return { bytes_in, bytes_out, packets_in, packets_out };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @private
|
|
145
|
+
*/
|
|
146
|
+
#rate(key) {
|
|
147
|
+
const n = this.#samples.length;
|
|
148
|
+
if (n < 2) return 0;
|
|
149
|
+
const oldest = this.#samples[0];
|
|
150
|
+
const newest = this.#samples[n - 1];
|
|
151
|
+
const dt_seconds = (newest.time_ms - oldest.time_ms) / 1000;
|
|
152
|
+
if (dt_seconds <= 0) return 0;
|
|
153
|
+
return (newest.totals[key] - oldest.totals[key]) / dt_seconds;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only record of action bytes per frame. Useful for offline replay,
|
|
3
|
+
* bug reproduction, and demos.
|
|
4
|
+
*
|
|
5
|
+
* Each entry stores a copy of the action bytes for one frame. The bytes
|
|
6
|
+
* typically come from the per-frame `ActionLog` buffer (the action portion
|
|
7
|
+
* after `Replicator.pack_for_peer` strips prior-state). The format on the wire
|
|
8
|
+
* is the same one `Replicator.unpack_from_peer` consumes, so a recorded
|
|
9
|
+
* session can be replayed by feeding the bytes back through a peer.
|
|
10
|
+
*
|
|
11
|
+
* Persistence to disk / IndexedDB / network is left to the application —
|
|
12
|
+
* `serialize_to_buffer` produces a single contiguous `BinaryBuffer` snapshot.
|
|
13
|
+
*
|
|
14
|
+
* @author Alex Goldring
|
|
15
|
+
* @copyright Company Named Limited (c) 2025
|
|
16
|
+
*/
|
|
17
|
+
export class ReplayLog {
|
|
18
|
+
/**
|
|
19
|
+
* Build a ReplayLog from a previously-serialized buffer. Reads from the
|
|
20
|
+
* buffer's current position; advances the position past the consumed bytes.
|
|
21
|
+
*
|
|
22
|
+
* @param {BinaryBuffer} buffer
|
|
23
|
+
* @returns {ReplayLog}
|
|
24
|
+
*/
|
|
25
|
+
static deserialize_from_buffer(buffer: BinaryBuffer): ReplayLog;
|
|
26
|
+
/**
|
|
27
|
+
* Append a frame's bytes. The source array is copied; caller may reuse it.
|
|
28
|
+
*
|
|
29
|
+
* @param {number} frame
|
|
30
|
+
* @param {Uint8Array} bytes
|
|
31
|
+
* @param {number} length
|
|
32
|
+
*/
|
|
33
|
+
record(frame: number, bytes: Uint8Array, length: number): void;
|
|
34
|
+
/**
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
size(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Iterate frames in `[start_frame, end_frame]` (inclusive).
|
|
40
|
+
* @param {number} start_frame
|
|
41
|
+
* @param {number} end_frame
|
|
42
|
+
* @param {function(number, Uint8Array): void} callback receives (frame, bytes)
|
|
43
|
+
*/
|
|
44
|
+
for_each_in_range(start_frame: number, end_frame: number, callback: (arg0: number, arg1: Uint8Array) => void): void;
|
|
45
|
+
/**
|
|
46
|
+
* Drop all entries.
|
|
47
|
+
*/
|
|
48
|
+
clear(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Frame number of the first entry, or -1 if empty.
|
|
51
|
+
*/
|
|
52
|
+
earliest_frame(): any;
|
|
53
|
+
/**
|
|
54
|
+
* Frame number of the last entry, or -1 if empty.
|
|
55
|
+
*/
|
|
56
|
+
latest_frame(): any;
|
|
57
|
+
/**
|
|
58
|
+
* Serialize the entire log to a fresh buffer. Format:
|
|
59
|
+
* ```
|
|
60
|
+
* varint: entry_count
|
|
61
|
+
* loop:
|
|
62
|
+
* varint: frame
|
|
63
|
+
* varint: bytes_length
|
|
64
|
+
* bytes: payload
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* @param {BinaryBuffer} [buffer] if provided, written into; otherwise a fresh one is created
|
|
68
|
+
* @returns {BinaryBuffer}
|
|
69
|
+
*/
|
|
70
|
+
serialize_to_buffer(buffer?: BinaryBuffer): BinaryBuffer;
|
|
71
|
+
#private;
|
|
72
|
+
}
|
|
73
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
74
|
+
//# sourceMappingURL=ReplayLog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ReplayLog.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/diagnostics/ReplayLog.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH;IAkGI;;;;;;OAMG;IACH,uCAHW,YAAY,GACV,SAAS,CAarB;IAxGD;;;;;;OAMG;IACH,cAJW,MAAM,SACN,UAAU,UACV,MAAM,QAQhB;IAED;;OAEG;IACH,QAFa,MAAM,CAIlB;IAED;;;;;OAKG;IACH,+BAJW,MAAM,aACN,MAAM,mBACG,MAAM,QAAE,UAAU,KAAG,IAAI,QAY5C;IAED;;OAEG;IACH,cAEC;IAED;;OAEG;IACH,sBAEC;IAED;;OAEG;IACH,oBAEC;IAED;;;;;;;;;;;;OAYG;IACH,6BAHW,YAAY,GACV,YAAY,CAYxB;;CAqBJ;6BAvI4B,sCAAsC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Append-only record of action bytes per frame. Useful for offline replay,
|
|
6
|
+
* bug reproduction, and demos.
|
|
7
|
+
*
|
|
8
|
+
* Each entry stores a copy of the action bytes for one frame. The bytes
|
|
9
|
+
* typically come from the per-frame `ActionLog` buffer (the action portion
|
|
10
|
+
* after `Replicator.pack_for_peer` strips prior-state). The format on the wire
|
|
11
|
+
* is the same one `Replicator.unpack_from_peer` consumes, so a recorded
|
|
12
|
+
* session can be replayed by feeding the bytes back through a peer.
|
|
13
|
+
*
|
|
14
|
+
* Persistence to disk / IndexedDB / network is left to the application —
|
|
15
|
+
* `serialize_to_buffer` produces a single contiguous `BinaryBuffer` snapshot.
|
|
16
|
+
*
|
|
17
|
+
* @author Alex Goldring
|
|
18
|
+
* @copyright Company Named Limited (c) 2025
|
|
19
|
+
*/
|
|
20
|
+
export class ReplayLog {
|
|
21
|
+
#entries;
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
/**
|
|
26
|
+
* @type {{frame: number, bytes: Uint8Array}[]}
|
|
27
|
+
* @private
|
|
28
|
+
*/
|
|
29
|
+
this.#entries = [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Append a frame's bytes. The source array is copied; caller may reuse it.
|
|
34
|
+
*
|
|
35
|
+
* @param {number} frame
|
|
36
|
+
* @param {Uint8Array} bytes
|
|
37
|
+
* @param {number} length
|
|
38
|
+
*/
|
|
39
|
+
record(frame, bytes, length) {
|
|
40
|
+
assert.isNonNegativeInteger(frame, 'frame');
|
|
41
|
+
assert.isNonNegativeInteger(length, 'length');
|
|
42
|
+
const copy = new Uint8Array(length);
|
|
43
|
+
copy.set(bytes.subarray(0, length));
|
|
44
|
+
this.#entries.push({ frame, bytes: copy });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @returns {number}
|
|
49
|
+
*/
|
|
50
|
+
size() {
|
|
51
|
+
return this.#entries.length;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Iterate frames in `[start_frame, end_frame]` (inclusive).
|
|
56
|
+
* @param {number} start_frame
|
|
57
|
+
* @param {number} end_frame
|
|
58
|
+
* @param {function(number, Uint8Array): void} callback receives (frame, bytes)
|
|
59
|
+
*/
|
|
60
|
+
for_each_in_range(start_frame, end_frame, callback) {
|
|
61
|
+
assert.isNonNegativeInteger(start_frame, 'start_frame');
|
|
62
|
+
assert.isNonNegativeInteger(end_frame, 'end_frame');
|
|
63
|
+
assert.isFunction(callback, 'callback');
|
|
64
|
+
for (let i = 0; i < this.#entries.length; i++) {
|
|
65
|
+
const e = this.#entries[i];
|
|
66
|
+
if (e.frame >= start_frame && e.frame <= end_frame) {
|
|
67
|
+
callback(e.frame, e.bytes);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Drop all entries.
|
|
74
|
+
*/
|
|
75
|
+
clear() {
|
|
76
|
+
this.#entries.length = 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Frame number of the first entry, or -1 if empty.
|
|
81
|
+
*/
|
|
82
|
+
earliest_frame() {
|
|
83
|
+
return this.#entries.length === 0 ? -1 : this.#entries[0].frame;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Frame number of the last entry, or -1 if empty.
|
|
88
|
+
*/
|
|
89
|
+
latest_frame() {
|
|
90
|
+
return this.#entries.length === 0 ? -1 : this.#entries[this.#entries.length - 1].frame;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Serialize the entire log to a fresh buffer. Format:
|
|
95
|
+
* ```
|
|
96
|
+
* varint: entry_count
|
|
97
|
+
* loop:
|
|
98
|
+
* varint: frame
|
|
99
|
+
* varint: bytes_length
|
|
100
|
+
* bytes: payload
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @param {BinaryBuffer} [buffer] if provided, written into; otherwise a fresh one is created
|
|
104
|
+
* @returns {BinaryBuffer}
|
|
105
|
+
*/
|
|
106
|
+
serialize_to_buffer(buffer) {
|
|
107
|
+
const buf = buffer === undefined ? new BinaryBuffer() : buffer;
|
|
108
|
+
buf.writeUintVar(this.#entries.length);
|
|
109
|
+
for (let i = 0; i < this.#entries.length; i++) {
|
|
110
|
+
const e = this.#entries[i];
|
|
111
|
+
buf.writeUintVar(e.frame);
|
|
112
|
+
buf.writeUintVar(e.bytes.length);
|
|
113
|
+
buf.writeBytes(e.bytes, 0, e.bytes.length);
|
|
114
|
+
}
|
|
115
|
+
return buf;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Build a ReplayLog from a previously-serialized buffer. Reads from the
|
|
120
|
+
* buffer's current position; advances the position past the consumed bytes.
|
|
121
|
+
*
|
|
122
|
+
* @param {BinaryBuffer} buffer
|
|
123
|
+
* @returns {ReplayLog}
|
|
124
|
+
*/
|
|
125
|
+
static deserialize_from_buffer(buffer) {
|
|
126
|
+
const log = new ReplayLog();
|
|
127
|
+
const count = buffer.readUintVar();
|
|
128
|
+
for (let i = 0; i < count; i++) {
|
|
129
|
+
const frame = buffer.readUintVar();
|
|
130
|
+
const length = buffer.readUintVar();
|
|
131
|
+
const bytes = new Uint8Array(length);
|
|
132
|
+
buffer.readUint8Array(bytes, 0, length);
|
|
133
|
+
log.#entries.push({ frame, bytes });
|
|
134
|
+
}
|
|
135
|
+
return log;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute a 32-bit fingerprint of every replicated component on every entity
|
|
3
|
+
* in the world. Used by `SyncTest` and as a general "did the world state change?"
|
|
4
|
+
* primitive.
|
|
5
|
+
*
|
|
6
|
+
* The hash is FNV-1a — fast, stable, no dependencies. Not cryptographic.
|
|
7
|
+
*
|
|
8
|
+
* Iteration order: entities in the order yielded by `world` (the
|
|
9
|
+
* EntityComponentDataset's iterator), then components in `type_id` order.
|
|
10
|
+
* As long as both sides iterate identically, the fingerprint is comparable.
|
|
11
|
+
*
|
|
12
|
+
* @param {EntityComponentDataset} world
|
|
13
|
+
* @param {ReplicatedComponentRegistry} component_registry
|
|
14
|
+
* @param {BinaryBuffer} [scratch] optional reusable scratch; resets to position 0 each call
|
|
15
|
+
* @returns {number} 32-bit unsigned hash
|
|
16
|
+
*/
|
|
17
|
+
export function fingerprint_world(world: EntityComponentDataset, component_registry: ReplicatedComponentRegistry, scratch?: BinaryBuffer): number;
|
|
18
|
+
/**
|
|
19
|
+
* Diagnostic harness for catching rewind / replay bugs.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* - `harness.checkpoint()` — records the current world fingerprint.
|
|
23
|
+
* - … run tick logic …
|
|
24
|
+
* - `harness.assert_recoverable_to_checkpoint(rewind_engine, current_frame, target_frame)`
|
|
25
|
+
* rewinds and asserts the world's fingerprint matches the checkpoint.
|
|
26
|
+
*
|
|
27
|
+
* For full nondeterminism detection (GGPO-style "save → advance → load →
|
|
28
|
+
* advance → diff"), the application's tick logic must be re-runnable; that
|
|
29
|
+
* coordination is left to the caller. This harness provides the fingerprint +
|
|
30
|
+
* compare primitives and the assertion shape.
|
|
31
|
+
*
|
|
32
|
+
* @author Alex Goldring
|
|
33
|
+
* @copyright Company Named Limited (c) 2025
|
|
34
|
+
*/
|
|
35
|
+
export class SyncTest {
|
|
36
|
+
/**
|
|
37
|
+
* @param {{
|
|
38
|
+
* world: EntityComponentDataset,
|
|
39
|
+
* component_registry: ReplicatedComponentRegistry,
|
|
40
|
+
* }} options
|
|
41
|
+
*/
|
|
42
|
+
constructor({ world, component_registry }: {
|
|
43
|
+
world: EntityComponentDataset;
|
|
44
|
+
component_registry: ReplicatedComponentRegistry;
|
|
45
|
+
});
|
|
46
|
+
/** @type {EntityComponentDataset} */
|
|
47
|
+
world: EntityComponentDataset;
|
|
48
|
+
/** @type {ReplicatedComponentRegistry} */
|
|
49
|
+
component_registry: ReplicatedComponentRegistry;
|
|
50
|
+
/**
|
|
51
|
+
* Capture the current world state as the reference point for the next
|
|
52
|
+
* `assert_recoverable_to_checkpoint` call.
|
|
53
|
+
*/
|
|
54
|
+
checkpoint(): void;
|
|
55
|
+
/**
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
current_fingerprint(): number;
|
|
59
|
+
/**
|
|
60
|
+
* Rewind from `current_frame` back to `target_frame` (typically the frame
|
|
61
|
+
* at which `checkpoint()` was called) and assert the resulting world state
|
|
62
|
+
* matches the checkpoint.
|
|
63
|
+
*
|
|
64
|
+
* Throws on mismatch with a diagnostic message including both fingerprints.
|
|
65
|
+
*
|
|
66
|
+
* @param {RewindEngine} rewind_engine
|
|
67
|
+
* @param {number} current_frame
|
|
68
|
+
* @param {number} target_frame
|
|
69
|
+
*/
|
|
70
|
+
assert_recoverable_to_checkpoint(rewind_engine: RewindEngine, current_frame: number, target_frame: number): void;
|
|
71
|
+
#private;
|
|
72
|
+
}
|
|
73
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
74
|
+
//# sourceMappingURL=SyncTest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SyncTest.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/diagnostics/SyncTest.js"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;GAeG;AACH,4HAHW,YAAY,GACV,MAAM,CAmClB;AAUD;;;;;;;;;;;;;;;;GAgBG;AACH;IAMI;;;;;OAKG;IACH,2CALW;QACV,KAAS,yBAAyB;QAClC,kBAAsB,8BAA8B;KACjD,EAoBH;IAdG,qCAAqC;IACrC,8BAAkB;IAElB,0CAA0C;IAC1C,gDAA4C;IAYhD;;;OAGG;IACH,mBAGC;IAED;;OAEG;IACH,uBAFa,MAAM,CAIlB;IAED;;;;;;;;;;OAUG;IACH,6EAHW,MAAM,gBACN,MAAM,QAahB;;CACJ;6BArJ4B,sCAAsC"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { assert } from "../../../core/assert.js";
|
|
2
|
+
import { BinaryBuffer } from "../../../core/binary/BinaryBuffer.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compute a 32-bit fingerprint of every replicated component on every entity
|
|
6
|
+
* in the world. Used by `SyncTest` and as a general "did the world state change?"
|
|
7
|
+
* primitive.
|
|
8
|
+
*
|
|
9
|
+
* The hash is FNV-1a — fast, stable, no dependencies. Not cryptographic.
|
|
10
|
+
*
|
|
11
|
+
* Iteration order: entities in the order yielded by `world` (the
|
|
12
|
+
* EntityComponentDataset's iterator), then components in `type_id` order.
|
|
13
|
+
* As long as both sides iterate identically, the fingerprint is comparable.
|
|
14
|
+
*
|
|
15
|
+
* @param {EntityComponentDataset} world
|
|
16
|
+
* @param {ReplicatedComponentRegistry} component_registry
|
|
17
|
+
* @param {BinaryBuffer} [scratch] optional reusable scratch; resets to position 0 each call
|
|
18
|
+
* @returns {number} 32-bit unsigned hash
|
|
19
|
+
*/
|
|
20
|
+
export function fingerprint_world(world, component_registry, scratch) {
|
|
21
|
+
assert.ok(world, 'world required');
|
|
22
|
+
assert.ok(component_registry, 'component_registry required');
|
|
23
|
+
|
|
24
|
+
const buffer = scratch === undefined ? new BinaryBuffer() : scratch;
|
|
25
|
+
let h = 0x811c9dc5; // FNV-1a 32-bit offset basis
|
|
26
|
+
|
|
27
|
+
const type_count = component_registry.type_count();
|
|
28
|
+
for (const entity of world) {
|
|
29
|
+
for (let type_id = 0; type_id < type_count; type_id++) {
|
|
30
|
+
const klass = component_registry.class_of(type_id);
|
|
31
|
+
if (!world.hasComponent(entity, klass)) continue;
|
|
32
|
+
|
|
33
|
+
const adapter = component_registry.adapter_for_id(type_id);
|
|
34
|
+
buffer.position = 0;
|
|
35
|
+
adapter.serialize(buffer, world.getComponent(entity, klass));
|
|
36
|
+
|
|
37
|
+
// Mix entity_id and type_id into the hash so structurally-identical
|
|
38
|
+
// components on different entities/types produce different prints.
|
|
39
|
+
h = __fnv_byte(h, entity & 0xFF);
|
|
40
|
+
h = __fnv_byte(h, (entity >> 8) & 0xFF);
|
|
41
|
+
h = __fnv_byte(h, (entity >> 16) & 0xFF);
|
|
42
|
+
h = __fnv_byte(h, (entity >> 24) & 0xFF);
|
|
43
|
+
h = __fnv_byte(h, type_id);
|
|
44
|
+
|
|
45
|
+
const bytes = buffer.raw_bytes;
|
|
46
|
+
const len = buffer.position;
|
|
47
|
+
for (let i = 0; i < len; i++) {
|
|
48
|
+
h = __fnv_byte(h, bytes[i]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return h >>> 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @private
|
|
57
|
+
*/
|
|
58
|
+
function __fnv_byte(h, byte) {
|
|
59
|
+
h ^= byte;
|
|
60
|
+
return Math.imul(h, 0x01000193) >>> 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Diagnostic harness for catching rewind / replay bugs.
|
|
65
|
+
*
|
|
66
|
+
* Usage:
|
|
67
|
+
* - `harness.checkpoint()` — records the current world fingerprint.
|
|
68
|
+
* - … run tick logic …
|
|
69
|
+
* - `harness.assert_recoverable_to_checkpoint(rewind_engine, current_frame, target_frame)`
|
|
70
|
+
* rewinds and asserts the world's fingerprint matches the checkpoint.
|
|
71
|
+
*
|
|
72
|
+
* For full nondeterminism detection (GGPO-style "save → advance → load →
|
|
73
|
+
* advance → diff"), the application's tick logic must be re-runnable; that
|
|
74
|
+
* coordination is left to the caller. This harness provides the fingerprint +
|
|
75
|
+
* compare primitives and the assertion shape.
|
|
76
|
+
*
|
|
77
|
+
* @author Alex Goldring
|
|
78
|
+
* @copyright Company Named Limited (c) 2025
|
|
79
|
+
*/
|
|
80
|
+
export class SyncTest {
|
|
81
|
+
#scratch;
|
|
82
|
+
#checkpoint_hash;
|
|
83
|
+
#has_checkpoint;
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {{
|
|
88
|
+
* world: EntityComponentDataset,
|
|
89
|
+
* component_registry: ReplicatedComponentRegistry,
|
|
90
|
+
* }} options
|
|
91
|
+
*/
|
|
92
|
+
constructor({ world, component_registry }) {
|
|
93
|
+
assert.ok(world, 'world required');
|
|
94
|
+
assert.ok(component_registry, 'component_registry required');
|
|
95
|
+
|
|
96
|
+
/** @type {EntityComponentDataset} */
|
|
97
|
+
this.world = world;
|
|
98
|
+
|
|
99
|
+
/** @type {ReplicatedComponentRegistry} */
|
|
100
|
+
this.component_registry = component_registry;
|
|
101
|
+
|
|
102
|
+
/** Reused scratch for fingerprint serialization. */
|
|
103
|
+
this.#scratch = new BinaryBuffer();
|
|
104
|
+
|
|
105
|
+
/** @type {number} @private */
|
|
106
|
+
this.#checkpoint_hash = 0;
|
|
107
|
+
|
|
108
|
+
/** @type {boolean} @private */
|
|
109
|
+
this.#has_checkpoint = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Capture the current world state as the reference point for the next
|
|
114
|
+
* `assert_recoverable_to_checkpoint` call.
|
|
115
|
+
*/
|
|
116
|
+
checkpoint() {
|
|
117
|
+
this.#checkpoint_hash = fingerprint_world(this.world, this.component_registry, this.#scratch);
|
|
118
|
+
this.#has_checkpoint = true;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @returns {number}
|
|
123
|
+
*/
|
|
124
|
+
current_fingerprint() {
|
|
125
|
+
return fingerprint_world(this.world, this.component_registry, this.#scratch);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Rewind from `current_frame` back to `target_frame` (typically the frame
|
|
130
|
+
* at which `checkpoint()` was called) and assert the resulting world state
|
|
131
|
+
* matches the checkpoint.
|
|
132
|
+
*
|
|
133
|
+
* Throws on mismatch with a diagnostic message including both fingerprints.
|
|
134
|
+
*
|
|
135
|
+
* @param {RewindEngine} rewind_engine
|
|
136
|
+
* @param {number} current_frame
|
|
137
|
+
* @param {number} target_frame
|
|
138
|
+
*/
|
|
139
|
+
assert_recoverable_to_checkpoint(rewind_engine, current_frame, target_frame) {
|
|
140
|
+
assert.ok(this.#has_checkpoint, 'SyncTest: no checkpoint() taken before assertion');
|
|
141
|
+
rewind_engine.rewind_to(current_frame, target_frame);
|
|
142
|
+
const after = fingerprint_world(this.world, this.component_registry, this.#scratch);
|
|
143
|
+
if (after !== this.#checkpoint_hash) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`SyncTest: world fingerprint after rewind (${after.toString(16)}) ` +
|
|
146
|
+
`does not match checkpoint (${this.#checkpoint_hash.toString(16)}) ` +
|
|
147
|
+
`— rewind is non-deterministic or did not fully restore state`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ECS system that owns the per-entity replication lifecycle.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - When an entity gains a {@link NetworkIdentity}, allocate a `network_id`
|
|
6
|
+
* in the peer's slot table (or honour an explicit pre-set one — used by
|
|
7
|
+
* `Snapshotter` and incoming-packet flows).
|
|
8
|
+
* - When an entity loses its `NetworkIdentity` (or is destroyed), free the
|
|
9
|
+
* slot.
|
|
10
|
+
*
|
|
11
|
+
* **Not** responsible for the per-tick `begin_tick` / `end_tick` cadence —
|
|
12
|
+
* the engine's `EntityManager` doesn't expose pre/post-simulate hooks, so the
|
|
13
|
+
* application code wraps its `engine.simulate(dt)` call:
|
|
14
|
+
*
|
|
15
|
+
* ```js
|
|
16
|
+
* peer.begin_tick(frame_number);
|
|
17
|
+
* engine.simulate(dt);
|
|
18
|
+
* peer.end_tick();
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Add this system to the engine once per `NetworkPeer` instance:
|
|
22
|
+
*
|
|
23
|
+
* ```js
|
|
24
|
+
* const peer = new NetworkPeer({ ... });
|
|
25
|
+
* const network_system = new NetworkSystem(peer);
|
|
26
|
+
* engine.entityManager.addSystem(network_system);
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @author Alex Goldring
|
|
30
|
+
* @copyright Company Named Limited (c) 2025
|
|
31
|
+
*/
|
|
32
|
+
export class NetworkSystem extends System<any, any, any, any, any> {
|
|
33
|
+
/**
|
|
34
|
+
* @param {NetworkPeer} peer
|
|
35
|
+
*/
|
|
36
|
+
constructor(peer: NetworkPeer);
|
|
37
|
+
dependencies: (typeof NetworkIdentity)[];
|
|
38
|
+
components_used: ResourceAccessSpecification<typeof NetworkIdentity>[];
|
|
39
|
+
/**
|
|
40
|
+
* @type {NetworkPeer}
|
|
41
|
+
*/
|
|
42
|
+
peer: NetworkPeer;
|
|
43
|
+
/**
|
|
44
|
+
* @param {NetworkIdentity} identity
|
|
45
|
+
* @param {number} entity
|
|
46
|
+
*/
|
|
47
|
+
link(identity: NetworkIdentity, entity: number): void;
|
|
48
|
+
/**
|
|
49
|
+
* @param {NetworkIdentity} identity
|
|
50
|
+
* @param {number} entity
|
|
51
|
+
*/
|
|
52
|
+
unlink(identity: NetworkIdentity, entity: number): void;
|
|
53
|
+
}
|
|
54
|
+
import { System } from "../../ecs/System.js";
|
|
55
|
+
import { NetworkIdentity } from "./components/NetworkIdentity.js";
|
|
56
|
+
import { ResourceAccessSpecification } from "../../../core/model/ResourceAccessSpecification.js";
|
|
57
|
+
//# sourceMappingURL=NetworkSystem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NetworkSystem.d.ts","sourceRoot":"","sources":["../../../../../src/engine/network/ecs/NetworkSystem.js"],"names":[],"mappings":"AAMA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH;IAQI;;OAEG;IACH,+BAQC;IAjBD,yCAAiC;IAEjC,uEAEE;IASE;;OAEG;IACH,kBAAgB;IAGpB;;;OAGG;IACH,eAHW,eAAe,UACf,MAAM,QAWhB;IAED;;;OAGG;IACH,iBAHW,eAAe,UACf,MAAM,QAOhB;CACJ;uBAhFsB,qBAAqB;gCACZ,iCAAiC;4CAFrB,oDAAoD"}
|