@series-inc/rundot-game-sdk 5.23.0-beta.5 → 5.23.0-beta.6

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.
Files changed (37) hide show
  1. package/dist/{SyncplayRoomTransport-DXNJxFKZ.d.ts → SyncplayRoomTransport-ByIm59Ie.d.ts} +157 -2
  2. package/dist/{chunk-T6Q5GL7B.js → chunk-G7TYQYHC.js} +437 -10
  3. package/dist/chunk-G7TYQYHC.js.map +1 -0
  4. package/dist/{chunk-WVJVQAW7.js → chunk-MCVCZ7W3.js} +3 -3
  5. package/dist/{chunk-WVJVQAW7.js.map → chunk-MCVCZ7W3.js.map} +1 -1
  6. package/dist/chunk-VZLX7S73.js +131 -0
  7. package/dist/chunk-VZLX7S73.js.map +1 -0
  8. package/dist/{index-DB99BKTt.d.ts → index-UMslJhz5.d.ts} +2 -2
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +1 -1
  11. package/dist/playground/index.js +26 -1
  12. package/dist/playground/index.js.map +1 -1
  13. package/dist/rundot-game-api/index.js +1 -1
  14. package/dist/session-promotion-DgrVQE2y.d.ts +136 -0
  15. package/dist/syncplay/browser.d.ts +103 -81
  16. package/dist/syncplay/browser.js +560 -388
  17. package/dist/syncplay/browser.js.map +1 -1
  18. package/dist/syncplay/creator.d.ts +36 -2
  19. package/dist/syncplay/creator.js +2 -2
  20. package/dist/syncplay/index.d.ts +2 -2
  21. package/dist/syncplay/index.js +2 -2
  22. package/dist/syncplay/node.js +4 -2
  23. package/dist/syncplay/node.js.map +1 -1
  24. package/dist/{types-BLIISDww.d.ts → types-DbhOvOdx.d.ts} +1 -1
  25. package/dist/vite/{dev-server-5OIDIMOM.js → dev-server-V3IZP5XX.js} +144 -28
  26. package/dist/vite/dev-server-V3IZP5XX.js.map +1 -0
  27. package/dist/vite/{esbuild-validate-IXG2OBFH.js → esbuild-validate-3ISKFZOW.js} +5 -2
  28. package/dist/vite/esbuild-validate-3ISKFZOW.js.map +1 -0
  29. package/dist/vite/index.js +30 -32
  30. package/dist/vite/index.js.map +1 -1
  31. package/docs/rundot-developer-platform/api/SYNCPLAY.md +47 -0
  32. package/package.json +2 -1
  33. package/dist/chunk-QLHQTYP6.js +0 -3
  34. package/dist/chunk-QLHQTYP6.js.map +0 -1
  35. package/dist/chunk-T6Q5GL7B.js.map +0 -1
  36. package/dist/vite/dev-server-5OIDIMOM.js.map +0 -1
  37. package/dist/vite/esbuild-validate-IXG2OBFH.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { C as CanonicalValue, R as ReplayFile, O as OfflineSession } from './types-BLIISDww.js';
1
+ import { C as CanonicalValue, h as ReplayFile, O as OfflineSession } from './types-DbhOvOdx.js';
2
2
  import { M as MultiplayerApi } from './MultiplayerApi-nnrGTTcO.js';
3
3
 
4
4
  /**
@@ -83,6 +83,95 @@ interface InputAuthority {
83
83
  }
84
84
  declare function createInputAuthority(config: InputAuthorityConfig): InputAuthority;
85
85
 
86
+ /**
87
+ * Frozen v1 session-control wire protocol for game-agnostic input-authority rooms (§8, M1).
88
+ *
89
+ * Covers the server/client roles for a live match: session-start (slot assignment + the
90
+ * server-owned RNG seed + tick params), clock/time-sync, input, confirmed-input (the verified
91
+ * tick), reconnect, spectator, and close/error. Snapshot request/offer/chunk/complete for
92
+ * late-join reuse the existing envelopes in `local-authority-wire.ts` (wired at M5).
93
+ *
94
+ * Both families share the `rdm-local-authority/v1` version namespace. Decoding is FAIL-CLOSED:
95
+ * unknown protocol, unknown version, unknown kind, malformed payload, or checksum mismatch are
96
+ * rejected with a reason — never silently coerced. This is a public forward-compat boundary; the
97
+ * discriminants below are frozen and must only be extended, never repurposed.
98
+ */
99
+ declare const deterministicSessionWireVersion = 1;
100
+ type DeterministicSessionRole = 'player' | 'spectator';
101
+ type DeterministicSessionMessage = {
102
+ readonly kind: 'session-start';
103
+ readonly slot: number;
104
+ readonly playerCount: number;
105
+ readonly seed: number;
106
+ readonly tickRateHz: number;
107
+ readonly hardToleranceTicks: number;
108
+ readonly role: DeterministicSessionRole;
109
+ } | {
110
+ readonly kind: 'time-sync-ping';
111
+ readonly clientSendTick: number;
112
+ readonly nonce: number;
113
+ } | {
114
+ readonly kind: 'time-sync-pong';
115
+ readonly clientSendTick: number;
116
+ readonly nonce: number;
117
+ readonly authorityTick: number;
118
+ } | {
119
+ readonly kind: 'input';
120
+ readonly slot: number;
121
+ readonly tick: number;
122
+ readonly input: EncodedInput;
123
+ readonly attempt: number;
124
+ } | {
125
+ readonly kind: 'confirmed-input';
126
+ readonly authorityTick: number;
127
+ readonly frames: readonly ConfirmedInputFrame[];
128
+ } | {
129
+ readonly kind: 'reconnect';
130
+ readonly slot: number;
131
+ readonly resumeFromTick: number;
132
+ } | {
133
+ readonly kind: 'spectator-join';
134
+ } | {
135
+ readonly kind: 'checksum-report';
136
+ readonly slot: number;
137
+ readonly tick: number;
138
+ readonly checksum: string;
139
+ } | {
140
+ /**
141
+ * Client → authority netcode telemetry (G8). Additive v1 extension: unknown
142
+ * kinds are dropped fail-closed by older decoders, so this never breaks an
143
+ * old authority. All fields are integers (rttMs rounded, timescale in
144
+ * permille) so the payload stays canonical-checksum friendly.
145
+ */
146
+ readonly kind: 'net-stats';
147
+ readonly slot: number;
148
+ readonly tick: number;
149
+ readonly rttMs: number;
150
+ readonly rollbacks: number;
151
+ readonly maxRollbackDepth: number;
152
+ readonly timescalePermille: number;
153
+ readonly inputDelayTicks: number;
154
+ } | {
155
+ readonly kind: 'close';
156
+ readonly code: string;
157
+ readonly reason: string;
158
+ } | {
159
+ readonly kind: 'error';
160
+ readonly code: string;
161
+ readonly message: string;
162
+ };
163
+ type DeterministicSessionMessageKind = DeterministicSessionMessage['kind'];
164
+ type DeterministicSessionDecodeRejection = 'invalid-json' | 'not-an-envelope' | 'unknown-protocol' | 'unknown-version' | 'unknown-kind' | 'checksum-mismatch' | 'malformed-payload';
165
+ type DeterministicSessionDecodeResult = {
166
+ readonly ok: true;
167
+ readonly message: DeterministicSessionMessage;
168
+ } | {
169
+ readonly ok: false;
170
+ readonly reason: DeterministicSessionDecodeRejection;
171
+ };
172
+ declare function encodeDeterministicSessionMessage(message: DeterministicSessionMessage): string;
173
+ declare function decodeDeterministicSessionMessage(text: string): DeterministicSessionDecodeResult;
174
+
86
175
  /**
87
176
  * Reusable client transport for game-agnostic deterministic multiplayer (§6 M4).
88
177
  *
@@ -120,17 +209,83 @@ interface NetworkedSyncplayClientOptions<State, Input> {
120
209
  * `restoreToFrame`, which would rewind the live session. Default 120.
121
210
  */
122
211
  readonly checksumCadenceTicks?: number;
212
+ /**
213
+ * Wall-clock pacing + clock-drift correction (G1), consumed by
214
+ * {@link NetworkedSyncplayClient.pumpPaced}. The step-counted `pump()` path
215
+ * ignores all of this, so step-driven tests and existing callers are
216
+ * unaffected. Rates are per-tick error corrections in the Quantum
217
+ * TimeCorrectionRate / GGPO frames-behind family: the client measures RTT via
218
+ * `time-sync-ping/pong`, estimates the authority frontier, and nudges its
219
+ * local timescale inside [minTimescale, maxTimescale] so it neither stalls at
220
+ * the prediction cap (fast clock / short path) nor starves into permanent
221
+ * substitution (slow clock / long path).
222
+ */
223
+ readonly pacing?: NetworkedSyncplayClientPacingOptions;
224
+ /**
225
+ * Own-input scheduling delay in ticks (G2): input sampled while predicting
226
+ * tick T is bound and sent for tick T+delay, trading a fixed sliver of local
227
+ * latency for inputs that arrive before the authority's deadline (fewer
228
+ * substitutions of THIS client's input, shallower rollbacks for peers).
229
+ * With `maxTicks > minTicks` the delay auto-tunes from measured RTT
230
+ * (ping-adaptive, Quantum InputDelayMin/Max analog) — RTT measurement
231
+ * requires driving `pumpPaced`. Defaults to a static 0 (today's behavior).
232
+ */
233
+ readonly inputDelay?: NetworkedSyncplayClientInputDelayOptions;
234
+ /** Monotonic-enough clock for RTT measurement. Default Date.now. NEVER used by the sim. */
235
+ readonly now?: () => number;
236
+ }
237
+ interface NetworkedSyncplayClientPacingOptions {
238
+ /** Interval between time-sync pings while pumpPaced is driven. Default 1000. */
239
+ readonly pingIntervalMs?: number;
240
+ /** Safety lead (ticks) kept over the input-arrival deadline. Default 2. */
241
+ readonly targetLeadTicks?: number;
242
+ /** Timescale clamp. Defaults 0.9 / 1.1 (±10%, imperceptible per Quantum/GGPO practice). */
243
+ readonly minTimescale?: number;
244
+ readonly maxTimescale?: number;
245
+ /** Timescale correction gain per tick of frame-advantage error. Default 0.02. */
246
+ readonly correctionPerTick?: number;
247
+ /** Catch-up cap: max frames stepped per pumpPaced call. Default 4. */
248
+ readonly maxStepsPerPump?: number;
249
+ }
250
+ interface NetworkedSyncplayClientInputDelayOptions {
251
+ /** Minimum (and initial) own-input delay in ticks. Default 0. */
252
+ readonly minTicks?: number;
253
+ /** Maximum delay for ping-adaptive tuning. Default = minTicks (static). */
254
+ readonly maxTicks?: number;
255
+ }
256
+ /** Live netcode telemetry for the local client (G8). */
257
+ interface NetworkedSyncplayClientNetStats {
258
+ /** Smoothed RTT in ms; -1 until the first time-sync pong arrives. */
259
+ readonly rttMs: number;
260
+ /** Current pacing timescale in permille (1000 = realtime). */
261
+ readonly timescalePermille: number;
262
+ readonly inputDelayTicks: number;
263
+ readonly rollbacks: number;
264
+ readonly maxRollbackDepth: number;
123
265
  }
124
266
  interface NetworkedSyncplayClient {
125
267
  readonly slot: number;
268
+ /** 'player' (holds a slot) or 'spectator' (read-only follower, slot -1). From session-start. */
269
+ readonly role: DeterministicSessionRole;
126
270
  readonly ready: boolean;
127
271
  readonly predictedThrough: number;
128
272
  readonly appliedThrough: number;
129
273
  readonly rollbacks: number;
274
+ /** Live local netcode telemetry (RTT, timescale, delay, rollback depth). */
275
+ readonly netStats: NetworkedSyncplayClientNetStats;
130
276
  /** Resolves once `session-start` has been received. */
131
277
  whenReady(): Promise<void>;
132
278
  /** Predict and submit local input up to the prediction budget. */
133
279
  pump(): void;
280
+ /**
281
+ * Wall-clock-paced pump (G1): steps as many fixed frames as elapsed time ×
282
+ * the drift-corrected timescale allows (bounded by the prediction budget and
283
+ * the catch-up cap), and drives RTT pings. Call once per host frame (rAF /
284
+ * host tick) with a monotonic-enough `nowMs`. Prefer this over raw `pump()`
285
+ * on real networks — raw pump() slams into the prediction cap and
286
+ * rubber-bands under asymmetric latency.
287
+ */
288
+ pumpPaced(nowMs: number): void;
134
289
  /** Report this client's confirmed-state checksum for desync telemetry. */
135
290
  reportChecksum(): void;
136
291
  /** Confirmed-state checksum at `frame` (must be <= appliedThrough + 1). */
@@ -193,4 +348,4 @@ declare function joinSyncplayRoomByCode(api: MultiplayerApi, code: string): Prom
193
348
  */
194
349
  declare function createSyncplayRoom(api: MultiplayerApi): Promise<SyncplayRoomTransport>;
195
350
 
196
- export { type ConfirmedInputFrame as C, type EncodedInput as E, type InputAuthority as I, type NetworkedClientTransport as N, type SyncplayRoomTransport as S, type InputAuthorityConfig as a, type InputAuthorityRestoreState as b, type InputAuthorityStats as c, type InputRejectionReason as d, type InputSubmissionResult as e, type NetworkedSyncplayClient as f, type NetworkedSyncplayClientOptions as g, createInputAuthority as h, createNetworkedSyncplayClient as i, createSyncplayRoom as j, joinSyncplayRoomByCode as k };
351
+ export { type ConfirmedInputFrame as C, type DeterministicSessionDecodeRejection as D, type EncodedInput as E, type InputAuthority as I, type NetworkedClientTransport as N, type SyncplayRoomTransport as S, type DeterministicSessionDecodeResult as a, type DeterministicSessionMessage as b, type DeterministicSessionMessageKind as c, type DeterministicSessionRole as d, type InputAuthorityConfig as e, type InputAuthorityRestoreState as f, type InputAuthorityStats as g, type InputRejectionReason as h, type InputSubmissionResult as i, type NetworkedSyncplayClient as j, type NetworkedSyncplayClientInputDelayOptions as k, type NetworkedSyncplayClientNetStats as l, type NetworkedSyncplayClientOptions as m, type NetworkedSyncplayClientPacingOptions as n, createInputAuthority as o, createNetworkedSyncplayClient as p, createSyncplayRoom as q, decodeDeterministicSessionMessage as r, deterministicSessionWireVersion as s, encodeDeterministicSessionMessage as t, joinSyncplayRoomByCode as u };
@@ -1714,8 +1714,12 @@ function verifyReplayWithDescriptor(config) {
1714
1714
  replay: config.replay,
1715
1715
  step: config.step,
1716
1716
  defaultInput: config.defaultInput,
1717
- inputSchemaId: artifacts.descriptor.input.id,
1718
- stateSchemaId: artifacts.descriptor.state.id,
1717
+ // The CONTENT-HASHED identity, not the author label: runtimes stamp
1718
+ // replayIdentity.* into exported replays (a field edit without a label
1719
+ // bump must change the identity), so the descriptor-derived verifier must
1720
+ // compare the same form or every defineSyncplayGame replay fails identity.
1721
+ inputSchemaId: artifacts.descriptor.replayIdentity.inputSchemaId,
1722
+ stateSchemaId: artifacts.descriptor.replayIdentity.stateSchemaId,
1719
1723
  deterministicVersion: artifacts.descriptor.deterministicVersion,
1720
1724
  canonicalizeInput(input) {
1721
1725
  return artifacts.canonicalizeInput(input);
@@ -2072,6 +2076,38 @@ function createSyncplayOfflineRuntime(config) {
2072
2076
  return runtime;
2073
2077
  }
2074
2078
 
2079
+ // ../syncplay/src/collections.ts
2080
+ function stableSortedKeys(record) {
2081
+ return Object.keys(record).sort(compareStrings3);
2082
+ }
2083
+ function stableEntries(record) {
2084
+ return stableSortedKeys(record).map((id) => ({ id, value: record[id] }));
2085
+ }
2086
+ function createStableIndexedMap(entries) {
2087
+ const sorted = [...entries].sort((left, right) => compareStrings3(left.id, right.id));
2088
+ const result = /* @__PURE__ */ new Map();
2089
+ for (const entry of sorted) {
2090
+ if (result.has(entry.id)) {
2091
+ throw new Error(`Duplicate stable id ${entry.id}`);
2092
+ }
2093
+ result.set(entry.id, entry.value);
2094
+ }
2095
+ return result;
2096
+ }
2097
+ function compareStrings3(left, right) {
2098
+ return left < right ? -1 : left > right ? 1 : 0;
2099
+ }
2100
+ function deterministicShuffle(values, random) {
2101
+ const result = values.slice();
2102
+ for (let index = result.length - 1; index > 0; index -= 1) {
2103
+ const swapIndex = random.nextInt(0, index);
2104
+ const value = result[index];
2105
+ result[index] = result[swapIndex];
2106
+ result[swapIndex] = value;
2107
+ }
2108
+ return result;
2109
+ }
2110
+
2075
2111
  // ../syncplay/src/commands.ts
2076
2112
  function createDeterministicCommandTimeline(config) {
2077
2113
  validateCommandTimelineConfig(config);
@@ -2966,19 +3002,19 @@ function collectCommandAssetRefs(value, fields) {
2966
3002
  } else {
2967
3003
  visitBySchema(value, fields);
2968
3004
  }
2969
- return refs.sort(compareStrings3);
3005
+ return refs.sort(compareStrings4);
2970
3006
  }
2971
3007
  function stableRecordKeys(record) {
2972
- return Reflect.ownKeys(record).filter((key) => typeof key === "string").sort(compareStrings3);
3008
+ return Reflect.ownKeys(record).filter((key) => typeof key === "string").sort(compareStrings4);
2973
3009
  }
2974
3010
  function reject(command, reason) {
2975
3011
  return { commandId: command.id, reason };
2976
3012
  }
2977
3013
  function compareCommands(left, right) {
2978
- return left.frame - right.frame || left.slot - right.slot || compareStrings3(left.channelId, right.channelId) || compareStrings3(left.id, right.id);
3014
+ return left.frame - right.frame || left.slot - right.slot || compareStrings4(left.channelId, right.channelId) || compareStrings4(left.id, right.id);
2979
3015
  }
2980
3016
  function compareRejectedCommands(left, right) {
2981
- return compareStrings3(left.commandId, right.commandId) || compareStrings3(left.reason, right.reason);
3017
+ return compareStrings4(left.commandId, right.commandId) || compareStrings4(left.reason, right.reason);
2982
3018
  }
2983
3019
  function sortCommands(commands) {
2984
3020
  return [...commands].sort(compareCommands);
@@ -3003,10 +3039,401 @@ function percentile(values, percentileRank) {
3003
3039
  const sorted = [...values].sort((left, right) => left - right);
3004
3040
  return sorted[Math.min(sorted.length - 1, Math.floor(percentileRank / 100 * sorted.length))];
3005
3041
  }
3006
- function compareStrings3(left, right) {
3042
+ function compareStrings4(left, right) {
3007
3043
  return left < right ? -1 : left > right ? 1 : 0;
3008
3044
  }
3009
3045
 
3010
- export { DeterministicRandom, SyncplayError, assertDeterministicPayloadFields, canonicalStringify, canonicalize, cloneCanonical, createDeterministicCommandTimeline, createDeterministicCommandTimelineSnapshot, createDeterministicMath, createOfflineSession, createSchemaArtifacts, createSyncplayOfflineRuntime, decodeCanonicalBytes, decodeCanonicalValue, decodeUtf8, defaultChecksum, defineSyncplayGame, deterministicPayloadMatchesFields, encodeCanonicalBytes, encodeUtf8, evaluateDeterministicCommandTimelineMigrationResult, invariant, runDeterministicCommandProtocolFixture, runDeterministicCommandTimelineMigrationFixture, runDeterministicReliableCommandDefaultsFixture, stableDescriptorId, verifyReplay, verifyReplayWithDescriptor, withRuntimeDeterminismGuards };
3011
- //# sourceMappingURL=chunk-T6Q5GL7B.js.map
3012
- //# sourceMappingURL=chunk-T6Q5GL7B.js.map
3046
+ // ../syncplay/src/instant-replay.ts
3047
+ function runInstantReplay(options) {
3048
+ const session = options.createSession();
3049
+ session.restoreSnapshot(options.snapshot);
3050
+ let expectedFrame = options.snapshot.frame;
3051
+ for (const inputFrame of options.inputs) {
3052
+ if (inputFrame.frame !== expectedFrame) {
3053
+ throw new SyncplayError(
3054
+ `Instant replay inputs must be contiguous from snapshot frame ${options.snapshot.frame}: expected frame ${expectedFrame}, got ${inputFrame.frame}`,
3055
+ { code: "instant-replay.non-contiguous-inputs", frame: inputFrame.frame }
3056
+ );
3057
+ }
3058
+ for (let slot = 0; slot < inputFrame.inputs.length; slot += 1) {
3059
+ const recorded = inputFrame.inputs[slot];
3060
+ session.setInput(slot, options.decodeInput !== void 0 ? options.decodeInput(recorded, slot) : recorded);
3061
+ }
3062
+ session.stepFrames(1);
3063
+ options.onFrame(session.getState(), session.currentFrame);
3064
+ expectedFrame += 1;
3065
+ }
3066
+ const finalSnapshot = session.getSnapshot(session.currentFrame);
3067
+ if (finalSnapshot === void 0) {
3068
+ throw new SyncplayError("Instant replay session has no snapshot at its final frame", {
3069
+ code: "instant-replay.missing-final-snapshot",
3070
+ frame: session.currentFrame
3071
+ });
3072
+ }
3073
+ return { framesPlayed: options.inputs.length, finalChecksum: finalSnapshot.checksum };
3074
+ }
3075
+ function createInstantReplayRunner(config, options) {
3076
+ invariant(
3077
+ Number.isInteger(options.window) && options.window >= 1,
3078
+ "Instant replay window must be a positive integer",
3079
+ "instant-replay.invalid-window"
3080
+ );
3081
+ const replayConfig = {
3082
+ ...config,
3083
+ replayRecordingMode: "none",
3084
+ snapshotBufferSize: 1,
3085
+ onInputFrameRecorded: void 0,
3086
+ onCommandFrameRecorded: void 0,
3087
+ onChecksumRecorded: void 0
3088
+ };
3089
+ return {
3090
+ window: options.window,
3091
+ run(runOptions) {
3092
+ if (runOptions.inputs.length > options.window) {
3093
+ throw new SyncplayError(
3094
+ `Instant replay covers ${runOptions.inputs.length} ticks, above window ${options.window}`,
3095
+ { code: "instant-replay.window-exceeded", frame: runOptions.snapshot.frame }
3096
+ );
3097
+ }
3098
+ return runInstantReplay({
3099
+ createSession: () => createOfflineSession(replayConfig),
3100
+ snapshot: runOptions.snapshot,
3101
+ inputs: runOptions.inputs,
3102
+ decodeInput: runOptions.decodeInput,
3103
+ onFrame: runOptions.onFrame
3104
+ });
3105
+ }
3106
+ };
3107
+ }
3108
+
3109
+ // ../syncplay/src/pause-resume.ts
3110
+ function createPauseController(options) {
3111
+ const step = (state, inputs, frame, ctx) => {
3112
+ const previousHeld = state.pause.toggleHeld;
3113
+ let risingEdges = 0;
3114
+ const toggleHeld = inputs.map((input, slot) => {
3115
+ const held = options.isPauseToggle(input, slot) === true;
3116
+ if (held && previousHeld[slot] !== true) {
3117
+ risingEdges += 1;
3118
+ }
3119
+ return held;
3120
+ });
3121
+ const flips = risingEdges % 2 === 1;
3122
+ const status = flips ? state.pause.status === "running" ? "paused" : "running" : state.pause.status;
3123
+ const toggleCount = state.pause.toggleCount + risingEdges;
3124
+ if (status === "paused") {
3125
+ return {
3126
+ pause: { status, pausedFrames: state.pause.pausedFrames + 1, toggleCount, toggleHeld },
3127
+ game: state.game
3128
+ };
3129
+ }
3130
+ return {
3131
+ pause: { status, pausedFrames: state.pause.pausedFrames, toggleCount, toggleHeld },
3132
+ game: options.wrap(state.game, inputs, frame, ctx)
3133
+ };
3134
+ };
3135
+ return {
3136
+ wrapInitialState(gameState) {
3137
+ return {
3138
+ pause: { status: "running", pausedFrames: 0, toggleCount: 0, toggleHeld: [] },
3139
+ game: gameState
3140
+ };
3141
+ },
3142
+ step,
3143
+ isPaused(state) {
3144
+ return state.pause.status === "paused";
3145
+ }
3146
+ };
3147
+ }
3148
+
3149
+ // ../syncplay/src/session-snapshot.ts
3150
+ function deterministicRoomDescriptorHash(params) {
3151
+ return defaultChecksum({
3152
+ kind: "rdm-room-descriptor",
3153
+ seed: params.seed,
3154
+ playerCount: params.playerCount,
3155
+ tickRateHz: params.tickRateHz,
3156
+ hardToleranceTicks: params.hardToleranceTicks
3157
+ });
3158
+ }
3159
+ function encodeDeterministicStateSnapshot(snapshot) {
3160
+ return canonicalStringify(snapshot);
3161
+ }
3162
+ function decodeDeterministicStateSnapshot(text) {
3163
+ let parsed;
3164
+ try {
3165
+ parsed = JSON.parse(text);
3166
+ } catch {
3167
+ return { ok: false, reason: "invalid-json" };
3168
+ }
3169
+ if (!isRecord2(parsed)) {
3170
+ return { ok: false, reason: "not-a-snapshot" };
3171
+ }
3172
+ if (parsed.v !== 1) {
3173
+ return { ok: false, reason: "unsupported-snapshot-version" };
3174
+ }
3175
+ if (!isNonNegativeInt(parsed.tick) || !isNonNegativeInt(parsed.sessionFrame) || parsed.sessionFrame !== parsed.tick + 1 || typeof parsed.stateBytes !== "string" || !Number.isInteger(parsed.randomState) || typeof parsed.stateChecksum !== "string" || parsed.stateChecksum.length === 0 || !Array.isArray(parsed.confirmedInputs)) {
3176
+ return { ok: false, reason: "malformed-snapshot" };
3177
+ }
3178
+ return {
3179
+ ok: true,
3180
+ snapshot: {
3181
+ v: 1,
3182
+ tick: parsed.tick,
3183
+ sessionFrame: parsed.sessionFrame,
3184
+ stateBytes: parsed.stateBytes,
3185
+ randomState: parsed.randomState,
3186
+ stateChecksum: parsed.stateChecksum,
3187
+ confirmedInputs: cloneCanonical(parsed.confirmedInputs)
3188
+ }
3189
+ };
3190
+ }
3191
+ var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3192
+ var BASE64_LOOKUP = new Map(
3193
+ [...BASE64_ALPHABET].map((char, index) => [char, index])
3194
+ );
3195
+ function bytesToBase64(bytes) {
3196
+ let out = "";
3197
+ for (let index = 0; index < bytes.length; index += 3) {
3198
+ const b0 = bytes[index];
3199
+ const b1 = index + 1 < bytes.length ? bytes[index + 1] : 0;
3200
+ const b2 = index + 2 < bytes.length ? bytes[index + 2] : 0;
3201
+ out += BASE64_ALPHABET[b0 >> 2];
3202
+ out += BASE64_ALPHABET[(b0 & 3) << 4 | b1 >> 4];
3203
+ out += index + 1 < bytes.length ? BASE64_ALPHABET[(b1 & 15) << 2 | b2 >> 6] : "=";
3204
+ out += index + 2 < bytes.length ? BASE64_ALPHABET[b2 & 63] : "=";
3205
+ }
3206
+ return out;
3207
+ }
3208
+ function base64ToBytes(text) {
3209
+ if (text.length % 4 !== 0) {
3210
+ throw new Error("base64 length must be a multiple of 4");
3211
+ }
3212
+ const padding = text.endsWith("==") ? 2 : text.endsWith("=") ? 1 : 0;
3213
+ const byteLength = text.length / 4 * 3 - padding;
3214
+ const bytes = new Uint8Array(byteLength);
3215
+ let byteIndex = 0;
3216
+ for (let index = 0; index < text.length; index += 4) {
3217
+ const values = [];
3218
+ for (let offset = 0; offset < 4; offset += 1) {
3219
+ const char = text[index + offset];
3220
+ if (char === "=") {
3221
+ if (index + 4 < text.length || offset < 2) {
3222
+ throw new Error("base64 padding is only valid at the end");
3223
+ }
3224
+ values.push(0);
3225
+ continue;
3226
+ }
3227
+ const value = BASE64_LOOKUP.get(char);
3228
+ if (value === void 0) {
3229
+ throw new Error(`invalid base64 character ${JSON.stringify(char)}`);
3230
+ }
3231
+ values.push(value);
3232
+ }
3233
+ const triple = values[0] << 18 | values[1] << 12 | values[2] << 6 | values[3];
3234
+ if (byteIndex < byteLength) bytes[byteIndex++] = triple >> 16 & 255;
3235
+ if (byteIndex < byteLength) bytes[byteIndex++] = triple >> 8 & 255;
3236
+ if (byteIndex < byteLength) bytes[byteIndex++] = triple & 255;
3237
+ }
3238
+ return bytes;
3239
+ }
3240
+ var DEFAULT_SNAPSHOT_CHUNK_SIZE = 262144;
3241
+ var DEFAULT_MAX_SNAPSHOT_BYTES = 4194304;
3242
+ function createSnapshotTransferFromBytes(snapshotBytes, options) {
3243
+ const chunkSize = options.chunkSize ?? DEFAULT_SNAPSHOT_CHUNK_SIZE;
3244
+ if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
3245
+ throw new Error("Snapshot chunkSize must be a positive integer");
3246
+ }
3247
+ if (snapshotBytes.length === 0) {
3248
+ throw new Error("Snapshot bytes must be non-empty");
3249
+ }
3250
+ const chunks = [];
3251
+ for (let offset = 0; offset < snapshotBytes.length; offset += chunkSize) {
3252
+ chunks.push(snapshotBytes.slice(offset, offset + chunkSize));
3253
+ }
3254
+ return {
3255
+ transferId: options.transferId,
3256
+ identity: cloneCanonical(options.identity),
3257
+ frame: options.frame,
3258
+ snapshotChecksum: defaultChecksum(snapshotBytes),
3259
+ snapshotBytes,
3260
+ chunkSize,
3261
+ chunks,
3262
+ chunkChecksums: chunks.map((chunk) => defaultChecksum(chunk))
3263
+ };
3264
+ }
3265
+ function createSnapshotTransferCollector(options = {}) {
3266
+ const maxSnapshotBytes = options.maxSnapshotBytes ?? DEFAULT_MAX_SNAPSHOT_BYTES;
3267
+ const pending = /* @__PURE__ */ new Map();
3268
+ return {
3269
+ offer(payload) {
3270
+ const identity = parseIdentity(payload.identity);
3271
+ if (typeof payload.transferId !== "string" || payload.transferId.length === 0 || identity === void 0 || !isNonNegativeInt(payload.frame) || typeof payload.snapshotChecksum !== "string" || !isNonNegativeInt(payload.chunkCount) || payload.chunkCount < 1 || !isNonNegativeInt(payload.totalBytes) || payload.totalBytes < 1 || // Every chunk carries at least one character, so a chunkCount that
3272
+ // exceeds totalBytes is self-contradictory (forged offer).
3273
+ payload.chunkCount > payload.totalBytes || !isStringArray(payload.chunkChecksums) || payload.chunkChecksums.length !== payload.chunkCount) {
3274
+ return { accepted: false, reason: "malformed-snapshot-offer" };
3275
+ }
3276
+ if (payload.totalBytes > maxSnapshotBytes) {
3277
+ return { accepted: false, reason: "snapshot-too-large" };
3278
+ }
3279
+ pending.set(payload.transferId, {
3280
+ transferId: payload.transferId,
3281
+ identity,
3282
+ frame: payload.frame,
3283
+ snapshotChecksum: payload.snapshotChecksum,
3284
+ chunkCount: payload.chunkCount,
3285
+ totalBytes: payload.totalBytes,
3286
+ chunkChecksums: [...payload.chunkChecksums],
3287
+ chunks: /* @__PURE__ */ new Map()
3288
+ });
3289
+ return { accepted: true };
3290
+ },
3291
+ chunk(payload) {
3292
+ if (typeof payload.transferId !== "string") {
3293
+ return { accepted: false, reason: "malformed-snapshot-chunk" };
3294
+ }
3295
+ const transfer = pending.get(payload.transferId);
3296
+ if (transfer === void 0) {
3297
+ return { accepted: false, reason: "unknown-snapshot-transfer" };
3298
+ }
3299
+ if (typeof payload.chunk !== "string" || typeof payload.chunkChecksum !== "string") {
3300
+ return { accepted: false, reason: "malformed-snapshot-chunk" };
3301
+ }
3302
+ const chunkIndex = payload.chunkIndex;
3303
+ if (!isNonNegativeInt(chunkIndex) || chunkIndex >= transfer.chunkCount) {
3304
+ return { accepted: false, reason: "snapshot-chunk-index-out-of-range" };
3305
+ }
3306
+ if (transfer.chunks.has(chunkIndex)) {
3307
+ return { accepted: false, reason: "duplicate-snapshot-chunk" };
3308
+ }
3309
+ if (transfer.chunkChecksums[chunkIndex] !== payload.chunkChecksum || defaultChecksum(payload.chunk) !== payload.chunkChecksum) {
3310
+ return { accepted: false, reason: "snapshot-chunk-checksum-mismatch" };
3311
+ }
3312
+ transfer.chunks.set(chunkIndex, payload.chunk);
3313
+ return { accepted: true };
3314
+ },
3315
+ complete(payload) {
3316
+ if (typeof payload.transferId !== "string") {
3317
+ return { accepted: false, reason: "malformed-snapshot-complete" };
3318
+ }
3319
+ const transfer = pending.get(payload.transferId);
3320
+ if (transfer === void 0) {
3321
+ return { accepted: false, reason: "unknown-snapshot-transfer" };
3322
+ }
3323
+ if (transfer.chunks.size !== transfer.chunkCount) {
3324
+ return { accepted: false, reason: "missing-snapshot-chunk" };
3325
+ }
3326
+ const snapshotBytes = Array.from(
3327
+ { length: transfer.chunkCount },
3328
+ (_, index) => transfer.chunks.get(index)
3329
+ ).join("");
3330
+ pending.delete(payload.transferId);
3331
+ if (snapshotBytes.length !== transfer.totalBytes) {
3332
+ return { accepted: false, reason: "snapshot-byte-length-mismatch" };
3333
+ }
3334
+ if (defaultChecksum(snapshotBytes) !== transfer.snapshotChecksum) {
3335
+ return { accepted: false, reason: "snapshot-transfer-checksum-mismatch" };
3336
+ }
3337
+ return {
3338
+ accepted: true,
3339
+ transfer: {
3340
+ transferId: transfer.transferId,
3341
+ identity: transfer.identity,
3342
+ frame: transfer.frame,
3343
+ snapshotChecksum: transfer.snapshotChecksum,
3344
+ snapshotBytes
3345
+ }
3346
+ };
3347
+ },
3348
+ abort(transferId) {
3349
+ pending.delete(transferId);
3350
+ },
3351
+ get pendingTransferIds() {
3352
+ return [...pending.keys()];
3353
+ }
3354
+ };
3355
+ }
3356
+ function parseIdentity(value) {
3357
+ if (!isRecord2(value) || typeof value.descriptorHash !== "string" || value.descriptorHash.length === 0) {
3358
+ return void 0;
3359
+ }
3360
+ return {
3361
+ descriptorHash: value.descriptorHash,
3362
+ ...typeof value.schemaHash === "string" ? { schemaHash: value.schemaHash } : {},
3363
+ ...typeof value.assetDbHash === "string" ? { assetDbHash: value.assetDbHash } : {},
3364
+ ...typeof value.configHash === "string" ? { configHash: value.configHash } : {}
3365
+ };
3366
+ }
3367
+ function isRecord2(value) {
3368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
3369
+ }
3370
+ function isNonNegativeInt(value) {
3371
+ return typeof value === "number" && Number.isInteger(value) && value >= 0;
3372
+ }
3373
+ function isStringArray(value) {
3374
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
3375
+ }
3376
+
3377
+ // ../syncplay/src/session-promotion.ts
3378
+ function exportPromotionSnapshot(session, frame) {
3379
+ const targetFrame = frame ?? session.currentFrame;
3380
+ invariant(
3381
+ Number.isInteger(targetFrame) && targetFrame >= 1,
3382
+ "Promotion export requires a stepped frame (>= 1): the v1 snapshot envelope encodes tick = frame - 1",
3383
+ "promotion.invalid-frame"
3384
+ );
3385
+ const snapshot = session.getSnapshot(targetFrame);
3386
+ if (snapshot === void 0) {
3387
+ throw new SyncplayError(
3388
+ `No snapshot buffered for frame ${targetFrame} \u2014 it may have been evicted from the rolling buffer`,
3389
+ { code: "promotion.missing-snapshot", frame: targetFrame }
3390
+ );
3391
+ }
3392
+ return encodeDeterministicStateSnapshot({
3393
+ v: 1,
3394
+ tick: targetFrame - 1,
3395
+ sessionFrame: targetFrame,
3396
+ stateBytes: bytesToBase64(snapshot.stateBytes),
3397
+ randomState: snapshot.randomState,
3398
+ stateChecksum: snapshot.checksum,
3399
+ // Session inputs are canonical by construction (cloneCanonical on setInput).
3400
+ confirmedInputs: snapshot.inputs.map((input) => canonicalize(input))
3401
+ });
3402
+ }
3403
+ function createSessionFromPromotionSnapshot(config, blob) {
3404
+ const decoded = decodeDeterministicStateSnapshot(blob);
3405
+ if (!decoded.ok) {
3406
+ throw new SyncplayError(`Promotion snapshot rejected: ${decoded.reason}`, {
3407
+ code: "promotion.invalid-snapshot"
3408
+ });
3409
+ }
3410
+ const snapshot = decoded.snapshot;
3411
+ invariant(
3412
+ snapshot.confirmedInputs.length === config.playerCount,
3413
+ `Promotion snapshot carries ${snapshot.confirmedInputs.length} input slots but config.playerCount is ${config.playerCount}`,
3414
+ "promotion.player-count-mismatch"
3415
+ );
3416
+ const session = createOfflineSession(config);
3417
+ session.restoreSnapshot({
3418
+ frame: snapshot.sessionFrame,
3419
+ stateBytes: base64ToBytes(snapshot.stateBytes),
3420
+ randomState: snapshot.randomState,
3421
+ inputs: snapshot.confirmedInputs.map(
3422
+ // NOT `??`: a hydrator returning null (the canonical neutral) must win
3423
+ // over the raw recorded value (adversarial-review LOW).
3424
+ (recorded) => config.hydrateInput !== void 0 ? config.hydrateInput(recorded) : recorded
3425
+ )
3426
+ });
3427
+ const restored = session.getSnapshot(session.currentFrame);
3428
+ if (restored === void 0 || restored.checksum !== snapshot.stateChecksum) {
3429
+ throw new SyncplayError(
3430
+ "Promotion snapshot checksum mismatch after hydration \u2014 the game build or state bytes do not match the exporter",
3431
+ { code: "promotion.checksum-mismatch", frame: snapshot.sessionFrame }
3432
+ );
3433
+ }
3434
+ return session;
3435
+ }
3436
+
3437
+ export { DEFAULT_MAX_SNAPSHOT_BYTES, DEFAULT_SNAPSHOT_CHUNK_SIZE, DeterministicRandom, SyncplayError, assertDeterministicPayloadFields, base64ToBytes, bytesToBase64, canonicalStringify, canonicalize, cloneCanonical, createDeterministicCommandTimeline, createDeterministicCommandTimelineSnapshot, createDeterministicMath, createInstantReplayRunner, createOfflineSession, createPauseController, createSchemaArtifacts, createSessionFromPromotionSnapshot, createSnapshotTransferCollector, createSnapshotTransferFromBytes, createStableIndexedMap, createSyncplayOfflineRuntime, decodeCanonicalBytes, decodeCanonicalValue, decodeDeterministicStateSnapshot, decodeUtf8, defaultChecksum, defineSyncplayGame, deterministicPayloadMatchesFields, deterministicRoomDescriptorHash, deterministicShuffle, encodeCanonicalBytes, encodeDeterministicStateSnapshot, encodeUtf8, evaluateDeterministicCommandTimelineMigrationResult, exportPromotionSnapshot, invariant, runDeterministicCommandProtocolFixture, runDeterministicCommandTimelineMigrationFixture, runDeterministicReliableCommandDefaultsFixture, runInstantReplay, stableDescriptorId, stableEntries, stableSortedKeys, verifyReplay, verifyReplayWithDescriptor, withRuntimeDeterminismGuards };
3438
+ //# sourceMappingURL=chunk-G7TYQYHC.js.map
3439
+ //# sourceMappingURL=chunk-G7TYQYHC.js.map