@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,236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.inspectReplayFrame = inspectReplayFrame;
|
|
4
|
+
exports.inspectReplayFrameWithDescriptor = inspectReplayFrameWithDescriptor;
|
|
5
|
+
exports.explainReplayChecksum = explainReplayChecksum;
|
|
6
|
+
exports.diffReplayFrames = diffReplayFrames;
|
|
7
|
+
exports.stableFrameDumpJson = stableFrameDumpJson;
|
|
8
|
+
exports.stableFrameDiffJson = stableFrameDiffJson;
|
|
9
|
+
const canonical_js_1 = require("./canonical.js");
|
|
10
|
+
const collections_js_1 = require("./collections.js");
|
|
11
|
+
const errors_js_1 = require("./errors.js");
|
|
12
|
+
const offline_session_js_1 = require("./offline-session.js");
|
|
13
|
+
const replay_js_1 = require("./replay.js");
|
|
14
|
+
const schema_artifacts_js_1 = require("./schema-artifacts.js");
|
|
15
|
+
function inspectReplayFrame(config, frame, options = {}) {
|
|
16
|
+
(0, replay_js_1.assertReplayFileStructure)(config.replay);
|
|
17
|
+
validateFrame(config.replay, frame);
|
|
18
|
+
assertReplayIdentity(config);
|
|
19
|
+
const initialState = (0, canonical_js_1.decodeCanonicalValue)(config.replay.initialState);
|
|
20
|
+
const session = (0, offline_session_js_1.createOfflineSession)({
|
|
21
|
+
...config,
|
|
22
|
+
initialState,
|
|
23
|
+
playerCount: config.replay.playerCount,
|
|
24
|
+
tickRate: config.replay.tickRate,
|
|
25
|
+
randomSeed: config.replay.randomSeed,
|
|
26
|
+
replayRecordingMode: 'none',
|
|
27
|
+
snapshotBufferSize: Math.max(1, frame + 1),
|
|
28
|
+
});
|
|
29
|
+
const commandFrames = new Map((config.replay.commandHistory ?? []).map((commandFrame) => [commandFrame.frame, commandFrame.commands]));
|
|
30
|
+
for (const inputFrame of config.replay.inputFrames) {
|
|
31
|
+
if (inputFrame.frame >= frame) {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
if (inputFrame.frame !== session.currentFrame) {
|
|
35
|
+
throw new errors_js_1.SyncplayError(`Replay input frame ${inputFrame.frame} does not match session frame ${session.currentFrame}`, { code: 'replay.input-frame-gap', frame: session.currentFrame });
|
|
36
|
+
}
|
|
37
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
38
|
+
const decodedInput = (0, canonical_js_1.decodeCanonicalValue)(input);
|
|
39
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
40
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decodedInput) ?? decodedInput);
|
|
41
|
+
session.setInput(slot, replayInput);
|
|
42
|
+
if (flags !== undefined) {
|
|
43
|
+
session.setInputFlags(slot, flags);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const commands = commandFrames.get(inputFrame.frame);
|
|
47
|
+
if (commands !== undefined) {
|
|
48
|
+
session.recordCommands(commands.map((command) => (0, canonical_js_1.decodeCanonicalValue)(command)));
|
|
49
|
+
}
|
|
50
|
+
session.stepFrames(1);
|
|
51
|
+
}
|
|
52
|
+
const state = config.canonicalizeState?.(session.getState()) ?? session.getState();
|
|
53
|
+
const checksum = String(config.checksum?.(session.getState()) ?? (0, canonical_js_1.defaultChecksum)(state));
|
|
54
|
+
const expectedChecksum = replayChecksumForFrame(config.replay, frame);
|
|
55
|
+
const dump = {
|
|
56
|
+
frame,
|
|
57
|
+
checksum,
|
|
58
|
+
expectedChecksum,
|
|
59
|
+
checksumMatch: expectedChecksum === undefined ? undefined : expectedChecksum === checksum,
|
|
60
|
+
inputFrame: cloneOptional(config.replay.inputFrames.find((record) => record.frame === frame)),
|
|
61
|
+
commandFrame: cloneOptional(config.replay.commandHistory?.find((record) => record.frame === frame)),
|
|
62
|
+
appliedInputFrame: cloneOptional(config.replay.inputFrames.find((record) => record.frame === frame - 1)),
|
|
63
|
+
appliedCommandFrame: cloneOptional(config.replay.commandHistory?.find((record) => record.frame === frame - 1)),
|
|
64
|
+
state: (0, canonical_js_1.canonicalize)(state),
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
...dump,
|
|
68
|
+
checksumExplanation: options.explainChecksum === true ? explainReplayChecksum(dump) : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function inspectReplayFrameWithDescriptor(config, frame, options = {}) {
|
|
72
|
+
const artifacts = (0, schema_artifacts_js_1.createSchemaArtifacts)(config.descriptor);
|
|
73
|
+
return inspectReplayFrame({
|
|
74
|
+
replay: config.replay,
|
|
75
|
+
step: config.step,
|
|
76
|
+
defaultInput: config.defaultInput,
|
|
77
|
+
// The content-hashed identity, matching what runtimes stamp into exported
|
|
78
|
+
// replays — same contract as verifyReplayWithDescriptor.
|
|
79
|
+
inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
|
|
80
|
+
stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
|
|
81
|
+
deterministicVersion: artifacts.descriptor.deterministicVersion,
|
|
82
|
+
canonicalizeInput(input) {
|
|
83
|
+
return artifacts.canonicalizeInput(input);
|
|
84
|
+
},
|
|
85
|
+
canonicalizeState(state) {
|
|
86
|
+
return artifacts.canonicalizeState(state);
|
|
87
|
+
},
|
|
88
|
+
checksum(state) {
|
|
89
|
+
return artifacts.checksumState(state);
|
|
90
|
+
},
|
|
91
|
+
serializeState(state) {
|
|
92
|
+
return artifacts.serializeState(state);
|
|
93
|
+
},
|
|
94
|
+
deserializeState(bytes) {
|
|
95
|
+
return artifacts.deserializeState(bytes);
|
|
96
|
+
},
|
|
97
|
+
hydrateInput: config.hydrateInput,
|
|
98
|
+
}, frame, options);
|
|
99
|
+
}
|
|
100
|
+
function explainReplayChecksum(dump) {
|
|
101
|
+
const state = dump.state;
|
|
102
|
+
const tables = isRecord(state)
|
|
103
|
+
? (0, collections_js_1.stableSortedKeys)(state)
|
|
104
|
+
.map((key) => explainChecksumTable(`state.${key}`, state[key]))
|
|
105
|
+
: [explainChecksumTable('state', state)];
|
|
106
|
+
return {
|
|
107
|
+
frame: dump.frame,
|
|
108
|
+
checksum: dump.checksum,
|
|
109
|
+
tableCount: tables.length,
|
|
110
|
+
tables,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function diffReplayFrames(left, right, maxDifferences = 200) {
|
|
114
|
+
const differences = [];
|
|
115
|
+
collectDiffs('state', left.state, right.state, differences, maxDifferences);
|
|
116
|
+
return {
|
|
117
|
+
frame: left.frame,
|
|
118
|
+
leftChecksum: left.checksum,
|
|
119
|
+
rightChecksum: right.checksum,
|
|
120
|
+
checksumMatch: left.checksum === right.checksum,
|
|
121
|
+
differences,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function stableFrameDumpJson(dump) {
|
|
125
|
+
return `${JSON.stringify(dump)}\n`;
|
|
126
|
+
}
|
|
127
|
+
function stableFrameDiffJson(diff) {
|
|
128
|
+
return `${JSON.stringify(diff)}\n`;
|
|
129
|
+
}
|
|
130
|
+
function collectDiffs(path, left, right, differences, maxDifferences) {
|
|
131
|
+
if (differences.length >= maxDifferences || stringifyCanonicalForDiff(left) === stringifyCanonicalForDiff(right)) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
135
|
+
const length = Math.max(left.length, right.length);
|
|
136
|
+
for (let index = 0; index < length; index += 1) {
|
|
137
|
+
collectDiffs(`${path}[${index}]`, left[index], right[index], differences, maxDifferences);
|
|
138
|
+
if (differences.length >= maxDifferences) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!isRecord(left) || !isRecord(right)) {
|
|
145
|
+
differences.push({ path, left, right });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const keys = mergeStableKeys(left, right);
|
|
149
|
+
for (const key of keys) {
|
|
150
|
+
collectDiffs(`${path}.${key}`, left[key], right[key], differences, maxDifferences);
|
|
151
|
+
if (differences.length >= maxDifferences) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isRecord(value) {
|
|
157
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
158
|
+
}
|
|
159
|
+
function stringifyCanonicalForDiff(value) {
|
|
160
|
+
return value === undefined ? '<undefined>' : JSON.stringify(value);
|
|
161
|
+
}
|
|
162
|
+
function explainChecksumTable(path, value) {
|
|
163
|
+
const source = JSON.stringify(value);
|
|
164
|
+
return {
|
|
165
|
+
path,
|
|
166
|
+
checksum: (0, canonical_js_1.defaultChecksum)(source),
|
|
167
|
+
bytes: (0, canonical_js_1.encodeUtf8)(source).byteLength,
|
|
168
|
+
entries: countEntries(value),
|
|
169
|
+
sampleKeys: sampleKeys(value),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function countEntries(value) {
|
|
173
|
+
if (Array.isArray(value)) {
|
|
174
|
+
return value.length;
|
|
175
|
+
}
|
|
176
|
+
if (isRecord(value)) {
|
|
177
|
+
return (0, collections_js_1.stableSortedKeys)(value).length;
|
|
178
|
+
}
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
function sampleKeys(value) {
|
|
182
|
+
if (Array.isArray(value)) {
|
|
183
|
+
return value.slice(0, 5).map((_, index) => `[${index}]`);
|
|
184
|
+
}
|
|
185
|
+
if (isRecord(value)) {
|
|
186
|
+
return (0, collections_js_1.stableSortedKeys)(value).slice(0, 5);
|
|
187
|
+
}
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
function validateFrame(replay, frame) {
|
|
191
|
+
if (!Number.isInteger(frame) || frame < 0 || frame > replay.inputFrames.length) {
|
|
192
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${frame} is outside 0..${replay.inputFrames.length}`, {
|
|
193
|
+
code: 'replay.inspect-frame',
|
|
194
|
+
frame,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function mergeStableKeys(left, right) {
|
|
199
|
+
return [...new Set([...(0, collections_js_1.stableSortedKeys)(left), ...(0, collections_js_1.stableSortedKeys)(right)])].sort(compareStrings);
|
|
200
|
+
}
|
|
201
|
+
function compareStrings(left, right) {
|
|
202
|
+
return Number(left > right) - Number(left < right);
|
|
203
|
+
}
|
|
204
|
+
function assertReplayIdentity(config) {
|
|
205
|
+
const checks = [
|
|
206
|
+
['deterministicVersion', config.deterministicVersion, config.replay.deterministicVersion],
|
|
207
|
+
['inputSchemaId', config.inputSchemaId, config.replay.inputSchemaId],
|
|
208
|
+
['stateSchemaId', config.stateSchemaId, config.replay.stateSchemaId],
|
|
209
|
+
['engineIdentityHash', config.engineIdentityHash, config.replay.engineIdentityHash],
|
|
210
|
+
['assetDbHash', config.assetDbHash, config.replay.assetDbHash],
|
|
211
|
+
['configHash', config.configHash, config.replay.configHash],
|
|
212
|
+
];
|
|
213
|
+
for (const [field, actual, expected] of checks) {
|
|
214
|
+
if (actual === undefined || expected === undefined) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (actual !== expected) {
|
|
218
|
+
throw new errors_js_1.SyncplayError(`Replay ${field} mismatch: expected ${expected}, actual ${actual}`, {
|
|
219
|
+
code: 'replay.identity-mismatch',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function replayChecksumForFrame(replay, frame) {
|
|
225
|
+
const checksums = replay.checksums.filter((record) => record.frame === frame);
|
|
226
|
+
if (checksums.length > 1) {
|
|
227
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${frame} has duplicate checksum records`, {
|
|
228
|
+
code: 'replay.inspect-checksum',
|
|
229
|
+
frame,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return checksums[0]?.checksum;
|
|
233
|
+
}
|
|
234
|
+
function cloneOptional(value) {
|
|
235
|
+
return value === undefined ? undefined : (0, canonical_js_1.cloneCanonical)(value);
|
|
236
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyScoredReplay = verifyScoredReplay;
|
|
4
|
+
const canonical_js_1 = require("./canonical.js");
|
|
5
|
+
const offline_session_js_1 = require("./offline-session.js");
|
|
6
|
+
const replay_js_1 = require("./replay.js");
|
|
7
|
+
function verifyScoredReplay(config) {
|
|
8
|
+
const { submittedScore, extractScore, ...verification } = config;
|
|
9
|
+
const result = (0, replay_js_1.verifyReplay)({ ...verification, throwOnMismatch: false });
|
|
10
|
+
if (result.mismatches > 0) {
|
|
11
|
+
return {
|
|
12
|
+
ok: false,
|
|
13
|
+
reason: 'replay-mismatch',
|
|
14
|
+
submittedScore,
|
|
15
|
+
frames: result.frames,
|
|
16
|
+
verifiedChecksums: result.checksums,
|
|
17
|
+
finalChecksum: result.finalChecksum,
|
|
18
|
+
...(result.firstMismatch === undefined ? {} : { mismatchFrame: result.firstMismatch.frame }),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// The replay re-simulated clean — walk it once more to materialize the final
|
|
22
|
+
// state for score extraction (verifyReplay reports checksums, not state).
|
|
23
|
+
const replay = config.replay;
|
|
24
|
+
const session = (0, offline_session_js_1.createOfflineSession)({
|
|
25
|
+
...verification,
|
|
26
|
+
initialState: (0, canonical_js_1.decodeCanonicalValue)(replay.initialState),
|
|
27
|
+
playerCount: replay.playerCount,
|
|
28
|
+
tickRate: replay.tickRate,
|
|
29
|
+
randomSeed: replay.randomSeed,
|
|
30
|
+
});
|
|
31
|
+
const commandFrames = new Map((replay.commandHistory ?? []).map((frame) => [frame.frame, frame.commands]));
|
|
32
|
+
for (const inputFrame of replay.inputFrames) {
|
|
33
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
34
|
+
const decoded = (0, canonical_js_1.decodeCanonicalValue)(input);
|
|
35
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
36
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decoded) ?? decoded);
|
|
37
|
+
session.setInput(slot, replayInput);
|
|
38
|
+
if (flags !== undefined) {
|
|
39
|
+
session.setInputFlags(slot, flags);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
const commands = commandFrames.get(inputFrame.frame);
|
|
43
|
+
if (commands !== undefined) {
|
|
44
|
+
session.recordCommands(commands.map((command) => (0, canonical_js_1.decodeCanonicalValue)(command)));
|
|
45
|
+
}
|
|
46
|
+
session.stepFrames(1);
|
|
47
|
+
}
|
|
48
|
+
const replayScore = extractScore(session.getState());
|
|
49
|
+
if (replayScore !== submittedScore) {
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'score-mismatch',
|
|
53
|
+
replayScore,
|
|
54
|
+
submittedScore,
|
|
55
|
+
frames: result.frames,
|
|
56
|
+
verifiedChecksums: result.checksums,
|
|
57
|
+
finalChecksum: result.finalChecksum,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: true,
|
|
62
|
+
replayScore,
|
|
63
|
+
submittedScore,
|
|
64
|
+
frames: result.frames,
|
|
65
|
+
verifiedChecksums: result.checksums,
|
|
66
|
+
finalChecksum: result.finalChecksum,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyReplay = verifyReplay;
|
|
4
|
+
exports.verifyReplayWithDescriptor = verifyReplayWithDescriptor;
|
|
5
|
+
exports.assertReplayFileStructure = assertReplayFileStructure;
|
|
6
|
+
const canonical_js_1 = require("./canonical.js");
|
|
7
|
+
const errors_js_1 = require("./errors.js");
|
|
8
|
+
const offline_session_js_1 = require("./offline-session.js");
|
|
9
|
+
const schema_artifacts_js_1 = require("./schema-artifacts.js");
|
|
10
|
+
function verifyReplay(config) {
|
|
11
|
+
const replay = config.replay;
|
|
12
|
+
assertReplayFileStructure(replay);
|
|
13
|
+
assertReplayIdentity(config);
|
|
14
|
+
const initialState = (0, canonical_js_1.decodeCanonicalValue)(replay.initialState);
|
|
15
|
+
const session = (0, offline_session_js_1.createOfflineSession)({
|
|
16
|
+
...config,
|
|
17
|
+
initialState,
|
|
18
|
+
playerCount: replay.playerCount,
|
|
19
|
+
tickRate: replay.tickRate,
|
|
20
|
+
randomSeed: replay.randomSeed,
|
|
21
|
+
});
|
|
22
|
+
const checksums = new Map(replay.checksums.map((checksum) => [checksum.frame, checksum.checksum]));
|
|
23
|
+
const commandFrames = new Map((replay.commandHistory ?? []).map((commandFrame) => [commandFrame.frame, commandFrame.commands]));
|
|
24
|
+
let verifiedChecksums = 0;
|
|
25
|
+
let finalChecksum = String((0, canonical_js_1.defaultChecksum)(initialState));
|
|
26
|
+
for (const inputFrame of replay.inputFrames) {
|
|
27
|
+
if (inputFrame.frame !== session.currentFrame) {
|
|
28
|
+
throw new errors_js_1.SyncplayError(`Replay input frame ${inputFrame.frame} does not match session frame ${session.currentFrame}`, { code: 'replay.input-frame-gap', frame: session.currentFrame });
|
|
29
|
+
}
|
|
30
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
31
|
+
const decodedInput = (0, canonical_js_1.decodeCanonicalValue)(input);
|
|
32
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
33
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decodedInput) ?? decodedInput);
|
|
34
|
+
session.setInput(slot, replayInput);
|
|
35
|
+
if (flags !== undefined) {
|
|
36
|
+
session.setInputFlags(slot, flags);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const commands = commandFrames.get(inputFrame.frame);
|
|
40
|
+
if (commands !== undefined) {
|
|
41
|
+
session.recordCommands(commands.map((command) => (0, canonical_js_1.decodeCanonicalValue)(command)));
|
|
42
|
+
}
|
|
43
|
+
session.stepFrames(1);
|
|
44
|
+
const expectedChecksum = checksums.get(session.currentFrame);
|
|
45
|
+
if (expectedChecksum !== undefined) {
|
|
46
|
+
const actualChecksum = String(config.checksum?.(session.getState()) ?? (0, canonical_js_1.defaultChecksum)(config.canonicalizeState?.(session.getState()) ?? session.getState()));
|
|
47
|
+
verifiedChecksums += 1;
|
|
48
|
+
finalChecksum = actualChecksum;
|
|
49
|
+
if (actualChecksum !== expectedChecksum) {
|
|
50
|
+
const mismatch = {
|
|
51
|
+
frame: session.currentFrame,
|
|
52
|
+
expectedChecksum,
|
|
53
|
+
actualChecksum,
|
|
54
|
+
inputFrame,
|
|
55
|
+
state: (0, canonical_js_1.canonicalize)(config.canonicalizeState?.(session.getState()) ?? session.getState()),
|
|
56
|
+
};
|
|
57
|
+
if (config.throwOnMismatch === false) {
|
|
58
|
+
return {
|
|
59
|
+
frames: session.currentFrame,
|
|
60
|
+
checksums: verifiedChecksums,
|
|
61
|
+
mismatches: 1,
|
|
62
|
+
finalChecksum,
|
|
63
|
+
firstMismatch: mismatch,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
throw new errors_js_1.SyncplayError(`Checksum mismatch at frame ${session.currentFrame}: expected ${expectedChecksum}, actual ${actualChecksum}`, { code: 'replay.checksum-mismatch', frame: session.currentFrame });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (verifiedChecksums !== replay.checksums.length) {
|
|
71
|
+
throw new errors_js_1.SyncplayError(`Replay contained ${replay.checksums.length - verifiedChecksums} checksum records beyond verified input frames`, { code: 'replay.unverified-checksum', frame: session.currentFrame });
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
frames: session.currentFrame,
|
|
75
|
+
checksums: verifiedChecksums,
|
|
76
|
+
mismatches: 0,
|
|
77
|
+
finalChecksum,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function verifyReplayWithDescriptor(config) {
|
|
81
|
+
const artifacts = (0, schema_artifacts_js_1.createSchemaArtifacts)(config.descriptor);
|
|
82
|
+
return verifyReplay({
|
|
83
|
+
replay: config.replay,
|
|
84
|
+
step: config.step,
|
|
85
|
+
defaultInput: config.defaultInput,
|
|
86
|
+
// The CONTENT-HASHED identity, not the author label: runtimes stamp
|
|
87
|
+
// replayIdentity.* into exported replays (a field edit without a label
|
|
88
|
+
// bump must change the identity), so the descriptor-derived verifier must
|
|
89
|
+
// compare the same form or every defineSyncplayGame replay fails identity.
|
|
90
|
+
inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
|
|
91
|
+
stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
|
|
92
|
+
deterministicVersion: artifacts.descriptor.deterministicVersion,
|
|
93
|
+
canonicalizeInput(input) {
|
|
94
|
+
return artifacts.canonicalizeInput(input);
|
|
95
|
+
},
|
|
96
|
+
canonicalizeState(state) {
|
|
97
|
+
return artifacts.canonicalizeState(state);
|
|
98
|
+
},
|
|
99
|
+
checksum(state) {
|
|
100
|
+
return artifacts.checksumState(state);
|
|
101
|
+
},
|
|
102
|
+
serializeState(state) {
|
|
103
|
+
return artifacts.serializeState(state);
|
|
104
|
+
},
|
|
105
|
+
deserializeState(bytes) {
|
|
106
|
+
return artifacts.deserializeState(bytes);
|
|
107
|
+
},
|
|
108
|
+
hydrateInput: config.hydrateInput,
|
|
109
|
+
throwOnMismatch: config.throwOnMismatch,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function assertReplayFileStructure(replay) {
|
|
113
|
+
if (replay.formatVersion !== 1) {
|
|
114
|
+
throw new errors_js_1.SyncplayError(`Unsupported replay format ${replay.formatVersion}`, {
|
|
115
|
+
code: 'replay.format-version',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
assertReplayCanonicalValue(replay.initialState, 'Replay initialState is not a valid canonical value', {});
|
|
119
|
+
for (const inputFrame of replay.inputFrames) {
|
|
120
|
+
if (inputFrame.inputs.length !== replay.playerCount) {
|
|
121
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} has ${inputFrame.inputs.length} inputs, expected ${replay.playerCount}`, {
|
|
122
|
+
code: 'replay.input-count',
|
|
123
|
+
frame: inputFrame.frame,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
validateInputFlags(replay, inputFrame);
|
|
127
|
+
inputFrame.inputs.forEach((input, slot) => assertReplayCanonicalValue(input, `Replay frame ${inputFrame.frame} input ${slot} is not a valid canonical value`, { frame: inputFrame.frame, slot }));
|
|
128
|
+
}
|
|
129
|
+
for (const commandFrame of replay.commandHistory ?? []) {
|
|
130
|
+
if (!Number.isInteger(commandFrame.frame) || commandFrame.frame < 0 || commandFrame.frame >= replay.inputFrames.length) {
|
|
131
|
+
throw new errors_js_1.SyncplayError(`Replay command frame ${commandFrame.frame} is outside replay input history`, {
|
|
132
|
+
code: 'replay.command-frame-gap',
|
|
133
|
+
frame: commandFrame.frame,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
for (const command of commandFrame.commands) {
|
|
137
|
+
const canonicalCommand = command;
|
|
138
|
+
assertReplayCanonicalValue(canonicalCommand, `Replay command frame ${commandFrame.frame} contains a non-canonical command`, { frame: commandFrame.frame });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function assertReplayIdentity(config) {
|
|
143
|
+
const replay = config.replay;
|
|
144
|
+
const checks = [
|
|
145
|
+
['deterministicVersion', config.deterministicVersion, replay.deterministicVersion],
|
|
146
|
+
['inputSchemaId', config.inputSchemaId, replay.inputSchemaId],
|
|
147
|
+
['stateSchemaId', config.stateSchemaId, replay.stateSchemaId],
|
|
148
|
+
['engineIdentityHash', config.engineIdentityHash, replay.engineIdentityHash],
|
|
149
|
+
['assetDbHash', config.assetDbHash, replay.assetDbHash],
|
|
150
|
+
['configHash', config.configHash, replay.configHash],
|
|
151
|
+
];
|
|
152
|
+
for (const [field, actual, expected] of checks) {
|
|
153
|
+
if (actual === undefined || expected === undefined) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (actual !== expected) {
|
|
157
|
+
throw new errors_js_1.SyncplayError(`Replay ${field} mismatch: expected ${expected}, actual ${actual}`, {
|
|
158
|
+
code: 'replay.identity-mismatch',
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function validateInputFlags(replay, inputFrame) {
|
|
164
|
+
if (inputFrame.inputFlags === undefined) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (inputFrame.inputFlags.length !== replay.playerCount) {
|
|
168
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} has ${inputFrame.inputFlags.length} input flag records, expected ${replay.playerCount}`, {
|
|
169
|
+
code: 'replay.input-flag-count',
|
|
170
|
+
frame: inputFrame.frame,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const commandSlots = new Set();
|
|
174
|
+
for (const commandFrame of replay.commandHistory ?? []) {
|
|
175
|
+
if (commandFrame.frame !== inputFrame.frame) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
for (const command of commandFrame.commands) {
|
|
179
|
+
if (!isCanonicalReplayValue(command)) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let decoded;
|
|
183
|
+
try {
|
|
184
|
+
decoded = (0, canonical_js_1.decodeCanonicalValue)(command);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (isRecord(decoded) && typeof decoded.slot === 'number') {
|
|
190
|
+
commandSlots.add(decoded.slot);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
inputFrame.inputFlags.forEach((flags, slot) => {
|
|
195
|
+
if (flags.slot !== slot) {
|
|
196
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} input flags slot ${flags.slot} do not match index ${slot}`, {
|
|
197
|
+
code: 'replay.input-flag-slot',
|
|
198
|
+
frame: inputFrame.frame,
|
|
199
|
+
slot,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (flags.playerPresent !== true && flags.playerPresent !== false) {
|
|
203
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} input flags for slot ${slot} have invalid playerPresent`, {
|
|
204
|
+
code: 'replay.input-flag-value',
|
|
205
|
+
frame: inputFrame.frame,
|
|
206
|
+
slot,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (flags.playerPresent === false && flags.replacedByServer === true) {
|
|
210
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} input flags for slot ${slot} cannot be absent and server-replaced`, {
|
|
211
|
+
code: 'replay.input-flag-conflict',
|
|
212
|
+
frame: inputFrame.frame,
|
|
213
|
+
slot,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (flags.commandInput !== commandSlots.has(slot)) {
|
|
217
|
+
throw new errors_js_1.SyncplayError(`Replay frame ${inputFrame.frame} input command flag for slot ${slot} does not match command history`, {
|
|
218
|
+
code: 'replay.input-flag-command',
|
|
219
|
+
frame: inputFrame.frame,
|
|
220
|
+
slot,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function isRecord(value) {
|
|
226
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function assertReplayCanonicalValue(value, message, options) {
|
|
229
|
+
try {
|
|
230
|
+
if (!isCanonicalReplayValue(value)) {
|
|
231
|
+
throw new Error('not canonical');
|
|
232
|
+
}
|
|
233
|
+
(0, canonical_js_1.decodeCanonicalValue)(value);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
throw new errors_js_1.SyncplayError(message, {
|
|
237
|
+
code: 'replay.canonical-value',
|
|
238
|
+
frame: options.frame,
|
|
239
|
+
slot: options.slot,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function isCanonicalReplayValue(value) {
|
|
244
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (typeof value === 'number') {
|
|
248
|
+
return Number.isFinite(value) && !Object.is(value, -0);
|
|
249
|
+
}
|
|
250
|
+
if (Array.isArray(value)) {
|
|
251
|
+
return value.every((item) => isCanonicalReplayValue(item));
|
|
252
|
+
}
|
|
253
|
+
if (!isRecord(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
if (Object.prototype.hasOwnProperty.call(value, '$rdm')) {
|
|
257
|
+
return isCanonicalReplayTag(value);
|
|
258
|
+
}
|
|
259
|
+
if (!replayObjectKeysAreCanonical(value)) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
return replayObjectKeys(value).every((key) => isCanonicalReplayValue(value[key]));
|
|
263
|
+
}
|
|
264
|
+
function isCanonicalReplayTag(value) {
|
|
265
|
+
const tag = value.$rdm;
|
|
266
|
+
if (typeof tag !== 'string') {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
switch (tag) {
|
|
270
|
+
case 'nan':
|
|
271
|
+
case 'infinity':
|
|
272
|
+
case 'negative-infinity':
|
|
273
|
+
case 'negative-zero':
|
|
274
|
+
case 'undefined':
|
|
275
|
+
return hasExactReplayTagKeys(value, ['$rdm']);
|
|
276
|
+
case 'bigint':
|
|
277
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'value']) && typeof value.value === 'string';
|
|
278
|
+
case 'map':
|
|
279
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'entries']) &&
|
|
280
|
+
Array.isArray(value.entries) &&
|
|
281
|
+
value.entries.every((entry) => Array.isArray(entry) &&
|
|
282
|
+
entry.length === 2 &&
|
|
283
|
+
isCanonicalReplayValue(entry[0]) &&
|
|
284
|
+
isCanonicalReplayValue(entry[1])) &&
|
|
285
|
+
replayMapEntriesAreCanonical(value.entries);
|
|
286
|
+
case 'set':
|
|
287
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'values']) &&
|
|
288
|
+
Array.isArray(value.values) &&
|
|
289
|
+
value.values.every((entry) => isCanonicalReplayValue(entry)) &&
|
|
290
|
+
replaySetValuesAreCanonical(value.values);
|
|
291
|
+
case 'typed-array':
|
|
292
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'constructor', 'bytes']) &&
|
|
293
|
+
typeof value.constructor === 'string' &&
|
|
294
|
+
Array.isArray(value.bytes) &&
|
|
295
|
+
value.bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
296
|
+
default:
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function hasExactReplayTagKeys(value, expectedKeys) {
|
|
301
|
+
const actual = replayObjectKeys(value);
|
|
302
|
+
if (actual.length !== expectedKeys.length) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return actual.every((key, index) => key === expectedKeys[index]);
|
|
306
|
+
}
|
|
307
|
+
function replayObjectKeysAreCanonical(value) {
|
|
308
|
+
const actual = replayObjectKeys(value);
|
|
309
|
+
const expected = [...actual].sort(compareStrings);
|
|
310
|
+
return actual.every((key, index) => key === expected[index]);
|
|
311
|
+
}
|
|
312
|
+
function replayObjectKeys(value) {
|
|
313
|
+
// rdm-ignore-next-line determinism/no-object-key-order: replay canonical validation intentionally reads encoded object key order before comparing it to canonical order
|
|
314
|
+
return Object.keys(value);
|
|
315
|
+
}
|
|
316
|
+
function replayMapEntriesAreCanonical(entries) {
|
|
317
|
+
let previousKeyText;
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
const key = entry[0];
|
|
320
|
+
const keyText = canonicalReplaySortText(key);
|
|
321
|
+
if (keyText === undefined) {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (previousKeyText !== undefined && previousKeyText > keyText) {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
previousKeyText = keyText;
|
|
328
|
+
}
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
function replaySetValuesAreCanonical(values) {
|
|
332
|
+
let previousValueText;
|
|
333
|
+
for (const value of values) {
|
|
334
|
+
const canonicalValue = value;
|
|
335
|
+
const valueText = canonicalReplaySortText(canonicalValue);
|
|
336
|
+
if (valueText === undefined) {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
if (previousValueText !== undefined && previousValueText > valueText) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
previousValueText = valueText;
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
function canonicalReplaySortText(value) {
|
|
347
|
+
try {
|
|
348
|
+
return (0, canonical_js_1.canonicalStringify)((0, canonical_js_1.decodeCanonicalValue)(value));
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function compareStrings(left, right) {
|
|
355
|
+
return Number(left > right) - Number(left < right);
|
|
356
|
+
}
|