@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.
- package/dist/{SyncplayRoomTransport-DXNJxFKZ.d.ts → SyncplayRoomTransport-ByIm59Ie.d.ts} +157 -2
- package/dist/{chunk-T6Q5GL7B.js → chunk-G7TYQYHC.js} +437 -10
- package/dist/chunk-G7TYQYHC.js.map +1 -0
- package/dist/{chunk-WVJVQAW7.js → chunk-MCVCZ7W3.js} +3 -3
- package/dist/{chunk-WVJVQAW7.js.map → chunk-MCVCZ7W3.js.map} +1 -1
- package/dist/chunk-VZLX7S73.js +131 -0
- package/dist/chunk-VZLX7S73.js.map +1 -0
- package/dist/{index-DB99BKTt.d.ts → index-UMslJhz5.d.ts} +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/playground/index.js +26 -1
- package/dist/playground/index.js.map +1 -1
- package/dist/rundot-game-api/index.js +1 -1
- package/dist/session-promotion-DgrVQE2y.d.ts +136 -0
- package/dist/syncplay/browser.d.ts +103 -81
- package/dist/syncplay/browser.js +560 -388
- package/dist/syncplay/browser.js.map +1 -1
- package/dist/syncplay/creator.d.ts +36 -2
- package/dist/syncplay/creator.js +2 -2
- package/dist/syncplay/index.d.ts +2 -2
- package/dist/syncplay/index.js +2 -2
- package/dist/syncplay/node.js +4 -2
- package/dist/syncplay/node.js.map +1 -1
- package/dist/{types-BLIISDww.d.ts → types-DbhOvOdx.d.ts} +1 -1
- package/dist/vite/{dev-server-5OIDIMOM.js → dev-server-V3IZP5XX.js} +144 -28
- package/dist/vite/dev-server-V3IZP5XX.js.map +1 -0
- package/dist/vite/{esbuild-validate-IXG2OBFH.js → esbuild-validate-3ISKFZOW.js} +5 -2
- package/dist/vite/esbuild-validate-3ISKFZOW.js.map +1 -0
- package/dist/vite/index.js +30 -32
- package/dist/vite/index.js.map +1 -1
- package/docs/rundot-developer-platform/api/SYNCPLAY.md +47 -0
- package/package.json +2 -1
- package/dist/chunk-QLHQTYP6.js +0 -3
- package/dist/chunk-QLHQTYP6.js.map +0 -1
- package/dist/chunk-T6Q5GL7B.js.map +0 -1
- package/dist/vite/dev-server-5OIDIMOM.js.map +0 -1
- package/dist/vite/esbuild-validate-IXG2OBFH.js.map +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as CanonicalValue,
|
|
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
|
|
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
|
-
|
|
1718
|
-
|
|
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(
|
|
3005
|
+
return refs.sort(compareStrings4);
|
|
2970
3006
|
}
|
|
2971
3007
|
function stableRecordKeys(record) {
|
|
2972
|
-
return Reflect.ownKeys(record).filter((key) => typeof key === "string").sort(
|
|
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 ||
|
|
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
|
|
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
|
|
3042
|
+
function compareStrings4(left, right) {
|
|
3007
3043
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
3008
3044
|
}
|
|
3009
3045
|
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
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
|