@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,82 @@
|
|
|
1
|
+
import { type DeterministicFrameRuntimeDescriptor } from './engine-runtime.js';
|
|
2
|
+
export interface DeterministicAssetSource {
|
|
3
|
+
readonly schemaVersion?: number;
|
|
4
|
+
readonly engineVersion?: string;
|
|
5
|
+
readonly dependencies?: readonly DeterministicAssetDependency[];
|
|
6
|
+
readonly assets: readonly DeterministicAssetRecord[];
|
|
7
|
+
}
|
|
8
|
+
export interface DeterministicAssetDependency {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DeterministicAssetRecord {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly type?: string;
|
|
15
|
+
readonly references?: readonly string[];
|
|
16
|
+
readonly prototype?: boolean;
|
|
17
|
+
readonly [key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
export interface DeterministicCookedAsset {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly type: string;
|
|
22
|
+
readonly hash: string;
|
|
23
|
+
readonly data: unknown;
|
|
24
|
+
}
|
|
25
|
+
export interface DeterministicAssetCookManifest {
|
|
26
|
+
readonly schemaVersion: 1;
|
|
27
|
+
readonly schemaHash: string;
|
|
28
|
+
readonly engineVersion: string;
|
|
29
|
+
readonly dependencyHash: string;
|
|
30
|
+
readonly dependencies: readonly DeterministicAssetDependency[];
|
|
31
|
+
readonly assetDbHash: string;
|
|
32
|
+
readonly lookupHash: string;
|
|
33
|
+
readonly prototypeIds: readonly string[];
|
|
34
|
+
readonly assetCount: number;
|
|
35
|
+
readonly assets: readonly {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly type: string;
|
|
38
|
+
readonly hash: string;
|
|
39
|
+
}[];
|
|
40
|
+
}
|
|
41
|
+
export interface DeterministicAssetCookResult {
|
|
42
|
+
readonly manifest: DeterministicAssetCookManifest;
|
|
43
|
+
readonly cookedAssets: readonly DeterministicCookedAsset[];
|
|
44
|
+
readonly bytes: string;
|
|
45
|
+
}
|
|
46
|
+
export interface DeterministicPrototypeEntity {
|
|
47
|
+
readonly entityId: number;
|
|
48
|
+
readonly prototypeId: string;
|
|
49
|
+
readonly assetDbHash: string;
|
|
50
|
+
readonly components: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface DeterministicPrototypeMaterializationOptions {
|
|
53
|
+
readonly startEntityId?: number;
|
|
54
|
+
readonly componentOverrides?: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface DeterministicMapMaterializationResult {
|
|
57
|
+
readonly mapId: string;
|
|
58
|
+
readonly assetDbHash: string;
|
|
59
|
+
readonly mapBakeHash: string;
|
|
60
|
+
readonly spawnCount: number;
|
|
61
|
+
readonly materializedEntityCount: number;
|
|
62
|
+
readonly prototypeIds: readonly string[];
|
|
63
|
+
readonly materializedEntities: readonly DeterministicPrototypeEntity[];
|
|
64
|
+
readonly checksum: string;
|
|
65
|
+
}
|
|
66
|
+
export interface DeterministicPrototypeRuntimeValidationResult {
|
|
67
|
+
readonly prototypeId: string;
|
|
68
|
+
readonly materializedEntityCount: number;
|
|
69
|
+
readonly injectedComponentCount: number;
|
|
70
|
+
readonly descriptorComponentCount: number;
|
|
71
|
+
readonly runtimeSnapshotChecksum: string;
|
|
72
|
+
readonly checksum: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function cookDeterministicAssets(source: DeterministicAssetSource): DeterministicAssetCookResult;
|
|
75
|
+
export declare function stableAssetCookJson(result: DeterministicAssetCookResult): string;
|
|
76
|
+
export declare function materializePrototypeEntities(cooked: DeterministicAssetCookResult, prototypeId: string, count: number, options?: DeterministicPrototypeMaterializationOptions): readonly DeterministicPrototypeEntity[];
|
|
77
|
+
export declare function materializeDeterministicMapEntities(cooked: DeterministicAssetCookResult, mapId: string, options?: DeterministicPrototypeMaterializationOptions): DeterministicMapMaterializationResult;
|
|
78
|
+
export declare function validatePrototypeEntitiesAgainstFrameRuntime(cooked: DeterministicAssetCookResult, prototypeId: string, descriptor: DeterministicFrameRuntimeDescriptor, count?: number): DeterministicPrototypeRuntimeValidationResult;
|
|
79
|
+
export declare function assertAssetManifestMatches(manifest: DeterministicAssetCookManifest, expected: {
|
|
80
|
+
readonly engineVersion?: string;
|
|
81
|
+
readonly assetDbHash?: string;
|
|
82
|
+
}): void;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { canonicalStringify, cloneCanonical, defaultChecksum } from './canonical.js';
|
|
2
|
+
import { createDeterministicFrameRuntime } from './engine-runtime.js';
|
|
3
|
+
export function cookDeterministicAssets(source) {
|
|
4
|
+
validateAssetSource(source);
|
|
5
|
+
const engineVersion = source.engineVersion ?? 'rdm-assets@1';
|
|
6
|
+
const dependencies = [...(source.dependencies ?? [])]
|
|
7
|
+
.map((dependency) => cloneCanonical(dependency))
|
|
8
|
+
.sort(compareDependencies);
|
|
9
|
+
const schemaHash = defaultChecksum({
|
|
10
|
+
kind: 'deterministic-asset-schema',
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
requiredFields: ['id'],
|
|
13
|
+
optionalFields: ['type', 'references', 'prototype'],
|
|
14
|
+
numericPolicy: 'integer-only',
|
|
15
|
+
dependencyVersionPolicy: 'exact',
|
|
16
|
+
});
|
|
17
|
+
const dependencyHash = defaultChecksum(dependencies);
|
|
18
|
+
const assets = source.assets
|
|
19
|
+
.map((asset) => cloneCanonical(asset))
|
|
20
|
+
.sort((left, right) => compareStrings(left.id, right.id));
|
|
21
|
+
const cookedAssets = assets.map((asset) => {
|
|
22
|
+
const data = cloneCanonical(asset);
|
|
23
|
+
return {
|
|
24
|
+
id: asset.id,
|
|
25
|
+
type: asset.type ?? 'asset',
|
|
26
|
+
hash: defaultChecksum({ id: asset.id, data }),
|
|
27
|
+
data,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const lookup = Object.fromEntries(cookedAssets.map((asset, index) => [asset.id, index]));
|
|
31
|
+
const prototypeIds = cookedAssets
|
|
32
|
+
.filter((asset) => asset.data.prototype === true)
|
|
33
|
+
.map((asset) => asset.id)
|
|
34
|
+
.sort(compareStrings);
|
|
35
|
+
const withoutHash = {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
schemaHash,
|
|
38
|
+
engineVersion,
|
|
39
|
+
dependencyHash,
|
|
40
|
+
dependencies,
|
|
41
|
+
lookupHash: defaultChecksum(lookup),
|
|
42
|
+
prototypeIds,
|
|
43
|
+
assetCount: cookedAssets.length,
|
|
44
|
+
assets: cookedAssets.map((asset) => ({
|
|
45
|
+
id: asset.id,
|
|
46
|
+
type: asset.type,
|
|
47
|
+
hash: asset.hash,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
const manifest = {
|
|
51
|
+
...withoutHash,
|
|
52
|
+
assetDbHash: defaultChecksum({ engineVersion, schemaHash, dependencyHash, dependencies, cookedAssets }),
|
|
53
|
+
};
|
|
54
|
+
const bytes = canonicalStringify({
|
|
55
|
+
manifest,
|
|
56
|
+
assets: cookedAssets,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
manifest,
|
|
60
|
+
cookedAssets,
|
|
61
|
+
bytes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function stableAssetCookJson(result) {
|
|
65
|
+
return `${canonicalStringify(result)}\n`;
|
|
66
|
+
}
|
|
67
|
+
export function materializePrototypeEntities(cooked, prototypeId, count, options = {}) {
|
|
68
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
69
|
+
throw new Error('Prototype count must be a non-negative integer');
|
|
70
|
+
}
|
|
71
|
+
const startEntityId = options.startEntityId ?? 1;
|
|
72
|
+
if (!Number.isInteger(startEntityId) || startEntityId <= 0) {
|
|
73
|
+
throw new Error('Prototype start entity id must be a positive integer');
|
|
74
|
+
}
|
|
75
|
+
const prototype = cooked.cookedAssets.find((asset) => asset.id === prototypeId);
|
|
76
|
+
if (prototype === undefined) {
|
|
77
|
+
throw new Error(`Unknown prototype ${prototypeId}`);
|
|
78
|
+
}
|
|
79
|
+
if (!cooked.manifest.prototypeIds.includes(prototypeId)) {
|
|
80
|
+
throw new Error(`Asset ${prototypeId} is not a prototype`);
|
|
81
|
+
}
|
|
82
|
+
const components = resolvePrototypeComponents(cooked, prototypeId, new Set());
|
|
83
|
+
const overrides = options.componentOverrides === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: recordOrThrow(options.componentOverrides, `Prototype ${prototypeId} overrides`);
|
|
86
|
+
const resolvedComponents = mergeRecords(components, overrides);
|
|
87
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
88
|
+
entityId: startEntityId + index,
|
|
89
|
+
prototypeId,
|
|
90
|
+
assetDbHash: cooked.manifest.assetDbHash,
|
|
91
|
+
components: materializeDynamicCollections(resolvedComponents),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
export function materializeDeterministicMapEntities(cooked, mapId, options = {}) {
|
|
95
|
+
const mapAsset = cooked.cookedAssets.find((asset) => asset.id === mapId);
|
|
96
|
+
if (mapAsset === undefined) {
|
|
97
|
+
throw new Error(`Unknown map ${mapId}`);
|
|
98
|
+
}
|
|
99
|
+
if (mapAsset.type !== 'map') {
|
|
100
|
+
throw new Error(`Asset ${mapId} is not a map`);
|
|
101
|
+
}
|
|
102
|
+
const mapData = recordOrThrow(mapAsset.data, `Map ${mapId}`);
|
|
103
|
+
const spawns = mapData.spawns;
|
|
104
|
+
if (!Array.isArray(spawns)) {
|
|
105
|
+
throw new Error(`Map ${mapId} requires a spawns array`);
|
|
106
|
+
}
|
|
107
|
+
const materialized = [];
|
|
108
|
+
const prototypeIds = [];
|
|
109
|
+
const usedEntityIds = new Set();
|
|
110
|
+
let nextEntityId = options.startEntityId ?? 1;
|
|
111
|
+
for (const [index, spawn] of spawns.entries()) {
|
|
112
|
+
const spawnRecord = recordOrThrow(spawn, `Map ${mapId}.spawns[${index}]`);
|
|
113
|
+
const prototypeRef = spawnRecord.prototypeRef;
|
|
114
|
+
if (typeof prototypeRef !== 'string') {
|
|
115
|
+
throw new Error(`Map ${mapId}.spawns[${index}].prototypeRef must be a string`);
|
|
116
|
+
}
|
|
117
|
+
const count = optionalInteger(spawnRecord.count, `Map ${mapId}.spawns[${index}].count`) ?? 1;
|
|
118
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
119
|
+
throw new Error(`Map ${mapId}.spawns[${index}].count must be a non-negative integer`);
|
|
120
|
+
}
|
|
121
|
+
const startEntityId = optionalInteger(spawnRecord.startEntityId, `Map ${mapId}.spawns[${index}].startEntityId`) ?? nextEntityId;
|
|
122
|
+
if (!Number.isInteger(startEntityId) || startEntityId <= 0) {
|
|
123
|
+
throw new Error(`Map ${mapId}.spawns[${index}].startEntityId must be a positive integer`);
|
|
124
|
+
}
|
|
125
|
+
const entities = materializePrototypeEntities(cooked, prototypeRef, count, {
|
|
126
|
+
startEntityId,
|
|
127
|
+
componentOverrides: spawnRecord.overrides,
|
|
128
|
+
});
|
|
129
|
+
for (const entity of entities) {
|
|
130
|
+
if (usedEntityIds.has(entity.entityId)) {
|
|
131
|
+
throw new Error(`Map ${mapId}.spawns[${index}] duplicates entityId ${entity.entityId}`);
|
|
132
|
+
}
|
|
133
|
+
usedEntityIds.add(entity.entityId);
|
|
134
|
+
}
|
|
135
|
+
materialized.push(...entities);
|
|
136
|
+
prototypeIds.push(prototypeRef);
|
|
137
|
+
nextEntityId = Math.max(nextEntityId, startEntityId + count);
|
|
138
|
+
}
|
|
139
|
+
const withoutChecksum = {
|
|
140
|
+
mapId,
|
|
141
|
+
assetDbHash: cooked.manifest.assetDbHash,
|
|
142
|
+
mapBakeHash: defaultChecksum({ mapId, assetDbHash: cooked.manifest.assetDbHash, materialized }),
|
|
143
|
+
spawnCount: spawns.length,
|
|
144
|
+
materializedEntityCount: materialized.length,
|
|
145
|
+
prototypeIds: [...new Set(prototypeIds)].sort(compareStrings),
|
|
146
|
+
materializedEntities: materialized,
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
...withoutChecksum,
|
|
150
|
+
checksum: defaultChecksum(withoutChecksum),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function validatePrototypeEntitiesAgainstFrameRuntime(cooked, prototypeId, descriptor, count = 1) {
|
|
154
|
+
const runtime = createDeterministicFrameRuntime(descriptor, 0);
|
|
155
|
+
const entities = materializePrototypeEntities(cooked, prototypeId, count);
|
|
156
|
+
let injectedComponentCount = 0;
|
|
157
|
+
for (const materialized of entities) {
|
|
158
|
+
const runtimeEntity = runtime.createEntity();
|
|
159
|
+
const components = recordOrThrow(materialized.components, `Prototype ${prototypeId} components`);
|
|
160
|
+
for (const componentId of Object.keys(components).sort(compareStrings)) {
|
|
161
|
+
const data = recordOrThrow(components[componentId], `Prototype ${prototypeId}.${componentId}`);
|
|
162
|
+
runtime.setComponent(runtimeEntity, componentId, cloneCanonical(data));
|
|
163
|
+
injectedComponentCount += 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const snapshot = runtime.snapshot();
|
|
167
|
+
const result = {
|
|
168
|
+
prototypeId,
|
|
169
|
+
materializedEntityCount: entities.length,
|
|
170
|
+
injectedComponentCount,
|
|
171
|
+
descriptorComponentCount: descriptor.components.filter((component) => component.singleton !== true).length,
|
|
172
|
+
runtimeSnapshotChecksum: snapshot.checksum,
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
...result,
|
|
176
|
+
checksum: defaultChecksum(result),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
export function assertAssetManifestMatches(manifest, expected) {
|
|
180
|
+
if (expected.engineVersion !== undefined && manifest.engineVersion !== expected.engineVersion) {
|
|
181
|
+
throw new Error(`Stale deterministic asset engine: expected ${expected.engineVersion}, actual ${manifest.engineVersion}`);
|
|
182
|
+
}
|
|
183
|
+
if (expected.assetDbHash !== undefined && manifest.assetDbHash !== expected.assetDbHash) {
|
|
184
|
+
throw new Error(`Stale deterministic asset database hash: expected ${expected.assetDbHash}, actual ${manifest.assetDbHash}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function validateAssetSource(source) {
|
|
188
|
+
if (!Array.isArray(source.assets)) {
|
|
189
|
+
throw new Error('Deterministic asset source requires an assets array');
|
|
190
|
+
}
|
|
191
|
+
if (source.schemaVersion !== undefined && source.schemaVersion !== 1) {
|
|
192
|
+
throw new Error(`Unsupported deterministic asset schema ${source.schemaVersion}`);
|
|
193
|
+
}
|
|
194
|
+
validateDependencies(source.dependencies ?? []);
|
|
195
|
+
const ids = new Set();
|
|
196
|
+
for (const asset of source.assets) {
|
|
197
|
+
if (typeof asset.id !== 'string' || asset.id.length === 0) {
|
|
198
|
+
throw new Error('Every deterministic asset requires a non-empty string id');
|
|
199
|
+
}
|
|
200
|
+
if (ids.has(asset.id)) {
|
|
201
|
+
throw new Error(`Duplicate deterministic asset id ${asset.id}`);
|
|
202
|
+
}
|
|
203
|
+
ids.add(asset.id);
|
|
204
|
+
rejectUnquantizedNumbers(asset, asset.id);
|
|
205
|
+
}
|
|
206
|
+
for (const asset of source.assets) {
|
|
207
|
+
for (const reference of asset.references ?? []) {
|
|
208
|
+
if (!ids.has(reference)) {
|
|
209
|
+
throw new Error(`Unresolved deterministic asset reference ${asset.id} -> ${reference}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
validateImplicitAssetRefs(asset, ids, asset.id);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function validateImplicitAssetRefs(value, ids, path) {
|
|
216
|
+
if (Array.isArray(value)) {
|
|
217
|
+
value.forEach((entry, index) => validateImplicitAssetRefs(entry, ids, `${path}[${index}]`));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (value === null || typeof value !== 'object') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
for (const key of Object.keys(value).sort(compareStrings)) {
|
|
224
|
+
const entry = value[key];
|
|
225
|
+
const entryPath = `${path}.${key}`;
|
|
226
|
+
if (key.endsWith('Ref') && typeof entry === 'string' && !ids.has(entry)) {
|
|
227
|
+
throw new Error(`Unresolved deterministic asset ref ${entryPath} -> ${entry}`);
|
|
228
|
+
}
|
|
229
|
+
if (key.endsWith('Refs') && Array.isArray(entry)) {
|
|
230
|
+
for (const [index, ref] of entry.entries()) {
|
|
231
|
+
if (typeof ref === 'string' && !ids.has(ref)) {
|
|
232
|
+
throw new Error(`Unresolved deterministic asset ref ${entryPath}[${index}] -> ${ref}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
validateImplicitAssetRefs(entry, ids, entryPath);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function recordOrThrow(value, label) {
|
|
240
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
241
|
+
throw new Error(`${label} must be an object`);
|
|
242
|
+
}
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
function resolvePrototypeComponents(cooked, prototypeId, seen) {
|
|
246
|
+
if (seen.has(prototypeId)) {
|
|
247
|
+
throw new Error(`Prototype inheritance cycle at ${prototypeId}`);
|
|
248
|
+
}
|
|
249
|
+
seen.add(prototypeId);
|
|
250
|
+
const prototype = cooked.cookedAssets.find((asset) => asset.id === prototypeId);
|
|
251
|
+
if (prototype === undefined) {
|
|
252
|
+
throw new Error(`Unknown prototype ${prototypeId}`);
|
|
253
|
+
}
|
|
254
|
+
const data = recordOrThrow(prototype.data, `Prototype ${prototypeId}`);
|
|
255
|
+
const parentRef = data.extendsRef;
|
|
256
|
+
const parent = parentRef === undefined
|
|
257
|
+
? {}
|
|
258
|
+
: resolvePrototypeComponents(cooked, stringOrThrow(parentRef, `Prototype ${prototypeId}.extendsRef`), seen);
|
|
259
|
+
const own = data.components === undefined
|
|
260
|
+
? {}
|
|
261
|
+
: recordOrThrow(data.components, `Prototype ${prototypeId} components`);
|
|
262
|
+
return mergeRecords(parent, own);
|
|
263
|
+
}
|
|
264
|
+
function mergeRecords(base, override) {
|
|
265
|
+
const merged = {};
|
|
266
|
+
for (const key of [...new Set([...stableRecordKeys(base), ...stableRecordKeys(override)])].sort(compareStrings)) {
|
|
267
|
+
const baseValue = base[key];
|
|
268
|
+
const overrideValue = override[key];
|
|
269
|
+
if (overrideValue === undefined) {
|
|
270
|
+
merged[key] = cloneCanonical(baseValue);
|
|
271
|
+
}
|
|
272
|
+
else if (isPlainRecord(baseValue) && isPlainRecord(overrideValue) && !isCollectionDescriptor(overrideValue)) {
|
|
273
|
+
merged[key] = mergeRecords(baseValue, overrideValue);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
merged[key] = cloneCanonical(overrideValue);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return merged;
|
|
280
|
+
}
|
|
281
|
+
function materializeDynamicCollections(value) {
|
|
282
|
+
if (Array.isArray(value)) {
|
|
283
|
+
return value.map(materializeDynamicCollections);
|
|
284
|
+
}
|
|
285
|
+
if (!isPlainRecord(value)) {
|
|
286
|
+
return cloneCanonical(value);
|
|
287
|
+
}
|
|
288
|
+
if (isCollectionDescriptor(value)) {
|
|
289
|
+
const kind = stringOrThrow(value.collection, 'collection');
|
|
290
|
+
if (kind === 'list' || kind === 'buffer') {
|
|
291
|
+
const values = Array.isArray(value.values) ? value.values : [];
|
|
292
|
+
return values.map(materializeDynamicCollections);
|
|
293
|
+
}
|
|
294
|
+
if (kind === 'set') {
|
|
295
|
+
const values = Array.isArray(value.values) ? value.values : [];
|
|
296
|
+
return [...new Set(values.map((entry) => canonicalStringify(materializeDynamicCollections(entry))))]
|
|
297
|
+
.sort(compareStrings)
|
|
298
|
+
.map((entry) => JSON.parse(entry));
|
|
299
|
+
}
|
|
300
|
+
if (kind === 'map' || kind === 'dictionary') {
|
|
301
|
+
const entries = recordOrThrow(value.entries ?? {}, `${kind} entries`);
|
|
302
|
+
return Object.fromEntries(Object.keys(entries).sort(compareStrings).map((key) => [key, materializeDynamicCollections(entries[key])]));
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`Unsupported deterministic prototype collection ${kind}`);
|
|
305
|
+
}
|
|
306
|
+
return Object.fromEntries(Object.keys(value).sort(compareStrings).map((key) => [key, materializeDynamicCollections(value[key])]));
|
|
307
|
+
}
|
|
308
|
+
function isCollectionDescriptor(value) {
|
|
309
|
+
return typeof value.collection === 'string';
|
|
310
|
+
}
|
|
311
|
+
function isPlainRecord(value) {
|
|
312
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
313
|
+
}
|
|
314
|
+
function stringOrThrow(value, label) {
|
|
315
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
316
|
+
throw new Error(`${label} must be a non-empty string`);
|
|
317
|
+
}
|
|
318
|
+
return value;
|
|
319
|
+
}
|
|
320
|
+
function optionalInteger(value, label) {
|
|
321
|
+
if (value === undefined) {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
if (!Number.isInteger(value)) {
|
|
325
|
+
throw new Error(`${label} must be an integer`);
|
|
326
|
+
}
|
|
327
|
+
return value;
|
|
328
|
+
}
|
|
329
|
+
function stableRecordKeys(record) {
|
|
330
|
+
return Reflect.ownKeys(record)
|
|
331
|
+
.filter((key) => typeof key === 'string')
|
|
332
|
+
.sort(compareStrings);
|
|
333
|
+
}
|
|
334
|
+
function validateDependencies(dependencies) {
|
|
335
|
+
for (const dependency of dependencies) {
|
|
336
|
+
if (/[\^~*xX]/.test(dependency.version) || dependency.version === 'latest') {
|
|
337
|
+
throw new Error(`Nondeterministic dependency version ${dependency.name}@${dependency.version}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function compareDependencies(left, right) {
|
|
342
|
+
return compareStrings(left.name, right.name) || compareStrings(left.version, right.version);
|
|
343
|
+
}
|
|
344
|
+
function rejectUnquantizedNumbers(value, path) {
|
|
345
|
+
if (typeof value === 'number') {
|
|
346
|
+
if (!Number.isInteger(value)) {
|
|
347
|
+
throw new Error(`Unquantized float at ${path}`);
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (Array.isArray(value)) {
|
|
352
|
+
value.forEach((entry, index) => rejectUnquantizedNumbers(entry, `${path}[${index}]`));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (value !== null && typeof value === 'object') {
|
|
356
|
+
for (const key of Object.keys(value).sort(compareStrings)) {
|
|
357
|
+
const entry = value[key];
|
|
358
|
+
rejectUnquantizedNumbers(entry, `${path}.${key}`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function compareStrings(left, right) {
|
|
363
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
364
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { type ConfirmedInputFrame, type EncodedInput } from './input-authority.js';
|
|
2
|
+
import type { SyncplayMatchLog } from './match-log.js';
|
|
3
|
+
/**
|
|
4
|
+
* Transport-agnostic, game-agnostic deterministic authority room (§6 M2).
|
|
5
|
+
*
|
|
6
|
+
* This is the reusable core a host (mp-room-server's worker) wraps: it assigns slots, owns and
|
|
7
|
+
* distributes the RNG seed, orders/confirms inputs via {@link createInputAuthority} (running NO
|
|
8
|
+
* simulation), and broadcasts confirmed-input frames on a server tick. Every deterministic game
|
|
9
|
+
* uses this one component — there is no per-game server code. The host supplies only a transport
|
|
10
|
+
* (broadcast/sendTo) and drives the tick.
|
|
11
|
+
*
|
|
12
|
+
* It speaks the frozen `rdm-local-authority/v1` session protocol (`session-wire.ts`): players are
|
|
13
|
+
* greeted with `session-start`, send `input`, and receive `confirmed-input`.
|
|
14
|
+
*/
|
|
15
|
+
export interface AuthorityRoomTransport {
|
|
16
|
+
broadcast(msgType: string, payload: string): void;
|
|
17
|
+
sendTo(playerId: string, msgType: string, payload: string): void;
|
|
18
|
+
}
|
|
19
|
+
export interface DeterministicAuthorityRoomConfig {
|
|
20
|
+
readonly playerCount: number;
|
|
21
|
+
readonly seed: number;
|
|
22
|
+
readonly hardToleranceTicks: number;
|
|
23
|
+
readonly tickRateHz: number;
|
|
24
|
+
readonly redundancyWindowTicks: number;
|
|
25
|
+
/**
|
|
26
|
+
* The game's canonical "no-op" input, used when a slot's real input never arrives and there is
|
|
27
|
+
* no prior confirmed input to repeat. Game-defined (the authority never interprets it), so it
|
|
28
|
+
* MUST be supplied — the room is game-agnostic and cannot assume an input shape.
|
|
29
|
+
*/
|
|
30
|
+
readonly neutralInput: EncodedInput;
|
|
31
|
+
/** Reject inputs further ahead than `confirmedThrough + maxFutureTicks` (bounds buffering). Default 256. */
|
|
32
|
+
readonly maxFutureTicks?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Hold the authority clock until EVERY slot has joined (lobby-style
|
|
35
|
+
* synchronized start). Default false: the clock runs from the first join and
|
|
36
|
+
* later players catch up via input-history replay (drop-in). Games that want
|
|
37
|
+
* a hard "everyone starts together" opt in.
|
|
38
|
+
*/
|
|
39
|
+
readonly waitForFullRoom?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* How many confirmed frames to retain for late-join/rejoin catch-up.
|
|
42
|
+
* A joiner must replay the input history from tick 0 to reconstruct state
|
|
43
|
+
* deterministically (the authority never runs the sim, so history IS the
|
|
44
|
+
* state). Joins after tick 0 has been pruned are rejected with
|
|
45
|
+
* 'history-unavailable' — fail loud, never desync. Frames are tiny (encoded
|
|
46
|
+
* inputs), so the default retains a full hour at 60 Hz for a few MB.
|
|
47
|
+
* Default 216_000.
|
|
48
|
+
*/
|
|
49
|
+
readonly historyRetentionTicks?: number;
|
|
50
|
+
/**
|
|
51
|
+
* Desync telemetry (§3): invoked when two clients report different state checksums for the same
|
|
52
|
+
* tick. Detection ONLY — the match is never halted (checksum cheat-gating is deliberately avoided).
|
|
53
|
+
*/
|
|
54
|
+
readonly onDesync?: (info: AuthorityRoomDesyncInfo) => void;
|
|
55
|
+
/** msgType used for session-start / confirmed-input broadcasts (host wire envelope). Default 'rdm'. */
|
|
56
|
+
readonly sessionMsgType?: string;
|
|
57
|
+
/**
|
|
58
|
+
* How often (in authority ticks) to acquire a validated client-donated state
|
|
59
|
+
* snapshot (M5.5). Once one is stored, history below its tail is pruned and
|
|
60
|
+
* late joins are served snapshot + input tail regardless of match age.
|
|
61
|
+
* Default 3600 (1 min @ 60 Hz).
|
|
62
|
+
*/
|
|
63
|
+
readonly snapshotCadenceTicks?: number;
|
|
64
|
+
/** Abandon a donor transfer that has not completed after this many ticks. Default 600. */
|
|
65
|
+
readonly transferTimeoutTicks?: number;
|
|
66
|
+
/** Snapshot acquisition telemetry: stored snapshots and rejected/abandoned transfers. */
|
|
67
|
+
readonly onSnapshotEvent?: (event: AuthorityRoomSnapshotEvent) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Keep the confirmed-input history back to tick 0 even after a validated
|
|
70
|
+
* snapshot is stored (G3 — replay-verified matches). Normally the snapshot
|
|
71
|
+
* prunes everything below it; with full history the room can export a
|
|
72
|
+
* complete server-ordered match log (`matchLog()`) that an offline verifier
|
|
73
|
+
* re-simulates for score verification. Memory cost is bounded by
|
|
74
|
+
* `historyRetentionTicks` — a match longer than that loses tick 0 and stops
|
|
75
|
+
* being log-verifiable (matchLog() returns undefined; fail loud, never lie).
|
|
76
|
+
*/
|
|
77
|
+
readonly preserveFullHistory?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Catch-up telemetry (M8): fired whenever the room serves a player state
|
|
80
|
+
* reconstruction — late join, rejoin, reconnect refill, or post-restore
|
|
81
|
+
* resync — with which plan ran and how much tail was replayed.
|
|
82
|
+
*/
|
|
83
|
+
readonly onCatchUpEvent?: (event: AuthorityRoomCatchUpEvent) => void;
|
|
84
|
+
/**
|
|
85
|
+
* Rebuild a crashed room from its persisted record (M6.5): frontier, rejoin
|
|
86
|
+
* map, seated players, the input tail, and the latest validated snapshot.
|
|
87
|
+
* Construction is fail-loud on any inconsistency — a silently-empty restore
|
|
88
|
+
* would strand every surviving client. After building the room the host
|
|
89
|
+
* MUST call `resyncSeatedPlayers()` so surviving clients re-seat.
|
|
90
|
+
*/
|
|
91
|
+
readonly restoreFrom?: DeterministicAuthorityRoomRestoreState;
|
|
92
|
+
}
|
|
93
|
+
export interface DeterministicAuthorityRoomRestoreState {
|
|
94
|
+
readonly now: number;
|
|
95
|
+
readonly confirmedThrough: number;
|
|
96
|
+
readonly everSlots: Readonly<Record<string, number>>;
|
|
97
|
+
readonly seatedPlayers: readonly string[];
|
|
98
|
+
readonly tail: readonly ConfirmedInputFrame[];
|
|
99
|
+
readonly snapshotBytes?: string;
|
|
100
|
+
readonly schemaHash?: string;
|
|
101
|
+
}
|
|
102
|
+
export interface AuthorityRoomCatchUpEvent {
|
|
103
|
+
readonly playerId: string;
|
|
104
|
+
readonly kind: 'join' | 'rejoin' | 'reconnect' | 'resync';
|
|
105
|
+
readonly plan: 'snapshot' | 'history';
|
|
106
|
+
/** Confirmed frames sent for replay (frontier - tail start + 1). */
|
|
107
|
+
readonly tailFrames: number;
|
|
108
|
+
readonly snapshotTick?: number;
|
|
109
|
+
}
|
|
110
|
+
export interface AuthorityRoomSnapshotEvent {
|
|
111
|
+
readonly kind: 'stored' | 'rejected';
|
|
112
|
+
readonly donorId?: string;
|
|
113
|
+
readonly tick?: number;
|
|
114
|
+
readonly reason?: string;
|
|
115
|
+
}
|
|
116
|
+
export interface AuthorityRoomDesyncInfo {
|
|
117
|
+
readonly tick: number;
|
|
118
|
+
readonly slot: number;
|
|
119
|
+
readonly checksum: string;
|
|
120
|
+
readonly conflictingSlot: number;
|
|
121
|
+
readonly conflictingChecksum: string;
|
|
122
|
+
/**
|
|
123
|
+
* Every checksum reported for this tick so far (including the report that
|
|
124
|
+
* triggered detection), by slot. Lets the host pick the MINORITY client to
|
|
125
|
+
* resync instead of guessing between the two conflicting slots.
|
|
126
|
+
*/
|
|
127
|
+
readonly reportsAtTick: Readonly<Record<number, string>>;
|
|
128
|
+
}
|
|
129
|
+
/** Latest client-reported netcode telemetry, by slot (G8). */
|
|
130
|
+
export interface AuthorityRoomClientNetStats {
|
|
131
|
+
readonly tick: number;
|
|
132
|
+
readonly rttMs: number;
|
|
133
|
+
readonly rollbacks: number;
|
|
134
|
+
readonly maxRollbackDepth: number;
|
|
135
|
+
readonly timescalePermille: number;
|
|
136
|
+
readonly inputDelayTicks: number;
|
|
137
|
+
}
|
|
138
|
+
export interface AuthorityRoomNetcodeStats {
|
|
139
|
+
readonly confirmedThrough: number;
|
|
140
|
+
/** Ticks in which each slot's input had to be substituted (cumulative). */
|
|
141
|
+
readonly substitutedTicksBySlot: Readonly<Record<number, number>>;
|
|
142
|
+
readonly clientStats: Readonly<Record<number, AuthorityRoomClientNetStats>>;
|
|
143
|
+
}
|
|
144
|
+
export interface AuthorityRoomJoinResult {
|
|
145
|
+
readonly accepted: boolean;
|
|
146
|
+
readonly slot?: number;
|
|
147
|
+
readonly reason?: 'room-full' | 'already-joined' | 'history-unavailable';
|
|
148
|
+
/** True when this join re-attached a previously-seen playerId to its original slot. */
|
|
149
|
+
readonly rejoined?: boolean;
|
|
150
|
+
}
|
|
151
|
+
export interface AuthorityRoomSnapshot {
|
|
152
|
+
readonly confirmedThrough: number;
|
|
153
|
+
readonly now: number;
|
|
154
|
+
readonly slots: Readonly<Record<string, number>>;
|
|
155
|
+
/** Every playerId ever seated, by slot — the rejoin reclaim map. */
|
|
156
|
+
readonly everSlots: Readonly<Record<string, number>>;
|
|
157
|
+
/** Lowest confirmed tick still retained (-1 when nothing is confirmed). */
|
|
158
|
+
readonly earliestRetainedTick: number;
|
|
159
|
+
/**
|
|
160
|
+
* Metadata of the latest VALIDATED state snapshot. The raw bytes are
|
|
161
|
+
* deliberately excluded — hosts persist this record (mp-room-server hard-caps
|
|
162
|
+
* persisted state at 1 MB) — and exposed via `latestSnapshotBytes()` for
|
|
163
|
+
* out-of-band persistence (M6.5).
|
|
164
|
+
*/
|
|
165
|
+
readonly latestSnapshot?: AuthorityRoomStoredSnapshotMeta;
|
|
166
|
+
}
|
|
167
|
+
export interface AuthorityRoomStoredSnapshotMeta {
|
|
168
|
+
readonly tick: number;
|
|
169
|
+
readonly snapshotChecksum: string;
|
|
170
|
+
readonly descriptorHash: string;
|
|
171
|
+
readonly schemaHash?: string;
|
|
172
|
+
readonly byteLength: number;
|
|
173
|
+
}
|
|
174
|
+
export interface DeterministicAuthorityRoom {
|
|
175
|
+
readonly confirmedThrough: number;
|
|
176
|
+
readonly playerCount: number;
|
|
177
|
+
onPlayerJoin(playerId: string): AuthorityRoomJoinResult;
|
|
178
|
+
/** Seat a read-only spectator (G11): role 'spectator', no slot, full catch-up + live follow. */
|
|
179
|
+
onSpectatorJoin(playerId: string): boolean;
|
|
180
|
+
onPlayerMessage(playerId: string, envelope: string): void;
|
|
181
|
+
onPlayerLeave(playerId: string): void;
|
|
182
|
+
/**
|
|
183
|
+
* A player's transport re-attached inside the host's grace window (socket
|
|
184
|
+
* swap; no leave/join fired). Re-unicasts the confirmed-input history so any
|
|
185
|
+
* frames missed while offline are refilled — clients dedupe already-applied
|
|
186
|
+
* ticks, so this is idempotent.
|
|
187
|
+
*/
|
|
188
|
+
onPlayerReconnected(playerId: string): void;
|
|
189
|
+
/** Advance the authority by one server tick and broadcast newly-confirmed frames. */
|
|
190
|
+
onTick(): void;
|
|
191
|
+
snapshot(): AuthorityRoomSnapshot;
|
|
192
|
+
/** Raw bytes of the latest validated state snapshot (see AuthorityRoomSnapshot.latestSnapshot). */
|
|
193
|
+
latestSnapshotBytes(): string | undefined;
|
|
194
|
+
/** The retained confirmed-input tail (earliest retained .. frontier), for persistence (M6.5). */
|
|
195
|
+
confirmedTail(): readonly ConfirmedInputFrame[];
|
|
196
|
+
/** After a restore: re-greet every seated player (session-start + snapshot + tail). */
|
|
197
|
+
resyncSeatedPlayers(): void;
|
|
198
|
+
/**
|
|
199
|
+
* Force one seated player through the full reset + re-hydrate path
|
|
200
|
+
* (session-start re-greet + snapshot/history catch-up) — the live-desync
|
|
201
|
+
* response (G4): the minority client of a checksum conflict is re-seated from
|
|
202
|
+
* the validated snapshot exactly like a late joiner instead of drifting for
|
|
203
|
+
* the rest of the match. Returns false when the player is not seated or no
|
|
204
|
+
* catch-up plan exists (nothing was sent).
|
|
205
|
+
*/
|
|
206
|
+
resyncPlayer(playerId: string): boolean;
|
|
207
|
+
/** Cumulative substitution counters + latest per-client telemetry (G8). */
|
|
208
|
+
netcodeStats(): AuthorityRoomNetcodeStats;
|
|
209
|
+
/**
|
|
210
|
+
* The complete server-ordered match log (G3): every confirmed input frame
|
|
211
|
+
* from tick 0, plus the descriptor a verifier needs to re-simulate. Returns
|
|
212
|
+
* undefined when history no longer reaches tick 0 (snapshot-pruned without
|
|
213
|
+
* `preserveFullHistory`, or the match outlived `historyRetentionTicks`).
|
|
214
|
+
*/
|
|
215
|
+
matchLog(): SyncplayMatchLog | undefined;
|
|
216
|
+
}
|
|
217
|
+
export declare function createDeterministicAuthorityRoom(transport: AuthorityRoomTransport, config: DeterministicAuthorityRoomConfig): DeterministicAuthorityRoom;
|