@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 +19 -0
- package/dist/runner.d.ts +35 -1
- package/dist/runner.js +29 -2
- package/package.json +1 -1
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: () =>
|
|
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