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