@woosh/meep-engine 3.46.0 → 3.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor/actions/concrete/TransformModifyAction.d.ts.map +1 -1
- package/editor/actions/concrete/TransformModifyAction.js +10 -2
- package/editor/ecs/component/FieldDescriptor.js +15 -0
- package/editor/ecs/component/createObjectEditor.js +41 -12
- package/editor/ecs/component/editors/ecs/Transform64Editor.d.ts.map +1 -1
- package/editor/ecs/component/editors/ecs/Transform64Editor.js +77 -70
- package/editor/tools/TransformToolV2.d.ts.map +1 -1
- package/editor/tools/TransformToolV2.js +4 -0
- package/editor/tools/v2/TransformControls.d.ts.map +1 -1
- package/editor/tools/v2/TransformControls.js +7 -0
- package/editor/view/ecs/EntityEditor.d.ts.map +1 -1
- package/editor/view/ecs/EntityEditor.js +4 -2
- package/package.json +1 -1
- package/src/core/collection/array/array_insert_at.d.ts +16 -0
- package/src/core/collection/array/array_insert_at.d.ts.map +1 -0
- package/src/core/collection/array/array_insert_at.js +32 -0
- package/src/core/collection/array/array_remove_at.d.ts +15 -0
- package/src/core/collection/array/array_remove_at.d.ts.map +1 -0
- package/src/core/collection/array/array_remove_at.js +30 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts +37 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.d.ts.map +1 -0
- package/src/core/geom/3d/quaternion/quat3_look_rotation.js +172 -0
- package/src/core/geom/Quaternion.d.ts +1 -0
- package/src/core/geom/Quaternion.d.ts.map +1 -1
- package/src/core/geom/Quaternion.js +12 -48
- package/src/engine/Engine.d.ts.map +1 -1
- package/src/engine/Engine.js +9 -4
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.d.ts.map +1 -1
- package/src/engine/ecs/storage/populateEngineSerializationRegistry.js +10 -0
- package/src/engine/ecs/transform/t64_look_rotation.d.ts +4 -4
- package/src/engine/ecs/transform/t64_look_rotation.d.ts.map +1 -1
- package/src/engine/ecs/transform/t64_look_rotation.js +7 -18
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.d.ts.map +1 -1
- package/src/engine/graphics/ecs/camera/topdown/TopDownCameraControllerSystem.js +30 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts +6 -0
- package/src/engine/graphics3/GPUParticleEmitterSystem.d.ts.map +1 -1
- package/src/engine/graphics3/GPUParticleEmitterSystem.js +72 -29
- package/src/engine/input/devices/KeyboardDevice.d.ts +1 -0
- package/src/engine/input/devices/KeyboardDevice.d.ts.map +1 -1
- package/src/engine/input/devices/KeyboardDevice.js +7 -6
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.d.ts.map +1 -1
- package/src/engine/navigation/ecs/path_following/PathFollowingSystem.js +15 -2
- package/src/engine/network/CONGESTION_CONTROL.md +9 -9
- package/src/engine/network/MESSAGE_TRANSPORT.md +364 -0
- package/src/engine/network/NetworkSession.d.ts +31 -11
- package/src/engine/network/NetworkSession.d.ts.map +1 -1
- package/src/engine/network/NetworkSession.js +453 -286
- package/src/engine/network/README.md +77 -27
- package/src/engine/network/SMOOTHNESS_DESIGN.md +13 -5
- package/src/engine/network/diagnostics/BandwidthMeter.d.ts +1 -1
- package/src/engine/network/diagnostics/BandwidthMeter.js +1 -1
- package/src/engine/network/orchestrator/HostJoin.d.ts +111 -0
- package/src/engine/network/orchestrator/HostJoin.d.ts.map +1 -0
- package/src/engine/network/orchestrator/HostJoin.js +271 -0
- package/src/engine/network/orchestrator/NetworkPeer.d.ts +151 -112
- package/src/engine/network/orchestrator/NetworkPeer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/NetworkPeer.js +450 -499
- package/src/engine/network/orchestrator/PendingSlices.d.ts +30 -0
- package/src/engine/network/orchestrator/PendingSlices.d.ts.map +1 -0
- package/src/engine/network/orchestrator/PendingSlices.js +83 -0
- package/src/engine/network/orchestrator/ReliableFrames.d.ts +34 -0
- package/src/engine/network/orchestrator/ReliableFrames.d.ts.map +1 -0
- package/src/engine/network/orchestrator/ReliableFrames.js +96 -0
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts +77 -10
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeClient.js +236 -61
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts +20 -6
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.d.ts.map +1 -1
- package/src/engine/network/orchestrator/ServerAuthoritativeServer.js +33 -8
- package/src/engine/network/replication/Replicator.d.ts.map +1 -1
- package/src/engine/network/replication/Replicator.js +7 -1
- package/src/engine/network/sim/ActionLog.d.ts +32 -5
- package/src/engine/network/sim/ActionLog.d.ts.map +1 -1
- package/src/engine/network/sim/ActionLog.js +47 -5
- package/src/engine/network/sim/SimActionExecutor.d.ts +42 -2
- package/src/engine/network/sim/SimActionExecutor.d.ts.map +1 -1
- package/src/engine/network/sim/SimActionExecutor.js +100 -4
- package/src/engine/network/sim/Snapshotter.d.ts +12 -0
- package/src/engine/network/sim/Snapshotter.d.ts.map +1 -1
- package/src/engine/network/sim/Snapshotter.js +43 -0
- package/src/engine/network/state/Baseline.d.ts +1 -1
- package/src/engine/network/state/Baseline.js +1 -1
- package/src/engine/network/state/FrameBufferRing.d.ts +25 -6
- package/src/engine/network/state/FrameBufferRing.d.ts.map +1 -1
- package/src/engine/network/state/FrameBufferRing.js +51 -6
- package/src/engine/network/state/InputRing.d.ts +4 -0
- package/src/engine/network/state/InputRing.d.ts.map +1 -1
- package/src/engine/network/state/InputRing.js +7 -0
- package/src/engine/network/transport/DeferredTransport.d.ts.map +1 -1
- package/src/engine/network/transport/DeferredTransport.js +1 -0
- package/src/engine/network/transport/LoopbackTransport.d.ts.map +1 -1
- package/src/engine/network/transport/LoopbackTransport.js +4 -2
- package/src/engine/network/transport/Transport.d.ts +7 -0
- package/src/engine/network/transport/Transport.d.ts.map +1 -1
- package/src/engine/network/transport/Transport.js +8 -0
- package/src/engine/network/transport/adapters/NodeUDPTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/NodeUDPTransport.js +20 -5
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebRTCDataChannelTransport.js +8 -0
- package/src/engine/network/transport/adapters/WebTransportTransport.d.ts.map +1 -1
- package/src/engine/network/transport/adapters/WebTransportTransport.js +6 -0
- package/src/engine/network/transport/connection/Connection.d.ts +191 -0
- package/src/engine/network/transport/connection/Connection.d.ts.map +1 -0
- package/src/engine/network/transport/connection/Connection.js +1262 -0
- package/src/engine/network/transport/connection/RangeSet.d.ts +94 -0
- package/src/engine/network/transport/connection/RangeSet.d.ts.map +1 -0
- package/src/engine/network/transport/connection/RangeSet.js +260 -0
- package/src/engine/network/transport/connection/RxStream.d.ts +48 -0
- package/src/engine/network/transport/connection/RxStream.d.ts.map +1 -0
- package/src/engine/network/transport/connection/RxStream.js +211 -0
- package/src/engine/network/transport/connection/TxStream.d.ts +90 -0
- package/src/engine/network/transport/connection/TxStream.d.ts.map +1 -0
- package/src/engine/network/transport/connection/TxStream.js +243 -0
- package/src/engine/network/transport/connection/bytes_copy.d.ts +17 -0
- package/src/engine/network/transport/connection/bytes_copy.d.ts.map +1 -0
- package/src/engine/network/transport/connection/bytes_copy.js +34 -0
- package/src/engine/network/transport/connection/varint.d.ts +46 -0
- package/src/engine/network/transport/connection/varint.d.ts.map +1 -0
- package/src/engine/network/transport/connection/varint.js +107 -0
- package/src/engine/physics/fluid/ecs/FluidObstacleSystem.d.ts +4 -4
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts +38 -7
- package/src/engine/sound/ecs/audio/AudioEmitter.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitter.js +35 -7
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationAdapter.js +5 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts +18 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSerializationUpgrader_0_1.js +48 -0
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts +12 -2
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/AudioEmitterSystem.js +173 -9
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts +9 -3
- package/src/engine/sound/ecs/audio/LiveEmitterSet.d.ts.map +1 -1
- package/src/engine/sound/ecs/audio/LiveEmitterSet.js +28 -8
- package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts +8 -0
- package/src/engine/sound/ecs/trigger/AudioEventTrigger.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationAdapter.js +5 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts +18 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSerializationUpgrader_0_1.js +51 -0
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.d.ts.map +1 -1
- package/src/engine/sound/ecs/trigger/AudioEventTriggerSystem.js +15 -1
- package/src/engine/sound/sopra/README.md +768 -644
- package/src/engine/sound/sopra/SopraEngine.d.ts +104 -8
- package/src/engine/sound/sopra/SopraEngine.d.ts.map +1 -1
- package/src/engine/sound/sopra/SopraEngine.js +327 -12
- package/src/engine/sound/sopra/definition/EventDescription.d.ts +28 -2
- package/src/engine/sound/sopra/definition/EventDescription.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/EventDescription.js +55 -5
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts +10 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationAdapter.js +77 -55
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts +31 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.d.ts.map +1 -0
- package/src/engine/sound/sopra/definition/EventDescriptionSerializationUpgrader_0_1.js +75 -0
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts +38 -9
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/AbstractAudioEffect.js +34 -5
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts +7 -2
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/CompressorEffect.js +137 -122
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts +3 -1
- package/src/engine/sound/sopra/definition/effect/EqEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/EqEffect.js +146 -131
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts +1 -0
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.d.ts.map +1 -1
- package/src/engine/sound/sopra/definition/effect/ReverbEffect.js +6 -1
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts +44 -2
- package/src/engine/sound/sopra/runtime/BusGraph.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/BusGraph.js +83 -43
- package/src/engine/sound/sopra/runtime/EffectChain.d.ts +96 -0
- package/src/engine/sound/sopra/runtime/EffectChain.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/EffectChain.js +286 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts +123 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.d.ts.map +1 -0
- package/src/engine/sound/sopra/runtime/EffectParamOverrides.js +320 -0
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts +55 -5
- package/src/engine/sound/sopra/runtime/EventInstance.d.ts.map +1 -1
- package/src/engine/sound/sopra/runtime/EventInstance.js +201 -26
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts +2 -1
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.d.ts.map +1 -1
- package/src/engine/sound/sopra/serialization/populateSopraSerializationRegistry.js +6 -1
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts +15 -0
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.d.ts.map +1 -1
- package/src/engine/sound/sopra/serialization/sopraSerializationHarness.js +32 -0
- package/src/engine/sound/sopra/util/MockAudioContext.d.ts.map +1 -1
- package/src/engine/sound/sopra/util/MockAudioContext.js +3 -0
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts +13 -3
- package/src/engine/sound/sopra/util/fadeOutAndStop.d.ts.map +1 -1
- package/src/engine/sound/sopra/util/fadeOutAndStop.js +25 -13
- package/src/shade/RENDERER_CONTRACT.md +6 -3
- package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts +24 -0
- package/src/shade/device/SHADE_DEVICE_FLOOR.d.ts.map +1 -0
- package/src/shade/device/SHADE_DEVICE_FLOOR.js +34 -0
- package/src/shade/device/ShadeDeviceFailure.d.ts +12 -5
- package/src/shade/device/ShadeDeviceFailure.d.ts.map +1 -1
- package/src/shade/device/ShadeDeviceFailure.js +17 -8
- package/src/shade/device/ShadeDeviceFailureReason.d.ts +1 -1
- package/src/shade/device/ShadeDeviceFailureReason.js +2 -2
- package/src/shade/device/shade_device_floor_unmet.d.ts +13 -0
- package/src/shade/device/shade_device_floor_unmet.d.ts.map +1 -0
- package/src/shade/device/shade_device_floor_unmet.js +32 -0
- package/src/shade/renderer/Renderer.d.ts +2 -0
- package/src/shade/renderer/Renderer.d.ts.map +1 -1
- package/src/shade/renderer/Renderer.js +29 -55
- package/src/shade/renderer/buffer/GPUBufferAllocator.d.ts.map +1 -1
- package/src/shade/renderer/buffer/GPUBufferAllocator.js +9 -0
- package/src/shade/renderer/buffer/buffer_pool_ops.d.ts +21 -12
- package/src/shade/renderer/buffer/buffer_pool_ops.d.ts.map +1 -1
- package/src/shade/renderer/buffer/buffer_pool_ops.js +18 -19
- package/src/shade/renderer/gpu_primitive/test/gpu_test.d.ts.map +1 -1
- package/src/shade/renderer/gpu_primitive/test/gpu_test.js +8 -11
- package/src/shade/renderer/loader/gltf/tiny-gltf.d.ts +1 -1
- package/src/shade/renderer/pool/TransientResourcePool.d.ts +24 -0
- package/src/shade/renderer/pool/TransientResourcePool.d.ts.map +1 -1
- package/src/shade/renderer/pool/TransientResourcePool.js +41 -10
- package/src/shade/renderer/shader/ComputeShader.d.ts.map +1 -1
- package/src/shade/renderer/shader/ComputeShader.js +18 -2
- package/src/shade/renderer/shader/ImageShader.d.ts.map +1 -1
- package/src/shade/renderer/shader/ImageShader.js +230 -218
- package/src/view/graphics/GraphicsContextFailureView.d.ts +1 -2
- package/src/view/graphics/GraphicsContextFailureView.d.ts.map +1 -1
- package/src/view/graphics/GraphicsContextFailureView.js +1 -2
- package/src/engine/network/transport/Channel.js +0 -336
- package/src/engine/network/transport/ReliableCommandPipeline.js +0 -335
- package/src/engine/network/transport/fragments/FragmentAssembler.js +0 -350
- package/src/engine/network/transport/fragments/FragmentRetention.js +0 -191
- package/src/engine/network/transport/fragments/fragment_send.js +0 -157
- package/src/engine/network/transport/fragments/packet_size.js +0 -107
|
@@ -9,7 +9,8 @@ import { NetworkIdentity } from "./ecs/components/NetworkIdentity.js";
|
|
|
9
9
|
import { make_owner_authorization } from "./ecs/owner_authorization.js";
|
|
10
10
|
import { NetworkSystem } from "./ecs/NetworkSystem.js";
|
|
11
11
|
import { NetworkIdentitySerializationAdapter, } from "./ecs/serialization/NetworkIdentitySerializationAdapter.js";
|
|
12
|
-
import { NetworkPeer, ResumeRejectReason, } from "./orchestrator/NetworkPeer.js";
|
|
12
|
+
import { InitialSyncPage, NetworkPeer, ResumeRejectReason, } from "./orchestrator/NetworkPeer.js";
|
|
13
|
+
import { HostJoin, JoinProgress } from "./orchestrator/HostJoin.js";
|
|
13
14
|
import { ServerAuthoritativeClient } from "./orchestrator/ServerAuthoritativeClient.js";
|
|
14
15
|
import { LateInput, ServerAuthoritativeServer } from "./orchestrator/ServerAuthoritativeServer.js";
|
|
15
16
|
import { OwnerAwareScope } from "./replication/ScopeFilter.js";
|
|
@@ -19,7 +20,6 @@ import { Interpolated, INTERPOLATION_SOURCE_LOCAL } from "../interpolation/Inter
|
|
|
19
20
|
import { InterpolationLog } from "../interpolation/InterpolationLog.js";
|
|
20
21
|
import { InterpolationSystem } from "../interpolation/InterpolationSystem.js";
|
|
21
22
|
import { SimAction } from "./sim/SimAction.js";
|
|
22
|
-
import { snapshotter_emit } from "./sim/Snapshotter.js";
|
|
23
23
|
import { PredictionHistory } from "./state/PredictionHistory.js";
|
|
24
24
|
import { AdaptiveRenderDelay } from "./time/AdaptiveRenderDelay.js";
|
|
25
25
|
import { RenderPlayout } from "./time/RenderPlayout.js";
|
|
@@ -143,13 +143,15 @@ const ReconnectState = Object.freeze({
|
|
|
143
143
|
/**
|
|
144
144
|
* Client-side: how the host has answered the transport binding in use. A new binding does not know
|
|
145
145
|
* which world the client is in until the host says: the one it has been predicting on, which the
|
|
146
|
-
* host resumes (RESUME_ACCEPT), or the host's own, sent as a snapshot (INITIAL_SYNC). The
|
|
147
|
-
* answer is the answer; the host's handshake arriving after it is stale.
|
|
146
|
+
* host resumes (RESUME_ACCEPT), or the host's own, sent as pages of a snapshot (INITIAL_SYNC). The
|
|
147
|
+
* first answer is the answer; the host's handshake arriving after it is stale. The first page is
|
|
148
|
+
* the answer (Syncing); the FINAL page completes it (Synced).
|
|
148
149
|
* @readonly @enum {string}
|
|
149
150
|
*/
|
|
150
151
|
const BindingAnswer = Object.freeze({
|
|
151
152
|
Pending: 'pending',
|
|
152
153
|
Resumed: 'resumed',
|
|
154
|
+
Syncing: 'syncing',
|
|
153
155
|
Synced: 'synced',
|
|
154
156
|
});
|
|
155
157
|
|
|
@@ -229,6 +231,18 @@ function assert_copies_itself(method, name, ComponentClass) {
|
|
|
229
231
|
* @param {number} length
|
|
230
232
|
* @returns {boolean}
|
|
231
233
|
*/
|
|
234
|
+
/**
|
|
235
|
+
* Close a transport the session is giving up on. Best effort: one already closed, or failing to
|
|
236
|
+
* close, changes nothing about what follows.
|
|
237
|
+
* @param {{disconnect?: function(): void}} transport
|
|
238
|
+
*/
|
|
239
|
+
function close_transport(transport) {
|
|
240
|
+
try {
|
|
241
|
+
transport.disconnect();
|
|
242
|
+
} catch (_) { /* swallow */
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
232
246
|
function bytes_equal(a, b, length) {
|
|
233
247
|
for (let i = 0; i < length; i++) {
|
|
234
248
|
if (a[i] !== b[i]) return false;
|
|
@@ -255,8 +269,17 @@ function bytes_equal_lengths(a, a_length, b, b_length) {
|
|
|
255
269
|
*
|
|
256
270
|
* Replication is component-driven: attaching a {@link NetworkIdentity}
|
|
257
271
|
* to an entity registers it with the session via an `EntityObserver`.
|
|
258
|
-
*
|
|
259
|
-
*
|
|
272
|
+
*
|
|
273
|
+
* **What travels is what goes through the action executor**, and nothing
|
|
274
|
+
* else: actions (`send`, a client's input), and direct writes their writer
|
|
275
|
+
* reports — `dataset.sendEvent(entity, "net_mutate_component", payload)`.
|
|
276
|
+
* The session never looks for changes by itself. That is what keeps a tick's
|
|
277
|
+
* cost in what changed and what is owned, never in the size of the world;
|
|
278
|
+
* the stack is built for worlds of millions of networked objects. A
|
|
279
|
+
* replicated component written directly and not reported stays where it was
|
|
280
|
+
* written. Ownership is set when an identity attaches, or by the join, and
|
|
281
|
+
* `owner_peer_id` / `network_id` are not written afterwards. The README's
|
|
282
|
+
* "What the session assumes" has the whole contract and its measured costs.
|
|
260
283
|
*
|
|
261
284
|
* **The session runs on the engine's clock.** The `EntityManager`'s fixed
|
|
262
285
|
* step is the network tick: each step opens a network frame before any
|
|
@@ -329,6 +352,11 @@ export class NetworkSession {
|
|
|
329
352
|
#client = null;
|
|
330
353
|
/** @private @type {NetworkPeer|null} */
|
|
331
354
|
#peer = null;
|
|
355
|
+
/**
|
|
356
|
+
* The session's place in the engine loop, added by `start()` and removed by `stop()`.
|
|
357
|
+
* @private @type {NetworkSystem|null}
|
|
358
|
+
*/
|
|
359
|
+
#network_system = null;
|
|
332
360
|
/** @private @type {EntityObserver|null} */
|
|
333
361
|
#identity_observer = null;
|
|
334
362
|
/** @private @type {InterpolationLog|null} */
|
|
@@ -352,6 +380,22 @@ export class NetworkSession {
|
|
|
352
380
|
*/
|
|
353
381
|
#remote_entities = new Set();
|
|
354
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Networked entities by owner: peer id → its entities. What a tick does per owned entity —
|
|
385
|
+
* the host's AUTH_STATE to each peer, the client's record of what it predicted — walks this,
|
|
386
|
+
* never every networked entity: the cost of a tick is in what is owned and what changed, not
|
|
387
|
+
* in the size of the world. Kept exact by attach, detach and {@link #apply_ownership}, the
|
|
388
|
+
* only places ownership is set.
|
|
389
|
+
* @private @type {Map<number, Set<number>>}
|
|
390
|
+
*/
|
|
391
|
+
#owned_by = new Map();
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Owner each networked entity is filed under in {@link #owned_by}.
|
|
395
|
+
* @private @type {Map<number, number>}
|
|
396
|
+
*/
|
|
397
|
+
#owner_of = new Map();
|
|
398
|
+
|
|
355
399
|
// ---- Render-frame interpolation state ----
|
|
356
400
|
|
|
357
401
|
/**
|
|
@@ -416,26 +460,11 @@ export class NetworkSession {
|
|
|
416
460
|
*/
|
|
417
461
|
#inbound = new Map();
|
|
418
462
|
|
|
419
|
-
// ---- Host
|
|
420
|
-
|
|
421
|
-
/** @private @type {boolean} */
|
|
422
|
-
#publish_changes;
|
|
463
|
+
// ---- Host ----
|
|
423
464
|
|
|
424
465
|
/** @private @type {string} */
|
|
425
466
|
#late_input;
|
|
426
467
|
|
|
427
|
-
/**
|
|
428
|
-
* What the peers hold for each networked entity, per replicated component type id, as the
|
|
429
|
-
* replication adapter's bytes — so "changed" means "changed on the wire". Updated by every
|
|
430
|
-
* record the host publishes and by every action it executes (the peers apply the action and
|
|
431
|
-
* arrive at the same value), and compared with the live state at the end of each step.
|
|
432
|
-
* @private @type {Map<number, Array<{bytes: Uint8Array, length: number}|null>>}
|
|
433
|
-
*/
|
|
434
|
-
#published = new Map();
|
|
435
|
-
|
|
436
|
-
/** @private @type {BinaryBuffer} */
|
|
437
|
-
#scratch_publish_buf = new BinaryBuffer();
|
|
438
|
-
|
|
439
468
|
// ---- Client: reconciliation by comparison ----
|
|
440
469
|
|
|
441
470
|
/** @private @type {string} */
|
|
@@ -506,19 +535,21 @@ export class NetworkSession {
|
|
|
506
535
|
#connected_peers = new Set();
|
|
507
536
|
|
|
508
537
|
/**
|
|
509
|
-
* Host-only: peer_id →
|
|
510
|
-
* `#on_host_tick_complete
|
|
511
|
-
* for
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
538
|
+
* Host-only: peer_id → its join, while one is going on. Advanced in
|
|
539
|
+
* `#on_host_tick_complete`; created only by {@link #queue_initial_sync}, so never on the
|
|
540
|
+
* peer's say-so: a join the peer could ask for is one it could keep asking for. The pages ride
|
|
541
|
+
* the connection's BULK lane, which re-sends what is lost; what a join costs beyond the world
|
|
542
|
+
* once is the entities that changed while it was going out, for at most
|
|
543
|
+
* {@link JOIN_MAX_ROUNDS} rounds.
|
|
544
|
+
* @private @type {Map<number, HostJoin>}
|
|
545
|
+
*/
|
|
546
|
+
#joins = new Map();
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Host-only: scratch every join builds its pages in.
|
|
550
|
+
* @private @type {BinaryBuffer}
|
|
520
551
|
*/
|
|
521
|
-
#
|
|
552
|
+
#page_buffer = new BinaryBuffer();
|
|
522
553
|
|
|
523
554
|
/**
|
|
524
555
|
* Host-only: peers whose current connection has been given its
|
|
@@ -541,6 +572,13 @@ export class NetworkSession {
|
|
|
541
572
|
/** @private @type {boolean} */
|
|
542
573
|
#started = false;
|
|
543
574
|
|
|
575
|
+
/**
|
|
576
|
+
* {@link stop} has run. A session is one match: what it knew of its peers outlives the stop, so
|
|
577
|
+
* it does not start again.
|
|
578
|
+
* @private @type {boolean}
|
|
579
|
+
*/
|
|
580
|
+
#stopped = false;
|
|
581
|
+
|
|
544
582
|
// ---- Host-side: per-peer session token + grace state ----
|
|
545
583
|
|
|
546
584
|
/**
|
|
@@ -613,14 +651,36 @@ export class NetworkSession {
|
|
|
613
651
|
/**
|
|
614
652
|
* Client-side: how the host has answered the binding the last `connect()`
|
|
615
653
|
* made. Once it has, what the binding carries about the handshake is news
|
|
616
|
-
* no longer:
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* after the resync it asks for.
|
|
654
|
+
* no longer: a host that has finished with our old session takes the new
|
|
655
|
+
* transport for a fresh peer and starts its join before reading the
|
|
656
|
+
* RESUME_HELLO the transport carried, so the RESUME_REJECT it answers that
|
|
657
|
+
* with arrives after the resync it asks for.
|
|
621
658
|
* @private @type {BindingAnswer|string}
|
|
622
659
|
*/
|
|
623
660
|
#binding_answer = BindingAnswer.Pending;
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Client: the newest sim frame the host has named on this binding — every TIME_DILATION
|
|
664
|
+
* carries it — or -1. A join's pages can take a while to arrive, and the frame a page names
|
|
665
|
+
* is as old as its trip; this is the freshest word of where the host is.
|
|
666
|
+
* @private @type {number}
|
|
667
|
+
*/
|
|
668
|
+
#host_frame_seen = -1;
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Client: the peer whose held inbound — action stream and AUTH_STATE, held from `connect()`
|
|
672
|
+
* until the binding's answer is whole — is to be released as the step's inbound is done, or -1.
|
|
673
|
+
* Released there rather than inside the answer's own dispatch: the release applies what was
|
|
674
|
+
* held, through the same receive buffer the answer is still being read from.
|
|
675
|
+
* @private @type {number}
|
|
676
|
+
*/
|
|
677
|
+
#inbound_release_peer = -1;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Client: whether that release completes a join, so {@link onSynced} follows it.
|
|
681
|
+
* @private @type {boolean}
|
|
682
|
+
*/
|
|
683
|
+
#inbound_release_syncs = false;
|
|
624
684
|
/**
|
|
625
685
|
* Client-side: the first frame predicted on the world this client holds —
|
|
626
686
|
* where the last INITIAL_SYNC it applied set the frame counter, 0 before
|
|
@@ -671,6 +731,14 @@ export class NetworkSession {
|
|
|
671
731
|
*/
|
|
672
732
|
onReconnected = new Signal();
|
|
673
733
|
|
|
734
|
+
/**
|
|
735
|
+
* Client: a join has completed — the host's FINAL page has landed, every networked entity's
|
|
736
|
+
* state is here, and from now on this client's input is sent. No arguments. A world is sent
|
|
737
|
+
* as pages over as many ticks as its size takes; until this fires it is still arriving.
|
|
738
|
+
* @type {Signal}
|
|
739
|
+
*/
|
|
740
|
+
onSynced = new Signal();
|
|
741
|
+
|
|
674
742
|
/**
|
|
675
743
|
* Client: the reconnect ladder is exhausted, or the host explicitly
|
|
676
744
|
* kicked us with a DISCONNECT packet, or `reconnect.enabled` is
|
|
@@ -762,12 +830,6 @@ export class NetworkSession {
|
|
|
762
830
|
* - `reconnect` (client role): policy for the reconnect ladder. See
|
|
763
831
|
* {@link DEFAULT_RECONNECT_POLICY}. Set `enabled: false` to opt
|
|
764
832
|
* out entirely.
|
|
765
|
-
* - `publish_changes` (host role, default true): at the end of every
|
|
766
|
-
* step, send each replicated component the simulation changed — physics,
|
|
767
|
-
* controllers, anything that wrote it directly. Components an action
|
|
768
|
-
* wrote are already on the wire and are not sent twice. Publishing never
|
|
769
|
-
* writes back into the host's own state. Set false to publish by hand
|
|
770
|
-
* (`net_mutate_component`).
|
|
771
833
|
* - `reconcile` (client role): see {@link NetworkSessionReconcile}. Default
|
|
772
834
|
* `'correct'`.
|
|
773
835
|
* - `late_input` (host role): what to do with client input that arrives
|
|
@@ -791,7 +853,6 @@ export class NetworkSession {
|
|
|
791
853
|
server_resume_grace_ms = DEFAULT_SERVER_RESUME_GRACE_MS,
|
|
792
854
|
connection_timeout_ms = DEFAULT_CONNECTION_TIMEOUT_MS,
|
|
793
855
|
reconnect = null,
|
|
794
|
-
publish_changes = true,
|
|
795
856
|
late_input = LateInput.Apply,
|
|
796
857
|
reconcile = NetworkSessionReconcile.Correct,
|
|
797
858
|
} = {}) {
|
|
@@ -904,7 +965,6 @@ export class NetworkSession {
|
|
|
904
965
|
this.#connection_timeout_ms = connection_timeout_ms;
|
|
905
966
|
this.#reconnect_policy = Object.freeze({ ...DEFAULT_RECONNECT_POLICY, ...(reconnect || {}) });
|
|
906
967
|
this.#transport_factory = transport_factory;
|
|
907
|
-
this.#publish_changes = publish_changes;
|
|
908
968
|
this.#late_input = late_input;
|
|
909
969
|
if (reconcile !== NetworkSessionReconcile.Correct && reconcile !== NetworkSessionReconcile.Replay) {
|
|
910
970
|
throw new Error(`NetworkSession: reconcile must be one of ${Object.values(NetworkSessionReconcile).join(', ')}, got '${reconcile}'`);
|
|
@@ -1092,7 +1152,7 @@ export class NetworkSession {
|
|
|
1092
1152
|
/**
|
|
1093
1153
|
* Wire the session to the engine. After this, the EntityObserver
|
|
1094
1154
|
* is active, the right orchestrator is constructed, and a
|
|
1095
|
-
* {@link NetworkSystem} is attached. Subsequent `replicate_*`
|
|
1155
|
+
* {@link NetworkSystem} is attached until {@link stop}. Subsequent `replicate_*`
|
|
1096
1156
|
* / {@link defineAction} / {@link defineInputSampler} calls throw.
|
|
1097
1157
|
*
|
|
1098
1158
|
* Pre-condition: every replicated component class must already have a
|
|
@@ -1101,6 +1161,7 @@ export class NetworkSession {
|
|
|
1101
1161
|
*/
|
|
1102
1162
|
async start() {
|
|
1103
1163
|
if (this.#started) throw new Error("NetworkSession.start: already started");
|
|
1164
|
+
if (this.#stopped) throw new Error("NetworkSession.start: a stopped session does not start again; the next match is a new NetworkSession");
|
|
1104
1165
|
|
|
1105
1166
|
// Auto-register the NetworkIdentity adapter so initial-sync can
|
|
1106
1167
|
// ship it across the wire. Allow caller-supplied registrations
|
|
@@ -1156,6 +1217,11 @@ export class NetworkSession {
|
|
|
1156
1217
|
replicated_components,
|
|
1157
1218
|
action_classes,
|
|
1158
1219
|
frame_capacity: this.frame_capacity,
|
|
1220
|
+
// The session's one clock: render-paced, so the fixed steps one update runs all see
|
|
1221
|
+
// the same time, and a burst of catch-up steps cannot age packets whose
|
|
1222
|
+
// acknowledgements could not have arrived yet.
|
|
1223
|
+
clock: () => this.#clock_ms,
|
|
1224
|
+
tick_period_ms: this.tick_period_ms,
|
|
1159
1225
|
};
|
|
1160
1226
|
|
|
1161
1227
|
if (this.role === NetworkSessionRole.Host) {
|
|
@@ -1171,9 +1237,6 @@ export class NetworkSession {
|
|
|
1171
1237
|
identity_class: NetworkIdentity,
|
|
1172
1238
|
});
|
|
1173
1239
|
this.#server.onTickComplete.add((sim_frame) => this.#on_host_tick_complete(sim_frame));
|
|
1174
|
-
if (this.#publish_changes) {
|
|
1175
|
-
this.#server.onBeforeTickClose.add(() => this.#publish_simulated_changes());
|
|
1176
|
-
}
|
|
1177
1240
|
} else {
|
|
1178
1241
|
// Client role: covers both "playing" (input sampler registered)
|
|
1179
1242
|
// and "spectator" (no sampler — sampler-driven onPredict
|
|
@@ -1204,13 +1267,6 @@ export class NetworkSession {
|
|
|
1204
1267
|
// blended component on the first execute after a render pass.
|
|
1205
1268
|
this.#peer.executor.before_execute = this.#bound_normalize_if_dirty;
|
|
1206
1269
|
|
|
1207
|
-
if (this.role === NetworkSessionRole.Host && this.#publish_changes) {
|
|
1208
|
-
// an action carries its own change to the peers: what it wrote is what they will hold
|
|
1209
|
-
this.#peer.executor.on_component_written = (entity_id, component_class) => {
|
|
1210
|
-
this.#note_published(entity_id, component_class);
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
1270
|
// Secure-by-default inbound authorization on the host: a remote client
|
|
1215
1271
|
// may only mutate entities it owns (NetworkIdentity.owner_peer_id ===
|
|
1216
1272
|
// sender); the authoritative server (sender === local_peer_id) may
|
|
@@ -1234,11 +1290,12 @@ export class NetworkSession {
|
|
|
1234
1290
|
//
|
|
1235
1291
|
// It is also the session's place in the engine loop: each fixed step opens a network frame
|
|
1236
1292
|
// and closes it, and every rendered frame runs the session's wall-paced work.
|
|
1237
|
-
|
|
1293
|
+
this.#network_system = new NetworkSystem(this.#peer, {
|
|
1238
1294
|
open_step: () => this.#open_step(),
|
|
1239
1295
|
close_step: () => this.#close_step(),
|
|
1240
1296
|
frame: (dt) => this.#on_frame(dt),
|
|
1241
|
-
})
|
|
1297
|
+
});
|
|
1298
|
+
await this.entity_manager.addSystem(this.#network_system);
|
|
1242
1299
|
|
|
1243
1300
|
// The network timeline: per-frame state of every remote entity, blended at render time on
|
|
1244
1301
|
// the render-delayed playout by the engine's InterpolationSystem.
|
|
@@ -1267,65 +1324,32 @@ export class NetworkSession {
|
|
|
1267
1324
|
// action-stream processing, and finalize any in-flight Tier-3
|
|
1268
1325
|
// resync state.
|
|
1269
1326
|
if (this.role === NetworkSessionRole.Client) {
|
|
1270
|
-
this.#peer.onInitialSync.add((peer_id, session_token, frame_number, buf, payload_end) => {
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1327
|
+
this.#peer.onInitialSync.add((peer_id, session_token, frame_number, flags, buf, payload_end) => {
|
|
1328
|
+
const first = (flags & InitialSyncPage.FIRST) !== 0;
|
|
1329
|
+
const final = (flags & InitialSyncPage.FINAL) !== 0;
|
|
1330
|
+
|
|
1331
|
+
if (first) {
|
|
1332
|
+
if (this.#binding_answer !== BindingAnswer.Pending) {
|
|
1333
|
+
// This binding has had its answer — a resume, or a join already under
|
|
1334
|
+
// way or done. The host starts one join per connection, so this is not
|
|
1335
|
+
// ours to act on: the client has been predicting on the world it holds.
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
this.#begin_sync(peer_id, session_token, frame_number);
|
|
1339
|
+
} else if (this.#binding_answer !== BindingAnswer.Syncing) {
|
|
1340
|
+
// a page of a join this binding is not in
|
|
1276
1341
|
return;
|
|
1277
1342
|
}
|
|
1278
1343
|
|
|
1279
|
-
const token = new UUID();
|
|
1280
|
-
token.data = session_token;
|
|
1281
|
-
this.session_token = token;
|
|
1282
|
-
|
|
1283
|
-
/*
|
|
1284
|
-
Align to the host's clock. This argument has always been on the wire and in this
|
|
1285
|
-
signature -- it was named `_frame_number` and dropped -- and without it a client
|
|
1286
|
-
joining a match in progress tags its inputs from 0 against a host at frame 6,000,
|
|
1287
|
-
which trims every one of them silently. GAP-042.
|
|
1288
|
-
|
|
1289
|
-
The lead is the buffer depth the time dilation is already steering toward: an
|
|
1290
|
-
input authored now arrives in the host's future, and this is how far ahead it has
|
|
1291
|
-
to be tagged to land in the window rather than behind it. It only has to be in the
|
|
1292
|
-
right neighbourhood -- `TimeDilation` converges on the real depth from the host's
|
|
1293
|
-
feedback within a second or two -- but starting six thousand frames out is not a
|
|
1294
|
-
neighbourhood, which is what made this total rather than approximate.
|
|
1295
|
-
*/
|
|
1296
|
-
this.seek_to_frame(frame_number + 1 + this.#time_dilation.target_buffer_depth);
|
|
1297
|
-
|
|
1298
|
-
// The world is about to be the host's. What this client predicted, sampled or was
|
|
1299
|
-
// told about the one it had is about a world, and a frame numbering, that is gone.
|
|
1300
|
-
this.#prediction_history.clear();
|
|
1301
|
-
this.#pending_auth.clear();
|
|
1302
|
-
this.#reconciled_frame.clear();
|
|
1303
|
-
this.#sampled_actions_per_frame.clear();
|
|
1304
|
-
|
|
1305
1344
|
this.#apply_initial_sync(buf, payload_end);
|
|
1306
|
-
this.#binding_answer = BindingAnswer.Synced;
|
|
1307
1345
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
// predicted on the world just replaced, and is never sent.
|
|
1311
|
-
this.#world_start_frame = this.#local_frame;
|
|
1312
|
-
this.#peer.release_outbound(peer_id, this.#world_start_frame);
|
|
1313
|
-
|
|
1314
|
-
// Reconnecting counts as well as Resyncing: a host that has
|
|
1315
|
-
// finished with our old session readmits us as a fresh peer,
|
|
1316
|
-
// so the snapshot arrives with no RESUME_ACCEPT ahead of it.
|
|
1317
|
-
// That is the reconnection — staying in the ladder would burn
|
|
1318
|
-
// attempts while holding the world the host just sent, and
|
|
1319
|
-
// recovering later via RESUME_ACCEPT would report
|
|
1320
|
-
// `state_resynced: false` over a world that was wholly
|
|
1321
|
-
// replaced. `Lost` is excluded on purpose: the application has
|
|
1322
|
-
// already been told that session is over.
|
|
1323
|
-
if (this.#reconnect_state === ReconnectState.Resyncing
|
|
1324
|
-
|| this.#reconnect_state === ReconnectState.Reconnecting) {
|
|
1325
|
-
this.#enter_connected();
|
|
1326
|
-
this.onReconnected.send1({ state_resynced: true });
|
|
1346
|
+
if (final) {
|
|
1347
|
+
this.#finish_sync(peer_id, frame_number);
|
|
1327
1348
|
}
|
|
1328
1349
|
});
|
|
1350
|
+
this.#peer.onTimeDilationFeedback.add((_peer_id, _depth, frame_number) => {
|
|
1351
|
+
if (frame_number > this.#host_frame_seen) this.#host_frame_seen = frame_number;
|
|
1352
|
+
});
|
|
1329
1353
|
// Host's response to our RESUME_HELLO during reconnect.
|
|
1330
1354
|
this.#peer.onResumeAccept.add((peer_id) => {
|
|
1331
1355
|
if (this.#binding_answer !== BindingAnswer.Pending) {
|
|
@@ -1336,6 +1360,9 @@ export class NetworkSession {
|
|
|
1336
1360
|
// The host kept the world this client holds, so what the client predicted on it —
|
|
1337
1361
|
// while cut off, and since — is owed to the host like any input, late as it is.
|
|
1338
1362
|
this.#peer.release_outbound(peer_id, this.#world_start_frame);
|
|
1363
|
+
// and what the host sent on it meanwhile is about that world
|
|
1364
|
+
this.#inbound_release_peer = peer_id;
|
|
1365
|
+
this.#inbound_release_syncs = false;
|
|
1339
1366
|
if (this.#reconnect_state === ReconnectState.Reconnecting
|
|
1340
1367
|
|| this.#reconnect_state === ReconnectState.Resyncing) {
|
|
1341
1368
|
this.#enter_connected();
|
|
@@ -1370,6 +1397,7 @@ export class NetworkSession {
|
|
|
1370
1397
|
this.#peer.onDisconnectPacket.add((_peer_id, reason_label) => {
|
|
1371
1398
|
this.#permanently_lose_connection(reason_label || 'disconnected_by_host');
|
|
1372
1399
|
});
|
|
1400
|
+
this.#peer.onProtocolError.add((_peer_id, reason) => this.#on_client_protocol_error(reason));
|
|
1373
1401
|
} else {
|
|
1374
1402
|
// Host: RESUME_HELLO validation + peer-initiated disconnects.
|
|
1375
1403
|
this.#peer.onResumeHello.add((peer_id, local_peer_id, last_acked_frame, has_token, session_token) => {
|
|
@@ -1380,6 +1408,7 @@ export class NetworkSession {
|
|
|
1380
1408
|
// and free state immediately.
|
|
1381
1409
|
this.drop_peer(peer_id, reason_label || 'client_disconnect');
|
|
1382
1410
|
});
|
|
1411
|
+
this.#peer.onProtocolError.add((peer_id, reason) => this.#on_peer_protocol_error(peer_id, reason));
|
|
1383
1412
|
}
|
|
1384
1413
|
|
|
1385
1414
|
// EntityObserver for NetworkIdentity: patches owner/network_id on
|
|
@@ -1478,7 +1507,13 @@ export class NetworkSession {
|
|
|
1478
1507
|
// snapshot replaces must never reach the host. So what the client predicts meanwhile is
|
|
1479
1508
|
// held: sent on an accept, dropped on a sync.
|
|
1480
1509
|
this.#binding_answer = BindingAnswer.Pending;
|
|
1510
|
+
this.#host_frame_seen = -1;
|
|
1481
1511
|
this.#peer.hold_outbound(remote_peer_id);
|
|
1512
|
+
// Inbound too, until the answer is whole: an action stream that arrives ahead of it —
|
|
1513
|
+
// a join's stream can, when the pages are lost on the way — is about the world the
|
|
1514
|
+
// answer settles, a resumed one or a new one, and is applied once it is settled.
|
|
1515
|
+
this.#peer.hold_inbound(remote_peer_id);
|
|
1516
|
+
this.#inbound_release_peer = -1;
|
|
1482
1517
|
|
|
1483
1518
|
// Announce ourselves whenever this connect could be a claim on an
|
|
1484
1519
|
// id the host still holds a record for: we have a token to prove
|
|
@@ -1499,6 +1534,9 @@ export class NetworkSession {
|
|
|
1499
1534
|
remote_peer_id, this.local_peer_id, last_acked,
|
|
1500
1535
|
token !== null, token !== null ? token.data : NO_SESSION_TOKEN,
|
|
1501
1536
|
);
|
|
1537
|
+
// Out at once rather than with the next tick: a connect is an event, not part of
|
|
1538
|
+
// a frame, and the host can answer nothing until it has heard the claim.
|
|
1539
|
+
this.#peer.flush_peer(remote_peer_id);
|
|
1502
1540
|
}
|
|
1503
1541
|
this.#has_ever_connected = true;
|
|
1504
1542
|
}
|
|
@@ -1542,6 +1580,7 @@ export class NetworkSession {
|
|
|
1542
1580
|
// Best-effort notification — peer may have already dropped.
|
|
1543
1581
|
try {
|
|
1544
1582
|
this.#peer.send_disconnect(peer_id, reason);
|
|
1583
|
+
this.#peer.flush_peer(peer_id);
|
|
1545
1584
|
} catch (_) { /* swallow */
|
|
1546
1585
|
}
|
|
1547
1586
|
this.#server.disconnect_peer(peer_id);
|
|
@@ -1551,10 +1590,23 @@ export class NetworkSession {
|
|
|
1551
1590
|
this.#peer_inbound_mark.delete(peer_id);
|
|
1552
1591
|
this.#peer_grace_state.delete(peer_id);
|
|
1553
1592
|
this.#peer_session_tokens.delete(peer_id);
|
|
1554
|
-
this.#
|
|
1593
|
+
this.#joins.delete(peer_id);
|
|
1555
1594
|
this.onPeerPermanentlyDropped.send2(peer_id, reason);
|
|
1556
1595
|
}
|
|
1557
1596
|
|
|
1597
|
+
/**
|
|
1598
|
+
* Send what is queued for `peer_id` — a kick's RESUME_REJECT and DISCONNECT — before its
|
|
1599
|
+
* connection is torn down. One best-effort flush: a transport that refuses it (closed, say)
|
|
1600
|
+
* changes nothing about the teardown that follows.
|
|
1601
|
+
* @param {number} peer_id
|
|
1602
|
+
*/
|
|
1603
|
+
#flush_last_word(peer_id) {
|
|
1604
|
+
try {
|
|
1605
|
+
this.#peer.flush_peer(peer_id);
|
|
1606
|
+
} catch (_) { /* swallow */
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1558
1610
|
/**
|
|
1559
1611
|
* Client-only: voluntarily disconnect from the server. Sends a
|
|
1560
1612
|
* DISCONNECT packet so the host can free state immediately and
|
|
@@ -1570,6 +1622,7 @@ export class NetworkSession {
|
|
|
1570
1622
|
const peer_id = this.#remote_peer_id;
|
|
1571
1623
|
try {
|
|
1572
1624
|
this.#peer.send_disconnect(peer_id, reason);
|
|
1625
|
+
this.#peer.flush_peer(peer_id);
|
|
1573
1626
|
} catch (_) { /* swallow */
|
|
1574
1627
|
}
|
|
1575
1628
|
// Tear down the channel binding so a subsequent connect() with a
|
|
@@ -1598,9 +1651,19 @@ export class NetworkSession {
|
|
|
1598
1651
|
|
|
1599
1652
|
/**
|
|
1600
1653
|
* Tear down. Idempotent — safe to call from cleanup paths.
|
|
1654
|
+
*
|
|
1655
|
+
* Takes back from the engine what {@link start} put in it: the {@link NetworkSystem} is out of
|
|
1656
|
+
* the engine's loop when this returns, so another session can start on the same engine; this one
|
|
1657
|
+
* does not start again. The
|
|
1658
|
+
* networked entities stay in the world, and their `network_id`s are released — an id means
|
|
1659
|
+
* nothing without the session that gave it. The {@link InterpolationSystem} stays too, whoever
|
|
1660
|
+
* added it: it is the engine's, and blends whatever else is interpolated; the session only
|
|
1661
|
+
* withdraws its own source from it.
|
|
1662
|
+
*
|
|
1663
|
+
* @returns {Promise} resolves once the engine has shut the {@link NetworkSystem} down
|
|
1601
1664
|
*/
|
|
1602
1665
|
stop() {
|
|
1603
|
-
if (!this.#started) return;
|
|
1666
|
+
if (!this.#started) return Promise.resolve();
|
|
1604
1667
|
|
|
1605
1668
|
this.entity_manager.fixedStepTimeScale = 1;
|
|
1606
1669
|
|
|
@@ -1634,6 +1697,8 @@ export class NetworkSession {
|
|
|
1634
1697
|
}
|
|
1635
1698
|
this.#event_listeners.clear();
|
|
1636
1699
|
this.#remote_entities.clear();
|
|
1700
|
+
this.#owned_by.clear();
|
|
1701
|
+
this.#owner_of.clear();
|
|
1637
1702
|
this.#sampled_actions_per_frame.clear();
|
|
1638
1703
|
this.#peer_inbound_mark.clear();
|
|
1639
1704
|
for (const transport of this.#inbound.values()) {
|
|
@@ -1641,6 +1706,13 @@ export class NetworkSession {
|
|
|
1641
1706
|
}
|
|
1642
1707
|
this.#inbound.clear();
|
|
1643
1708
|
this.#started = false;
|
|
1709
|
+
this.#stopped = true;
|
|
1710
|
+
|
|
1711
|
+
// last: removing the system unlinks every networked entity, and nothing of the session's
|
|
1712
|
+
// may still be listening to that
|
|
1713
|
+
const network_system = this.#network_system;
|
|
1714
|
+
this.#network_system = null;
|
|
1715
|
+
return this.entity_manager.removeSystem(network_system);
|
|
1644
1716
|
}
|
|
1645
1717
|
|
|
1646
1718
|
// ----------------------------------------------------------------
|
|
@@ -1679,6 +1751,7 @@ export class NetworkSession {
|
|
|
1679
1751
|
this.#interpolation.settle();
|
|
1680
1752
|
|
|
1681
1753
|
this.#flush_inbound();
|
|
1754
|
+
this.#release_held_inbound();
|
|
1682
1755
|
|
|
1683
1756
|
this.#apply_pending_interpolation();
|
|
1684
1757
|
|
|
@@ -1710,11 +1783,12 @@ export class NetworkSession {
|
|
|
1710
1783
|
|
|
1711
1784
|
try {
|
|
1712
1785
|
if (this.#server !== null) {
|
|
1713
|
-
// runs the frame's local sim, then
|
|
1786
|
+
// runs the frame's local sim, then closes it
|
|
1714
1787
|
this.#server.end_tick();
|
|
1715
1788
|
} else {
|
|
1716
|
-
|
|
1789
|
+
// queued before end_tick, which sends: the marker leaves with the tick's stream
|
|
1717
1790
|
this.#send_frame_marker(this.#local_frame);
|
|
1791
|
+
this.#client.end_tick();
|
|
1718
1792
|
if (this.#reconcile === NetworkSessionReconcile.Correct) {
|
|
1719
1793
|
this.#record_prediction(this.#local_frame);
|
|
1720
1794
|
}
|
|
@@ -1769,102 +1843,6 @@ export class NetworkSession {
|
|
|
1769
1843
|
return deferred;
|
|
1770
1844
|
}
|
|
1771
1845
|
|
|
1772
|
-
/**
|
|
1773
|
-
* Host, end of step: record every replicated component whose wire image differs from what the
|
|
1774
|
-
* peers hold, into the still-open frame, for the action stream to carry. Recorded, not
|
|
1775
|
-
* executed — see {@link SimActionExecutor#record}. A component an action wrote this step and
|
|
1776
|
-
* nothing changed since is not sent again: the peers apply the action.
|
|
1777
|
-
*/
|
|
1778
|
-
#publish_simulated_changes() {
|
|
1779
|
-
const world = this.world;
|
|
1780
|
-
const executor = this.#peer.executor;
|
|
1781
|
-
const scratch = this.#scratch_publish_buf;
|
|
1782
|
-
const changed_entities = executor.changed_entities;
|
|
1783
|
-
|
|
1784
|
-
for (const entity_id of this.#event_listeners.keys()) {
|
|
1785
|
-
const identity = world.getComponent(entity_id, NetworkIdentity);
|
|
1786
|
-
if (identity === undefined || identity.network_id < 0) continue;
|
|
1787
|
-
|
|
1788
|
-
for (const [ComponentClass, desc] of this.#replicated) {
|
|
1789
|
-
if (ComponentClass === NetworkIdentity) continue;
|
|
1790
|
-
|
|
1791
|
-
const live = world.getComponent(entity_id, ComponentClass);
|
|
1792
|
-
if (live === undefined) continue;
|
|
1793
|
-
|
|
1794
|
-
const type_id = desc.interp_type_id;
|
|
1795
|
-
|
|
1796
|
-
scratch.position = 0;
|
|
1797
|
-
this.binary_registry.getAdapter(ComponentClass.typeName).serialize(scratch, live);
|
|
1798
|
-
const length = scratch.position;
|
|
1799
|
-
|
|
1800
|
-
if (!this.#remember_published(entity_id, type_id, scratch.raw_bytes, length)) continue;
|
|
1801
|
-
|
|
1802
|
-
const action = this.#peer.action_registry.acquire(this.#ReplaceComponentAction);
|
|
1803
|
-
try {
|
|
1804
|
-
action.network_id = identity.network_id;
|
|
1805
|
-
action.component_type_id = type_id;
|
|
1806
|
-
action.payload_buf.position = 0;
|
|
1807
|
-
action.payload_buf.writeBytes(scratch.raw_bytes, 0, length);
|
|
1808
|
-
action.payload_length = length;
|
|
1809
|
-
executor.record(action);
|
|
1810
|
-
} finally {
|
|
1811
|
-
this.#peer.action_registry.release(action);
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
if (changed_entities !== null) changed_entities.add(identity.network_id);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
/**
|
|
1820
|
-
* Host: an action wrote `ComponentClass` of `entity_id`; the peers will hold what it wrote.
|
|
1821
|
-
* @param {number} entity_id
|
|
1822
|
-
* @param {Function} ComponentClass
|
|
1823
|
-
*/
|
|
1824
|
-
#note_published(entity_id, ComponentClass) {
|
|
1825
|
-
const desc = this.#replicated.get(ComponentClass);
|
|
1826
|
-
if (desc === undefined || ComponentClass === NetworkIdentity) return;
|
|
1827
|
-
|
|
1828
|
-
const live = this.world.getComponent(entity_id, ComponentClass);
|
|
1829
|
-
if (live === undefined) return;
|
|
1830
|
-
|
|
1831
|
-
const scratch = this.#scratch_publish_buf;
|
|
1832
|
-
scratch.position = 0;
|
|
1833
|
-
this.binary_registry.getAdapter(ComponentClass.typeName).serialize(scratch, live);
|
|
1834
|
-
this.#remember_published(entity_id, desc.interp_type_id, scratch.raw_bytes, scratch.position);
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
/**
|
|
1838
|
-
* Set what the peers hold for `(entity_id, type_id)`.
|
|
1839
|
-
* @param {number} entity_id
|
|
1840
|
-
* @param {number} type_id
|
|
1841
|
-
* @param {Uint8Array} bytes
|
|
1842
|
-
* @param {number} length
|
|
1843
|
-
* @returns {boolean} whether it differs from what was held before
|
|
1844
|
-
*/
|
|
1845
|
-
#remember_published(entity_id, type_id, bytes, length) {
|
|
1846
|
-
let published = this.#published.get(entity_id);
|
|
1847
|
-
if (published === undefined) {
|
|
1848
|
-
published = [];
|
|
1849
|
-
this.#published.set(entity_id, published);
|
|
1850
|
-
}
|
|
1851
|
-
while (published.length <= type_id) published.push(null);
|
|
1852
|
-
|
|
1853
|
-
let held = published[type_id];
|
|
1854
|
-
if (held !== null && held.length === length && bytes_equal(bytes, held.bytes, length)) {
|
|
1855
|
-
return false;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
if (held === null || held.bytes.length < length) {
|
|
1859
|
-
held = { bytes: new Uint8Array(Math.max(length, 32)), length: 0 };
|
|
1860
|
-
published[type_id] = held;
|
|
1861
|
-
}
|
|
1862
|
-
held.bytes.set(bytes.subarray(0, length), 0);
|
|
1863
|
-
held.length = length;
|
|
1864
|
-
|
|
1865
|
-
return true;
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
1846
|
/**
|
|
1869
1847
|
* Once per rendered frame: the work paced by wall time rather than by the simulation.
|
|
1870
1848
|
* @param {number} dt_seconds
|
|
@@ -2109,6 +2087,7 @@ export class NetworkSession {
|
|
|
2109
2087
|
if (identity.owner_peer_id !== this.local_peer_id) {
|
|
2110
2088
|
this.#remote_entities.add(entity_id);
|
|
2111
2089
|
}
|
|
2090
|
+
this.#file_owner(entity_id, identity.owner_peer_id);
|
|
2112
2091
|
this.#interpolation_pending.add(entity_id);
|
|
2113
2092
|
// Per-entity event listener for net_mutate_component.
|
|
2114
2093
|
const listener = (payload) => this.#on_net_mutate_component(entity_id, payload);
|
|
@@ -2123,23 +2102,26 @@ export class NetworkSession {
|
|
|
2123
2102
|
this.#event_listeners.delete(entity_id);
|
|
2124
2103
|
}
|
|
2125
2104
|
this.#remote_entities.delete(entity_id);
|
|
2126
|
-
this.#
|
|
2105
|
+
this.#file_owner(entity_id, -1);
|
|
2127
2106
|
this.#prediction_history.forget(entity_id);
|
|
2128
2107
|
// take back our Interpolated at the next step or frame, if the entity outlives its identity
|
|
2129
2108
|
this.#interpolation_pending.add(entity_id);
|
|
2130
2109
|
}
|
|
2131
2110
|
|
|
2132
2111
|
/**
|
|
2133
|
-
* Handler for `"net_mutate_component"` events
|
|
2134
|
-
*
|
|
2112
|
+
* Handler for `"net_mutate_component"` events: a replicated component changed outside an
|
|
2113
|
+
* action, and goes through the executor as a `ReplaceComponentAction`. The session finds no
|
|
2114
|
+
* change by itself (see the README, "What travels"); this is the door for a system that writes
|
|
2115
|
+
* replicated state directly — physics publishing its moving bodies, say — at a cost per
|
|
2116
|
+
* change, never per entity.
|
|
2135
2117
|
*
|
|
2136
2118
|
* Payload:
|
|
2137
|
-
* - `{ component_type, new_state }` —
|
|
2138
|
-
*
|
|
2139
|
-
*
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2119
|
+
* - `{ component_type, new_state }` — the engine captures the current bytes as prior, then
|
|
2120
|
+
* applies `new_state` through the replication adapter.
|
|
2121
|
+
* - `{ component_type }` — the caller has already written live. On the host it is recorded,
|
|
2122
|
+
* not executed ({@link SimActionExecutor#record}): live is the truth, at full precision,
|
|
2123
|
+
* and executing would write the wire's quantised image back over it. On a client it is
|
|
2124
|
+
* executed, so prediction and rewind keep their prior state.
|
|
2143
2125
|
*/
|
|
2144
2126
|
#on_net_mutate_component(entity_id, payload) {
|
|
2145
2127
|
const { component_type, new_state } = payload || {};
|
|
@@ -2174,8 +2156,15 @@ export class NetworkSession {
|
|
|
2174
2156
|
}
|
|
2175
2157
|
action.payload_length = action.payload_buf.position;
|
|
2176
2158
|
|
|
2159
|
+
const executor = this.#peer.executor;
|
|
2177
2160
|
try {
|
|
2178
|
-
this.#
|
|
2161
|
+
if (this.#server !== null && (new_state === undefined || new_state === null)) {
|
|
2162
|
+
executor.record(action);
|
|
2163
|
+
// recorded writes are changes too: a join re-sends an entity changed after its page
|
|
2164
|
+
if (executor.changed_entities !== null) executor.changed_entities.add(network_id);
|
|
2165
|
+
} else {
|
|
2166
|
+
executor.execute(action, this.local_peer_id);
|
|
2167
|
+
}
|
|
2179
2168
|
} finally {
|
|
2180
2169
|
this.#peer.action_registry.release(action);
|
|
2181
2170
|
}
|
|
@@ -2280,41 +2269,31 @@ export class NetworkSession {
|
|
|
2280
2269
|
* @param {number} sim_frame
|
|
2281
2270
|
*/
|
|
2282
2271
|
#on_host_tick_complete(sim_frame) {
|
|
2283
|
-
//
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
if (sends_remaining <= 1) {
|
|
2292
|
-
this.#pending_initial_sync.delete(peer_id);
|
|
2293
|
-
} else {
|
|
2294
|
-
this.#pending_initial_sync.set(peer_id, sends_remaining - 1);
|
|
2272
|
+
// Joins first: a page is the state at this frame, which has just closed.
|
|
2273
|
+
if (this.#joins.size > 0) {
|
|
2274
|
+
for (const [peer_id, join] of this.#joins) {
|
|
2275
|
+
const progress = join.advance(sim_frame);
|
|
2276
|
+
if (progress === JoinProgress.Done) {
|
|
2277
|
+
this.#joins.delete(peer_id);
|
|
2278
|
+
} else if (progress === JoinProgress.DidNotConverge) {
|
|
2279
|
+
this.drop_peer(peer_id, 'join_did_not_converge');
|
|
2295
2280
|
}
|
|
2296
|
-
|
|
2297
2281
|
}
|
|
2298
|
-
|
|
2299
2282
|
}
|
|
2300
2283
|
|
|
2301
2284
|
// AUTH_STATE per owned entity, then time-dilation feedback, to
|
|
2302
|
-
// every connected peer.
|
|
2285
|
+
// every connected peer. A peer still joining gets no AUTH_STATE: it
|
|
2286
|
+
// does not hold the world the state would correct yet.
|
|
2303
2287
|
for (const peer_id of this.#connected_peers) {
|
|
2304
2288
|
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
const identity = this.world.getComponent(entity_id, NetworkIdentity);
|
|
2289
|
+
const owned = this.#joins.has(peer_id) ? undefined : this.#owned_by.get(peer_id);
|
|
2308
2290
|
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2291
|
+
if (owned !== undefined) {
|
|
2292
|
+
for (const entity_id of owned) {
|
|
2293
|
+
const identity = this.world.getComponent(entity_id, NetworkIdentity);
|
|
2294
|
+
if (identity === undefined) continue;
|
|
2295
|
+
this.#send_auth_state_for_entity(peer_id, sim_frame, entity_id, identity);
|
|
2314
2296
|
}
|
|
2315
|
-
|
|
2316
|
-
this.#send_auth_state_for_entity(peer_id, sim_frame, entity_id, identity);
|
|
2317
|
-
|
|
2318
2297
|
}
|
|
2319
2298
|
|
|
2320
2299
|
this.#server.send_time_dilation_feedback(peer_id);
|
|
@@ -2323,49 +2302,160 @@ export class NetworkSession {
|
|
|
2323
2302
|
|
|
2324
2303
|
|
|
2325
2304
|
/**
|
|
2326
|
-
*
|
|
2327
|
-
*
|
|
2328
|
-
*
|
|
2329
|
-
*
|
|
2330
|
-
*
|
|
2331
|
-
*
|
|
2305
|
+
* Start `peer_id`'s join, unless its connection has been given one already — started, or
|
|
2306
|
+
* finished. These are the host's only whole-world sends, so how many a connection gets is the
|
|
2307
|
+
* host's decision: one, when it takes the connection for a fresh peer — as `connect()` binds
|
|
2308
|
+
* it, or after refusing its claim to resume. Nothing the peer sends afterwards earns it another.
|
|
2309
|
+
*
|
|
2310
|
+
* The peer's action stream is held from here until the join's FINAL page: what it would carry
|
|
2311
|
+
* is about a world the peer does not hold yet. The session token is issued here, and goes out
|
|
2312
|
+
* with every page; the receiver echoes it back via RESUME_HELLO to authenticate a later
|
|
2313
|
+
* reconnect.
|
|
2332
2314
|
* @param {number} peer_id
|
|
2333
2315
|
*/
|
|
2334
2316
|
#queue_initial_sync(peer_id) {
|
|
2335
2317
|
if (this.#initial_sync_given.has(peer_id)) return;
|
|
2336
2318
|
this.#initial_sync_given.add(peer_id);
|
|
2337
|
-
|
|
2319
|
+
|
|
2320
|
+
let token = this.#peer_session_tokens.get(peer_id);
|
|
2321
|
+
if (token === undefined) {
|
|
2322
|
+
token = UUID.v1();
|
|
2323
|
+
this.#peer_session_tokens.set(peer_id, token);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
this.#peer.hold_outbound(peer_id);
|
|
2327
|
+
this.#joins.set(peer_id, new HostJoin({
|
|
2328
|
+
peer: this.#peer,
|
|
2329
|
+
peer_id,
|
|
2330
|
+
session_token: token.data,
|
|
2331
|
+
world: this.world,
|
|
2332
|
+
for_each_entity: this.#for_each_networked_entity,
|
|
2333
|
+
page_buffer: this.#page_buffer,
|
|
2334
|
+
window_bytes: 128 * 1024,
|
|
2335
|
+
}));
|
|
2338
2336
|
}
|
|
2339
2337
|
|
|
2340
2338
|
/**
|
|
2341
|
-
*
|
|
2342
|
-
*
|
|
2343
|
-
|
|
2344
|
-
|
|
2339
|
+
* Visits every networked entity: those whose NetworkIdentity the session is listening to.
|
|
2340
|
+
* @type {function(function(number): void): void}
|
|
2341
|
+
*/
|
|
2342
|
+
#for_each_networked_entity = (visit) => {
|
|
2343
|
+
for (const entity_id of this.#event_listeners.keys()) {
|
|
2344
|
+
visit(entity_id);
|
|
2345
|
+
}
|
|
2346
|
+
};
|
|
2347
|
+
|
|
2348
|
+
/**
|
|
2349
|
+
* Client: the first page of a join. It is this binding's answer: the world is about to be the
|
|
2350
|
+
* host's, so what was predicted, sampled or told about the one the client had goes, and the
|
|
2351
|
+
* reconnect ladder stops — a join of a large world takes a while, and an attempt that tore the
|
|
2352
|
+
* binding down would restart it from the first page.
|
|
2353
|
+
*
|
|
2345
2354
|
* @param {number} peer_id
|
|
2346
|
-
* @param {
|
|
2355
|
+
* @param {Uint8Array} session_token
|
|
2356
|
+
* @param {number} frame_number the frame the page's state is from
|
|
2357
|
+
*/
|
|
2358
|
+
#begin_sync(peer_id, session_token, frame_number) {
|
|
2359
|
+
const token = new UUID();
|
|
2360
|
+
token.data = session_token;
|
|
2361
|
+
this.session_token = token;
|
|
2362
|
+
|
|
2363
|
+
this.#align_to_host(frame_number);
|
|
2364
|
+
this.#forget_predicted_world();
|
|
2365
|
+
this.#binding_answer = BindingAnswer.Syncing;
|
|
2366
|
+
// The host releases its action stream, and its AUTH_STATE, from the frame after the last
|
|
2367
|
+
// page; their datagrams can overtake the pages. The binding's inbound hold, taken at
|
|
2368
|
+
// `connect()`, keeps them until the world is whole.
|
|
2369
|
+
|
|
2370
|
+
// Reconnecting counts as well as Resyncing: a host that has
|
|
2371
|
+
// finished with our old session readmits us as a fresh peer,
|
|
2372
|
+
// so the snapshot arrives with no RESUME_ACCEPT ahead of it.
|
|
2373
|
+
// That is the reconnection — staying in the ladder would burn
|
|
2374
|
+
// attempts while holding the world the host is sending, and
|
|
2375
|
+
// recovering later via RESUME_ACCEPT would report
|
|
2376
|
+
// `state_resynced: false` over a world that was wholly
|
|
2377
|
+
// replaced. `Lost` is excluded on purpose: the application has
|
|
2378
|
+
// already been told that session is over.
|
|
2379
|
+
if (this.#reconnect_state === ReconnectState.Resyncing
|
|
2380
|
+
|| this.#reconnect_state === ReconnectState.Reconnecting) {
|
|
2381
|
+
this.#enter_connected();
|
|
2382
|
+
this.onReconnected.send1({ state_resynced: true });
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2386
|
+
/**
|
|
2387
|
+
* Client: the FINAL page. Every entity's state at `frame_number` is here, and the host's action
|
|
2388
|
+
* stream resumes after it. The client predicted on a partial world while the pages came in, and
|
|
2389
|
+
* against the frame the first page named, which is as old as the join is long: both are put
|
|
2390
|
+
* right here, and only then does the client's input go out.
|
|
2391
|
+
*
|
|
2392
|
+
* @param {number} peer_id
|
|
2393
|
+
* @param {number} frame_number
|
|
2347
2394
|
*/
|
|
2348
|
-
#
|
|
2349
|
-
|
|
2395
|
+
#finish_sync(peer_id, frame_number) {
|
|
2396
|
+
this.#align_to_host(frame_number);
|
|
2397
|
+
this.#forget_predicted_world();
|
|
2398
|
+
this.#binding_answer = BindingAnswer.Synced;
|
|
2350
2399
|
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2400
|
+
// Input from here on is predicted on the host's world. What was predicted before —
|
|
2401
|
+
// held since this binding was made, or still in the log from before it — was
|
|
2402
|
+
// predicted on a world that is gone, or half-built, and is never sent.
|
|
2403
|
+
this.#world_start_frame = this.#local_frame;
|
|
2404
|
+
this.#peer.release_outbound(peer_id, this.#world_start_frame);
|
|
2405
|
+
|
|
2406
|
+
// what the host sent after the last page, applied as the step's inbound is done, now that
|
|
2407
|
+
// the world it is about is here; then onSynced
|
|
2408
|
+
this.#inbound_release_peer = peer_id;
|
|
2409
|
+
this.#inbound_release_syncs = true;
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* Client: apply what the binding held, once its answer is whole (see {@link #inbound_release_peer}).
|
|
2414
|
+
*/
|
|
2415
|
+
#release_held_inbound() {
|
|
2416
|
+
const peer_id = this.#inbound_release_peer;
|
|
2417
|
+
if (peer_id < 0) return;
|
|
2418
|
+
this.#inbound_release_peer = -1;
|
|
2419
|
+
if (!this.#peer.is_connected(peer_id)) return;
|
|
2420
|
+
this.#peer.release_inbound(peer_id);
|
|
2421
|
+
if (this.#inbound_release_syncs) {
|
|
2422
|
+
this.#inbound_release_syncs = false;
|
|
2423
|
+
this.onSynced.send0();
|
|
2354
2424
|
}
|
|
2425
|
+
}
|
|
2355
2426
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2427
|
+
/**
|
|
2428
|
+
* Seek the frame counter to the host's `frame_number` — or the newer frame a TIME_DILATION
|
|
2429
|
+
* has named since — plus the lead input needs.
|
|
2430
|
+
*
|
|
2431
|
+
* The frame has always been on the wire — it was named `_frame_number` and dropped — and
|
|
2432
|
+
* without it a client joining a match in progress tags its inputs from 0 against a host at
|
|
2433
|
+
* frame 6,000, which trims every one of them silently. GAP-042.
|
|
2434
|
+
*
|
|
2435
|
+
* The lead is the buffer depth the time dilation is already steering toward: an input
|
|
2436
|
+
* authored now arrives in the host's future, and this is how far ahead it has to be tagged to
|
|
2437
|
+
* land in the window rather than behind it. It only has to be in the right neighbourhood —
|
|
2438
|
+
* `TimeDilation` converges on the real depth from the host's feedback within a second or two —
|
|
2439
|
+
* but starting six thousand frames out is not a neighbourhood, which is what made this total
|
|
2440
|
+
* rather than approximate.
|
|
2441
|
+
*
|
|
2442
|
+
* @param {number} frame_number
|
|
2443
|
+
*/
|
|
2444
|
+
#align_to_host(frame_number) {
|
|
2445
|
+
const host_frame = Math.max(frame_number, this.#host_frame_seen);
|
|
2446
|
+
this.seek_to_frame(host_frame + 1 + this.#time_dilation.target_buffer_depth);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2449
|
+
/**
|
|
2450
|
+
* Drop what this client predicted, sampled or was told about the world it held: it is about a
|
|
2451
|
+
* world, and a frame numbering, that is being replaced.
|
|
2452
|
+
*/
|
|
2453
|
+
#forget_predicted_world() {
|
|
2454
|
+
this.#prediction_history.clear();
|
|
2455
|
+
this.#pending_auth.clear();
|
|
2456
|
+
this.#reconciled_frame.clear();
|
|
2457
|
+
this.#sampled_actions_per_frame.clear();
|
|
2458
|
+
this.#client.forget_timeline();
|
|
2369
2459
|
}
|
|
2370
2460
|
|
|
2371
2461
|
/**
|
|
@@ -2382,7 +2472,7 @@ export class NetworkSession {
|
|
|
2382
2472
|
// written over the truth, not over a render blend that a restore would then put back
|
|
2383
2473
|
this.normalize_if_dirty();
|
|
2384
2474
|
|
|
2385
|
-
const entity_count = buffer.
|
|
2475
|
+
const entity_count = buffer.readUint32();
|
|
2386
2476
|
for (let i = 0; i < entity_count; i++) {
|
|
2387
2477
|
const network_id = buffer.readUintVar();
|
|
2388
2478
|
const component_count = buffer.readUint8();
|
|
@@ -2471,9 +2561,35 @@ export class NetworkSession {
|
|
|
2471
2561
|
} else {
|
|
2472
2562
|
this.#remote_entities.add(entity_id);
|
|
2473
2563
|
}
|
|
2564
|
+
this.#file_owner(entity_id, owner_peer_id);
|
|
2474
2565
|
this.#interpolation_pending.add(entity_id);
|
|
2475
2566
|
}
|
|
2476
2567
|
|
|
2568
|
+
/**
|
|
2569
|
+
* File `entity_id` under `owner_peer_id` in {@link #owned_by}, out of wherever it was; -1 files
|
|
2570
|
+
* it nowhere.
|
|
2571
|
+
* @param {number} entity_id
|
|
2572
|
+
* @param {number} owner_peer_id
|
|
2573
|
+
*/
|
|
2574
|
+
#file_owner(entity_id, owner_peer_id) {
|
|
2575
|
+
const previous = this.#owner_of.get(entity_id);
|
|
2576
|
+
if (previous === owner_peer_id) return;
|
|
2577
|
+
if (previous !== undefined) {
|
|
2578
|
+
const set = this.#owned_by.get(previous);
|
|
2579
|
+
set.delete(entity_id);
|
|
2580
|
+
if (set.size === 0) this.#owned_by.delete(previous);
|
|
2581
|
+
this.#owner_of.delete(entity_id);
|
|
2582
|
+
}
|
|
2583
|
+
if (owner_peer_id < 0) return;
|
|
2584
|
+
let set = this.#owned_by.get(owner_peer_id);
|
|
2585
|
+
if (set === undefined) {
|
|
2586
|
+
set = new Set();
|
|
2587
|
+
this.#owned_by.set(owner_peer_id, set);
|
|
2588
|
+
}
|
|
2589
|
+
set.add(entity_id);
|
|
2590
|
+
this.#owner_of.set(entity_id, owner_peer_id);
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2477
2593
|
/**
|
|
2478
2594
|
*
|
|
2479
2595
|
* @param {number} peer_id
|
|
@@ -2551,7 +2667,7 @@ export class NetworkSession {
|
|
|
2551
2667
|
// session. Both calls are no-ops when there is nothing to release.
|
|
2552
2668
|
this.#server.disconnect_peer(peer_id);
|
|
2553
2669
|
this.#unwire_peer_transport_disconnect(peer_id);
|
|
2554
|
-
this.#
|
|
2670
|
+
this.#joins.delete(peer_id);
|
|
2555
2671
|
return;
|
|
2556
2672
|
}
|
|
2557
2673
|
const last_acked = this.#peer.baseline.last_acked(peer_id);
|
|
@@ -2563,15 +2679,32 @@ export class NetworkSession {
|
|
|
2563
2679
|
// from `#on_host_tick_complete` and throws — before AUTH_STATE and
|
|
2564
2680
|
// time-dilation feedback go out to *any* peer, and without ever
|
|
2565
2681
|
// draining the entry, so every later tick throws the same way.
|
|
2566
|
-
|
|
2682
|
+
// a peer that drops before its join finished holds half a world: it may not resume it
|
|
2683
|
+
const join_complete = !this.#joins.has(peer_id);
|
|
2684
|
+
this.#joins.delete(peer_id);
|
|
2567
2685
|
this.#unwire_peer_transport_disconnect(peer_id);
|
|
2568
2686
|
this.#peer_grace_state.set(peer_id, {
|
|
2569
2687
|
deadline_ms: this.#clock_ms + this.#server_resume_grace_ms,
|
|
2570
2688
|
last_acked,
|
|
2689
|
+
join_complete,
|
|
2571
2690
|
});
|
|
2572
2691
|
this.onPeerLost.send2(peer_id, reason);
|
|
2573
2692
|
}
|
|
2574
2693
|
|
|
2694
|
+
/**
|
|
2695
|
+
* Host: `peer_id`'s connection is no longer whole (see Connection.onProtocolError) and can
|
|
2696
|
+
* carry nothing more. Handled as its transport dropping — the peer goes into the grace window
|
|
2697
|
+
* and may resume on a new connection — and the transport is closed, so the client hears of it
|
|
2698
|
+
* now rather than when silence reaps it.
|
|
2699
|
+
* @param {number} peer_id
|
|
2700
|
+
* @param {string} reason
|
|
2701
|
+
*/
|
|
2702
|
+
#on_peer_protocol_error(peer_id, reason) {
|
|
2703
|
+
const entry = this.#peer_transport_listeners.get(peer_id);
|
|
2704
|
+
this.#on_peer_transport_disconnected(peer_id, `protocol_error: ${reason}`);
|
|
2705
|
+
if (entry !== undefined) close_transport(entry.transport);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2575
2708
|
/**
|
|
2576
2709
|
* Validate a RESUME_HELLO and either welcome the peer back (restore
|
|
2577
2710
|
* baseline + send RESUME_ACCEPT) or reject with a reason code. A claim
|
|
@@ -2603,6 +2736,7 @@ export class NetworkSession {
|
|
|
2603
2736
|
if (transport_peer_id !== claimed_peer_id) {
|
|
2604
2737
|
this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.PeerIdCollision);
|
|
2605
2738
|
this.#peer.send_disconnect(transport_peer_id, 'peer_id_collision');
|
|
2739
|
+
this.#flush_last_word(transport_peer_id);
|
|
2606
2740
|
this.#server.disconnect_peer(transport_peer_id);
|
|
2607
2741
|
this.#unwire_peer_transport_disconnect(transport_peer_id);
|
|
2608
2742
|
// The transport may have been added to the live set in connect()
|
|
@@ -2611,7 +2745,7 @@ export class NetworkSession {
|
|
|
2611
2745
|
// serializes AUTH_STATE / time-dilation to a now-disposed conn and
|
|
2612
2746
|
// throws "peer not connected", taking down the tick for all peers.
|
|
2613
2747
|
this.#connected_peers.delete(transport_peer_id);
|
|
2614
|
-
this.#
|
|
2748
|
+
this.#joins.delete(transport_peer_id);
|
|
2615
2749
|
this.#peer_inbound_mark.delete(transport_peer_id);
|
|
2616
2750
|
return;
|
|
2617
2751
|
}
|
|
@@ -2639,6 +2773,7 @@ export class NetworkSession {
|
|
|
2639
2773
|
// INITIAL_SYNC queue.
|
|
2640
2774
|
this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.NoSessionToken);
|
|
2641
2775
|
this.#peer.send_disconnect(transport_peer_id, 'no_session_token');
|
|
2776
|
+
this.#flush_last_word(transport_peer_id);
|
|
2642
2777
|
this.#server.disconnect_peer(transport_peer_id);
|
|
2643
2778
|
this.#unwire_peer_transport_disconnect(transport_peer_id);
|
|
2644
2779
|
return;
|
|
@@ -2679,16 +2814,28 @@ export class NetworkSession {
|
|
|
2679
2814
|
// a new peer_id from the matchmaking layer.
|
|
2680
2815
|
this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.TokenMismatch);
|
|
2681
2816
|
this.#peer.send_disconnect(transport_peer_id, 'token_mismatch');
|
|
2817
|
+
this.#flush_last_word(transport_peer_id);
|
|
2682
2818
|
this.#server.disconnect_peer(transport_peer_id);
|
|
2683
2819
|
this.#unwire_peer_transport_disconnect(transport_peer_id);
|
|
2684
2820
|
// Same cleanup as the collision branch above: if this transport was
|
|
2685
2821
|
// a fresh peer added to the live set, drop it from both iterated
|
|
2686
2822
|
// maps so the next host tick doesn't address a disposed conn.
|
|
2687
2823
|
this.#connected_peers.delete(transport_peer_id);
|
|
2688
|
-
this.#
|
|
2824
|
+
this.#joins.delete(transport_peer_id);
|
|
2689
2825
|
this.#peer_inbound_mark.delete(transport_peer_id);
|
|
2690
2826
|
return;
|
|
2691
2827
|
}
|
|
2828
|
+
// A join that never finished. The client holds whatever pages reached it — part of a
|
|
2829
|
+
// world — and resuming would keep it. Refused like a stale frame: the client falls through
|
|
2830
|
+
// to a full resync.
|
|
2831
|
+
if (grace.join_complete === false) {
|
|
2832
|
+
this.#peer.send_resume_reject(transport_peer_id, ResumeRejectReason.StaleFrame);
|
|
2833
|
+
this.#peer_grace_state.delete(claimed_peer_id);
|
|
2834
|
+
this.#queue_initial_sync(transport_peer_id);
|
|
2835
|
+
this.#connected_peers.add(transport_peer_id);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2692
2839
|
// Ring-floor gate. The client asks to resume from `last_acked_frame + 1`,
|
|
2693
2840
|
// but the action-log ring only still holds frames back to
|
|
2694
2841
|
// `current_sim_frame - frame_capacity + 1`. If the requested frame
|
|
@@ -2733,7 +2880,7 @@ export class NetworkSession {
|
|
|
2733
2880
|
// so release it here or the id stays unconnectable forever.
|
|
2734
2881
|
this.#server.disconnect_peer(peer_id);
|
|
2735
2882
|
this.#unwire_peer_transport_disconnect(peer_id);
|
|
2736
|
-
this.#
|
|
2883
|
+
this.#joins.delete(peer_id);
|
|
2737
2884
|
this.onPeerPermanentlyDropped.send2(peer_id, 'grace_expired');
|
|
2738
2885
|
}
|
|
2739
2886
|
}
|
|
@@ -2813,6 +2960,25 @@ export class NetworkSession {
|
|
|
2813
2960
|
this.#current_transport_listener = listener;
|
|
2814
2961
|
}
|
|
2815
2962
|
|
|
2963
|
+
/**
|
|
2964
|
+
* Client: the connection to the host is no longer whole (see Connection.onProtocolError) and
|
|
2965
|
+
* can carry nothing more. Given up as a dropped transport is — the reconnect ladder resumes on
|
|
2966
|
+
* a new one — and closed, so the host hears of it now rather than when silence reaps it.
|
|
2967
|
+
* @param {string} reason
|
|
2968
|
+
*/
|
|
2969
|
+
#on_client_protocol_error(reason) {
|
|
2970
|
+
const transport = this.#current_transport;
|
|
2971
|
+
// unhooked first: the close must not be taken for a second loss
|
|
2972
|
+
if (transport !== null && this.#current_transport_listener !== null) {
|
|
2973
|
+
try {
|
|
2974
|
+
transport.onDisconnect.remove(this.#current_transport_listener);
|
|
2975
|
+
} catch (_) { /* swallow */
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
this.#on_client_transport_disconnected(`protocol_error: ${reason}`);
|
|
2979
|
+
if (transport !== null) close_transport(transport);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2816
2982
|
#on_client_transport_disconnected(reason) {
|
|
2817
2983
|
if (this.#reconnect_state === ReconnectState.Lost) return;
|
|
2818
2984
|
// Tear down the dead peer/channel binding so a fresh transport
|
|
@@ -3001,9 +3167,10 @@ export class NetworkSession {
|
|
|
3001
3167
|
const scratch = this.#scratch_send_buf;
|
|
3002
3168
|
const world = this.world;
|
|
3003
3169
|
|
|
3004
|
-
|
|
3005
|
-
|
|
3170
|
+
const owned = this.#owned_by.get(this.local_peer_id);
|
|
3171
|
+
if (owned === undefined) return;
|
|
3006
3172
|
|
|
3173
|
+
for (const entity_id of owned) {
|
|
3007
3174
|
const identity = world.getComponent(entity_id, NetworkIdentity);
|
|
3008
3175
|
if (identity === undefined || identity.network_id < 0) continue;
|
|
3009
3176
|
|