@series-inc/rundot-syncplay 5.25.0-beta.4 → 5.25.0-beta.5

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,21 @@ 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
+
160
175
  Direct `NetworkedSyncplayClient` users get the same projection surface:
161
176
  `getPresentationFrame()`, `renderAlpha`, `currentFrame`, `caughtUp`, and a
162
177
  bounded confirmed-frame queue via the optional `confirmedPresentationBufferSize`
package/dist/runner.d.ts CHANGED
@@ -31,6 +31,14 @@ 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;
34
42
  }
35
43
  export interface SyncplayRunnerOfflineOptions<Input> {
36
44
  readonly identity: KinetixRuntimeIdentity;
@@ -45,6 +53,16 @@ export interface SyncplayRunnerSnapshot<RenderState> {
45
53
  readonly renderAlpha: number;
46
54
  readonly localSlot: number;
47
55
  readonly rollbackCount: number;
56
+ /**
57
+ * Networked session telemetry, surfaced so a connection UI (e.g. a room
58
+ * badge) can retrofit onto the runner without reaching into the client.
59
+ * Offline: `ready` is true once a session exists and the frame counters
60
+ * track the local session. Before any session/client: `ready` false,
61
+ * `appliedThrough` -1, `predictedThrough` 0.
62
+ */
63
+ readonly ready: boolean;
64
+ readonly appliedThrough: number;
65
+ readonly predictedThrough: number;
48
66
  readonly error?: Error;
49
67
  }
50
68
  export interface SyncplayRunner<Input, RenderState, EventPayload> {
@@ -52,6 +70,9 @@ export interface SyncplayRunner<Input, RenderState, EventPayload> {
52
70
  readonly renderAlpha: number;
53
71
  readonly localSlot: number;
54
72
  readonly rollbackCount: number;
73
+ readonly ready: boolean;
74
+ readonly appliedThrough: number;
75
+ readonly predictedThrough: number;
55
76
  startOffline(options: SyncplayRunnerOfflineOptions<Input>): void;
56
77
  startNetworked(connect: () => Promise<SyncplayRunnerTransport>): Promise<void>;
57
78
  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,9 @@ 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(),
44
62
  ...(status === 'error' && error !== undefined ? { error } : {}),
45
63
  });
46
64
  }
@@ -165,7 +183,9 @@ export function createSyncplayRunner(config) {
165
183
  transport: clientTransport,
166
184
  runtimeFactory: config.runtimeFactory,
167
185
  defaultInput: config.defaultInput,
168
- localInputForTick: () => structuredClone(latestInput),
186
+ localInputForTick: (slot, tick) => config.localInputForTick !== undefined
187
+ ? config.localInputForTick(slot, tick)
188
+ : structuredClone(latestInput),
169
189
  encodeInput: config.encodeInput,
170
190
  decodeInput: config.decodeInput,
171
191
  confirmedPresentationBufferSize: 256,
@@ -205,7 +225,7 @@ export function createSyncplayRunner(config) {
205
225
  const nextFrame = localSession.currentFrame + 1;
206
226
  for (let slot = 0; slot < offlineOptions.playerCount; slot += 1) {
207
227
  const input = slot === localSlot
208
- ? latestInput
228
+ ? (config.localInputForTick !== undefined ? config.localInputForTick(slot, nextFrame) : latestInput)
209
229
  : offlineOptions.botInputForTick?.(slot, nextFrame) ?? config.defaultInput;
210
230
  localSession.setInput(slot, structuredClone(input));
211
231
  }
@@ -265,6 +285,9 @@ export function createSyncplayRunner(config) {
265
285
  get renderAlpha() { return renderAlpha; },
266
286
  get localSlot() { return localSlot; },
267
287
  get rollbackCount() { return networkClient?.rollbacks ?? 0; },
288
+ get ready() { return telemetryReady(); },
289
+ get appliedThrough() { return telemetryAppliedThrough(); },
290
+ get predictedThrough() { return telemetryPredictedThrough(); },
268
291
  startOffline,
269
292
  startNetworked,
270
293
  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.5",
4
4
  "description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
5
5
  "repository": {
6
6
  "type": "git",