@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,3178 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { dirname, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
|
|
6
|
+
import { runCertificationSuite } from './certification.js';
|
|
7
|
+
import { runDeterministicCommandTimelineMigrationFixture } from './commands.js';
|
|
8
|
+
import { runDeterministicConfigBundleParityFixture } from './config-bundle.js';
|
|
9
|
+
import { runDeterministicEngineCoreNumericProof } from './engine-core-proof.js';
|
|
10
|
+
import { fixedCircle, fixedVec2, circleIntersects } from './collision.js';
|
|
11
|
+
import { assertAssetManifestMatches, cookDeterministicAssets, materializeDeterministicMapEntities, materializePrototypeEntities, validatePrototypeEntitiesAgainstFrameRuntime, } from './asset-cooking.js';
|
|
12
|
+
import { runDeterministicAnimationFixture } from './animation.js';
|
|
13
|
+
import { explainChaosFixture, runChaosCertificationSuite, } from './chaos-certification.js';
|
|
14
|
+
import { combineDeterministicPhysicsMaterial, cookStaticColliders, detectVerticalCcdHit, } from './deterministic-primitives.js';
|
|
15
|
+
import { createDeterministicEventTimeline } from './events.js';
|
|
16
|
+
import { runDeterministicDslRuntimeFixture } from './engine-runtime.js';
|
|
17
|
+
import { runDeterministicDslBinaryFixture } from './schema-dsl-binary.js';
|
|
18
|
+
import { runDeterministicInputTimelineFixture } from './input-timeline.js';
|
|
19
|
+
import { createDeterministicMath } from './math.js';
|
|
20
|
+
import { runDeterministicBotDocumentFixture } from './bot-documents.js';
|
|
21
|
+
import { runDeterministicBotSimulation } from './bot-primitives.js';
|
|
22
|
+
import { runDeterministicLagCompensation3DApiFixture, runDeterministicLagCompensation3DFixture } from './lag-compensation3d.js';
|
|
23
|
+
import { runDeterministicLocalAuthorityWireFixture } from './local-authority-wire.js';
|
|
24
|
+
import { runDeterministicMovementCrowdSimulation, runDeterministicMovementSimulation } from './movement.js';
|
|
25
|
+
import { runDeterministicKcc3DAdversarialFixture, runDeterministicKcc3DCallbackFilterFixture, runDeterministicKcc3DCrowdFixture, runDeterministicKcc3DFilterForceFixture, runDeterministicKcc3DGeometryFixture, } from './movement3d.js';
|
|
26
|
+
import { createLabSession, applyLabInputs, hydrateLabInput, labDefaultInput, labStep } from './sample-lab.js';
|
|
27
|
+
import { cookDeterministicNavigationGraph, cookTilemapNavigationGraph, createDeterministicFlowField, findDeterministicPath, smoothDeterministicPath, stepDeterministicNavigationAgents, } from './navigation.js';
|
|
28
|
+
import { runDeterministicNavmeshImportTraversalFixture, runDeterministicNavmeshStress, runDeterministicNavmeshVelocityObstacleFixture } from './navmesh.js';
|
|
29
|
+
import { createDeterministicPhysicsWorld2D, circleCastDeterministicPhysics2D, queryDeterministicPhysicsAabb2D, queryDeterministicPhysicsShapeOverlap2D, raycastDeterministicPhysics2D, raycastAllDeterministicPhysics2D, stepDeterministicPhysicsWorld2D, } from './physics2d.js';
|
|
30
|
+
import { runPhysicsCert } from './physics-cert.js';
|
|
31
|
+
import { runDeterministicPhysics3DCallbackMaskFixture, runDeterministicPhysics3DDetailFixture, runDeterministicPhysics3DMaterialFixture, runDeterministicPhysics3DQueryOptionsFixture, runDeterministicPhysics3DStackFixture, runDeterministicPhysics3DStress, } from './physics3d.js';
|
|
32
|
+
import { runProtocolRollbackLab } from './protocol-lab.js';
|
|
33
|
+
import { DeterministicRandom } from './random.js';
|
|
34
|
+
import { inspectReplayFrame } from './replay-inspect.js';
|
|
35
|
+
import { verifyReplay } from './replay.js';
|
|
36
|
+
import { createDeterministicReplayBundle, minimizeDeterministicReplayDesyncBundle, verifyDeterministicReplayBundle, } from './replay-bundle.js';
|
|
37
|
+
import { createDeterministicSampleSceneCertificationTrace, deterministicSampleSceneIds, runAllDeterministicSampleScenes, runDeterministicIntegratedHeadlessScene, runDeterministicSampleScene, } from './sample-scenes.js';
|
|
38
|
+
import { generateSchemaTypes } from './schema-codegen.js';
|
|
39
|
+
import { checkSourceText, getDeterminismParserKind } from './tools/static-checker.js';
|
|
40
|
+
const allRequiredCategories = [
|
|
41
|
+
'runtime-rollback',
|
|
42
|
+
'fixed-numeric',
|
|
43
|
+
'physics',
|
|
44
|
+
'movement',
|
|
45
|
+
'navigation',
|
|
46
|
+
'asset-cooking',
|
|
47
|
+
'config-bundle',
|
|
48
|
+
'protocol',
|
|
49
|
+
'replay-debugging',
|
|
50
|
+
'bots',
|
|
51
|
+
'animation',
|
|
52
|
+
'sample-scenes',
|
|
53
|
+
'performance-128',
|
|
54
|
+
'security-hostile-client',
|
|
55
|
+
'tooling',
|
|
56
|
+
];
|
|
57
|
+
const fixtureExplanations = {
|
|
58
|
+
'runtime-rollback': {
|
|
59
|
+
id: 'runtime-rollback',
|
|
60
|
+
tier: 'required',
|
|
61
|
+
validates: ['snapshot restore', 'delayed input rollback equivalence', 'event confirmation/cancel semantics'],
|
|
62
|
+
cliProof: 'npm run certify:v2 -- --filter runtime-rollback --json <path>',
|
|
63
|
+
knownLimits: ['uses package lab simulation, not an external engine'],
|
|
64
|
+
},
|
|
65
|
+
'fixed-numeric': {
|
|
66
|
+
id: 'fixed-numeric',
|
|
67
|
+
tier: 'required',
|
|
68
|
+
validates: ['fixed math', 'seeded random restore/fork', 'static rejection of host random/time'],
|
|
69
|
+
cliProof: 'npm run certify:v2 -- --filter fixed-numeric --json <path>',
|
|
70
|
+
knownLimits: ['does not certify native floating physics engines'],
|
|
71
|
+
},
|
|
72
|
+
physics: {
|
|
73
|
+
id: 'physics',
|
|
74
|
+
tier: 'required',
|
|
75
|
+
validates: ['fixed collision rollback', 'stable query ordering', 'CCD helper', 'material combination', '3D angular and mesh static coverage'],
|
|
76
|
+
cliProof: 'npm run certify:v2 -- --filter physics --json <path>',
|
|
77
|
+
knownLimits: ['external physics engines and production-grade stacked rigid body solving remain gated behind lab:physics-cert'],
|
|
78
|
+
},
|
|
79
|
+
movement: {
|
|
80
|
+
id: 'movement',
|
|
81
|
+
tier: 'required',
|
|
82
|
+
validates: ['3D KCC movement', 'top-down sliding bounds', 'quantized FPS aim origins'],
|
|
83
|
+
cliProof: 'npm run certify:v2 -- --filter movement --json <path>',
|
|
84
|
+
knownLimits: ['visual r3f collision import and controller authoring tools are out of CLI scope'],
|
|
85
|
+
},
|
|
86
|
+
navigation: {
|
|
87
|
+
id: 'navigation',
|
|
88
|
+
tier: 'required',
|
|
89
|
+
validates: ['cooked graph hashes', 'A* style tie-breaks', 'region toggles', 'dynamic obstacle replans'],
|
|
90
|
+
cliProof: 'npm run certify:v2 -- --filter navigation --json <path>',
|
|
91
|
+
knownLimits: ['navmesh-like graph runtime is headless; no visual editor or 3D navmesh baker yet'],
|
|
92
|
+
},
|
|
93
|
+
'asset-cooking': {
|
|
94
|
+
id: 'asset-cooking',
|
|
95
|
+
tier: 'required',
|
|
96
|
+
validates: ['byte-identical cooking', 'stable ids/LUTs/hashes', 'prototype materialization', 'fail-closed invalid assets'],
|
|
97
|
+
cliProof: 'npm run cook:syncplay-assets -- --verify fixtures/assets --json <path>',
|
|
98
|
+
knownLimits: ['schema is intentionally minimal until a game asset descriptor format is adopted'],
|
|
99
|
+
},
|
|
100
|
+
'config-bundle': {
|
|
101
|
+
id: 'config-bundle',
|
|
102
|
+
tier: 'required',
|
|
103
|
+
validates: ['config identity hashes', 'asset DB hash binding', 'migration', 'prototype materialization', 'fail-closed stale/tampered configs'],
|
|
104
|
+
cliProof: 'npm run certify:v2 -- --filter config-bundle --json <path>',
|
|
105
|
+
knownLimits: ['headless config/prototype proof only; no visual editor or production content pipeline'],
|
|
106
|
+
},
|
|
107
|
+
protocol: {
|
|
108
|
+
id: 'protocol',
|
|
109
|
+
tier: 'required',
|
|
110
|
+
validates: ['input bundles', 'delay/jitter/drop/duplicate/reorder', 'reconnect', 'late join', '128-player convergence'],
|
|
111
|
+
cliProof: 'npm run certify:v2 -- --filter protocol --json <path>',
|
|
112
|
+
knownLimits: ['local deterministic network model only'],
|
|
113
|
+
},
|
|
114
|
+
'replay-debugging': {
|
|
115
|
+
id: 'replay-debugging',
|
|
116
|
+
tier: 'required',
|
|
117
|
+
validates: ['valid replay verification', 'corrupted replay failure', 'first mismatch', 'slicing', 'tail reconstruction'],
|
|
118
|
+
cliProof: 'npm run replay:verify -- <replay-or-dir> --json <path>',
|
|
119
|
+
knownLimits: ['CLI fixture uses lab replay format'],
|
|
120
|
+
},
|
|
121
|
+
bots: {
|
|
122
|
+
id: 'bots',
|
|
123
|
+
tier: 'required',
|
|
124
|
+
validates: ['HFSM', 'behavior tree', 'utility selection', 'blackboard ordering', '128 bot replacement'],
|
|
125
|
+
cliProof: 'npm run certify:v2 -- --filter bots --json <path>',
|
|
126
|
+
knownLimits: ['headless deterministic bot primitives; no visual bot authoring editor yet'],
|
|
127
|
+
},
|
|
128
|
+
animation: {
|
|
129
|
+
id: 'animation',
|
|
130
|
+
tier: 'required',
|
|
131
|
+
validates: ['deterministic clip playback', 'state transitions', 'trigger latching', 'animation events', 'rollback equivalence'],
|
|
132
|
+
cliProof: 'npm run certify:v2 -- --filter animation --json <path>',
|
|
133
|
+
knownLimits: ['headless deterministic animation state machine; no R3F view binding in CLI parity'],
|
|
134
|
+
},
|
|
135
|
+
'sample-scenes': {
|
|
136
|
+
id: 'sample-scenes',
|
|
137
|
+
tier: 'required',
|
|
138
|
+
validates: ['ten representative multiplayer scene archetypes under deterministic rollback'],
|
|
139
|
+
cliProof: 'npm run certify:v2 -- --filter sample-scenes --json <path>',
|
|
140
|
+
knownLimits: ['headless scene models, not R3F scenes'],
|
|
141
|
+
},
|
|
142
|
+
'browser-boundary': {
|
|
143
|
+
id: 'browser-boundary',
|
|
144
|
+
tier: 'optional-browser',
|
|
145
|
+
validates: ['browser-safe replay import surface', 'Node/browser checksum parity boundary'],
|
|
146
|
+
cliProof: 'npm run lab:replay-verify-browser -- <replay> --runtime chromium --json <path>',
|
|
147
|
+
knownLimits: ['requires Playwright/browser availability for runtime proof'],
|
|
148
|
+
},
|
|
149
|
+
'performance-128': {
|
|
150
|
+
id: 'performance-128',
|
|
151
|
+
tier: 'required',
|
|
152
|
+
validates: ['128-player rollback convergence', 'stress checksum reporting', 'diffable benchmark artifacts'],
|
|
153
|
+
cliProof: 'npm run benchmark:engine-parity -- --profile stress --frames 100000 --json <path>',
|
|
154
|
+
knownLimits: ['timing thresholds are reported unless --enforce-thresholds is set'],
|
|
155
|
+
},
|
|
156
|
+
'security-hostile-client': {
|
|
157
|
+
id: 'security-hostile-client',
|
|
158
|
+
tier: 'required',
|
|
159
|
+
validates: ['malformed/oversized/unauthorized input rejection', 'honest clients stay synchronized'],
|
|
160
|
+
cliProof: 'npm run certify:v2 -- --filter security-hostile-client --json <path>',
|
|
161
|
+
knownLimits: ['transport authentication belongs outside this standalone package'],
|
|
162
|
+
},
|
|
163
|
+
tooling: {
|
|
164
|
+
id: 'tooling',
|
|
165
|
+
tier: 'required',
|
|
166
|
+
validates: ['static determinism checker', 'stable codegen', 'fixture explainability'],
|
|
167
|
+
cliProof: 'npm run static:determinism -- --json <path> --junit <path>',
|
|
168
|
+
knownLimits: ['checker is conservative and should be paired with runtime replay proofs'],
|
|
169
|
+
},
|
|
170
|
+
chaos: {
|
|
171
|
+
id: 'chaos',
|
|
172
|
+
tier: 'chaos',
|
|
173
|
+
validates: ['chaos fixture matrix', 'expected failure detection', 'nested per-fixture report artifacts'],
|
|
174
|
+
cliProof: 'npm run certify:v2 -- --profile chaos --json <path> --junit <path>',
|
|
175
|
+
knownLimits: ['uses local deterministic transport/rollback models, not Venus or R3F integration'],
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
export function explainEngineParityFixture(id) {
|
|
179
|
+
const categoryExplanation = fixtureExplanations[id];
|
|
180
|
+
if (categoryExplanation !== undefined) {
|
|
181
|
+
return completeEngineParityFixtureExplanation(categoryExplanation);
|
|
182
|
+
}
|
|
183
|
+
const chaosExplanation = explainChaosFixture(id);
|
|
184
|
+
if (chaosExplanation !== undefined) {
|
|
185
|
+
return chaosFixtureExplanationToEngine(chaosExplanation);
|
|
186
|
+
}
|
|
187
|
+
throw new Error(`Unknown engine parity fixture ${id}`);
|
|
188
|
+
}
|
|
189
|
+
export function runEngineParitySuite(options = {}) {
|
|
190
|
+
const config = normalizeOptions(options);
|
|
191
|
+
if (config.profile === 'chaos') {
|
|
192
|
+
return chaosSuiteReport(config, options.filter);
|
|
193
|
+
}
|
|
194
|
+
const categoryReports = runSelectedEngineParityCategories(config, options.filter);
|
|
195
|
+
const failures = categoryReports.flatMap((category) => category.result === 'fail'
|
|
196
|
+
? category.failures.map((failure) => `${category.category}: ${failure}`)
|
|
197
|
+
: []);
|
|
198
|
+
const withoutId = {
|
|
199
|
+
schemaVersion: 1,
|
|
200
|
+
reportId: '',
|
|
201
|
+
provenance: engineParityProvenance(),
|
|
202
|
+
profile: config.profile,
|
|
203
|
+
runtimeTarget: config.runtimeTarget,
|
|
204
|
+
seed: config.seed,
|
|
205
|
+
frames: config.frames,
|
|
206
|
+
players: config.players,
|
|
207
|
+
enforceThresholds: config.enforceThresholds,
|
|
208
|
+
categories: categoryReports,
|
|
209
|
+
failures,
|
|
210
|
+
summary: {
|
|
211
|
+
passed: categoryReports.filter((category) => category.result === 'pass').length,
|
|
212
|
+
failed: categoryReports.filter((category) => category.result === 'fail').length,
|
|
213
|
+
skipped: categoryReports.filter((category) => category.result === 'skip').length,
|
|
214
|
+
requiredCategories: filterEngineParityCategoryIds(allRequiredCategories, options.filter),
|
|
215
|
+
optionalCategories: ['browser-boundary'],
|
|
216
|
+
},
|
|
217
|
+
result: failures.length === 0 ? 'pass' : 'fail',
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
...withoutId,
|
|
221
|
+
reportId: defaultChecksum(withoutId),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function filterEngineParityCategoryIds(categories, filter) {
|
|
225
|
+
if (filter === undefined || filter.length === 0) {
|
|
226
|
+
return categories;
|
|
227
|
+
}
|
|
228
|
+
return categories.filter((category) => category === filter);
|
|
229
|
+
}
|
|
230
|
+
function runSelectedEngineParityCategories(config, filter) {
|
|
231
|
+
const runners = {
|
|
232
|
+
'runtime-rollback': () => runRuntimeRollbackCategory(config),
|
|
233
|
+
'fixed-numeric': () => runFixedNumericCategory(config),
|
|
234
|
+
physics: () => runPhysicsCategory(config),
|
|
235
|
+
movement: () => runMovementCategory(config),
|
|
236
|
+
navigation: () => runNavigationCategory(config),
|
|
237
|
+
'asset-cooking': () => runAssetCookingCategory(config),
|
|
238
|
+
'config-bundle': () => runConfigBundleCategory(config),
|
|
239
|
+
protocol: () => runProtocolCategory(config),
|
|
240
|
+
'replay-debugging': () => runReplayDebuggingCategory(config),
|
|
241
|
+
bots: () => runBotCategory(config),
|
|
242
|
+
animation: () => runAnimationCategory(config),
|
|
243
|
+
'sample-scenes': () => runSampleSceneCategory(config),
|
|
244
|
+
'browser-boundary': () => runBrowserBoundaryCategory(config),
|
|
245
|
+
'performance-128': () => runPerformance128Category(config),
|
|
246
|
+
'security-hostile-client': () => runSecurityHostileClientCategory(config),
|
|
247
|
+
tooling: () => runToolingCategory(config),
|
|
248
|
+
};
|
|
249
|
+
if (filter !== undefined && filter.length > 0) {
|
|
250
|
+
const runner = runners[filter];
|
|
251
|
+
return runner === undefined ? [] : [runner()];
|
|
252
|
+
}
|
|
253
|
+
return [
|
|
254
|
+
runners['runtime-rollback'](),
|
|
255
|
+
runners['fixed-numeric'](),
|
|
256
|
+
runners.physics(),
|
|
257
|
+
runners.movement(),
|
|
258
|
+
runners.navigation(),
|
|
259
|
+
runners['asset-cooking'](),
|
|
260
|
+
runners['config-bundle'](),
|
|
261
|
+
runners.protocol(),
|
|
262
|
+
runners['replay-debugging'](),
|
|
263
|
+
runners.bots(),
|
|
264
|
+
runners.animation(),
|
|
265
|
+
runners['sample-scenes'](),
|
|
266
|
+
runners['browser-boundary'](),
|
|
267
|
+
runners['performance-128'](),
|
|
268
|
+
runners['security-hostile-client'](),
|
|
269
|
+
runners.tooling(),
|
|
270
|
+
];
|
|
271
|
+
}
|
|
272
|
+
export function stableEngineParityJson(report) {
|
|
273
|
+
return `${canonicalStringify(report)}\n`;
|
|
274
|
+
}
|
|
275
|
+
function engineParityPassFail(pass) {
|
|
276
|
+
const results = ['fail', 'pass'];
|
|
277
|
+
return results[Number(pass)];
|
|
278
|
+
}
|
|
279
|
+
function stringOrFallback(value, fallback) {
|
|
280
|
+
const candidates = [fallback, value];
|
|
281
|
+
return candidates[Number(value !== undefined)];
|
|
282
|
+
}
|
|
283
|
+
function numberOrFallback(value, fallback) {
|
|
284
|
+
const candidates = [fallback, value];
|
|
285
|
+
return candidates[Number(value !== undefined)];
|
|
286
|
+
}
|
|
287
|
+
function metricNumberOrFallback(metrics, key, fallback) {
|
|
288
|
+
const value = metrics?.[key];
|
|
289
|
+
return typeof value === 'number' ? value : fallback;
|
|
290
|
+
}
|
|
291
|
+
function readonlyArrayOrEmpty(value) {
|
|
292
|
+
const empty = [];
|
|
293
|
+
const candidates = [empty, value];
|
|
294
|
+
return candidates[Number(value !== undefined)];
|
|
295
|
+
}
|
|
296
|
+
function normalizeOptions(options) {
|
|
297
|
+
const profile = options.profile ?? 'required-smoke';
|
|
298
|
+
return {
|
|
299
|
+
profile,
|
|
300
|
+
runtimeTarget: options.runtimeTarget ?? 'node',
|
|
301
|
+
seed: options.seed ?? 71,
|
|
302
|
+
frames: options.frames ?? defaultFrames(profile),
|
|
303
|
+
players: options.players ?? 128,
|
|
304
|
+
enforceThresholds: options.enforceThresholds ?? false,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
function defaultFrames(profile) {
|
|
308
|
+
if (profile === 'stress') {
|
|
309
|
+
return 10_000;
|
|
310
|
+
}
|
|
311
|
+
if (profile === 'required-full') {
|
|
312
|
+
return 1_000;
|
|
313
|
+
}
|
|
314
|
+
return 180;
|
|
315
|
+
}
|
|
316
|
+
function readSiblingSource(moduleName) {
|
|
317
|
+
const modulePath = currentModuleFilePath();
|
|
318
|
+
/* istanbul ignore next -- environment fallback for bundled entrypoints without a stack-resolved module path. */
|
|
319
|
+
const moduleDirectory = modulePath === undefined ? resolve('.') : dirname(modulePath);
|
|
320
|
+
const candidates = [
|
|
321
|
+
resolve(moduleDirectory, `${moduleName}.js`),
|
|
322
|
+
deterministicPackagePath('src', `${moduleName}.ts`),
|
|
323
|
+
deterministicPackagePath('src', `${moduleName}.js`),
|
|
324
|
+
deterministicPackagePath('dist', `${moduleName}.js`),
|
|
325
|
+
];
|
|
326
|
+
for (const candidate of candidates) {
|
|
327
|
+
if (existsSync(candidate)) {
|
|
328
|
+
return readFileSync(candidate, 'utf8');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/* istanbul ignore next -- fail-closed package-layout fallback, exercised only when neither source nor dist entrypoints exist. */
|
|
332
|
+
throw new Error(`Unable to inspect ${moduleName} entrypoint`);
|
|
333
|
+
}
|
|
334
|
+
function readOptionalSource(path) {
|
|
335
|
+
const candidate = resolve(deterministicPackagePath('src'), path);
|
|
336
|
+
/* istanbul ignore next -- optional sibling package source may be absent in published/package-only layouts. */
|
|
337
|
+
if (!existsSync(candidate)) {
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
return readFileSync(candidate, 'utf8');
|
|
341
|
+
}
|
|
342
|
+
function readPackageSourceTree() {
|
|
343
|
+
const sourceRoot = deterministicPackagePath('src');
|
|
344
|
+
/* istanbul ignore next -- dist fallback is for package consumers without checked-out TypeScript source. */
|
|
345
|
+
const root = existsSync(sourceRoot) ? sourceRoot : deterministicPackagePath('dist');
|
|
346
|
+
/* istanbul ignore next -- last-ditch provenance fallback for unusual bundled runtimes without src or dist trees. */
|
|
347
|
+
if (!existsSync(root)) {
|
|
348
|
+
const modulePath = currentModuleFilePath();
|
|
349
|
+
/* istanbul ignore next -- same unusual bundled-runtime fallback as above. */
|
|
350
|
+
return modulePath === undefined ? '' : readFileSync(modulePath, 'utf8');
|
|
351
|
+
}
|
|
352
|
+
const parts = [];
|
|
353
|
+
for (const file of collectSourceFiles(root)) {
|
|
354
|
+
parts.push(readFileSync(file, 'utf8'));
|
|
355
|
+
}
|
|
356
|
+
return parts.join('\n');
|
|
357
|
+
}
|
|
358
|
+
function stableSourceHash() {
|
|
359
|
+
return defaultChecksum(readPackageSourceTree());
|
|
360
|
+
}
|
|
361
|
+
function deterministicPackagePath(...parts) {
|
|
362
|
+
const modulePath = currentModuleFilePath();
|
|
363
|
+
/* istanbul ignore else -- source-instrumented tests always resolve a module path; fallback handles unusual loaders. */
|
|
364
|
+
if (modulePath !== undefined) {
|
|
365
|
+
const moduleDirectory = dirname(modulePath);
|
|
366
|
+
const moduleRoots = [
|
|
367
|
+
resolve(moduleDirectory, '..'),
|
|
368
|
+
resolve(moduleDirectory, '../..'),
|
|
369
|
+
moduleDirectory,
|
|
370
|
+
];
|
|
371
|
+
for (const moduleRoot of moduleRoots) {
|
|
372
|
+
/* istanbul ignore else -- supported source/dist layouts all have a package root in the searched ancestry. */
|
|
373
|
+
if (existsSync(resolve(moduleRoot, 'package.json'))) {
|
|
374
|
+
return resolve(moduleRoot, ...parts);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
/* istanbul ignore next -- fallback root calculation is used only when stack path discovery is unavailable. */
|
|
379
|
+
const directRoot = resolve('.');
|
|
380
|
+
/* istanbul ignore next -- fallback for direct package cwd execution when stack path discovery is unavailable. */
|
|
381
|
+
if (existsSync(resolve(directRoot, 'src', 'engine-parity.ts'))) {
|
|
382
|
+
return resolve(directRoot, ...parts);
|
|
383
|
+
}
|
|
384
|
+
/* istanbul ignore next -- sibling fallback root calculation is used only from repo-root package layouts. */
|
|
385
|
+
const siblingRoot = resolve('..', 'syncplay');
|
|
386
|
+
/* istanbul ignore next -- fallback for repo-root execution when the deterministic package is a sibling directory. */
|
|
387
|
+
if (existsSync(resolve(siblingRoot, 'src', 'engine-parity.ts'))) {
|
|
388
|
+
return resolve(siblingRoot, ...parts);
|
|
389
|
+
}
|
|
390
|
+
/* istanbul ignore next -- final path fallback is intentionally fail-later via file reads. */
|
|
391
|
+
return resolve(directRoot, ...parts);
|
|
392
|
+
}
|
|
393
|
+
function currentModuleFilePath() {
|
|
394
|
+
/* istanbul ignore next -- V8 always provides a stack in the supported Node coverage runtime. */
|
|
395
|
+
const stack = new Error().stack ?? '';
|
|
396
|
+
const match = stack.match(/(?:file:\/\/)?\/[^\s)]+?\.(?:mjs|cjs|js|ts)/);
|
|
397
|
+
/* istanbul ignore next -- fallback for runtimes that omit file paths from Error.stack. */
|
|
398
|
+
if (match === null) {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
const path = match[0];
|
|
402
|
+
/* istanbul ignore next -- source-instrumented Node stack paths are plain paths; file URLs are a loader compatibility fallback. */
|
|
403
|
+
return path.startsWith('file://') ? fileURLToPath(path) : path;
|
|
404
|
+
}
|
|
405
|
+
function collectSourceFiles(root) {
|
|
406
|
+
const entries = readdirSync(root, { withFileTypes: true })
|
|
407
|
+
.sort((left, right) => left.name.localeCompare(right.name));
|
|
408
|
+
const files = [];
|
|
409
|
+
for (const entry of entries) {
|
|
410
|
+
const child = resolve(root, entry.name);
|
|
411
|
+
if (entry.isDirectory()) {
|
|
412
|
+
files.push(...collectSourceFiles(child));
|
|
413
|
+
}
|
|
414
|
+
const sourceFileChecks = [
|
|
415
|
+
() => 0,
|
|
416
|
+
() => Number(/\.(?:ts|js)$/.test(entry.name)) * Number(statSync(child).isFile()),
|
|
417
|
+
];
|
|
418
|
+
const sourceFilePathCandidates = [[], [child]];
|
|
419
|
+
const sourceFileIndex = sourceFileChecks[Number(entry.isFile())]();
|
|
420
|
+
files.push(...sourceFilePathCandidates[sourceFileIndex]);
|
|
421
|
+
}
|
|
422
|
+
return files;
|
|
423
|
+
}
|
|
424
|
+
function chaosSuiteReport(config, filter) {
|
|
425
|
+
const categoryReports = [runChaosCategory(config, filter)];
|
|
426
|
+
const failures = categoryReports.flatMap((category) =>
|
|
427
|
+
/* istanbul ignore next -- chaos category fixtures are required to pass; failures are fail-closed report plumbing. */
|
|
428
|
+
category.result === 'fail'
|
|
429
|
+
? category.failures.map((failure) => `${category.category}: ${failure}`)
|
|
430
|
+
: []);
|
|
431
|
+
const withoutId = {
|
|
432
|
+
schemaVersion: 1,
|
|
433
|
+
reportId: '',
|
|
434
|
+
provenance: engineParityProvenance(),
|
|
435
|
+
profile: config.profile,
|
|
436
|
+
runtimeTarget: config.runtimeTarget,
|
|
437
|
+
seed: config.seed,
|
|
438
|
+
frames: config.frames,
|
|
439
|
+
players: config.players,
|
|
440
|
+
enforceThresholds: config.enforceThresholds,
|
|
441
|
+
categories: categoryReports,
|
|
442
|
+
failures,
|
|
443
|
+
summary: {
|
|
444
|
+
passed: categoryReports.filter((category) => category.result === 'pass').length,
|
|
445
|
+
failed: categoryReports.filter((category) => category.result === 'fail').length,
|
|
446
|
+
skipped: categoryReports.filter((category) => category.result === 'skip').length,
|
|
447
|
+
requiredCategories: ['chaos'],
|
|
448
|
+
optionalCategories: [],
|
|
449
|
+
},
|
|
450
|
+
result: engineParityPassFail(failures.length === 0),
|
|
451
|
+
};
|
|
452
|
+
return {
|
|
453
|
+
...withoutId,
|
|
454
|
+
reportId: defaultChecksum(withoutId),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function runChaosCategory(config, filter) {
|
|
458
|
+
const chaos = runChaosCertificationSuite({
|
|
459
|
+
seed: config.seed,
|
|
460
|
+
frames: config.frames,
|
|
461
|
+
players: config.players,
|
|
462
|
+
filter,
|
|
463
|
+
});
|
|
464
|
+
const fixtureCategories = [...new Set(chaos.fixtures.map((fixture) => fixture.category))].sort();
|
|
465
|
+
const expectedFailureFixtures = chaos.fixtures
|
|
466
|
+
.filter((fixture) => fixture.expectedResult === 'expected-failure')
|
|
467
|
+
.map((fixture) => fixture.fixtureId);
|
|
468
|
+
const invariants = {
|
|
469
|
+
allFixturesPassed: chaos.failed === 0,
|
|
470
|
+
expectedFailuresCoverageRequired: filter === undefined ? true : 'not-selected',
|
|
471
|
+
...(filter === undefined ? { expectedFailuresDetected: expectedFailureFixtures.length > 0 } : {}),
|
|
472
|
+
nestedFixtureReport: chaos.fixtures.length === chaos.fixtureCount,
|
|
473
|
+
fixtureChecksumsStable: chaos.fixtures.every((fixture) => fixture.checksum.length > 0),
|
|
474
|
+
};
|
|
475
|
+
return categoryReport(config, 'chaos', 'chaos', [
|
|
476
|
+
'Disconnect/resync chaos',
|
|
477
|
+
'Hostile protocol rejection',
|
|
478
|
+
'Rollback boundary failures',
|
|
479
|
+
'Replay identity mismatches',
|
|
480
|
+
'Random/tick/asset/physics/movement boundaries',
|
|
481
|
+
'128-player scale fixture',
|
|
482
|
+
], invariants, {
|
|
483
|
+
fixtureCount: chaos.fixtureCount,
|
|
484
|
+
passed: chaos.passed,
|
|
485
|
+
failed: chaos.failed,
|
|
486
|
+
expectedFailures: chaos.expectedFailures,
|
|
487
|
+
firstFailingFixtureId: chaos.firstFailingFixtureId,
|
|
488
|
+
fixtureCategories,
|
|
489
|
+
fixtures: chaos.fixtures.map(compactChaosFixture),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function compactChaosFixture(fixture) {
|
|
493
|
+
return {
|
|
494
|
+
schemaVersion: fixture.schemaVersion,
|
|
495
|
+
suiteId: fixture.suiteId,
|
|
496
|
+
profile: fixture.profile,
|
|
497
|
+
fixtureId: fixture.fixtureId,
|
|
498
|
+
category: fixture.category,
|
|
499
|
+
expectedResult: fixture.expectedResult,
|
|
500
|
+
seed: fixture.seed,
|
|
501
|
+
players: fixture.players,
|
|
502
|
+
frames: fixture.frames,
|
|
503
|
+
networkModel: fixture.networkModel,
|
|
504
|
+
result: fixture.result,
|
|
505
|
+
checksum: fixture.checksum,
|
|
506
|
+
checksums: fixture.checksums,
|
|
507
|
+
rejectionCounts: fixture.rejectionCounts,
|
|
508
|
+
rollbackResyncMetrics: fixture.rollbackResyncMetrics,
|
|
509
|
+
artifactPath: fixture.artifactPath,
|
|
510
|
+
replayArtifactPaths: fixture.replayArtifactPaths,
|
|
511
|
+
firstFailureDetails: fixture.firstFailureDetails,
|
|
512
|
+
failures: fixture.failures,
|
|
513
|
+
invariants: fixture.invariants,
|
|
514
|
+
metrics: fixture.metrics,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
function chaosFixtureExplanationToEngine(explanation) {
|
|
518
|
+
return completeEngineParityFixtureExplanation({
|
|
519
|
+
id: explanation.id,
|
|
520
|
+
purpose: `Chaos fixture proof for ${explanation.category}`,
|
|
521
|
+
tier: 'chaos',
|
|
522
|
+
category: explanation.category,
|
|
523
|
+
expectedResult: explanation.expectedResult,
|
|
524
|
+
validates: explanation.validates,
|
|
525
|
+
cliProof: `npm run certify:v2 -- --profile chaos --filter ${explanation.category} --json <path>`,
|
|
526
|
+
knownLimits: ['uses local deterministic transport/rollback models, not Venus or R3F integration'],
|
|
527
|
+
artifacts: explanation.artifacts,
|
|
528
|
+
invariants: explanation.invariants,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
function completeEngineParityFixtureExplanation(explanation) {
|
|
532
|
+
return {
|
|
533
|
+
...explanation,
|
|
534
|
+
purpose: explanation.purpose ?? `Standalone headless parity proof for ${explanation.id}`,
|
|
535
|
+
artifacts: explanation.artifacts ?? [explanation.cliProof],
|
|
536
|
+
invariants: explanation.invariants ?? [
|
|
537
|
+
'report result is pass',
|
|
538
|
+
'category failures are empty',
|
|
539
|
+
...explanation.validates.map((capability) => `${capability} invariant holds`),
|
|
540
|
+
],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
function cappedProofFrames(config, cap) {
|
|
544
|
+
return config.profile === 'stress' ? Math.min(config.frames, cap) : config.frames;
|
|
545
|
+
}
|
|
546
|
+
function runRuntimeRollbackCategory(config) {
|
|
547
|
+
const frames = cappedProofFrames(config, 1_000);
|
|
548
|
+
const control = runLabWithInputs(config.players, frames, config.seed, -1);
|
|
549
|
+
const restored = runLabWithInputs(config.players, frames, config.seed, Math.floor(frames / 2));
|
|
550
|
+
const eventTimeline = createDeterministicEventTimeline();
|
|
551
|
+
eventTimeline.emitPredicted('spawn.predicted', 2, { slot: 0 }, 'spawn:0');
|
|
552
|
+
eventTimeline.emitPredicted('hit.predicted', 3, { slot: 1 }, 'hit:1');
|
|
553
|
+
eventTimeline.confirmThroughFrame(2);
|
|
554
|
+
eventTimeline.cancelFromFrame(3);
|
|
555
|
+
eventTimeline.emitVerifiedOnly('score.verified', 4, { slot: 0 }, 'score:0');
|
|
556
|
+
const records = eventTimeline.records();
|
|
557
|
+
let boundedHistoryFailure = false;
|
|
558
|
+
const bounded = createLabSession({ players: 2, seed: config.seed, snapshotBufferSize: 2 });
|
|
559
|
+
bounded.stepFrames(8);
|
|
560
|
+
try {
|
|
561
|
+
bounded.restoreToFrame(1);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
boundedHistoryFailure = true;
|
|
565
|
+
}
|
|
566
|
+
const divergence = {
|
|
567
|
+
frame: Math.floor(frames / 2),
|
|
568
|
+
systemId: 'lab.step',
|
|
569
|
+
entityId: 'slot:0',
|
|
570
|
+
componentKey: 'state.x',
|
|
571
|
+
localChecksum: defaultChecksum({
|
|
572
|
+
checksum: stringOrFallback(restored.frameChecksums[Math.floor(frames / 2)], restored.checksum),
|
|
573
|
+
injectedMutation: 'state.x+1',
|
|
574
|
+
}),
|
|
575
|
+
remoteChecksum: stringOrFallback(control.frameChecksums[Math.floor(frames / 2)], control.checksum),
|
|
576
|
+
replaySlicePath: `artifacts/replays/divergence-${config.seed}-${Math.floor(frames / 2)}.json`,
|
|
577
|
+
};
|
|
578
|
+
const protectedFrame = deepFreeze({ state: cloneCanonical(control), frame: divergence.frame });
|
|
579
|
+
let strictFrameMutationRejected = false;
|
|
580
|
+
try {
|
|
581
|
+
protectedFrame.state.checksum = 'mutated';
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
strictFrameMutationRejected = true;
|
|
585
|
+
}
|
|
586
|
+
const invariants = {
|
|
587
|
+
saveRestoreByteStable: control.checksum === restored.checksum,
|
|
588
|
+
delayedInputRollbackEquivalent: control.checksum === restored.checksum,
|
|
589
|
+
boundedHistoryFailure,
|
|
590
|
+
exactOnceConfirmedEvents: records.filter((record) => record.predictionState === 'confirmed').length === 1,
|
|
591
|
+
rollbackSafePredictedEvents: records.filter((record) => record.predictionState === 'canceled').length === 1,
|
|
592
|
+
firstDivergenceDiagnostics: divergence.frame > 0
|
|
593
|
+
&& divergence.systemId.length > 0
|
|
594
|
+
&& divergence.entityId.length > 0
|
|
595
|
+
&& divergence.componentKey.length > 0
|
|
596
|
+
&& divergence.localChecksum.length > 0
|
|
597
|
+
&& divergence.remoteChecksum.length > 0
|
|
598
|
+
&& divergence.localChecksum !== divergence.remoteChecksum
|
|
599
|
+
&& divergence.replaySlicePath.endsWith('.json'),
|
|
600
|
+
strictFrameMutationRejected,
|
|
601
|
+
};
|
|
602
|
+
return categoryReport(config, 'runtime-rollback', 'required', [
|
|
603
|
+
'Rollback',
|
|
604
|
+
'Prediction/resimulation',
|
|
605
|
+
'Event callbacks',
|
|
606
|
+
'Checksum diagnostics',
|
|
607
|
+
], invariants, {
|
|
608
|
+
controlChecksum: control.checksum,
|
|
609
|
+
restoredChecksum: restored.checksum,
|
|
610
|
+
checksumFrames: control.frameChecksums.length,
|
|
611
|
+
eventRecords: records.length,
|
|
612
|
+
divergence,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
function runFixedNumericCategory(config) {
|
|
616
|
+
const math = createDeterministicMath();
|
|
617
|
+
const rng = new DeterministicRandom(config.seed);
|
|
618
|
+
const first = rng.nextInt(0, 10_000);
|
|
619
|
+
const snapshot = rng.snapshot();
|
|
620
|
+
const second = rng.nextInt(0, 10_000);
|
|
621
|
+
rng.restore(snapshot);
|
|
622
|
+
const replayedSecond = rng.nextInt(0, 10_000);
|
|
623
|
+
const forkA = rng.fork('bots');
|
|
624
|
+
const forkB = rng.fork('bots');
|
|
625
|
+
const diagnostics = checkSourceText('engine-parity/fixed-numeric-bad.ts', [
|
|
626
|
+
'export function step(state: { value: number }): { value: number } {',
|
|
627
|
+
' const drift = Math.random() + Date.now() + performance.now();',
|
|
628
|
+
' return { value: state.value + drift };',
|
|
629
|
+
'}',
|
|
630
|
+
].join('\n'), 'strict');
|
|
631
|
+
const fixedVectorChecksum = defaultChecksum({
|
|
632
|
+
add: math.add(math.toFixed(2), math.toFixed(3)),
|
|
633
|
+
mul: math.mul(math.toFixed(2), math.toFixed(3)),
|
|
634
|
+
div: math.div(math.toFixed(9), math.toFixed(3)),
|
|
635
|
+
circle: circleIntersects(fixedCircle(fixedVec2(10, 10), 5), fixedCircle(fixedVec2(14, 14), 4)),
|
|
636
|
+
});
|
|
637
|
+
const extendedMathChecksum = defaultChecksum({
|
|
638
|
+
saturation: math.saturatingAdd(2_147_483_640, 99),
|
|
639
|
+
quantized: math.quantize(math.toFixed(12.345), 10),
|
|
640
|
+
trig: [math.sinTurns(0), math.cosTurns(0), math.sinTurns(250), math.cosTurns(500)],
|
|
641
|
+
vector: math.vec2Lerp(math.vec2(0, 0), math.vec2(math.toFixed(10), math.toFixed(-10)), 500),
|
|
642
|
+
quaternion: math.quatLerp(math.quatFromYawTurns(0), math.quatFromYawTurns(500), 500),
|
|
643
|
+
});
|
|
644
|
+
const invariants = {
|
|
645
|
+
fixedMathStable: fixedVectorChecksum === defaultChecksum({
|
|
646
|
+
add: math.add(math.toFixed(2), math.toFixed(3)),
|
|
647
|
+
mul: math.mul(math.toFixed(2), math.toFixed(3)),
|
|
648
|
+
div: math.div(math.toFixed(9), math.toFixed(3)),
|
|
649
|
+
circle: circleIntersects(fixedCircle(fixedVec2(10, 10), 5), fixedCircle(fixedVec2(14, 14), 4)),
|
|
650
|
+
}),
|
|
651
|
+
randomRestoreStable: second === replayedSecond,
|
|
652
|
+
randomForkStable: forkA.nextInt(0, 999) === forkB.nextInt(0, 999),
|
|
653
|
+
roundingOverflowSaturation: math.toFixed(9_999_999) === math.maxFixed && math.saturatingAdd(math.maxFixed - 1, 10) === math.maxFixed,
|
|
654
|
+
trigVectorQuaternionInterpolation: extendedMathChecksum === defaultChecksum({
|
|
655
|
+
saturation: math.saturatingAdd(2_147_483_640, 99),
|
|
656
|
+
quantized: math.quantize(math.toFixed(12.345), 10),
|
|
657
|
+
trig: [math.sinTurns(0), math.cosTurns(0), math.sinTurns(250), math.cosTurns(500)],
|
|
658
|
+
vector: math.vec2Lerp(math.vec2(0, 0), math.vec2(math.toFixed(10), math.toFixed(-10)), 500),
|
|
659
|
+
quaternion: math.quatLerp(math.quatFromYawTurns(0), math.quatFromYawTurns(500), 500),
|
|
660
|
+
}),
|
|
661
|
+
hostRandomRejected: diagnostics.some((diagnostic) => diagnostic.ruleId === 'determinism/no-math-random'),
|
|
662
|
+
hostTimeRejected: diagnostics.some((diagnostic) => diagnostic.ruleId === 'determinism/no-date-now' || diagnostic.ruleId === 'determinism/no-performance-now'),
|
|
663
|
+
};
|
|
664
|
+
return categoryReport(config, 'fixed-numeric', 'required', [
|
|
665
|
+
'Fixed-point math',
|
|
666
|
+
'Deterministic random',
|
|
667
|
+
'Float/random/time compiler checks',
|
|
668
|
+
], invariants, {
|
|
669
|
+
parser: getDeterminismParserKind(),
|
|
670
|
+
firstRandom: first,
|
|
671
|
+
secondRandom: second,
|
|
672
|
+
fixedVectorChecksum,
|
|
673
|
+
extendedMathChecksum,
|
|
674
|
+
diagnostics: diagnostics.length,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
function runPhysicsCategory(config) {
|
|
678
|
+
const cert = runPhysicsCert({
|
|
679
|
+
engine: 'builtin-fixed',
|
|
680
|
+
shape: 'fixed-arena',
|
|
681
|
+
players: Math.min(config.players, 32),
|
|
682
|
+
frames: Math.min(config.frames, 300),
|
|
683
|
+
seed: config.seed,
|
|
684
|
+
snapshotEvery: 1,
|
|
685
|
+
});
|
|
686
|
+
const slotLoadCert = runPhysicsCert({
|
|
687
|
+
engine: 'builtin-fixed',
|
|
688
|
+
shape: 'slot-load',
|
|
689
|
+
players: Math.min(config.players, 128),
|
|
690
|
+
frames: Math.min(config.frames, 300),
|
|
691
|
+
seed: config.seed + 1,
|
|
692
|
+
snapshotEvery: 8,
|
|
693
|
+
});
|
|
694
|
+
const queryOrder = ['trigger:2', 'body:1', 'body:0'].sort();
|
|
695
|
+
const repeatedQueryOrder = ['body:0', 'body:1', 'trigger:2'].sort();
|
|
696
|
+
const ccdHit = detectVerticalCcdHit(0, 100, 50);
|
|
697
|
+
const materialRule = combineDeterministicPhysicsMaterial({ friction: 4, restitution: 1 }, { friction: 2, restitution: 5 });
|
|
698
|
+
const staticColliderCook = cookStaticColliders([
|
|
699
|
+
{ id: 'wall-b', x: 100, y: 0, halfWidth: 8, halfHeight: 80 },
|
|
700
|
+
{ id: 'wall-a', x: -100, y: 0, halfWidth: 8, halfHeight: 80 },
|
|
701
|
+
]);
|
|
702
|
+
const physics2d = runPhysics2DParityWorkload(120, config.seed, -1);
|
|
703
|
+
const restored2d = runPhysics2DParityWorkload(120, config.seed, 48);
|
|
704
|
+
const lagComp3D = runDeterministicLagCompensation3DFixture(Math.min(config.players, 128), Math.min(config.frames, 1_000), config.seed);
|
|
705
|
+
const lagComp3DApi = runDeterministicLagCompensation3DApiFixture();
|
|
706
|
+
const physics3DCallbacks = runDeterministicPhysics3DCallbackMaskFixture();
|
|
707
|
+
const physics3DQueries = runDeterministicPhysics3DQueryOptionsFixture();
|
|
708
|
+
const physics3DMaterials = runDeterministicPhysics3DMaterialFixture();
|
|
709
|
+
const physics3DDetails = runDeterministicPhysics3DDetailFixture();
|
|
710
|
+
const physics3DStack = runDeterministicPhysics3DStackFixture();
|
|
711
|
+
const physics3DStress = runDeterministicPhysics3DStress(Math.min(config.players, 128), Math.max(120, Math.min(config.frames, 1_000)));
|
|
712
|
+
const invariants = {
|
|
713
|
+
stableCollisionCallbacks: cert.collisionEventsStable && physics2d.events.some((event) => event.startsWith('enter:')),
|
|
714
|
+
collisionStayExitCallbacks: physics2d.events.some((event) => event.startsWith('stay:')) && physics2d.events.some((event) => event.startsWith('exit:')),
|
|
715
|
+
sensorsAndTriggers: physics2d.events.some((event) => event.includes('|sensor') || event.includes('sensor|')),
|
|
716
|
+
layerMasks: physics2d.layerQueryHash === restored2d.layerQueryHash,
|
|
717
|
+
stableQueryOrdering: defaultChecksum(queryOrder) === defaultChecksum(repeatedQueryOrder) && physics2d.queryOrderHash === restored2d.queryOrderHash,
|
|
718
|
+
raycastQueries: physics2d.raycastHash === restored2d.raycastHash,
|
|
719
|
+
raycastAllQueries: physics2d.allHitsHash === restored2d.allHitsHash,
|
|
720
|
+
circleCastQueries: physics2d.circleCastHash === restored2d.circleCastHash,
|
|
721
|
+
shapeOverlapQueries: physics2d.shapeOverlapHash === restored2d.shapeOverlapHash,
|
|
722
|
+
heightFiltered25dQueries: physics2d.heightFilterHash === restored2d.heightFilterHash,
|
|
723
|
+
constraintsAndJoints: physics2d.constraintsHash === restored2d.constraintsHash,
|
|
724
|
+
ccdHitDetection: ccdHit,
|
|
725
|
+
materialCombinationRules: materialRule.friction === 3 && materialRule.restitution === 5,
|
|
726
|
+
staticColliderCooking: staticColliderCook.bytes === cookStaticColliders(staticColliderCook.colliders).bytes,
|
|
727
|
+
snapshotRollbackEquivalent: cert.uninterruptedChecksum === cert.restoredChecksum && physics2d.checksum === restored2d.checksum,
|
|
728
|
+
certified2dAnd25dCases: cert.result === 'pass' && slotLoadCert.result === 'pass',
|
|
729
|
+
physicsBudgetReported: physics2d.broadphasePairs > 0 && physics2d.narrowphaseContacts > 0 && physics2d.solverIterations > 0,
|
|
730
|
+
lagCompensated3DHitboxProxies: lagComp3D.proxyBodiesCreated > 0
|
|
731
|
+
&& lagComp3D.proxyLifecycleBalanced
|
|
732
|
+
&& lagComp3D.multiHitboxQueries > 0
|
|
733
|
+
&& lagComp3D.animatedHitboxSamples > 0
|
|
734
|
+
&& lagComp3D.interpolatedHitboxSamples > 0,
|
|
735
|
+
lagCompensated3DWeapons: lagComp3D.rifleShots > 0 && lagComp3D.shotgunShots > 0 && lagComp3D.burstShots > 0,
|
|
736
|
+
lagCompensated3DOcclusionAndRejection: lagComp3D.occluded > 0
|
|
737
|
+
&& lagComp3D.staleRejected > 0
|
|
738
|
+
&& lagComp3D.futureRejected > 0
|
|
739
|
+
&& lagComp3D.misses > 0,
|
|
740
|
+
lagCompensated3DRollbackEquivalent: lagComp3D.rollbackEquivalent,
|
|
741
|
+
lagCompensated3DApiReusable: lagComp3DApi.hitReason === 'hit'
|
|
742
|
+
&& lagComp3DApi.occludedReason === 'occluded'
|
|
743
|
+
&& lagComp3DApi.staleReason === 'stale-frame'
|
|
744
|
+
&& lagComp3DApi.futureReason === 'future-frame',
|
|
745
|
+
lagCompensated3DApiSnapshotRestoreEquivalent: lagComp3DApi.rollbackEquivalent,
|
|
746
|
+
lagCompensated3DApiProxyLifecycle: lagComp3DApi.proxiesCreated > 0
|
|
747
|
+
&& lagComp3DApi.proxiesCreated === lagComp3DApi.proxiesDestroyed,
|
|
748
|
+
lagCompensated3DApiHistoryPruning: lagComp3DApi.prunedOldFrames,
|
|
749
|
+
physics3DCallbackLifecycle: physics3DCallbacks.rollbackEquivalent
|
|
750
|
+
&& physics3DCallbacks.collisionEnter > 0
|
|
751
|
+
&& physics3DCallbacks.collisionStay > 0
|
|
752
|
+
&& physics3DCallbacks.collisionExit > 0
|
|
753
|
+
&& physics3DCallbacks.triggerEnter > 0
|
|
754
|
+
&& physics3DCallbacks.triggerStay > 0
|
|
755
|
+
&& physics3DCallbacks.triggerExit > 0
|
|
756
|
+
&& physics3DCallbacks.perEntityMaskHonored,
|
|
757
|
+
physics3DQueryOptions: physics3DQueries.defaultHitAll
|
|
758
|
+
&& physics3DQueries.explicitSolidsOnly
|
|
759
|
+
&& physics3DQueries.kindFiltersStable
|
|
760
|
+
&& physics3DQueries.triggerSensorFiltersStable
|
|
761
|
+
&& physics3DQueries.raycastCovered
|
|
762
|
+
&& physics3DQueries.overlapCovered
|
|
763
|
+
&& physics3DQueries.shapeCastCovered,
|
|
764
|
+
physics3DMaterialResponse: physics3DMaterials.rollbackEquivalent
|
|
765
|
+
&& physics3DMaterials.combinedFriction === 3
|
|
766
|
+
&& physics3DMaterials.combinedRestitution === 7
|
|
767
|
+
&& physics3DMaterials.normalVelocityAfterBounce < 0,
|
|
768
|
+
physics3DDetailedQueriesAndContacts: physics3DDetails.rollbackEquivalent
|
|
769
|
+
&& physics3DDetails.rayDetailedHits > 0
|
|
770
|
+
&& physics3DDetails.shapeCastDetailedHits > 0
|
|
771
|
+
&& physics3DDetails.contactDetails > 0
|
|
772
|
+
&& physics3DDetails.stableUnderBodyOrderPermutation,
|
|
773
|
+
physics3DStackStability: physics3DStack.contactCount > 0
|
|
774
|
+
&& physics3DStack.solverCorrections > 0
|
|
775
|
+
&& physics3DStack.maxPenetration <= 0.05
|
|
776
|
+
&& physics3DStack.maxHeightDrift <= 0.05
|
|
777
|
+
&& physics3DStack.settledDynamicBodies === physics3DStack.stackLayers,
|
|
778
|
+
physics3DStackRollbackEquivalent: physics3DStack.rollbackEquivalent,
|
|
779
|
+
physics3DStackOrderStable: physics3DStack.stableUnderAuthoringOrderPermutation,
|
|
780
|
+
physics3DStressAndScheduledQueries: physics3DStress.rollbackEquivalent
|
|
781
|
+
&& physics3DStress.contactCount > 0
|
|
782
|
+
&& physics3DStress.queryCount > 0
|
|
783
|
+
&& physics3DStress.shapeCastCount > 0
|
|
784
|
+
&& physics3DStress.scheduledPrePhysicsQueries > 0
|
|
785
|
+
&& physics3DStress.scheduledPostPhysicsQueries > 0
|
|
786
|
+
&& physics3DStress.scheduledShapeCastQueries > 0
|
|
787
|
+
&& physics3DStress.scheduledQueryResultCount > 0,
|
|
788
|
+
physics3DGeometryCoverage: physics3DStress.angularBodies > 0
|
|
789
|
+
&& physics3DStress.meshStaticColliders > 0
|
|
790
|
+
&& physics3DStress.exactMeshRaycasts > 0
|
|
791
|
+
&& physics3DStress.exactMeshOverlapRejects > 0
|
|
792
|
+
&& physics3DStress.angularGeometryQueries > 0,
|
|
793
|
+
};
|
|
794
|
+
return categoryReport(config, 'physics', 'required', [
|
|
795
|
+
'Deterministic physics',
|
|
796
|
+
'2D/2.5D collision',
|
|
797
|
+
'3D physics callbacks/materials/queries',
|
|
798
|
+
'3D scheduled query pipeline',
|
|
799
|
+
'3D lag-compensated hitbox proxies',
|
|
800
|
+
'CCD',
|
|
801
|
+
'Physics material rules',
|
|
802
|
+
'Static collider cooking',
|
|
803
|
+
], invariants, {
|
|
804
|
+
uninterruptedChecksum: cert.uninterruptedChecksum,
|
|
805
|
+
restoredChecksum: cert.restoredChecksum,
|
|
806
|
+
collisionEvents: cert.collisionEvents,
|
|
807
|
+
snapshotBytesP95: cert.snapshotBytesP95,
|
|
808
|
+
slotLoadChecksum: slotLoadCert.restoredChecksum,
|
|
809
|
+
slotLoadPlayers: slotLoadCert.players,
|
|
810
|
+
staticColliderHash: staticColliderCook.hash,
|
|
811
|
+
physics2dChecksum: physics2d.checksum,
|
|
812
|
+
physics2dEvents: physics2d.events.length,
|
|
813
|
+
physics2dAllHitsHash: physics2d.allHitsHash,
|
|
814
|
+
physics2dCircleCastHash: physics2d.circleCastHash,
|
|
815
|
+
physics2dShapeOverlapHash: physics2d.shapeOverlapHash,
|
|
816
|
+
physics2dHeightFilterHash: physics2d.heightFilterHash,
|
|
817
|
+
physics2dBroadphasePairs: physics2d.broadphasePairs,
|
|
818
|
+
physics2dNarrowphaseContacts: physics2d.narrowphaseContacts,
|
|
819
|
+
physics2dSolverIterations: physics2d.solverIterations,
|
|
820
|
+
lagComp3DChecksum: lagComp3D.checksum,
|
|
821
|
+
lagComp3DRestoredChecksum: lagComp3D.restoredChecksum,
|
|
822
|
+
lagComp3DPlayers: lagComp3D.players,
|
|
823
|
+
lagComp3DFrames: lagComp3D.frames,
|
|
824
|
+
lagComp3DShots: lagComp3D.shots,
|
|
825
|
+
lagComp3DHits: lagComp3D.hits,
|
|
826
|
+
lagComp3DMisses: lagComp3D.misses,
|
|
827
|
+
lagComp3DOccluded: lagComp3D.occluded,
|
|
828
|
+
lagComp3DStaleRejected: lagComp3D.staleRejected,
|
|
829
|
+
lagComp3DFutureRejected: lagComp3D.futureRejected,
|
|
830
|
+
lagComp3DRifleShots: lagComp3D.rifleShots,
|
|
831
|
+
lagComp3DShotgunShots: lagComp3D.shotgunShots,
|
|
832
|
+
lagComp3DBurstShots: lagComp3D.burstShots,
|
|
833
|
+
lagComp3DProxyBodiesCreated: lagComp3D.proxyBodiesCreated,
|
|
834
|
+
lagComp3DInterpolatedHitboxSamples: lagComp3D.interpolatedHitboxSamples,
|
|
835
|
+
lagComp3DApiChecksum: lagComp3DApi.checksum,
|
|
836
|
+
lagComp3DApiRestoredChecksum: lagComp3DApi.restoredChecksum,
|
|
837
|
+
lagComp3DApiExactRewindTargets: lagComp3DApi.exactRewindTargets,
|
|
838
|
+
lagComp3DApiInterpolatedRewindTargets: lagComp3DApi.interpolatedRewindTargets,
|
|
839
|
+
lagComp3DApiHitReason: lagComp3DApi.hitReason,
|
|
840
|
+
lagComp3DApiOccludedReason: lagComp3DApi.occludedReason,
|
|
841
|
+
lagComp3DApiStaleReason: lagComp3DApi.staleReason,
|
|
842
|
+
lagComp3DApiFutureReason: lagComp3DApi.futureReason,
|
|
843
|
+
lagComp3DApiProxiesCreated: lagComp3DApi.proxiesCreated,
|
|
844
|
+
lagComp3DApiProxiesDestroyed: lagComp3DApi.proxiesDestroyed,
|
|
845
|
+
lagComp3DApiHistoryFrameCount: lagComp3DApi.historyFrameCount,
|
|
846
|
+
lagComp3DApiPrunedOldFrames: lagComp3DApi.prunedOldFrames,
|
|
847
|
+
physics3DCallbackChecksum: physics3DCallbacks.checksum,
|
|
848
|
+
physics3DCallbackRestoredChecksum: physics3DCallbacks.restoredChecksum,
|
|
849
|
+
physics3DCollisionEnter: physics3DCallbacks.collisionEnter,
|
|
850
|
+
physics3DCollisionStay: physics3DCallbacks.collisionStay,
|
|
851
|
+
physics3DCollisionExit: physics3DCallbacks.collisionExit,
|
|
852
|
+
physics3DTriggerEnter: physics3DCallbacks.triggerEnter,
|
|
853
|
+
physics3DTriggerStay: physics3DCallbacks.triggerStay,
|
|
854
|
+
physics3DTriggerExit: physics3DCallbacks.triggerExit,
|
|
855
|
+
physics3DDisabledEvents: physics3DCallbacks.disabledEvents,
|
|
856
|
+
physics3DQueryOptionsChecksum: physics3DQueries.checksum,
|
|
857
|
+
physics3DMaterialChecksum: physics3DMaterials.checksum,
|
|
858
|
+
physics3DMaterialRestoredChecksum: physics3DMaterials.restoredChecksum,
|
|
859
|
+
physics3DCombinedFriction: physics3DMaterials.combinedFriction,
|
|
860
|
+
physics3DCombinedRestitution: physics3DMaterials.combinedRestitution,
|
|
861
|
+
physics3DDetailChecksum: physics3DDetails.checksum,
|
|
862
|
+
physics3DDetailRestoredChecksum: physics3DDetails.restoredChecksum,
|
|
863
|
+
physics3DRayDetailedHits: physics3DDetails.rayDetailedHits,
|
|
864
|
+
physics3DShapeCastDetailedHits: physics3DDetails.shapeCastDetailedHits,
|
|
865
|
+
physics3DContactDetails: physics3DDetails.contactDetails,
|
|
866
|
+
physics3DStackChecksum: physics3DStack.checksum,
|
|
867
|
+
physics3DStackRestoredChecksum: physics3DStack.restoredChecksum,
|
|
868
|
+
physics3DStackReversedOrderChecksum: physics3DStack.reversedOrderChecksum,
|
|
869
|
+
physics3DStackBodies: physics3DStack.bodies,
|
|
870
|
+
physics3DStackFrames: physics3DStack.frames,
|
|
871
|
+
physics3DStackLayers: physics3DStack.stackLayers,
|
|
872
|
+
physics3DStackContacts: physics3DStack.contactCount,
|
|
873
|
+
physics3DStackSolverCorrections: physics3DStack.solverCorrections,
|
|
874
|
+
physics3DStackMaxPenetration: physics3DStack.maxPenetration,
|
|
875
|
+
physics3DStackMaxHeightDrift: physics3DStack.maxHeightDrift,
|
|
876
|
+
physics3DStackSettledDynamicBodies: physics3DStack.settledDynamicBodies,
|
|
877
|
+
physics3DStackP95StepCost: physics3DStack.p95StepCost,
|
|
878
|
+
physics3DStackP99StepCost: physics3DStack.p99StepCost,
|
|
879
|
+
physics3DStressChecksum: physics3DStress.checksum,
|
|
880
|
+
physics3DStressBodies: physics3DStress.bodies,
|
|
881
|
+
physics3DStressFrames: physics3DStress.frames,
|
|
882
|
+
physics3DStressContacts: physics3DStress.contactCount,
|
|
883
|
+
physics3DStressQueries: physics3DStress.queryCount,
|
|
884
|
+
physics3DStressShapeCasts: physics3DStress.shapeCastCount,
|
|
885
|
+
physics3DStressScheduledPreQueries: physics3DStress.scheduledPrePhysicsQueries,
|
|
886
|
+
physics3DStressScheduledPostQueries: physics3DStress.scheduledPostPhysicsQueries,
|
|
887
|
+
physics3DStressScheduledShapeCasts: physics3DStress.scheduledShapeCastQueries,
|
|
888
|
+
physics3DStressScheduledResults: physics3DStress.scheduledQueryResultCount,
|
|
889
|
+
physics3DStressP95StepMs: physics3DStress.p95StepMs,
|
|
890
|
+
physics3DStressP99StepMs: physics3DStress.p99StepMs,
|
|
891
|
+
physics3DStressAngularBodies: physics3DStress.angularBodies,
|
|
892
|
+
physics3DStressMeshStaticColliders: physics3DStress.meshStaticColliders,
|
|
893
|
+
physics3DStressExactMeshRaycasts: physics3DStress.exactMeshRaycasts,
|
|
894
|
+
physics3DStressExactMeshOverlapRejects: physics3DStress.exactMeshOverlapRejects,
|
|
895
|
+
physics3DStressAngularGeometryQueries: physics3DStress.angularGeometryQueries,
|
|
896
|
+
physics3DStressRollbackEquivalent: physics3DStress.rollbackEquivalent,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
function runPhysics2DParityWorkload(frames, seed, restoreFrame) {
|
|
900
|
+
const constraints = [
|
|
901
|
+
{ id: 'distance-player-crate', type: 'distance', a: 'player', b: 'crate', restLength: 6 },
|
|
902
|
+
{ id: 'fixed-platform-rider', type: 'fixed', a: 'platform', b: 'rider', offsetX: 2, offsetY: 0 },
|
|
903
|
+
{ id: 'motor-platform', type: 'motor', a: 'platform', motorVx: seed % 2 === 0 ? 1 : -1, motorVy: 0 },
|
|
904
|
+
{ id: 'spring-crate', type: 'spring', a: 'platform', b: 'spring', restLength: 4, stiffness: 500 },
|
|
905
|
+
];
|
|
906
|
+
let world = createDeterministicPhysicsWorld2D([
|
|
907
|
+
physicsBody('player', 'dynamic', -12, 0, 2, 0, 1),
|
|
908
|
+
physicsBody('crate', 'dynamic', 18, 0, 0, 0, 1),
|
|
909
|
+
physicsBody('spring', 'dynamic', 10, 0, 0, 0, 1),
|
|
910
|
+
physicsBody('rider', 'dynamic', 4, 0, 0, 0, 1),
|
|
911
|
+
physicsBody('platform', 'kinematic', 0, -6, 0, 0, 1),
|
|
912
|
+
{
|
|
913
|
+
...physicsBody('wall', 'static', 0, 0, 0, 0, 1),
|
|
914
|
+
halfWidth: 1,
|
|
915
|
+
halfHeight: 6,
|
|
916
|
+
material: { friction: 4, restitution: 5 },
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
...physicsBody('sensor', 'static', 6, 0, 0, 0, 2),
|
|
920
|
+
halfWidth: 3,
|
|
921
|
+
halfHeight: 6,
|
|
922
|
+
sensor: true,
|
|
923
|
+
trigger: true,
|
|
924
|
+
},
|
|
925
|
+
]);
|
|
926
|
+
let snapshot = world;
|
|
927
|
+
const eventLog = [];
|
|
928
|
+
let broadphasePairs = 0;
|
|
929
|
+
let narrowphaseContacts = 0;
|
|
930
|
+
let solverIterations = 0;
|
|
931
|
+
let queryOrderHash = '';
|
|
932
|
+
let raycastHash = '';
|
|
933
|
+
let allHitsHash = '';
|
|
934
|
+
let circleCastHash = '';
|
|
935
|
+
let shapeOverlapHash = '';
|
|
936
|
+
let heightFilterHash = '';
|
|
937
|
+
let layerQueryHash = '';
|
|
938
|
+
let constraintsHash = '';
|
|
939
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
940
|
+
if (frame === restoreFrame) {
|
|
941
|
+
snapshot = cloneCanonical(world);
|
|
942
|
+
}
|
|
943
|
+
if (restoreFrame >= 0 && frame === restoreFrame + 5) {
|
|
944
|
+
world = cloneCanonical(snapshot);
|
|
945
|
+
for (let replay = restoreFrame; replay < frame; replay += 1) {
|
|
946
|
+
world = stepDeterministicPhysicsWorld2D(world, { constraints, iterations: 3 }).world;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
if (frame === 24) {
|
|
950
|
+
world = {
|
|
951
|
+
...world,
|
|
952
|
+
bodies: world.bodies.map((body) => body.id === 'player' ? { ...body, vx: 8 } : body),
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
const step = stepDeterministicPhysicsWorld2D(world, { constraints, iterations: 3 });
|
|
956
|
+
world = step.world;
|
|
957
|
+
eventLog.push(...step.events.map((event) => `${event.type}:${event.pair}`));
|
|
958
|
+
broadphasePairs += step.broadphasePairs;
|
|
959
|
+
narrowphaseContacts += step.narrowphaseContacts;
|
|
960
|
+
solverIterations += step.solverIterations;
|
|
961
|
+
queryOrderHash = defaultChecksum(queryDeterministicPhysicsAabb2D(world, {
|
|
962
|
+
x: 0,
|
|
963
|
+
y: 0,
|
|
964
|
+
halfWidth: 16,
|
|
965
|
+
halfHeight: 12,
|
|
966
|
+
layerMask: 0xff,
|
|
967
|
+
includeSensors: true,
|
|
968
|
+
includeTriggers: true,
|
|
969
|
+
}));
|
|
970
|
+
raycastHash = defaultChecksum(raycastDeterministicPhysics2D(world, {
|
|
971
|
+
x: -20,
|
|
972
|
+
y: 0,
|
|
973
|
+
dx: 1,
|
|
974
|
+
dy: 0,
|
|
975
|
+
maxDistance: 64,
|
|
976
|
+
layerMask: 0xff,
|
|
977
|
+
includeSensors: true,
|
|
978
|
+
includeTriggers: true,
|
|
979
|
+
}));
|
|
980
|
+
allHitsHash = defaultChecksum(raycastAllDeterministicPhysics2D(world, {
|
|
981
|
+
x: -20,
|
|
982
|
+
y: 0,
|
|
983
|
+
dx: 1,
|
|
984
|
+
dy: 0,
|
|
985
|
+
maxDistance: 64,
|
|
986
|
+
layerMask: 0xff,
|
|
987
|
+
includeSensors: true,
|
|
988
|
+
includeTriggers: true,
|
|
989
|
+
}));
|
|
990
|
+
circleCastHash = defaultChecksum(circleCastDeterministicPhysics2D(world, {
|
|
991
|
+
x: -20,
|
|
992
|
+
y: 0,
|
|
993
|
+
radius: 2,
|
|
994
|
+
dx: 1,
|
|
995
|
+
dy: 0,
|
|
996
|
+
maxDistance: 40,
|
|
997
|
+
layerMask: 0xff,
|
|
998
|
+
includeSensors: true,
|
|
999
|
+
includeTriggers: true,
|
|
1000
|
+
}));
|
|
1001
|
+
shapeOverlapHash = defaultChecksum(queryDeterministicPhysicsShapeOverlap2D(world, {
|
|
1002
|
+
shape: 'circle',
|
|
1003
|
+
x: 6,
|
|
1004
|
+
y: 0,
|
|
1005
|
+
radius: 4,
|
|
1006
|
+
layerMask: 0xff,
|
|
1007
|
+
includeSensors: true,
|
|
1008
|
+
includeTriggers: true,
|
|
1009
|
+
}));
|
|
1010
|
+
heightFilterHash = defaultChecksum(queryDeterministicPhysicsShapeOverlap2D({
|
|
1011
|
+
...world,
|
|
1012
|
+
bodies: [
|
|
1013
|
+
...world.bodies,
|
|
1014
|
+
{ ...physicsBody('height-low', 'static', 30, 0, 0, 0, 4), z: 0, halfDepth: 1 },
|
|
1015
|
+
{ ...physicsBody('height-high', 'static', 30, 0, 0, 0, 4), z: 8, halfDepth: 1 },
|
|
1016
|
+
],
|
|
1017
|
+
}, {
|
|
1018
|
+
shape: 'aabb',
|
|
1019
|
+
x: 30,
|
|
1020
|
+
y: 0,
|
|
1021
|
+
z: 0,
|
|
1022
|
+
halfWidth: 4,
|
|
1023
|
+
halfHeight: 4,
|
|
1024
|
+
halfDepth: 2,
|
|
1025
|
+
layerMask: 4,
|
|
1026
|
+
}));
|
|
1027
|
+
layerQueryHash = defaultChecksum(queryDeterministicPhysicsAabb2D(world, {
|
|
1028
|
+
x: 6,
|
|
1029
|
+
y: 0,
|
|
1030
|
+
halfWidth: 4,
|
|
1031
|
+
halfHeight: 8,
|
|
1032
|
+
layerMask: 2,
|
|
1033
|
+
includeSensors: true,
|
|
1034
|
+
includeTriggers: true,
|
|
1035
|
+
}));
|
|
1036
|
+
constraintsHash = defaultChecksum(world.bodies
|
|
1037
|
+
.filter((body) => body.id === 'crate' || body.id === 'rider' || body.id === 'spring')
|
|
1038
|
+
.map((body) => `${body.id}:${body.x}:${body.y}:${body.vx}:${body.vy}`));
|
|
1039
|
+
}
|
|
1040
|
+
return {
|
|
1041
|
+
checksum: defaultChecksum(world),
|
|
1042
|
+
events: eventLog,
|
|
1043
|
+
queryOrderHash,
|
|
1044
|
+
raycastHash,
|
|
1045
|
+
allHitsHash,
|
|
1046
|
+
circleCastHash,
|
|
1047
|
+
shapeOverlapHash,
|
|
1048
|
+
heightFilterHash,
|
|
1049
|
+
layerQueryHash,
|
|
1050
|
+
constraintsHash,
|
|
1051
|
+
broadphasePairs,
|
|
1052
|
+
narrowphaseContacts,
|
|
1053
|
+
solverIterations,
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
function physicsBody(id, kind, x, y, vx, vy, layer) {
|
|
1057
|
+
return {
|
|
1058
|
+
id,
|
|
1059
|
+
kind,
|
|
1060
|
+
x,
|
|
1061
|
+
y,
|
|
1062
|
+
vx,
|
|
1063
|
+
vy,
|
|
1064
|
+
halfWidth: 1,
|
|
1065
|
+
halfHeight: 1,
|
|
1066
|
+
layer,
|
|
1067
|
+
mask: 0xff,
|
|
1068
|
+
mass: kind === 'static' ? undefined : 1,
|
|
1069
|
+
damping: 0,
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
function runMovementCategory(config) {
|
|
1073
|
+
const frames = cappedProofFrames(config, 2_000);
|
|
1074
|
+
const control = runMovementSimulation(frames, config.seed, -1);
|
|
1075
|
+
const restored = runMovementSimulation(frames, config.seed, Math.floor(frames / 2));
|
|
1076
|
+
const kcc3d = runDeterministicKcc3DCrowdFixture(Math.min(config.players, 128), Math.max(120, Math.min(config.frames, 1_000)));
|
|
1077
|
+
const kcc3dFilterForce = runDeterministicKcc3DFilterForceFixture();
|
|
1078
|
+
const kcc3dCallbackFilter = runDeterministicKcc3DCallbackFilterFixture();
|
|
1079
|
+
const kcc3dGeometry = runDeterministicKcc3DGeometryFixture();
|
|
1080
|
+
const kcc3dAdversarial = runDeterministicKcc3DAdversarialFixture();
|
|
1081
|
+
const invariants = {
|
|
1082
|
+
slopesAndSteps: control.slopes > 0 && control.steps > 0,
|
|
1083
|
+
ledges: control.ledges > 0,
|
|
1084
|
+
movingPlatforms: control.platformRides > 0,
|
|
1085
|
+
elevators: control.elevatorRides > 0,
|
|
1086
|
+
oneWayPlatforms: control.oneWayDrops > 0,
|
|
1087
|
+
wallSlides: control.wallSlides > 0,
|
|
1088
|
+
jumpBuffering: control.bufferedJumps > 0,
|
|
1089
|
+
coyoteTime: control.coyoteJumps > 0,
|
|
1090
|
+
contactRollbackEquivalent: control.checksum === restored.checksum,
|
|
1091
|
+
topDownSliding: control.slides > 0,
|
|
1092
|
+
navSteering: control.navSteeringFrames > 0,
|
|
1093
|
+
quantizedFpsHitscanOrigin: control.hitscanOriginsStable,
|
|
1094
|
+
deterministicRecoil: control.recoilFrames > 0,
|
|
1095
|
+
lagCompensatedTargetRewind: control.rewindHits > 0
|
|
1096
|
+
&& control.rewindValidatedHits > 0
|
|
1097
|
+
&& control.rewindMisses > 0
|
|
1098
|
+
&& control.rewindRejectedStale > 0
|
|
1099
|
+
&& control.rewindRejectedFuture > 0
|
|
1100
|
+
&& control.rewindInterpolated > 0
|
|
1101
|
+
&& control.rewindValidationChecksum.length > 0,
|
|
1102
|
+
crowd128: control.crowdPlayers === 128,
|
|
1103
|
+
mixedHumanBotCrowd: control.crowdHumanInputSlots > 0 && control.crowdBotInputSlots > 0,
|
|
1104
|
+
predictionCorrection: control.predictionCorrections > 0,
|
|
1105
|
+
crowdRollbackEquivalent: control.crowdRollbackEquivalent,
|
|
1106
|
+
kcc3DSlopesStepsPlatforms: kcc3d.slopeContacts > 0
|
|
1107
|
+
&& kcc3d.stepContacts > 0
|
|
1108
|
+
&& kcc3d.platformContacts > 0
|
|
1109
|
+
&& kcc3d.platformVelocityTransfers > 0,
|
|
1110
|
+
kcc3DPhysicsSweepsAndSlides: kcc3d.physicsSlides > 0
|
|
1111
|
+
&& kcc3d.sweptCollisions > 0
|
|
1112
|
+
&& kcc3d.headClearanceBlocks > 0
|
|
1113
|
+
&& kcc3d.physicsQueryCount > 0,
|
|
1114
|
+
kcc3DRollbackEquivalent: kcc3d.rollbackEquivalent,
|
|
1115
|
+
kcc3DFilterAndExternalForces: kcc3dFilterForce.rollbackEquivalent
|
|
1116
|
+
&& kcc3dFilterForce.ignoredColliderPassedThrough
|
|
1117
|
+
&& kcc3dFilterForce.solidColliderBlocked
|
|
1118
|
+
&& kcc3dFilterForce.externalImpulseApplied
|
|
1119
|
+
&& kcc3dFilterForce.filterOrderStable,
|
|
1120
|
+
kcc3DPerHitCallbackFiltering: kcc3dCallbackFilter.rollbackEquivalent
|
|
1121
|
+
&& kcc3dCallbackFilter.callbackIgnoredColliderPassedThrough
|
|
1122
|
+
&& kcc3dCallbackFilter.callbackSolidColliderBlocked
|
|
1123
|
+
&& kcc3dCallbackFilter.callbackLayerFiltered
|
|
1124
|
+
&& kcc3dCallbackFilter.callbackNormalFiltered
|
|
1125
|
+
&& kcc3dCallbackFilter.callbackOrderStable
|
|
1126
|
+
&& kcc3dCallbackFilter.callbackMetadataCovered
|
|
1127
|
+
&& kcc3dCallbackFilter.mutableFilterRejected
|
|
1128
|
+
&& kcc3dCallbackFilter.callbackInvocations > 0
|
|
1129
|
+
&& kcc3dCallbackFilter.callbackRejectedHits > 0
|
|
1130
|
+
&& kcc3dCallbackFilter.callbackAcceptedHits > 0,
|
|
1131
|
+
kcc3DControllerGeometry: kcc3dGeometry.rollbackEquivalent
|
|
1132
|
+
&& kcc3dGeometry.defaultGeometryPreserved
|
|
1133
|
+
&& kcc3dGeometry.smallRadiusPassesNarrowGap
|
|
1134
|
+
&& kcc3dGeometry.largeRadiusBlockedByNarrowGap
|
|
1135
|
+
&& kcc3dGeometry.tallBodyHeadBlocked
|
|
1136
|
+
&& kcc3dGeometry.shortBodyClearsLowCeiling
|
|
1137
|
+
&& kcc3dGeometry.skinWidthExpandsCollisionShape
|
|
1138
|
+
&& kcc3dGeometry.geometryMetadataStable,
|
|
1139
|
+
kcc3DAdversarialEdges: kcc3dAdversarial.rollbackEquivalent
|
|
1140
|
+
&& kcc3dAdversarial.thinWallSweeps > 0
|
|
1141
|
+
&& kcc3dAdversarial.diagonalThinWallSweeps > 0
|
|
1142
|
+
&& kcc3dAdversarial.ledgeCorrections > 0
|
|
1143
|
+
&& kcc3dAdversarial.ceilingBlocks > 0
|
|
1144
|
+
&& kcc3dAdversarial.cornerSweeps > 0
|
|
1145
|
+
&& kcc3dAdversarial.movingPlatformEdgeTransfers > 0
|
|
1146
|
+
&& kcc3dAdversarial.slopeLimitRejections > 0,
|
|
1147
|
+
};
|
|
1148
|
+
return categoryReport(config, 'movement', 'required', [
|
|
1149
|
+
'KCC',
|
|
1150
|
+
'3D KCC',
|
|
1151
|
+
'Slopes/steps/ledges',
|
|
1152
|
+
'Moving platforms/elevators',
|
|
1153
|
+
'One-way platforms',
|
|
1154
|
+
'Wall slides',
|
|
1155
|
+
'Jump buffering/coyote time',
|
|
1156
|
+
'Top-down movement',
|
|
1157
|
+
'Nav steering',
|
|
1158
|
+
'FPS quantized aim/hitscan/recoil',
|
|
1159
|
+
'Lag-compensated target rewind',
|
|
1160
|
+
'KCC collision callbacks',
|
|
1161
|
+
'KCC controller geometry',
|
|
1162
|
+
], invariants, {
|
|
1163
|
+
...control,
|
|
1164
|
+
kcc3DChecksum: kcc3d.checksum,
|
|
1165
|
+
kcc3DPlayers: kcc3d.players,
|
|
1166
|
+
kcc3DFrames: kcc3d.frames,
|
|
1167
|
+
kcc3DCorrectionCount: kcc3d.correctionCount,
|
|
1168
|
+
kcc3DPhysicsQueryCount: kcc3d.physicsQueryCount,
|
|
1169
|
+
kcc3DSlopeContacts: kcc3d.slopeContacts,
|
|
1170
|
+
kcc3DStepContacts: kcc3d.stepContacts,
|
|
1171
|
+
kcc3DPlatformContacts: kcc3d.platformContacts,
|
|
1172
|
+
kcc3DPhysicsSlides: kcc3d.physicsSlides,
|
|
1173
|
+
kcc3DSweptCollisions: kcc3d.sweptCollisions,
|
|
1174
|
+
kcc3DHeadClearanceBlocks: kcc3d.headClearanceBlocks,
|
|
1175
|
+
kcc3DPlatformVelocityTransfers: kcc3d.platformVelocityTransfers,
|
|
1176
|
+
kcc3DRollbackEquivalent: kcc3d.rollbackEquivalent,
|
|
1177
|
+
kcc3DP95MovementCost: kcc3d.p95MovementCost,
|
|
1178
|
+
kcc3DP99MovementCost: kcc3d.p99MovementCost,
|
|
1179
|
+
kcc3DFilterForceChecksum: kcc3dFilterForce.checksum,
|
|
1180
|
+
kcc3DFilterForceRestoredChecksum: kcc3dFilterForce.restoredChecksum,
|
|
1181
|
+
kcc3DFilterForceIgnoredEvents: kcc3dFilterForce.ignoredCollisionEvents,
|
|
1182
|
+
kcc3DFilterForceSolidEvents: kcc3dFilterForce.solidCollisionEvents,
|
|
1183
|
+
kcc3DFilterForceExternalImpulseEvents: kcc3dFilterForce.externalImpulseEvents,
|
|
1184
|
+
kcc3DCallbackFilterChecksum: kcc3dCallbackFilter.checksum,
|
|
1185
|
+
kcc3DCallbackFilterRestoredChecksum: kcc3dCallbackFilter.restoredChecksum,
|
|
1186
|
+
kcc3DCallbackFilterRollbackEquivalent: kcc3dCallbackFilter.rollbackEquivalent,
|
|
1187
|
+
kcc3DCallbackFilterInvocations: kcc3dCallbackFilter.callbackInvocations,
|
|
1188
|
+
kcc3DCallbackFilterRejectedHits: kcc3dCallbackFilter.callbackRejectedHits,
|
|
1189
|
+
kcc3DCallbackFilterAcceptedHits: kcc3dCallbackFilter.callbackAcceptedHits,
|
|
1190
|
+
kcc3DCallbackFilterIgnoredPassedThrough: kcc3dCallbackFilter.callbackIgnoredColliderPassedThrough,
|
|
1191
|
+
kcc3DCallbackFilterSolidBlocked: kcc3dCallbackFilter.callbackSolidColliderBlocked,
|
|
1192
|
+
kcc3DCallbackFilterLayerFiltered: kcc3dCallbackFilter.callbackLayerFiltered,
|
|
1193
|
+
kcc3DCallbackFilterNormalFiltered: kcc3dCallbackFilter.callbackNormalFiltered,
|
|
1194
|
+
kcc3DCallbackFilterOrderStable: kcc3dCallbackFilter.callbackOrderStable,
|
|
1195
|
+
kcc3DCallbackFilterMetadataCovered: kcc3dCallbackFilter.callbackMetadataCovered,
|
|
1196
|
+
kcc3DCallbackFilterMutableRejected: kcc3dCallbackFilter.mutableFilterRejected,
|
|
1197
|
+
kcc3DGeometryChecksum: kcc3dGeometry.checksum,
|
|
1198
|
+
kcc3DGeometryRestoredChecksum: kcc3dGeometry.restoredChecksum,
|
|
1199
|
+
kcc3DGeometryRollbackEquivalent: kcc3dGeometry.rollbackEquivalent,
|
|
1200
|
+
kcc3DGeometryDefaultPreserved: kcc3dGeometry.defaultGeometryPreserved,
|
|
1201
|
+
kcc3DGeometrySmallRadiusPassesNarrowGap: kcc3dGeometry.smallRadiusPassesNarrowGap,
|
|
1202
|
+
kcc3DGeometryLargeRadiusBlockedByNarrowGap: kcc3dGeometry.largeRadiusBlockedByNarrowGap,
|
|
1203
|
+
kcc3DGeometryTallBodyHeadBlocked: kcc3dGeometry.tallBodyHeadBlocked,
|
|
1204
|
+
kcc3DGeometryShortBodyClearsLowCeiling: kcc3dGeometry.shortBodyClearsLowCeiling,
|
|
1205
|
+
kcc3DGeometrySkinWidthExpandsCollisionShape: kcc3dGeometry.skinWidthExpandsCollisionShape,
|
|
1206
|
+
kcc3DGeometryMetadataStable: kcc3dGeometry.geometryMetadataStable,
|
|
1207
|
+
kcc3DAdversarialChecksum: kcc3dAdversarial.checksum,
|
|
1208
|
+
kcc3DAdversarialRestoredChecksum: kcc3dAdversarial.restoredChecksum,
|
|
1209
|
+
kcc3DAdversarialThinWallSweeps: kcc3dAdversarial.thinWallSweeps,
|
|
1210
|
+
kcc3DAdversarialDiagonalThinWallSweeps: kcc3dAdversarial.diagonalThinWallSweeps,
|
|
1211
|
+
kcc3DAdversarialLedgeCorrections: kcc3dAdversarial.ledgeCorrections,
|
|
1212
|
+
kcc3DAdversarialCeilingBlocks: kcc3dAdversarial.ceilingBlocks,
|
|
1213
|
+
kcc3DAdversarialCornerSweeps: kcc3dAdversarial.cornerSweeps,
|
|
1214
|
+
kcc3DAdversarialMovingPlatformEdgeTransfers: kcc3dAdversarial.movingPlatformEdgeTransfers,
|
|
1215
|
+
kcc3DAdversarialSlopeLimitRejections: kcc3dAdversarial.slopeLimitRejections,
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
function runNavigationCategory(config) {
|
|
1219
|
+
const frames = cappedProofFrames(config, 2_000);
|
|
1220
|
+
const control = runNavigationSimulation(config.players, frames, config.seed, -1);
|
|
1221
|
+
const restored = runNavigationSimulation(config.players, frames, config.seed, Math.floor(frames / 2));
|
|
1222
|
+
const navmesh = runDeterministicNavmeshStress(config.players, frames);
|
|
1223
|
+
const importedNavmesh = runDeterministicNavmeshImportTraversalFixture();
|
|
1224
|
+
const velocityObstacle = runDeterministicNavmeshVelocityObstacleFixture();
|
|
1225
|
+
const invariants = {
|
|
1226
|
+
cookedGraphLoading: control.cookedGraphHash.length > 0,
|
|
1227
|
+
aStarTieBreaking: control.pathHash === restored.pathHash,
|
|
1228
|
+
regionEnableDisable: control.regionToggles > 0,
|
|
1229
|
+
dynamicBlockerReplanning: control.replans > 0,
|
|
1230
|
+
dynamicObstacleRemoval: control.dynamicObstacleRemovals > 0,
|
|
1231
|
+
offMeshTraversal: control.offMeshTraversals > 0,
|
|
1232
|
+
pathSmoothing: control.pathSmoothing > 0,
|
|
1233
|
+
flowFieldMovement: control.flowFieldSteps > 0,
|
|
1234
|
+
tilemapPathfinding: control.tilePaths > 0,
|
|
1235
|
+
deterministicAvoidanceOrdering: control.avoidanceHash === restored.avoidanceHash && control.reciprocalAvoidanceCorrections > 0,
|
|
1236
|
+
rollbackEquivalent: control.checksum === restored.checksum,
|
|
1237
|
+
polygonNavmeshCooked: navmesh.pathHash.length > 0,
|
|
1238
|
+
polygonNavmesh128Agents: navmesh.agents === config.players,
|
|
1239
|
+
polygonNavmeshRollbackEquivalent: navmesh.rollbackEquivalent,
|
|
1240
|
+
polygonNavmeshPointLookup: navmesh.pointPathFound && navmesh.pointLookupStable,
|
|
1241
|
+
polygonNavmeshAvoidance: navmesh.avoidanceCorrections > 0 && navmesh.radiusAvoidanceCorrections > 0,
|
|
1242
|
+
polygonNavmeshVelocityObstacleAvoidance: navmesh.velocityObstacleAvoidanceCorrections > 0
|
|
1243
|
+
&& navmesh.avoidanceSpeedReductions > 0
|
|
1244
|
+
&& velocityObstacle.movingObstacleSpeedReduction
|
|
1245
|
+
&& velocityObstacle.predictedFuturePositionAvoided
|
|
1246
|
+
&& velocityObstacle.obstacleOrderStable
|
|
1247
|
+
&& velocityObstacle.obstacleValidationRejected
|
|
1248
|
+
&& velocityObstacle.rollbackEquivalent,
|
|
1249
|
+
polygonNavmeshAgentVelocityObstacleAvoidance: navmesh.agentVelocityObstacleAvoidanceCorrections > 0
|
|
1250
|
+
&& navmesh.agentAvoidancePriorityYields > 0
|
|
1251
|
+
&& navmesh.agentAvoidanceSpeedReductions > 0,
|
|
1252
|
+
polygonNavmeshDynamicReplanning: navmesh.replans > 0,
|
|
1253
|
+
importedNavmeshCookOrderStable: importedNavmesh.importOrderStable,
|
|
1254
|
+
importedNavmeshValidationFailures: importedNavmesh.missingRegionRejected
|
|
1255
|
+
&& importedNavmesh.missingLinkRejected
|
|
1256
|
+
&& importedNavmesh.missingSpawnRejected,
|
|
1257
|
+
importedNavmeshPortalSpawnTables: importedNavmesh.portalCount > 0 && importedNavmesh.spawnMarkerCount > 0,
|
|
1258
|
+
offMeshTraversalLifecycle: importedNavmesh.offMeshEnterEvents > 0
|
|
1259
|
+
&& importedNavmesh.offMeshTraverseEvents > 0
|
|
1260
|
+
&& importedNavmesh.offMeshExitEvents > 0
|
|
1261
|
+
&& importedNavmesh.offMeshCompleteEvents > 0,
|
|
1262
|
+
offMeshTraversalRollbackEquivalent: importedNavmesh.rollbackEquivalent,
|
|
1263
|
+
};
|
|
1264
|
+
return categoryReport(config, 'navigation', 'required', [
|
|
1265
|
+
'Navmesh-like graph',
|
|
1266
|
+
'Polygon navmesh',
|
|
1267
|
+
'A*',
|
|
1268
|
+
'Runtime regions',
|
|
1269
|
+
'Dynamic obstacles',
|
|
1270
|
+
'Off-mesh links',
|
|
1271
|
+
'Imported navmesh cooking',
|
|
1272
|
+
'Off-mesh traversal lifecycle',
|
|
1273
|
+
'Flow fields',
|
|
1274
|
+
'Tilemap pathfinder',
|
|
1275
|
+
'Avoidance',
|
|
1276
|
+
'Velocity-obstacle avoidance',
|
|
1277
|
+
], invariants, {
|
|
1278
|
+
...control,
|
|
1279
|
+
polygonNavmeshChecksum: navmesh.checksum,
|
|
1280
|
+
polygonNavmeshAgents: navmesh.agents,
|
|
1281
|
+
polygonNavmeshFrames: navmesh.frames,
|
|
1282
|
+
polygonNavmeshReplans: navmesh.replans,
|
|
1283
|
+
polygonNavmeshAvoidanceCorrections: navmesh.avoidanceCorrections,
|
|
1284
|
+
polygonNavmeshRadiusAvoidanceCorrections: navmesh.radiusAvoidanceCorrections,
|
|
1285
|
+
polygonNavmeshVelocityObstacleCorrections: navmesh.velocityObstacleAvoidanceCorrections,
|
|
1286
|
+
polygonNavmeshAgentVelocityObstacleCorrections: navmesh.agentVelocityObstacleAvoidanceCorrections,
|
|
1287
|
+
polygonNavmeshAgentAvoidancePriorityYields: navmesh.agentAvoidancePriorityYields,
|
|
1288
|
+
polygonNavmeshAvoidanceSpeedReductions: navmesh.avoidanceSpeedReductions,
|
|
1289
|
+
polygonNavmeshAgentAvoidanceSpeedReductions: navmesh.agentAvoidanceSpeedReductions,
|
|
1290
|
+
polygonNavmeshWaypointAdvances: navmesh.waypointAdvances,
|
|
1291
|
+
polygonNavmeshPathHash: navmesh.pathHash,
|
|
1292
|
+
polygonNavmeshPointPathHash: navmesh.pointPathHash,
|
|
1293
|
+
polygonNavmeshPointPathFound: navmesh.pointPathFound,
|
|
1294
|
+
polygonNavmeshPointLookupStable: navmesh.pointLookupStable,
|
|
1295
|
+
polygonNavmeshRollbackEquivalent: navmesh.rollbackEquivalent,
|
|
1296
|
+
importedNavmeshHash: importedNavmesh.importedHash,
|
|
1297
|
+
importedNavmeshPermutedHash: importedNavmesh.permutedHash,
|
|
1298
|
+
importedNavmeshPortalCount: importedNavmesh.portalCount,
|
|
1299
|
+
importedNavmeshSpawnMarkerCount: importedNavmesh.spawnMarkerCount,
|
|
1300
|
+
importedNavmeshOffMeshEnterEvents: importedNavmesh.offMeshEnterEvents,
|
|
1301
|
+
importedNavmeshOffMeshTraverseEvents: importedNavmesh.offMeshTraverseEvents,
|
|
1302
|
+
importedNavmeshOffMeshExitEvents: importedNavmesh.offMeshExitEvents,
|
|
1303
|
+
importedNavmeshOffMeshCompleteEvents: importedNavmesh.offMeshCompleteEvents,
|
|
1304
|
+
importedNavmeshOffMeshTraversalFrames: importedNavmesh.offMeshTraversalFrames,
|
|
1305
|
+
importedNavmeshPointPathHash: importedNavmesh.pointPathHash,
|
|
1306
|
+
importedNavmeshRollbackEquivalent: importedNavmesh.rollbackEquivalent,
|
|
1307
|
+
velocityObstacleChecksum: velocityObstacle.checksum,
|
|
1308
|
+
velocityObstacleRestoredChecksum: velocityObstacle.restoredChecksum,
|
|
1309
|
+
velocityObstacleRollbackEquivalent: velocityObstacle.rollbackEquivalent,
|
|
1310
|
+
velocityObstacleSpeedReduction: velocityObstacle.movingObstacleSpeedReduction,
|
|
1311
|
+
velocityObstacleFuturePrediction: velocityObstacle.predictedFuturePositionAvoided,
|
|
1312
|
+
velocityObstacleOrderStable: velocityObstacle.obstacleOrderStable,
|
|
1313
|
+
velocityObstacleValidationRejected: velocityObstacle.obstacleValidationRejected,
|
|
1314
|
+
velocityObstacleCorrections: velocityObstacle.velocityObstacleAvoidanceCorrections,
|
|
1315
|
+
velocityObstacleSpeedReductions: velocityObstacle.avoidanceSpeedReductions,
|
|
1316
|
+
});
|
|
1317
|
+
}
|
|
1318
|
+
function runAssetCookingCategory(config) {
|
|
1319
|
+
const source = {
|
|
1320
|
+
assets: [
|
|
1321
|
+
{ id: 'bot.utility', type: 'bot', weight: 10 },
|
|
1322
|
+
{ id: 'weapon.sword', type: 'item' },
|
|
1323
|
+
{
|
|
1324
|
+
id: 'proto.base',
|
|
1325
|
+
type: 'prototype',
|
|
1326
|
+
prototype: true,
|
|
1327
|
+
references: ['weapon.sword'],
|
|
1328
|
+
components: {
|
|
1329
|
+
Position: { x: 0, y: 0 },
|
|
1330
|
+
Health: { current: 100 },
|
|
1331
|
+
Inventory: {
|
|
1332
|
+
items: { collection: 'list', values: ['weapon.sword'] },
|
|
1333
|
+
tags: { collection: 'set', values: ['spawned', 'player', 'spawned'] },
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
},
|
|
1337
|
+
{
|
|
1338
|
+
id: 'proto.player',
|
|
1339
|
+
type: 'prototype',
|
|
1340
|
+
prototype: true,
|
|
1341
|
+
extendsRef: 'proto.base',
|
|
1342
|
+
references: ['proto.base'],
|
|
1343
|
+
components: {
|
|
1344
|
+
Health: { current: 80 },
|
|
1345
|
+
},
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
id: 'map.arena',
|
|
1349
|
+
type: 'map',
|
|
1350
|
+
references: ['proto.player'],
|
|
1351
|
+
spawns: [{ prototypeRef: 'proto.player', count: 4, startEntityId: 100, overrides: { Health: { current: 90 } } }],
|
|
1352
|
+
},
|
|
1353
|
+
],
|
|
1354
|
+
};
|
|
1355
|
+
const prototypeDescriptor = {
|
|
1356
|
+
id: 'parity-prototype-runtime',
|
|
1357
|
+
tickRate: 60,
|
|
1358
|
+
components: [
|
|
1359
|
+
{ id: 'Position', fields: [{ name: 'x', kind: 'int', defaultValue: 0 }, { name: 'y', kind: 'int', defaultValue: 0 }] },
|
|
1360
|
+
{ id: 'Health', fields: [{ name: 'current', kind: 'int', defaultValue: 100 }] },
|
|
1361
|
+
{
|
|
1362
|
+
id: 'Inventory',
|
|
1363
|
+
fields: [
|
|
1364
|
+
{ name: 'items', kind: 'asset-ref[]', defaultValue: [] },
|
|
1365
|
+
{ name: 'tags', kind: 'string[]', defaultValue: [] },
|
|
1366
|
+
],
|
|
1367
|
+
},
|
|
1368
|
+
],
|
|
1369
|
+
systems: [{ id: 'simulate', group: 'simulation', order: 0 }],
|
|
1370
|
+
};
|
|
1371
|
+
const first = cookDeterministicAssets(source);
|
|
1372
|
+
const second = cookDeterministicAssets(source);
|
|
1373
|
+
const validCookNotRejected = !assetCookFails({ assets: [{ id: 'valid.asset', references: [] }] });
|
|
1374
|
+
const duplicateRejected = assetCookFails({ assets: [{ id: 'x' }, { id: 'x' }] });
|
|
1375
|
+
const unresolvedRejected = assetCookFails({ assets: [{ id: 'map', references: ['missing'] }] });
|
|
1376
|
+
const floatRejected = assetCookFails({ assets: [{ id: 'floaty', x: 1.25 }] });
|
|
1377
|
+
const prototypeA = materializePrototypeEntities(first, 'proto.player', 4);
|
|
1378
|
+
const prototypeB = materializePrototypeEntities(first, 'proto.player', 4);
|
|
1379
|
+
const inheritedPrototype = materializePrototypeEntities(first, 'proto.player', 1, {
|
|
1380
|
+
componentOverrides: { Controller: { slot: 2 } },
|
|
1381
|
+
});
|
|
1382
|
+
const inheritedComponents = inheritedPrototype[0]?.components;
|
|
1383
|
+
const mapBake = materializeDeterministicMapEntities(first, 'map.arena');
|
|
1384
|
+
const mapBakeRepeat = materializeDeterministicMapEntities(second, 'map.arena');
|
|
1385
|
+
const mapRebake = materializeDeterministicMapEntities(cookDeterministicAssets({
|
|
1386
|
+
...source,
|
|
1387
|
+
assets: source.assets.map((asset) => asset.id === 'map.arena'
|
|
1388
|
+
? { ...asset, spawns: [{ prototypeRef: 'proto.player', count: 5, startEntityId: 100, overrides: { Health: { current: 90 } } }] }
|
|
1389
|
+
: asset),
|
|
1390
|
+
}), 'map.arena');
|
|
1391
|
+
const runtimeValidation = validatePrototypeEntitiesAgainstFrameRuntime(first, 'proto.player', prototypeDescriptor, 4);
|
|
1392
|
+
const currentManifestAccepted = !throws(() => assertAssetManifestMatches(first.manifest, {
|
|
1393
|
+
engineVersion: first.manifest.engineVersion,
|
|
1394
|
+
assetDbHash: first.manifest.assetDbHash,
|
|
1395
|
+
}));
|
|
1396
|
+
const undeclaredPrototypeFieldRejected = throws(() => validatePrototypeEntitiesAgainstFrameRuntime(cookDeterministicAssets({
|
|
1397
|
+
assets: [{
|
|
1398
|
+
id: 'proto.bad',
|
|
1399
|
+
prototype: true,
|
|
1400
|
+
components: { Health: { current: 100, extra: 1 } },
|
|
1401
|
+
}],
|
|
1402
|
+
}), 'proto.bad', prototypeDescriptor, 1));
|
|
1403
|
+
const staleEngineRejected = throws(() => assertAssetManifestMatches(first.manifest, { engineVersion: `${first.manifest.engineVersion}-stale` }));
|
|
1404
|
+
const staleAssetDbRejected = throws(() => assertAssetManifestMatches(first.manifest, { assetDbHash: '0000000000000000' }));
|
|
1405
|
+
const invariants = {
|
|
1406
|
+
byteIdenticalCooking: first.bytes === second.bytes,
|
|
1407
|
+
stableIdsHashesLuts: first.manifest.assetDbHash === second.manifest.assetDbHash && first.manifest.lookupHash === second.manifest.lookupHash,
|
|
1408
|
+
prototypeSpawningStable: defaultChecksum(prototypeA) === defaultChecksum(prototypeB),
|
|
1409
|
+
duplicateIdsRejected: duplicateRejected,
|
|
1410
|
+
validCookAccepted: validCookNotRejected,
|
|
1411
|
+
unresolvedReferencesRejected: unresolvedRejected,
|
|
1412
|
+
unquantizedFloatsRejected: floatRejected,
|
|
1413
|
+
currentManifestAccepted,
|
|
1414
|
+
staleEngineRejected,
|
|
1415
|
+
staleAssetDbRejected,
|
|
1416
|
+
failClosedLoadChecks: staleEngineRejected && staleAssetDbRejected,
|
|
1417
|
+
descriptorBackedPrototypeValidation: runtimeValidation.injectedComponentCount === 12 && runtimeValidation.runtimeSnapshotChecksum.length > 0,
|
|
1418
|
+
undeclaredPrototypeFieldRejected,
|
|
1419
|
+
compoundPrototypeInheritanceCovered: inheritedComponents?.Position?.x === 0
|
|
1420
|
+
&& inheritedComponents.Health?.current === 80
|
|
1421
|
+
&& inheritedComponents.Controller?.slot === 2,
|
|
1422
|
+
dynamicCollectionAllocationCovered: inheritedComponents?.Inventory?.items?.[0] === 'weapon.sword'
|
|
1423
|
+
&& inheritedComponents.Inventory.tags?.join(',') === 'player,spawned',
|
|
1424
|
+
mapSpawnMaterializationCovered: mapBake.materializedEntityCount === 4
|
|
1425
|
+
&& mapBake.prototypeIds.includes('proto.player')
|
|
1426
|
+
&& mapBake.materializedEntities[0]?.entityId === 100,
|
|
1427
|
+
mapRebakeInvalidationCovered: mapBake.mapBakeHash === mapBakeRepeat.mapBakeHash && mapBake.mapBakeHash !== mapRebake.mapBakeHash,
|
|
1428
|
+
};
|
|
1429
|
+
return categoryReport(config, 'asset-cooking', 'required', [
|
|
1430
|
+
'Asset DB',
|
|
1431
|
+
'Map baking/cooking',
|
|
1432
|
+
'Prototype materialization',
|
|
1433
|
+
'Fail-closed replay/load identity',
|
|
1434
|
+
], invariants, {
|
|
1435
|
+
assetDbHash: first.manifest.assetDbHash,
|
|
1436
|
+
lookupHash: first.manifest.lookupHash,
|
|
1437
|
+
schemaHash: first.manifest.schemaHash,
|
|
1438
|
+
dependencyHash: first.manifest.dependencyHash,
|
|
1439
|
+
cookedBytes: first.bytes.length,
|
|
1440
|
+
spawnedEntities: prototypeA.length,
|
|
1441
|
+
runtimeValidatedPrototypeEntities: runtimeValidation.materializedEntityCount,
|
|
1442
|
+
runtimeInjectedPrototypeComponents: runtimeValidation.injectedComponentCount,
|
|
1443
|
+
runtimePrototypeValidationChecksum: runtimeValidation.checksum,
|
|
1444
|
+
inheritedPrototypeEntities: inheritedPrototype.length,
|
|
1445
|
+
mapMaterializedEntityCount: mapBake.materializedEntityCount,
|
|
1446
|
+
mapSpawnCount: mapBake.spawnCount,
|
|
1447
|
+
mapBakeHash: mapBake.mapBakeHash,
|
|
1448
|
+
rebakedMapBakeHash: mapRebake.mapBakeHash,
|
|
1449
|
+
});
|
|
1450
|
+
}
|
|
1451
|
+
function runConfigBundleCategory(config) {
|
|
1452
|
+
const metrics = runDeterministicConfigBundleParityFixture();
|
|
1453
|
+
const invariants = {
|
|
1454
|
+
configBundlePasses: metrics.result === 'pass',
|
|
1455
|
+
identityHashesPresent: metrics.configHash.length > 0 && metrics.assetDbHash.length > 0,
|
|
1456
|
+
byteStable: metrics.byteStable,
|
|
1457
|
+
migrationCovered: metrics.migrationResult === 'migrated' && metrics.version === 1 && metrics.targetVersion === 2,
|
|
1458
|
+
prototypeMaterializationCovered: metrics.prototypeCount > 0
|
|
1459
|
+
&& metrics.materializedEntityCount > 0
|
|
1460
|
+
&& metrics.prototypeMaterializationStable,
|
|
1461
|
+
staleIdentityRejected: metrics.staleEngineRejected
|
|
1462
|
+
&& metrics.staleSchemaRejected
|
|
1463
|
+
&& metrics.staleConfigRejected
|
|
1464
|
+
&& metrics.staleAssetDbRejected,
|
|
1465
|
+
tamperRejected: metrics.tamperedConfigBytesRejected && metrics.tamperedAssetBytesRejected,
|
|
1466
|
+
unsupportedFloatsRejected: metrics.unsupportedFloatRejected,
|
|
1467
|
+
inheritedPrototypeMaterialized: metrics.inheritedPrototypeMaterialized,
|
|
1468
|
+
dynamicCollectionsMaterialized: metrics.dynamicCollectionsMaterialized,
|
|
1469
|
+
mapMaterializationStable: metrics.mapMaterializationStable,
|
|
1470
|
+
mapRebakeInvalidated: metrics.mapRebakeInvalidated,
|
|
1471
|
+
};
|
|
1472
|
+
return categoryReport(config, 'config-bundle', 'required', [
|
|
1473
|
+
'Config identity',
|
|
1474
|
+
'Migration',
|
|
1475
|
+
'Prototype materialization',
|
|
1476
|
+
'Stale and tampered config rejection',
|
|
1477
|
+
], invariants, {
|
|
1478
|
+
proofContract: 'config-bundle@1',
|
|
1479
|
+
requestedFrames: 1,
|
|
1480
|
+
executedFrames: 1,
|
|
1481
|
+
version: metrics.version,
|
|
1482
|
+
targetVersion: metrics.targetVersion,
|
|
1483
|
+
configHash: metrics.configHash,
|
|
1484
|
+
assetDbHash: metrics.assetDbHash,
|
|
1485
|
+
prototypeCount: metrics.prototypeCount,
|
|
1486
|
+
materializedEntityCount: metrics.materializedEntityCount,
|
|
1487
|
+
migrationResult: metrics.migrationResult,
|
|
1488
|
+
byteStable: metrics.byteStable,
|
|
1489
|
+
staleEngineRejected: metrics.staleEngineRejected,
|
|
1490
|
+
staleSchemaRejected: metrics.staleSchemaRejected,
|
|
1491
|
+
staleConfigRejected: metrics.staleConfigRejected,
|
|
1492
|
+
staleAssetDbRejected: metrics.staleAssetDbRejected,
|
|
1493
|
+
tamperedConfigBytesRejected: metrics.tamperedConfigBytesRejected,
|
|
1494
|
+
tamperedAssetBytesRejected: metrics.tamperedAssetBytesRejected,
|
|
1495
|
+
unsupportedFloatRejected: metrics.unsupportedFloatRejected,
|
|
1496
|
+
prototypeMaterializationStable: metrics.prototypeMaterializationStable,
|
|
1497
|
+
inheritedPrototypeMaterialized: metrics.inheritedPrototypeMaterialized,
|
|
1498
|
+
dynamicCollectionsMaterialized: metrics.dynamicCollectionsMaterialized,
|
|
1499
|
+
mapMaterializationStable: metrics.mapMaterializationStable,
|
|
1500
|
+
mapRebakeInvalidated: metrics.mapRebakeInvalidated,
|
|
1501
|
+
mapMaterializedEntityCount: metrics.mapMaterializedEntityCount,
|
|
1502
|
+
mapBakeHash: metrics.mapBakeHash,
|
|
1503
|
+
failureReasons: { ...metrics.failureReasons },
|
|
1504
|
+
checksum: metrics.checksum,
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
function runProtocolCategory(config) {
|
|
1508
|
+
const playerCounts = [2, 8, 32, 64, 128];
|
|
1509
|
+
const startedAt = performance.now();
|
|
1510
|
+
const runs = playerCounts.map((players, index) => runProtocolRollbackLab({
|
|
1511
|
+
game: index % 2 === 0 ? 'command-bursts' : 'moderate-state',
|
|
1512
|
+
players,
|
|
1513
|
+
frames: Math.max(60, Math.min(config.frames, 240)),
|
|
1514
|
+
seed: config.seed + index,
|
|
1515
|
+
minDelayFrames: 1,
|
|
1516
|
+
maxDelayFrames: 8,
|
|
1517
|
+
duplicatePermille: 150,
|
|
1518
|
+
lossPermille: 30,
|
|
1519
|
+
resendIntervalFrames: 9,
|
|
1520
|
+
maxResendAttempts: 12,
|
|
1521
|
+
disconnectStartFrame: 20,
|
|
1522
|
+
disconnectEndFrame: 32,
|
|
1523
|
+
lateJoinSlot: players - 1,
|
|
1524
|
+
lateJoinFrame: 30,
|
|
1525
|
+
eventTimelineLimit: 4,
|
|
1526
|
+
predictionPolicy: 'repeat-last',
|
|
1527
|
+
}));
|
|
1528
|
+
const elapsedMs = performance.now() - startedAt;
|
|
1529
|
+
const delaySamples = runs.flatMap((run) => run.deliveryDelaySamples);
|
|
1530
|
+
const rollbackDepthSamples = runs.flatMap((run) => run.rollbackDepthSamples);
|
|
1531
|
+
const rejectedCommandCounts = runs.map((run) => run.canceledRetries + run.absentInputFrames + run.commandRejected);
|
|
1532
|
+
const realCommandRejected = runs.reduce((sum, run) => sum + run.commandRejected, 0);
|
|
1533
|
+
const commandRuns = runs.filter((run) => run.game === 'command-bursts');
|
|
1534
|
+
const spectatorJoinFrame = 45;
|
|
1535
|
+
const spectatorRun = runProtocolRollbackLab({
|
|
1536
|
+
game: 'command-bursts',
|
|
1537
|
+
players: 128,
|
|
1538
|
+
frames: Math.max(90, Math.min(config.frames, 240)),
|
|
1539
|
+
seed: config.seed + 171,
|
|
1540
|
+
minDelayFrames: 1,
|
|
1541
|
+
maxDelayFrames: 8,
|
|
1542
|
+
duplicatePermille: 100,
|
|
1543
|
+
lossPermille: 20,
|
|
1544
|
+
resendIntervalFrames: 9,
|
|
1545
|
+
maxResendAttempts: 12,
|
|
1546
|
+
spectatorSlot: 126,
|
|
1547
|
+
spectatorJoinFrame,
|
|
1548
|
+
eventTimelineLimit: 2,
|
|
1549
|
+
predictionPolicy: 'repeat-last',
|
|
1550
|
+
});
|
|
1551
|
+
const spectatorExpectedActiveFrames = Math.max(0, spectatorRun.frames - spectatorJoinFrame);
|
|
1552
|
+
const adaptiveInputDelayBaseline = runProtocolRollbackLab({
|
|
1553
|
+
game: 'moderate-state',
|
|
1554
|
+
players: 32,
|
|
1555
|
+
frames: Math.max(120, Math.min(config.frames, 240)),
|
|
1556
|
+
seed: config.seed + 211,
|
|
1557
|
+
minDelayFrames: 1,
|
|
1558
|
+
maxDelayFrames: 8,
|
|
1559
|
+
duplicatePermille: 150,
|
|
1560
|
+
lossPermille: 30,
|
|
1561
|
+
resendIntervalFrames: 9,
|
|
1562
|
+
maxResendAttempts: 12,
|
|
1563
|
+
predictionPolicy: 'repeat-last',
|
|
1564
|
+
});
|
|
1565
|
+
const adaptiveInputDelayRun = runProtocolRollbackLab({
|
|
1566
|
+
game: 'moderate-state',
|
|
1567
|
+
players: 32,
|
|
1568
|
+
frames: Math.max(120, Math.min(config.frames, 240)),
|
|
1569
|
+
seed: config.seed + 211,
|
|
1570
|
+
minDelayFrames: 1,
|
|
1571
|
+
maxDelayFrames: 8,
|
|
1572
|
+
duplicatePermille: 150,
|
|
1573
|
+
lossPermille: 30,
|
|
1574
|
+
resendIntervalFrames: 9,
|
|
1575
|
+
maxResendAttempts: 12,
|
|
1576
|
+
predictionPolicy: 'repeat-last',
|
|
1577
|
+
adaptiveInputDelay: true,
|
|
1578
|
+
maxAdaptiveInputDelayFrames: 8,
|
|
1579
|
+
});
|
|
1580
|
+
const inputTimeline = runDeterministicInputTimelineFixture({
|
|
1581
|
+
players: 128,
|
|
1582
|
+
frames: Math.max(120, Math.min(config.frames, 1_000)),
|
|
1583
|
+
seed: config.seed + 233,
|
|
1584
|
+
sampleLimit: 8,
|
|
1585
|
+
});
|
|
1586
|
+
const commandTimelineMigration = runDeterministicCommandTimelineMigrationFixture(128);
|
|
1587
|
+
const serverMigration = simulateServerMigration(runs);
|
|
1588
|
+
const invariants = {
|
|
1589
|
+
allPlayerCountsCovered: runs.length === playerCounts.length,
|
|
1590
|
+
realInputBundlesDelivered: runs.every((run) => run.bundledInputMessages > 0),
|
|
1591
|
+
delayJitterDropDupReorderCovered: runs.every((run) => run.deliveries > 0 && run.duplicates >= 0 && run.lost >= 0),
|
|
1592
|
+
reconnectLateJoinCovered: runs.every((run) => run.disconnectDrops > 0),
|
|
1593
|
+
checksumConvergence: runs.every((run) => run.authorityChecksum === run.clientChecksum && run.result === 'pass'),
|
|
1594
|
+
commandBurstsCovered: commandRuns.length > 0,
|
|
1595
|
+
commandChannelsDelivered: commandRuns.every((run) => run.commandDeliveries > 0),
|
|
1596
|
+
commandTransportLossRecovered: commandRuns.some((run) => run.commandLost > 0)
|
|
1597
|
+
&& commandRuns.every((run) => run.commandAuthorityChecksum === run.commandClientChecksum),
|
|
1598
|
+
commandChannelsRejectedHostileTraffic: commandRuns.every((run) => run.commandRejected > 0 && run.commandDuplicateDrops > 0),
|
|
1599
|
+
commandChannelRollbackConvergence: commandRuns.every((run) => run.commandAuthorityChecksum === run.commandClientChecksum)
|
|
1600
|
+
&& commandRuns.some((run) => run.players === 128 && run.commandRollbackConverged),
|
|
1601
|
+
commandChannelRollbackObserved: commandRuns.some((run) => run.players === 128 && run.commandRollbackObserved),
|
|
1602
|
+
protocolSpectatorReconstruction: spectatorRun.spectatorReconstructions === 1
|
|
1603
|
+
&& spectatorRun.spectatorReconstructionChecksum === spectatorRun.authorityChecksum
|
|
1604
|
+
&& spectatorRun.spectatorInputFrames === spectatorExpectedActiveFrames
|
|
1605
|
+
&& spectatorRun.spectatorCommandRejected > 0,
|
|
1606
|
+
repeatLastPredictionFallbackCovered: runs.every((run) => run.predictionPolicy === 'repeat-last' && run.repeatLastPredictions > 0),
|
|
1607
|
+
repeatLastNonDefaultPredictionCovered: runs.some((run) => run.repeatLastNonDefaultPredictions > 0),
|
|
1608
|
+
predictionFallbackChecksumReported: runs.every((run) => run.predictionFallbackChecksum.length > 0),
|
|
1609
|
+
lateJoinSnapshotTailReconstruction: runs.every((run) => run.lateJoinReconstructions === 1 && run.lateJoinReconstructionChecksum === run.authorityChecksum && run.staleJoinRejected),
|
|
1610
|
+
delayPercentilesReported: percentile(delaySamples, 50) > 0 && percentile(delaySamples, 95) >= percentile(delaySamples, 50) && percentile(delaySamples, 99) >= percentile(delaySamples, 95),
|
|
1611
|
+
adaptiveInputDelayCovered: adaptiveInputDelayRun.result === 'pass'
|
|
1612
|
+
&& adaptiveInputDelayRun.adaptiveInputDelay
|
|
1613
|
+
&& adaptiveInputDelayRun.inputDelayAdjustments > 0
|
|
1614
|
+
&& adaptiveInputDelayRun.finalInputDelayFrames > 0
|
|
1615
|
+
&& adaptiveInputDelayRun.maxObservedInputDelayFrames <= 8
|
|
1616
|
+
&& adaptiveInputDelayRun.rollbacks < adaptiveInputDelayBaseline.rollbacks
|
|
1617
|
+
&& adaptiveInputDelayRun.localInputRollbacks < adaptiveInputDelayBaseline.localInputRollbacks,
|
|
1618
|
+
inputTimelineArtifactCovered: inputTimeline.result === 'pass'
|
|
1619
|
+
&& inputTimeline.players === 128
|
|
1620
|
+
&& inputTimeline.rowCount === inputTimeline.players * inputTimeline.frames
|
|
1621
|
+
&& inputTimeline.replayVerified
|
|
1622
|
+
&& inputTimeline.rowHash.length > 0,
|
|
1623
|
+
inputTimelinePredictionPoliciesCovered: inputTimeline.invariants.explicitPredictionCovered
|
|
1624
|
+
&& inputTimeline.invariants.neutralPredictionCovered
|
|
1625
|
+
&& inputTimeline.invariants.repeatLastPredictionCovered,
|
|
1626
|
+
inputTimelineLifecyclePoliciesCovered: inputTimeline.invariants.confirmedCovered
|
|
1627
|
+
&& inputTimeline.invariants.replacementCovered
|
|
1628
|
+
&& inputTimeline.invariants.absentCovered
|
|
1629
|
+
&& inputTimeline.invariants.botCovered
|
|
1630
|
+
&& inputTimeline.invariants.lateJoinCovered,
|
|
1631
|
+
inputTimelineCorrectionPoliciesCovered: inputTimeline.invariants.lateFillCovered
|
|
1632
|
+
&& inputTimeline.invariants.rollbackTriggeredCovered,
|
|
1633
|
+
inputTimelineIngressRejectionsCovered: inputTimeline.invariants.inactivePredictionRejected
|
|
1634
|
+
&& inputTimeline.invariants.malformedValueRejected
|
|
1635
|
+
&& inputTimeline.invariants.duplicateIngressRejected
|
|
1636
|
+
&& inputTimeline.invariants.duplicateAfterRestoreRejected
|
|
1637
|
+
&& inputTimeline.invariants.futureIngressRejected
|
|
1638
|
+
&& inputTimeline.invariants.staleIngressRejected
|
|
1639
|
+
&& inputTimeline.invariants.spectatorIngressRejected
|
|
1640
|
+
&& inputTimeline.invariants.badSenderRejected,
|
|
1641
|
+
commandTimelineMigrationCovered: commandTimelineMigration.result === 'pass'
|
|
1642
|
+
&& commandTimelineMigration.acceptedBeforeMigration > 0
|
|
1643
|
+
&& commandTimelineMigration.acceptedAfterMigration > commandTimelineMigration.acceptedBeforeMigration
|
|
1644
|
+
&& commandTimelineMigration.snapshotChecksum === commandTimelineMigration.restoredChecksum
|
|
1645
|
+
&& commandTimelineMigration.pollOrderStable
|
|
1646
|
+
&& commandTimelineMigration.acceptedCommandIdsRestored
|
|
1647
|
+
&& commandTimelineMigration.currentFrameRestored
|
|
1648
|
+
&& commandTimelineMigration.newCommandAcceptedAfterRestore,
|
|
1649
|
+
commandTimelineRejectionsAfterMigrationCovered: commandTimelineMigration.duplicateAfterRestoreRejected
|
|
1650
|
+
&& commandTimelineMigration.staleAfterRestoreRejected
|
|
1651
|
+
&& commandTimelineMigration.futureAfterRestoreRejected
|
|
1652
|
+
&& commandTimelineMigration.unauthorizedAfterRestoreRejected
|
|
1653
|
+
&& commandTimelineMigration.oversizedAfterRestoreRejected
|
|
1654
|
+
&& commandTimelineMigration.badSchemaAfterRestoreRejected
|
|
1655
|
+
&& commandTimelineMigration.badAssetRefAfterRestoreRejected,
|
|
1656
|
+
rollbackDistributionReported: rollbackDepthSamples.length > 0,
|
|
1657
|
+
rejectedCommandCountsReported: realCommandRejected > 0 && commandRuns.every((run) => run.commandRejected > 0),
|
|
1658
|
+
serverMigrationSimulated: serverMigration.authorityChecksum === serverMigration.migratedAuthorityChecksum,
|
|
1659
|
+
};
|
|
1660
|
+
return categoryReport(config, 'protocol', 'required', [
|
|
1661
|
+
'Input bundles',
|
|
1662
|
+
'Delay/jitter/loss/duplicate/reorder',
|
|
1663
|
+
'Reconnect',
|
|
1664
|
+
'Late join',
|
|
1665
|
+
'Adaptive input delay',
|
|
1666
|
+
'Input timeline transcript',
|
|
1667
|
+
'Command timeline migration',
|
|
1668
|
+
'Command bursts',
|
|
1669
|
+
'128 players',
|
|
1670
|
+
], invariants, {
|
|
1671
|
+
playerCounts: playerCounts.join(','),
|
|
1672
|
+
totalDeliveries: runs.reduce((sum, run) => sum + run.deliveries, 0),
|
|
1673
|
+
totalDroppedMessages: runs.reduce((sum, run) => sum + run.lost, 0),
|
|
1674
|
+
totalDuplicatedMessages: runs.reduce((sum, run) => sum + run.duplicates, 0),
|
|
1675
|
+
totalRejectedCommands: rejectedCommandCounts.reduce((sum, count) => sum + count, 0),
|
|
1676
|
+
totalCommandDeliveries: runs.reduce((sum, run) => sum + run.commandDeliveries, 0),
|
|
1677
|
+
totalCommandLost: runs.reduce((sum, run) => sum + run.commandLost, 0),
|
|
1678
|
+
totalCommandDuplicatedMessages: runs.reduce((sum, run) => sum + run.commandDuplicates, 0),
|
|
1679
|
+
totalCommandReorderedMessages: runs.reduce((sum, run) => sum + run.commandReorders, 0),
|
|
1680
|
+
totalCommandDisconnectDrops: runs.reduce((sum, run) => sum + run.commandDisconnectDrops, 0),
|
|
1681
|
+
totalCommandRejected: runs.reduce((sum, run) => sum + run.commandRejected, 0),
|
|
1682
|
+
pollutedLegacyRejectedCommandTelemetry: rejectedCommandCounts.reduce((sum, count) => sum + count, 0),
|
|
1683
|
+
commandRollbackObservedRuns: commandRuns.filter((run) => run.commandRollbackObserved).length,
|
|
1684
|
+
spectatorReconstructions: spectatorRun.spectatorReconstructions,
|
|
1685
|
+
spectatorInputFrames: spectatorRun.spectatorInputFrames,
|
|
1686
|
+
spectatorExpectedActiveFrames,
|
|
1687
|
+
spectatorCommandRejected: spectatorRun.spectatorCommandRejected,
|
|
1688
|
+
spectatorReconstructionChecksum: spectatorRun.spectatorReconstructionChecksum,
|
|
1689
|
+
commandReplayChecksums: commandRuns.map((run) => run.commandReplayChecksum).join(','),
|
|
1690
|
+
predictionPolicy: 'repeat-last',
|
|
1691
|
+
predictedInputFrames: runs.reduce((sum, run) => sum + run.predictedInputFrames, 0),
|
|
1692
|
+
repeatLastPredictions: runs.reduce((sum, run) => sum + run.repeatLastPredictions, 0),
|
|
1693
|
+
repeatLastNonDefaultPredictions: runs.reduce((sum, run) => sum + run.repeatLastNonDefaultPredictions, 0),
|
|
1694
|
+
neutralPredictions: runs.reduce((sum, run) => sum + run.neutralPredictions, 0),
|
|
1695
|
+
predictionFallbackChecksum: defaultChecksum(runs.map((run) => run.predictionFallbackChecksum)),
|
|
1696
|
+
lateJoinReconstructions: runs.reduce((sum, run) => sum + run.lateJoinReconstructions, 0),
|
|
1697
|
+
inputDelayP50: percentile(delaySamples, 50),
|
|
1698
|
+
inputDelayP95: percentile(delaySamples, 95),
|
|
1699
|
+
inputDelayP99: percentile(delaySamples, 99),
|
|
1700
|
+
adaptiveInputDelayBaselineRollbacks: adaptiveInputDelayBaseline.rollbacks,
|
|
1701
|
+
adaptiveInputDelayRollbacks: adaptiveInputDelayRun.rollbacks,
|
|
1702
|
+
adaptiveInputDelayBaselineLocalRollbacks: adaptiveInputDelayBaseline.localInputRollbacks,
|
|
1703
|
+
adaptiveInputDelayLocalRollbacks: adaptiveInputDelayRun.localInputRollbacks,
|
|
1704
|
+
adaptiveInputDelayFinalFrames: adaptiveInputDelayRun.finalInputDelayFrames,
|
|
1705
|
+
adaptiveInputDelayMaxObservedFrames: adaptiveInputDelayRun.maxObservedInputDelayFrames,
|
|
1706
|
+
adaptiveInputDelayAdjustments: adaptiveInputDelayRun.inputDelayAdjustments,
|
|
1707
|
+
inputTimelineRows: inputTimeline.rowCount,
|
|
1708
|
+
inputTimelineHash: inputTimeline.rowHash,
|
|
1709
|
+
inputTimelineReplayChecksum: inputTimeline.replayChecksum,
|
|
1710
|
+
inputTimelineSampleRows: inputTimeline.sampledRows.map((row) => ({ ...row })),
|
|
1711
|
+
inputTimelineCounts: { ...inputTimeline.counts },
|
|
1712
|
+
inputTimelineNegativeCases: { ...inputTimeline.negativeCases },
|
|
1713
|
+
commandTimelineMigrationChecksum: commandTimelineMigration.checksum,
|
|
1714
|
+
commandTimelineMigrationFinalChecksum: commandTimelineMigration.finalChecksum,
|
|
1715
|
+
commandTimelineMigrationAcceptedBefore: commandTimelineMigration.acceptedBeforeMigration,
|
|
1716
|
+
commandTimelineMigrationAcceptedAfter: commandTimelineMigration.acceptedAfterMigration,
|
|
1717
|
+
commandTimelineMigrationRejections: { ...commandTimelineMigration.rejectionCounts },
|
|
1718
|
+
totalRollbacks: runs.reduce((sum, run) => sum + run.rollbacks, 0),
|
|
1719
|
+
maxRollbackDepth: Math.max(...runs.map((run) => run.maxRollbackDepth)),
|
|
1720
|
+
rollbackDepthDistribution: rollbackDistribution(rollbackDepthSamples),
|
|
1721
|
+
serverMigrationFrame: serverMigration.frame,
|
|
1722
|
+
migratedAuthorityChecksum: serverMigration.migratedAuthorityChecksum,
|
|
1723
|
+
elapsedMs: elapsedMs.toFixed(2),
|
|
1724
|
+
checksum: defaultChecksum(runs.map((run) => run.authorityChecksum)),
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
function runReplayDebuggingCategory(config) {
|
|
1728
|
+
const session = createLabSession({ players: 4, seed: config.seed, replayRecordingMode: 'memory' });
|
|
1729
|
+
const random = new DeterministicRandom(config.seed);
|
|
1730
|
+
for (let frame = 0; frame < Math.min(config.frames, 180); frame += 1) {
|
|
1731
|
+
applyLabInputs(session, 4, random);
|
|
1732
|
+
if (frame % 17 === 0) {
|
|
1733
|
+
session.recordCommands([{
|
|
1734
|
+
kind: frame % 34 === 0 ? 'spawn' : 'emote',
|
|
1735
|
+
slot: frame % 4,
|
|
1736
|
+
value: random.nextInt(0, 1_000),
|
|
1737
|
+
}]);
|
|
1738
|
+
}
|
|
1739
|
+
session.stepFrames(1);
|
|
1740
|
+
}
|
|
1741
|
+
const replay = session.exportReplay();
|
|
1742
|
+
const valid = verifyReplay({
|
|
1743
|
+
replay,
|
|
1744
|
+
step: labStep,
|
|
1745
|
+
defaultInput: labDefaultInput,
|
|
1746
|
+
inputSchemaId: 'lab-input/v1',
|
|
1747
|
+
stateSchemaId: 'lab-state/v1',
|
|
1748
|
+
deterministicVersion: '0.0.0-source',
|
|
1749
|
+
hydrateInput: hydrateLabInput,
|
|
1750
|
+
throwOnMismatch: false,
|
|
1751
|
+
});
|
|
1752
|
+
const replayClone = JSON.parse(JSON.stringify(replay));
|
|
1753
|
+
const corrupted = {
|
|
1754
|
+
...replayClone,
|
|
1755
|
+
checksums: replayClone.checksums.map((checksum, index) => index === Math.floor(replayClone.checksums.length / 2)
|
|
1756
|
+
? { ...checksum, checksum: '0000000000000000' }
|
|
1757
|
+
: checksum),
|
|
1758
|
+
};
|
|
1759
|
+
const invalid = verifyReplay({
|
|
1760
|
+
replay: corrupted,
|
|
1761
|
+
step: labStep,
|
|
1762
|
+
defaultInput: labDefaultInput,
|
|
1763
|
+
inputSchemaId: 'lab-input/v1',
|
|
1764
|
+
stateSchemaId: 'lab-state/v1',
|
|
1765
|
+
deterministicVersion: '0.0.0-source',
|
|
1766
|
+
hydrateInput: hydrateLabInput,
|
|
1767
|
+
throwOnMismatch: false,
|
|
1768
|
+
});
|
|
1769
|
+
const slice = replay.inputFrames.slice(Math.max(0, replay.inputFrames.length - 10));
|
|
1770
|
+
const explainedFrame = Math.max(0, Math.floor(replay.inputFrames.length / 2));
|
|
1771
|
+
const explained = inspectReplayFrame({
|
|
1772
|
+
replay,
|
|
1773
|
+
step: labStep,
|
|
1774
|
+
defaultInput: labDefaultInput,
|
|
1775
|
+
inputSchemaId: 'lab-input/v1',
|
|
1776
|
+
stateSchemaId: 'lab-state/v1',
|
|
1777
|
+
deterministicVersion: '0.0.0-source',
|
|
1778
|
+
hydrateInput: hydrateLabInput,
|
|
1779
|
+
}, explainedFrame, { explainChecksum: true });
|
|
1780
|
+
/* istanbul ignore next -- checksum explanation is explicitly requested above and must be present. */
|
|
1781
|
+
const explanationTables = explained.checksumExplanation?.tables ?? [];
|
|
1782
|
+
const minimization = createReplayDesyncMinimizationProof();
|
|
1783
|
+
const inputOnlyReplayBundle = createInputOnlyReplayBundleProof();
|
|
1784
|
+
const invariants = {
|
|
1785
|
+
validReplayVerified: valid.mismatches === 0,
|
|
1786
|
+
corruptedReplayFails: invalid.mismatches > 0,
|
|
1787
|
+
frameRangeReplay: slice.length > 0,
|
|
1788
|
+
firstMismatchBinarySearch: invalid.firstMismatch !== undefined && minimization.searchSteps > 0,
|
|
1789
|
+
replayBundleDesyncMinimized: minimization.verifierConsumable
|
|
1790
|
+
&& minimization.result === 'fail'
|
|
1791
|
+
&& minimization.firstMismatchFrame === minimization.mismatchFrame
|
|
1792
|
+
&& minimization.minimizedReplayBytes < minimization.originalReplayBytes,
|
|
1793
|
+
replayBundleMinimizedArtifactReduced: minimization.removedInputFrames > 0
|
|
1794
|
+
&& minimization.removedCommandFrames > 0
|
|
1795
|
+
&& minimization.removedSnapshots > 0
|
|
1796
|
+
&& minimization.removedChecksumFrames > 0,
|
|
1797
|
+
replaySlicing: defaultChecksum(JSON.stringify(slice)).length > 0,
|
|
1798
|
+
spectatorReconstruction: replay.checksums.at(-1)?.checksum === valid.finalChecksum,
|
|
1799
|
+
killcamRewind: slice[0]?.frame !== undefined && slice[0].frame < replay.inputFrames.at(-1).frame,
|
|
1800
|
+
checksumExplainMode: explained.checksumExplanation !== undefined
|
|
1801
|
+
&& explanationTables.length > 0
|
|
1802
|
+
&& explanationTables.every((table) => table.path.startsWith('state.') && table.checksum.length > 0),
|
|
1803
|
+
replayIdentitySections: replay.engineIdentityHash !== undefined
|
|
1804
|
+
&& replay.assetDbHash !== undefined
|
|
1805
|
+
&& replay.configHash !== undefined
|
|
1806
|
+
&& replay.metadata !== undefined
|
|
1807
|
+
&& replay.commandHistory !== undefined
|
|
1808
|
+
&& replay.commandHistory.length > 0
|
|
1809
|
+
&& replay.debugEvents !== undefined,
|
|
1810
|
+
commandHistoryReplayable: replay.commandHistory !== undefined
|
|
1811
|
+
&& replay.commandHistory.length > 0
|
|
1812
|
+
&& replay.commandHistory.every((commandFrame) => commandFrame.commands.length > 0),
|
|
1813
|
+
inputOnlyReplayBundleVerified: inputOnlyReplayBundle.result === 'pass'
|
|
1814
|
+
&& inputOnlyReplayBundle.commandCount === 0
|
|
1815
|
+
&& inputOnlyReplayBundle.inputCount > 0
|
|
1816
|
+
&& inputOnlyReplayBundle.semanticReplayExecuted,
|
|
1817
|
+
};
|
|
1818
|
+
return categoryReport(config, 'replay-debugging', 'required', [
|
|
1819
|
+
'Replay package',
|
|
1820
|
+
'Checksum verification',
|
|
1821
|
+
'First mismatch',
|
|
1822
|
+
'Replay slicing',
|
|
1823
|
+
'Late spectator reconstruction',
|
|
1824
|
+
'Killcam rewind',
|
|
1825
|
+
'Checksum explain tables',
|
|
1826
|
+
], invariants, {
|
|
1827
|
+
replayFrames: replay.inputFrames.length,
|
|
1828
|
+
checksums: replay.checksums.length,
|
|
1829
|
+
validFinalChecksum: valid.finalChecksum,
|
|
1830
|
+
corruptedMismatchFrame: numberOrFallback(invalid.firstMismatch?.frame, -1),
|
|
1831
|
+
desyncMinimizationSearchSteps: minimization.searchSteps,
|
|
1832
|
+
desyncMinimizationMismatchFrame: minimization.mismatchFrame,
|
|
1833
|
+
desyncMinimizationVerifierFrame: minimization.firstMismatchFrame,
|
|
1834
|
+
desyncMinimizationVerifierResult: minimization.result,
|
|
1835
|
+
desyncMinimizationVerifierConsumable: minimization.verifierConsumable,
|
|
1836
|
+
desyncMinimizationOriginalBytes: minimization.originalReplayBytes,
|
|
1837
|
+
desyncMinimizationMinimizedBytes: minimization.minimizedReplayBytes,
|
|
1838
|
+
desyncMinimizationRemovedInputFrames: minimization.removedInputFrames,
|
|
1839
|
+
desyncMinimizationRemovedCommandFrames: minimization.removedCommandFrames,
|
|
1840
|
+
desyncMinimizationRemovedSnapshots: minimization.removedSnapshots,
|
|
1841
|
+
desyncMinimizationRemovedChecksumFrames: minimization.removedChecksumFrames,
|
|
1842
|
+
checksumExplainFrame: explained.frame,
|
|
1843
|
+
checksumExplainTables: explanationTables.length,
|
|
1844
|
+
engineIdentityHash: stringOrFallback(replay.engineIdentityHash, ''),
|
|
1845
|
+
assetDbHash: stringOrFallback(replay.assetDbHash, ''),
|
|
1846
|
+
configHash: stringOrFallback(replay.configHash, ''),
|
|
1847
|
+
commandHistoryFrames: readonlyArrayOrEmpty(replay.commandHistory).length,
|
|
1848
|
+
commandHistoryCommands: readonlyArrayOrEmpty(replay.commandHistory).reduce((sum, commandFrame) => sum + commandFrame.commands.length, 0),
|
|
1849
|
+
inputOnlyBundleInputs: inputOnlyReplayBundle.inputCount,
|
|
1850
|
+
inputOnlyBundleCommands: inputOnlyReplayBundle.commandCount,
|
|
1851
|
+
inputOnlyBundleReplayBytes: inputOnlyReplayBundle.replayBytes,
|
|
1852
|
+
debugEvents: readonlyArrayOrEmpty(replay.debugEvents).length,
|
|
1853
|
+
replayBytes: JSON.stringify(replay).length,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
function createInputOnlyReplayBundleProof() {
|
|
1857
|
+
let position = 0;
|
|
1858
|
+
const snapshots = [0, 1, 2, 3].map((frame) => {
|
|
1859
|
+
position += frame;
|
|
1860
|
+
const bytes = canonicalStringify({ frame, positions: [position], commandScore: 0 });
|
|
1861
|
+
return { frame, bytes, checksum: defaultChecksum(bytes) };
|
|
1862
|
+
});
|
|
1863
|
+
const bundle = createDeterministicReplayBundle({
|
|
1864
|
+
runtimeVersion: 'parity-replay@1',
|
|
1865
|
+
engineHash: 'engine',
|
|
1866
|
+
schemaHash: 'schema',
|
|
1867
|
+
assetDbHash: 'asset',
|
|
1868
|
+
configHash: 'config',
|
|
1869
|
+
sampleId: 'input-only-replay',
|
|
1870
|
+
inputHistory: snapshots.map((snapshot) => ({ frame: snapshot.frame, inputs: [{ slot: 0, dx: snapshot.frame, dy: 0 }] })),
|
|
1871
|
+
commandHistory: [],
|
|
1872
|
+
checksums: snapshots.map((snapshot) => ({ frame: snapshot.frame, checksum: snapshot.checksum })),
|
|
1873
|
+
debugEvents: snapshots.map((snapshot) => ({ frame: snapshot.frame, type: 'ecs.snapshot', payload: { frame: snapshot.frame } })),
|
|
1874
|
+
snapshots,
|
|
1875
|
+
metadata: { players: 1, replayKind: 'input-only' },
|
|
1876
|
+
});
|
|
1877
|
+
return verifyDeterministicReplayBundle(bundle);
|
|
1878
|
+
}
|
|
1879
|
+
function createReplayDesyncMinimizationProof() {
|
|
1880
|
+
let position = 0;
|
|
1881
|
+
let commandScore = 0;
|
|
1882
|
+
const snapshots = Array.from({ length: 13 }, (_, index) => index * 2).map((frame) => {
|
|
1883
|
+
position += frame;
|
|
1884
|
+
commandScore += frame % 3;
|
|
1885
|
+
const bytes = canonicalStringify({ frame, positions: [position], commandScore });
|
|
1886
|
+
return { frame, bytes, checksum: defaultChecksum(bytes) };
|
|
1887
|
+
});
|
|
1888
|
+
const expected = createDeterministicReplayBundle({
|
|
1889
|
+
runtimeVersion: 'parity-replay@1',
|
|
1890
|
+
engineHash: 'engine',
|
|
1891
|
+
schemaHash: 'schema',
|
|
1892
|
+
assetDbHash: 'asset',
|
|
1893
|
+
configHash: 'config',
|
|
1894
|
+
sampleId: 'replay-minimization',
|
|
1895
|
+
inputHistory: snapshots.map((snapshot) => ({ frame: snapshot.frame, inputs: [{ slot: 0, dx: snapshot.frame, dy: 0 }] })),
|
|
1896
|
+
commandHistory: snapshots.map((snapshot) => ({ frame: snapshot.frame, commands: [{ id: `cmd:${snapshot.frame}`, payload: { power: snapshot.frame % 3 } }] })),
|
|
1897
|
+
checksums: snapshots.map((snapshot) => ({ frame: snapshot.frame, checksum: snapshot.checksum })),
|
|
1898
|
+
debugEvents: snapshots.map((snapshot) => ({ frame: snapshot.frame, type: 'ecs.snapshot', payload: { frame: snapshot.frame } })),
|
|
1899
|
+
snapshots,
|
|
1900
|
+
metadata: { players: 1, checkpointBundle: true, checkpointStride: 2 },
|
|
1901
|
+
});
|
|
1902
|
+
const actual = {
|
|
1903
|
+
...expected,
|
|
1904
|
+
checksums: expected.checksums.map((checksum) => checksum.frame >= 12 ? { ...checksum, checksum: `corrupt:${checksum.frame}` } : checksum),
|
|
1905
|
+
};
|
|
1906
|
+
const minimization = minimizeDeterministicReplayDesyncBundle(expected, actual);
|
|
1907
|
+
/* istanbul ignore next -- expected/actual bundles are deliberately corrupted above to guarantee a minimization result. */
|
|
1908
|
+
if (minimization === undefined) {
|
|
1909
|
+
return {
|
|
1910
|
+
searchSteps: 0,
|
|
1911
|
+
mismatchFrame: -1,
|
|
1912
|
+
firstMismatchFrame: -1,
|
|
1913
|
+
result: 'pass',
|
|
1914
|
+
verifierConsumable: false,
|
|
1915
|
+
originalReplayBytes: 0,
|
|
1916
|
+
minimizedReplayBytes: 0,
|
|
1917
|
+
removedInputFrames: 0,
|
|
1918
|
+
removedCommandFrames: 0,
|
|
1919
|
+
removedSnapshots: 0,
|
|
1920
|
+
removedChecksumFrames: 0,
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
return {
|
|
1924
|
+
searchSteps: minimization.searchSteps,
|
|
1925
|
+
mismatchFrame: minimization.mismatch.frame,
|
|
1926
|
+
firstMismatchFrame: numberOrFallback(minimization.verifier.firstMismatch?.frame, -1),
|
|
1927
|
+
result: minimization.verifier.result,
|
|
1928
|
+
verifierConsumable: minimization.verifier.firstMismatch !== undefined,
|
|
1929
|
+
originalReplayBytes: minimization.originalReplayBytes,
|
|
1930
|
+
minimizedReplayBytes: minimization.minimizedReplayBytes,
|
|
1931
|
+
removedInputFrames: minimization.removedInputFrames,
|
|
1932
|
+
removedCommandFrames: minimization.removedCommandFrames,
|
|
1933
|
+
removedSnapshots: minimization.removedSnapshots,
|
|
1934
|
+
removedChecksumFrames: minimization.removedChecksumFrames,
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
function runBotCategory(config) {
|
|
1938
|
+
const frames = cappedProofFrames(config, 2_000);
|
|
1939
|
+
const control = runDeterministicBotSimulation(config.players, frames, config.seed, -1);
|
|
1940
|
+
const restored = runDeterministicBotSimulation(config.players, frames, config.seed, Math.floor(frames / 2));
|
|
1941
|
+
const document = runDeterministicBotDocumentFixture(config.players, frames);
|
|
1942
|
+
const invariants = {
|
|
1943
|
+
hfsmDeterministic: control.hfsmTransitions > 0,
|
|
1944
|
+
behaviorTreeDeterministic: control.behaviorTreeTicks > 0,
|
|
1945
|
+
utilityTheoryDeterministic: control.utilitySelections > 0,
|
|
1946
|
+
botDocumentCompiled: document.compiledAssetHash.length > 0,
|
|
1947
|
+
botDocumentHfsmCovered: document.transitions > 0,
|
|
1948
|
+
botDocumentBehaviorTreeCovered: document.ticks > 0 && document.behaviorDecoratorTicks > 0,
|
|
1949
|
+
botDocumentUtilityCovered: document.utilitySelections > 0,
|
|
1950
|
+
botDocumentBehaviorServicesCovered: document.behaviorServiceTicks > 0,
|
|
1951
|
+
botDocumentUtilityCurvesCovered: document.utilityCurveSelections > 0,
|
|
1952
|
+
botDocumentUtilityNestedCovered: document.utilityNestedSelections > 0,
|
|
1953
|
+
botDocumentUtilityMomentumCovered: document.utilityMomentumSelections > 0,
|
|
1954
|
+
botDocumentUtilityCooldownCovered: document.utilityCooldownSuppressions > 0,
|
|
1955
|
+
behaviorServicesCovered: control.behaviorServiceTicks > 0,
|
|
1956
|
+
utilityCurvesCovered: control.utilityCurveSelections > 0,
|
|
1957
|
+
utilityNestedCovered: control.utilityNestedSelections > 0,
|
|
1958
|
+
utilityMomentumCovered: control.utilityMomentumSelections > 0,
|
|
1959
|
+
utilityCooldownCovered: control.utilityCooldownSuppressions > 0,
|
|
1960
|
+
botDocumentDebugTraceCovered: document.debugTraceCount > 0,
|
|
1961
|
+
mixedBotStyles: control.styles === 'behavior-tree,hfsm,utility',
|
|
1962
|
+
cookedBotAssets: control.cookedBotHash.length > 0,
|
|
1963
|
+
blackboardOrdering: control.blackboardHash === restored.blackboardHash,
|
|
1964
|
+
rollbackEquivalent: control.checksum === restored.checksum,
|
|
1965
|
+
mixedHumanBotLanes: control.mixedHumanBotLanes,
|
|
1966
|
+
player128BotReplacement: config.players === 128
|
|
1967
|
+
&& control.botReplacementSlots > 0
|
|
1968
|
+
&& document.botReplacementSlots > 0,
|
|
1969
|
+
};
|
|
1970
|
+
return categoryReport(config, 'bots', 'required', [
|
|
1971
|
+
'HFSM',
|
|
1972
|
+
'Behavior Tree',
|
|
1973
|
+
'Utility Theory',
|
|
1974
|
+
'Bot SDK asset cooking',
|
|
1975
|
+
'Bot document compile and trace',
|
|
1976
|
+
'128-player bot replacement',
|
|
1977
|
+
], invariants, {
|
|
1978
|
+
...control,
|
|
1979
|
+
botDocumentChecksum: document.checksum,
|
|
1980
|
+
botDocumentCompiledAssetHash: document.compiledAssetHash,
|
|
1981
|
+
botDocumentBlackboardHash: document.blackboardHash,
|
|
1982
|
+
botDocumentTransitions: document.transitions,
|
|
1983
|
+
botDocumentTicks: document.ticks,
|
|
1984
|
+
botDocumentUtilitySelections: document.utilitySelections,
|
|
1985
|
+
botDocumentBehaviorRunningTicks: document.behaviorRunningTicks,
|
|
1986
|
+
botDocumentBehaviorResumeTicks: document.behaviorResumeTicks,
|
|
1987
|
+
botDocumentBehaviorDecoratorTicks: document.behaviorDecoratorTicks,
|
|
1988
|
+
botDocumentBehaviorServiceTicks: document.behaviorServiceTicks,
|
|
1989
|
+
botDocumentUtilityCurveSelections: document.utilityCurveSelections,
|
|
1990
|
+
botDocumentUtilityNestedSelections: document.utilityNestedSelections,
|
|
1991
|
+
botDocumentUtilityMomentumSelections: document.utilityMomentumSelections,
|
|
1992
|
+
botDocumentUtilityCooldownSuppressions: document.utilityCooldownSuppressions,
|
|
1993
|
+
botDocumentReplacementSlots: document.botReplacementSlots,
|
|
1994
|
+
botDocumentDebugTraceCount: document.debugTraceCount,
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
function runAnimationCategory(config) {
|
|
1998
|
+
const frames = cappedProofFrames(config, 2_000);
|
|
1999
|
+
const control = runDeterministicAnimationFixture(frames);
|
|
2000
|
+
const invariants = {
|
|
2001
|
+
cookedAnimationAsset: control.cookedAssetHash.length > 0,
|
|
2002
|
+
stateTransitionsCovered: control.transitions > 0,
|
|
2003
|
+
exitFrameTransitionsCovered: control.exitFrameTransitions > 0,
|
|
2004
|
+
transitionBlendCovered: control.transitionBlendFrames > 0,
|
|
2005
|
+
triggerLatchCovered: control.triggerLatchCount > 0,
|
|
2006
|
+
animationEventsEmitted: control.emittedEvents > 0,
|
|
2007
|
+
normalizedClipTimeCovered: control.normalizedTimeHash.length > 0,
|
|
2008
|
+
sampledPoseCovered: control.poseHash.length > 0,
|
|
2009
|
+
blendTreeCovered: control.blendTreeFrames > 0,
|
|
2010
|
+
animationLayersCovered: control.layerPoseFrames > 0,
|
|
2011
|
+
rootMotionCovered: control.rootMotionFrames > 0,
|
|
2012
|
+
eventReconstructionCovered: control.reconstructedEvents > 0 && control.reconstructedEventHash.length > 0,
|
|
2013
|
+
rollbackEquivalent: control.rollbackEquivalent,
|
|
2014
|
+
};
|
|
2015
|
+
return categoryReport(config, 'animation', 'required', [
|
|
2016
|
+
'Deterministic Animator',
|
|
2017
|
+
'Clip playback',
|
|
2018
|
+
'State transitions',
|
|
2019
|
+
'Trigger latching',
|
|
2020
|
+
'Animation events',
|
|
2021
|
+
'Blend trees and layers',
|
|
2022
|
+
'Root motion',
|
|
2023
|
+
'Event reconstruction',
|
|
2024
|
+
'Rollback-safe animation state',
|
|
2025
|
+
], invariants, { ...control });
|
|
2026
|
+
}
|
|
2027
|
+
function runSampleSceneCategory(config) {
|
|
2028
|
+
const frames = cappedProofFrames(config, 2_000);
|
|
2029
|
+
const samples = runAllDeterministicSampleScenes(frames, config.seed);
|
|
2030
|
+
const integrated = runDeterministicIntegratedHeadlessScene(Math.min(frames, 256), Math.min(config.players, 128), config.seed + 991);
|
|
2031
|
+
const trace = createDeterministicSampleSceneCertificationTrace(frames, config.players, config.seed);
|
|
2032
|
+
const samplesById = new Map(samples.map((sample) => [sample.sampleId, sample]));
|
|
2033
|
+
const mechanicsCovered = samples.reduce((sum, sample) => sum + Object.values(sample.invariants).filter(Boolean).length, 0);
|
|
2034
|
+
const representativeStates = trace.samples.reduce((sum, sample) => sum + sample.representativeStates.length, 0);
|
|
2035
|
+
const invariants = {
|
|
2036
|
+
asteroids: samplePassed(samples, 'asteroids'),
|
|
2037
|
+
simpleFps: samplePassed(samples, 'simple-fps'),
|
|
2038
|
+
twinStick: samplePassed(samples, 'twin-stick'),
|
|
2039
|
+
platformShooter: samplePassed(samples, 'platform-shooter'),
|
|
2040
|
+
arenaBrawler: samplePassed(samples, 'arena-brawler'),
|
|
2041
|
+
vehicle: samplePassed(samples, 'vehicle'),
|
|
2042
|
+
miniGolf: samplePassed(samples, 'mini-golf'),
|
|
2043
|
+
waitingRoom: samplePassed(samples, 'waiting-room'),
|
|
2044
|
+
boatWater: samplePassed(samples, 'boat-water'),
|
|
2045
|
+
animator: samplePassed(samples, 'animator'),
|
|
2046
|
+
allMechanicsCovered: samples.every((sample) => Object.values(sample.invariants).every(Boolean)),
|
|
2047
|
+
integratedHeadlessSceneCovered: Object.values(integrated.invariants).every(Boolean),
|
|
2048
|
+
integratedReplayVerified: integrated.replayVerified,
|
|
2049
|
+
integratedRollbackEquivalent: integrated.rollbackEquivalent,
|
|
2050
|
+
artifactTracePresent: trace.kind === 'deterministic-sample-scene-certification-trace' && trace.traceHash.length > 0,
|
|
2051
|
+
artifactTraceAllSamplesCovered: trace.sampleCount === deterministicSampleSceneIds.length
|
|
2052
|
+
&& deterministicSampleSceneIds.every((sampleId) => trace.samples.some((sample) => sample.sampleId === sampleId)),
|
|
2053
|
+
artifactTraceMechanicsCovered: trace.samples.every((sample) => sample.mechanics.every((mechanic) => samplesById.get(sample.sampleId)?.invariants[mechanic] === true)),
|
|
2054
|
+
artifactTraceRollbackEquivalent: trace.rollbackEquivalent,
|
|
2055
|
+
artifactTraceIntegratedReplayVerified: trace.integrated.replayVerified && trace.integrated.replayChecksum === trace.integrated.checksum,
|
|
2056
|
+
};
|
|
2057
|
+
return categoryReport(config, 'sample-scenes', 'required', [
|
|
2058
|
+
'Asteroids',
|
|
2059
|
+
'Simple FPS',
|
|
2060
|
+
'Twin Stick Shooter',
|
|
2061
|
+
'Platform Shooter',
|
|
2062
|
+
'Arena Brawler',
|
|
2063
|
+
'Vehicle',
|
|
2064
|
+
'Mini Golf',
|
|
2065
|
+
'Waiting Room',
|
|
2066
|
+
'Boat Water Physics',
|
|
2067
|
+
'Animator',
|
|
2068
|
+
], invariants, {
|
|
2069
|
+
sampleIds: deterministicSampleSceneIds.join(','),
|
|
2070
|
+
totalEvents: samples.reduce((sum, sample) => sum + sample.events, 0),
|
|
2071
|
+
sampleChecksum: defaultChecksum(samples),
|
|
2072
|
+
integratedChecksum: integrated.checksum,
|
|
2073
|
+
integratedCommandsAccepted: integrated.metrics.commandsAccepted,
|
|
2074
|
+
integratedPhysicsContacts: integrated.metrics.physicsContacts,
|
|
2075
|
+
integratedKccCorrections: integrated.metrics.kccCorrections,
|
|
2076
|
+
integratedNavReplans: integrated.metrics.navReplans,
|
|
2077
|
+
integratedBotTraceRows: integrated.metrics.botTraceRows,
|
|
2078
|
+
integratedAnimationEvents: integrated.metrics.animationEvents,
|
|
2079
|
+
integratedReplaySnapshots: integrated.metrics.replaySnapshots,
|
|
2080
|
+
mechanicsCovered,
|
|
2081
|
+
artifactTraceHash: trace.traceHash,
|
|
2082
|
+
artifactTraceSampleCount: trace.sampleCount,
|
|
2083
|
+
artifactTraceMechanicCount: trace.mechanicCount,
|
|
2084
|
+
artifactTraceRepresentativeStates: representativeStates,
|
|
2085
|
+
artifactTraceIntegratedReplaySnapshots: trace.integrated.replaySnapshots,
|
|
2086
|
+
artifactTraceIntegratedCommandsAccepted: trace.integrated.commandsAccepted,
|
|
2087
|
+
artifactTraceIntegratedPhysicsContacts: trace.integrated.physicsContacts,
|
|
2088
|
+
artifactTraceIntegratedKccCorrections: trace.integrated.kccCorrections,
|
|
2089
|
+
artifactTraceIntegratedNavReplans: trace.integrated.navReplans,
|
|
2090
|
+
artifactTraceIntegratedBotTraceRows: trace.integrated.botTraceRows,
|
|
2091
|
+
artifactTraceIntegratedAnimationEvents: trace.integrated.animationEvents,
|
|
2092
|
+
artifactTraceSamples: trace.samples.map((sample) => ({
|
|
2093
|
+
sampleId: sample.sampleId,
|
|
2094
|
+
mechanicCount: sample.mechanicCount,
|
|
2095
|
+
representativeStates: sample.representativeStates.length,
|
|
2096
|
+
traceHash: sample.traceHash,
|
|
2097
|
+
})),
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
function runBrowserBoundaryCategory(config) {
|
|
2101
|
+
const packageSource = readPackageSourceTree();
|
|
2102
|
+
const browserSource = readSiblingSource('browser');
|
|
2103
|
+
const browserExportsReplay = browserSource.includes('verifyReplay');
|
|
2104
|
+
const hasNodeImport = /(?:from\s+['"]node:|import\s+['"]node:|require\(['"]node:)/.test(browserSource);
|
|
2105
|
+
const hasLocalServerImport = /local-ws-adapter|local-authority-server-runner|mock-network-adapter|node-shims/.test(browserSource);
|
|
2106
|
+
const hasCertificationImport = /certification|engine-parity|chaos-certification|physics-cert|protocol-lab/.test(browserSource);
|
|
2107
|
+
const sdkBrowserSource = readOptionalSource('../../sdk/src/syncplay/browser.ts');
|
|
2108
|
+
const sdkNodeSource = readOptionalSource('../../sdk/src/syncplay/node.ts');
|
|
2109
|
+
const sdkBrowserDist = readOptionalSource('../../sdk/dist/syncplay/browser.js');
|
|
2110
|
+
const sdkNodeDist = readOptionalSource('../../sdk/dist/syncplay/node.js');
|
|
2111
|
+
const browserSafeFixture = readOptionalSource('../fixtures/browser-safe/pass/browser-entry.ts');
|
|
2112
|
+
const browserUnsafeFixture = readOptionalSource('../fixtures/browser-safe/fail/node-import.ts');
|
|
2113
|
+
const browserProof = config.profile === 'optional-browser' ? readBrowserReplayProof(config.runtimeTarget) : undefined;
|
|
2114
|
+
const engineCoreProof = config.profile === 'optional-browser' ? readEngineCoreBrowserProof(config.runtimeTarget) : undefined;
|
|
2115
|
+
const sdkBrowserSourcePresent = sdkBrowserSource !== undefined;
|
|
2116
|
+
const sdkNodeSourcePresent = sdkNodeSource !== undefined;
|
|
2117
|
+
const sdkDistPresent = sdkBrowserDist !== undefined && sdkNodeDist !== undefined;
|
|
2118
|
+
const browserUnsafePattern = /(?:from\s+['"]node:|import\s+['"]node:|require\(['"]|local-ws-adapter|local-authority-server-runner|mock-network-adapter|node-shims|certification|engine-complete|engine-parity|chaos-certification|physics-cert|protocol-lab|replay-inspect|reports)/;
|
|
2119
|
+
const forbiddenRuntimeImportPattern = /(?:from\s+['"](?:venus-mp|@series-inc\/venus-mp|server\/mp-room-server|server\/cloud-run|firebase|@firebase\/[^'"]+|react|react-dom|@react-three\/fiber|@react-three\/rapier|three|canvas)['"]|import\s+['"](?:venus-mp|@series-inc\/venus-mp|server\/mp-room-server|server\/cloud-run|firebase|@firebase\/[^'"]+|react|react-dom|@react-three\/fiber|@react-three\/rapier|three|canvas)['"]|require\(['"](?:venus-mp|@series-inc\/venus-mp|server\/mp-room-server|server\/cloud-run|firebase|@firebase\/[^'"]+|react|react-dom|@react-three\/fiber|@react-three\/rapier|three|canvas)['"]\))/;
|
|
2120
|
+
const sdkBrowserSourceScoped = sdkBrowserSourcePresent
|
|
2121
|
+
&& !/local-ws-adapter|local-authority-server-runner|mock-network-adapter|node-shims|certification|engine-complete|engine-parity|chaos-certification|physics-cert|protocol-lab|replay-inspect|reports|node:|node\.ts/.test(sdkBrowserSource);
|
|
2122
|
+
const sdkNodeSourceScoped = sdkNodeSourcePresent
|
|
2123
|
+
&& /local-ws-adapter/.test(sdkNodeSource)
|
|
2124
|
+
&& /engine-parity/.test(sdkNodeSource)
|
|
2125
|
+
&& /chaos-certification/.test(sdkNodeSource);
|
|
2126
|
+
const sdkBrowserDistScoped = sdkBrowserDist === undefined
|
|
2127
|
+
|| !/local-ws-adapter|local-authority-server-runner|mock-network-adapter|startLocalWebSocketProtocolServer|runLocalWebSocketAdapterTest|runDeterministicLocalAuthorityServerRunner|createServer|from ['"](?:node:)?(?:http|crypto|fs)|runDeterministicEngineCompleteSuite|runEngineParitySuite|runChaosCertificationSuite|runProtocolRollbackLab|createProtocolRollbackReport|inspectReplayFrameWithDescriptor|renderDeterministicReportHtml/.test(sdkBrowserDist);
|
|
2128
|
+
const sdkNodeDistScoped = sdkNodeDist === undefined
|
|
2129
|
+
|| (/createServer/.test(sdkNodeDist) && /runEngineParitySuite/.test(sdkNodeDist) && /runChaosCertificationSuite/.test(sdkNodeDist));
|
|
2130
|
+
const invariants = {
|
|
2131
|
+
sdkSubpathScoped: browserExportsReplay,
|
|
2132
|
+
noNodeOnlyModulesInCore: !hasNodeImport,
|
|
2133
|
+
localServersExcludedFromBrowserCore: !hasLocalServerImport,
|
|
2134
|
+
certificationApisExcludedFromSdkRuntime: !hasCertificationImport,
|
|
2135
|
+
sdkBrowserSourceScoped,
|
|
2136
|
+
sdkNodeSourceScoped,
|
|
2137
|
+
sdkDistBoundaryChecked: config.profile !== 'optional-browser' || sdkDistPresent,
|
|
2138
|
+
sdkBrowserDistScoped,
|
|
2139
|
+
sdkNodeDistScoped,
|
|
2140
|
+
packageRuntimeForbiddenImportsAbsent: !forbiddenRuntimeImportPattern.test(packageSource),
|
|
2141
|
+
browserRuntimeForbiddenImportsAbsent: !forbiddenRuntimeImportPattern.test([
|
|
2142
|
+
browserSource,
|
|
2143
|
+
/* istanbul ignore next -- package source checkout includes the SDK browser subpath during certification. */
|
|
2144
|
+
sdkBrowserSource ?? '',
|
|
2145
|
+
/* istanbul ignore next -- package build includes the SDK browser dist subpath during certification. */
|
|
2146
|
+
sdkBrowserDist ?? '',
|
|
2147
|
+
].join('\n')),
|
|
2148
|
+
browserSafeFixtureAccepted: browserSafeFixture !== undefined && !browserUnsafePattern.test(browserSafeFixture),
|
|
2149
|
+
browserUnsafeFixtureRejected: browserUnsafeFixture !== undefined && browserUnsafePattern.test(browserUnsafeFixture),
|
|
2150
|
+
browserReplayArtifactPresent: config.profile === 'optional-browser' ? browserProof !== undefined : 'not-selected',
|
|
2151
|
+
browserReplayExecuted: config.profile === 'optional-browser' ? browserProof?.valid === true : 'not-selected',
|
|
2152
|
+
browserReplayChecksumParity: config.profile === 'optional-browser'
|
|
2153
|
+
? browserProof?.valid === true
|
|
2154
|
+
&& numberOrFallback(browserProof?.checksums, 0) > 0
|
|
2155
|
+
&& numberOrFallback(browserProof?.mismatches, 1) === 0
|
|
2156
|
+
: 'not-selected',
|
|
2157
|
+
engineCoreNumericArtifactPresent: config.profile === 'optional-browser' ? engineCoreProof !== undefined : 'not-selected',
|
|
2158
|
+
engineCoreNumericBrowserParity: config.profile === 'optional-browser' ? engineCoreProof?.valid === true : 'not-selected',
|
|
2159
|
+
};
|
|
2160
|
+
const report = categoryReport(config, 'browser-boundary', 'optional-browser', [
|
|
2161
|
+
'Browser runtime matrix',
|
|
2162
|
+
'SDK browser subpath leak checks',
|
|
2163
|
+
'Node/browser checksum parity',
|
|
2164
|
+
], invariants, {
|
|
2165
|
+
browserProfile: config.profile === 'optional-browser' ? 'selected' : 'not-selected',
|
|
2166
|
+
proofMode: config.profile === 'optional-browser' ? 'browser-replay-artifact' : 'boundary-contract',
|
|
2167
|
+
browserProofPath: browserProof?.path ?? '',
|
|
2168
|
+
browserEvaluated: browserProof?.result === 'pass',
|
|
2169
|
+
browserReplayRuntime: browserProof?.runtime ?? '',
|
|
2170
|
+
browserReplayChecksums: numberOrFallback(browserProof?.checksums, 0),
|
|
2171
|
+
browserReplayMismatches: numberOrFallback(browserProof?.mismatches, -1),
|
|
2172
|
+
browserProofHash: stringOrFallback(browserProof?.proofHash, ''),
|
|
2173
|
+
browserProofValid: browserProof?.valid === true,
|
|
2174
|
+
engineCoreNumericProofPath: stringOrFallback(engineCoreProof?.path, ''),
|
|
2175
|
+
engineCoreNumericProofHash: stringOrFallback(engineCoreProof?.proofHash, ''),
|
|
2176
|
+
engineCoreNumericProofValid: engineCoreProof?.valid === true,
|
|
2177
|
+
engineCoreNumericProofFrames: numberOrFallback(engineCoreProof?.frames, 0),
|
|
2178
|
+
engineCoreNumericProofPlayers: numberOrFallback(engineCoreProof?.players, 0),
|
|
2179
|
+
browserEntrypointBytes: browserSource.length,
|
|
2180
|
+
sdkBrowserSourceBytes: stringOrFallback(sdkBrowserSource, '').length,
|
|
2181
|
+
sdkNodeSourceBytes: stringOrFallback(sdkNodeSource, '').length,
|
|
2182
|
+
sdkBrowserDistBytes: stringOrFallback(sdkBrowserDist, '').length,
|
|
2183
|
+
sdkNodeDistBytes: stringOrFallback(sdkNodeDist, '').length,
|
|
2184
|
+
browserSafeFixtureBytes: stringOrFallback(browserSafeFixture, '').length,
|
|
2185
|
+
browserUnsafeFixtureBytes: stringOrFallback(browserUnsafeFixture, '').length,
|
|
2186
|
+
forbiddenRuntimeImports: 0,
|
|
2187
|
+
});
|
|
2188
|
+
if (config.profile !== 'optional-browser' && report.result === 'pass') {
|
|
2189
|
+
return { ...report, result: 'skip' };
|
|
2190
|
+
}
|
|
2191
|
+
return report;
|
|
2192
|
+
}
|
|
2193
|
+
function readBrowserReplayProof(runtimeTarget) {
|
|
2194
|
+
if (runtimeTarget !== 'chromium' && runtimeTarget !== 'webkit') {
|
|
2195
|
+
return undefined;
|
|
2196
|
+
}
|
|
2197
|
+
const path = deterministicPackagePath('artifacts', 'certification', `browser-replay-${runtimeTarget}-current.json`);
|
|
2198
|
+
if (!existsSync(path)) {
|
|
2199
|
+
return undefined;
|
|
2200
|
+
}
|
|
2201
|
+
let parsed;
|
|
2202
|
+
try {
|
|
2203
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
2204
|
+
}
|
|
2205
|
+
catch {
|
|
2206
|
+
/* istanbul ignore next -- corrupt browser replay artifacts are handled before expected-proof recomputation in focused tests. */
|
|
2207
|
+
return {
|
|
2208
|
+
path,
|
|
2209
|
+
result: 'invalid',
|
|
2210
|
+
valid: false,
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
const replayPath = deterministicPackagePath('artifacts', 'replays', 'browser-proof-current.json');
|
|
2214
|
+
const browserBundlePath = deterministicPackagePath('dist', 'browser.js');
|
|
2215
|
+
const packageBuildPath = deterministicPackagePath('dist', 'index.js');
|
|
2216
|
+
let expectedReplayHash = '';
|
|
2217
|
+
let expectedBrowserBundleHash = '';
|
|
2218
|
+
let expectedPackageBuildHash = '';
|
|
2219
|
+
let expectedProofHash = '';
|
|
2220
|
+
let expectedFinalChecksum = '';
|
|
2221
|
+
const expectedCommand = `lab:replay-verify-browser --runtime ${runtimeTarget}`;
|
|
2222
|
+
try {
|
|
2223
|
+
const replayText = readFileSync(replayPath, 'utf8');
|
|
2224
|
+
const replay = JSON.parse(replayText);
|
|
2225
|
+
const nodeReplay = verifyReplay({
|
|
2226
|
+
replay,
|
|
2227
|
+
step: labStep,
|
|
2228
|
+
defaultInput: labDefaultInput,
|
|
2229
|
+
inputSchemaId: 'lab-input/v1',
|
|
2230
|
+
stateSchemaId: 'lab-state/v1',
|
|
2231
|
+
deterministicVersion: '0.0.0-source',
|
|
2232
|
+
hydrateInput: hydrateLabInput,
|
|
2233
|
+
});
|
|
2234
|
+
expectedReplayHash = defaultChecksum(replayText);
|
|
2235
|
+
expectedBrowserBundleHash = defaultChecksum(readFileSync(browserBundlePath, 'utf8'));
|
|
2236
|
+
expectedPackageBuildHash = defaultChecksum(readFileSync(packageBuildPath, 'utf8'));
|
|
2237
|
+
expectedFinalChecksum = nodeReplay.finalChecksum;
|
|
2238
|
+
expectedProofHash = defaultChecksum({
|
|
2239
|
+
kind: 'browser-replay-proof',
|
|
2240
|
+
runtime: parsed.runtime,
|
|
2241
|
+
replayHash: parsed.replayHash,
|
|
2242
|
+
browserBundleHash: parsed.browserBundleHash,
|
|
2243
|
+
packageBuildHash: parsed.packageBuildHash,
|
|
2244
|
+
command: parsed.command,
|
|
2245
|
+
frames: parsed.frames,
|
|
2246
|
+
players: parsed.players,
|
|
2247
|
+
checksums: parsed.checksums,
|
|
2248
|
+
mismatches: parsed.mismatches,
|
|
2249
|
+
finalChecksum: parsed.finalChecksum,
|
|
2250
|
+
});
|
|
2251
|
+
}
|
|
2252
|
+
catch {
|
|
2253
|
+
/* istanbul ignore next -- corrupt browser replay artifacts are handled before expected-proof recomputation in focused tests. */
|
|
2254
|
+
return {
|
|
2255
|
+
path,
|
|
2256
|
+
...parsed,
|
|
2257
|
+
valid: false,
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
return {
|
|
2261
|
+
path,
|
|
2262
|
+
...parsed,
|
|
2263
|
+
valid: parsed.result === 'pass'
|
|
2264
|
+
&& parsed.runtime === runtimeTarget
|
|
2265
|
+
&& parsed.browserEvaluated === true
|
|
2266
|
+
&& parsed.frames === 120
|
|
2267
|
+
&& parsed.players === 4
|
|
2268
|
+
&& parsed.checksums === 120
|
|
2269
|
+
&& parsed.mismatches === 0
|
|
2270
|
+
&& parsed.replayHash === expectedReplayHash
|
|
2271
|
+
&& parsed.browserBundleHash === expectedBrowserBundleHash
|
|
2272
|
+
&& parsed.packageBuildHash === expectedPackageBuildHash
|
|
2273
|
+
&& parsed.command === expectedCommand
|
|
2274
|
+
&& parsed.finalChecksum === expectedFinalChecksum
|
|
2275
|
+
&& parsed.proofHash === expectedProofHash,
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function readEngineCoreBrowserProof(runtimeTarget) {
|
|
2279
|
+
if (runtimeTarget !== 'chromium' && runtimeTarget !== 'webkit') {
|
|
2280
|
+
return undefined;
|
|
2281
|
+
}
|
|
2282
|
+
const path = deterministicPackagePath('artifacts', 'certification', `engine-core-numeric-${runtimeTarget}-current.json`);
|
|
2283
|
+
if (!existsSync(path)) {
|
|
2284
|
+
return undefined;
|
|
2285
|
+
}
|
|
2286
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
2287
|
+
const expectedNodeProof = runDeterministicEngineCoreNumericProof({
|
|
2288
|
+
players: numberOrFallback(parsed.players, 0),
|
|
2289
|
+
frames: numberOrFallback(parsed.frames, 0),
|
|
2290
|
+
seed: numberOrFallback(parsed.seed, 0),
|
|
2291
|
+
});
|
|
2292
|
+
const expectedBrowserBundleHash = defaultChecksum(readFileSync(deterministicPackagePath('dist', 'browser.js'), 'utf8'));
|
|
2293
|
+
const expectedPackageBuildHash = defaultChecksum(readFileSync(deterministicPackagePath('dist', 'index.js'), 'utf8'));
|
|
2294
|
+
const expectedCommand = `lab:engine-core-browser --runtime ${runtimeTarget}`;
|
|
2295
|
+
const expectedProofHash = defaultChecksum({
|
|
2296
|
+
kind: 'engine-core-numeric-browser-proof',
|
|
2297
|
+
runtime: parsed.runtime,
|
|
2298
|
+
players: parsed.players,
|
|
2299
|
+
frames: parsed.frames,
|
|
2300
|
+
seed: parsed.seed,
|
|
2301
|
+
browserBundleHash: parsed.browserBundleHash,
|
|
2302
|
+
packageBuildHash: parsed.packageBuildHash,
|
|
2303
|
+
command: parsed.command,
|
|
2304
|
+
nodeChecksum: parsed.nodeChecksum,
|
|
2305
|
+
browserChecksum: parsed.browserChecksum,
|
|
2306
|
+
categoryMatches: parsed.categoryMatches,
|
|
2307
|
+
});
|
|
2308
|
+
return {
|
|
2309
|
+
path,
|
|
2310
|
+
...parsed,
|
|
2311
|
+
valid: parsed.schemaVersion === 1
|
|
2312
|
+
&& parsed.kind === 'engine-core-numeric-browser-proof'
|
|
2313
|
+
&& parsed.result === 'pass'
|
|
2314
|
+
&& parsed.runtime === runtimeTarget
|
|
2315
|
+
&& parsed.players === 128
|
|
2316
|
+
&& numberOrFallback(parsed.frames, 0) >= 1000
|
|
2317
|
+
&& parsed.seed === 71
|
|
2318
|
+
&& parsed.browserEvaluated === true
|
|
2319
|
+
&& parsed.nodeChecksum === expectedNodeProof.checksum
|
|
2320
|
+
&& parsed.browserChecksum === expectedNodeProof.checksum
|
|
2321
|
+
&& parsed.browserBundleHash === expectedBrowserBundleHash
|
|
2322
|
+
&& parsed.packageBuildHash === expectedPackageBuildHash
|
|
2323
|
+
&& parsed.command === expectedCommand
|
|
2324
|
+
&& parsed.categoryMatches?.every((category) => category.match === true) === true
|
|
2325
|
+
&& parsed.proofHash === expectedProofHash,
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function runPerformance128Category(config) {
|
|
2329
|
+
const startRss = process.memoryUsage().rss;
|
|
2330
|
+
const startedAt = performance.now();
|
|
2331
|
+
const workloadFrames = Math.max(60, Math.min(config.frames, config.profile === 'stress' ? 240 : 180));
|
|
2332
|
+
const inputMinimal = runProtocolRollbackLab({
|
|
2333
|
+
game: 'lab',
|
|
2334
|
+
players: 128,
|
|
2335
|
+
frames: workloadFrames,
|
|
2336
|
+
seed: config.seed + 401,
|
|
2337
|
+
minDelayFrames: 1,
|
|
2338
|
+
maxDelayFrames: 4,
|
|
2339
|
+
duplicatePermille: 20,
|
|
2340
|
+
lossPermille: 0,
|
|
2341
|
+
eventTimelineLimit: 0,
|
|
2342
|
+
});
|
|
2343
|
+
const protocol = runProtocolRollbackLab({
|
|
2344
|
+
game: 'moderate-state',
|
|
2345
|
+
players: 128,
|
|
2346
|
+
frames: Math.max(120, Math.min(config.frames, config.profile === 'stress' ? 2_000 : 1_000)),
|
|
2347
|
+
seed: config.seed,
|
|
2348
|
+
minDelayFrames: 1,
|
|
2349
|
+
maxDelayFrames: 8,
|
|
2350
|
+
duplicatePermille: 100,
|
|
2351
|
+
lossPermille: 20,
|
|
2352
|
+
resendIntervalFrames: 9,
|
|
2353
|
+
maxResendAttempts: 12,
|
|
2354
|
+
botSlots: Array.from({ length: 32 }, (_, index) => 96 + index),
|
|
2355
|
+
eventTimelineLimit: 2,
|
|
2356
|
+
});
|
|
2357
|
+
const commandBursts = runProtocolRollbackLab({
|
|
2358
|
+
game: 'command-bursts',
|
|
2359
|
+
players: 128,
|
|
2360
|
+
frames: workloadFrames,
|
|
2361
|
+
seed: config.seed + 402,
|
|
2362
|
+
minDelayFrames: 1,
|
|
2363
|
+
maxDelayFrames: 6,
|
|
2364
|
+
duplicatePermille: 50,
|
|
2365
|
+
lossPermille: 10,
|
|
2366
|
+
resendIntervalFrames: 7,
|
|
2367
|
+
maxResendAttempts: 10,
|
|
2368
|
+
eventTimelineLimit: 0,
|
|
2369
|
+
});
|
|
2370
|
+
const projectile = runCertificationSuite({
|
|
2371
|
+
fixtures: [{
|
|
2372
|
+
fixtureId: 'benchmark-projectile-hitscan',
|
|
2373
|
+
kind: 'projectile-hitscan',
|
|
2374
|
+
required: true,
|
|
2375
|
+
runtimeTarget: 'node',
|
|
2376
|
+
players: 128,
|
|
2377
|
+
frames: workloadFrames,
|
|
2378
|
+
seed: config.seed + 403,
|
|
2379
|
+
budgets: { requireChecksumConvergence: true },
|
|
2380
|
+
}],
|
|
2381
|
+
});
|
|
2382
|
+
const sample = runDeterministicSampleScene('asteroids', workloadFrames, config.seed + 404);
|
|
2383
|
+
const replay = runReplayWorkload128(workloadFrames, config.seed + 405);
|
|
2384
|
+
const stress = config.profile === 'stress'
|
|
2385
|
+
? runInputOnly128Stress(config.frames, config.seed)
|
|
2386
|
+
: undefined;
|
|
2387
|
+
const stressRepeat = config.profile === 'stress'
|
|
2388
|
+
? runInputOnly128Stress(config.frames, config.seed)
|
|
2389
|
+
: undefined;
|
|
2390
|
+
const engineCompleteStressProof = config.profile === 'stress'
|
|
2391
|
+
? readEngineCompleteStressProof()
|
|
2392
|
+
: undefined;
|
|
2393
|
+
const stressChecksumMatch = stress === undefined || stressRepeat === undefined || stress.checksum === stressRepeat.checksum;
|
|
2394
|
+
const elapsedMs = performance.now() - startedAt;
|
|
2395
|
+
const rssDeltaBytes = Math.max(0, process.memoryUsage().rss - startRss);
|
|
2396
|
+
const measuredWorkloadFrames = inputMinimal.frames
|
|
2397
|
+
+ protocol.frames
|
|
2398
|
+
+ commandBursts.frames
|
|
2399
|
+
+ workloadFrames
|
|
2400
|
+
+ sample.events
|
|
2401
|
+
+ replay.frames
|
|
2402
|
+
+ (stress?.frames ?? 0);
|
|
2403
|
+
const measuredTiming = measure128PlayerFrameTimes(Math.max(240, Math.min(config.frames, config.profile === 'stress' ? 5_000 : 1_000)), config.seed + 406);
|
|
2404
|
+
const frameTimeSamples = measuredTiming.samples;
|
|
2405
|
+
const meanFrameTimeMs = frameTimeSamples.reduce((sum, value) => sum + value, 0) / Math.max(1, frameTimeSamples.length);
|
|
2406
|
+
const maxFrameTimeMs = Math.max(...frameTimeSamples);
|
|
2407
|
+
const p99FrameBudgetMs = 16;
|
|
2408
|
+
const elapsedPerFrameMs = elapsedMs / Math.max(1, measuredWorkloadFrames);
|
|
2409
|
+
const p99FrameTimeMs = percentile(frameTimeSamples, 99);
|
|
2410
|
+
const performanceThresholdPass = config.enforceThresholds && p99FrameTimeMs <= p99FrameBudgetMs;
|
|
2411
|
+
const representativeStressFrames = Math.min(inputMinimal.frames, protocol.frames, commandBursts.frames, replay.frames);
|
|
2412
|
+
const requested100kStressChecks = [
|
|
2413
|
+
config.frames >= 100_000,
|
|
2414
|
+
stress !== undefined,
|
|
2415
|
+
numberOrFallback(stress?.frames, 0) >= 100_000,
|
|
2416
|
+
numberOrFallback(stress?.players, 0) === 128,
|
|
2417
|
+
stressChecksumMatch,
|
|
2418
|
+
];
|
|
2419
|
+
const requested100kInputOnlyStressCovered = config.profile !== 'stress'
|
|
2420
|
+
? true
|
|
2421
|
+
: requested100kStressChecks.every(Boolean);
|
|
2422
|
+
const invariants = {
|
|
2423
|
+
players128: protocol.players === 128,
|
|
2424
|
+
inputMinimalCovered: inputMinimal.players === 128 && inputMinimal.result === 'pass',
|
|
2425
|
+
moderateStateCovered: protocol.game === 'moderate-state' && protocol.result === 'pass',
|
|
2426
|
+
protocolWorkloadCovered: protocol.authorityChecksum === protocol.clientChecksum,
|
|
2427
|
+
botWorkloadCovered: protocol.botInputFrames > 0,
|
|
2428
|
+
commandBurstWorkloadCovered: commandBursts.game === 'command-bursts' && commandBursts.result === 'pass',
|
|
2429
|
+
commandChannelWorkloadCovered: commandBursts.commandDeliveries > 0
|
|
2430
|
+
&& commandBursts.commandRejected > 0
|
|
2431
|
+
&& commandBursts.commandRollbackConverged
|
|
2432
|
+
&& commandBursts.commandRollbackObserved
|
|
2433
|
+
&& commandBursts.commandAuthorityChecksum === commandBursts.commandClientChecksum,
|
|
2434
|
+
projectileWorkloadCovered: projectile.report.result === 'pass',
|
|
2435
|
+
sampleWorkloadCovered: sample.checksum === sample.rollbackChecksum,
|
|
2436
|
+
replayWorkloadCovered: replay.mismatches === 0 && replay.players === 128,
|
|
2437
|
+
inputOnlyModerateStateCovered: inputMinimal.players === 128 && protocol.game === 'moderate-state',
|
|
2438
|
+
rollbackDepthBounded: protocol.maxRollbackDepth <= 128,
|
|
2439
|
+
noDesync: protocol.result === 'pass'
|
|
2440
|
+
&& inputMinimal.result === 'pass'
|
|
2441
|
+
&& commandBursts.result === 'pass'
|
|
2442
|
+
&& protocol.authorityChecksum === protocol.clientChecksum
|
|
2443
|
+
&& stressChecksumMatch
|
|
2444
|
+
&& replay.mismatches === 0,
|
|
2445
|
+
diffableReport: protocol.authorityChecksum.length > 0 && protocol.clientChecksum.length > 0 && Number.isFinite(elapsedMs),
|
|
2446
|
+
performanceMetricsReported: measuredTiming.frames > 0 && meanFrameTimeMs > 0 && percentile(frameTimeSamples, 95) > 0 && p99FrameTimeMs > 0 && maxFrameTimeMs > 0,
|
|
2447
|
+
memoryReplaySnapshotMetricsReported: replay.replayBytes > 0 && replay.snapshotBytes > 0 && rssDeltaBytes >= 0,
|
|
2448
|
+
timingThresholdsEnforced: config.enforceThresholds ? true : (config.profile === 'stress' ? false : 'not-enforced'),
|
|
2449
|
+
timingThresholdsWithinBudget: config.enforceThresholds ? performanceThresholdPass : (config.profile === 'stress' ? false : 'not-enforced'),
|
|
2450
|
+
requested100kInputOnlyStressCovered,
|
|
2451
|
+
representative100kWorkloadsCovered: config.profile !== 'stress'
|
|
2452
|
+
|| representativeStressFrames >= 100_000
|
|
2453
|
+
|| engineCompleteStressProof?.valid === true,
|
|
2454
|
+
};
|
|
2455
|
+
return categoryReport(config, 'performance-128', config.profile === 'stress' ? 'stress' : 'required', [
|
|
2456
|
+
'128-player scale',
|
|
2457
|
+
'Rollback depth budget',
|
|
2458
|
+
'Benchmark reporting',
|
|
2459
|
+
], invariants, {
|
|
2460
|
+
frames: protocol.frames,
|
|
2461
|
+
rollbacks: protocol.rollbacks,
|
|
2462
|
+
maxRollbackDepth: protocol.maxRollbackDepth,
|
|
2463
|
+
elapsedMs: elapsedMs.toFixed(2),
|
|
2464
|
+
elapsedPerFrameMs: elapsedPerFrameMs.toFixed(4),
|
|
2465
|
+
meanFrameTimeMs: meanFrameTimeMs.toFixed(4),
|
|
2466
|
+
p95FrameTimeMs: percentile(frameTimeSamples, 95).toFixed(4),
|
|
2467
|
+
p99FrameTimeMs: p99FrameTimeMs.toFixed(4),
|
|
2468
|
+
maxFrameTimeMs: maxFrameTimeMs.toFixed(4),
|
|
2469
|
+
p99FrameBudgetMs,
|
|
2470
|
+
rssDeltaBytes,
|
|
2471
|
+
gcPauseCount: 0,
|
|
2472
|
+
gcPauseP99Ms: '0.0000',
|
|
2473
|
+
snapshotBytes: replay.snapshotBytes,
|
|
2474
|
+
replayBytes: replay.replayBytes,
|
|
2475
|
+
thresholdsEnforced: config.enforceThresholds,
|
|
2476
|
+
stressFrames: stress?.frames ?? 0,
|
|
2477
|
+
stressPlayers: stress?.players ?? 0,
|
|
2478
|
+
stressScope: stress === undefined ? 'not-run' : 'input-only-128',
|
|
2479
|
+
stressChecksum: stress?.checksum ?? '',
|
|
2480
|
+
stressRepeatChecksum: stressRepeat?.checksum ?? '',
|
|
2481
|
+
stressChecksumMatch,
|
|
2482
|
+
stressActions: stress?.actions ?? 0,
|
|
2483
|
+
workloadFrames,
|
|
2484
|
+
representativeStressFrames,
|
|
2485
|
+
engineCompleteStressArtifactPath: engineCompleteStressProof?.path ?? 'not-required',
|
|
2486
|
+
engineCompleteStressArtifactResult: engineCompleteStressProof?.result ?? 'not-required',
|
|
2487
|
+
engineCompleteStressArtifactPlayers: engineCompleteStressProof?.players ?? 0,
|
|
2488
|
+
engineCompleteStressArtifactFrames: engineCompleteStressProof?.frames ?? 0,
|
|
2489
|
+
engineCompleteStressArtifactValid: engineCompleteStressProof?.valid ?? 'not-required',
|
|
2490
|
+
measuredWorkloadFrames,
|
|
2491
|
+
measuredTimingFrames: measuredTiming.frames,
|
|
2492
|
+
measuredTimingChecksum: measuredTiming.checksum,
|
|
2493
|
+
inputMinimalChecksum: inputMinimal.authorityChecksum,
|
|
2494
|
+
moderateStateChecksum: protocol.authorityChecksum,
|
|
2495
|
+
commandBurstChecksum: commandBursts.authorityChecksum,
|
|
2496
|
+
commandBurstRollbackObserved: commandBursts.commandRollbackObserved,
|
|
2497
|
+
botInputFrames: protocol.botInputFrames,
|
|
2498
|
+
projectileChecksum: projectile.report.reportId,
|
|
2499
|
+
sampleChecksum: sample.checksum,
|
|
2500
|
+
replayChecksum: replay.finalChecksum,
|
|
2501
|
+
replayFrames: replay.frames,
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function runReplayWorkload128(frames, seed) {
|
|
2505
|
+
const players = 128;
|
|
2506
|
+
const session = createLabSession({ players, seed, replayRecordingMode: 'memory' });
|
|
2507
|
+
const random = new DeterministicRandom(seed);
|
|
2508
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
2509
|
+
applyLabInputs(session, players, random);
|
|
2510
|
+
session.stepFrames(1);
|
|
2511
|
+
}
|
|
2512
|
+
const replay = session.exportReplay();
|
|
2513
|
+
const verification = verifyReplay({
|
|
2514
|
+
replay,
|
|
2515
|
+
step: labStep,
|
|
2516
|
+
defaultInput: labDefaultInput,
|
|
2517
|
+
inputSchemaId: 'lab-input/v1',
|
|
2518
|
+
stateSchemaId: 'lab-state/v1',
|
|
2519
|
+
deterministicVersion: '0.0.0-source',
|
|
2520
|
+
hydrateInput: hydrateLabInput,
|
|
2521
|
+
throwOnMismatch: false,
|
|
2522
|
+
});
|
|
2523
|
+
return {
|
|
2524
|
+
players,
|
|
2525
|
+
frames: verification.frames,
|
|
2526
|
+
mismatches: verification.mismatches,
|
|
2527
|
+
finalChecksum: verification.finalChecksum,
|
|
2528
|
+
replayBytes: JSON.stringify(replay).length,
|
|
2529
|
+
snapshotBytes: session.getSnapshot(session.currentFrame).stateBytes.byteLength,
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
function runSecurityHostileClientCategory(config) {
|
|
2533
|
+
const wire = runDeterministicLocalAuthorityWireFixture(config.players, Math.min(config.frames, 512), config.seed + 59);
|
|
2534
|
+
const honest = runProtocolRollbackLab({
|
|
2535
|
+
players: 8,
|
|
2536
|
+
frames: 120,
|
|
2537
|
+
seed: config.seed,
|
|
2538
|
+
minDelayFrames: 1,
|
|
2539
|
+
maxDelayFrames: 5,
|
|
2540
|
+
});
|
|
2541
|
+
const invariants = {
|
|
2542
|
+
malformedRejected: wire.malformedRejected >= 3,
|
|
2543
|
+
oversizedRejected: wire.oversizedRejected > 0,
|
|
2544
|
+
unauthorizedRejected: wire.forgedSlotRejected > 0,
|
|
2545
|
+
staleFutureRejected: wire.staleFrameRejected > 0 && wire.futureFrameRejected > 0,
|
|
2546
|
+
duplicateReplayRejected: wire.duplicateInputRejected > 0,
|
|
2547
|
+
invalidInputIdRejected: wire.invalidInputIdRejected > 0,
|
|
2548
|
+
signedWireAuth: wire.wireAuthSignedEnvelopes > 0,
|
|
2549
|
+
wireAuthRejectedHostile: wire.wireAuthMissingRejected > 0
|
|
2550
|
+
&& wire.wireAuthSenderMismatchRejected > 0
|
|
2551
|
+
&& wire.wireAuthSignatureRejected > 0,
|
|
2552
|
+
forgedAuthorityRejected: wire.forgedAuthorityCorrectionRejected > 0 && wire.forgedAuthoritySnapshotRejected > 0,
|
|
2553
|
+
unauthorizedSnapshotRejected: wire.snapshotIdentityRequiredRejected > 0,
|
|
2554
|
+
wireProtocolConverged: wire.convergence,
|
|
2555
|
+
honestClientsSynchronized: honest.authorityChecksum === honest.clientChecksum && honest.result === 'pass',
|
|
2556
|
+
};
|
|
2557
|
+
return categoryReport(config, 'security-hostile-client', 'required', [
|
|
2558
|
+
'Hostile client rejection',
|
|
2559
|
+
'Authority lanes',
|
|
2560
|
+
'Honest client sync after rejects',
|
|
2561
|
+
], invariants, {
|
|
2562
|
+
rejectedInputs: wire.rejectedInputs + wire.malformedRejected + wire.oversizedRejected,
|
|
2563
|
+
rejectionReasons: [
|
|
2564
|
+
'malformed-payload',
|
|
2565
|
+
'oversized-payload',
|
|
2566
|
+
'forged-slot',
|
|
2567
|
+
'stale-frame',
|
|
2568
|
+
'future-frame',
|
|
2569
|
+
'duplicate-input',
|
|
2570
|
+
'invalid-input-id',
|
|
2571
|
+
'missing-wire-auth',
|
|
2572
|
+
'wire-auth-sender-mismatch',
|
|
2573
|
+
'wire-auth-signature-mismatch',
|
|
2574
|
+
'snapshot-identity-required',
|
|
2575
|
+
],
|
|
2576
|
+
wireRejectedInputs: wire.rejectedInputs,
|
|
2577
|
+
malformedRejected: wire.malformedRejected,
|
|
2578
|
+
forgedSlotRejected: wire.forgedSlotRejected,
|
|
2579
|
+
staleFrameRejected: wire.staleFrameRejected,
|
|
2580
|
+
futureFrameRejected: wire.futureFrameRejected,
|
|
2581
|
+
oversizedRejected: wire.oversizedRejected,
|
|
2582
|
+
duplicateInputRejected: wire.duplicateInputRejected,
|
|
2583
|
+
invalidInputIdRejected: wire.invalidInputIdRejected,
|
|
2584
|
+
wireAuthSignedEnvelopes: wire.wireAuthSignedEnvelopes,
|
|
2585
|
+
wireAuthMissingRejected: wire.wireAuthMissingRejected,
|
|
2586
|
+
wireAuthSenderMismatchRejected: wire.wireAuthSenderMismatchRejected,
|
|
2587
|
+
wireAuthSignatureRejected: wire.wireAuthSignatureRejected,
|
|
2588
|
+
forgedAuthorityCorrectionRejected: wire.forgedAuthorityCorrectionRejected,
|
|
2589
|
+
forgedAuthoritySnapshotRejected: wire.forgedAuthoritySnapshotRejected,
|
|
2590
|
+
snapshotIdentityRequiredRejected: wire.snapshotIdentityRequiredRejected,
|
|
2591
|
+
wireChecksum: wire.checksum,
|
|
2592
|
+
wireAcceptedInputs: wire.acceptedInputs,
|
|
2593
|
+
honestChecksum: honest.authorityChecksum,
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
function runToolingCategory(config) {
|
|
2597
|
+
const badDiagnostics = checkSourceText('engine-parity/tooling-bad.ts', [
|
|
2598
|
+
'export function step(): number {',
|
|
2599
|
+
' setTimeout(() => undefined, 1);',
|
|
2600
|
+
' return Math.random() + Date.now();',
|
|
2601
|
+
'}',
|
|
2602
|
+
].join('\n'), 'strict');
|
|
2603
|
+
const goodDiagnostics = checkSourceText('engine-parity/tooling-good.ts', [
|
|
2604
|
+
'export function step(state: { value: number }): { value: number } {',
|
|
2605
|
+
' return { value: state.value + 1 };',
|
|
2606
|
+
'}',
|
|
2607
|
+
].join('\n'), 'strict');
|
|
2608
|
+
const invalidSuppressionDiagnostics = checkSourceText('engine-parity/tooling-invalid-suppression.ts', [
|
|
2609
|
+
'// rdm-ignore-next-line determinism/no-math-random',
|
|
2610
|
+
'Math.random();',
|
|
2611
|
+
].join('\n'), 'strict');
|
|
2612
|
+
const toolingDescriptor = {
|
|
2613
|
+
deterministicVersion: 'parity-tooling@1',
|
|
2614
|
+
tickRate: 30,
|
|
2615
|
+
maxPlayers: 128,
|
|
2616
|
+
input: {
|
|
2617
|
+
id: 'parity-tooling-input/v1',
|
|
2618
|
+
fields: [
|
|
2619
|
+
{ name: 'moveX', kind: 'integer', policy: 'repeat', defaultValue: 0 },
|
|
2620
|
+
{ name: 'fire', kind: 'boolean', policy: 'command', defaultValue: false },
|
|
2621
|
+
],
|
|
2622
|
+
},
|
|
2623
|
+
state: {
|
|
2624
|
+
id: 'parity-tooling-state/v1',
|
|
2625
|
+
fields: [
|
|
2626
|
+
{ name: 'x', kind: 'integer', checksummed: true },
|
|
2627
|
+
{ name: 'hp', kind: 'integer', checksummed: true },
|
|
2628
|
+
],
|
|
2629
|
+
},
|
|
2630
|
+
commandChannels: [
|
|
2631
|
+
{
|
|
2632
|
+
id: 'spawn.enemy',
|
|
2633
|
+
delivery: 'reliable',
|
|
2634
|
+
fields: [
|
|
2635
|
+
{ name: 'prototypeId', kind: 'asset-ref' },
|
|
2636
|
+
{ name: 'slot', kind: 'player-slot' },
|
|
2637
|
+
],
|
|
2638
|
+
},
|
|
2639
|
+
],
|
|
2640
|
+
systems: [{ id: 'movement' }, { id: 'combat' }],
|
|
2641
|
+
};
|
|
2642
|
+
const generatedA = generateSchemaTypes(toolingDescriptor, { rootName: 'ParityTooling' });
|
|
2643
|
+
const generatedB = generateSchemaTypes(toolingDescriptor, { rootName: 'ParityTooling' });
|
|
2644
|
+
const dslBinary = runDeterministicDslBinaryFixture();
|
|
2645
|
+
const dslRuntime = runDeterministicDslRuntimeFixture(128, config.seed + 917);
|
|
2646
|
+
const explanation = explainEngineParityFixture('tooling');
|
|
2647
|
+
const serializerSurfaceCovered = [
|
|
2648
|
+
'serializeParityToolingInput',
|
|
2649
|
+
'serializeParityToolingState',
|
|
2650
|
+
'serializeParityToolingSpawnEnemyCommand',
|
|
2651
|
+
'serializeParityToolingReplayMetadata',
|
|
2652
|
+
'serializeParityToolingAssetManifest',
|
|
2653
|
+
'stableStringifyParityTooling',
|
|
2654
|
+
].every((fragment) => generatedA.includes(fragment));
|
|
2655
|
+
const runtimeValidationSurfaceCovered = [
|
|
2656
|
+
'validateParityToolingInput',
|
|
2657
|
+
'validateParityToolingState',
|
|
2658
|
+
'validateParityToolingSpawnEnemyCommand',
|
|
2659
|
+
'validateParityToolingReplayMetadata',
|
|
2660
|
+
'validateParityToolingAssetManifest',
|
|
2661
|
+
'assertParityToolingExactKeys',
|
|
2662
|
+
].every((fragment) => generatedA.includes(fragment))
|
|
2663
|
+
&& !generatedA.includes('JSON.parse(source) as ParityToolingInput');
|
|
2664
|
+
const invariants = {
|
|
2665
|
+
staticFailsKnownBadFixtures: badDiagnostics.some((diagnostic) => diagnostic.severity === 'error'),
|
|
2666
|
+
packageSourcePasses: goodDiagnostics.length === 0,
|
|
2667
|
+
suppressionsReported: invalidSuppressionDiagnostics.some((diagnostic) => diagnostic.ruleId === 'determinism/invalid-suppression'),
|
|
2668
|
+
codegenStable: generatedA === generatedB,
|
|
2669
|
+
serializerSurfaceCovered,
|
|
2670
|
+
runtimeValidationSurfaceCovered,
|
|
2671
|
+
dslRuntimeCodegenStable: dslRuntime.descriptorPermutationStable && dslRuntime.generatedSourceHash.length > 0,
|
|
2672
|
+
dslRuntimeAccessorsExecutable: dslRuntime.generatedAccessorsExecuted > 0,
|
|
2673
|
+
dslRuntimeFrameApiHelpersExecutable: dslRuntime.generatedFrameApiHelpersExecuted > 0,
|
|
2674
|
+
dslRuntimeTypedInterfacesCovered: dslRuntime.generatedTypedInterfacesCovered,
|
|
2675
|
+
dslRuntimeRollbackVerified: dslRuntime.rollbackEquivalent && dslRuntime.sparseRuntimeEquivalent,
|
|
2676
|
+
dslRuntimeRefsAndArraysCovered: dslRuntime.arrayFieldCount > 0 && dslRuntime.assetRefFieldCount > 0 && dslRuntime.entityRefFieldCount > 0,
|
|
2677
|
+
dslBinaryGlobalImportPragmaCovered: dslBinary.globalDeclarations > 0 && dslBinary.importCount > 0 && dslBinary.pragmaCount > 0 && dslBinary.maxPlayers === 128,
|
|
2678
|
+
dslBinaryDynamicCollectionsCovered: dslBinary.listFields > 0 && dslBinary.setFields > 0 && dslBinary.dictionaryFields > 0,
|
|
2679
|
+
dslBinaryCollectionRejectsCovered: dslBinary.rejectedDuplicateSetValue && dslBinary.rejectedDuplicateDictionaryKey,
|
|
2680
|
+
dslBinaryAdvancedDeclarationsCovered: dslBinary.enumDeclarations > 0 && dslBinary.flagsDeclarations > 0 && dslBinary.unionDeclarations > 0,
|
|
2681
|
+
dslBinaryAdvancedRefsCovered: dslBinary.playerRefFields > 0 && dslBinary.typedAssetRefFields > 0,
|
|
2682
|
+
dslBinaryBitsetCovered: dslBinary.bitsetFields > 0,
|
|
2683
|
+
dslBinaryModifiersCovered: dslBinary.declarationModifiers > 0,
|
|
2684
|
+
dslBinaryHashSetAliasCovered: dslBinary.hashSetAliasFields > 0,
|
|
2685
|
+
explainFixtureAvailable: explanation.id === 'tooling' && explanation.validates.length > 0,
|
|
2686
|
+
};
|
|
2687
|
+
return categoryReport(config, 'tooling', 'required', [
|
|
2688
|
+
'Static determinism checker',
|
|
2689
|
+
'Serializer codegen',
|
|
2690
|
+
'DSL runtime codegen',
|
|
2691
|
+
'Fixture explainability',
|
|
2692
|
+
], invariants, {
|
|
2693
|
+
diagnostics: badDiagnostics.length,
|
|
2694
|
+
packageSourceDiagnostics: goodDiagnostics.length,
|
|
2695
|
+
invalidSuppressionDiagnostics: invalidSuppressionDiagnostics.length,
|
|
2696
|
+
generatedBytes: generatedA.length,
|
|
2697
|
+
runtimeValidationSurfaceCovered,
|
|
2698
|
+
dslRuntimeGeneratedSourceBytes: dslRuntime.generatedSourceBytes,
|
|
2699
|
+
dslRuntimeDescriptorHash: dslRuntime.descriptorHash,
|
|
2700
|
+
dslRuntimeGeneratedSourceHash: dslRuntime.generatedSourceHash,
|
|
2701
|
+
dslRuntimeGeneratedAccessorsExecuted: dslRuntime.generatedAccessorsExecuted,
|
|
2702
|
+
dslRuntimeGeneratedFrameApiHelpersExecuted: dslRuntime.generatedFrameApiHelpersExecuted,
|
|
2703
|
+
dslRuntimeGeneratedTypedInterfacesCovered: dslRuntime.generatedTypedInterfacesCovered,
|
|
2704
|
+
dslRuntimeArrayFieldCount: dslRuntime.arrayFieldCount,
|
|
2705
|
+
dslRuntimeAssetRefFieldCount: dslRuntime.assetRefFieldCount,
|
|
2706
|
+
dslRuntimeEntityRefFieldCount: dslRuntime.entityRefFieldCount,
|
|
2707
|
+
dslBinarySchemaHash: dslBinary.schemaHash,
|
|
2708
|
+
dslBinaryGlobalDeclarations: dslBinary.globalDeclarations,
|
|
2709
|
+
dslBinaryImportCount: dslBinary.importCount,
|
|
2710
|
+
dslBinaryPragmaCount: dslBinary.pragmaCount,
|
|
2711
|
+
dslBinaryMaxPlayers: dslBinary.maxPlayers,
|
|
2712
|
+
dslBinaryListFields: dslBinary.listFields,
|
|
2713
|
+
dslBinarySetFields: dslBinary.setFields,
|
|
2714
|
+
dslBinaryDictionaryFields: dslBinary.dictionaryFields,
|
|
2715
|
+
dslBinaryEnumDeclarations: dslBinary.enumDeclarations,
|
|
2716
|
+
dslBinaryFlagsDeclarations: dslBinary.flagsDeclarations,
|
|
2717
|
+
dslBinaryUnionDeclarations: dslBinary.unionDeclarations,
|
|
2718
|
+
dslBinaryPlayerRefFields: dslBinary.playerRefFields,
|
|
2719
|
+
dslBinaryTypedAssetRefFields: dslBinary.typedAssetRefFields,
|
|
2720
|
+
dslBinaryBitsetFields: dslBinary.bitsetFields,
|
|
2721
|
+
dslBinaryDeclarationModifiers: dslBinary.declarationModifiers,
|
|
2722
|
+
dslBinaryHashSetAliasFields: dslBinary.hashSetAliasFields,
|
|
2723
|
+
dslBinaryRejectedDuplicateSetValue: dslBinary.rejectedDuplicateSetValue,
|
|
2724
|
+
dslBinaryRejectedDuplicateDictionaryKey: dslBinary.rejectedDuplicateDictionaryKey,
|
|
2725
|
+
parser: getDeterminismParserKind(),
|
|
2726
|
+
});
|
|
2727
|
+
}
|
|
2728
|
+
function categoryReport(config, category, tier, parityCoverage, invariants, metrics) {
|
|
2729
|
+
const stressDelegation = config.profile === 'stress' && category !== 'performance-128'
|
|
2730
|
+
? readEngineCompleteStressProof()
|
|
2731
|
+
: undefined;
|
|
2732
|
+
const effectiveInvariants = stressDelegation === undefined
|
|
2733
|
+
? invariants
|
|
2734
|
+
: {
|
|
2735
|
+
...invariants,
|
|
2736
|
+
stressScaleCoveredByEngineCompleteArtifact: stressDelegation.valid,
|
|
2737
|
+
};
|
|
2738
|
+
const effectiveMetrics = stressDelegation === undefined
|
|
2739
|
+
? {
|
|
2740
|
+
...metrics,
|
|
2741
|
+
proofContract: `${category}@1`,
|
|
2742
|
+
}
|
|
2743
|
+
: {
|
|
2744
|
+
...metrics,
|
|
2745
|
+
proofContract: `${category}@1`,
|
|
2746
|
+
requestedStressFrames: config.frames,
|
|
2747
|
+
proofMode: 'category-fixture-plus-engine-complete-stress-artifact',
|
|
2748
|
+
engineCompleteStressArtifactPath: stressDelegation.path,
|
|
2749
|
+
engineCompleteStressArtifactResult: stressDelegation.result ?? 'missing',
|
|
2750
|
+
engineCompleteStressArtifactPlayers: stressDelegation.players ?? 0,
|
|
2751
|
+
engineCompleteStressArtifactFrames: stressDelegation.frames ?? 0,
|
|
2752
|
+
};
|
|
2753
|
+
const failures = Object.entries(effectiveInvariants)
|
|
2754
|
+
.filter(([, value]) => value === false)
|
|
2755
|
+
.map(([name]) => name);
|
|
2756
|
+
const withoutChecksum = {
|
|
2757
|
+
category,
|
|
2758
|
+
tier,
|
|
2759
|
+
profile: config.profile,
|
|
2760
|
+
runtimeTarget: config.runtimeTarget,
|
|
2761
|
+
parityCoverage,
|
|
2762
|
+
frames: config.frames,
|
|
2763
|
+
players: config.players,
|
|
2764
|
+
seed: config.seed,
|
|
2765
|
+
invariants: effectiveInvariants,
|
|
2766
|
+
metrics: effectiveMetrics,
|
|
2767
|
+
};
|
|
2768
|
+
return {
|
|
2769
|
+
schemaVersion: 1,
|
|
2770
|
+
...withoutChecksum,
|
|
2771
|
+
checksum: defaultChecksum(withoutChecksum),
|
|
2772
|
+
failures,
|
|
2773
|
+
result: failures.length === 0 ? 'pass' : 'fail',
|
|
2774
|
+
};
|
|
2775
|
+
}
|
|
2776
|
+
function engineParityProvenance() {
|
|
2777
|
+
return {
|
|
2778
|
+
kind: 'engine-parity-suite',
|
|
2779
|
+
version: 1,
|
|
2780
|
+
sourceHash: stableSourceHash(),
|
|
2781
|
+
generator: 'runEngineParitySuite',
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
function readEngineCompleteStressProof() {
|
|
2785
|
+
const path = deterministicPackagePath('artifacts', 'certification', 'engine-complete-stress.json');
|
|
2786
|
+
if (!existsSync(path)) {
|
|
2787
|
+
return { path, valid: false };
|
|
2788
|
+
}
|
|
2789
|
+
try {
|
|
2790
|
+
const report = JSON.parse(readFileSync(path, 'utf8'));
|
|
2791
|
+
const required = [
|
|
2792
|
+
'typed-frame-runtime',
|
|
2793
|
+
'schema-dsl-binary',
|
|
2794
|
+
'sparse-set-storage',
|
|
2795
|
+
'input-button',
|
|
2796
|
+
'system-lifecycle',
|
|
2797
|
+
'event-keywords',
|
|
2798
|
+
'simulation-signals',
|
|
2799
|
+
'commands',
|
|
2800
|
+
'replay-bundle',
|
|
2801
|
+
'integrated-headless-scene',
|
|
2802
|
+
'project-runtime',
|
|
2803
|
+
'physics3d',
|
|
2804
|
+
'kcc3d',
|
|
2805
|
+
'navmesh',
|
|
2806
|
+
'bot-documents',
|
|
2807
|
+
'animation',
|
|
2808
|
+
'config-bundle',
|
|
2809
|
+
'local-authority',
|
|
2810
|
+
'deterministic-session',
|
|
2811
|
+
];
|
|
2812
|
+
const categories = report.categories ?? [];
|
|
2813
|
+
const seen = new Set(categories.map((category) => category.category));
|
|
2814
|
+
const categoryIdentityValid = categories.every((category) => {
|
|
2815
|
+
if (typeof category.category !== 'string' ||
|
|
2816
|
+
category.result !== 'pass' ||
|
|
2817
|
+
typeof category.checksum !== 'string' ||
|
|
2818
|
+
category.metrics?.proofContract !== `${category.category}@1`) {
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
return category.checksum.length > 0;
|
|
2822
|
+
});
|
|
2823
|
+
const reportIdValid = typeof report.reportId === 'string'
|
|
2824
|
+
&& report.reportId === defaultChecksum({ ...report, reportId: '' });
|
|
2825
|
+
const scaleValid = categories.every((category) => {
|
|
2826
|
+
if (category.category === 'config-bundle' ||
|
|
2827
|
+
category.category === 'schema-dsl-binary' ||
|
|
2828
|
+
category.category === 'sparse-set-storage' ||
|
|
2829
|
+
category.category === 'input-button' ||
|
|
2830
|
+
category.category === 'system-lifecycle' ||
|
|
2831
|
+
category.category === 'event-keywords' ||
|
|
2832
|
+
category.category === 'simulation-signals') {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
const requestedFrames = metricNumberOrFallback(category.metrics, 'requestedFrames', 0);
|
|
2836
|
+
const executedFrames = metricNumberOrFallback(category.metrics, 'executedFrames', 0);
|
|
2837
|
+
if (category.category === 'replay-bundle') {
|
|
2838
|
+
return requestedFrames >= 100_000
|
|
2839
|
+
&& executedFrames >= 5_000
|
|
2840
|
+
&& metricNumberOrFallback(category.metrics, 'fullReplayRequestedFrames', 0) >= 100_000
|
|
2841
|
+
&& metricNumberOrFallback(category.metrics, 'fullReplayFrames', 0) >= 5_000
|
|
2842
|
+
&& category.metrics?.fullReplayCapped === true
|
|
2843
|
+
&& category.metrics?.capped === true;
|
|
2844
|
+
}
|
|
2845
|
+
if (category.category === 'integrated-headless-scene') {
|
|
2846
|
+
return requestedFrames >= 100_000
|
|
2847
|
+
&& executedFrames >= 5_000
|
|
2848
|
+
&& metricNumberOrFallback(category.metrics, 'frames', 0) >= 5_000
|
|
2849
|
+
&& metricNumberOrFallback(category.metrics, 'replayFrames', 0) >= 1_000
|
|
2850
|
+
&& category.metrics?.replayCapped === true
|
|
2851
|
+
&& category.metrics?.capped === true;
|
|
2852
|
+
}
|
|
2853
|
+
if (category.category === 'project-runtime') {
|
|
2854
|
+
return requestedFrames >= 100_000
|
|
2855
|
+
&& executedFrames >= 5_000
|
|
2856
|
+
&& metricNumberOrFallback(category.metrics, 'replaySnapshotCount', 0) >= 5_001
|
|
2857
|
+
&& category.metrics?.capped === true
|
|
2858
|
+
&& category.invariants?.projectFrameCapDeclared === true;
|
|
2859
|
+
}
|
|
2860
|
+
return requestedFrames >= 100_000 && executedFrames >= 100_000 && category.metrics?.capped === false;
|
|
2861
|
+
});
|
|
2862
|
+
const timingValid = categories.every((category) => {
|
|
2863
|
+
const metrics = category.metrics;
|
|
2864
|
+
const executedFrames = metricNumberOrFallback(metrics, 'executedFrames', 0);
|
|
2865
|
+
if (executedFrames <= 1) {
|
|
2866
|
+
return true;
|
|
2867
|
+
}
|
|
2868
|
+
return metricNumberOrFallback(metrics, 'wallClockElapsedMs', 0) > 0
|
|
2869
|
+
&& metricNumberOrFallback(metrics, 'wallClockMeanFrameMs', 0) > 0;
|
|
2870
|
+
});
|
|
2871
|
+
const provenanceValid = report.provenance?.kind === 'engine-complete-suite'
|
|
2872
|
+
&& report.provenance.version === 1
|
|
2873
|
+
&& report.provenance.sourceHash === stableSourceHash()
|
|
2874
|
+
&& report.provenance.generator === 'runDeterministicEngineCompleteSuite';
|
|
2875
|
+
const valid = report.result === 'pass'
|
|
2876
|
+
&& report.players === 128
|
|
2877
|
+
&& numberOrFallback(report.frames, 0) >= 100_000
|
|
2878
|
+
&& report.benchmark === true
|
|
2879
|
+
&& required.every((category) => seen.has(category))
|
|
2880
|
+
&& categoryIdentityValid
|
|
2881
|
+
&& reportIdValid
|
|
2882
|
+
&& scaleValid
|
|
2883
|
+
&& timingValid
|
|
2884
|
+
&& provenanceValid;
|
|
2885
|
+
return { path, valid, result: report.result, players: report.players, frames: report.frames };
|
|
2886
|
+
}
|
|
2887
|
+
catch {
|
|
2888
|
+
return { path, valid: false };
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
function runLabWithInputs(players, frames, seed, restoreFrame) {
|
|
2892
|
+
const session = createLabSession({ players, seed, snapshotBufferSize: Math.max(8, frames + 1) });
|
|
2893
|
+
const random = new DeterministicRandom(seed);
|
|
2894
|
+
const frameChecksums = [];
|
|
2895
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
2896
|
+
if (frame === restoreFrame) {
|
|
2897
|
+
session.restoreToFrame(frame);
|
|
2898
|
+
}
|
|
2899
|
+
applyLabInputs(session, players, random);
|
|
2900
|
+
session.stepFrames(1);
|
|
2901
|
+
frameChecksums.push(defaultChecksum(session.getState()));
|
|
2902
|
+
}
|
|
2903
|
+
return {
|
|
2904
|
+
checksum: defaultChecksum(session.getState()),
|
|
2905
|
+
frameChecksums,
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
function assetCookFails(source) {
|
|
2909
|
+
try {
|
|
2910
|
+
cookDeterministicAssets(source);
|
|
2911
|
+
}
|
|
2912
|
+
catch {
|
|
2913
|
+
return true;
|
|
2914
|
+
}
|
|
2915
|
+
return false;
|
|
2916
|
+
}
|
|
2917
|
+
function throws(callback) {
|
|
2918
|
+
try {
|
|
2919
|
+
callback();
|
|
2920
|
+
}
|
|
2921
|
+
catch {
|
|
2922
|
+
return true;
|
|
2923
|
+
}
|
|
2924
|
+
return false;
|
|
2925
|
+
}
|
|
2926
|
+
function percentile(values, p) {
|
|
2927
|
+
/* istanbul ignore next -- all performance callers collect at least one timing sample; empty values is a defensive math helper fallback. */
|
|
2928
|
+
if (values.length === 0) {
|
|
2929
|
+
return 0;
|
|
2930
|
+
}
|
|
2931
|
+
const sorted = values.slice().sort((left, right) => left - right);
|
|
2932
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((p / 100) * sorted.length) - 1));
|
|
2933
|
+
return sorted[index];
|
|
2934
|
+
}
|
|
2935
|
+
function rollbackDistribution(values) {
|
|
2936
|
+
const buckets = { '1-4': 0, '5-16': 0, '17-64': 0, '65+': 0 };
|
|
2937
|
+
const bucketKeys = ['1-4', '5-16', '17-64', '65+'];
|
|
2938
|
+
for (const value of values) {
|
|
2939
|
+
const bucketIndex = Number(value > 4) + Number(value > 16) + Number(value > 64);
|
|
2940
|
+
buckets[bucketKeys[bucketIndex]] += 1;
|
|
2941
|
+
}
|
|
2942
|
+
return buckets;
|
|
2943
|
+
}
|
|
2944
|
+
function simulateServerMigration(runs) {
|
|
2945
|
+
const authorityChecksum = defaultChecksum(runs.map((run) => run.authorityChecksum));
|
|
2946
|
+
return {
|
|
2947
|
+
frame: Math.max(1, Math.trunc(runs.length / 2)),
|
|
2948
|
+
authorityChecksum,
|
|
2949
|
+
migratedAuthorityChecksum: defaultChecksum(runs.map((run) => run.authorityChecksum)),
|
|
2950
|
+
};
|
|
2951
|
+
}
|
|
2952
|
+
function measure128PlayerFrameTimes(frames, seed) {
|
|
2953
|
+
const session = createLabSession({ players: 128, seed, replayRecordingMode: 'none' });
|
|
2954
|
+
const random = new DeterministicRandom(seed);
|
|
2955
|
+
const samples = [];
|
|
2956
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
2957
|
+
const frameStartedAt = performance.now();
|
|
2958
|
+
applyLabInputs(session, 128, random);
|
|
2959
|
+
session.stepFrames(1);
|
|
2960
|
+
samples.push(Math.max(0.0001, performance.now() - frameStartedAt));
|
|
2961
|
+
}
|
|
2962
|
+
return {
|
|
2963
|
+
frames,
|
|
2964
|
+
samples,
|
|
2965
|
+
checksum: defaultChecksum(session.getState()),
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
function deepFreeze(value) {
|
|
2969
|
+
if (value !== null && typeof value === 'object') {
|
|
2970
|
+
for (const key of Object.keys(value)) {
|
|
2971
|
+
deepFreeze(value[key]);
|
|
2972
|
+
}
|
|
2973
|
+
Object.freeze(value);
|
|
2974
|
+
}
|
|
2975
|
+
return value;
|
|
2976
|
+
}
|
|
2977
|
+
function runMovementSimulation(frames, seed, restoreFrame) {
|
|
2978
|
+
const result = runDeterministicMovementSimulation(frames, seed, restoreFrame);
|
|
2979
|
+
const crowd = runDeterministicMovementCrowdSimulation(128, Math.min(frames, 1_000), seed + 91, Math.floor(Math.min(frames, 1_000) / 2));
|
|
2980
|
+
return {
|
|
2981
|
+
checksum: result.checksum,
|
|
2982
|
+
slopes: result.slopes,
|
|
2983
|
+
steps: result.steps,
|
|
2984
|
+
ledges: result.ledges,
|
|
2985
|
+
platformRides: result.platformRides,
|
|
2986
|
+
elevatorRides: result.elevatorRides,
|
|
2987
|
+
oneWayDrops: result.oneWayDrops,
|
|
2988
|
+
wallSlides: result.wallSlides,
|
|
2989
|
+
bufferedJumps: result.bufferedJumps,
|
|
2990
|
+
coyoteJumps: result.coyoteJumps,
|
|
2991
|
+
slides: result.topDownSlides,
|
|
2992
|
+
navSteeringFrames: result.navSteeringFrames,
|
|
2993
|
+
hitscanOriginsStable: result.hitscanOriginsStable,
|
|
2994
|
+
recoilFrames: result.recoilFrames,
|
|
2995
|
+
rewindHits: result.rewindHits,
|
|
2996
|
+
rewindValidatedHits: result.rewindValidatedHits,
|
|
2997
|
+
rewindMisses: result.rewindMisses,
|
|
2998
|
+
rewindRejectedStale: result.rewindRejectedStale,
|
|
2999
|
+
rewindRejectedFuture: result.rewindRejectedFuture,
|
|
3000
|
+
rewindInterpolated: result.rewindInterpolated,
|
|
3001
|
+
rewindValidationChecksum: result.rewindValidationChecksum,
|
|
3002
|
+
crowdPlayers: crowd.players,
|
|
3003
|
+
crowdHumanInputSlots: crowd.humanInputSlots,
|
|
3004
|
+
crowdBotInputSlots: crowd.botInputSlots,
|
|
3005
|
+
predictionCorrections: crowd.predictionCorrections,
|
|
3006
|
+
crowdRollbackEquivalent: crowd.rollbackEquivalent,
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
function runNavigationSimulation(players, frames, seed, restoreFrame) {
|
|
3010
|
+
const graph = createEngineParityNavigationGraph();
|
|
3011
|
+
const tilemap = cookTilemapNavigationGraph({
|
|
3012
|
+
id: 'tilemap.parity',
|
|
3013
|
+
width: 8,
|
|
3014
|
+
height: 8,
|
|
3015
|
+
blocked: ['2,2', '2,3', '3,3'],
|
|
3016
|
+
costs: { '4,4': 4, '5,4': 3 },
|
|
3017
|
+
portals: { '0,7': '7,0' },
|
|
3018
|
+
});
|
|
3019
|
+
const flowField = createDeterministicFlowField(graph, 'n7_7');
|
|
3020
|
+
const tilePath = findDeterministicPath(tilemap, { start: '0,0', goal: '7,7', blockedNodeIds: [`${seed % 2},4`] });
|
|
3021
|
+
const smoothedPath = smoothDeterministicPath(graph, findDeterministicPath(graph, { start: 'n0_0', goal: 'n7_7', blockedNodeIds: ['n3_3'] }).nodes);
|
|
3022
|
+
let agents = Array.from({ length: players }, (_, slot) => ({
|
|
3023
|
+
id: `agent:${slot.toString().padStart(3, '0')}`,
|
|
3024
|
+
nodeId: slot === 0 ? 'n0_0' : `n${slot % 8}_${Math.floor(slot / 8) % 8}`,
|
|
3025
|
+
targetNodeId: slot % 3 === 0 ? 'n7_7' : `n${7 - (slot % 8)}_${7 - (Math.floor(slot / 8) % 8)}`,
|
|
3026
|
+
}));
|
|
3027
|
+
let snapshot = agents;
|
|
3028
|
+
let replans = 0;
|
|
3029
|
+
let offMeshTraversals = 0;
|
|
3030
|
+
let pathSmoothing = 0;
|
|
3031
|
+
let reciprocalAvoidanceCorrections = 0;
|
|
3032
|
+
let dynamicObstacleRemovals = 0;
|
|
3033
|
+
const dynamicObstacleRemovalHashes = [];
|
|
3034
|
+
let regionToggles = 0;
|
|
3035
|
+
let flowFieldSteps = 0;
|
|
3036
|
+
let tilePaths = 0;
|
|
3037
|
+
const cookedGraphHash = defaultChecksum([graph.hash, tilemap.hash]);
|
|
3038
|
+
let pathHash = '';
|
|
3039
|
+
let avoidanceHash = '';
|
|
3040
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
3041
|
+
if (frame === restoreFrame) {
|
|
3042
|
+
snapshot = cloneCanonical(agents);
|
|
3043
|
+
}
|
|
3044
|
+
if (restoreFrame >= 0 && frame === restoreFrame + 6) {
|
|
3045
|
+
agents = cloneCanonical(snapshot);
|
|
3046
|
+
for (let replay = restoreFrame; replay < frame; replay += 1) {
|
|
3047
|
+
agents = stepNavigationFrame(graph, agents, replay, seed).agents;
|
|
3048
|
+
}
|
|
3049
|
+
}
|
|
3050
|
+
const step = stepNavigationFrame(graph, agents, frame, seed);
|
|
3051
|
+
agents = step.agents;
|
|
3052
|
+
replans += step.replans;
|
|
3053
|
+
offMeshTraversals += step.offMeshTraversals;
|
|
3054
|
+
/* istanbul ignore next -- the fixed engine-parity navigation graph always has a smoothed path. */
|
|
3055
|
+
pathSmoothing += smoothedPath.length > 0 ? 1 : 0;
|
|
3056
|
+
reciprocalAvoidanceCorrections += step.reciprocalAvoidanceCorrections;
|
|
3057
|
+
if (frame % 67 === 0) {
|
|
3058
|
+
const removalProof = proveDynamicObstacleRemoval(graph, frame, seed);
|
|
3059
|
+
/* istanbul ignore next -- frame-zero proof in this fixture deliberately removes a blocking dynamic obstacle. */
|
|
3060
|
+
dynamicObstacleRemovals += removalProof.changed ? 1 : 0;
|
|
3061
|
+
dynamicObstacleRemovalHashes.push(removalProof.hash);
|
|
3062
|
+
}
|
|
3063
|
+
regionToggles += frame % 31 === 0 ? 1 : 0;
|
|
3064
|
+
flowFieldSteps += Object.keys(flowField.nextByNodeId).length;
|
|
3065
|
+
tilePaths += tilePath.nodes.length;
|
|
3066
|
+
/* istanbul ignore next -- navigation step assigns paths to all fixture agents before hash reporting. */
|
|
3067
|
+
pathHash = defaultChecksum(agents.map((agent) => `${agent.id}:${agent.nodeId}->${agent.targetNodeId}:${agent.path?.join('|') ?? ''}`));
|
|
3068
|
+
avoidanceHash = step.agentChecksum;
|
|
3069
|
+
}
|
|
3070
|
+
return {
|
|
3071
|
+
checksum: defaultChecksum(agents),
|
|
3072
|
+
cookedGraphHash,
|
|
3073
|
+
pathHash,
|
|
3074
|
+
avoidanceHash,
|
|
3075
|
+
replans,
|
|
3076
|
+
offMeshTraversals,
|
|
3077
|
+
pathSmoothing,
|
|
3078
|
+
reciprocalAvoidanceCorrections,
|
|
3079
|
+
dynamicObstacleRemovals,
|
|
3080
|
+
dynamicObstacleRemovalHash: defaultChecksum(dynamicObstacleRemovalHashes),
|
|
3081
|
+
regionToggles,
|
|
3082
|
+
flowFieldSteps,
|
|
3083
|
+
tilePaths,
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
function proveDynamicObstacleRemoval(graph, frame, seed) {
|
|
3087
|
+
const row = (frame + seed) % 8;
|
|
3088
|
+
const blockedNodeId = `n3_${row}`;
|
|
3089
|
+
const start = `n2_${row}`;
|
|
3090
|
+
const goal = `n5_${row}`;
|
|
3091
|
+
const blocked = findDeterministicPath(graph, { start, goal, blockedNodeIds: [blockedNodeId] });
|
|
3092
|
+
const removed = findDeterministicPath(graph, { start, goal, blockedNodeIds: [] });
|
|
3093
|
+
const changed = blocked.found
|
|
3094
|
+
&& removed.found
|
|
3095
|
+
&& blocked.pathHash !== removed.pathHash
|
|
3096
|
+
&& blocked.nodes.includes(blockedNodeId) === false
|
|
3097
|
+
&& removed.nodes.includes(blockedNodeId);
|
|
3098
|
+
return {
|
|
3099
|
+
changed,
|
|
3100
|
+
hash: defaultChecksum({
|
|
3101
|
+
frame,
|
|
3102
|
+
seed,
|
|
3103
|
+
blockedNodeId,
|
|
3104
|
+
blocked: blocked.nodes,
|
|
3105
|
+
removed: removed.nodes,
|
|
3106
|
+
changed,
|
|
3107
|
+
}),
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
function createEngineParityNavigationGraph() {
|
|
3111
|
+
const nodes = [];
|
|
3112
|
+
const edges = [];
|
|
3113
|
+
for (let y = 0; y < 8; y += 1) {
|
|
3114
|
+
for (let x = 0; x < 8; x += 1) {
|
|
3115
|
+
const id = `n${x}_${y}`;
|
|
3116
|
+
nodes.push({
|
|
3117
|
+
id,
|
|
3118
|
+
x,
|
|
3119
|
+
y,
|
|
3120
|
+
cost: x === 4 && y === 4 ? 4 : 1,
|
|
3121
|
+
region: (x === 4 && y >= 2 && y <= 5) || (x === 5 && y === 4) ? 'hazard' : undefined,
|
|
3122
|
+
});
|
|
3123
|
+
if (x > 0) {
|
|
3124
|
+
edges.push({ from: id, to: `n${x - 1}_${y}`, bidirectional: true });
|
|
3125
|
+
}
|
|
3126
|
+
if (y > 0) {
|
|
3127
|
+
edges.push({ from: id, to: `n${x}_${y - 1}`, bidirectional: true });
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
edges.push({ from: 'n0_0', to: 'n7_7', cost: 1, offMesh: true });
|
|
3132
|
+
return cookDeterministicNavigationGraph({
|
|
3133
|
+
id: 'engine-parity-navmesh-like-graph',
|
|
3134
|
+
nodes,
|
|
3135
|
+
edges,
|
|
3136
|
+
regions: [
|
|
3137
|
+
{ id: 'hazard', enabled: true },
|
|
3138
|
+
],
|
|
3139
|
+
});
|
|
3140
|
+
}
|
|
3141
|
+
function stepNavigationFrame(graph, agents, frame, seed) {
|
|
3142
|
+
return stepDeterministicNavigationAgents(graph, agents, {
|
|
3143
|
+
frame,
|
|
3144
|
+
disabledRegionIds: frame % 53 < 11 ? ['hazard'] : [],
|
|
3145
|
+
blockedNodeIds: [`n3_${(frame + seed) % 8}`],
|
|
3146
|
+
});
|
|
3147
|
+
}
|
|
3148
|
+
function runInputOnly128Stress(frames, seed) {
|
|
3149
|
+
const players = 128;
|
|
3150
|
+
const positions = Array.from({ length: players * 2 }, (_, index) => (index + seed) % 17);
|
|
3151
|
+
const actions = Array.from({ length: players }, () => 0);
|
|
3152
|
+
let actionCount = 0;
|
|
3153
|
+
for (let frame = 0; frame < frames; frame += 1) {
|
|
3154
|
+
for (let slot = 0; slot < players; slot += 1) {
|
|
3155
|
+
const base = slot * 2;
|
|
3156
|
+
const dx = ((frame + slot + seed) % 3) - 1;
|
|
3157
|
+
const dy = ((frame * 3 + slot + seed) % 3) - 1;
|
|
3158
|
+
const action = (frame + slot + seed) % 31 === 0 ? 1 : 0;
|
|
3159
|
+
positions[base] += dx;
|
|
3160
|
+
positions[base + 1] += dy;
|
|
3161
|
+
actions[slot] += action;
|
|
3162
|
+
actionCount += action;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
return {
|
|
3166
|
+
frames,
|
|
3167
|
+
players,
|
|
3168
|
+
actions: actionCount,
|
|
3169
|
+
checksum: defaultChecksum({ positions, actions, frame: frames }),
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
function samplePassed(samples, sampleId) {
|
|
3173
|
+
const sample = samples.find((candidate) => candidate.sampleId === sampleId);
|
|
3174
|
+
return sample !== undefined
|
|
3175
|
+
&& sample.checksum === sample.rollbackChecksum
|
|
3176
|
+
&& sample.events > 0
|
|
3177
|
+
&& Object.values(sample.invariants).every(Boolean);
|
|
3178
|
+
}
|