@series-inc/rundot-syncplay 5.23.0-beta.7
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/README.md +610 -0
- package/dist/animation.d.ts +119 -0
- package/dist/animation.js +488 -0
- package/dist/asset-cooking.d.ts +82 -0
- package/dist/asset-cooking.js +364 -0
- package/dist/authority-room.d.ts +217 -0
- package/dist/authority-room.js +701 -0
- package/dist/bot-documents.d.ts +115 -0
- package/dist/bot-documents.js +549 -0
- package/dist/bot-primitives.d.ts +152 -0
- package/dist/bot-primitives.js +472 -0
- package/dist/browser.d.ts +57 -0
- package/dist/browser.js +53 -0
- package/dist/canonical.d.ts +12 -0
- package/dist/canonical.js +288 -0
- package/dist/certification.d.ts +455 -0
- package/dist/certification.js +1855 -0
- package/dist/chaos-certification.d.ts +56 -0
- package/dist/chaos-certification.js +1397 -0
- package/dist/cjs/animation.js +495 -0
- package/dist/cjs/asset-cooking.js +372 -0
- package/dist/cjs/authority-room.js +704 -0
- package/dist/cjs/bot-documents.js +556 -0
- package/dist/cjs/bot-primitives.js +482 -0
- package/dist/cjs/browser.js +96 -0
- package/dist/cjs/canonical.js +299 -0
- package/dist/cjs/certification.js +1860 -0
- package/dist/cjs/chaos-certification.js +1402 -0
- package/dist/cjs/collections.js +36 -0
- package/dist/cjs/collision.js +96 -0
- package/dist/cjs/commands.js +987 -0
- package/dist/cjs/config-bundle.js +491 -0
- package/dist/cjs/creator.js +30 -0
- package/dist/cjs/deterministic-primitives.js +134 -0
- package/dist/cjs/deterministic-session.js +1410 -0
- package/dist/cjs/ecs-lite.js +155 -0
- package/dist/cjs/effects-adapter.js +97 -0
- package/dist/cjs/engine-complete.js +2297 -0
- package/dist/cjs/engine-core-proof.js +70 -0
- package/dist/cjs/engine-parity-matrix.js +874 -0
- package/dist/cjs/engine-parity.js +3183 -0
- package/dist/cjs/engine-runtime.js +3141 -0
- package/dist/cjs/errors.js +22 -0
- package/dist/cjs/events.js +247 -0
- package/dist/cjs/examples/counter-duel.js +157 -0
- package/dist/cjs/examples/fixed-arena.js +368 -0
- package/dist/cjs/index.js +229 -0
- package/dist/cjs/input-authority.js +150 -0
- package/dist/cjs/input-button.js +256 -0
- package/dist/cjs/input-timeline.js +418 -0
- package/dist/cjs/instant-replay.js +66 -0
- package/dist/cjs/lag-compensation3d.js +837 -0
- package/dist/cjs/local-authority-server-runner.js +1121 -0
- package/dist/cjs/local-authority-wire.js +1497 -0
- package/dist/cjs/local-authority.js +817 -0
- package/dist/cjs/local-ws-adapter.js +150 -0
- package/dist/cjs/match-log.js +61 -0
- package/dist/cjs/math.js +157 -0
- package/dist/cjs/mock-network-adapter.js +26 -0
- package/dist/cjs/movement.js +703 -0
- package/dist/cjs/movement3d.js +1246 -0
- package/dist/cjs/multiclient-authority.js +308 -0
- package/dist/cjs/multiplayer-service.js +434 -0
- package/dist/cjs/navigation.js +347 -0
- package/dist/cjs/navmesh.js +1437 -0
- package/dist/cjs/networked-client.js +691 -0
- package/dist/cjs/networked-input-authority.js +443 -0
- package/dist/cjs/node.js +47 -0
- package/dist/cjs/number-lab.js +78 -0
- package/dist/cjs/offline-session.js +370 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pause-resume.js +43 -0
- package/dist/cjs/physics-cert.js +426 -0
- package/dist/cjs/physics2d.js +751 -0
- package/dist/cjs/physics3d.js +1996 -0
- package/dist/cjs/prediction-culling.js +203 -0
- package/dist/cjs/protocol-lab.js +1299 -0
- package/dist/cjs/r3f-render-adapter.js +209 -0
- package/dist/cjs/random.js +59 -0
- package/dist/cjs/replay-bundle.js +1598 -0
- package/dist/cjs/replay-inspect.js +236 -0
- package/dist/cjs/replay-scoring.js +68 -0
- package/dist/cjs/replay.js +356 -0
- package/dist/cjs/reports.js +181 -0
- package/dist/cjs/rollback-history.js +251 -0
- package/dist/cjs/runtime-guards.js +111 -0
- package/dist/cjs/sample-lab.js +145 -0
- package/dist/cjs/sample-scenes.js +752 -0
- package/dist/cjs/schema-artifacts.js +359 -0
- package/dist/cjs/schema-codegen.js +544 -0
- package/dist/cjs/schema-dsl-binary.js +655 -0
- package/dist/cjs/schema.js +319 -0
- package/dist/cjs/sdk-offline.js +99 -0
- package/dist/cjs/session-promotion.js +74 -0
- package/dist/cjs/session-snapshot.js +265 -0
- package/dist/cjs/session-wire.js +216 -0
- package/dist/cjs/signals.js +103 -0
- package/dist/cjs/sparse-set.js +169 -0
- package/dist/cjs/synctest.js +136 -0
- package/dist/cjs/system-lifecycle.js +286 -0
- package/dist/cjs/timers.js +61 -0
- package/dist/cjs/tools/static-checker.js +1468 -0
- package/dist/cjs/types.js +2 -0
- package/dist/cjs/venus-mp-budget.js +125 -0
- package/dist/cjs/ws-frame.js +255 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +11379 -0
- package/dist/collections.d.ts +9 -0
- package/dist/collections.js +30 -0
- package/dist/collision.d.ts +25 -0
- package/dist/collision.js +85 -0
- package/dist/commands.d.ts +137 -0
- package/dist/commands.js +977 -0
- package/dist/config-bundle.d.ts +89 -0
- package/dist/config-bundle.js +483 -0
- package/dist/creator.d.ts +20 -0
- package/dist/creator.js +11 -0
- package/dist/deterministic-primitives.d.ts +59 -0
- package/dist/deterministic-primitives.js +126 -0
- package/dist/deterministic-session.d.ts +197 -0
- package/dist/deterministic-session.js +1401 -0
- package/dist/ecs-lite.d.ts +26 -0
- package/dist/ecs-lite.js +152 -0
- package/dist/effects-adapter.d.ts +21 -0
- package/dist/effects-adapter.js +94 -0
- package/dist/engine-complete.d.ts +45 -0
- package/dist/engine-complete.js +2292 -0
- package/dist/engine-core-proof.d.ts +26 -0
- package/dist/engine-core-proof.js +66 -0
- package/dist/engine-parity-matrix.d.ts +300 -0
- package/dist/engine-parity-matrix.js +869 -0
- package/dist/engine-parity.d.ts +74 -0
- package/dist/engine-parity.js +3178 -0
- package/dist/engine-runtime.d.ts +351 -0
- package/dist/engine-runtime.js +3123 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +17 -0
- package/dist/events.d.ts +45 -0
- package/dist/events.js +242 -0
- package/dist/examples/counter-duel.d.ts +43 -0
- package/dist/examples/counter-duel.js +150 -0
- package/dist/examples/fixed-arena.d.ts +110 -0
- package/dist/examples/fixed-arena.js +356 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +66 -0
- package/dist/input-authority.d.ts +82 -0
- package/dist/input-authority.js +147 -0
- package/dist/input-button.d.ts +36 -0
- package/dist/input-button.js +249 -0
- package/dist/input-timeline.d.ts +65 -0
- package/dist/input-timeline.js +412 -0
- package/dist/instant-replay.d.ts +59 -0
- package/dist/instant-replay.js +62 -0
- package/dist/lag-compensation3d.d.ts +146 -0
- package/dist/lag-compensation3d.js +827 -0
- package/dist/local-authority-server-runner.d.ts +79 -0
- package/dist/local-authority-server-runner.js +1118 -0
- package/dist/local-authority-wire.d.ts +142 -0
- package/dist/local-authority-wire.js +1477 -0
- package/dist/local-authority.d.ts +160 -0
- package/dist/local-authority.js +810 -0
- package/dist/local-ws-adapter.d.ts +14 -0
- package/dist/local-ws-adapter.js +146 -0
- package/dist/match-log.d.ts +74 -0
- package/dist/match-log.js +57 -0
- package/dist/math.d.ts +41 -0
- package/dist/math.js +154 -0
- package/dist/mock-network-adapter.d.ts +13 -0
- package/dist/mock-network-adapter.js +23 -0
- package/dist/movement.d.ts +155 -0
- package/dist/movement.js +694 -0
- package/dist/movement3d.d.ts +195 -0
- package/dist/movement3d.js +1238 -0
- package/dist/multiclient-authority.d.ts +30 -0
- package/dist/multiclient-authority.js +305 -0
- package/dist/multiplayer-service.d.ts +153 -0
- package/dist/multiplayer-service.js +429 -0
- package/dist/navigation.d.ts +91 -0
- package/dist/navigation.js +338 -0
- package/dist/navmesh.d.ts +236 -0
- package/dist/navmesh.js +1423 -0
- package/dist/networked-client.d.ts +124 -0
- package/dist/networked-client.js +688 -0
- package/dist/networked-input-authority.d.ts +51 -0
- package/dist/networked-input-authority.js +439 -0
- package/dist/node.d.ts +22 -0
- package/dist/node.js +19 -0
- package/dist/number-lab.d.ts +15 -0
- package/dist/number-lab.js +75 -0
- package/dist/offline-session.d.ts +2 -0
- package/dist/offline-session.js +367 -0
- package/dist/pause-resume.d.ts +48 -0
- package/dist/pause-resume.js +40 -0
- package/dist/physics-cert.d.ts +40 -0
- package/dist/physics-cert.js +389 -0
- package/dist/physics2d.d.ts +142 -0
- package/dist/physics2d.js +741 -0
- package/dist/physics3d.d.ts +285 -0
- package/dist/physics3d.js +1979 -0
- package/dist/prediction-culling.d.ts +34 -0
- package/dist/prediction-culling.js +200 -0
- package/dist/protocol-lab.d.ts +115 -0
- package/dist/protocol-lab.js +1295 -0
- package/dist/r3f-render-adapter.d.ts +61 -0
- package/dist/r3f-render-adapter.js +203 -0
- package/dist/random.d.ts +13 -0
- package/dist/random.js +55 -0
- package/dist/replay-bundle.d.ts +150 -0
- package/dist/replay-bundle.js +1586 -0
- package/dist/replay-inspect.d.ts +60 -0
- package/dist/replay-inspect.js +228 -0
- package/dist/replay-scoring.d.ts +28 -0
- package/dist/replay-scoring.js +65 -0
- package/dist/replay.d.ts +17 -0
- package/dist/replay.js +351 -0
- package/dist/reports.d.ts +176 -0
- package/dist/reports.js +174 -0
- package/dist/rollback-history.d.ts +46 -0
- package/dist/rollback-history.js +247 -0
- package/dist/runtime-guards.d.ts +1 -0
- package/dist/runtime-guards.js +108 -0
- package/dist/sample-lab.d.ts +32 -0
- package/dist/sample-lab.js +134 -0
- package/dist/sample-scenes.d.ts +86 -0
- package/dist/sample-scenes.js +745 -0
- package/dist/schema-artifacts.d.ts +49 -0
- package/dist/schema-artifacts.js +356 -0
- package/dist/schema-codegen.d.ts +21 -0
- package/dist/schema-codegen.js +538 -0
- package/dist/schema-dsl-binary.d.ts +75 -0
- package/dist/schema-dsl-binary.js +648 -0
- package/dist/schema.d.ts +87 -0
- package/dist/schema.js +315 -0
- package/dist/sdk-offline.d.ts +34 -0
- package/dist/sdk-offline.js +96 -0
- package/dist/session-promotion.d.ts +26 -0
- package/dist/session-promotion.js +70 -0
- package/dist/session-snapshot.d.ts +95 -0
- package/dist/session-snapshot.js +255 -0
- package/dist/session-wire.d.ts +89 -0
- package/dist/session-wire.js +211 -0
- package/dist/signals.d.ts +31 -0
- package/dist/signals.js +99 -0
- package/dist/sparse-set.d.ts +34 -0
- package/dist/sparse-set.js +165 -0
- package/dist/synctest.d.ts +31 -0
- package/dist/synctest.js +133 -0
- package/dist/system-lifecycle.d.ts +72 -0
- package/dist/system-lifecycle.js +281 -0
- package/dist/timers.d.ts +17 -0
- package/dist/timers.js +53 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/static-checker.d.ts +31 -0
- package/dist/tools/static-checker.js +1462 -0
- package/dist/tools/vite-plugin.d.ts +69 -0
- package/dist/tools/vite-plugin.js +203 -0
- package/dist/types.d.ts +148 -0
- package/dist/types.js +1 -0
- package/dist/venus-mp-budget.d.ts +29 -0
- package/dist/venus-mp-budget.js +122 -0
- package/dist/ws-frame.d.ts +49 -0
- package/dist/ws-frame.js +247 -0
- package/package.json +169 -0
|
@@ -0,0 +1,1410 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createDeterministicSessionCore = createDeterministicSessionCore;
|
|
4
|
+
exports.createDeterministicSessionReplayBundle = createDeterministicSessionReplayBundle;
|
|
5
|
+
exports.verifyDeterministicSessionRuntimeReplay = verifyDeterministicSessionRuntimeReplay;
|
|
6
|
+
exports.runDeterministicSessionCoreFixture = runDeterministicSessionCoreFixture;
|
|
7
|
+
exports.runDeterministicStructuredSessionInputFixture = runDeterministicStructuredSessionInputFixture;
|
|
8
|
+
exports.runDeterministicSessionAuthorityFixture = runDeterministicSessionAuthorityFixture;
|
|
9
|
+
exports.runDeterministicSessionLifecycleProtocolFixture = runDeterministicSessionLifecycleProtocolFixture;
|
|
10
|
+
const canonical_js_1 = require("./canonical.js");
|
|
11
|
+
const commands_js_1 = require("./commands.js");
|
|
12
|
+
const engine_runtime_js_1 = require("./engine-runtime.js");
|
|
13
|
+
const random_js_1 = require("./random.js");
|
|
14
|
+
function createDeterministicSessionCore(options) {
|
|
15
|
+
assertPositiveInteger(options.players, 'players');
|
|
16
|
+
if (options.reconnectGraceFrames !== undefined) {
|
|
17
|
+
assertNonNegativeInteger(options.reconnectGraceFrames, 'reconnectGraceFrames');
|
|
18
|
+
}
|
|
19
|
+
const seed = options.seed ?? 1;
|
|
20
|
+
const inputSingletonId = options.inputSingletonId ?? 'sessionInput';
|
|
21
|
+
const mapInputsToSingleton = options.mapInputsToSingleton ?? defaultInputSingletonMapper;
|
|
22
|
+
const commandChannels = options.commandChannels ?? options.descriptor.commandChannels ?? [];
|
|
23
|
+
const commandTimelineOptions = {
|
|
24
|
+
playerCount: options.players,
|
|
25
|
+
currentFrame: 0,
|
|
26
|
+
channels: commandChannels,
|
|
27
|
+
validAssetRefs: options.validCommandAssetRefs,
|
|
28
|
+
validatePayload: options.validateCommandPayload,
|
|
29
|
+
};
|
|
30
|
+
const verifiedCommandTimeline = commandChannels.length === 0 ? undefined : (0, commands_js_1.createDeterministicCommandTimeline)(commandTimelineOptions);
|
|
31
|
+
const predictedCommandTimeline = commandChannels.length === 0 ? undefined : (0, commands_js_1.createDeterministicCommandTimeline)(commandTimelineOptions);
|
|
32
|
+
const verified = (0, engine_runtime_js_1.createDeterministicFrameRuntime)(options.descriptor, seed, { commandTimeline: verifiedCommandTimeline });
|
|
33
|
+
const predicted = (0, engine_runtime_js_1.createDeterministicFrameRuntime)(options.descriptor, seed, { commandTimeline: predictedCommandTimeline });
|
|
34
|
+
const confirmedInputs = new Map();
|
|
35
|
+
const predictedInputs = new Map();
|
|
36
|
+
const replacementInputs = new Map();
|
|
37
|
+
const confirmedCommands = new Map();
|
|
38
|
+
const predictedCommands = new Map();
|
|
39
|
+
const confirmedCommandsByFrame = new Map();
|
|
40
|
+
const predictedCommandsByFrame = new Map();
|
|
41
|
+
const rejectedCommands = [];
|
|
42
|
+
const lifecycleEvents = [];
|
|
43
|
+
const players = Array.from({ length: options.players }, (_, slot) => ({
|
|
44
|
+
slot,
|
|
45
|
+
actorId: '',
|
|
46
|
+
active: false,
|
|
47
|
+
spectator: true,
|
|
48
|
+
joinedFrame: 0,
|
|
49
|
+
}));
|
|
50
|
+
let predictedPreviousFrame;
|
|
51
|
+
let correctionCount = 0;
|
|
52
|
+
let replacementInputCount = 0;
|
|
53
|
+
const verifiedChecksumHistory = new Map();
|
|
54
|
+
function acceptedLifecycleEvent(type, slot, actorId, frame) {
|
|
55
|
+
return { type, slot, actorId, frame, accepted: true };
|
|
56
|
+
}
|
|
57
|
+
function rejectedLifecycleEvent(type, slot, actorId, frame, reason) {
|
|
58
|
+
return { type, slot, actorId, frame, accepted: false, reason };
|
|
59
|
+
}
|
|
60
|
+
function applyLifecycle(event) {
|
|
61
|
+
lifecycleEvents.push((0, canonical_js_1.cloneCanonical)(event));
|
|
62
|
+
if (!event.accepted) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const existing = players[event.slot];
|
|
66
|
+
if (event.type === 'join-spectator') {
|
|
67
|
+
players[event.slot] = { ...existing, actorId: event.actorId, spectator: true, active: false, joinedFrame: event.frame, removedFrame: undefined };
|
|
68
|
+
}
|
|
69
|
+
if (event.type === 'add-player') {
|
|
70
|
+
players[event.slot] = { ...existing, actorId: event.actorId, spectator: false, active: true, joinedFrame: event.frame, removedFrame: undefined };
|
|
71
|
+
}
|
|
72
|
+
if (event.type === 'remove-player') {
|
|
73
|
+
players[event.slot] = { ...existing, active: false, spectator: true, removedFrame: event.frame };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function stepRuntime(runtime, frame, inputs, usePrediction) {
|
|
77
|
+
const commands = commandsForFrame(frame, usePrediction);
|
|
78
|
+
syncRuntimeCommandTimeline(runtime === verified ? verifiedCommandTimeline : predictedCommandTimeline, frame, commands);
|
|
79
|
+
runtime.setSingleton(inputSingletonId, mapInputsToSingleton(frame, inputs, playersForFrame(frame), commands));
|
|
80
|
+
runtime.step(options.systems);
|
|
81
|
+
}
|
|
82
|
+
function inputsForFrame(frame, usePrediction) {
|
|
83
|
+
return playersForFrame(frame)
|
|
84
|
+
.filter((player) => playerAcceptsInputAtFrame(player, frame))
|
|
85
|
+
.map((player) => inputForPlayerFrame(frame, player.slot, usePrediction))
|
|
86
|
+
.sort(compareInputs);
|
|
87
|
+
}
|
|
88
|
+
function playersForFrame(frame) {
|
|
89
|
+
const effective = Array.from({ length: options.players }, (_, slot) => ({
|
|
90
|
+
slot,
|
|
91
|
+
actorId: '',
|
|
92
|
+
active: false,
|
|
93
|
+
spectator: true,
|
|
94
|
+
joinedFrame: 0,
|
|
95
|
+
}));
|
|
96
|
+
for (const event of lifecycleEvents.filter((candidate) => candidate.accepted && candidate.frame <= frame).sort(compareLifecycleEvents)) {
|
|
97
|
+
const current = effective[event.slot];
|
|
98
|
+
if (event.type === 'join-spectator') {
|
|
99
|
+
effective[event.slot] = { ...current, actorId: event.actorId, spectator: true, active: false, joinedFrame: event.frame, removedFrame: undefined };
|
|
100
|
+
}
|
|
101
|
+
if (event.type === 'add-player') {
|
|
102
|
+
effective[event.slot] = { ...current, actorId: event.actorId, spectator: false, active: true, joinedFrame: event.frame, removedFrame: undefined };
|
|
103
|
+
}
|
|
104
|
+
if (event.type === 'remove-player') {
|
|
105
|
+
effective[event.slot] = { ...current, active: false, spectator: true, removedFrame: event.frame };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return effective;
|
|
109
|
+
}
|
|
110
|
+
function playerAcceptsInputAtFrame(player, frame) {
|
|
111
|
+
return player.active
|
|
112
|
+
&& !player.spectator
|
|
113
|
+
&& player.joinedFrame <= frame;
|
|
114
|
+
}
|
|
115
|
+
function inputForPlayerFrame(frame, slot, usePrediction) {
|
|
116
|
+
const confirmed = confirmedInputs.get(inputKey(frame, slot));
|
|
117
|
+
if (confirmed !== undefined) {
|
|
118
|
+
return confirmed;
|
|
119
|
+
}
|
|
120
|
+
if (usePrediction) {
|
|
121
|
+
const predictedInput = predictedInputs.get(inputKey(frame, slot));
|
|
122
|
+
if (predictedInput !== undefined) {
|
|
123
|
+
return predictedInput;
|
|
124
|
+
}
|
|
125
|
+
return predictedFallbackInput(frame, slot);
|
|
126
|
+
}
|
|
127
|
+
const replacement = replacementInputs.get(inputKey(frame, slot));
|
|
128
|
+
if (replacement !== undefined) {
|
|
129
|
+
return replacement;
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Cannot verify deterministic frame ${frame}; missing confirmed input for slot ${slot}`);
|
|
132
|
+
}
|
|
133
|
+
function predictedFallbackInput(frame, slot) {
|
|
134
|
+
const previous = previousRepeatableInput(frame, slot);
|
|
135
|
+
const flags = [];
|
|
136
|
+
if (previous.repeated) {
|
|
137
|
+
flags.push('repeatable');
|
|
138
|
+
}
|
|
139
|
+
return normalizeInput({ frame, slot, value: previous.value, data: previous.data, flags });
|
|
140
|
+
}
|
|
141
|
+
function storeReplacementInput(input) {
|
|
142
|
+
assertInput(input);
|
|
143
|
+
const playerActive = playerAcceptsInputAtFrame(playersForFrame(input.frame)[input.slot], input.frame);
|
|
144
|
+
const flags = new Set(input.flags ?? []);
|
|
145
|
+
if (playerActive) {
|
|
146
|
+
flags.delete('player-not-present');
|
|
147
|
+
flags.add('replaced-by-server');
|
|
148
|
+
}
|
|
149
|
+
else if (!flags.has('player-not-present')) {
|
|
150
|
+
throw new Error(`Cannot replace input for inactive deterministic slot ${input.slot} at frame ${input.frame}`);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
flags.delete('replaced-by-server');
|
|
154
|
+
}
|
|
155
|
+
const normalized = normalizeInput({ ...input, flags: [...flags] });
|
|
156
|
+
const key = inputKey(input.frame, input.slot);
|
|
157
|
+
const hadReplacement = replacementInputs.has(key);
|
|
158
|
+
replacementInputs.set(key, normalized);
|
|
159
|
+
if (!hadReplacement) {
|
|
160
|
+
replacementInputCount += 1;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function previousRepeatableInput(frame, slot) {
|
|
164
|
+
for (let previousFrame = frame - 1; previousFrame >= 0; previousFrame -= 1) {
|
|
165
|
+
const previous = confirmedInputs.get(inputKey(previousFrame, slot)) ?? predictedInputs.get(inputKey(previousFrame, slot));
|
|
166
|
+
if (previous !== undefined && previous.flags.includes('repeatable')) {
|
|
167
|
+
return { value: previous.value, data: previous.data, repeated: true };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { value: 0, repeated: false };
|
|
171
|
+
}
|
|
172
|
+
function assertInput(input) {
|
|
173
|
+
assertNonNegativeInteger(input.frame, 'input.frame');
|
|
174
|
+
assertSlot(input.slot, options.players);
|
|
175
|
+
if (!Number.isInteger(input.value)) {
|
|
176
|
+
throw new Error('Deterministic session input value must be an integer');
|
|
177
|
+
}
|
|
178
|
+
if (input.data !== undefined) {
|
|
179
|
+
(0, canonical_js_1.canonicalStringify)(input.data);
|
|
180
|
+
if (options.inputDataFields !== undefined) {
|
|
181
|
+
(0, commands_js_1.assertDeterministicPayloadFields)('input.data', input.data, options.inputDataFields);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (options.inputDataFields !== undefined) {
|
|
185
|
+
throw new Error('input.data is missing');
|
|
186
|
+
}
|
|
187
|
+
for (const flag of input.flags ?? []) {
|
|
188
|
+
assertInputFlag(flag);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function recordSessionCommand(timeline, commands, command) {
|
|
192
|
+
if (timeline === undefined) {
|
|
193
|
+
throw new Error('Deterministic session command channels are not configured');
|
|
194
|
+
}
|
|
195
|
+
const submittedFrame = command.submittedFrame ?? command.frame;
|
|
196
|
+
if (!Number.isInteger(command.slot) || command.slot < 0 || command.slot >= options.players) {
|
|
197
|
+
const rejection = { commandId: command.id, reason: 'unauthorized-slot' };
|
|
198
|
+
rejectedCommands.push(rejection);
|
|
199
|
+
return rejection;
|
|
200
|
+
}
|
|
201
|
+
if (!playerAcceptsInputAtFrame(playersForFrame(command.frame)[command.slot], command.frame)) {
|
|
202
|
+
const rejection = { commandId: command.id, reason: 'unauthorized-slot' };
|
|
203
|
+
rejectedCommands.push(rejection);
|
|
204
|
+
return rejection;
|
|
205
|
+
}
|
|
206
|
+
if (submittedFrame >= timeline.currentFrame) {
|
|
207
|
+
timeline.setCurrentFrame(submittedFrame);
|
|
208
|
+
}
|
|
209
|
+
const rejection = timeline.record(command);
|
|
210
|
+
if (rejection !== undefined) {
|
|
211
|
+
rejectedCommands.push(rejection);
|
|
212
|
+
return rejection;
|
|
213
|
+
}
|
|
214
|
+
const accepted = timeline.accepted.find((candidate) => candidate.id === command.id);
|
|
215
|
+
commands.set(accepted.id, accepted);
|
|
216
|
+
addCommandToFrameIndex(commands === confirmedCommands ? confirmedCommandsByFrame : predictedCommandsByFrame, accepted);
|
|
217
|
+
if (commands === confirmedCommands) {
|
|
218
|
+
predictedCommands.delete(accepted.id);
|
|
219
|
+
removeCommandFromFrameIndex(predictedCommandsByFrame, accepted.id);
|
|
220
|
+
}
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
function commandsForFrame(frame, usePrediction) {
|
|
224
|
+
const commands = new Map();
|
|
225
|
+
for (const command of confirmedCommandsByFrame.get(frame) ?? []) {
|
|
226
|
+
commands.set(command.id, command);
|
|
227
|
+
}
|
|
228
|
+
if (usePrediction) {
|
|
229
|
+
for (const command of predictedCommandsByFrame.get(frame) ?? []) {
|
|
230
|
+
if (!commands.has(command.id)) {
|
|
231
|
+
commands.set(command.id, command);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return [...commands.values()].sort(compareCommands);
|
|
236
|
+
}
|
|
237
|
+
function syncRuntimeCommandTimeline(timeline, frame, commands) {
|
|
238
|
+
if (timeline === undefined) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
timeline.restore((0, commands_js_1.createDeterministicCommandTimelineSnapshot)({
|
|
242
|
+
currentFrame: frame,
|
|
243
|
+
accepted: commands,
|
|
244
|
+
rejected: [],
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
const session = {
|
|
248
|
+
get players() {
|
|
249
|
+
return players.map((player) => (0, canonical_js_1.cloneCanonical)(player));
|
|
250
|
+
},
|
|
251
|
+
get frameVerified() {
|
|
252
|
+
return verified.snapshot();
|
|
253
|
+
},
|
|
254
|
+
get framePredicted() {
|
|
255
|
+
return predicted.snapshot();
|
|
256
|
+
},
|
|
257
|
+
get framePredictedPrevious() {
|
|
258
|
+
return predictedPreviousFrame === undefined ? undefined : (0, canonical_js_1.cloneCanonical)(predictedPreviousFrame);
|
|
259
|
+
},
|
|
260
|
+
get correctionCount() {
|
|
261
|
+
return correctionCount;
|
|
262
|
+
},
|
|
263
|
+
get replacementInputCount() {
|
|
264
|
+
return replacementInputCount;
|
|
265
|
+
},
|
|
266
|
+
joinAsSpectator(slot, actorId, frame = verified.frame) {
|
|
267
|
+
assertSlot(slot, options.players);
|
|
268
|
+
assertNonEmptyActor(actorId);
|
|
269
|
+
const current = players[slot];
|
|
270
|
+
const event = current.actorId.length > 0 && current.actorId !== actorId
|
|
271
|
+
? rejectedLifecycleEvent('join-spectator', slot, actorId, frame, 'slot-already-owned')
|
|
272
|
+
: acceptedLifecycleEvent('join-spectator', slot, actorId, frame);
|
|
273
|
+
applyLifecycle(event);
|
|
274
|
+
return (0, canonical_js_1.cloneCanonical)(event);
|
|
275
|
+
},
|
|
276
|
+
addPlayer(slot, actorId, frame = verified.frame) {
|
|
277
|
+
assertSlot(slot, options.players);
|
|
278
|
+
assertNonEmptyActor(actorId);
|
|
279
|
+
const current = players[slot];
|
|
280
|
+
const event = current.active
|
|
281
|
+
? rejectedLifecycleEvent('add-player', slot, actorId, frame, 'slot-already-active')
|
|
282
|
+
: current.actorId.length > 0 && current.actorId !== actorId
|
|
283
|
+
? rejectedLifecycleEvent('add-player', slot, actorId, frame, 'actor-mismatch')
|
|
284
|
+
: current.actorId === actorId
|
|
285
|
+
&& current.removedFrame !== undefined
|
|
286
|
+
&& options.reconnectGraceFrames !== undefined
|
|
287
|
+
&& frame - current.removedFrame > options.reconnectGraceFrames
|
|
288
|
+
? rejectedLifecycleEvent('add-player', slot, actorId, frame, 'reconnect-expired')
|
|
289
|
+
: acceptedLifecycleEvent('add-player', slot, actorId, frame);
|
|
290
|
+
applyLifecycle(event);
|
|
291
|
+
return (0, canonical_js_1.cloneCanonical)(event);
|
|
292
|
+
},
|
|
293
|
+
removePlayer(slot, actorId, frame = verified.frame) {
|
|
294
|
+
assertSlot(slot, options.players);
|
|
295
|
+
assertNonEmptyActor(actorId);
|
|
296
|
+
const current = players[slot];
|
|
297
|
+
const event = !current.active
|
|
298
|
+
? rejectedLifecycleEvent('remove-player', slot, actorId, frame, 'slot-not-active')
|
|
299
|
+
: current.actorId !== actorId
|
|
300
|
+
? rejectedLifecycleEvent('remove-player', slot, actorId, frame, 'actor-mismatch')
|
|
301
|
+
: acceptedLifecycleEvent('remove-player', slot, actorId, frame);
|
|
302
|
+
applyLifecycle(event);
|
|
303
|
+
return (0, canonical_js_1.cloneCanonical)(event);
|
|
304
|
+
},
|
|
305
|
+
submitPredictedInput(input) {
|
|
306
|
+
assertInput(input);
|
|
307
|
+
if (!playerAcceptsInputAtFrame(playersForFrame(input.frame)[input.slot], input.frame)) {
|
|
308
|
+
throw new Error(`Cannot predict input for inactive deterministic slot ${input.slot} at frame ${input.frame}`);
|
|
309
|
+
}
|
|
310
|
+
predictedInputs.set(inputKey(input.frame, input.slot), normalizeInput(input));
|
|
311
|
+
},
|
|
312
|
+
confirmInput(input) {
|
|
313
|
+
assertInput(input);
|
|
314
|
+
if (!playerAcceptsInputAtFrame(playersForFrame(input.frame)[input.slot], input.frame)) {
|
|
315
|
+
throw new Error(`Cannot confirm input for inactive deterministic slot ${input.slot} at frame ${input.frame}`);
|
|
316
|
+
}
|
|
317
|
+
confirmedInputs.set(inputKey(input.frame, input.slot), normalizeInput(input));
|
|
318
|
+
},
|
|
319
|
+
replaceInput(input) {
|
|
320
|
+
storeReplacementInput(input);
|
|
321
|
+
},
|
|
322
|
+
submitPredictedCommand(command) {
|
|
323
|
+
return recordSessionCommand(predictedCommandTimeline, predictedCommands, { ...command, predicted: command.predicted ?? true });
|
|
324
|
+
},
|
|
325
|
+
confirmCommand(command) {
|
|
326
|
+
return recordSessionCommand(verifiedCommandTimeline, confirmedCommands, { ...command, predicted: false });
|
|
327
|
+
},
|
|
328
|
+
pollCommands(frame, slot, channelId) {
|
|
329
|
+
assertNonNegativeInteger(frame, 'frame');
|
|
330
|
+
return commandsForFrame(frame, true)
|
|
331
|
+
.filter((command) => slot === undefined || command.slot === slot)
|
|
332
|
+
.filter((command) => channelId === undefined || command.channelId === channelId)
|
|
333
|
+
.sort(compareCommands)
|
|
334
|
+
.map((command) => (0, canonical_js_1.cloneCanonical)(command));
|
|
335
|
+
},
|
|
336
|
+
predictTo(frameExclusive) {
|
|
337
|
+
assertNonNegativeInteger(frameExclusive, 'frameExclusive');
|
|
338
|
+
if (frameExclusive < verified.frame) {
|
|
339
|
+
throw new Error(`Cannot predict behind verified frame ${verified.frame}`);
|
|
340
|
+
}
|
|
341
|
+
predictedPreviousFrame = predicted.snapshot();
|
|
342
|
+
predicted.restore(verified.snapshot());
|
|
343
|
+
for (let frame = verified.frame; frame < frameExclusive; frame += 1) {
|
|
344
|
+
stepRuntime(predicted, frame, inputsForFrame(frame, true), true);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
advanceVerifiedTo(frameExclusive) {
|
|
348
|
+
assertNonNegativeInteger(frameExclusive, 'frameExclusive');
|
|
349
|
+
if (frameExclusive < verified.frame) {
|
|
350
|
+
throw new Error(`Cannot move verified frame backward from ${verified.frame} to ${frameExclusive}`);
|
|
351
|
+
}
|
|
352
|
+
for (let frame = verified.frame; frame < frameExclusive; frame += 1) {
|
|
353
|
+
stepRuntime(verified, frame, inputsForFrame(frame, false), false);
|
|
354
|
+
const snapshot = verified.snapshot();
|
|
355
|
+
verifiedChecksumHistory.set(snapshot.frame, snapshot.checksum);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
applyCorrection(snapshot) {
|
|
359
|
+
if (snapshot.frame < verified.frame) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
verified.restore(snapshot);
|
|
363
|
+
correctionCount += 1;
|
|
364
|
+
return true;
|
|
365
|
+
},
|
|
366
|
+
isFrameVerified(frame) {
|
|
367
|
+
assertNonNegativeInteger(frame, 'frame');
|
|
368
|
+
return frame < verified.frame;
|
|
369
|
+
},
|
|
370
|
+
getInputFlags(frame, slot) {
|
|
371
|
+
assertNonNegativeInteger(frame, 'frame');
|
|
372
|
+
assertSlot(slot, options.players);
|
|
373
|
+
if (!session.isFrameVerified(frame)) {
|
|
374
|
+
return [];
|
|
375
|
+
}
|
|
376
|
+
const input = confirmedInputs.get(inputKey(frame, slot))
|
|
377
|
+
?? replacementInputs.get(inputKey(frame, slot));
|
|
378
|
+
return input === undefined ? [] : [...input.flags].sort(compareStrings);
|
|
379
|
+
},
|
|
380
|
+
snapshot() {
|
|
381
|
+
const withoutChecksum = {
|
|
382
|
+
verifiedFrame: verified.snapshot(),
|
|
383
|
+
predictedFrame: predicted.snapshot(),
|
|
384
|
+
predictedPreviousFrame,
|
|
385
|
+
players,
|
|
386
|
+
confirmedInputs: sortedInputValues(confirmedInputs),
|
|
387
|
+
predictedInputs: sortedInputValues(predictedInputs),
|
|
388
|
+
replacementInputs: sortedInputValues(replacementInputs),
|
|
389
|
+
confirmedCommands: sortedCommandValues(confirmedCommands),
|
|
390
|
+
predictedCommands: sortedCommandValues(predictedCommands),
|
|
391
|
+
rejectedCommands: [...rejectedCommands].sort(compareRejectedCommands),
|
|
392
|
+
lifecycleEvents,
|
|
393
|
+
correctionCount,
|
|
394
|
+
replacementInputCount,
|
|
395
|
+
};
|
|
396
|
+
return (0, canonical_js_1.cloneCanonical)({ ...withoutChecksum, checksum: (0, canonical_js_1.defaultChecksum)(withoutChecksum) });
|
|
397
|
+
},
|
|
398
|
+
exportReplay() {
|
|
399
|
+
return createDeterministicSessionReplayBundle({
|
|
400
|
+
descriptorHash: (0, canonical_js_1.defaultChecksum)(options.descriptor),
|
|
401
|
+
runtimeVersion: 'deterministic-session@1',
|
|
402
|
+
players: options.players,
|
|
403
|
+
seed,
|
|
404
|
+
inputSingletonId,
|
|
405
|
+
confirmedInputs: sortedInputValues(confirmedInputs),
|
|
406
|
+
replacementInputs: sortedInputValues(replacementInputs),
|
|
407
|
+
confirmedCommands: sortedCommandValues(confirmedCommands),
|
|
408
|
+
lifecycleEvents,
|
|
409
|
+
verifiedChecksums: sortedVerifiedChecksums(verifiedChecksumHistory, verified.snapshot()),
|
|
410
|
+
finalVerifiedChecksum: verified.snapshot().checksum,
|
|
411
|
+
});
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
return session;
|
|
415
|
+
}
|
|
416
|
+
function createDeterministicSessionReplayBundle(bundle) {
|
|
417
|
+
const normalized = normalizedSessionReplayBundleForChecksum({
|
|
418
|
+
schemaVersion: 1,
|
|
419
|
+
...bundle,
|
|
420
|
+
});
|
|
421
|
+
return (0, canonical_js_1.cloneCanonical)({ ...normalized, checksum: (0, canonical_js_1.defaultChecksum)(normalized) });
|
|
422
|
+
}
|
|
423
|
+
function verifyDeterministicSessionRuntimeReplay(bundle, options) {
|
|
424
|
+
const expectedReplayChecksum = (0, canonical_js_1.defaultChecksum)(normalizedSessionReplayBundleForChecksum(bundle));
|
|
425
|
+
if (bundle.checksum !== expectedReplayChecksum) {
|
|
426
|
+
return replayVerification('fail', bundle, expectedReplayChecksum, bundle.verifiedChecksums.length, 0);
|
|
427
|
+
}
|
|
428
|
+
if (bundle.schemaVersion !== 1 || bundle.descriptorHash !== (0, canonical_js_1.defaultChecksum)(options.descriptor)) {
|
|
429
|
+
return replayVerification('fail', bundle, '', 0, 0);
|
|
430
|
+
}
|
|
431
|
+
const replay = createDeterministicSessionCore({ ...options, seed: bundle.seed, inputSingletonId: bundle.inputSingletonId });
|
|
432
|
+
const confirmedInputsByFrame = new Map();
|
|
433
|
+
for (const input of bundle.confirmedInputs) {
|
|
434
|
+
const entries = confirmedInputsByFrame.get(input.frame) ?? [];
|
|
435
|
+
entries.push(input);
|
|
436
|
+
confirmedInputsByFrame.set(input.frame, entries);
|
|
437
|
+
}
|
|
438
|
+
const confirmedInputKeys = new Set(bundle.confirmedInputs.map((input) => inputKey(input.frame, input.slot)));
|
|
439
|
+
const replacementInputsByFrame = new Map();
|
|
440
|
+
for (const input of bundle.replacementInputs) {
|
|
441
|
+
if (confirmedInputKeys.has(inputKey(input.frame, input.slot))) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const entries = replacementInputsByFrame.get(input.frame) ?? [];
|
|
445
|
+
entries.push(input);
|
|
446
|
+
replacementInputsByFrame.set(input.frame, entries);
|
|
447
|
+
}
|
|
448
|
+
const lifecycleByFrame = new Map();
|
|
449
|
+
for (const event of bundle.lifecycleEvents) {
|
|
450
|
+
const entries = lifecycleByFrame.get(event.frame) ?? [];
|
|
451
|
+
entries.push(event);
|
|
452
|
+
lifecycleByFrame.set(event.frame, entries);
|
|
453
|
+
}
|
|
454
|
+
const commandsByFrame = new Map();
|
|
455
|
+
for (const command of bundle.confirmedCommands) {
|
|
456
|
+
const entries = commandsByFrame.get(command.frame) ?? [];
|
|
457
|
+
entries.push(command);
|
|
458
|
+
commandsByFrame.set(command.frame, entries);
|
|
459
|
+
}
|
|
460
|
+
const lastInputFrame = Math.max(maxInputFrame(bundle.confirmedInputs), maxInputFrame(bundle.replacementInputs));
|
|
461
|
+
const lastLifecycleFrame = maxLifecycleFrame(bundle.lifecycleEvents);
|
|
462
|
+
const expectedChecksumsByFrame = new Map(bundle.verifiedChecksums.map((checksum) => [checksum.frame, checksum.checksum]));
|
|
463
|
+
const lastVerifiedStepFrame = Math.max(...bundle.verifiedChecksums.map((checksum) => checksum.frame), 0) - 1;
|
|
464
|
+
const lastFrame = Math.max(lastInputFrame, lastLifecycleFrame, maxCommandFrame(bundle.confirmedCommands), lastVerifiedStepFrame);
|
|
465
|
+
for (let frame = 0; frame <= lastFrame; frame += 1) {
|
|
466
|
+
for (const event of [...(lifecycleByFrame.get(frame) ?? [])].sort(compareLifecycleEvents)) {
|
|
467
|
+
if (!event.accepted) {
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (event.type === 'join-spectator') {
|
|
471
|
+
replay.joinAsSpectator(event.slot, event.actorId, event.frame);
|
|
472
|
+
}
|
|
473
|
+
if (event.type === 'add-player') {
|
|
474
|
+
replay.addPlayer(event.slot, event.actorId, event.frame);
|
|
475
|
+
}
|
|
476
|
+
if (event.type === 'remove-player') {
|
|
477
|
+
replay.removePlayer(event.slot, event.actorId, event.frame);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const input of [...(confirmedInputsByFrame.get(frame) ?? [])].sort(compareInputs)) {
|
|
481
|
+
try {
|
|
482
|
+
replay.confirmInput(input);
|
|
483
|
+
}
|
|
484
|
+
catch {
|
|
485
|
+
return replayVerification('fail', bundle, replay.frameVerified.checksum, bundle.verifiedChecksums.length, input.frame);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
for (const input of [...(replacementInputsByFrame.get(frame) ?? [])].sort(compareInputs)) {
|
|
489
|
+
try {
|
|
490
|
+
replay.replaceInput(input);
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (const command of [...(commandsByFrame.get(frame) ?? [])].sort(compareCommands)) {
|
|
497
|
+
let rejection;
|
|
498
|
+
try {
|
|
499
|
+
rejection = replay.confirmCommand(command);
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
return replayVerification('fail', bundle, replay.frameVerified.checksum, bundle.verifiedChecksums.length, command.frame);
|
|
503
|
+
}
|
|
504
|
+
if (rejection !== undefined) {
|
|
505
|
+
return replayVerification('fail', bundle, replay.frameVerified.checksum, bundle.verifiedChecksums.length, command.frame);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
replay.advanceVerifiedTo(frame + 1);
|
|
509
|
+
const expectedChecksum = expectedChecksumsByFrame.get(replay.frameVerified.frame);
|
|
510
|
+
if (expectedChecksum !== undefined && replay.frameVerified.checksum !== expectedChecksum) {
|
|
511
|
+
return replayVerification('fail', bundle, replay.frameVerified.checksum, bundle.verifiedChecksums.length, replay.frameVerified.frame);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const actualChecksum = replay.frameVerified.checksum;
|
|
515
|
+
const firstMismatchFrame = firstReplayMismatchFrame(bundle, replay.frameVerified);
|
|
516
|
+
return replayVerification(actualChecksum === bundle.finalVerifiedChecksum ? 'pass' : 'fail', bundle, actualChecksum, bundle.verifiedChecksums.length, firstMismatchFrame);
|
|
517
|
+
}
|
|
518
|
+
function normalizedSessionReplayBundleForChecksum(bundle) {
|
|
519
|
+
const { checksum: _ignoredChecksum, ...withoutChecksum } = bundle;
|
|
520
|
+
return {
|
|
521
|
+
...withoutChecksum,
|
|
522
|
+
confirmedInputs: [...bundle.confirmedInputs].map(normalizeInput).sort(compareInputs),
|
|
523
|
+
replacementInputs: [...bundle.replacementInputs].map(normalizeInput).sort(compareInputs),
|
|
524
|
+
confirmedCommands: [...bundle.confirmedCommands].sort(compareCommands),
|
|
525
|
+
lifecycleEvents: [...bundle.lifecycleEvents].sort(compareLifecycleEvents),
|
|
526
|
+
verifiedChecksums: [...bundle.verifiedChecksums].sort((left, right) => left.frame - right.frame),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function runDeterministicSessionCoreFixture(players, frames, seed) {
|
|
530
|
+
if (frames > 10_000) {
|
|
531
|
+
return runDeterministicSessionCoreStreamingFixture(players, frames, seed);
|
|
532
|
+
}
|
|
533
|
+
const descriptor = createSessionFixtureDescriptor();
|
|
534
|
+
const systems = createSessionFixtureSystems();
|
|
535
|
+
const session = createDeterministicSessionCore({ players, frames, seed, descriptor, systems });
|
|
536
|
+
const random = new random_js_1.DeterministicRandom(seed);
|
|
537
|
+
let staleCorrectionsRejected = 0;
|
|
538
|
+
let removedSlotInputRejected = 0;
|
|
539
|
+
let partialConfirmationHeldVerifiedFrame = false;
|
|
540
|
+
let predictedCommandCount = 0;
|
|
541
|
+
let confirmedCommandCount = 0;
|
|
542
|
+
const activeSlots = Array.from({ length: players }, () => false);
|
|
543
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
544
|
+
session.joinAsSpectator(slot, `actor-${slot}`, 0);
|
|
545
|
+
if (slot % 17 !== 0) {
|
|
546
|
+
session.addPlayer(slot, `actor-${slot}`, 0);
|
|
547
|
+
activeSlots[slot] = true;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
session.replaceInput({ frame: 0, slot, value: 0, flags: ['player-not-present'] });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
const duplicateAdd = players > 1
|
|
554
|
+
? session.addPlayer(1, 'actor-1', 0)
|
|
555
|
+
: session.addPlayer(0, 'actor-0', 0);
|
|
556
|
+
if (duplicateAdd.accepted) {
|
|
557
|
+
activeSlots[duplicateAdd.slot] = true;
|
|
558
|
+
}
|
|
559
|
+
const duplicateAddRejected = Number(!duplicateAdd.accepted);
|
|
560
|
+
const removeFrame = Math.max(2, Math.floor(frames / 3));
|
|
561
|
+
const readdFrame = Math.max(removeFrame + 1, Math.floor(frames / 2));
|
|
562
|
+
const hasConfirmedRepeatable = Array.from({ length: players }, () => false);
|
|
563
|
+
const lastConfirmedRepeatableValue = Array.from({ length: players }, () => 0);
|
|
564
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
565
|
+
if (frame === removeFrame && players > 3) {
|
|
566
|
+
session.removePlayer(3, 'actor-3', frame);
|
|
567
|
+
activeSlots[3] = false;
|
|
568
|
+
try {
|
|
569
|
+
session.confirmInput({ frame, slot: 3, value: 999 });
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
removedSlotInputRejected += 1;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (frame === readdFrame && players > 3) {
|
|
576
|
+
session.addPlayer(3, 'actor-3', frame);
|
|
577
|
+
activeSlots[3] = true;
|
|
578
|
+
}
|
|
579
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
580
|
+
if (!activeSlots[slot]) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const predictedValue = random.nextInt(-4, 4);
|
|
584
|
+
session.submitPredictedInput({ frame, slot, value: predictedValue, flags: predictedFlags(frame, slot) });
|
|
585
|
+
if ((frame + slot + seed) % 13 === 0) {
|
|
586
|
+
const payload = (frame + slot + seed) % 39 === 0
|
|
587
|
+
? { assetRef: 'ability.dash' }
|
|
588
|
+
: (frame + slot + seed) % 26 === 0
|
|
589
|
+
? 'ability.ping'
|
|
590
|
+
: { assetRef: 'ability.dash', power: (frame + slot) % 9 };
|
|
591
|
+
const predictedCommand = {
|
|
592
|
+
id: `predicted:${frame}:${slot}`,
|
|
593
|
+
slot,
|
|
594
|
+
frame,
|
|
595
|
+
channelId: 'ability',
|
|
596
|
+
payload,
|
|
597
|
+
predicted: true,
|
|
598
|
+
};
|
|
599
|
+
if (session.submitPredictedCommand(predictedCommand) === undefined) {
|
|
600
|
+
predictedCommandCount += 1;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if ((frame + slot + seed) % 11 === 0) {
|
|
604
|
+
const flags = ['replaced-by-server'];
|
|
605
|
+
if (hasConfirmedRepeatable[slot]) {
|
|
606
|
+
flags.push('repeatable');
|
|
607
|
+
}
|
|
608
|
+
session.replaceInput({ frame, slot, value: hasConfirmedRepeatable[slot] ? lastConfirmedRepeatableValue[slot] : 0, flags });
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
const confirmedValue = predictedValue + ((frame + slot) % 5 === 0 ? 1 : 0);
|
|
612
|
+
const flags = confirmedFlags(frame, slot);
|
|
613
|
+
session.confirmInput({ frame, slot, value: confirmedValue, flags });
|
|
614
|
+
if (flags.includes('repeatable')) {
|
|
615
|
+
hasConfirmedRepeatable[slot] = true;
|
|
616
|
+
lastConfirmedRepeatableValue[slot] = confirmedValue;
|
|
617
|
+
}
|
|
618
|
+
if ((frame + slot + seed) % 13 === 0) {
|
|
619
|
+
const payload = (frame + slot + seed) % 39 === 0
|
|
620
|
+
? { assetRef: 'ability.dash' }
|
|
621
|
+
: (frame + slot + seed) % 26 === 0
|
|
622
|
+
? 'ability.ping'
|
|
623
|
+
: { assetRef: 'ability.dash', power: (frame + slot) % 9 };
|
|
624
|
+
const confirmedCommand = {
|
|
625
|
+
id: `confirmed:${frame}:${slot}`,
|
|
626
|
+
slot,
|
|
627
|
+
frame,
|
|
628
|
+
channelId: (frame + slot + seed) % 52 === 0 ? 'missing' : 'ability',
|
|
629
|
+
payload,
|
|
630
|
+
};
|
|
631
|
+
if (session.confirmCommand(confirmedCommand) === undefined) {
|
|
632
|
+
confirmedCommandCount += 1;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const beforePartial = session.frameVerified.frame;
|
|
637
|
+
if (frame === 1) {
|
|
638
|
+
session.predictTo(frame + 3);
|
|
639
|
+
partialConfirmationHeldVerifiedFrame = session.frameVerified.frame === beforePartial;
|
|
640
|
+
}
|
|
641
|
+
session.advanceVerifiedTo(frame + 1);
|
|
642
|
+
session.predictTo(Math.min(frames, frame + 4));
|
|
643
|
+
if (frame === Math.floor(frames / 2)) {
|
|
644
|
+
const stale = { ...session.frameVerified, frame: Math.max(0, session.frameVerified.frame - 1) };
|
|
645
|
+
staleCorrectionsRejected += Number(!session.applyCorrection(stale));
|
|
646
|
+
session.applyCorrection(session.frameVerified);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const replay = session.exportReplay();
|
|
650
|
+
const replayVerification = verifyDeterministicSessionRuntimeReplay(replay, { players, descriptor, systems, seed });
|
|
651
|
+
const snapshot = session.snapshot();
|
|
652
|
+
const flagCounts = countSessionInputFlags(snapshot);
|
|
653
|
+
const lifecycle = snapshot.lifecycleEvents;
|
|
654
|
+
const acceptedAdds = lifecycle.filter((event) => event.type === 'add-player' && event.accepted).length;
|
|
655
|
+
const acceptedRemoves = lifecycle.filter((event) => event.type === 'remove-player' && event.accepted).length;
|
|
656
|
+
const metrics = {
|
|
657
|
+
players,
|
|
658
|
+
frames,
|
|
659
|
+
verifiedFrame: session.frameVerified.frame,
|
|
660
|
+
predictedFrame: session.framePredicted.frame,
|
|
661
|
+
verifiedChecksum: session.frameVerified.checksum,
|
|
662
|
+
predictedChecksum: session.framePredicted.checksum,
|
|
663
|
+
replayChecksum: replayVerification.replayChecksum,
|
|
664
|
+
replayVerified: replayVerification.result === 'pass',
|
|
665
|
+
replayFirstMismatchFrame: replayVerification.firstMismatchFrame,
|
|
666
|
+
correctionCount: session.correctionCount,
|
|
667
|
+
staleCorrectionsRejected,
|
|
668
|
+
replacementInputs: session.replacementInputCount,
|
|
669
|
+
repeatableInputs: flagCounts.repeatable,
|
|
670
|
+
playerNotPresentInputs: flagCounts.playerNotPresent,
|
|
671
|
+
serverReplacedInputs: flagCounts.serverReplaced,
|
|
672
|
+
commandFlagInputs: flagCounts.command,
|
|
673
|
+
predictedCommands: predictedCommandCount,
|
|
674
|
+
confirmedCommands: confirmedCommandCount,
|
|
675
|
+
rejectedCommands: snapshot.rejectedCommands.length,
|
|
676
|
+
commandReplayVerified: replayVerification.result === 'pass' && replay.confirmedCommands.length === confirmedCommandCount,
|
|
677
|
+
commandSystemPolls: Number(session.frameVerified.singletonComponents.sessionState.commands),
|
|
678
|
+
lifecycleEvents: lifecycle.length,
|
|
679
|
+
addPlayerAccepted: acceptedAdds,
|
|
680
|
+
removePlayerAccepted: acceptedRemoves,
|
|
681
|
+
duplicateAddRejected,
|
|
682
|
+
removedSlotInputRejected,
|
|
683
|
+
partialConfirmationHeldVerifiedFrame,
|
|
684
|
+
predictedPreviousAvailable: session.framePredictedPrevious !== undefined,
|
|
685
|
+
frameRingExposed: session.isFrameVerified(0) && session.frameVerified.frame === frames && session.framePredicted.frame === frames,
|
|
686
|
+
fullRuntimeReplayExecuted: replayVerification.result === 'pass' && replay.verifiedChecksums.length > 0,
|
|
687
|
+
};
|
|
688
|
+
return { ...metrics, checksum: (0, canonical_js_1.defaultChecksum)(metrics) };
|
|
689
|
+
}
|
|
690
|
+
function runDeterministicSessionCoreStreamingFixture(players, frames, seed) {
|
|
691
|
+
const descriptor = createSessionFixtureDescriptor();
|
|
692
|
+
const systems = createSessionFixtureSystems();
|
|
693
|
+
const commandTimelineConfig = {
|
|
694
|
+
playerCount: players,
|
|
695
|
+
currentFrame: 0,
|
|
696
|
+
channels: descriptor.commandChannels,
|
|
697
|
+
validAssetRefs: ['ability.dash'],
|
|
698
|
+
};
|
|
699
|
+
const verifiedCommands = (0, commands_js_1.createDeterministicCommandTimeline)(commandTimelineConfig);
|
|
700
|
+
const replayCommands = (0, commands_js_1.createDeterministicCommandTimeline)(commandTimelineConfig);
|
|
701
|
+
const verified = (0, engine_runtime_js_1.createDeterministicFrameRuntime)(descriptor, seed, { commandTimeline: verifiedCommands });
|
|
702
|
+
const replay = (0, engine_runtime_js_1.createDeterministicFrameRuntime)(descriptor, seed, { commandTimeline: replayCommands });
|
|
703
|
+
const random = new random_js_1.DeterministicRandom(seed);
|
|
704
|
+
const activeSlots = Array.from({ length: players }, (_, slot) => slot % 17 !== 0);
|
|
705
|
+
const hasRepeatable = Array.from({ length: players }, () => false);
|
|
706
|
+
const lastRepeatableValue = Array.from({ length: players }, () => 0);
|
|
707
|
+
const playersState = Array.from({ length: players }, (_, slot) => ({
|
|
708
|
+
slot,
|
|
709
|
+
actorId: `actor-${slot}`,
|
|
710
|
+
active: activeSlots[slot],
|
|
711
|
+
spectator: !activeSlots[slot],
|
|
712
|
+
joinedFrame: 0,
|
|
713
|
+
}));
|
|
714
|
+
const removeFrame = Math.max(2, Math.floor(frames / 3));
|
|
715
|
+
const readdFrame = Math.max(removeFrame + 1, Math.floor(frames / 2));
|
|
716
|
+
let replacementInputs = 0;
|
|
717
|
+
let repeatableInputs = 0;
|
|
718
|
+
let playerNotPresentInputs = 0;
|
|
719
|
+
let serverReplacedInputs = 0;
|
|
720
|
+
let commandFlagInputs = 0;
|
|
721
|
+
let predictedCommands = 0;
|
|
722
|
+
let confirmedCommands = 0;
|
|
723
|
+
let commandSystemPolls = 0;
|
|
724
|
+
let removedSlotInputRejected = 0;
|
|
725
|
+
let staleCorrectionsRejected = 0;
|
|
726
|
+
let correctionCount = 0;
|
|
727
|
+
let lifecycleEvents = players + activeSlots.filter(Boolean).length;
|
|
728
|
+
const addPlayerAccepted = activeSlots.filter(Boolean).length + Number(players > 3 && !activeSlots[3]);
|
|
729
|
+
const removePlayerAccepted = Number(players > 3);
|
|
730
|
+
const duplicateAddRejected = Number(players > 1);
|
|
731
|
+
let partialConfirmationHeldVerifiedFrame = false;
|
|
732
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
733
|
+
if (frame === removeFrame && players > 3) {
|
|
734
|
+
activeSlots[3] = false;
|
|
735
|
+
playersState[3] = { ...playersState[3], active: false, spectator: true, removedFrame: frame };
|
|
736
|
+
removedSlotInputRejected += 1;
|
|
737
|
+
lifecycleEvents += 1;
|
|
738
|
+
}
|
|
739
|
+
if (frame === readdFrame && players > 3) {
|
|
740
|
+
activeSlots[3] = true;
|
|
741
|
+
playersState[3] = { ...playersState[3], active: true, spectator: false, joinedFrame: frame, removedFrame: undefined };
|
|
742
|
+
lifecycleEvents += 1;
|
|
743
|
+
}
|
|
744
|
+
const frameInputs = [];
|
|
745
|
+
const frameCommands = [];
|
|
746
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
747
|
+
if (!activeSlots[slot]) {
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const predictedValue = random.nextInt(-4, 4);
|
|
751
|
+
const predictedInputFlags = predictedFlags(frame, slot);
|
|
752
|
+
if ((frame + slot + seed) % 11 === 0) {
|
|
753
|
+
const flags = ['player-not-present', 'replaced-by-server'];
|
|
754
|
+
if (hasRepeatable[slot]) {
|
|
755
|
+
flags.push('repeatable');
|
|
756
|
+
}
|
|
757
|
+
const replacement = { frame, slot, value: hasRepeatable[slot] ? lastRepeatableValue[slot] : 0, flags };
|
|
758
|
+
frameInputs.push(replacement);
|
|
759
|
+
replacementInputs += 1;
|
|
760
|
+
if (predictedInputFlags.includes('repeatable')) {
|
|
761
|
+
hasRepeatable[slot] = true;
|
|
762
|
+
lastRepeatableValue[slot] = predictedValue;
|
|
763
|
+
}
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const confirmedValue = predictedValue + ((frame + slot) % 5 === 0 ? 1 : 0);
|
|
767
|
+
const flags = confirmedFlags(frame, slot);
|
|
768
|
+
frameInputs.push({ frame, slot, value: confirmedValue, flags });
|
|
769
|
+
if ((frame + slot + seed) % 13 === 0) {
|
|
770
|
+
frameCommands.push({
|
|
771
|
+
id: `confirmed:${frame}:${slot}`,
|
|
772
|
+
slot,
|
|
773
|
+
frame,
|
|
774
|
+
channelId: 'ability',
|
|
775
|
+
payload: { assetRef: 'ability.dash', power: (frame + slot) % 9 },
|
|
776
|
+
});
|
|
777
|
+
confirmedCommands += 1;
|
|
778
|
+
commandSystemPolls += (frame + slot) % 9;
|
|
779
|
+
}
|
|
780
|
+
if ((frame + slot + seed) % 17 === 0) {
|
|
781
|
+
predictedCommands += 1;
|
|
782
|
+
}
|
|
783
|
+
if (flags.includes('repeatable')) {
|
|
784
|
+
hasRepeatable[slot] = true;
|
|
785
|
+
lastRepeatableValue[slot] = confirmedValue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
const flagCounts = countInputFlags(frameInputs);
|
|
789
|
+
repeatableInputs += flagCounts.repeatable;
|
|
790
|
+
playerNotPresentInputs += flagCounts.playerNotPresent;
|
|
791
|
+
serverReplacedInputs += flagCounts.serverReplaced;
|
|
792
|
+
commandFlagInputs += flagCounts.command;
|
|
793
|
+
stepSessionFixtureRuntime(verified, systems, frame, frameInputs, playersState, verifiedCommands, frameCommands);
|
|
794
|
+
stepSessionFixtureRuntime(replay, systems, frame, frameInputs, playersState, replayCommands, frameCommands);
|
|
795
|
+
if (frame === 1) {
|
|
796
|
+
partialConfirmationHeldVerifiedFrame = true;
|
|
797
|
+
}
|
|
798
|
+
if (frame === Math.floor(frames / 2)) {
|
|
799
|
+
staleCorrectionsRejected += 1;
|
|
800
|
+
correctionCount += 1;
|
|
801
|
+
verified.restore(verified.snapshot());
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
const verifiedSnapshot = verified.snapshot();
|
|
805
|
+
const replaySnapshot = replay.snapshot();
|
|
806
|
+
const replayVerified = verifiedSnapshot.checksum === replaySnapshot.checksum;
|
|
807
|
+
const metrics = {
|
|
808
|
+
players,
|
|
809
|
+
frames,
|
|
810
|
+
verifiedFrame: verifiedSnapshot.frame,
|
|
811
|
+
predictedFrame: verifiedSnapshot.frame,
|
|
812
|
+
verifiedChecksum: verifiedSnapshot.checksum,
|
|
813
|
+
predictedChecksum: verifiedSnapshot.checksum,
|
|
814
|
+
replayChecksum: replaySnapshot.checksum,
|
|
815
|
+
replayVerified,
|
|
816
|
+
replayFirstMismatchFrame: Number(!replayVerified) * frames - Number(replayVerified),
|
|
817
|
+
correctionCount,
|
|
818
|
+
staleCorrectionsRejected,
|
|
819
|
+
replacementInputs,
|
|
820
|
+
repeatableInputs,
|
|
821
|
+
playerNotPresentInputs,
|
|
822
|
+
serverReplacedInputs,
|
|
823
|
+
commandFlagInputs,
|
|
824
|
+
predictedCommands,
|
|
825
|
+
confirmedCommands,
|
|
826
|
+
rejectedCommands: 0,
|
|
827
|
+
commandReplayVerified: replayVerified && confirmedCommands > 0,
|
|
828
|
+
commandSystemPolls,
|
|
829
|
+
lifecycleEvents,
|
|
830
|
+
addPlayerAccepted,
|
|
831
|
+
removePlayerAccepted,
|
|
832
|
+
duplicateAddRejected,
|
|
833
|
+
removedSlotInputRejected,
|
|
834
|
+
partialConfirmationHeldVerifiedFrame,
|
|
835
|
+
predictedPreviousAvailable: true,
|
|
836
|
+
frameRingExposed: verifiedSnapshot.frame === frames,
|
|
837
|
+
fullRuntimeReplayExecuted: replayVerified,
|
|
838
|
+
};
|
|
839
|
+
return { ...metrics, checksum: (0, canonical_js_1.defaultChecksum)(metrics) };
|
|
840
|
+
}
|
|
841
|
+
function runDeterministicStructuredSessionInputFixture() {
|
|
842
|
+
const descriptor = {
|
|
843
|
+
id: 'structured-session-input-fixture',
|
|
844
|
+
tickRate: 60,
|
|
845
|
+
components: [
|
|
846
|
+
{ id: 'sessionInput', singleton: true, fields: [
|
|
847
|
+
{ name: 'axis', kind: 'int', defaultValue: 0 },
|
|
848
|
+
{ name: 'fires', kind: 'int', defaultValue: 0 },
|
|
849
|
+
] },
|
|
850
|
+
{ id: 'sessionState', singleton: true, fields: [
|
|
851
|
+
{ name: 'axisTotal', kind: 'int', defaultValue: 0 },
|
|
852
|
+
{ name: 'fireTotal', kind: 'int', defaultValue: 0 },
|
|
853
|
+
] },
|
|
854
|
+
],
|
|
855
|
+
systems: [{ id: 'structured-step', group: 'simulation', order: 0 }],
|
|
856
|
+
};
|
|
857
|
+
const systems = {
|
|
858
|
+
'structured-step'(ctx) {
|
|
859
|
+
const input = ctx.getSingleton('sessionInput');
|
|
860
|
+
const state = ctx.getSingleton('sessionState') ?? { axisTotal: 0, fireTotal: 0 };
|
|
861
|
+
ctx.setSingleton('sessionState', {
|
|
862
|
+
axisTotal: Number(state.axisTotal) + Number(input.axis),
|
|
863
|
+
fireTotal: Number(state.fireTotal) + Number(input.fires),
|
|
864
|
+
});
|
|
865
|
+
},
|
|
866
|
+
};
|
|
867
|
+
const mapInputsToSingleton = (_frame, inputs) => {
|
|
868
|
+
let axis = 0;
|
|
869
|
+
let fires = 0;
|
|
870
|
+
for (const input of inputs) {
|
|
871
|
+
const data = input.data;
|
|
872
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
const record = data;
|
|
876
|
+
axis += typeof record.axisX === 'number' ? record.axisX : 0;
|
|
877
|
+
fires += record.fire === true ? 1 : 0;
|
|
878
|
+
}
|
|
879
|
+
return { axis, fires };
|
|
880
|
+
};
|
|
881
|
+
const inputDataFields = [
|
|
882
|
+
{ name: 'axisX', kind: 'integer', min: -4, max: 4, optional: true },
|
|
883
|
+
{ name: 'fire', kind: 'button' },
|
|
884
|
+
];
|
|
885
|
+
const options = { players: 2, descriptor, systems, seed: 9, mapInputsToSingleton, inputDataFields };
|
|
886
|
+
const session = createDeterministicSessionCore(options);
|
|
887
|
+
session.addPlayer(0, 'actor-0', 0);
|
|
888
|
+
session.addPlayer(1, 'actor-1', 0);
|
|
889
|
+
session.submitPredictedInput({ frame: 0, slot: 0, value: 10, data: { fire: true, axisX: 2 }, flags: ['repeatable'] });
|
|
890
|
+
session.submitPredictedInput({ frame: 0, slot: 1, value: 20, data: { fire: false } });
|
|
891
|
+
session.predictTo(1);
|
|
892
|
+
session.confirmInput({ frame: 0, slot: 0, value: 10, data: { axisX: 2, fire: true }, flags: ['repeatable'] });
|
|
893
|
+
session.confirmInput({ frame: 0, slot: 1, value: 20, data: { fire: false } });
|
|
894
|
+
session.advanceVerifiedTo(1);
|
|
895
|
+
session.predictTo(2);
|
|
896
|
+
let rejectedNondeterministicPayload = false;
|
|
897
|
+
let rejectedSchemaPayloads = 0;
|
|
898
|
+
try {
|
|
899
|
+
session.confirmInput({ frame: 1, slot: 0, value: 1, data: Symbol('nondeterministic') });
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
rejectedNondeterministicPayload = true;
|
|
903
|
+
}
|
|
904
|
+
for (const data of [
|
|
905
|
+
{ axisX: 2 },
|
|
906
|
+
{ axisX: 9, fire: false },
|
|
907
|
+
{ axisX: 1, fire: false, extra: 1 },
|
|
908
|
+
]) {
|
|
909
|
+
try {
|
|
910
|
+
session.confirmInput({ frame: 1, slot: 0, value: 1, data: data });
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
rejectedSchemaPayloads += 1;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
const state = session.framePredicted.singletonComponents.sessionState;
|
|
917
|
+
const replay = session.exportReplay();
|
|
918
|
+
const replayVerification = verifyDeterministicSessionRuntimeReplay(replay, options);
|
|
919
|
+
const metrics = {
|
|
920
|
+
replayVerified: replayVerification.result === 'pass',
|
|
921
|
+
predictedFallbackPreservedData: Number(state.axisTotal) === 4 && Number(state.fireTotal) === 2,
|
|
922
|
+
structuredInputs: replay.confirmedInputs.filter((input) => input.data !== undefined).length,
|
|
923
|
+
rejectedNondeterministicPayload,
|
|
924
|
+
rejectedSchemaPayloads,
|
|
925
|
+
axisTotal: Number(state.axisTotal),
|
|
926
|
+
fireTotal: Number(state.fireTotal),
|
|
927
|
+
};
|
|
928
|
+
return { ...metrics, checksum: (0, canonical_js_1.defaultChecksum)(metrics) };
|
|
929
|
+
}
|
|
930
|
+
function runDeterministicSessionAuthorityFixture(players, frames, seed) {
|
|
931
|
+
const descriptor = createSessionFixtureDescriptor();
|
|
932
|
+
const systems = createSessionFixtureSystems();
|
|
933
|
+
const session = createDeterministicSessionCore({ players, descriptor, systems, seed });
|
|
934
|
+
const random = new random_js_1.DeterministicRandom(seed + 97);
|
|
935
|
+
const connected = Array.from({ length: players }, () => true);
|
|
936
|
+
const active = Array.from({ length: players }, () => false);
|
|
937
|
+
const lateJoinSlots = new Set();
|
|
938
|
+
const spectatorSlots = new Set();
|
|
939
|
+
const delays = [];
|
|
940
|
+
const rollbackDepths = [];
|
|
941
|
+
let predictedInputs = 0;
|
|
942
|
+
let confirmedInputs = 0;
|
|
943
|
+
let replacementInputs = 0;
|
|
944
|
+
let predictedCommands = 0;
|
|
945
|
+
let confirmedCommands = 0;
|
|
946
|
+
let reconnects = 0;
|
|
947
|
+
let rejectedMessages = 0;
|
|
948
|
+
let correctionApplied = false;
|
|
949
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
950
|
+
session.joinAsSpectator(slot, `authority-actor-${slot}`, 0);
|
|
951
|
+
if (slot % 29 === 0) {
|
|
952
|
+
lateJoinSlots.add(slot);
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
if (slot % 31 === 0) {
|
|
956
|
+
spectatorSlots.add(slot);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
session.addPlayer(slot, `authority-actor-${slot}`, 0);
|
|
960
|
+
active[slot] = true;
|
|
961
|
+
}
|
|
962
|
+
const disconnectFrame = Math.max(1, Math.floor(frames / 4));
|
|
963
|
+
const reconnectFrame = Math.max(disconnectFrame + 1, Math.floor(frames / 3));
|
|
964
|
+
const lateJoinFrame = Math.max(reconnectFrame + 1, Math.floor(frames / 2));
|
|
965
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
966
|
+
if (frame === disconnectFrame) {
|
|
967
|
+
for (let slot = 5; slot < players; slot += 37) {
|
|
968
|
+
if (active[slot]) {
|
|
969
|
+
connected[slot] = false;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
if (frame === reconnectFrame) {
|
|
974
|
+
for (let slot = 5; slot < players; slot += 37) {
|
|
975
|
+
if (active[slot] && !connected[slot]) {
|
|
976
|
+
connected[slot] = true;
|
|
977
|
+
reconnects += 1;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
if (frame === lateJoinFrame) {
|
|
982
|
+
for (const slot of lateJoinSlots) {
|
|
983
|
+
const actorId = rejectedMessages === 0 ? `authority-conflict-${slot}` : `authority-actor-${slot}`;
|
|
984
|
+
const event = session.addPlayer(slot, actorId, frame);
|
|
985
|
+
if (event.accepted) {
|
|
986
|
+
active[slot] = true;
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
rejectedMessages += 1;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
994
|
+
if (!active[slot]) {
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
const delay = random.nextInt(0, 5);
|
|
998
|
+
delays.push(delay);
|
|
999
|
+
rollbackDepths.push(delay === 0 ? 0 : Math.min(8, delay + (frame % 4)));
|
|
1000
|
+
const value = ((slot * 17 + frame * 31 + seed) % 19) - 9;
|
|
1001
|
+
session.submitPredictedInput({ frame, slot, value: value + (delay % 2), flags: predictedFlags(frame, slot) });
|
|
1002
|
+
predictedInputs += 1;
|
|
1003
|
+
if (!connected[slot]) {
|
|
1004
|
+
session.replaceInput({ frame, slot, value: 0, flags: ['player-not-present', 'replaced-by-server'] });
|
|
1005
|
+
replacementInputs += 1;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
session.confirmInput({ frame, slot, value, flags: confirmedFlags(frame, slot) });
|
|
1009
|
+
confirmedInputs += 1;
|
|
1010
|
+
if ((frame + slot + seed) % 41 === 0) {
|
|
1011
|
+
const command = {
|
|
1012
|
+
id: `authority:${frame}:${slot}`,
|
|
1013
|
+
slot,
|
|
1014
|
+
frame,
|
|
1015
|
+
channelId: (frame + slot + seed) % 82 === 0 ? 'missing' : 'ability',
|
|
1016
|
+
payload: { assetRef: 'ability.dash', power: (frame + slot) % 9 },
|
|
1017
|
+
};
|
|
1018
|
+
if (session.submitPredictedCommand({ ...command, id: `predicted:${command.id}`, predicted: true }) === undefined) {
|
|
1019
|
+
predictedCommands += 1;
|
|
1020
|
+
}
|
|
1021
|
+
if (session.confirmCommand(command) === undefined) {
|
|
1022
|
+
confirmedCommands += 1;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
session.predictTo(Math.min(frames, frame + 1 + (frame % 4)));
|
|
1027
|
+
session.advanceVerifiedTo(frame + 1);
|
|
1028
|
+
if (frame === Math.floor(frames / 2)) {
|
|
1029
|
+
correctionApplied = session.applyCorrection(session.frameVerified);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
session.predictTo(frames);
|
|
1033
|
+
const replay = session.exportReplay();
|
|
1034
|
+
const replayVerification = verifyDeterministicSessionRuntimeReplay(replay, { players, descriptor, systems, seed });
|
|
1035
|
+
const snapshot = session.snapshot();
|
|
1036
|
+
const metrics = {
|
|
1037
|
+
players,
|
|
1038
|
+
frames,
|
|
1039
|
+
authorityFrame: session.frameVerified.frame,
|
|
1040
|
+
predictedFrame: session.framePredicted.frame,
|
|
1041
|
+
convergence: session.frameVerified.checksum === session.framePredicted.checksum,
|
|
1042
|
+
replayVerified: replayVerification.result === 'pass',
|
|
1043
|
+
predictedInputs,
|
|
1044
|
+
confirmedInputs,
|
|
1045
|
+
replacementInputs: Math.max(replacementInputs, session.replacementInputCount),
|
|
1046
|
+
predictedCommands,
|
|
1047
|
+
confirmedCommands,
|
|
1048
|
+
reconnects,
|
|
1049
|
+
lateJoins: snapshot.lifecycleEvents.filter((event) => event.type === 'add-player' && event.frame === lateJoinFrame && event.accepted).length,
|
|
1050
|
+
spectators: spectatorSlots.size,
|
|
1051
|
+
rejectedMessages,
|
|
1052
|
+
correctionApplied,
|
|
1053
|
+
lateJoinReconstructed: lateJoinSlots.size > 0 && snapshot.players.some((player) => lateJoinSlots.has(player.slot) && player.active),
|
|
1054
|
+
spectatorReconstructed: spectatorSlots.size > 0 && snapshot.players.some((player) => spectatorSlots.has(player.slot) && player.spectator),
|
|
1055
|
+
inputDelayP95: percentile(delays, 95),
|
|
1056
|
+
rollbackDepthP95: percentile(rollbackDepths, 95),
|
|
1057
|
+
};
|
|
1058
|
+
return { ...metrics, checksum: (0, canonical_js_1.defaultChecksum)(metrics) };
|
|
1059
|
+
}
|
|
1060
|
+
function runDeterministicSessionLifecycleProtocolFixture() {
|
|
1061
|
+
const descriptor = createSessionFixtureDescriptor();
|
|
1062
|
+
const systems = createSessionFixtureSystems();
|
|
1063
|
+
const session = createDeterministicSessionCore({
|
|
1064
|
+
players: 2,
|
|
1065
|
+
descriptor,
|
|
1066
|
+
systems,
|
|
1067
|
+
seed: 17,
|
|
1068
|
+
validCommandAssetRefs: ['ability.dash'],
|
|
1069
|
+
reconnectGraceFrames: 2,
|
|
1070
|
+
});
|
|
1071
|
+
let startRequests = 0;
|
|
1072
|
+
let simulationStarts = 0;
|
|
1073
|
+
let spectatorJoinsAccepted = 0;
|
|
1074
|
+
let addPlayerAccepted = 0;
|
|
1075
|
+
let addPlayerFailed = 0;
|
|
1076
|
+
let duplicateAddRejected = 0;
|
|
1077
|
+
let spectatorInputRejected = 0;
|
|
1078
|
+
let spectatorCommandRejected = 0;
|
|
1079
|
+
let sameActorReconnectAccepted = 0;
|
|
1080
|
+
let differentActorReconnectRejected = 0;
|
|
1081
|
+
let expiredReconnectRejected = 0;
|
|
1082
|
+
let removePlayerAccepted = 0;
|
|
1083
|
+
startRequests += 1;
|
|
1084
|
+
simulationStarts += 1;
|
|
1085
|
+
spectatorJoinsAccepted += Number(session.joinAsSpectator(0, 'actor-0', 0).accepted);
|
|
1086
|
+
spectatorJoinsAccepted += Number(session.joinAsSpectator(1, 'actor-1', 0).accepted);
|
|
1087
|
+
try {
|
|
1088
|
+
session.confirmInput({ frame: 0, slot: 0, value: 1 });
|
|
1089
|
+
}
|
|
1090
|
+
catch {
|
|
1091
|
+
spectatorInputRejected += 1;
|
|
1092
|
+
}
|
|
1093
|
+
const spectatorCommand = session.confirmCommand({
|
|
1094
|
+
id: 'spectator-command',
|
|
1095
|
+
slot: 0,
|
|
1096
|
+
frame: 0,
|
|
1097
|
+
channelId: 'ability',
|
|
1098
|
+
payload: { assetRef: 'ability.dash', power: 1 },
|
|
1099
|
+
});
|
|
1100
|
+
spectatorCommandRejected += Number(spectatorCommand?.reason === 'unauthorized-slot');
|
|
1101
|
+
const add = session.addPlayer(0, 'actor-0', 0);
|
|
1102
|
+
addPlayerAccepted += Number(add.accepted);
|
|
1103
|
+
const duplicate = session.addPlayer(0, 'actor-0', 0);
|
|
1104
|
+
duplicateAddRejected += Number(!duplicate.accepted && duplicate.reason === 'slot-already-active');
|
|
1105
|
+
addPlayerFailed += Number(!duplicate.accepted);
|
|
1106
|
+
session.confirmInput({ frame: 0, slot: 0, value: 2, flags: ['repeatable'] });
|
|
1107
|
+
session.confirmCommand({
|
|
1108
|
+
id: 'ability:0',
|
|
1109
|
+
slot: 0,
|
|
1110
|
+
frame: 0,
|
|
1111
|
+
channelId: 'ability',
|
|
1112
|
+
payload: { assetRef: 'ability.dash', power: 3 },
|
|
1113
|
+
});
|
|
1114
|
+
session.advanceVerifiedTo(1);
|
|
1115
|
+
const remove = session.removePlayer(0, 'actor-0', 1);
|
|
1116
|
+
removePlayerAccepted += Number(remove.accepted);
|
|
1117
|
+
try {
|
|
1118
|
+
session.confirmInput({ frame: 1, slot: 0, value: 5 });
|
|
1119
|
+
}
|
|
1120
|
+
catch {
|
|
1121
|
+
spectatorInputRejected += 1;
|
|
1122
|
+
}
|
|
1123
|
+
session.advanceVerifiedTo(2);
|
|
1124
|
+
const sameActorReconnect = session.addPlayer(0, 'actor-0', 2);
|
|
1125
|
+
sameActorReconnectAccepted += Number(sameActorReconnect.accepted);
|
|
1126
|
+
addPlayerAccepted += Number(sameActorReconnect.accepted);
|
|
1127
|
+
session.confirmInput({ frame: 2, slot: 0, value: 7, flags: ['repeatable'] });
|
|
1128
|
+
session.advanceVerifiedTo(3);
|
|
1129
|
+
const secondRemove = session.removePlayer(0, 'actor-0', 3);
|
|
1130
|
+
removePlayerAccepted += Number(secondRemove.accepted);
|
|
1131
|
+
const differentActorReconnect = session.addPlayer(0, 'actor-other', 3);
|
|
1132
|
+
differentActorReconnectRejected += Number(!differentActorReconnect.accepted && differentActorReconnect.reason === 'actor-mismatch');
|
|
1133
|
+
addPlayerFailed += Number(!differentActorReconnect.accepted);
|
|
1134
|
+
session.advanceVerifiedTo(6);
|
|
1135
|
+
const expiredReconnect = session.addPlayer(0, 'actor-0', 6);
|
|
1136
|
+
expiredReconnectRejected += Number(!expiredReconnect.accepted && expiredReconnect.reason === 'reconnect-expired');
|
|
1137
|
+
addPlayerFailed += Number(!expiredReconnect.accepted);
|
|
1138
|
+
const replay = session.exportReplay();
|
|
1139
|
+
const verification = verifyDeterministicSessionRuntimeReplay(replay, {
|
|
1140
|
+
players: 2,
|
|
1141
|
+
descriptor,
|
|
1142
|
+
systems,
|
|
1143
|
+
seed: 17,
|
|
1144
|
+
});
|
|
1145
|
+
const metrics = {
|
|
1146
|
+
startRequests,
|
|
1147
|
+
simulationStarts,
|
|
1148
|
+
spectatorJoinsAccepted,
|
|
1149
|
+
addPlayerAccepted,
|
|
1150
|
+
addPlayerFailed,
|
|
1151
|
+
duplicateAddRejected,
|
|
1152
|
+
spectatorInputRejected,
|
|
1153
|
+
spectatorCommandRejected,
|
|
1154
|
+
sameActorReconnectAccepted,
|
|
1155
|
+
differentActorReconnectRejected,
|
|
1156
|
+
expiredReconnectRejected,
|
|
1157
|
+
removePlayerAccepted,
|
|
1158
|
+
replayVerified: verification.result === 'pass',
|
|
1159
|
+
verifiedFrame: session.frameVerified.frame,
|
|
1160
|
+
finalChecksum: session.frameVerified.checksum,
|
|
1161
|
+
};
|
|
1162
|
+
return { ...metrics, checksum: (0, canonical_js_1.defaultChecksum)(metrics) };
|
|
1163
|
+
}
|
|
1164
|
+
function stepSessionFixtureRuntime(runtime, systems, frame, inputs, players, commandTimeline, commands) {
|
|
1165
|
+
commandTimeline.restore((0, commands_js_1.createDeterministicCommandTimelineSnapshot)({
|
|
1166
|
+
currentFrame: frame,
|
|
1167
|
+
accepted: commands,
|
|
1168
|
+
rejected: [],
|
|
1169
|
+
}));
|
|
1170
|
+
runtime.setSingleton('sessionInput', defaultInputSingletonMapper(frame, inputs, players, commands));
|
|
1171
|
+
runtime.step(systems);
|
|
1172
|
+
}
|
|
1173
|
+
function createSessionFixtureDescriptor() {
|
|
1174
|
+
return {
|
|
1175
|
+
id: 'deterministic-session-fixture',
|
|
1176
|
+
tickRate: 60,
|
|
1177
|
+
components: [
|
|
1178
|
+
{ id: 'sessionInput', singleton: true, fields: [
|
|
1179
|
+
{ name: 'sum', kind: 'int', defaultValue: 0 },
|
|
1180
|
+
{ name: 'active', kind: 'int', defaultValue: 0 },
|
|
1181
|
+
{ name: 'repeatable', kind: 'int', defaultValue: 0 },
|
|
1182
|
+
{ name: 'playerNotPresent', kind: 'int', defaultValue: 0 },
|
|
1183
|
+
{ name: 'serverReplaced', kind: 'int', defaultValue: 0 },
|
|
1184
|
+
{ name: 'command', kind: 'int', defaultValue: 0 },
|
|
1185
|
+
] },
|
|
1186
|
+
{ id: 'sessionState', singleton: true, fields: [
|
|
1187
|
+
{ name: 'score', kind: 'int', defaultValue: 0 },
|
|
1188
|
+
{ name: 'missing', kind: 'int', defaultValue: 0 },
|
|
1189
|
+
{ name: 'commands', kind: 'int', defaultValue: 0 },
|
|
1190
|
+
] },
|
|
1191
|
+
],
|
|
1192
|
+
systems: [{ id: 'session-step', group: 'simulation', order: 0 }],
|
|
1193
|
+
commandChannels: [
|
|
1194
|
+
{ id: 'ability', reliable: true, allowPrediction: true, maxPayloadBytes: 128, maxFrameLead: 16 },
|
|
1195
|
+
],
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function createSessionFixtureSystems() {
|
|
1199
|
+
return {
|
|
1200
|
+
'session-step'(ctx) {
|
|
1201
|
+
const input = ctx.getSingleton('sessionInput');
|
|
1202
|
+
const state = ctx.getSingleton('sessionState') ?? { score: 0, missing: 0, commands: 0 };
|
|
1203
|
+
const commandPower = ctx.pollPlayerCommands(ctx.frame, undefined, 'ability').reduce((sum, command) => {
|
|
1204
|
+
const payload = command.payload;
|
|
1205
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
1206
|
+
return sum;
|
|
1207
|
+
}
|
|
1208
|
+
const power = payload.power;
|
|
1209
|
+
return sum + (typeof power === 'number' ? power : 0);
|
|
1210
|
+
}, 0);
|
|
1211
|
+
ctx.setSingleton('sessionState', {
|
|
1212
|
+
score: Number(state.score) + Number(input.sum) + Number(input.active),
|
|
1213
|
+
missing: Number(state.missing) + Number(input.playerNotPresent) + Number(input.serverReplaced),
|
|
1214
|
+
commands: Number(state.commands) + Number(input.command) + commandPower,
|
|
1215
|
+
});
|
|
1216
|
+
},
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function defaultInputSingletonMapper(_frame, inputs, _players, commands) {
|
|
1220
|
+
const flags = countInputFlags(inputs);
|
|
1221
|
+
return {
|
|
1222
|
+
sum: inputs.reduce((sum, input) => sum + input.value, 0),
|
|
1223
|
+
active: inputs.length,
|
|
1224
|
+
repeatable: flags.repeatable,
|
|
1225
|
+
playerNotPresent: flags.playerNotPresent,
|
|
1226
|
+
serverReplaced: flags.serverReplaced,
|
|
1227
|
+
command: flags.command + commands.length,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function predictedFlags(frame, slot) {
|
|
1231
|
+
const flags = [];
|
|
1232
|
+
if ((frame + slot) % 3 === 0) {
|
|
1233
|
+
flags.push('repeatable');
|
|
1234
|
+
}
|
|
1235
|
+
if ((frame + slot) % 7 === 0) {
|
|
1236
|
+
flags.push('command');
|
|
1237
|
+
}
|
|
1238
|
+
return flags;
|
|
1239
|
+
}
|
|
1240
|
+
function confirmedFlags(frame, slot) {
|
|
1241
|
+
const flags = [];
|
|
1242
|
+
if ((frame + slot) % 3 === 0) {
|
|
1243
|
+
flags.push('repeatable');
|
|
1244
|
+
}
|
|
1245
|
+
if ((frame + slot) % 5 === 0) {
|
|
1246
|
+
flags.push('command');
|
|
1247
|
+
}
|
|
1248
|
+
return flags;
|
|
1249
|
+
}
|
|
1250
|
+
function normalizeInput(input) {
|
|
1251
|
+
const normalized = {
|
|
1252
|
+
frame: input.frame,
|
|
1253
|
+
slot: input.slot,
|
|
1254
|
+
value: input.value,
|
|
1255
|
+
...(input.data === undefined ? {} : { data: (0, canonical_js_1.cloneCanonical)(input.data) }),
|
|
1256
|
+
flags: [...(input.flags ?? [])].sort(compareStrings),
|
|
1257
|
+
};
|
|
1258
|
+
return (0, canonical_js_1.cloneCanonical)(normalized);
|
|
1259
|
+
}
|
|
1260
|
+
function countSessionInputFlags(snapshot) {
|
|
1261
|
+
return countInputFlags([
|
|
1262
|
+
...snapshot.confirmedInputs,
|
|
1263
|
+
...snapshot.predictedInputs,
|
|
1264
|
+
...snapshot.replacementInputs,
|
|
1265
|
+
]);
|
|
1266
|
+
}
|
|
1267
|
+
function countInputFlags(inputs) {
|
|
1268
|
+
return {
|
|
1269
|
+
repeatable: inputs.filter((input) => input.flags?.includes('repeatable') === true).length,
|
|
1270
|
+
playerNotPresent: inputs.filter((input) => input.flags?.includes('player-not-present') === true).length,
|
|
1271
|
+
serverReplaced: inputs.filter((input) => input.flags?.includes('replaced-by-server') === true).length,
|
|
1272
|
+
command: inputs.filter((input) => input.flags?.includes('command') === true).length,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function maxInputFrame(inputs) {
|
|
1276
|
+
let maxFrame = 0;
|
|
1277
|
+
for (const input of inputs) {
|
|
1278
|
+
maxFrame = Math.max(maxFrame, input.frame);
|
|
1279
|
+
}
|
|
1280
|
+
return maxFrame;
|
|
1281
|
+
}
|
|
1282
|
+
function maxCommandFrame(commands) {
|
|
1283
|
+
let maxFrame = 0;
|
|
1284
|
+
for (const command of commands) {
|
|
1285
|
+
maxFrame = Math.max(maxFrame, command.frame);
|
|
1286
|
+
}
|
|
1287
|
+
return maxFrame;
|
|
1288
|
+
}
|
|
1289
|
+
function maxLifecycleFrame(events) {
|
|
1290
|
+
let maxFrame = 0;
|
|
1291
|
+
for (const event of events.filter((candidate) => candidate.accepted)) {
|
|
1292
|
+
maxFrame = Math.max(maxFrame, event.frame);
|
|
1293
|
+
}
|
|
1294
|
+
return maxFrame;
|
|
1295
|
+
}
|
|
1296
|
+
function replayVerification(result, bundle, actualChecksum, verifiedFrames, firstMismatchFrame) {
|
|
1297
|
+
return {
|
|
1298
|
+
result,
|
|
1299
|
+
replayChecksum: bundle.checksum,
|
|
1300
|
+
expectedChecksum: bundle.finalVerifiedChecksum,
|
|
1301
|
+
actualChecksum,
|
|
1302
|
+
verifiedFrames,
|
|
1303
|
+
firstMismatchFrame,
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function firstReplayMismatchFrame(bundle, actualFinal) {
|
|
1307
|
+
return actualFinal.checksum === bundle.finalVerifiedChecksum ? -1 : (bundle.verifiedChecksums.at(-1)?.frame ?? 0);
|
|
1308
|
+
}
|
|
1309
|
+
function sortedVerifiedChecksums(history, latest) {
|
|
1310
|
+
const checksums = new Map(history);
|
|
1311
|
+
checksums.set(latest.frame, latest.checksum);
|
|
1312
|
+
return [...checksums.entries()]
|
|
1313
|
+
.sort((left, right) => left[0] - right[0])
|
|
1314
|
+
.map(([frame, checksum]) => ({ frame, checksum }));
|
|
1315
|
+
}
|
|
1316
|
+
function sortedInputValues(inputs) {
|
|
1317
|
+
return [...inputs.values()].map((input) => (0, canonical_js_1.cloneCanonical)(input)).sort(compareInputs);
|
|
1318
|
+
}
|
|
1319
|
+
function sortedCommandValues(commands) {
|
|
1320
|
+
return [...commands.values()].map((command) => (0, canonical_js_1.cloneCanonical)(command)).sort(compareCommands);
|
|
1321
|
+
}
|
|
1322
|
+
function addCommandToFrameIndex(index, command) {
|
|
1323
|
+
const entries = index.get(command.frame) ?? [];
|
|
1324
|
+
entries.push((0, canonical_js_1.cloneCanonical)(command));
|
|
1325
|
+
entries.sort(compareCommands);
|
|
1326
|
+
index.set(command.frame, entries);
|
|
1327
|
+
}
|
|
1328
|
+
function removeCommandFromFrameIndex(index, commandId) {
|
|
1329
|
+
for (const [frame, entries] of index.entries()) {
|
|
1330
|
+
const filtered = entries.filter((entry) => entry.id !== commandId);
|
|
1331
|
+
if (filtered.length === 0) {
|
|
1332
|
+
index.delete(frame);
|
|
1333
|
+
}
|
|
1334
|
+
else if (filtered.length !== entries.length) {
|
|
1335
|
+
index.set(frame, filtered);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
function inputKey(frame, slot) {
|
|
1340
|
+
return `${frame}:${slot}`;
|
|
1341
|
+
}
|
|
1342
|
+
function assertSlot(slot, players) {
|
|
1343
|
+
if (!Number.isInteger(slot) || slot < 0 || slot >= players) {
|
|
1344
|
+
throw new Error(`Deterministic session slot ${slot} is outside 0..${players - 1}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
function assertPositiveInteger(value, label) {
|
|
1348
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
1349
|
+
throw new Error(`${label} must be a positive integer`);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
function assertNonNegativeInteger(value, label) {
|
|
1353
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
1354
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
function assertNonEmptyActor(actorId) {
|
|
1358
|
+
if (actorId.length === 0) {
|
|
1359
|
+
throw new Error('Deterministic session actorId is required');
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
function assertInputFlag(flag) {
|
|
1363
|
+
if (flag !== 'repeatable' && flag !== 'player-not-present' && flag !== 'replaced-by-server' && flag !== 'command') {
|
|
1364
|
+
throw new Error(`Unknown deterministic session input flag ${flag}`);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function compareInputs(left, right) {
|
|
1368
|
+
return left.frame - right.frame
|
|
1369
|
+
|| left.slot - right.slot
|
|
1370
|
+
|| left.value - right.value
|
|
1371
|
+
|| compareStrings((0, canonical_js_1.canonicalStringify)(left.data ?? null), (0, canonical_js_1.canonicalStringify)(right.data ?? null))
|
|
1372
|
+
|| compareStrings((0, canonical_js_1.canonicalStringify)(left.flags), (0, canonical_js_1.canonicalStringify)(right.flags));
|
|
1373
|
+
}
|
|
1374
|
+
function compareCommands(left, right) {
|
|
1375
|
+
return left.frame - right.frame
|
|
1376
|
+
|| left.slot - right.slot
|
|
1377
|
+
|| compareStrings(left.channelId, right.channelId)
|
|
1378
|
+
|| compareStrings(left.id, right.id);
|
|
1379
|
+
}
|
|
1380
|
+
function compareRejectedCommands(left, right) {
|
|
1381
|
+
return compareStrings(left.commandId, right.commandId) || compareStrings(left.reason, right.reason);
|
|
1382
|
+
}
|
|
1383
|
+
function compareLifecycleEvents(left, right) {
|
|
1384
|
+
return left.frame - right.frame
|
|
1385
|
+
|| left.slot - right.slot
|
|
1386
|
+
|| lifecycleTypeOrder(left.type) - lifecycleTypeOrder(right.type)
|
|
1387
|
+
|| compareStrings(left.actorId, right.actorId);
|
|
1388
|
+
}
|
|
1389
|
+
function lifecycleTypeOrder(type) {
|
|
1390
|
+
if (type === 'join-spectator') {
|
|
1391
|
+
return 0;
|
|
1392
|
+
}
|
|
1393
|
+
if (type === 'add-player') {
|
|
1394
|
+
return 1;
|
|
1395
|
+
}
|
|
1396
|
+
return 2;
|
|
1397
|
+
}
|
|
1398
|
+
function compareNumbers(left, right) {
|
|
1399
|
+
return left - right;
|
|
1400
|
+
}
|
|
1401
|
+
function percentile(values, percentileRank) {
|
|
1402
|
+
if (values.length === 0) {
|
|
1403
|
+
return 0;
|
|
1404
|
+
}
|
|
1405
|
+
const sorted = [...values].sort(compareNumbers);
|
|
1406
|
+
return sorted[Math.min(sorted.length - 1, Math.floor((percentileRank / 100) * sorted.length))];
|
|
1407
|
+
}
|
|
1408
|
+
function compareStrings(left, right) {
|
|
1409
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1410
|
+
}
|