@series-inc/rundot-game-sdk 5.23.0-beta.5 → 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.
Files changed (38) hide show
  1. package/dist/SyncplayRoomTransport-D6xsCdn5.d.ts +379 -0
  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-RXJPLBXQ.js} +3 -3
  5. package/dist/{chunk-WVJVQAW7.js.map → chunk-RXJPLBXQ.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 +129 -81
  16. package/dist/syncplay/browser.js +710 -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/SyncplayRoomTransport-DXNJxFKZ.d.ts +0 -196
  34. package/dist/chunk-QLHQTYP6.js +0 -3
  35. package/dist/chunk-QLHQTYP6.js.map +0 -1
  36. package/dist/chunk-T6Q5GL7B.js.map +0 -1
  37. package/dist/vite/dev-server-5OIDIMOM.js.map +0 -1
  38. package/dist/vite/esbuild-validate-IXG2OBFH.js.map +0 -1
@@ -0,0 +1,379 @@
1
+ import { C as CanonicalValue, h as ReplayFile, O as OfflineSession } from './types-DbhOvOdx.js';
2
+ import { M as MultiplayerApi } from './MultiplayerApi-nnrGTTcO.js';
3
+
4
+ /**
5
+ * Game-agnostic input authority (server orders inputs, clients simulate — §2 Option 1).
6
+ *
7
+ * It orders per-slot inputs by tick and confirms each tick exactly once, in order,
8
+ * either when every player slot has reported or when a HardTolerance deadline elapses
9
+ * (substituting the slot's last confirmed input). It never runs the simulation — only
10
+ * inputs cross the wire and clients run the deterministic sim locally.
11
+ *
12
+ * This module is the reusable core: the M0 spike hosts it over sockets, and M2 lifts it
13
+ * into the mp-room-server room type unchanged.
14
+ */
15
+ /** Opaque, engine-defined input payload. The authority never interprets it. */
16
+ type EncodedInput = CanonicalValue;
17
+ interface ConfirmedInputFrame {
18
+ readonly tick: number;
19
+ /** One entry per player slot, in slot order. */
20
+ readonly inputs: readonly EncodedInput[];
21
+ /** Slots whose input was substituted (repeated) because it never arrived in time. */
22
+ readonly substitutedSlots: readonly number[];
23
+ readonly checksum: string;
24
+ }
25
+ interface InputAuthorityConfig {
26
+ readonly playerCount: number;
27
+ /** Confirm a tick once `now - tick >= hardToleranceTicks`, substituting any missing input. */
28
+ readonly hardToleranceTicks: number;
29
+ /** Substitute this when a slot has no prior confirmed input to repeat. */
30
+ readonly neutralInput: EncodedInput;
31
+ /** Reject inputs whose tick is further ahead than `confirmedThrough + maxFutureTicks`. */
32
+ readonly maxFutureTicks?: number;
33
+ /** Confirmed frames retained for the redundant broadcast window. */
34
+ readonly retainedConfirmedFrames?: number;
35
+ /**
36
+ * Rebuild a previously-persisted authority (M6.5 crash recovery): the
37
+ * retained frames, frontier, and per-slot last-confirmed inputs (derived
38
+ * from the final frame — confirmed inputs equal the post-substitution
39
+ * last-confirmed values by construction). Fail-loud on gaps or a frontier
40
+ * mismatch.
41
+ */
42
+ readonly restoreFrom?: InputAuthorityRestoreState;
43
+ }
44
+ interface InputAuthorityRestoreState {
45
+ readonly confirmedThrough: number;
46
+ readonly frames: readonly ConfirmedInputFrame[];
47
+ }
48
+ type InputRejectionReason = 'slot-out-of-range' | 'invalid-tick' | 'tick-already-confirmed' | 'tick-too-far-ahead';
49
+ interface InputSubmissionResult {
50
+ readonly accepted: boolean;
51
+ readonly reason?: InputRejectionReason;
52
+ }
53
+ interface InputAuthorityStats {
54
+ readonly confirmedTicks: number;
55
+ readonly substitutedInputs: number;
56
+ readonly droppedLateInputs: number;
57
+ readonly rejectedFutureInputs: number;
58
+ }
59
+ interface InputAuthority {
60
+ readonly playerCount: number;
61
+ /** Highest confirmed tick, or -1 when nothing is confirmed yet. */
62
+ readonly confirmedThrough: number;
63
+ /** Lowest confirmed tick still retained, or -1 when nothing is confirmed yet. */
64
+ readonly earliestRetainedTick: number;
65
+ receiveInput(slot: number, tick: number, input: EncodedInput): InputSubmissionResult;
66
+ /** Confirm every tick that can be confirmed up to and including `now`. Returns the new frames in order. */
67
+ advance(now: number): readonly ConfirmedInputFrame[];
68
+ /** A confirmed frame still inside the retained window, for redundant re-broadcast. */
69
+ confirmedFrame(tick: number): ConfirmedInputFrame | undefined;
70
+ /**
71
+ * The retained frames `from..to` inclusive, for persistence (M6.5). Throws
72
+ * if any tick in the range is no longer retained — a persisted record with
73
+ * a silent gap would strand every restored client.
74
+ */
75
+ confirmedFrameRange(from: number, to: number): readonly ConfirmedInputFrame[];
76
+ /**
77
+ * Drop retained frames below `tick` (M5.5: history before a validated
78
+ * snapshot is no longer needed for late-join catch-up). Never moves the
79
+ * frontier; a no-op at or below the current earliest retained tick.
80
+ */
81
+ pruneConfirmedBefore(tick: number): void;
82
+ stats(): InputAuthorityStats;
83
+ }
84
+ declare function createInputAuthority(config: InputAuthorityConfig): InputAuthority;
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
+
175
+ /**
176
+ * Reusable client transport for game-agnostic deterministic multiplayer (§6 M4).
177
+ *
178
+ * A game (or the H5 host bridge) hands this a socket-like transport and a factory for the game's
179
+ * deterministic session. The client speaks the frozen `rdm-local-authority/v1` session protocol:
180
+ * it consumes `session-start` (slot + server-owned seed), predicts locally (repeating opponents'
181
+ * last confirmed input), submits `input`, and reconciles `confirmed-input` via rollback/resim.
182
+ * Only inputs cross the wire; the simulation runs entirely here. This is the same predict/rollback
183
+ * contract the M0 spike proved, packaged for real transports.
184
+ */
185
+ interface NetworkedClientTransport {
186
+ send(text: string): void;
187
+ onMessage(handler: (text: string) => void): void;
188
+ }
189
+ interface NetworkedSyncplayClientOptions<State, Input> {
190
+ readonly transport: NetworkedClientTransport;
191
+ /** Build the game session once the server issues the seed and player count. */
192
+ readonly createSession: (params: {
193
+ seed: number;
194
+ playerCount: number;
195
+ }) => OfflineSession<State, Input>;
196
+ /** This client's own input for a tick (game-defined). */
197
+ readonly localInputForTick: (slot: number, tick: number) => Input;
198
+ readonly encodeInput: (input: Input) => EncodedInput;
199
+ /** Decode a wire input to a game input; MUST map the neutral substitute (canonical null) to the default. */
200
+ readonly decodeInput: (encoded: EncodedInput) => Input;
201
+ readonly maxPredictionTicks?: number;
202
+ /** Host wire envelope msgType carrying session messages. Default 'rdm'. */
203
+ readonly sessionMsgType?: string;
204
+ /**
205
+ * Auto-send a checksum-report whenever `appliedThrough` crosses a multiple
206
+ * of this cadence (M5.5). Aligned reports across clients are what let the
207
+ * authority quorum-validate donated snapshots; they also feed desync
208
+ * telemetry. Reports are read from the rolling snapshot buffer — never via
209
+ * `restoreToFrame`, which would rewind the live session. Default 120.
210
+ */
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;
265
+ }
266
+ interface NetworkedSyncplayClient {
267
+ readonly slot: number;
268
+ /** 'player' (holds a slot) or 'spectator' (read-only follower, slot -1). From session-start. */
269
+ readonly role: DeterministicSessionRole;
270
+ readonly ready: boolean;
271
+ readonly predictedThrough: number;
272
+ readonly appliedThrough: number;
273
+ readonly rollbacks: number;
274
+ /** Live local netcode telemetry (RTT, timescale, delay, rollback depth). */
275
+ readonly netStats: NetworkedSyncplayClientNetStats;
276
+ /** Resolves once `session-start` has been received. */
277
+ whenReady(): Promise<void>;
278
+ /** Predict and submit local input up to the prediction budget. */
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;
289
+ /** Report this client's confirmed-state checksum for desync telemetry. */
290
+ reportChecksum(): void;
291
+ /** Confirmed-state checksum at `frame` (must be <= appliedThrough + 1). */
292
+ confirmedStateChecksum(frame: number): string;
293
+ exportReplay(): ReplayFile;
294
+ }
295
+ declare function createNetworkedSyncplayClient<State, Input>(options: NetworkedSyncplayClientOptions<State, Input>): NetworkedSyncplayClient;
296
+
297
+ /**
298
+ * Reusable, production-shaped client surface for deterministic multiplayer.
299
+ *
300
+ * A game joins a deterministic room by code (or creates one) and gets back a
301
+ * {@link NetworkedClientTransport} — the exact socket-like surface the
302
+ * deterministic engine's `createNetworkedSyncplayClient` consumes. There is
303
+ * NO per-game server logic: the server-side deterministic room (built-in input
304
+ * authority) is generic, and this transport is a thin, opaque pass-through of the
305
+ * frozen `rdm-local-authority/v1` session envelope over the `rdm` room channel.
306
+ *
307
+ * The room's raw channel ({@link ServerRoom.sendRaw}/{@link ServerRoom.onRaw})
308
+ * carries the session envelope verbatim; the typed-message path would spread a
309
+ * raw string and corrupt it, which is why the raw channel exists.
310
+ */
311
+
312
+ /** Connection-health signal surfaced by {@link SyncplayRoomTransport.onConnectionEvent}. */
313
+ type SyncplayConnectionEvent = {
314
+ readonly kind: 'error';
315
+ readonly message: string;
316
+ } | {
317
+ readonly kind: 'disconnected';
318
+ } | {
319
+ readonly kind: 'reconnecting';
320
+ };
321
+ interface SyncplayRoomTransport extends NetworkedClientTransport {
322
+ /** The shareable room code (e.g. "HX9KWR"). */
323
+ readonly roomCode: string;
324
+ /** This client's player id, as assigned by the server on join. */
325
+ readonly playerId: string;
326
+ /** Leave the deterministic room and close the underlying connection. */
327
+ close(): void;
328
+ /**
329
+ * Connection-health signal: socket errors, disconnects, and reconnect
330
+ * attempts. Without observing this, a mid-match socket drop is silent — the
331
+ * room layer no-ops sends while dead, so the engine would predict into a
332
+ * dead socket forever. Games should surface these to the player and/or tear
333
+ * the match down.
334
+ */
335
+ onConnectionEvent(handler: (event: SyncplayConnectionEvent) => void): void;
336
+ }
337
+ /**
338
+ * Join an existing deterministic room by its 6-char code and return a transport
339
+ * ready to hand to `createNetworkedSyncplayClient`.
340
+ */
341
+ declare function joinSyncplayRoomByCode(api: MultiplayerApi, code: string): Promise<SyncplayRoomTransport>;
342
+ /** Options for {@link quickMatchSyncplayRoom}. */
343
+ interface QuickMatchSyncplayOptions {
344
+ /**
345
+ * Auto-inject a coarse `region` criterion (default true). See
346
+ * {@link quickMatchSyncplayRoom} for the semantics and the tradeoff.
347
+ */
348
+ readonly autoRegion?: boolean;
349
+ }
350
+ /**
351
+ * Quick-match into a deterministic room (M9): the platform's joinOrCreate
352
+ * matchmaking pools compatible players into an open room or creates a fresh
353
+ * one. Optional `criteria` narrow the pool (matched exactly by the platform).
354
+ * With drop-in defaults the match starts immediately; unfilled slots
355
+ * substitute the canonical null input, which a game can treat as
356
+ * "bot-controlled" deterministically (see SYNCPLAY.md — Bots & quick match).
357
+ *
358
+ * Region-aware by default (G6): a coarse `region` criterion ('na' | 'sa' |
359
+ * 'eu' | 'africa' | 'asia' | 'oceania', derived from the device timezone) is
360
+ * auto-injected so cross-ocean pairings — whose RTT forces constant deep
361
+ * rollback — don't happen by accident. An explicit `criteria.region` wins
362
+ * over the derived value, and an underivable region ('unknown') is never
363
+ * injected: pooling globally beats fragmenting into an 'unknown' bucket.
364
+ *
365
+ * Tradeoff: strict region pools can go thin for low-traffic games. Pass
366
+ * `{ autoRegion: false }` (or your own `region` value) to pool globally;
367
+ * cross-region fallback-after-timeout is future work.
368
+ */
369
+ declare function quickMatchSyncplayRoom(api: MultiplayerApi, criteria?: Record<string, string | number>, options?: QuickMatchSyncplayOptions): Promise<SyncplayRoomTransport>;
370
+ /**
371
+ * Create a new deterministic room and return a transport ready to hand to
372
+ * `createNetworkedSyncplayClient`. The server mints the room code; read it
373
+ * back from {@link SyncplayRoomTransport.roomCode} to share with peers.
374
+ * (Caller-chosen codes are not supported on create — when the platform grows
375
+ * that path, this signature gains a typed parameter rather than a dead one.)
376
+ */
377
+ declare function createSyncplayRoom(api: MultiplayerApi): Promise<SyncplayRoomTransport>;
378
+
379
+ export { type ConfirmedInputFrame as C, type DeterministicSessionDecodeRejection as D, type EncodedInput as E, type InputAuthority as I, type NetworkedClientTransport as N, type QuickMatchSyncplayOptions as Q, 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, quickMatchSyncplayRoom as v };