@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,49 @@
|
|
|
1
|
+
import { type CanonicalValue } from './canonical.js';
|
|
2
|
+
import type { DeterministicGameDescriptor, ValidatedDeterministicGameDescriptor } from './schema.js';
|
|
3
|
+
import type { RecordedInputFrame } from './types.js';
|
|
4
|
+
export interface SchemaFrameDump {
|
|
5
|
+
readonly frame: number;
|
|
6
|
+
readonly artifactId: string;
|
|
7
|
+
readonly inputSchemaId: string;
|
|
8
|
+
readonly stateSchemaId: string;
|
|
9
|
+
readonly checksum: string;
|
|
10
|
+
readonly state: CanonicalValue;
|
|
11
|
+
readonly inputFrame?: RecordedInputFrame;
|
|
12
|
+
}
|
|
13
|
+
export interface DeterministicSchemaArtifacts {
|
|
14
|
+
readonly descriptor: ValidatedDeterministicGameDescriptor;
|
|
15
|
+
canonicalizeInput(input: unknown): CanonicalValue;
|
|
16
|
+
canonicalizeCommand(channelId: string, command: unknown): CanonicalValue;
|
|
17
|
+
/** Checksum projection: ONLY checksummed fields — feeds checksumState. */
|
|
18
|
+
canonicalizeState(state: unknown): CanonicalValue;
|
|
19
|
+
/** Every declared state field — the shape snapshots/restores must carry. */
|
|
20
|
+
canonicalizeFullState(state: unknown): CanonicalValue;
|
|
21
|
+
canonicalizeReplayMetadata(metadata: unknown): CanonicalValue;
|
|
22
|
+
canonicalizeAssetManifest(manifest: unknown): CanonicalValue;
|
|
23
|
+
serializeInput(input: unknown): Uint8Array;
|
|
24
|
+
deserializeInput(bytes: Uint8Array): CanonicalValue;
|
|
25
|
+
serializeCommand(channelId: string, command: unknown): Uint8Array;
|
|
26
|
+
deserializeCommand(channelId: string, bytes: Uint8Array): CanonicalValue;
|
|
27
|
+
serializeState(state: unknown): Uint8Array;
|
|
28
|
+
deserializeState(bytes: Uint8Array): CanonicalValue;
|
|
29
|
+
/**
|
|
30
|
+
* Snapshot serialization over the FULL declared state. Rollback/late-join
|
|
31
|
+
* restore replaces the live state with the deserialized snapshot — the
|
|
32
|
+
* checksum projection would silently wipe every checksummed:false field to
|
|
33
|
+
* undefined on restore (divergence the checksum can't see, because those
|
|
34
|
+
* fields aren't in it).
|
|
35
|
+
*/
|
|
36
|
+
serializeFullState(state: unknown): Uint8Array;
|
|
37
|
+
deserializeFullState(bytes: Uint8Array): CanonicalValue;
|
|
38
|
+
serializeReplayMetadata(metadata: unknown): Uint8Array;
|
|
39
|
+
deserializeReplayMetadata(bytes: Uint8Array): CanonicalValue;
|
|
40
|
+
serializeAssetManifest(manifest: unknown): Uint8Array;
|
|
41
|
+
deserializeAssetManifest(bytes: Uint8Array): CanonicalValue;
|
|
42
|
+
checksumState(state: unknown): string;
|
|
43
|
+
dumpFrame(config: {
|
|
44
|
+
readonly frame: number;
|
|
45
|
+
readonly state: unknown;
|
|
46
|
+
readonly inputFrame?: RecordedInputFrame;
|
|
47
|
+
}): SchemaFrameDump;
|
|
48
|
+
}
|
|
49
|
+
export declare function createSchemaArtifacts(descriptor: DeterministicGameDescriptor): DeterministicSchemaArtifacts;
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { canonicalize, cloneCanonical, decodeUtf8, defaultChecksum, encodeUtf8 } from './canonical.js';
|
|
2
|
+
import { SyncplayError } from './errors.js';
|
|
3
|
+
import { defineSyncplayGame } from './schema.js';
|
|
4
|
+
export function createSchemaArtifacts(descriptor) {
|
|
5
|
+
const validated = defineSyncplayGame(descriptor);
|
|
6
|
+
const canonicalizeInput = (input) => {
|
|
7
|
+
return canonicalizeDeclaredFields('input', input, validated.input.fields, validated.maxPlayers);
|
|
8
|
+
};
|
|
9
|
+
const canonicalizeState = (state) => {
|
|
10
|
+
return canonicalizeDeclaredFields('state', state, validated.state.fields, validated.maxPlayers, (field) => 'checksummed' in field && field.checksummed);
|
|
11
|
+
};
|
|
12
|
+
const canonicalizeFullState = (state) => {
|
|
13
|
+
return canonicalizeDeclaredFields('state', state, validated.state.fields, validated.maxPlayers);
|
|
14
|
+
};
|
|
15
|
+
const commandChannels = new Map((validated.commandChannels ?? []).map((channel) => [channel.id, channel]));
|
|
16
|
+
const canonicalizeCommand = (channelId, command) => {
|
|
17
|
+
const channel = commandChannels.get(channelId);
|
|
18
|
+
if (channel === undefined) {
|
|
19
|
+
throw new SyncplayError(`Command channel ${channelId} is not declared in schema`, {
|
|
20
|
+
code: 'schema-artifacts.command-channel',
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return canonicalizeDeclaredFields(`command.${channelId}`, command, channel.fields, validated.maxPlayers);
|
|
24
|
+
};
|
|
25
|
+
const canonicalizeReplayMetadata = (metadata) => {
|
|
26
|
+
return canonicalizeLabeledRecord('replayMetadata', metadata);
|
|
27
|
+
};
|
|
28
|
+
const canonicalizeAssetManifest = (manifest) => {
|
|
29
|
+
return canonicalizeLabeledRecord('assetManifest', manifest);
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
descriptor: validated,
|
|
33
|
+
canonicalizeInput,
|
|
34
|
+
canonicalizeCommand,
|
|
35
|
+
canonicalizeState,
|
|
36
|
+
canonicalizeFullState,
|
|
37
|
+
canonicalizeReplayMetadata,
|
|
38
|
+
canonicalizeAssetManifest,
|
|
39
|
+
serializeInput(input) {
|
|
40
|
+
return serializeCanonicalValue(canonicalizeInput(input));
|
|
41
|
+
},
|
|
42
|
+
deserializeInput(bytes) {
|
|
43
|
+
return canonicalizeInput(deserializeCanonicalValue('input', bytes));
|
|
44
|
+
},
|
|
45
|
+
serializeCommand(channelId, command) {
|
|
46
|
+
return serializeCanonicalValue(canonicalizeCommand(channelId, command));
|
|
47
|
+
},
|
|
48
|
+
deserializeCommand(channelId, bytes) {
|
|
49
|
+
return canonicalizeCommand(channelId, deserializeCanonicalValue(`command.${channelId}`, bytes));
|
|
50
|
+
},
|
|
51
|
+
serializeState(state) {
|
|
52
|
+
return serializeCanonicalValue(canonicalizeState(state));
|
|
53
|
+
},
|
|
54
|
+
deserializeState(bytes) {
|
|
55
|
+
return canonicalizeState(deserializeCanonicalValue('state', bytes));
|
|
56
|
+
},
|
|
57
|
+
serializeFullState(state) {
|
|
58
|
+
return serializeCanonicalValue(canonicalizeFullState(state));
|
|
59
|
+
},
|
|
60
|
+
deserializeFullState(bytes) {
|
|
61
|
+
return canonicalizeFullState(deserializeCanonicalValue('state', bytes));
|
|
62
|
+
},
|
|
63
|
+
serializeReplayMetadata(metadata) {
|
|
64
|
+
return serializeCanonicalValue(canonicalizeReplayMetadata(metadata));
|
|
65
|
+
},
|
|
66
|
+
deserializeReplayMetadata(bytes) {
|
|
67
|
+
return canonicalizeReplayMetadata(deserializeCanonicalValue('replayMetadata', bytes));
|
|
68
|
+
},
|
|
69
|
+
serializeAssetManifest(manifest) {
|
|
70
|
+
return serializeCanonicalValue(canonicalizeAssetManifest(manifest));
|
|
71
|
+
},
|
|
72
|
+
deserializeAssetManifest(bytes) {
|
|
73
|
+
return canonicalizeAssetManifest(deserializeCanonicalValue('assetManifest', bytes));
|
|
74
|
+
},
|
|
75
|
+
checksumState(state) {
|
|
76
|
+
return defaultChecksum(canonicalizeState(state));
|
|
77
|
+
},
|
|
78
|
+
dumpFrame(config) {
|
|
79
|
+
if (!Number.isInteger(config.frame) || config.frame < 0) {
|
|
80
|
+
throw new SyncplayError('Frame dump requires a non-negative integer frame', {
|
|
81
|
+
code: 'schema-artifacts.frame',
|
|
82
|
+
frame: config.frame,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const state = canonicalizeState(config.state);
|
|
86
|
+
const inputFrame = config.inputFrame === undefined
|
|
87
|
+
? undefined
|
|
88
|
+
: canonicalizeRecordedInputFrame(config.inputFrame, canonicalizeInput, validated.maxPlayers);
|
|
89
|
+
return {
|
|
90
|
+
frame: config.frame,
|
|
91
|
+
artifactId: validated.artifactId,
|
|
92
|
+
inputSchemaId: validated.input.id,
|
|
93
|
+
stateSchemaId: validated.state.id,
|
|
94
|
+
checksum: defaultChecksum(state),
|
|
95
|
+
state,
|
|
96
|
+
...(inputFrame === undefined ? {} : { inputFrame }),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const serializeCanonicalValue = (value) => encodeUtf8(JSON.stringify(value));
|
|
102
|
+
function deserializeCanonicalValue(label, bytes) {
|
|
103
|
+
try {
|
|
104
|
+
const source = decodeUtf8(bytes);
|
|
105
|
+
return JSON.parse(source);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
throw new SyncplayError(`${label} bytes are not valid schema JSON`, {
|
|
109
|
+
code: 'schema-artifacts.deserialize',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function canonicalizeDeclaredFields(label, value, fields, maxPlayers, includeField = () => true) {
|
|
114
|
+
const record = requireRecord(label, value);
|
|
115
|
+
const output = {};
|
|
116
|
+
const declaredFields = new Map(fields.map((field) => [field.name, field]));
|
|
117
|
+
for (const field of fields) {
|
|
118
|
+
if (!Object.prototype.hasOwnProperty.call(record, field.name)) {
|
|
119
|
+
if ('defaultValue' in field) {
|
|
120
|
+
if (includeField(field)) {
|
|
121
|
+
output[field.name] = canonicalize(validateFieldValue(`${label}.${field.name}`, field, field.defaultValue, maxPlayers));
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (includeField(field)) {
|
|
126
|
+
throw new SyncplayError(`${label}.${field.name} is missing`, {
|
|
127
|
+
code: 'schema-artifacts.missing-field',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const canonicalValue = canonicalize(validateFieldValue(`${label}.${field.name}`, field, record[field.name], maxPlayers));
|
|
133
|
+
if (includeField(field)) {
|
|
134
|
+
output[field.name] = canonicalValue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const key of Object.keys(record).sort(compareStrings)) {
|
|
138
|
+
if (!declaredFields.has(key)) {
|
|
139
|
+
throw new SyncplayError(`${label}.${key} is not declared in schema`, {
|
|
140
|
+
code: 'schema-artifacts.undeclared-field',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return output;
|
|
145
|
+
}
|
|
146
|
+
function compareStrings(left, right) {
|
|
147
|
+
if (left < right) {
|
|
148
|
+
return -1;
|
|
149
|
+
}
|
|
150
|
+
/* istanbul ignore else -- record-key sorts in this module cannot compare duplicate keys. */
|
|
151
|
+
if (left > right) {
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
/* istanbul ignore next -- record-key sorts in this module cannot compare duplicate keys. */
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
function canonicalizeLabeledRecord(label, value) {
|
|
158
|
+
return canonicalize(requireRecord(label, value));
|
|
159
|
+
}
|
|
160
|
+
function validateFieldValue(label, field, value, maxPlayers) {
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
if (field.collectionOrdering === undefined) {
|
|
163
|
+
throw new SyncplayError(`${label} is an array but does not declare collectionOrdering`, {
|
|
164
|
+
code: 'schema-artifacts.collection-ordering',
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
const items = value.map((item, index) => validateScalarValue(`${label}[${index}]`, field, item, maxPlayers));
|
|
168
|
+
// ENFORCE the declared ordering, don't just validate the enum: two peers
|
|
169
|
+
// holding the same logical collection in different array order must
|
|
170
|
+
// canonicalize identically, or the engine reports a desync that isn't one.
|
|
171
|
+
// 'indexed' means array order IS the meaning — preserved as-is.
|
|
172
|
+
if (field.collectionOrdering === 'sorted-key') {
|
|
173
|
+
return [...items].sort((left, right) => compareCanonicalScalars(label, left, right));
|
|
174
|
+
}
|
|
175
|
+
return items;
|
|
176
|
+
}
|
|
177
|
+
return validateScalarValue(label, field, value, maxPlayers);
|
|
178
|
+
}
|
|
179
|
+
function compareCanonicalScalars(label, left, right) {
|
|
180
|
+
if (typeof left === 'number' && typeof right === 'number') {
|
|
181
|
+
return left - right;
|
|
182
|
+
}
|
|
183
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
184
|
+
return compareStrings(left, right);
|
|
185
|
+
}
|
|
186
|
+
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
|
187
|
+
return Number(left) - Number(right);
|
|
188
|
+
}
|
|
189
|
+
throw new SyncplayError(`${label} sorted-key collection requires uniformly comparable scalar items`, {
|
|
190
|
+
code: 'schema-artifacts.collection-ordering',
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function validateScalarValue(label, field, value, maxPlayers) {
|
|
194
|
+
switch (field.kind) {
|
|
195
|
+
case 'boolean':
|
|
196
|
+
case 'button':
|
|
197
|
+
if (typeof value !== 'boolean') {
|
|
198
|
+
throwTypeError(label, 'boolean');
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
case 'string':
|
|
202
|
+
case 'asset-ref':
|
|
203
|
+
if (typeof value !== 'string') {
|
|
204
|
+
throwTypeError(label, 'string');
|
|
205
|
+
}
|
|
206
|
+
return value;
|
|
207
|
+
case 'integer':
|
|
208
|
+
case 'fixed-q16':
|
|
209
|
+
case 'bitfield':
|
|
210
|
+
case 'player-slot':
|
|
211
|
+
if (typeof value !== 'number' || !Number.isInteger(value)) {
|
|
212
|
+
throwTypeError(label, 'integer');
|
|
213
|
+
}
|
|
214
|
+
assertRange(label, field, value);
|
|
215
|
+
if (field.kind === 'player-slot' && (value < 0 || value >= maxPlayers)) {
|
|
216
|
+
throw new SyncplayError(`${label} must be a player slot between 0 and ${maxPlayers - 1}`, {
|
|
217
|
+
code: 'schema-artifacts.player-slot',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return value;
|
|
221
|
+
/* istanbul ignore next -- defineSyncplayGame rejects unsupported field kinds before artifact creation. */
|
|
222
|
+
default:
|
|
223
|
+
throw new SyncplayError(`${label} uses unsupported field kind`, {
|
|
224
|
+
code: 'schema-artifacts.unsupported-kind',
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function canonicalizeRecordedInputFrame(inputFrame, canonicalizeInput, maxPlayers) {
|
|
229
|
+
const record = requireRecord('inputFrame', inputFrame);
|
|
230
|
+
const allowedKeys = new Set(['frame', 'inputs', 'inputFlags']);
|
|
231
|
+
for (const key of Object.keys(record).sort(compareStrings)) {
|
|
232
|
+
if (!allowedKeys.has(key)) {
|
|
233
|
+
throw new SyncplayError(`inputFrame.${key} is not declared`, {
|
|
234
|
+
code: 'schema-artifacts.input-frame',
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const frame = requireOwnProperty(record, 'inputFrame', 'frame');
|
|
239
|
+
if (typeof frame !== 'number' || !Number.isInteger(frame) || frame < 0) {
|
|
240
|
+
throw new SyncplayError('inputFrame.frame must be a non-negative integer', {
|
|
241
|
+
code: 'schema-artifacts.input-frame',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
const inputs = requireOwnProperty(record, 'inputFrame', 'inputs');
|
|
245
|
+
if (!Array.isArray(inputs)) {
|
|
246
|
+
throw new SyncplayError('inputFrame.inputs must be an array', {
|
|
247
|
+
code: 'schema-artifacts.input-frame',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
if (inputs.length > maxPlayers) {
|
|
251
|
+
throw new SyncplayError(`inputFrame.inputs cannot exceed maxPlayers ${maxPlayers}`, {
|
|
252
|
+
code: 'schema-artifacts.input-frame',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const output = {
|
|
256
|
+
frame,
|
|
257
|
+
inputs: inputs.map((input) => canonicalizeInput(input)),
|
|
258
|
+
};
|
|
259
|
+
const hasInputFlags = Object.prototype.hasOwnProperty.call(record, 'inputFlags');
|
|
260
|
+
if (!hasInputFlags && record.inputFlags !== undefined) {
|
|
261
|
+
throw new SyncplayError('inputFrame.inputFlags must be an own property when present', {
|
|
262
|
+
code: 'schema-artifacts.input-frame',
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
if (!hasInputFlags) {
|
|
266
|
+
return cloneCanonical(output);
|
|
267
|
+
}
|
|
268
|
+
const inputFlags = record.inputFlags;
|
|
269
|
+
if (!Array.isArray(inputFlags)) {
|
|
270
|
+
throw new SyncplayError('inputFrame.inputFlags must be an array when present', {
|
|
271
|
+
code: 'schema-artifacts.input-frame',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return cloneCanonical({
|
|
275
|
+
...output,
|
|
276
|
+
inputFlags: canonicalizeRecordedInputFlags(inputFlags, inputs.length, maxPlayers),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function canonicalizeRecordedInputFlags(inputFlags, expectedLength, maxPlayers) {
|
|
280
|
+
if (inputFlags.length !== expectedLength) {
|
|
281
|
+
throw new SyncplayError(`inputFrame.inputFlags length ${inputFlags.length} must match inputs length ${expectedLength}`, {
|
|
282
|
+
code: 'schema-artifacts.input-frame',
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return inputFlags.map((flag, index) => {
|
|
286
|
+
const record = requireRecord(`inputFrame.inputFlags[${index}]`, flag);
|
|
287
|
+
const allowedKeys = new Set(['slot', 'playerPresent', 'repeatedPrevious', 'replacedByServer', 'commandInput']);
|
|
288
|
+
for (const key of Object.keys(record).sort(compareStrings)) {
|
|
289
|
+
if (!allowedKeys.has(key)) {
|
|
290
|
+
throw new SyncplayError(`inputFrame.inputFlags[${index}].${key} is not declared`, {
|
|
291
|
+
code: 'schema-artifacts.input-frame',
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const slot = requireOwnProperty(record, `inputFrame.inputFlags[${index}]`, 'slot');
|
|
296
|
+
if (typeof slot !== 'number' || !Number.isInteger(slot) || slot < 0 || slot >= maxPlayers) {
|
|
297
|
+
throw new SyncplayError(`inputFrame.inputFlags[${index}].slot must be a valid player slot`, {
|
|
298
|
+
code: 'schema-artifacts.input-frame',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (slot !== index) {
|
|
302
|
+
throw new SyncplayError(`inputFrame.inputFlags[${index}].slot must match its index`, {
|
|
303
|
+
code: 'schema-artifacts.input-frame',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
slot,
|
|
308
|
+
playerPresent: requireBooleanFlag(record, index, 'playerPresent'),
|
|
309
|
+
repeatedPrevious: requireBooleanFlag(record, index, 'repeatedPrevious'),
|
|
310
|
+
replacedByServer: requireBooleanFlag(record, index, 'replacedByServer'),
|
|
311
|
+
commandInput: requireBooleanFlag(record, index, 'commandInput'),
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
function requireBooleanFlag(record, index, key) {
|
|
316
|
+
const value = requireOwnProperty(record, `inputFrame.inputFlags[${index}]`, key);
|
|
317
|
+
if (typeof value !== 'boolean') {
|
|
318
|
+
throw new SyncplayError(`inputFrame.inputFlags[${index}].${key} must be boolean`, {
|
|
319
|
+
code: 'schema-artifacts.input-frame',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return value;
|
|
323
|
+
}
|
|
324
|
+
function requireOwnProperty(record, label, key) {
|
|
325
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
326
|
+
throw new SyncplayError(`${label}.${key} must be an own property`, {
|
|
327
|
+
code: 'schema-artifacts.input-frame',
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return record[key];
|
|
331
|
+
}
|
|
332
|
+
function assertRange(label, field, value) {
|
|
333
|
+
if (field.min !== undefined && value < field.min) {
|
|
334
|
+
throw new SyncplayError(`${label} is below minimum ${field.min}`, {
|
|
335
|
+
code: 'schema-artifacts.range',
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (field.max !== undefined && value > field.max) {
|
|
339
|
+
throw new SyncplayError(`${label} is above maximum ${field.max}`, {
|
|
340
|
+
code: 'schema-artifacts.range',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
function requireRecord(label, value) {
|
|
345
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
346
|
+
throw new SyncplayError(`${label} must be an object`, {
|
|
347
|
+
code: 'schema-artifacts.record',
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return value;
|
|
351
|
+
}
|
|
352
|
+
function throwTypeError(label, expected) {
|
|
353
|
+
throw new SyncplayError(`${label} must be ${expected}`, {
|
|
354
|
+
code: 'schema-artifacts.type',
|
|
355
|
+
});
|
|
356
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DeterministicGameDescriptor } from './schema.js';
|
|
2
|
+
import { type DeterministicDslSchema } from './schema-dsl-binary.js';
|
|
3
|
+
export interface SchemaTypesOptions {
|
|
4
|
+
readonly rootName?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function generateSchemaTypes(descriptor: DeterministicGameDescriptor, options?: SchemaTypesOptions): string;
|
|
7
|
+
export interface SchemaBinarySourceOptions {
|
|
8
|
+
readonly rootName?: string;
|
|
9
|
+
readonly importFrom?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SchemaBinaryProof {
|
|
12
|
+
readonly result: 'pass';
|
|
13
|
+
readonly schemaHash: string;
|
|
14
|
+
readonly declarationCount: number;
|
|
15
|
+
readonly fieldCount: number;
|
|
16
|
+
readonly encodedBytes: number;
|
|
17
|
+
readonly roundTripChecksum: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function createDeterministicDescriptorBinarySchema(descriptor: DeterministicGameDescriptor, options?: SchemaBinarySourceOptions): DeterministicDslSchema;
|
|
20
|
+
export declare function generateSchemaBinarySource(descriptor: DeterministicGameDescriptor, options?: SchemaBinarySourceOptions): string;
|
|
21
|
+
export declare function proveSchemaBinaryRoundTrip(descriptor: DeterministicGameDescriptor, options?: SchemaBinarySourceOptions): SchemaBinaryProof;
|