@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,60 @@
|
|
|
1
|
+
import { type CanonicalValue } from './canonical.js';
|
|
2
|
+
import type { DeterministicGameDescriptor } from './schema.js';
|
|
3
|
+
import type { DeterministicSessionConfig, RecordedCommandFrame, ReplayFile, RecordedInputFrame } from './types.js';
|
|
4
|
+
export interface ReplayInspectConfig<State, Input> extends Omit<DeterministicSessionConfig<State, Input>, 'initialState' | 'playerCount' | 'tickRate' | 'randomSeed'> {
|
|
5
|
+
readonly replay: ReplayFile;
|
|
6
|
+
}
|
|
7
|
+
export interface ReplayFrameDump {
|
|
8
|
+
readonly frame: number;
|
|
9
|
+
readonly checksum: string;
|
|
10
|
+
readonly expectedChecksum?: string;
|
|
11
|
+
readonly checksumMatch?: boolean;
|
|
12
|
+
readonly inputFrame?: RecordedInputFrame;
|
|
13
|
+
readonly commandFrame?: RecordedCommandFrame;
|
|
14
|
+
readonly appliedInputFrame?: RecordedInputFrame;
|
|
15
|
+
readonly appliedCommandFrame?: RecordedCommandFrame;
|
|
16
|
+
readonly state: CanonicalValue;
|
|
17
|
+
readonly checksumExplanation?: ReplayChecksumExplanation;
|
|
18
|
+
}
|
|
19
|
+
export interface ReplayFrameDiff {
|
|
20
|
+
readonly path: string;
|
|
21
|
+
readonly left: CanonicalValue | undefined;
|
|
22
|
+
readonly right: CanonicalValue | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface ReplayDiffResult {
|
|
25
|
+
readonly frame: number;
|
|
26
|
+
readonly leftChecksum: string;
|
|
27
|
+
readonly rightChecksum: string;
|
|
28
|
+
readonly checksumMatch: boolean;
|
|
29
|
+
readonly differences: readonly ReplayFrameDiff[];
|
|
30
|
+
}
|
|
31
|
+
export interface ReplayChecksumExplanation {
|
|
32
|
+
readonly frame: number;
|
|
33
|
+
readonly checksum: string;
|
|
34
|
+
readonly tableCount: number;
|
|
35
|
+
readonly tables: readonly ReplayChecksumTableExplanation[];
|
|
36
|
+
}
|
|
37
|
+
export interface ReplayChecksumTableExplanation {
|
|
38
|
+
readonly path: string;
|
|
39
|
+
readonly checksum: string;
|
|
40
|
+
readonly bytes: number;
|
|
41
|
+
readonly entries: number;
|
|
42
|
+
readonly sampleKeys: readonly string[];
|
|
43
|
+
}
|
|
44
|
+
export interface DescriptorReplayInspectConfig<State, Input> {
|
|
45
|
+
readonly descriptor: DeterministicGameDescriptor;
|
|
46
|
+
readonly replay: ReplayFile;
|
|
47
|
+
readonly step: DeterministicSessionConfig<State, Input>['step'];
|
|
48
|
+
readonly defaultInput: Input;
|
|
49
|
+
readonly hydrateInput?: (recordedInput: unknown) => Input;
|
|
50
|
+
}
|
|
51
|
+
export declare function inspectReplayFrame<State, Input>(config: ReplayInspectConfig<State, Input>, frame: number, options?: {
|
|
52
|
+
readonly explainChecksum?: boolean;
|
|
53
|
+
}): ReplayFrameDump;
|
|
54
|
+
export declare function inspectReplayFrameWithDescriptor<State, Input>(config: DescriptorReplayInspectConfig<State, Input>, frame: number, options?: {
|
|
55
|
+
readonly explainChecksum?: boolean;
|
|
56
|
+
}): ReplayFrameDump;
|
|
57
|
+
export declare function explainReplayChecksum(dump: Pick<ReplayFrameDump, 'frame' | 'checksum' | 'state'>): ReplayChecksumExplanation;
|
|
58
|
+
export declare function diffReplayFrames(left: ReplayFrameDump, right: ReplayFrameDump, maxDifferences?: number): ReplayDiffResult;
|
|
59
|
+
export declare function stableFrameDumpJson(dump: ReplayFrameDump): string;
|
|
60
|
+
export declare function stableFrameDiffJson(diff: ReplayDiffResult): string;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { canonicalize, cloneCanonical, decodeCanonicalValue, defaultChecksum, encodeUtf8 } from './canonical.js';
|
|
2
|
+
import { stableSortedKeys } from './collections.js';
|
|
3
|
+
import { SyncplayError } from './errors.js';
|
|
4
|
+
import { createOfflineSession } from './offline-session.js';
|
|
5
|
+
import { assertReplayFileStructure } from './replay.js';
|
|
6
|
+
import { createSchemaArtifacts } from './schema-artifacts.js';
|
|
7
|
+
export function inspectReplayFrame(config, frame, options = {}) {
|
|
8
|
+
assertReplayFileStructure(config.replay);
|
|
9
|
+
validateFrame(config.replay, frame);
|
|
10
|
+
assertReplayIdentity(config);
|
|
11
|
+
const initialState = decodeCanonicalValue(config.replay.initialState);
|
|
12
|
+
const session = createOfflineSession({
|
|
13
|
+
...config,
|
|
14
|
+
initialState,
|
|
15
|
+
playerCount: config.replay.playerCount,
|
|
16
|
+
tickRate: config.replay.tickRate,
|
|
17
|
+
randomSeed: config.replay.randomSeed,
|
|
18
|
+
replayRecordingMode: 'none',
|
|
19
|
+
snapshotBufferSize: Math.max(1, frame + 1),
|
|
20
|
+
});
|
|
21
|
+
const commandFrames = new Map((config.replay.commandHistory ?? []).map((commandFrame) => [commandFrame.frame, commandFrame.commands]));
|
|
22
|
+
for (const inputFrame of config.replay.inputFrames) {
|
|
23
|
+
if (inputFrame.frame >= frame) {
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
if (inputFrame.frame !== session.currentFrame) {
|
|
27
|
+
throw new SyncplayError(`Replay input frame ${inputFrame.frame} does not match session frame ${session.currentFrame}`, { code: 'replay.input-frame-gap', frame: session.currentFrame });
|
|
28
|
+
}
|
|
29
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
30
|
+
const decodedInput = decodeCanonicalValue(input);
|
|
31
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
32
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decodedInput) ?? decodedInput);
|
|
33
|
+
session.setInput(slot, replayInput);
|
|
34
|
+
if (flags !== undefined) {
|
|
35
|
+
session.setInputFlags(slot, flags);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
const commands = commandFrames.get(inputFrame.frame);
|
|
39
|
+
if (commands !== undefined) {
|
|
40
|
+
session.recordCommands(commands.map((command) => decodeCanonicalValue(command)));
|
|
41
|
+
}
|
|
42
|
+
session.stepFrames(1);
|
|
43
|
+
}
|
|
44
|
+
const state = config.canonicalizeState?.(session.getState()) ?? session.getState();
|
|
45
|
+
const checksum = String(config.checksum?.(session.getState()) ?? defaultChecksum(state));
|
|
46
|
+
const expectedChecksum = replayChecksumForFrame(config.replay, frame);
|
|
47
|
+
const dump = {
|
|
48
|
+
frame,
|
|
49
|
+
checksum,
|
|
50
|
+
expectedChecksum,
|
|
51
|
+
checksumMatch: expectedChecksum === undefined ? undefined : expectedChecksum === checksum,
|
|
52
|
+
inputFrame: cloneOptional(config.replay.inputFrames.find((record) => record.frame === frame)),
|
|
53
|
+
commandFrame: cloneOptional(config.replay.commandHistory?.find((record) => record.frame === frame)),
|
|
54
|
+
appliedInputFrame: cloneOptional(config.replay.inputFrames.find((record) => record.frame === frame - 1)),
|
|
55
|
+
appliedCommandFrame: cloneOptional(config.replay.commandHistory?.find((record) => record.frame === frame - 1)),
|
|
56
|
+
state: canonicalize(state),
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
...dump,
|
|
60
|
+
checksumExplanation: options.explainChecksum === true ? explainReplayChecksum(dump) : undefined,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export function inspectReplayFrameWithDescriptor(config, frame, options = {}) {
|
|
64
|
+
const artifacts = createSchemaArtifacts(config.descriptor);
|
|
65
|
+
return inspectReplayFrame({
|
|
66
|
+
replay: config.replay,
|
|
67
|
+
step: config.step,
|
|
68
|
+
defaultInput: config.defaultInput,
|
|
69
|
+
// The content-hashed identity, matching what runtimes stamp into exported
|
|
70
|
+
// replays — same contract as verifyReplayWithDescriptor.
|
|
71
|
+
inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
|
|
72
|
+
stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
|
|
73
|
+
deterministicVersion: artifacts.descriptor.deterministicVersion,
|
|
74
|
+
canonicalizeInput(input) {
|
|
75
|
+
return artifacts.canonicalizeInput(input);
|
|
76
|
+
},
|
|
77
|
+
canonicalizeState(state) {
|
|
78
|
+
return artifacts.canonicalizeState(state);
|
|
79
|
+
},
|
|
80
|
+
checksum(state) {
|
|
81
|
+
return artifacts.checksumState(state);
|
|
82
|
+
},
|
|
83
|
+
serializeState(state) {
|
|
84
|
+
return artifacts.serializeState(state);
|
|
85
|
+
},
|
|
86
|
+
deserializeState(bytes) {
|
|
87
|
+
return artifacts.deserializeState(bytes);
|
|
88
|
+
},
|
|
89
|
+
hydrateInput: config.hydrateInput,
|
|
90
|
+
}, frame, options);
|
|
91
|
+
}
|
|
92
|
+
export function explainReplayChecksum(dump) {
|
|
93
|
+
const state = dump.state;
|
|
94
|
+
const tables = isRecord(state)
|
|
95
|
+
? stableSortedKeys(state)
|
|
96
|
+
.map((key) => explainChecksumTable(`state.${key}`, state[key]))
|
|
97
|
+
: [explainChecksumTable('state', state)];
|
|
98
|
+
return {
|
|
99
|
+
frame: dump.frame,
|
|
100
|
+
checksum: dump.checksum,
|
|
101
|
+
tableCount: tables.length,
|
|
102
|
+
tables,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
export function diffReplayFrames(left, right, maxDifferences = 200) {
|
|
106
|
+
const differences = [];
|
|
107
|
+
collectDiffs('state', left.state, right.state, differences, maxDifferences);
|
|
108
|
+
return {
|
|
109
|
+
frame: left.frame,
|
|
110
|
+
leftChecksum: left.checksum,
|
|
111
|
+
rightChecksum: right.checksum,
|
|
112
|
+
checksumMatch: left.checksum === right.checksum,
|
|
113
|
+
differences,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function stableFrameDumpJson(dump) {
|
|
117
|
+
return `${JSON.stringify(dump)}\n`;
|
|
118
|
+
}
|
|
119
|
+
export function stableFrameDiffJson(diff) {
|
|
120
|
+
return `${JSON.stringify(diff)}\n`;
|
|
121
|
+
}
|
|
122
|
+
function collectDiffs(path, left, right, differences, maxDifferences) {
|
|
123
|
+
if (differences.length >= maxDifferences || stringifyCanonicalForDiff(left) === stringifyCanonicalForDiff(right)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
127
|
+
const length = Math.max(left.length, right.length);
|
|
128
|
+
for (let index = 0; index < length; index += 1) {
|
|
129
|
+
collectDiffs(`${path}[${index}]`, left[index], right[index], differences, maxDifferences);
|
|
130
|
+
if (differences.length >= maxDifferences) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!isRecord(left) || !isRecord(right)) {
|
|
137
|
+
differences.push({ path, left, right });
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const keys = mergeStableKeys(left, right);
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
collectDiffs(`${path}.${key}`, left[key], right[key], differences, maxDifferences);
|
|
143
|
+
if (differences.length >= maxDifferences) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isRecord(value) {
|
|
149
|
+
return value !== undefined && value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
150
|
+
}
|
|
151
|
+
function stringifyCanonicalForDiff(value) {
|
|
152
|
+
return value === undefined ? '<undefined>' : JSON.stringify(value);
|
|
153
|
+
}
|
|
154
|
+
function explainChecksumTable(path, value) {
|
|
155
|
+
const source = JSON.stringify(value);
|
|
156
|
+
return {
|
|
157
|
+
path,
|
|
158
|
+
checksum: defaultChecksum(source),
|
|
159
|
+
bytes: encodeUtf8(source).byteLength,
|
|
160
|
+
entries: countEntries(value),
|
|
161
|
+
sampleKeys: sampleKeys(value),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function countEntries(value) {
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
return value.length;
|
|
167
|
+
}
|
|
168
|
+
if (isRecord(value)) {
|
|
169
|
+
return stableSortedKeys(value).length;
|
|
170
|
+
}
|
|
171
|
+
return 1;
|
|
172
|
+
}
|
|
173
|
+
function sampleKeys(value) {
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return value.slice(0, 5).map((_, index) => `[${index}]`);
|
|
176
|
+
}
|
|
177
|
+
if (isRecord(value)) {
|
|
178
|
+
return stableSortedKeys(value).slice(0, 5);
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
function validateFrame(replay, frame) {
|
|
183
|
+
if (!Number.isInteger(frame) || frame < 0 || frame > replay.inputFrames.length) {
|
|
184
|
+
throw new SyncplayError(`Replay frame ${frame} is outside 0..${replay.inputFrames.length}`, {
|
|
185
|
+
code: 'replay.inspect-frame',
|
|
186
|
+
frame,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function mergeStableKeys(left, right) {
|
|
191
|
+
return [...new Set([...stableSortedKeys(left), ...stableSortedKeys(right)])].sort(compareStrings);
|
|
192
|
+
}
|
|
193
|
+
function compareStrings(left, right) {
|
|
194
|
+
return Number(left > right) - Number(left < right);
|
|
195
|
+
}
|
|
196
|
+
function assertReplayIdentity(config) {
|
|
197
|
+
const checks = [
|
|
198
|
+
['deterministicVersion', config.deterministicVersion, config.replay.deterministicVersion],
|
|
199
|
+
['inputSchemaId', config.inputSchemaId, config.replay.inputSchemaId],
|
|
200
|
+
['stateSchemaId', config.stateSchemaId, config.replay.stateSchemaId],
|
|
201
|
+
['engineIdentityHash', config.engineIdentityHash, config.replay.engineIdentityHash],
|
|
202
|
+
['assetDbHash', config.assetDbHash, config.replay.assetDbHash],
|
|
203
|
+
['configHash', config.configHash, config.replay.configHash],
|
|
204
|
+
];
|
|
205
|
+
for (const [field, actual, expected] of checks) {
|
|
206
|
+
if (actual === undefined || expected === undefined) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (actual !== expected) {
|
|
210
|
+
throw new SyncplayError(`Replay ${field} mismatch: expected ${expected}, actual ${actual}`, {
|
|
211
|
+
code: 'replay.identity-mismatch',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function replayChecksumForFrame(replay, frame) {
|
|
217
|
+
const checksums = replay.checksums.filter((record) => record.frame === frame);
|
|
218
|
+
if (checksums.length > 1) {
|
|
219
|
+
throw new SyncplayError(`Replay frame ${frame} has duplicate checksum records`, {
|
|
220
|
+
code: 'replay.inspect-checksum',
|
|
221
|
+
frame,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return checksums[0]?.checksum;
|
|
225
|
+
}
|
|
226
|
+
function cloneOptional(value) {
|
|
227
|
+
return value === undefined ? undefined : cloneCanonical(value);
|
|
228
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReplayVerificationConfig } from './replay.js';
|
|
2
|
+
/**
|
|
3
|
+
* Replay-verified score submission (M8.5): re-simulate a submitted
|
|
4
|
+
* {@link ReplayFile} and check that the game's own score extraction over the
|
|
5
|
+
* FINAL state matches the score the client claimed. A tampered score fails the
|
|
6
|
+
* extraction check; tampered inputs/state fail the recorded-checksum
|
|
7
|
+
* re-simulation. The verifier needs the game's deterministic config (step,
|
|
8
|
+
* schemas) — hosting that server-side is the submission pipeline's problem;
|
|
9
|
+
* this is the game-agnostic core it calls.
|
|
10
|
+
*/
|
|
11
|
+
export interface ScoredReplayVerificationConfig<State, Input> extends ReplayVerificationConfig<State, Input> {
|
|
12
|
+
readonly submittedScore: number;
|
|
13
|
+
/** Game-supplied: read the canonical score out of the final state. */
|
|
14
|
+
readonly extractScore: (finalState: State) => number;
|
|
15
|
+
}
|
|
16
|
+
export type ScoredReplayRejection = 'replay-mismatch' | 'score-mismatch';
|
|
17
|
+
export interface ScoredReplayVerdict {
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly reason?: ScoredReplayRejection;
|
|
20
|
+
readonly replayScore?: number;
|
|
21
|
+
readonly submittedScore: number;
|
|
22
|
+
readonly frames: number;
|
|
23
|
+
readonly verifiedChecksums: number;
|
|
24
|
+
readonly finalChecksum: string;
|
|
25
|
+
/** First mismatching frame when reason is 'replay-mismatch'. */
|
|
26
|
+
readonly mismatchFrame?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function verifyScoredReplay<State, Input>(config: ScoredReplayVerificationConfig<State, Input>): ScoredReplayVerdict;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { decodeCanonicalValue } from './canonical.js';
|
|
2
|
+
import { createOfflineSession } from './offline-session.js';
|
|
3
|
+
import { verifyReplay } from './replay.js';
|
|
4
|
+
export function verifyScoredReplay(config) {
|
|
5
|
+
const { submittedScore, extractScore, ...verification } = config;
|
|
6
|
+
const result = verifyReplay({ ...verification, throwOnMismatch: false });
|
|
7
|
+
if (result.mismatches > 0) {
|
|
8
|
+
return {
|
|
9
|
+
ok: false,
|
|
10
|
+
reason: 'replay-mismatch',
|
|
11
|
+
submittedScore,
|
|
12
|
+
frames: result.frames,
|
|
13
|
+
verifiedChecksums: result.checksums,
|
|
14
|
+
finalChecksum: result.finalChecksum,
|
|
15
|
+
...(result.firstMismatch === undefined ? {} : { mismatchFrame: result.firstMismatch.frame }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
// The replay re-simulated clean — walk it once more to materialize the final
|
|
19
|
+
// state for score extraction (verifyReplay reports checksums, not state).
|
|
20
|
+
const replay = config.replay;
|
|
21
|
+
const session = createOfflineSession({
|
|
22
|
+
...verification,
|
|
23
|
+
initialState: decodeCanonicalValue(replay.initialState),
|
|
24
|
+
playerCount: replay.playerCount,
|
|
25
|
+
tickRate: replay.tickRate,
|
|
26
|
+
randomSeed: replay.randomSeed,
|
|
27
|
+
});
|
|
28
|
+
const commandFrames = new Map((replay.commandHistory ?? []).map((frame) => [frame.frame, frame.commands]));
|
|
29
|
+
for (const inputFrame of replay.inputFrames) {
|
|
30
|
+
inputFrame.inputs.forEach((input, slot) => {
|
|
31
|
+
const decoded = decodeCanonicalValue(input);
|
|
32
|
+
const flags = inputFrame.inputFlags?.[slot];
|
|
33
|
+
const replayInput = flags?.playerPresent === false ? config.defaultInput : (config.hydrateInput?.(decoded) ?? decoded);
|
|
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) => decodeCanonicalValue(command)));
|
|
42
|
+
}
|
|
43
|
+
session.stepFrames(1);
|
|
44
|
+
}
|
|
45
|
+
const replayScore = extractScore(session.getState());
|
|
46
|
+
if (replayScore !== submittedScore) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
reason: 'score-mismatch',
|
|
50
|
+
replayScore,
|
|
51
|
+
submittedScore,
|
|
52
|
+
frames: result.frames,
|
|
53
|
+
verifiedChecksums: result.checksums,
|
|
54
|
+
finalChecksum: result.finalChecksum,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
replayScore,
|
|
60
|
+
submittedScore,
|
|
61
|
+
frames: result.frames,
|
|
62
|
+
verifiedChecksums: result.checksums,
|
|
63
|
+
finalChecksum: result.finalChecksum,
|
|
64
|
+
};
|
|
65
|
+
}
|
package/dist/replay.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DeterministicGameDescriptor } from './schema.js';
|
|
2
|
+
import type { DeterministicSessionConfig, ReplayFile, ReplayVerificationResult } from './types.js';
|
|
3
|
+
export interface ReplayVerificationConfig<State, Input> extends Omit<DeterministicSessionConfig<State, Input>, 'initialState' | 'playerCount' | 'tickRate' | 'randomSeed'> {
|
|
4
|
+
readonly replay: ReplayFile;
|
|
5
|
+
readonly throwOnMismatch?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface DescriptorReplayVerificationConfig<State, Input> {
|
|
8
|
+
readonly descriptor: DeterministicGameDescriptor;
|
|
9
|
+
readonly replay: ReplayFile;
|
|
10
|
+
readonly step: DeterministicSessionConfig<State, Input>['step'];
|
|
11
|
+
readonly defaultInput: Input;
|
|
12
|
+
readonly hydrateInput?: (recordedInput: unknown) => Input;
|
|
13
|
+
readonly throwOnMismatch?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function verifyReplay<State, Input>(config: ReplayVerificationConfig<State, Input>): ReplayVerificationResult;
|
|
16
|
+
export declare function verifyReplayWithDescriptor<State, Input>(config: DescriptorReplayVerificationConfig<State, Input>): ReplayVerificationResult;
|
|
17
|
+
export declare function assertReplayFileStructure(replay: ReplayFile): void;
|