@series-inc/rundot-syncplay 5.25.0-beta.1 → 5.25.0-beta.2
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 +27 -0
- package/dist/runtime-session.d.ts +24 -4
- package/dist/runtime-session.js +42 -1
- package/dist/sdk-session.d.ts +4 -5
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -52,6 +52,33 @@ session-config bytes, asks the game for a matching runtime through its runtime
|
|
|
52
52
|
factory, and rejects a locally loaded pack mismatch before frame 0. Replays use
|
|
53
53
|
the same identity/config/factory path.
|
|
54
54
|
|
|
55
|
+
## Presentation frames
|
|
56
|
+
|
|
57
|
+
`session.getState()` returns a detached deep copy of the current frame, which
|
|
58
|
+
costs O(state) per read and is unaffordable for a large mutable world (a
|
|
59
|
+
destructible voxel city, a big streaming map).
|
|
60
|
+
|
|
61
|
+
`session.getPresentationFrame()` is the no-clone path. It calls the runtime's
|
|
62
|
+
optional `project(checkpoint, previous)` and returns the result untouched:
|
|
63
|
+
|
|
64
|
+
- **Requires** a runtime built with a `projectState` hook. A runtime without
|
|
65
|
+
`project` throws `SYNCPLAY_PROJECTION_UNSUPPORTED` — there is no silent
|
|
66
|
+
fallback to `inspect`.
|
|
67
|
+
- **The result is immutable.** Callers must not mutate it. It may share structure
|
|
68
|
+
with authoritative state and with previously returned projections, which is
|
|
69
|
+
exactly what lets a renderer diff persistent roots and skip shared subtrees.
|
|
70
|
+
- **`previous` is the last frame this session projected**, enabling delta
|
|
71
|
+
projections. It is absent — meaning the runtime should produce a full frame —
|
|
72
|
+
on the first call, after `restoreToFrame` or `restoreSnapshot`, and whenever
|
|
73
|
+
the previously projected frame is no longer retained (pruned or rolled past).
|
|
74
|
+
- **Reading the same frame twice is free and idempotent.** Repeat calls return
|
|
75
|
+
the identical projection rather than re-projecting the frame against itself
|
|
76
|
+
(which would report an empty delta). Authoritative state cannot change without
|
|
77
|
+
the frame advancing, so multiple render passes per frame are safe.
|
|
78
|
+
|
|
79
|
+
Projections are presentation-only: they never re-enter simulation and are not
|
|
80
|
+
part of deterministic identity.
|
|
81
|
+
|
|
55
82
|
## Public projections and secrets
|
|
56
83
|
|
|
57
84
|
Every peer applies the same confirmed inputs and authority commands. Optional
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { type KinetixRuntime, type KinetixRuntimeIdentity } from '@series-inc/rundot-kinetix/runtime';
|
|
2
2
|
import type { RecordedChecksum, RecordedCommandFrame, RecordedInputFlags, RecordedInputFrame, ReplayFile } from './types.js';
|
|
3
3
|
export type { KinetixRuntime, KinetixRuntimeIdentity } from '@series-inc/rundot-kinetix/runtime';
|
|
4
|
+
/**
|
|
5
|
+
* A Kinetix runtime that may expose the optional no-clone presentation
|
|
6
|
+
* projection, re-typed for this session's Projection.
|
|
7
|
+
*
|
|
8
|
+
* `project` is omitted and re-declared rather than inherited so this compiles
|
|
9
|
+
* against both Kinetix builds we resolve against: the published ABI v1 runtime
|
|
10
|
+
* (no `project`) that the lockfile pins and publish lanes keep, and the
|
|
11
|
+
* in-repo runtime (whose `project` defaults to `Projection = State`) that test
|
|
12
|
+
* lanes overlay. Presence is feature-detected at runtime either way.
|
|
13
|
+
*/
|
|
14
|
+
export type ProjectionCapableRuntime<State, Input, Checkpoint, Projection> = Omit<KinetixRuntime<State, Input, Checkpoint>, 'project'> & {
|
|
15
|
+
project?(checkpoint: Checkpoint, previous?: Checkpoint): Readonly<Projection>;
|
|
16
|
+
};
|
|
4
17
|
export interface RuntimeExternalSnapshot<Input> {
|
|
5
18
|
readonly frame: number;
|
|
6
19
|
readonly runtimeBytes: Uint8Array;
|
|
@@ -16,8 +29,8 @@ export interface RuntimeFrameSnapshot<State, Input, Checkpoint> {
|
|
|
16
29
|
readonly checkpoint: Checkpoint;
|
|
17
30
|
readonly state: State;
|
|
18
31
|
}
|
|
19
|
-
export interface RuntimeSessionConfig<State, Input, Checkpoint> {
|
|
20
|
-
readonly runtime:
|
|
32
|
+
export interface RuntimeSessionConfig<State, Input, Checkpoint, Projection = State> {
|
|
33
|
+
readonly runtime: ProjectionCapableRuntime<State, Input, Checkpoint, Projection>;
|
|
21
34
|
readonly playerCount: number;
|
|
22
35
|
readonly defaultInput: Input;
|
|
23
36
|
readonly snapshotBufferSize?: number;
|
|
@@ -30,7 +43,7 @@ export interface RuntimeSessionConfig<State, Input, Checkpoint> {
|
|
|
30
43
|
readonly onCommandFrameRecorded?: (frame: RecordedCommandFrame) => void;
|
|
31
44
|
readonly onChecksumRecorded?: (checksum: RecordedChecksum) => void;
|
|
32
45
|
}
|
|
33
|
-
export interface RuntimeSession<State, Input, Checkpoint = unknown> {
|
|
46
|
+
export interface RuntimeSession<State, Input, Checkpoint = unknown, Projection = State> {
|
|
34
47
|
readonly currentFrame: number;
|
|
35
48
|
readonly accumulatedMs: number;
|
|
36
49
|
readonly schemaIdentity: KinetixRuntimeIdentity;
|
|
@@ -40,6 +53,13 @@ export interface RuntimeSession<State, Input, Checkpoint = unknown> {
|
|
|
40
53
|
update(deltaMs: number): number;
|
|
41
54
|
stepFrames(count: number): void;
|
|
42
55
|
getState(): State;
|
|
56
|
+
/**
|
|
57
|
+
* Projects the current frame for presentation without cloning. Requires a
|
|
58
|
+
* runtime that implements `project`; throws SYNCPLAY_PROJECTION_UNSUPPORTED
|
|
59
|
+
* otherwise. The result is immutable and may share structure with previous
|
|
60
|
+
* frames — callers must not mutate it.
|
|
61
|
+
*/
|
|
62
|
+
getPresentationFrame(): Readonly<Projection>;
|
|
43
63
|
exportReplay(): ReplayFile;
|
|
44
64
|
peekSnapshot(frame: number): RuntimeFrameSnapshot<State, Input, Checkpoint> | undefined;
|
|
45
65
|
getSnapshot(frame: number): RuntimeFrameSnapshot<State, Input, Checkpoint> | undefined;
|
|
@@ -53,4 +73,4 @@ export interface RuntimeSession<State, Input, Checkpoint = unknown> {
|
|
|
53
73
|
getSnapshotFrames(): readonly number[];
|
|
54
74
|
dispose(): void;
|
|
55
75
|
}
|
|
56
|
-
export declare function createRuntimeSession<State, Input, Checkpoint>(config: RuntimeSessionConfig<State, Input, Checkpoint>): RuntimeSession<State, Input, Checkpoint>;
|
|
76
|
+
export declare function createRuntimeSession<State, Input, Checkpoint, Projection = State>(config: RuntimeSessionConfig<State, Input, Checkpoint, Projection>): RuntimeSession<State, Input, Checkpoint, Projection>;
|
package/dist/runtime-session.js
CHANGED
|
@@ -38,6 +38,7 @@ export function createRuntimeSession(config) {
|
|
|
38
38
|
let accumulatedMs = 0;
|
|
39
39
|
let disposed = false;
|
|
40
40
|
let hydrated = false;
|
|
41
|
+
let lastProjected;
|
|
41
42
|
const frame0 = config.runtime.capture();
|
|
42
43
|
retained.set(config.runtime.currentFrame, {
|
|
43
44
|
checkpoint: frame0,
|
|
@@ -116,7 +117,19 @@ export function createRuntimeSession(config) {
|
|
|
116
117
|
checksum: frame % checksumInterval === 0,
|
|
117
118
|
});
|
|
118
119
|
}
|
|
119
|
-
|
|
120
|
+
let results;
|
|
121
|
+
try {
|
|
122
|
+
results = config.runtime.advance(requests);
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
// advance() is atomic: the batch stepped no frames, so its commands must
|
|
126
|
+
// not be replayed on the next attempt. A command the runtime refuses to
|
|
127
|
+
// decode would otherwise be rebuilt into every subsequent request and
|
|
128
|
+
// wedge the session permanently.
|
|
129
|
+
for (const request of requests)
|
|
130
|
+
pendingCommands.delete(request.frame);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
120
133
|
if (results.length !== requests.length)
|
|
121
134
|
throw new Error('SYNCPLAY_RUNTIME_RESULT_COUNT_MISMATCH');
|
|
122
135
|
for (let index = 0; index < results.length; index += 1) {
|
|
@@ -217,6 +230,29 @@ export function createRuntimeSession(config) {
|
|
|
217
230
|
active();
|
|
218
231
|
return structuredClone(config.runtime.inspect(requireRetained(config.runtime.currentFrame).checkpoint));
|
|
219
232
|
},
|
|
233
|
+
getPresentationFrame() {
|
|
234
|
+
active();
|
|
235
|
+
const project = config.runtime.project;
|
|
236
|
+
if (typeof project !== 'function')
|
|
237
|
+
throw new Error('SYNCPLAY_PROJECTION_UNSUPPORTED');
|
|
238
|
+
const frame = config.runtime.currentFrame;
|
|
239
|
+
const entry = requireRetained(frame);
|
|
240
|
+
// Re-reading a frame must not diff it against itself — a delta hook given
|
|
241
|
+
// the current frame as its own baseline would report nothing changed.
|
|
242
|
+
// State cannot move without the frame advancing, so the projection this
|
|
243
|
+
// frame already produced is still the answer.
|
|
244
|
+
if (lastProjected?.frame === frame)
|
|
245
|
+
return lastProjected.projection;
|
|
246
|
+
// A previously projected frame may have been pruned or rolled past; the
|
|
247
|
+
// runtime then gets no baseline and produces a full frame. Deliberate
|
|
248
|
+
// contract, not a fallback.
|
|
249
|
+
const previous = lastProjected === undefined
|
|
250
|
+
? undefined
|
|
251
|
+
: retained.get(lastProjected.frame)?.checkpoint;
|
|
252
|
+
const projection = project.call(config.runtime, entry.checkpoint, previous);
|
|
253
|
+
lastProjected = { frame, projection };
|
|
254
|
+
return projection;
|
|
255
|
+
},
|
|
220
256
|
exportReplay() {
|
|
221
257
|
active();
|
|
222
258
|
if (hydrated)
|
|
@@ -279,6 +315,7 @@ export function createRuntimeSession(config) {
|
|
|
279
315
|
commandFrames.splice(0, commandFrames.length, ...commandFrames.filter((record) => record.frame <= frame));
|
|
280
316
|
checksums.splice(0, checksums.length, ...checksums.filter((record) => record.frame <= frame));
|
|
281
317
|
accumulatedMs = 0;
|
|
318
|
+
lastProjected = undefined;
|
|
282
319
|
},
|
|
283
320
|
restoreSnapshot(snapshot) {
|
|
284
321
|
active();
|
|
@@ -316,6 +353,7 @@ export function createRuntimeSession(config) {
|
|
|
316
353
|
commandFrames.length = 0;
|
|
317
354
|
checksums.length = 0;
|
|
318
355
|
accumulatedMs = 0;
|
|
356
|
+
lastProjected = undefined;
|
|
319
357
|
hydrated = true;
|
|
320
358
|
},
|
|
321
359
|
getSnapshotFrames() { active(); return [...retained.keys()]; },
|
|
@@ -325,6 +363,9 @@ export function createRuntimeSession(config) {
|
|
|
325
363
|
for (const entry of retained.values())
|
|
326
364
|
release(entry.checkpoint);
|
|
327
365
|
retained.clear();
|
|
366
|
+
// A projection may share structure with the whole world; releasing the
|
|
367
|
+
// checkpoints while still pinning it would defeat the release.
|
|
368
|
+
lastProjected = undefined;
|
|
328
369
|
disposed = true;
|
|
329
370
|
},
|
|
330
371
|
};
|
package/dist/sdk-session.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type
|
|
2
|
-
|
|
3
|
-
export type
|
|
4
|
-
export
|
|
5
|
-
export declare function createSyncplaySession<State, Input, Checkpoint = unknown>(runtime: KinetixRuntime<State, Input, Checkpoint>, policy: SyncplaySessionPolicy<State, Input, Checkpoint>): SyncplaySession<State, Input, Checkpoint>;
|
|
1
|
+
import { type ProjectionCapableRuntime, type RuntimeSession, type RuntimeSessionConfig } from './runtime-session.js';
|
|
2
|
+
export type SyncplaySessionPolicy<State, Input, Checkpoint = unknown, Projection = State> = Omit<RuntimeSessionConfig<State, Input, Checkpoint, Projection>, 'runtime'>;
|
|
3
|
+
export type SyncplaySession<State, Input, Checkpoint = unknown, Projection = State> = RuntimeSession<State, Input, Checkpoint, Projection>;
|
|
4
|
+
export declare function createSyncplaySession<State, Input, Checkpoint = unknown, Projection = State>(runtime: ProjectionCapableRuntime<State, Input, Checkpoint, Projection>, policy: SyncplaySessionPolicy<State, Input, Checkpoint, Projection>): SyncplaySession<State, Input, Checkpoint, Projection>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@series-inc/rundot-syncplay",
|
|
3
|
-
"version": "5.25.0-beta.
|
|
3
|
+
"version": "5.25.0-beta.2",
|
|
4
4
|
"description": "Kinetix orchestration, rollback, replay, late-join, and deterministic networking for RUN.game",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -49,8 +49,9 @@
|
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.json",
|
|
51
51
|
"test": "npm run test:core",
|
|
52
|
-
"test:core": "tsx --test tests/canonical.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts",
|
|
52
|
+
"test:core": "tsx --test tests/canonical.test.ts tests/runtime-session.test.ts tests/kinetix-runtime-session.test.ts tests/runtime-support.test.ts tests/runtime-guards.test.ts tests/r3f-render-adapter.test.ts tests/snapshot-cost.test.ts tests/sdkSession.test.ts tests/input-authority.test.ts tests/session-wire.test.ts tests/session-snapshot.test.ts tests/authority-room.test.ts tests/networked-client-snapshot.test.ts tests/secret-config.test.ts tests/secret-protocol.test.ts tests/secret-authority.test.ts tests/secret-client.test.ts tests/late-join-checksum.test.ts tests/replay-scoring.test.ts tests/bot-backfill.test.ts tests/latency-simulation.test.ts tests/time-sync.test.ts tests/match-log.test.ts tests/spectator.test.ts tests/synctest.test.ts tests/instant-replay.test.ts tests/session-promotion.test.ts tests/physics3d-broadphase-stats.test.ts tests/entrypoints.test.ts tests/export-surface.test.ts tests/test-manifest.test.ts",
|
|
53
53
|
"test:soak": "tsx --test tests/runtime-soak.test.ts",
|
|
54
|
+
"test:extended": "tsx --test tests/animation.test.ts tests/asset-cooking.test.ts tests/backend-proof-checksum-vectors.test.ts tests/bot-documents.test.ts tests/bot-primitives.test.ts tests/collider-cooking.test.ts tests/collision.test.ts tests/command-timeline.test.ts tests/config-bundle.test.ts tests/cosmetic-physics3d.test.ts tests/deterministic-primitives.test.ts tests/ecs-lite.test.ts tests/effects-adapter.test.ts tests/engine-runtime.test.ts tests/errors.test.ts tests/events.test.ts tests/input-button.test.ts tests/load-128-players.test.ts tests/math.test.ts tests/movement.test.ts tests/movement3d.test.ts tests/multiclient-authority.test.ts tests/navigation.test.ts tests/noise.test.ts tests/physics-cert.test.ts tests/physics2d.test.ts tests/physics3d-ccd.test.ts tests/physics3d-coverage.test.ts tests/physics3d-destruction-cert.test.ts tests/physics3d-heightfield.test.ts tests/physics3d-hull.test.ts tests/physics3d-joints.test.ts tests/physics3d-kinematic-mesh.test.ts tests/physics3d-mass.test.ts tests/physics3d-materials.test.ts tests/physics3d-mesh-onesided.test.ts tests/physics3d-query-index.test.ts tests/physics3d-recipes.test.ts tests/physics3d-rotation-convention.test.ts tests/physics3d-shared.test.ts tests/physics3d-sleeping-manifold-reuse.test.ts tests/physics3d-solver.test.ts tests/physics3d-stacking.test.ts tests/physics3d-toi.test.ts tests/physics3d-vehicle.test.ts tests/physics3d-voxel.test.ts tests/physics3d-world-edit.test.ts tests/physics3d.test.ts tests/random.test.ts tests/rollback-history.test.ts tests/schema-artifacts.test.ts tests/schema-codegen.test.ts tests/schema-dsl-binary.test.ts tests/schema.test.ts tests/signals.test.ts tests/sparse-set.test.ts tests/static-checker.test.ts tests/system-lifecycle.test.ts tests/wire-canonical-sentinels.test.ts tests/ws-frame.test.ts",
|
|
54
55
|
"test:ci": "npm run build && npm run test:core",
|
|
55
56
|
"pack:dry-run": "npm pack --dry-run"
|
|
56
57
|
},
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
"Kinetix"
|
|
65
66
|
],
|
|
66
67
|
"dependencies": {
|
|
67
|
-
"@series-inc/rundot-kinetix": "0.1.0-beta.
|
|
68
|
+
"@series-inc/rundot-kinetix": "0.1.0-beta.6"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
71
|
"@dimforge/rapier2d-compat": "0.19.3",
|