@series-inc/rundot-syncplay 5.25.0-beta.4 → 5.25.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/README.md CHANGED
@@ -157,6 +157,25 @@ presentation-only and never feeds simulation. Starting a new mode always tears
157
157
  down the old one, and a stale async connection/preparation closes without
158
158
  replacing the current session.
159
159
 
160
+ **Incremental adoption (mature games).** A game with its own stateful renderer,
161
+ input model, and connection UI can swap only its lifecycle orchestration onto the
162
+ runner without rewriting those layers:
163
+
164
+ - Keep your own presentation singleton — use an identity `project: (frame) => frame`
165
+ and feed your renderer from the `subscribe` snapshot (`renderState`, `renderAlpha`)
166
+ instead of `getRenderState()`.
167
+ - Supply `localInputForTick: (slot, frame) => Input` when your local input is a
168
+ per-tick pulse/edge that must be consumed and cleared each frame; the runner then
169
+ reads it (offline local slot and networked prediction) instead of the persistent
170
+ `applyInput(...)` value.
171
+ - Drive a room badge from the snapshot's `ready`, `appliedThrough`, `predictedThrough`,
172
+ `localSlot`, and `status` — no reach into the underlying client (e.g. `appliedThrough < 0`
173
+ means "waiting for peers"; `predictedThrough - appliedThrough` is the catch-up depth).
174
+ - Tune the networked client through the runner: `pacing` (`targetLeadTicks`,
175
+ `maxStepsPerPump`, drift correction) and `inputDelay` (`minTicks`/`maxTicks`,
176
+ RTT-adaptive) are forwarded verbatim, and live `netStats` (RTT, timescale, input
177
+ delay) is surfaced on the snapshot + runner (`null` offline) for a netcode HUD.
178
+
160
179
  Direct `NetworkedSyncplayClient` users get the same projection surface:
161
180
  `getPresentationFrame()`, `renderAlpha`, `currentFrame`, `caughtUp`, and a
162
181
  bounded confirmed-frame queue via the optional `confirmedPresentationBufferSize`
package/dist/runner.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DeterministicEventRecord } from './events.js';
2
- import { type KinetixRuntimeFactory, type NetworkedClientTransport, type NetworkedSyncplaySessionDescriptor, type NetworkedSyncplayClientOptions } from './networked-client.js';
2
+ import { type KinetixRuntimeFactory, type NetworkedClientTransport, type NetworkedSyncplaySessionDescriptor, type NetworkedSyncplayClientInputDelayOptions, type NetworkedSyncplayClientNetStats, type NetworkedSyncplayClientOptions, type NetworkedSyncplayClientPacingOptions } from './networked-client.js';
3
3
  import type { KinetixRuntimeIdentity } from './runtime-session.js';
4
4
  export type SyncplayRunnerStatus = 'offline' | 'connecting' | 'syncing' | 'live' | 'stopped' | 'error';
5
5
  export type SyncplayRunnerConnectionEvent = {
@@ -31,6 +31,24 @@ export interface SyncplayRunnerConfig<State, Input, Checkpoint, Projection, Rend
31
31
  readonly prepareNetworkRuntime?: (descriptor: NetworkedSyncplaySessionDescriptor, signal: AbortSignal) => Promise<void>;
32
32
  readonly maxPredictionTicks?: number;
33
33
  readonly maxOfflineStepsPerUpdate?: number;
34
+ /**
35
+ * Local-slot input provider for incremental adoption. A mature game whose
36
+ * input is a per-tick pulse/edge (consumed and cleared each frame) supplies
37
+ * its own reader here instead of relying on the persistent `applyInput`
38
+ * value. Called once per simulated local frame (offline and networked). When
39
+ * omitted, the runner uses the last `applyInput(...)` value verbatim.
40
+ */
41
+ readonly localInputForTick?: (slot: number, frame: number) => Input;
42
+ /**
43
+ * Wall-clock pacing + clock-drift correction for the networked client (G1).
44
+ * Forwarded verbatim; omit for the client defaults.
45
+ */
46
+ readonly pacing?: NetworkedSyncplayClientPacingOptions;
47
+ /**
48
+ * Local input delay for the networked client. `maxTicks > minTicks` auto-tunes
49
+ * from measured RTT. Forwarded verbatim; omit for the client default (0).
50
+ */
51
+ readonly inputDelay?: NetworkedSyncplayClientInputDelayOptions;
34
52
  }
35
53
  export interface SyncplayRunnerOfflineOptions<Input> {
36
54
  readonly identity: KinetixRuntimeIdentity;
@@ -45,6 +63,18 @@ export interface SyncplayRunnerSnapshot<RenderState> {
45
63
  readonly renderAlpha: number;
46
64
  readonly localSlot: number;
47
65
  readonly rollbackCount: number;
66
+ /**
67
+ * Networked session telemetry, surfaced so a connection UI (e.g. a room
68
+ * badge) can retrofit onto the runner without reaching into the client.
69
+ * Offline: `ready` is true once a session exists and the frame counters
70
+ * track the local session. Before any session/client: `ready` false,
71
+ * `appliedThrough` -1, `predictedThrough` 0.
72
+ */
73
+ readonly ready: boolean;
74
+ readonly appliedThrough: number;
75
+ readonly predictedThrough: number;
76
+ /** Live networked netcode telemetry (RTT, timescale, input delay); null offline. */
77
+ readonly netStats: NetworkedSyncplayClientNetStats | null;
48
78
  readonly error?: Error;
49
79
  }
50
80
  export interface SyncplayRunner<Input, RenderState, EventPayload> {
@@ -52,6 +82,10 @@ export interface SyncplayRunner<Input, RenderState, EventPayload> {
52
82
  readonly renderAlpha: number;
53
83
  readonly localSlot: number;
54
84
  readonly rollbackCount: number;
85
+ readonly ready: boolean;
86
+ readonly appliedThrough: number;
87
+ readonly predictedThrough: number;
88
+ readonly netStats: NetworkedSyncplayClientNetStats | null;
55
89
  startOffline(options: SyncplayRunnerOfflineOptions<Input>): void;
56
90
  startNetworked(connect: () => Promise<SyncplayRunnerTransport>): Promise<void>;
57
91
  applyInput(input: Input): void;
package/dist/runner.js CHANGED
@@ -34,6 +34,21 @@ export function createSyncplayRunner(config) {
34
34
  for (const listener of eventListeners)
35
35
  listener(dispatch.record);
36
36
  });
37
+ function telemetryReady() {
38
+ if (networkClient !== undefined)
39
+ return networkClient.ready;
40
+ return localSession !== undefined;
41
+ }
42
+ function telemetryAppliedThrough() {
43
+ if (networkClient !== undefined)
44
+ return networkClient.appliedThrough;
45
+ return localSession !== undefined ? localSession.currentFrame : -1;
46
+ }
47
+ function telemetryPredictedThrough() {
48
+ if (networkClient !== undefined)
49
+ return networkClient.predictedThrough;
50
+ return localSession !== undefined ? localSession.currentFrame : 0;
51
+ }
37
52
  function snapshot() {
38
53
  return Object.freeze({
39
54
  status,
@@ -41,6 +56,10 @@ export function createSyncplayRunner(config) {
41
56
  renderAlpha,
42
57
  localSlot,
43
58
  rollbackCount: networkClient?.rollbacks ?? 0,
59
+ ready: telemetryReady(),
60
+ appliedThrough: telemetryAppliedThrough(),
61
+ predictedThrough: telemetryPredictedThrough(),
62
+ netStats: networkClient?.netStats ?? null,
44
63
  ...(status === 'error' && error !== undefined ? { error } : {}),
45
64
  });
46
65
  }
@@ -165,11 +184,15 @@ export function createSyncplayRunner(config) {
165
184
  transport: clientTransport,
166
185
  runtimeFactory: config.runtimeFactory,
167
186
  defaultInput: config.defaultInput,
168
- localInputForTick: () => structuredClone(latestInput),
187
+ localInputForTick: (slot, tick) => config.localInputForTick !== undefined
188
+ ? config.localInputForTick(slot, tick)
189
+ : structuredClone(latestInput),
169
190
  encodeInput: config.encodeInput,
170
191
  decodeInput: config.decodeInput,
171
192
  confirmedPresentationBufferSize: 256,
172
193
  ...(config.maxPredictionTicks === undefined ? {} : { maxPredictionTicks: config.maxPredictionTicks }),
194
+ ...(config.pacing === undefined ? {} : { pacing: config.pacing }),
195
+ ...(config.inputDelay === undefined ? {} : { inputDelay: config.inputDelay }),
173
196
  });
174
197
  networkClient = client;
175
198
  await client.whenReady();
@@ -205,7 +228,7 @@ export function createSyncplayRunner(config) {
205
228
  const nextFrame = localSession.currentFrame + 1;
206
229
  for (let slot = 0; slot < offlineOptions.playerCount; slot += 1) {
207
230
  const input = slot === localSlot
208
- ? latestInput
231
+ ? (config.localInputForTick !== undefined ? config.localInputForTick(slot, nextFrame) : latestInput)
209
232
  : offlineOptions.botInputForTick?.(slot, nextFrame) ?? config.defaultInput;
210
233
  localSession.setInput(slot, structuredClone(input));
211
234
  }
@@ -265,6 +288,10 @@ export function createSyncplayRunner(config) {
265
288
  get renderAlpha() { return renderAlpha; },
266
289
  get localSlot() { return localSlot; },
267
290
  get rollbackCount() { return networkClient?.rollbacks ?? 0; },
291
+ get ready() { return telemetryReady(); },
292
+ get appliedThrough() { return telemetryAppliedThrough(); },
293
+ get predictedThrough() { return telemetryPredictedThrough(); },
294
+ get netStats() { return networkClient?.netStats ?? null; },
268
295
  startOffline,
269
296
  startNetworked,
270
297
  applyInput(input) { latestInput = structuredClone(input); },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-inc/rundot-syncplay",
3
- "version": "5.25.0-beta.4",
3
+ "version": "5.25.0-beta.6",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",