@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
package/dist/replay.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { canonicalize, canonicalStringify, decodeCanonicalValue, defaultChecksum } from './canonical.js';
|
|
2
|
+
import { SyncplayError } from './errors.js';
|
|
3
|
+
import { createOfflineSession } from './offline-session.js';
|
|
4
|
+
import { createSchemaArtifacts } from './schema-artifacts.js';
|
|
5
|
+
export function verifyReplay(config) {
|
|
6
|
+
const replay = config.replay;
|
|
7
|
+
assertReplayFileStructure(replay);
|
|
8
|
+
assertReplayIdentity(config);
|
|
9
|
+
const initialState = decodeCanonicalValue(replay.initialState);
|
|
10
|
+
const session = createOfflineSession({
|
|
11
|
+
...config,
|
|
12
|
+
initialState,
|
|
13
|
+
playerCount: replay.playerCount,
|
|
14
|
+
tickRate: replay.tickRate,
|
|
15
|
+
randomSeed: replay.randomSeed,
|
|
16
|
+
});
|
|
17
|
+
const checksums = new Map(replay.checksums.map((checksum) => [checksum.frame, checksum.checksum]));
|
|
18
|
+
const commandFrames = new Map((replay.commandHistory ?? []).map((commandFrame) => [commandFrame.frame, commandFrame.commands]));
|
|
19
|
+
let verifiedChecksums = 0;
|
|
20
|
+
let finalChecksum = String(defaultChecksum(initialState));
|
|
21
|
+
for (const inputFrame of replay.inputFrames) {
|
|
22
|
+
if (inputFrame.frame !== session.currentFrame) {
|
|
23
|
+
throw new SyncplayError(`Replay input frame ${inputFrame.frame} does not match session frame ${session.currentFrame}`, { code: 'replay.input-frame-gap', frame: session.currentFrame });
|
|
24
|
+
}
|
|
25
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
26
|
+
const decodedInput = decodeCanonicalValue(input);
|
|
27
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
28
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decodedInput) ?? decodedInput);
|
|
29
|
+
session.setInput(slot, replayInput);
|
|
30
|
+
if (flags !== undefined) {
|
|
31
|
+
session.setInputFlags(slot, flags);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const commands = commandFrames.get(inputFrame.frame);
|
|
35
|
+
if (commands !== undefined) {
|
|
36
|
+
session.recordCommands(commands.map((command) => decodeCanonicalValue(command)));
|
|
37
|
+
}
|
|
38
|
+
session.stepFrames(1);
|
|
39
|
+
const expectedChecksum = checksums.get(session.currentFrame);
|
|
40
|
+
if (expectedChecksum !== undefined) {
|
|
41
|
+
const actualChecksum = String(config.checksum?.(session.getState()) ?? defaultChecksum(config.canonicalizeState?.(session.getState()) ?? session.getState()));
|
|
42
|
+
verifiedChecksums += 1;
|
|
43
|
+
finalChecksum = actualChecksum;
|
|
44
|
+
if (actualChecksum !== expectedChecksum) {
|
|
45
|
+
const mismatch = {
|
|
46
|
+
frame: session.currentFrame,
|
|
47
|
+
expectedChecksum,
|
|
48
|
+
actualChecksum,
|
|
49
|
+
inputFrame,
|
|
50
|
+
state: canonicalize(config.canonicalizeState?.(session.getState()) ?? session.getState()),
|
|
51
|
+
};
|
|
52
|
+
if (config.throwOnMismatch === false) {
|
|
53
|
+
return {
|
|
54
|
+
frames: session.currentFrame,
|
|
55
|
+
checksums: verifiedChecksums,
|
|
56
|
+
mismatches: 1,
|
|
57
|
+
finalChecksum,
|
|
58
|
+
firstMismatch: mismatch,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
throw new SyncplayError(`Checksum mismatch at frame ${session.currentFrame}: expected ${expectedChecksum}, actual ${actualChecksum}`, { code: 'replay.checksum-mismatch', frame: session.currentFrame });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (verifiedChecksums !== replay.checksums.length) {
|
|
66
|
+
throw new SyncplayError(`Replay contained ${replay.checksums.length - verifiedChecksums} checksum records beyond verified input frames`, { code: 'replay.unverified-checksum', frame: session.currentFrame });
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
frames: session.currentFrame,
|
|
70
|
+
checksums: verifiedChecksums,
|
|
71
|
+
mismatches: 0,
|
|
72
|
+
finalChecksum,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function verifyReplayWithDescriptor(config) {
|
|
76
|
+
const artifacts = createSchemaArtifacts(config.descriptor);
|
|
77
|
+
return verifyReplay({
|
|
78
|
+
replay: config.replay,
|
|
79
|
+
step: config.step,
|
|
80
|
+
defaultInput: config.defaultInput,
|
|
81
|
+
// The CONTENT-HASHED identity, not the author label: runtimes stamp
|
|
82
|
+
// replayIdentity.* into exported replays (a field edit without a label
|
|
83
|
+
// bump must change the identity), so the descriptor-derived verifier must
|
|
84
|
+
// compare the same form or every defineSyncplayGame replay fails identity.
|
|
85
|
+
inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
|
|
86
|
+
stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
|
|
87
|
+
deterministicVersion: artifacts.descriptor.deterministicVersion,
|
|
88
|
+
canonicalizeInput(input) {
|
|
89
|
+
return artifacts.canonicalizeInput(input);
|
|
90
|
+
},
|
|
91
|
+
canonicalizeState(state) {
|
|
92
|
+
return artifacts.canonicalizeState(state);
|
|
93
|
+
},
|
|
94
|
+
checksum(state) {
|
|
95
|
+
return artifacts.checksumState(state);
|
|
96
|
+
},
|
|
97
|
+
serializeState(state) {
|
|
98
|
+
return artifacts.serializeState(state);
|
|
99
|
+
},
|
|
100
|
+
deserializeState(bytes) {
|
|
101
|
+
return artifacts.deserializeState(bytes);
|
|
102
|
+
},
|
|
103
|
+
hydrateInput: config.hydrateInput,
|
|
104
|
+
throwOnMismatch: config.throwOnMismatch,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
export function assertReplayFileStructure(replay) {
|
|
108
|
+
if (replay.formatVersion !== 1) {
|
|
109
|
+
throw new SyncplayError(`Unsupported replay format ${replay.formatVersion}`, {
|
|
110
|
+
code: 'replay.format-version',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
assertReplayCanonicalValue(replay.initialState, 'Replay initialState is not a valid canonical value', {});
|
|
114
|
+
for (const inputFrame of replay.inputFrames) {
|
|
115
|
+
if (inputFrame.inputs.length !== replay.playerCount) {
|
|
116
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} has ${inputFrame.inputs.length} inputs, expected ${replay.playerCount}`, {
|
|
117
|
+
code: 'replay.input-count',
|
|
118
|
+
frame: inputFrame.frame,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
validateInputFlags(replay, inputFrame);
|
|
122
|
+
inputFrame.inputs.forEach((input, slot) => assertReplayCanonicalValue(input, `Replay frame ${inputFrame.frame} input ${slot} is not a valid canonical value`, { frame: inputFrame.frame, slot }));
|
|
123
|
+
}
|
|
124
|
+
for (const commandFrame of replay.commandHistory ?? []) {
|
|
125
|
+
if (!Number.isInteger(commandFrame.frame) || commandFrame.frame < 0 || commandFrame.frame >= replay.inputFrames.length) {
|
|
126
|
+
throw new SyncplayError(`Replay command frame ${commandFrame.frame} is outside replay input history`, {
|
|
127
|
+
code: 'replay.command-frame-gap',
|
|
128
|
+
frame: commandFrame.frame,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
for (const command of commandFrame.commands) {
|
|
132
|
+
const canonicalCommand = command;
|
|
133
|
+
assertReplayCanonicalValue(canonicalCommand, `Replay command frame ${commandFrame.frame} contains a non-canonical command`, { frame: commandFrame.frame });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function assertReplayIdentity(config) {
|
|
138
|
+
const replay = config.replay;
|
|
139
|
+
const checks = [
|
|
140
|
+
['deterministicVersion', config.deterministicVersion, replay.deterministicVersion],
|
|
141
|
+
['inputSchemaId', config.inputSchemaId, replay.inputSchemaId],
|
|
142
|
+
['stateSchemaId', config.stateSchemaId, replay.stateSchemaId],
|
|
143
|
+
['engineIdentityHash', config.engineIdentityHash, replay.engineIdentityHash],
|
|
144
|
+
['assetDbHash', config.assetDbHash, replay.assetDbHash],
|
|
145
|
+
['configHash', config.configHash, replay.configHash],
|
|
146
|
+
];
|
|
147
|
+
for (const [field, actual, expected] of checks) {
|
|
148
|
+
if (actual === undefined || expected === undefined) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (actual !== expected) {
|
|
152
|
+
throw new SyncplayError(`Replay ${field} mismatch: expected ${expected}, actual ${actual}`, {
|
|
153
|
+
code: 'replay.identity-mismatch',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function validateInputFlags(replay, inputFrame) {
|
|
159
|
+
if (inputFrame.inputFlags === undefined) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (inputFrame.inputFlags.length !== replay.playerCount) {
|
|
163
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} has ${inputFrame.inputFlags.length} input flag records, expected ${replay.playerCount}`, {
|
|
164
|
+
code: 'replay.input-flag-count',
|
|
165
|
+
frame: inputFrame.frame,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const commandSlots = new Set();
|
|
169
|
+
for (const commandFrame of replay.commandHistory ?? []) {
|
|
170
|
+
if (commandFrame.frame !== inputFrame.frame) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
for (const command of commandFrame.commands) {
|
|
174
|
+
if (!isCanonicalReplayValue(command)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let decoded;
|
|
178
|
+
try {
|
|
179
|
+
decoded = decodeCanonicalValue(command);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (isRecord(decoded) && typeof decoded.slot === 'number') {
|
|
185
|
+
commandSlots.add(decoded.slot);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
inputFrame.inputFlags.forEach((flags, slot) => {
|
|
190
|
+
if (flags.slot !== slot) {
|
|
191
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} input flags slot ${flags.slot} do not match index ${slot}`, {
|
|
192
|
+
code: 'replay.input-flag-slot',
|
|
193
|
+
frame: inputFrame.frame,
|
|
194
|
+
slot,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
if (flags.playerPresent !== true && flags.playerPresent !== false) {
|
|
198
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} input flags for slot ${slot} have invalid playerPresent`, {
|
|
199
|
+
code: 'replay.input-flag-value',
|
|
200
|
+
frame: inputFrame.frame,
|
|
201
|
+
slot,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (flags.playerPresent === false && flags.replacedByServer === true) {
|
|
205
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} input flags for slot ${slot} cannot be absent and server-replaced`, {
|
|
206
|
+
code: 'replay.input-flag-conflict',
|
|
207
|
+
frame: inputFrame.frame,
|
|
208
|
+
slot,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (flags.commandInput !== commandSlots.has(slot)) {
|
|
212
|
+
throw new SyncplayError(`Replay frame ${inputFrame.frame} input command flag for slot ${slot} does not match command history`, {
|
|
213
|
+
code: 'replay.input-flag-command',
|
|
214
|
+
frame: inputFrame.frame,
|
|
215
|
+
slot,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function isRecord(value) {
|
|
221
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
222
|
+
}
|
|
223
|
+
function assertReplayCanonicalValue(value, message, options) {
|
|
224
|
+
try {
|
|
225
|
+
if (!isCanonicalReplayValue(value)) {
|
|
226
|
+
throw new Error('not canonical');
|
|
227
|
+
}
|
|
228
|
+
decodeCanonicalValue(value);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
throw new SyncplayError(message, {
|
|
232
|
+
code: 'replay.canonical-value',
|
|
233
|
+
frame: options.frame,
|
|
234
|
+
slot: options.slot,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function isCanonicalReplayValue(value) {
|
|
239
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string') {
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
if (typeof value === 'number') {
|
|
243
|
+
return Number.isFinite(value) && !Object.is(value, -0);
|
|
244
|
+
}
|
|
245
|
+
if (Array.isArray(value)) {
|
|
246
|
+
return value.every((item) => isCanonicalReplayValue(item));
|
|
247
|
+
}
|
|
248
|
+
if (!isRecord(value) || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
if (Object.prototype.hasOwnProperty.call(value, '$rdm')) {
|
|
252
|
+
return isCanonicalReplayTag(value);
|
|
253
|
+
}
|
|
254
|
+
if (!replayObjectKeysAreCanonical(value)) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return replayObjectKeys(value).every((key) => isCanonicalReplayValue(value[key]));
|
|
258
|
+
}
|
|
259
|
+
function isCanonicalReplayTag(value) {
|
|
260
|
+
const tag = value.$rdm;
|
|
261
|
+
if (typeof tag !== 'string') {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
switch (tag) {
|
|
265
|
+
case 'nan':
|
|
266
|
+
case 'infinity':
|
|
267
|
+
case 'negative-infinity':
|
|
268
|
+
case 'negative-zero':
|
|
269
|
+
case 'undefined':
|
|
270
|
+
return hasExactReplayTagKeys(value, ['$rdm']);
|
|
271
|
+
case 'bigint':
|
|
272
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'value']) && typeof value.value === 'string';
|
|
273
|
+
case 'map':
|
|
274
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'entries']) &&
|
|
275
|
+
Array.isArray(value.entries) &&
|
|
276
|
+
value.entries.every((entry) => Array.isArray(entry) &&
|
|
277
|
+
entry.length === 2 &&
|
|
278
|
+
isCanonicalReplayValue(entry[0]) &&
|
|
279
|
+
isCanonicalReplayValue(entry[1])) &&
|
|
280
|
+
replayMapEntriesAreCanonical(value.entries);
|
|
281
|
+
case 'set':
|
|
282
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'values']) &&
|
|
283
|
+
Array.isArray(value.values) &&
|
|
284
|
+
value.values.every((entry) => isCanonicalReplayValue(entry)) &&
|
|
285
|
+
replaySetValuesAreCanonical(value.values);
|
|
286
|
+
case 'typed-array':
|
|
287
|
+
return hasExactReplayTagKeys(value, ['$rdm', 'constructor', 'bytes']) &&
|
|
288
|
+
typeof value.constructor === 'string' &&
|
|
289
|
+
Array.isArray(value.bytes) &&
|
|
290
|
+
value.bytes.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255);
|
|
291
|
+
default:
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function hasExactReplayTagKeys(value, expectedKeys) {
|
|
296
|
+
const actual = replayObjectKeys(value);
|
|
297
|
+
if (actual.length !== expectedKeys.length) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return actual.every((key, index) => key === expectedKeys[index]);
|
|
301
|
+
}
|
|
302
|
+
function replayObjectKeysAreCanonical(value) {
|
|
303
|
+
const actual = replayObjectKeys(value);
|
|
304
|
+
const expected = [...actual].sort(compareStrings);
|
|
305
|
+
return actual.every((key, index) => key === expected[index]);
|
|
306
|
+
}
|
|
307
|
+
function replayObjectKeys(value) {
|
|
308
|
+
// rdm-ignore-next-line determinism/no-object-key-order: replay canonical validation intentionally reads encoded object key order before comparing it to canonical order
|
|
309
|
+
return Object.keys(value);
|
|
310
|
+
}
|
|
311
|
+
function replayMapEntriesAreCanonical(entries) {
|
|
312
|
+
let previousKeyText;
|
|
313
|
+
for (const entry of entries) {
|
|
314
|
+
const key = entry[0];
|
|
315
|
+
const keyText = canonicalReplaySortText(key);
|
|
316
|
+
if (keyText === undefined) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
if (previousKeyText !== undefined && previousKeyText > keyText) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
previousKeyText = keyText;
|
|
323
|
+
}
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
function replaySetValuesAreCanonical(values) {
|
|
327
|
+
let previousValueText;
|
|
328
|
+
for (const value of values) {
|
|
329
|
+
const canonicalValue = value;
|
|
330
|
+
const valueText = canonicalReplaySortText(canonicalValue);
|
|
331
|
+
if (valueText === undefined) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
if (previousValueText !== undefined && previousValueText > valueText) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
previousValueText = valueText;
|
|
338
|
+
}
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
function canonicalReplaySortText(value) {
|
|
342
|
+
try {
|
|
343
|
+
return canonicalStringify(decodeCanonicalValue(value));
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
function compareStrings(left, right) {
|
|
350
|
+
return Number(left > right) - Number(left < right);
|
|
351
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { DeterministicAssetCookManifest } from './asset-cooking.js';
|
|
2
|
+
import type { ProtocolRollbackLabOptions, ProtocolRollbackLabResult } from './protocol-lab.js';
|
|
3
|
+
import type { DeterminismDiagnostic, DeterminismParserKind, DeterminismSuppression } from './tools/static-checker.js';
|
|
4
|
+
import type { ReplayVerificationMismatch, TickRate } from './types.js';
|
|
5
|
+
export interface DeterministicReport<Config, Metrics> {
|
|
6
|
+
readonly schemaVersion: 1;
|
|
7
|
+
readonly reportId: string;
|
|
8
|
+
readonly kind: string;
|
|
9
|
+
readonly generatedAtFrame: number;
|
|
10
|
+
readonly config: Config;
|
|
11
|
+
readonly metrics: Metrics;
|
|
12
|
+
readonly result: 'pass' | 'fail' | 'desync' | 'warn';
|
|
13
|
+
}
|
|
14
|
+
export type ProtocolRollbackReport = DeterministicReport<{
|
|
15
|
+
readonly game: string;
|
|
16
|
+
readonly players: number;
|
|
17
|
+
readonly frames: number;
|
|
18
|
+
readonly seed: number;
|
|
19
|
+
readonly minDelayFrames: number;
|
|
20
|
+
readonly maxDelayFrames: number;
|
|
21
|
+
readonly bundleSizeFrames: number;
|
|
22
|
+
readonly inputDelayFrames: number;
|
|
23
|
+
readonly duplicatePermille: number;
|
|
24
|
+
readonly lossPermille: number;
|
|
25
|
+
readonly resendIntervalFrames: number;
|
|
26
|
+
readonly maxResendAttempts: number;
|
|
27
|
+
readonly disconnectStartFrame?: number;
|
|
28
|
+
readonly disconnectEndFrame?: number;
|
|
29
|
+
readonly lateJoinSlot?: number;
|
|
30
|
+
readonly lateJoinFrame?: number;
|
|
31
|
+
readonly latencySkewSlot?: number;
|
|
32
|
+
readonly latencySkewFrames?: number;
|
|
33
|
+
readonly botSlots: readonly number[];
|
|
34
|
+
readonly desyncFrame?: number;
|
|
35
|
+
readonly desyncSlot?: number;
|
|
36
|
+
readonly expectDesync: boolean;
|
|
37
|
+
readonly eventTimelineLimit: number;
|
|
38
|
+
readonly predictionPolicy: string;
|
|
39
|
+
}, ProtocolRollbackLabResult & {
|
|
40
|
+
readonly elapsedMs: string;
|
|
41
|
+
readonly heapUsedBytes: number;
|
|
42
|
+
readonly rssBytes: number;
|
|
43
|
+
}>;
|
|
44
|
+
export interface DeterministicReportDiffEntry {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly left: string;
|
|
47
|
+
readonly right: string;
|
|
48
|
+
}
|
|
49
|
+
export interface DeterministicReportDiff {
|
|
50
|
+
readonly schemaVersion: 1;
|
|
51
|
+
readonly kind: 'report-diff';
|
|
52
|
+
readonly leftReportId: string;
|
|
53
|
+
readonly rightReportId: string;
|
|
54
|
+
readonly differences: number;
|
|
55
|
+
readonly truncatedDifferences: number;
|
|
56
|
+
readonly entries: readonly DeterministicReportDiffEntry[];
|
|
57
|
+
readonly result: 'pass' | 'diff';
|
|
58
|
+
}
|
|
59
|
+
export interface DeterministicAssetCookCliFileResult {
|
|
60
|
+
readonly path: string;
|
|
61
|
+
readonly expected: 'pass' | 'fail';
|
|
62
|
+
readonly actual: 'pass' | 'fail';
|
|
63
|
+
readonly assetDbHash?: string;
|
|
64
|
+
readonly schemaHash?: string;
|
|
65
|
+
readonly dependencyHash?: string;
|
|
66
|
+
readonly lookupHash?: string;
|
|
67
|
+
readonly assetCount?: number;
|
|
68
|
+
readonly byteStable?: boolean;
|
|
69
|
+
readonly lookupStable?: boolean;
|
|
70
|
+
readonly schemaHashStable?: boolean;
|
|
71
|
+
readonly dependencyOrderStable?: boolean;
|
|
72
|
+
readonly prototypeSpawned?: boolean;
|
|
73
|
+
readonly staleEngineRejected?: boolean;
|
|
74
|
+
readonly staleAssetDbRejected?: boolean;
|
|
75
|
+
readonly manifest?: DeterministicAssetCookManifest;
|
|
76
|
+
readonly error?: string;
|
|
77
|
+
}
|
|
78
|
+
export interface DeterministicAssetCookCliReport {
|
|
79
|
+
readonly schemaVersion: 1;
|
|
80
|
+
readonly kind: 'deterministic-asset-cook';
|
|
81
|
+
readonly files: number;
|
|
82
|
+
readonly cookedAssets: number;
|
|
83
|
+
readonly combinedHash: string;
|
|
84
|
+
readonly results: readonly DeterministicAssetCookCliFileResult[];
|
|
85
|
+
readonly result: 'pass' | 'fail';
|
|
86
|
+
}
|
|
87
|
+
export interface ReplayVerifyCliFileResult {
|
|
88
|
+
readonly path: string;
|
|
89
|
+
readonly frames: number;
|
|
90
|
+
readonly players: number;
|
|
91
|
+
readonly tickRate: TickRate;
|
|
92
|
+
readonly checksums: number;
|
|
93
|
+
readonly mismatches: number;
|
|
94
|
+
readonly finalChecksum: string;
|
|
95
|
+
readonly firstMismatch?: ReplayVerificationMismatch;
|
|
96
|
+
readonly result: 'pass' | 'fail';
|
|
97
|
+
}
|
|
98
|
+
export interface ReplayVerifyCliReport {
|
|
99
|
+
readonly schemaVersion: 1;
|
|
100
|
+
readonly kind: 'replay-verify';
|
|
101
|
+
readonly files: number;
|
|
102
|
+
readonly startFrame: number;
|
|
103
|
+
readonly endFrame: number | '';
|
|
104
|
+
readonly frames: number;
|
|
105
|
+
readonly checksums: number;
|
|
106
|
+
readonly mismatches: number;
|
|
107
|
+
readonly results: readonly ReplayVerifyCliFileResult[];
|
|
108
|
+
readonly firstMismatch?: ReplayVerificationMismatch;
|
|
109
|
+
readonly heapUsedBytes: number;
|
|
110
|
+
readonly rssBytes: number;
|
|
111
|
+
readonly elapsedMs: string;
|
|
112
|
+
readonly result: 'pass' | 'fail';
|
|
113
|
+
}
|
|
114
|
+
export interface StaticDeterminismReport {
|
|
115
|
+
readonly schemaVersion: 1;
|
|
116
|
+
readonly kind: 'static-determinism';
|
|
117
|
+
readonly parser: DeterminismParserKind;
|
|
118
|
+
readonly certificationMode: boolean;
|
|
119
|
+
readonly sourceOnly: boolean;
|
|
120
|
+
readonly checkedPaths: readonly string[];
|
|
121
|
+
readonly project?: string;
|
|
122
|
+
readonly classifiedExcludedPaths?: readonly string[];
|
|
123
|
+
readonly boundaryCheckedPaths?: readonly string[];
|
|
124
|
+
readonly knownBadPaths: readonly string[];
|
|
125
|
+
readonly diagnostics: readonly DeterminismDiagnostic[];
|
|
126
|
+
readonly boundaryDiagnostics?: readonly DeterminismDiagnostic[];
|
|
127
|
+
readonly knownBadDiagnostics: readonly DeterminismDiagnostic[];
|
|
128
|
+
readonly passErrors: number;
|
|
129
|
+
readonly boundaryErrors?: number;
|
|
130
|
+
readonly knownBadErrors: number;
|
|
131
|
+
readonly suppressions: readonly DeterminismSuppression[];
|
|
132
|
+
readonly suppressionCount: number;
|
|
133
|
+
readonly invalidSuppressions: number;
|
|
134
|
+
readonly engineCompleteAudit?: StaticDeterminismEngineCompleteArtifactAudit;
|
|
135
|
+
readonly engineParityAudit?: StaticDeterminismEngineParityArtifactAudit;
|
|
136
|
+
readonly result: 'pass' | 'fail';
|
|
137
|
+
}
|
|
138
|
+
export interface StaticDeterminismEngineCompleteArtifactAudit {
|
|
139
|
+
readonly checked: boolean;
|
|
140
|
+
readonly result: 'pass' | 'warn' | 'fail';
|
|
141
|
+
readonly reports: readonly StaticDeterminismEngineCompleteArtifactAuditReport[];
|
|
142
|
+
readonly warnings: readonly string[];
|
|
143
|
+
readonly failures: readonly string[];
|
|
144
|
+
}
|
|
145
|
+
export interface StaticDeterminismEngineCompleteArtifactAuditReport {
|
|
146
|
+
readonly path: string;
|
|
147
|
+
readonly kind: 'certification' | 'benchmark';
|
|
148
|
+
readonly result: 'pass' | 'fail' | 'missing' | 'invalid';
|
|
149
|
+
readonly categories: readonly StaticDeterminismEngineCompleteArtifactAuditCategory[];
|
|
150
|
+
readonly warnings: readonly string[];
|
|
151
|
+
readonly failures: readonly string[];
|
|
152
|
+
}
|
|
153
|
+
export interface StaticDeterminismEngineCompleteArtifactAuditCategory {
|
|
154
|
+
readonly category: string;
|
|
155
|
+
readonly result: 'pass' | 'fail';
|
|
156
|
+
readonly requestedFrames: number;
|
|
157
|
+
readonly executedFrames: number;
|
|
158
|
+
readonly capped: boolean;
|
|
159
|
+
readonly wallClockElapsedMs: number;
|
|
160
|
+
readonly wallClockMeanFrameMs: number;
|
|
161
|
+
}
|
|
162
|
+
export interface StaticDeterminismEngineParityArtifactAudit {
|
|
163
|
+
readonly checked: boolean;
|
|
164
|
+
readonly result: 'pass' | 'fail';
|
|
165
|
+
readonly files: readonly string[];
|
|
166
|
+
readonly failures: readonly string[];
|
|
167
|
+
}
|
|
168
|
+
export declare function createProtocolRollbackReport(options: ProtocolRollbackLabOptions, result: ProtocolRollbackLabResult & {
|
|
169
|
+
readonly elapsedMs: string;
|
|
170
|
+
readonly heapUsedBytes: number;
|
|
171
|
+
readonly rssBytes: number;
|
|
172
|
+
}): ProtocolRollbackReport;
|
|
173
|
+
export declare function stableReportJson(report: DeterministicReport<unknown, unknown>): string;
|
|
174
|
+
export declare function diffDeterministicReports(left: unknown, right: unknown, maxDifferences?: number): DeterministicReportDiff;
|
|
175
|
+
export declare function stableReportDiffJson(diff: DeterministicReportDiff): string;
|
|
176
|
+
export declare function renderDeterministicReportHtml(report: DeterministicReport<unknown, unknown>): string;
|