@series-inc/rundot-syncplay 5.25.0-beta.6 → 5.25.0-beta.8
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 +70 -6
- package/dist/authority-room.d.ts +38 -0
- package/dist/authority-room.js +183 -17
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +2 -0
- package/dist/dev-authority-room.js +12 -0
- package/dist/effects-adapter.d.ts +6 -0
- package/dist/effects-adapter.js +35 -5
- package/dist/index.d.ts +7 -3
- package/dist/index.js +2 -0
- package/dist/input-codec.d.ts +32 -0
- package/dist/input-codec.js +191 -0
- package/dist/networked-client.d.ts +45 -1
- package/dist/networked-client.js +163 -10
- package/dist/platformer-content.d.ts +49 -0
- package/dist/platformer-content.js +100 -0
- package/dist/runner-harness.d.ts +77 -0
- package/dist/runner-harness.js +226 -0
- package/dist/runner.d.ts +32 -2
- package/dist/runner.js +106 -9
- package/dist/session-wire.d.ts +41 -1
- package/dist/session-wire.js +58 -2
- package/dist/testing-network.d.ts +13 -1
- package/dist/testing-network.js +64 -15
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +1 -0
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -5,6 +5,11 @@ deterministic state, RNG streams, stepping, opaque checkpoints, serialization,
|
|
|
5
5
|
hydration, and checksums. Syncplay owns inputs, prediction, confirmation,
|
|
6
6
|
rollback policy, replay logs, late join, secrets, and transport.
|
|
7
7
|
|
|
8
|
+
> **Upgrading an existing game?** See
|
|
9
|
+
> [MIGRATING-SESSION-PRIMITIVES.md](./MIGRATING-SESSION-PRIMITIVES.md). Two
|
|
10
|
+
> changes are not additive: the runner now rejects an unsafe input codec at
|
|
11
|
+
> construction, and the effects adapter now dedupes on your event `dedupeKey`.
|
|
12
|
+
|
|
8
13
|
## Ownership boundary
|
|
9
14
|
|
|
10
15
|
| Concern | Owner |
|
|
@@ -184,8 +189,60 @@ bounded confirmed-frame queue via the optional `confirmedPresentationBufferSize`
|
|
|
184
189
|
transport's reconnect signal so replayed history never fills the queue.
|
|
185
190
|
`prepareNetworkedSyncplayTransport(transport, prepare, { signal })` buffers the
|
|
186
191
|
ordered `session-start`/history traffic (same 8,192-message ceiling as the SDK
|
|
187
|
-
transport), exposes a frozen
|
|
188
|
-
|
|
192
|
+
transport), exposes a frozen `NetworkedSyncplaySessionDescriptor`, awaits your
|
|
193
|
+
async preparation, then replays every buffered message once.
|
|
194
|
+
|
|
195
|
+
The descriptor is exactly `{ slot, playerCount, seed, tickRateHz,
|
|
196
|
+
hardToleranceTicks, role, runtimeIdentity, sessionConfigBytes,
|
|
197
|
+
sessionConfigDigest }`. It does **not** carry `levelEntryId`, a level digest, or a
|
|
198
|
+
level length — those are game-specific fields inside `sessionConfigBytes`, and you
|
|
199
|
+
decode them with your own session-config schema:
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { decodeKinetixSessionConfig } from '@series-inc/rundot-kinetix/runtime';
|
|
203
|
+
import { kinetixPlatformer2dSessionConfigSchema } from '@series-inc/rundot-kinetix/runtime';
|
|
204
|
+
|
|
205
|
+
const prepareNetworkRuntime = async (descriptor, signal) => {
|
|
206
|
+
const session = decodeKinetixSessionConfig(
|
|
207
|
+
kinetixPlatformer2dSessionConfigSchema,
|
|
208
|
+
descriptor.sessionConfigBytes,
|
|
209
|
+
);
|
|
210
|
+
// session.levelEntryId / session.levelDigest / session.levelByteLength
|
|
211
|
+
const levelBytes = await preparePlatformerLevel(api.ugc, {
|
|
212
|
+
levelEntryId: session.levelEntryId,
|
|
213
|
+
levelDigest: session.levelDigest,
|
|
214
|
+
levelByteLength: session.levelByteLength,
|
|
215
|
+
encodeLevel: (level) => encodeKinetixPlatformer2dLevel(level, { maxBytes: 102_400 }),
|
|
216
|
+
});
|
|
217
|
+
registerPreparedLevel(descriptor.sessionConfigDigest, levelBytes);
|
|
218
|
+
};
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## Safe input codecs
|
|
222
|
+
|
|
223
|
+
`defineSyncplayInputCodec(schema)` derives `{ encodeInput, decodeInput,
|
|
224
|
+
neutralInput }` from an exact field declaration (`boolean`, `int` with
|
|
225
|
+
`min`/`max`, `enum` of numbers). Decoding a non-conforming payload throws
|
|
226
|
+
`SYNCPLAY_INPUT_DECODE_INVALID`, and the canonical `null` substitute maps to
|
|
227
|
+
`neutralInput` — i.e. correct `decodeInputSafe` semantics by construction, rather
|
|
228
|
+
than by remembering to write them. Neutral is `false` for booleans and `0` for
|
|
229
|
+
numeric fields when their range admits it; otherwise the field must declare
|
|
230
|
+
`neutral` or construction throws `SYNCPLAY_INPUT_SCHEMA_INVALID`.
|
|
231
|
+
|
|
232
|
+
For an existing hand-written codec, `validateSyncplayInputCodec({ encodeInput,
|
|
233
|
+
decodeInput, samples })` probes round-trip stability, `null → neutral`, and a
|
|
234
|
+
battery of hostile payloads, failing with `SYNCPLAY_INPUT_CODEC_UNSAFE`.
|
|
235
|
+
|
|
236
|
+
## Version-safe content references
|
|
237
|
+
|
|
238
|
+
`publishPlatformerLevel(api, level, options)` always calls `ugc.create` — never
|
|
239
|
+
`ugc.update` on a referenced entry, which would make every live room pinned to it
|
|
240
|
+
permanently unjoinable. `preparePlatformerLevel(api, declaration)` resolves a pin
|
|
241
|
+
and raises the typed `SYNCPLAY_LEVEL_CONTENT_CHANGED` (naming the entry id and the
|
|
242
|
+
edit-vs-pin cause) rather than an opaque `KINETIX_AUX_ASSET_MISMATCH` deep inside
|
|
243
|
+
runtime construction. `declaration.encodeLevel` is an **injected callback**, not a
|
|
244
|
+
static Kinetix import, because Syncplay must compile against both the published
|
|
245
|
+
Kinetix pin and an overlaid local build.
|
|
189
246
|
|
|
190
247
|
**Capacity and late-join budgets.** Snapshot transfer defaults are **256 KiB**
|
|
191
248
|
chunks and a **4 MiB** total ceiling; the immutable level payload is fetched and
|
|
@@ -195,15 +252,20 @@ protocol/load ceiling, not a 60 Hz performance guarantee for an arbitrary game.
|
|
|
195
252
|
|
|
196
253
|
## Supported correctness kit
|
|
197
254
|
|
|
198
|
-
`@series-inc/rundot-syncplay/testing` exposes exactly
|
|
199
|
-
no browser or runtime APIs leak through it:
|
|
255
|
+
`@series-inc/rundot-syncplay/testing` exposes exactly nine supported
|
|
256
|
+
entry points — no browser or runtime APIs leak through it:
|
|
200
257
|
|
|
201
258
|
```ts
|
|
202
259
|
import {
|
|
203
260
|
assertDeterministic,
|
|
204
261
|
assertLateJoinHydration,
|
|
262
|
+
assertPresentationParity,
|
|
205
263
|
assertReplayVerifies,
|
|
264
|
+
assertRunnerLifecycle,
|
|
206
265
|
assertTwoClientConvergence,
|
|
266
|
+
createSimulatedSyncplayMatch,
|
|
267
|
+
createSyncplayRunnerHarness,
|
|
268
|
+
runSyncplaySynctest,
|
|
207
269
|
} from '@series-inc/rundot-syncplay/testing'
|
|
208
270
|
```
|
|
209
271
|
|
|
@@ -211,5 +273,7 @@ import {
|
|
|
211
273
|
restore-resim / hydration / replay cert; `assertTwoClientConvergence` and
|
|
212
274
|
`assertLateJoinHydration` run the **real** authority room and networked client
|
|
213
275
|
over a deterministic simulated network (`createSimulatedSyncplayMatch`,
|
|
214
|
-
`runSyncplaySynctest`).
|
|
215
|
-
|
|
276
|
+
`runSyncplaySynctest`). `createSyncplayRunnerHarness`, `assertRunnerLifecycle`,
|
|
277
|
+
and `assertPresentationParity` cover the runner seam — a real runner driven on
|
|
278
|
+
a fully controlled clock against the simulated match. Failures name the phase,
|
|
279
|
+
frame, and disagreeing checksums.
|
package/dist/authority-room.d.ts
CHANGED
|
@@ -106,6 +106,23 @@ export interface DeterministicAuthorityRoomConfig {
|
|
|
106
106
|
readonly secrets?: SyncplaySecretSystemsConfig;
|
|
107
107
|
readonly secretCrypto?: SyncplaySecretAuthorityCrypto;
|
|
108
108
|
readonly secretMsgType?: string;
|
|
109
|
+
/** Per-slot ceiling on accepted `command-request` messages in a one-second window. Default 60. */
|
|
110
|
+
readonly commandsPerSecond?: number;
|
|
111
|
+
/**
|
|
112
|
+
* Who may drive {@link DeterministicAuthorityRoom.reconfigureSession} over the
|
|
113
|
+
* wire. Default 'none' — a `reconfigure-request` is dropped and counted, and
|
|
114
|
+
* only the host may reconfigure in-process.
|
|
115
|
+
*/
|
|
116
|
+
readonly reconfigurePolicy?: 'creator' | 'any' | 'none';
|
|
117
|
+
/** The creator's seat, required to enforce `reconfigurePolicy: 'creator'`. */
|
|
118
|
+
readonly creatorSlot?: number;
|
|
119
|
+
}
|
|
120
|
+
export interface DeterministicAuthorityRoomReconfiguration {
|
|
121
|
+
readonly sessionConfigBytes: Uint8Array;
|
|
122
|
+
readonly sessionConfigDigest: string;
|
|
123
|
+
readonly seed?: number;
|
|
124
|
+
/** When supplied, must equal the live identity; a mismatch throws. */
|
|
125
|
+
readonly runtimeIdentity?: KinetixRuntimeIdentity;
|
|
109
126
|
}
|
|
110
127
|
export interface DeterministicAuthorityRoomRestoreState {
|
|
111
128
|
readonly now: number;
|
|
@@ -119,6 +136,8 @@ export interface DeterministicAuthorityRoomRestoreState {
|
|
|
119
136
|
readonly sessionConfigBytes: Uint8Array;
|
|
120
137
|
readonly sessionConfigDigest: string;
|
|
121
138
|
readonly secretState?: SyncplaySecretAuthoritySnapshot;
|
|
139
|
+
/** Session generation at persist time; a restored room re-greets with it. Default 0. */
|
|
140
|
+
readonly generation?: number;
|
|
122
141
|
}
|
|
123
142
|
export interface AuthorityRoomCatchUpEvent {
|
|
124
143
|
readonly playerId: string;
|
|
@@ -161,6 +180,12 @@ export interface AuthorityRoomNetcodeStats {
|
|
|
161
180
|
/** Ticks in which each slot's input had to be substituted (cumulative). */
|
|
162
181
|
readonly substitutedTicksBySlot: Readonly<Record<number, number>>;
|
|
163
182
|
readonly clientStats: Readonly<Record<number, AuthorityRoomClientNetStats>>;
|
|
183
|
+
/** Command requests dropped for seat, size, rate, sequence, or stale generation. */
|
|
184
|
+
readonly rejectedCommands: number;
|
|
185
|
+
/** Reconfigure requests dropped for policy or size. */
|
|
186
|
+
readonly rejectedReconfigures: number;
|
|
187
|
+
/** Ephemeral relays dropped for seat, size, or rate. */
|
|
188
|
+
readonly rejectedEphemeral: number;
|
|
164
189
|
}
|
|
165
190
|
export interface AuthorityRoomJoinResult {
|
|
166
191
|
readonly accepted: boolean;
|
|
@@ -178,8 +203,11 @@ export interface AuthorityRoomSnapshot {
|
|
|
178
203
|
/** Lowest confirmed tick still retained (-1 when nothing is confirmed). */
|
|
179
204
|
readonly earliestRetainedTick: number;
|
|
180
205
|
readonly runtimeIdentity: KinetixRuntimeIdentity;
|
|
206
|
+
/** The CURRENT session config (post-reconfigure), not the creation-time one. */
|
|
181
207
|
readonly sessionConfigBytes: Uint8Array;
|
|
182
208
|
readonly sessionConfigDigest: string;
|
|
209
|
+
/** Session generation: 0 until the first reconfigure. */
|
|
210
|
+
readonly generation: number;
|
|
183
211
|
/**
|
|
184
212
|
* Metadata of the latest VALIDATED state snapshot. The raw bytes are
|
|
185
213
|
* deliberately excluded — hosts persist this record (mp-room-server hard-caps
|
|
@@ -213,6 +241,16 @@ export interface DeterministicAuthorityRoom {
|
|
|
213
241
|
onPlayerReconnected(playerId: string): void;
|
|
214
242
|
/** Advance the authority by one server tick and broadcast newly-confirmed frames. */
|
|
215
243
|
onTick(): void;
|
|
244
|
+
/**
|
|
245
|
+
* Swap the session config in a live room: bumps the generation, resets the
|
|
246
|
+
* authority's own session state, and re-greets every seated player and
|
|
247
|
+
* spectator with a fresh `session-start`. Nobody disconnects — clients treat
|
|
248
|
+
* a differing generation as a full session swap. Throws
|
|
249
|
+
* SYNCPLAY_RECONFIGURE_IDENTITY_MISMATCH when the supplied runtime identity
|
|
250
|
+
* differs from the live one, and SYNCPLAY_RECONFIGURE_CONFIG_INVALID on bad
|
|
251
|
+
* or oversized bytes.
|
|
252
|
+
*/
|
|
253
|
+
reconfigureSession(next: DeterministicAuthorityRoomReconfiguration): void;
|
|
216
254
|
snapshot(): AuthorityRoomSnapshot;
|
|
217
255
|
/** Raw bytes of the latest validated state snapshot (see AuthorityRoomSnapshot.latestSnapshot). */
|
|
218
256
|
latestSnapshotBytes(): string | undefined;
|
package/dist/authority-room.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { defaultChecksum } from './canonical.js';
|
|
1
|
+
import { canonicalStringify, defaultChecksum, encodeUtf8 } from './canonical.js';
|
|
2
2
|
import { assertKinetixRuntimeIdentity, kinetixSessionConfigDigest, } from '@series-inc/rundot-kinetix/runtime';
|
|
3
3
|
import { invariant } from './errors.js';
|
|
4
4
|
import { createInputAuthority } from './input-authority.js';
|
|
5
5
|
import { createDeterministicLocalAuthoritySnapshotChunkEnvelope, createDeterministicLocalAuthoritySnapshotCompleteEnvelope, createDeterministicLocalAuthoritySnapshotOfferEnvelope, createDeterministicLocalAuthoritySnapshotRequestEnvelope, decodeDeterministicLocalAuthorityWireEnvelope, } from './local-authority-wire.js';
|
|
6
6
|
import { DEFAULT_SNAPSHOT_CHUNK_SIZE, createSnapshotTransferCollector, createSnapshotTransferFromBytes, decodeDeterministicStateSnapshot, deterministicRoomDescriptorHash, } from './session-snapshot.js';
|
|
7
|
-
import { MAX_KINETIX_SESSION_CONFIG_BYTES, decodeDeterministicSessionMessage, encodeDeterministicSessionMessage, } from './session-wire.js';
|
|
7
|
+
import { MAX_KINETIX_SESSION_CONFIG_BYTES, MAX_SYNCPLAY_COMMAND_PAYLOAD_BYTES, MAX_SYNCPLAY_EPHEMERAL_PAYLOAD_BYTES, decodeDeterministicSessionMessage, encodeDeterministicSessionMessage, } from './session-wire.js';
|
|
8
8
|
import { createSyncplaySecretAuthority, } from './secret-authority.js';
|
|
9
9
|
const DEFAULT_SESSION_MSG_TYPE = 'rdm';
|
|
10
10
|
const DEFAULT_SECRET_MSG_TYPE = 'rdm-secret';
|
|
@@ -16,6 +16,10 @@ const HISTORY_CHUNK_FRAMES = 500;
|
|
|
16
16
|
const DEFAULT_SNAPSHOT_CADENCE_TICKS = 3600;
|
|
17
17
|
/** Ten seconds at 60 Hz — see DeterministicAuthorityRoomConfig.transferTimeoutTicks. */
|
|
18
18
|
const DEFAULT_TRANSFER_TIMEOUT_TICKS = 600;
|
|
19
|
+
/** See DeterministicAuthorityRoomConfig.commandsPerSecond. */
|
|
20
|
+
const DEFAULT_COMMANDS_PER_SECOND = 60;
|
|
21
|
+
/** Per-slot ephemeral relay ceiling per one-second window (matches the client's outbound coalescing). */
|
|
22
|
+
const EPHEMERAL_RELAYS_PER_SECOND = 20;
|
|
19
23
|
export function createDeterministicAuthorityRoom(transport, config) {
|
|
20
24
|
assertKinetixRuntimeIdentity(config.runtimeIdentity);
|
|
21
25
|
invariant(config.runtimeIdentity.tickRate === config.tickRateHz, 'runtime identity tick rate does not match the authority clock', 'authority-room.runtime-tick-rate');
|
|
@@ -30,10 +34,20 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
30
34
|
}
|
|
31
35
|
const sessionMsgType = config.sessionMsgType ?? DEFAULT_SESSION_MSG_TYPE;
|
|
32
36
|
const historyRetentionTicks = config.historyRetentionTicks ?? DEFAULT_HISTORY_RETENTION_TICKS;
|
|
37
|
+
const commandsPerSecond = config.commandsPerSecond ?? DEFAULT_COMMANDS_PER_SECOND;
|
|
38
|
+
invariant(Number.isInteger(commandsPerSecond) && commandsPerSecond >= 0, 'commandsPerSecond must be a non-negative integer', 'authority-room.commands-per-second');
|
|
39
|
+
const reconfigurePolicy = config.reconfigurePolicy ?? 'none';
|
|
40
|
+
// Mutable session identity: reconfigureSession swaps these, and every greet,
|
|
41
|
+
// snapshot validation, and persisted record must read the CURRENT values.
|
|
42
|
+
let sessionConfigBytes = config.sessionConfigBytes.slice();
|
|
43
|
+
let sessionConfigDigest = config.sessionConfigDigest;
|
|
44
|
+
let seed = config.seed;
|
|
45
|
+
let generation = config.restoreFrom?.generation ?? 0;
|
|
46
|
+
invariant(Number.isInteger(generation) && generation >= 0, 'restored generation must be a non-negative integer', 'authority-room.restore-generation');
|
|
33
47
|
if (config.maxSnapshotBytes !== undefined && (!Number.isInteger(config.maxSnapshotBytes) || config.maxSnapshotBytes < 1)) {
|
|
34
48
|
throw new Error(`createDeterministicAuthorityRoom: maxSnapshotBytes must be a positive integer, received ${config.maxSnapshotBytes}`);
|
|
35
49
|
}
|
|
36
|
-
const
|
|
50
|
+
const createSessionAuthority = (restoreFrom) => createInputAuthority({
|
|
37
51
|
playerCount: config.playerCount,
|
|
38
52
|
hardToleranceTicks: config.hardToleranceTicks,
|
|
39
53
|
neutralInput: config.neutralInput,
|
|
@@ -43,10 +57,11 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
43
57
|
// (historyRetentionTicks — explicit caller choice wins; the generous floor
|
|
44
58
|
// lives in DEFAULT_HISTORY_RETENTION_TICKS, not here, so tests can shrink it).
|
|
45
59
|
retainedConfirmedFrames: Math.max(historyRetentionTicks, config.redundancyWindowTicks * 4),
|
|
46
|
-
...(
|
|
47
|
-
? {}
|
|
48
|
-
: { restoreFrom: { confirmedThrough: config.restoreFrom.confirmedThrough, frames: config.restoreFrom.tail } }),
|
|
60
|
+
...(restoreFrom === undefined ? {} : { restoreFrom }),
|
|
49
61
|
});
|
|
62
|
+
let authority = createSessionAuthority(config.restoreFrom === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: { confirmedThrough: config.restoreFrom.confirmedThrough, frames: config.restoreFrom.tail });
|
|
50
65
|
const slotByPlayer = new Map();
|
|
51
66
|
// G11 spectators: read-only connections with no slot. Their input /
|
|
52
67
|
// checksum-report / net-stats envelopes are already dropped by the
|
|
@@ -81,12 +96,13 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
81
96
|
// snapshot and runs at most one donor transfer at a time.
|
|
82
97
|
const snapshotCadenceTicks = config.snapshotCadenceTicks ?? DEFAULT_SNAPSHOT_CADENCE_TICKS;
|
|
83
98
|
const transferTimeoutTicks = config.transferTimeoutTicks ?? DEFAULT_TRANSFER_TIMEOUT_TICKS;
|
|
84
|
-
const
|
|
85
|
-
seed
|
|
99
|
+
const describeRoom = () => deterministicRoomDescriptorHash({
|
|
100
|
+
seed,
|
|
86
101
|
playerCount: config.playerCount,
|
|
87
102
|
tickRateHz: config.tickRateHz,
|
|
88
103
|
hardToleranceTicks: config.hardToleranceTicks,
|
|
89
104
|
});
|
|
105
|
+
let roomDescriptorHash = describeRoom();
|
|
90
106
|
let storedSnapshot;
|
|
91
107
|
let pendingTransfer;
|
|
92
108
|
let nextCadenceTarget = snapshotCadenceTicks;
|
|
@@ -98,6 +114,14 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
98
114
|
// once per newly-confirmed tick) and the latest client-reported stats.
|
|
99
115
|
const substitutedTicksBySlot = new Map();
|
|
100
116
|
const clientNetStatsBySlot = new Map();
|
|
117
|
+
// Command ingress bookkeeping: per-slot sliding acceptance window (in authority
|
|
118
|
+
// ticks) and the last accepted sequence, both reset by a reconfigure.
|
|
119
|
+
let commandTicksBySlot = new Map();
|
|
120
|
+
let lastCommandSequenceBySlot = new Map();
|
|
121
|
+
const ephemeralTicksBySlot = new Map();
|
|
122
|
+
let rejectedCommands = 0;
|
|
123
|
+
let rejectedReconfigures = 0;
|
|
124
|
+
let rejectedEphemeral = 0;
|
|
101
125
|
let substitutionCountedThrough = -1;
|
|
102
126
|
// Highest tick ever included in a confirmed-input broadcast — the anchor
|
|
103
127
|
// that guarantees frontier jumps larger than the redundancy window can
|
|
@@ -126,7 +150,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
126
150
|
const decoded = decodeDeterministicStateSnapshot(restore.snapshotBytes);
|
|
127
151
|
invariant(decoded.ok, 'restored snapshotBytes do not decode', 'authority-room.restore-snapshot');
|
|
128
152
|
invariant(equalRuntimeIdentity(decoded.snapshot.runtimeIdentity, config.runtimeIdentity)
|
|
129
|
-
&& decoded.snapshot.sessionConfigDigest ===
|
|
153
|
+
&& decoded.snapshot.sessionConfigDigest === sessionConfigDigest, 'restored snapshot runtime identity or config digest does not match the room', 'authority-room.restore-snapshot-runtime-config');
|
|
130
154
|
// With preserveFullHistory the persisted tail reaches back to tick 0
|
|
131
155
|
// (below the snapshot); a tail starting ABOVE the snapshot tick would
|
|
132
156
|
// leave an unreplayable gap and must still fail loud.
|
|
@@ -159,13 +183,16 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
159
183
|
kind: 'session-start',
|
|
160
184
|
slot,
|
|
161
185
|
playerCount: config.playerCount,
|
|
162
|
-
seed
|
|
186
|
+
seed,
|
|
163
187
|
tickRateHz: config.tickRateHz,
|
|
164
188
|
hardToleranceTicks: config.hardToleranceTicks,
|
|
165
189
|
role,
|
|
166
190
|
runtimeIdentity: config.runtimeIdentity,
|
|
167
|
-
sessionConfigBytes:
|
|
168
|
-
sessionConfigDigest
|
|
191
|
+
sessionConfigBytes: sessionConfigBytes.slice(),
|
|
192
|
+
sessionConfigDigest,
|
|
193
|
+
// Omitted at generation 0 so a room that never reconfigures encodes
|
|
194
|
+
// byte-identically to what pre-generation peers expect.
|
|
195
|
+
...(generation === 0 ? {} : { generation }),
|
|
169
196
|
}));
|
|
170
197
|
}
|
|
171
198
|
/**
|
|
@@ -379,12 +406,31 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
379
406
|
}));
|
|
380
407
|
return;
|
|
381
408
|
}
|
|
382
|
-
if (message.kind !== 'input' && message.kind !== 'checksum-report' && message.kind !== 'net-stats'
|
|
409
|
+
if (message.kind !== 'input' && message.kind !== 'checksum-report' && message.kind !== 'net-stats'
|
|
410
|
+
&& message.kind !== 'command-request' && message.kind !== 'ephemeral' && message.kind !== 'reconfigure-request') {
|
|
383
411
|
return;
|
|
384
412
|
}
|
|
385
413
|
const slot = slotByPlayer.get(playerId);
|
|
386
414
|
// Server owns the slot mapping — a client cannot act for a slot it was not assigned.
|
|
387
415
|
if (slot === undefined || message.slot !== slot) {
|
|
416
|
+
if (message.kind === 'command-request')
|
|
417
|
+
rejectedCommands += 1;
|
|
418
|
+
else if (message.kind === 'ephemeral')
|
|
419
|
+
rejectedEphemeral += 1;
|
|
420
|
+
else if (message.kind === 'reconfigure-request')
|
|
421
|
+
rejectedReconfigures += 1;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (message.kind === 'command-request') {
|
|
425
|
+
acceptCommandRequest(slot, message.generation, message.sequence, message.command);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (message.kind === 'ephemeral') {
|
|
429
|
+
relayEphemeral(playerId, slot, message.payload);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (message.kind === 'reconfigure-request') {
|
|
433
|
+
acceptReconfigureRequest(slot, message.sessionConfigBytes, message.sessionConfigDigest, message.seed);
|
|
388
434
|
return;
|
|
389
435
|
}
|
|
390
436
|
if (message.kind === 'input') {
|
|
@@ -404,6 +450,118 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
404
450
|
});
|
|
405
451
|
}
|
|
406
452
|
}
|
|
453
|
+
/**
|
|
454
|
+
* Sliding one-second acceptance window, measured in authority ticks so the
|
|
455
|
+
* limit is clock-free (the room owns no wall clock). Returns false when the
|
|
456
|
+
* slot is over budget.
|
|
457
|
+
*/
|
|
458
|
+
function admitInWindow(windows, slot, perSecond) {
|
|
459
|
+
const floor = now - config.tickRateHz + 1;
|
|
460
|
+
const retained = (windows.get(slot) ?? []).filter((tick) => tick >= floor);
|
|
461
|
+
if (retained.length >= perSecond) {
|
|
462
|
+
windows.set(slot, retained);
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
retained.push(now);
|
|
466
|
+
windows.set(slot, retained);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
function acceptCommandRequest(slot, requestGeneration, sequence, command) {
|
|
470
|
+
// A stale generation means the request was authored against a session that
|
|
471
|
+
// no longer exists — applying it would mutate the wrong level.
|
|
472
|
+
if (requestGeneration !== generation) {
|
|
473
|
+
rejectedCommands += 1;
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const lastSequence = lastCommandSequenceBySlot.get(slot);
|
|
477
|
+
if (lastSequence !== undefined && sequence <= lastSequence) {
|
|
478
|
+
rejectedCommands += 1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (canonicalStringifyByteLength(command.payload) > MAX_SYNCPLAY_COMMAND_PAYLOAD_BYTES) {
|
|
482
|
+
rejectedCommands += 1;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!admitInWindow(commandTicksBySlot, slot, commandsPerSecond)) {
|
|
486
|
+
rejectedCommands += 1;
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
lastCommandSequenceBySlot.set(slot, sequence);
|
|
490
|
+
// The same tick-assignment path secret commands use, so canonicalization,
|
|
491
|
+
// confirmed-frame distribution, replay, and snapshots need no change.
|
|
492
|
+
const tick = Math.max(now, authority.confirmedThrough) + 1;
|
|
493
|
+
authority.queueCommand(tick, {
|
|
494
|
+
kind: `game.${command.kind}`,
|
|
495
|
+
slot,
|
|
496
|
+
payload: command.payload,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
function relayEphemeral(senderId, slot, payload) {
|
|
500
|
+
if (canonicalStringifyByteLength(payload) > MAX_SYNCPLAY_EPHEMERAL_PAYLOAD_BYTES) {
|
|
501
|
+
rejectedEphemeral += 1;
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!admitInWindow(ephemeralTicksBySlot, slot, EPHEMERAL_RELAYS_PER_SECOND)) {
|
|
505
|
+
rejectedEphemeral += 1;
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
const encoded = encodeDeterministicSessionMessage({ kind: 'ephemeral', slot, payload });
|
|
509
|
+
for (const playerId of [...slotByPlayer.keys(), ...spectators]) {
|
|
510
|
+
if (playerId === senderId)
|
|
511
|
+
continue;
|
|
512
|
+
transport.sendTo(playerId, sessionMsgType, encoded);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
function acceptReconfigureRequest(slot, nextBytes, nextDigest, nextSeed) {
|
|
516
|
+
const authorized = reconfigurePolicy === 'any'
|
|
517
|
+
|| (reconfigurePolicy === 'creator' && config.creatorSlot !== undefined && slot === config.creatorSlot);
|
|
518
|
+
if (!authorized || nextBytes.byteLength > MAX_KINETIX_SESSION_CONFIG_BYTES) {
|
|
519
|
+
rejectedReconfigures += 1;
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
reconfigureSession({
|
|
523
|
+
sessionConfigBytes: nextBytes,
|
|
524
|
+
sessionConfigDigest: nextDigest,
|
|
525
|
+
...(nextSeed === undefined ? {} : { seed: nextSeed }),
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
function reconfigureSession(next) {
|
|
529
|
+
if (next.runtimeIdentity !== undefined && !equalRuntimeIdentity(next.runtimeIdentity, config.runtimeIdentity)) {
|
|
530
|
+
throw new Error('SYNCPLAY_RECONFIGURE_IDENTITY_MISMATCH');
|
|
531
|
+
}
|
|
532
|
+
if (!(next.sessionConfigBytes instanceof Uint8Array)
|
|
533
|
+
|| next.sessionConfigBytes.byteLength === 0
|
|
534
|
+
|| next.sessionConfigBytes.byteLength > MAX_KINETIX_SESSION_CONFIG_BYTES
|
|
535
|
+
|| kinetixSessionConfigDigest(next.sessionConfigBytes) !== next.sessionConfigDigest) {
|
|
536
|
+
throw new Error('SYNCPLAY_RECONFIGURE_CONFIG_INVALID');
|
|
537
|
+
}
|
|
538
|
+
generation += 1;
|
|
539
|
+
sessionConfigBytes = next.sessionConfigBytes.slice();
|
|
540
|
+
sessionConfigDigest = next.sessionConfigDigest;
|
|
541
|
+
if (next.seed !== undefined)
|
|
542
|
+
seed = next.seed;
|
|
543
|
+
roomDescriptorHash = describeRoom();
|
|
544
|
+
// The tick space restarts at 0: every client full-resets on the differing
|
|
545
|
+
// generation, so retained history, snapshots, and reports all describe a
|
|
546
|
+
// session that no longer exists.
|
|
547
|
+
authority = createSessionAuthority();
|
|
548
|
+
storedSnapshot = undefined;
|
|
549
|
+
pendingTransfer = undefined;
|
|
550
|
+
checksumReports.clear();
|
|
551
|
+
reportsDirty = false;
|
|
552
|
+
substitutedTicksBySlot.clear();
|
|
553
|
+
substitutionCountedThrough = -1;
|
|
554
|
+
commandTicksBySlot = new Map();
|
|
555
|
+
lastCommandSequenceBySlot = new Map();
|
|
556
|
+
nextCadenceTarget = snapshotCadenceTicks;
|
|
557
|
+
broadcastThrough = -1;
|
|
558
|
+
restoredRoom = false;
|
|
559
|
+
now = -1;
|
|
560
|
+
for (const [playerId, slot] of slotByPlayer)
|
|
561
|
+
sendSessionStart(playerId, slot);
|
|
562
|
+
for (const playerId of spectators)
|
|
563
|
+
sendSessionStart(playerId, -1, 'spectator');
|
|
564
|
+
}
|
|
407
565
|
function recordChecksumReport(slot, tick, checksum) {
|
|
408
566
|
let reports = checksumReports.get(tick);
|
|
409
567
|
if (reports === undefined) {
|
|
@@ -507,7 +665,7 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
507
665
|
return;
|
|
508
666
|
}
|
|
509
667
|
if (!equalRuntimeIdentity(snapshot.runtimeIdentity, config.runtimeIdentity)
|
|
510
|
-
|| snapshot.sessionConfigDigest !==
|
|
668
|
+
|| snapshot.sessionConfigDigest !== sessionConfigDigest) {
|
|
511
669
|
failPendingTransfer('snapshot-runtime-config-mismatch');
|
|
512
670
|
return;
|
|
513
671
|
}
|
|
@@ -704,8 +862,9 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
704
862
|
everSlots: Object.fromEntries(everSlotByPlayer),
|
|
705
863
|
earliestRetainedTick: authority.earliestRetainedTick,
|
|
706
864
|
runtimeIdentity: config.runtimeIdentity,
|
|
707
|
-
sessionConfigBytes:
|
|
708
|
-
sessionConfigDigest
|
|
865
|
+
sessionConfigBytes: sessionConfigBytes.slice(),
|
|
866
|
+
sessionConfigDigest,
|
|
867
|
+
generation,
|
|
709
868
|
...(storedSnapshot === undefined
|
|
710
869
|
? {}
|
|
711
870
|
: {
|
|
@@ -741,15 +900,19 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
741
900
|
confirmedThrough: authority.confirmedThrough,
|
|
742
901
|
substitutedTicksBySlot: Object.fromEntries(substitutedTicksBySlot),
|
|
743
902
|
clientStats: Object.fromEntries(clientNetStatsBySlot),
|
|
903
|
+
rejectedCommands,
|
|
904
|
+
rejectedReconfigures,
|
|
905
|
+
rejectedEphemeral,
|
|
744
906
|
};
|
|
745
907
|
},
|
|
908
|
+
reconfigureSession,
|
|
746
909
|
matchLog() {
|
|
747
910
|
if (authority.confirmedThrough < 0 || !historyReachesTickZero()) {
|
|
748
911
|
return undefined;
|
|
749
912
|
}
|
|
750
913
|
return {
|
|
751
914
|
v: 1,
|
|
752
|
-
seed
|
|
915
|
+
seed,
|
|
753
916
|
tickRateHz: config.tickRateHz,
|
|
754
917
|
playerCount: config.playerCount,
|
|
755
918
|
hardToleranceTicks: config.hardToleranceTicks,
|
|
@@ -763,6 +926,9 @@ export function createDeterministicAuthorityRoom(transport, config) {
|
|
|
763
926
|
},
|
|
764
927
|
};
|
|
765
928
|
}
|
|
929
|
+
function canonicalStringifyByteLength(value) {
|
|
930
|
+
return encodeUtf8(canonicalStringify(value)).byteLength;
|
|
931
|
+
}
|
|
766
932
|
function requireSecretAuthorityId(authorityId) {
|
|
767
933
|
invariant(typeof authorityId === 'string' && authorityId.length > 0, 'secret authorityId is required', 'authority-room.secret-authority-id');
|
|
768
934
|
return authorityId;
|
package/dist/browser.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export { createDeterministicEventEffectsAdapter } from './effects-adapter.js';
|
|
|
11
11
|
export { assertDeterministicFrameHeapStaleHandleError, assertDeterministicFrameSystemsStateless, compileDeterministicDslRuntimeDescriptor, createDeterministicBitset, createDeterministicFrameRuntime, createDeterministicFrameRuntimeAccessors, createDeterministicSlotMap, createDeterministicSparseSetFrameRuntime, generateDeterministicFrameRuntimeSource, isDeterministicFrameHeapStaleHandleError, runDeterministicFrameRuntimeMutationCorpus, validateDeterministicFrameSystemsStateless } from './engine-runtime.js';
|
|
12
12
|
export { createDeterministicEventTimeline, filterDeterministicEventsForPlayer } from './events.js';
|
|
13
13
|
export { createInputAuthority } from './input-authority.js';
|
|
14
|
+
export { defineSyncplayInputCodec, validateSyncplayInputCodec } from './input-codec.js';
|
|
15
|
+
export { preparePlatformerLevel, publishPlatformerLevel, SYNCPLAY_PLATFORMER_LEVEL_CONTENT_TYPE, SYNCPLAY_PLATFORMER_LEVEL_DATA_KEY } from './platformer-content.js';
|
|
14
16
|
export { createInstantReplayRunner, runInstantReplay } from './instant-replay.js';
|
|
15
17
|
export { buildDeterministicLagCompensatedHitboxProxies3D, createDeterministicLagCompensation3DHistory, rewindDeterministicLagCompensatedTargets3D, validateDeterministicAuthoritativeLagCompensatedShot3D, validateDeterministicLagCompensatedShot3D } from './lag-compensation3d.js';
|
|
16
18
|
export { createDeterministicLocalAuthoritySnapshotChunkEnvelope, createDeterministicLocalAuthoritySnapshotCompleteEnvelope, createDeterministicLocalAuthoritySnapshotOfferEnvelope, createDeterministicLocalAuthoritySnapshotRequestEnvelope, decodeDeterministicLocalAuthorityWireEnvelope, deterministicLocalAuthorityWireProtocol, encodeDeterministicLocalAuthorityWireEnvelope } from './local-authority-wire.js';
|
|
@@ -51,6 +53,8 @@ export type { DeterministicEffectDispatch, DeterministicEffectTarget, Determinis
|
|
|
51
53
|
export type { DeterministicCommandAccessor, DeterministicComponentAccessor, DeterministicComponentData, DeterministicComponentDescriptor, DeterministicComponentFieldDescriptor, DeterministicComponentFieldKind, DeterministicComponentFixedArrayFieldKind, DeterministicComponentFrameHeapFieldKind, DeterministicComponentScalarFieldKind, DeterministicDslRuntimeCompileOptions, DeterministicEntityRef, DeterministicEventAccessor, DeterministicFrameHeapCollectionKind, DeterministicFrameHeapCollectionSnapshot, DeterministicFrameHeapHandle, DeterministicFrameHeapSnapshot, DeterministicFrameQueryFilter, DeterministicFrameRuntime, DeterministicFrameRuntimeAccessors, DeterministicFrameRuntimeDescriptor, DeterministicFrameRuntimeOptions, DeterministicFrameSnapshot, DeterministicQueryRow, DeterministicRuntimeMessage, DeterministicRuntimePayloadDescriptor, DeterministicRuntimeSignalHandler, DeterministicSignalAccessor, DeterministicSingletonAccessor, DeterministicStatelessSystemDiagnostic, DeterministicStatelessSystemDiagnosticReason, DeterministicStatelessSystemValidationResult, DeterministicSystem, DeterministicSystemContext, DeterministicSystemDescriptor } from './engine-runtime.js';
|
|
52
54
|
export type { DeterministicEventDeliveryScope, DeterministicEventEmitOptions, DeterministicEventPredictionState, DeterministicEventRecord, DeterministicEventTimeline } from './events.js';
|
|
53
55
|
export type { ConfirmedInputFrame, EncodedInput, InputAuthority, InputAuthorityConfig, InputAuthorityRestoreState, InputAuthorityStats, InputRejectionReason, InputSubmissionResult } from './input-authority.js';
|
|
56
|
+
export type { SyncplayBooleanInputField, SyncplayEnumInputField, SyncplayInputCodec, SyncplayInputCodecProbe, SyncplayInputField, SyncplayInputSchema, SyncplayIntInputField } from './input-codec.js';
|
|
57
|
+
export type { PlatformerLevelDeclaration, PlatformerLevelUgcApi, PlatformerLevelUgcCreateParams, PlatformerLevelUgcEntry, PublishPlatformerLevelOptions } from './platformer-content.js';
|
|
54
58
|
export type { InstantReplayInputFrame, InstantReplayResult, InstantReplayRunner, InstantReplayRunnerOptions, InstantReplayRunOptions, RunInstantReplayOptions } from './instant-replay.js';
|
|
55
59
|
export type { DeterministicAuthoritativeLagCompensation3DShotContext, DeterministicLagCompensatedPellet3DResult, DeterministicLagCompensatedShot3D, DeterministicLagCompensatedShot3DResult, DeterministicLagCompensatedTarget3D, DeterministicLagCompensation3DHistory, DeterministicLagCompensation3DHistoryOptions, DeterministicLagCompensation3DHistorySnapshot, DeterministicLagCompensation3DRewindSnapshot } from './lag-compensation3d.js';
|
|
56
60
|
export type { DeterministicLocalAuthoritySnapshotIdentity, DeterministicLocalAuthoritySnapshotTransfer, DeterministicLocalAuthorityWireEnvelope, DeterministicLocalAuthorityWireKind } from './local-authority-wire.js';
|
|
@@ -59,8 +63,8 @@ export type { DeterministicFpsAimState, DeterministicFpsInput, DeterministicKccB
|
|
|
59
63
|
export type { DeterministicKccBody3D, DeterministicKccCarrierPose3D, DeterministicKccCarryResult3D, DeterministicKccCarryState3D, DeterministicKccCollisionCandidate3D, DeterministicKccCollisionFilter3D, DeterministicKccExternalImpulse3D, DeterministicKccGeometry3D, DeterministicKccInput3D, DeterministicKccPhysicsImpulse3D, DeterministicKccProcessor3D, DeterministicKccStepResult3D, DeterministicKccWorld3D, DeterministicMovingPlatform3D, DeterministicSlope3D, DeterministicStep3D } from './movement3d.js';
|
|
60
64
|
export type { CookedDeterministicNavigationGraph, DeterministicFlowField, DeterministicNavEdge, DeterministicNavigationAgent, DeterministicNavigationGraphSource, DeterministicNavigationStepOptions, DeterministicNavigationStepResult, DeterministicNavNode, DeterministicNavRegion, DeterministicPathQuery, DeterministicPathResult, DeterministicTilemapSource } from './navigation.js';
|
|
61
65
|
export type { CookedDeterministicNavmesh, DeterministicNavmeshAgent, DeterministicNavmeshAgentStepResult, DeterministicNavmeshImportSource, DeterministicNavmeshLink, DeterministicNavmeshMovingAvoidanceObstacle, DeterministicNavmeshOffMeshTraversalState, DeterministicNavmeshOffMeshTraversalStep, DeterministicNavmeshPath, DeterministicNavmeshPoint, DeterministicNavmeshPolygon, DeterministicNavmeshSource } from './navmesh.js';
|
|
62
|
-
export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, NetworkedSyncplaySlotPresence, PreparedNetworkedSyncplayTransport } from './networked-client.js';
|
|
63
|
-
export type { SyncplayRunner, SyncplayRunnerConfig, SyncplayRunnerConnectionEvent, SyncplayRunnerOfflineOptions, SyncplayRunnerPresentation, SyncplayRunnerSnapshot, SyncplayRunnerStatus, SyncplayRunnerTransport } from './runner.js';
|
|
66
|
+
export type { ConfirmedSyncplayPresentationFrame, KinetixRuntimeFactory, NetworkedClientTransport, NetworkedSyncplayClient, NetworkedSyncplayClientInputDelayOptions, NetworkedSyncplayClientNetStats, NetworkedSyncplayClientOptions, NetworkedSyncplayClientPacingOptions, NetworkedSyncplayPreparationOptions, NetworkedSyncplaySessionDescriptor, NetworkedSyncplaySlotPresence, PreparedNetworkedSyncplayTransport, SyncplayPredictionMode } from './networked-client.js';
|
|
67
|
+
export type { SyncplayRunner, SyncplayRunnerConfig, SyncplayRunnerConnectionEvent, SyncplayRunnerOccupancy, SyncplayRunnerOfflineOptions, SyncplayRunnerPresentation, SyncplayRunnerSnapshot, SyncplayRunnerStatus, SyncplayRunnerTransport } from './runner.js';
|
|
64
68
|
export type { DeterministicBodyKind, DeterministicContactEvent2D, DeterministicContactEventType, DeterministicPhysicsAabbQuery2D, DeterministicPhysicsActivePair2D, DeterministicPhysicsBody2D, DeterministicPhysicsCircleCastQuery2D, DeterministicPhysicsConstraint2D, DeterministicPhysicsHit2D, DeterministicPhysicsQueryOptions2D, DeterministicPhysicsRaycastQuery2D, DeterministicPhysicsShape2D, DeterministicPhysicsShapeOverlapQuery2D, DeterministicPhysicsStepOptions2D, DeterministicPhysicsStepResult2D, DeterministicPhysicsWorld2D } from './physics2d.js';
|
|
65
69
|
export type { DeterministicContactCacheEntry3D, DeterministicJointCacheEntry3D, DeterministicPhysicsActivePair3D, DeterministicPhysicsAxisLock3D, DeterministicPhysicsBody3D, DeterministicPhysicsCallbackMask3D, DeterministicPhysicsCompoundChild3D, DeterministicPhysicsCompoundChildShape3D, DeterministicPhysicsConstraint3D, DeterministicPhysicsContactDetail3D, DeterministicPhysicsContactEvent3D, DeterministicPhysicsDetailedHit3D, DeterministicPhysicsHit3D, DeterministicPhysicsHullShape3D, DeterministicPhysicsImpulseApplication3D, DeterministicPhysicsOverlapQuery3D, DeterministicPhysicsPrimitiveShape3D, DeterministicPhysicsQuaternion3D, DeterministicPhysicsQueryOptions3D, DeterministicPhysicsRaycastQuery3D, DeterministicPhysicsScheduledQuery3D, DeterministicPhysicsShape3D, DeterministicPhysicsShapeCastQuery3D, DeterministicPhysicsStepResult3D, DeterministicPhysicsVec3, DeterministicPhysicsWorld3D, DeterministicPhysicsWorldEdit3D } from './physics3d.js';
|
|
66
70
|
export type { DeterministicVoxelChunk3D, DeterministicVoxelCompoundShape3D } from './physics3d-voxel.js';
|
|
@@ -71,7 +75,7 @@ export type { SchemaBinaryProof, SchemaBinarySourceOptions, SchemaTypesOptions }
|
|
|
71
75
|
export type { DeterministicBinarySerializer, DeterministicDslDeclaration, DeterministicDslDeclarationKind, DeterministicDslDictionaryEntry, DeterministicDslDictionaryFieldKind, DeterministicDslField, DeterministicDslFieldKind, DeterministicDslFixedArrayFieldKind, DeterministicDslListFieldKind, DeterministicDslScalarFieldKind, DeterministicDslScalarValue, DeterministicDslSchema, DeterministicDslSetFieldKind, DeterministicDslValue } from './schema-dsl-binary.js';
|
|
72
76
|
export type { SyncplaySession, SyncplaySessionPolicy } from './sdk-session.js';
|
|
73
77
|
export type { CollectedSnapshotTransfer, CreateSnapshotTransferOptions, DeterministicStateSnapshot, DeterministicStateSnapshotDecodeRejection, DeterministicStateSnapshotDecodeResult, SnapshotTransferCollector, SnapshotTransferCollectorOptions, SnapshotTransferCompleteResult, SnapshotTransferRejection, SnapshotTransferStepResult } from './session-snapshot.js';
|
|
74
|
-
export type { DeterministicSessionDecodeRejection, DeterministicSessionDecodeResult, DeterministicSessionMessage, DeterministicSessionMessageKind, DeterministicSessionRole } from './session-wire.js';
|
|
78
|
+
export type { DeterministicCommandRequestMessage, DeterministicEphemeralMessage, DeterministicReconfigureRequestMessage, DeterministicSessionDecodeRejection, DeterministicSessionDecodeResult, DeterministicSessionMessage, DeterministicSessionMessageKind, DeterministicSessionRole } from './session-wire.js';
|
|
75
79
|
export type { DeterministicSignal, DeterministicSignalBus, DeterministicSignalBusSnapshot, DeterministicSignalSubscription } from './signals.js';
|
|
76
80
|
export type { DeterministicSparseSet, DeterministicSparseSetSnapshot } from './sparse-set.js';
|
|
77
81
|
export type { DeterministicLifecycleCounterSummary, DeterministicLifecycleRunner, DeterministicLifecycleSignal, DeterministicLifecycleSnapshot, DeterministicLifecycleSystemContext, DeterministicLifecycleSystemDescriptor, DeterministicLifecycleSystemHooks } from './system-lifecycle.js';
|
package/dist/browser.js
CHANGED
|
@@ -12,6 +12,8 @@ export { createDeterministicEventEffectsAdapter } from './effects-adapter.js';
|
|
|
12
12
|
export { assertDeterministicFrameHeapStaleHandleError, assertDeterministicFrameSystemsStateless, compileDeterministicDslRuntimeDescriptor, createDeterministicBitset, createDeterministicFrameRuntime, createDeterministicFrameRuntimeAccessors, createDeterministicSlotMap, createDeterministicSparseSetFrameRuntime, generateDeterministicFrameRuntimeSource, isDeterministicFrameHeapStaleHandleError, runDeterministicFrameRuntimeMutationCorpus, validateDeterministicFrameSystemsStateless } from './engine-runtime.js';
|
|
13
13
|
export { createDeterministicEventTimeline, filterDeterministicEventsForPlayer } from './events.js';
|
|
14
14
|
export { createInputAuthority } from './input-authority.js';
|
|
15
|
+
export { defineSyncplayInputCodec, validateSyncplayInputCodec } from './input-codec.js';
|
|
16
|
+
export { preparePlatformerLevel, publishPlatformerLevel, SYNCPLAY_PLATFORMER_LEVEL_CONTENT_TYPE, SYNCPLAY_PLATFORMER_LEVEL_DATA_KEY } from './platformer-content.js';
|
|
15
17
|
export { createInstantReplayRunner, runInstantReplay } from './instant-replay.js';
|
|
16
18
|
export { buildDeterministicLagCompensatedHitboxProxies3D, createDeterministicLagCompensation3DHistory, rewindDeterministicLagCompensatedTargets3D, validateDeterministicAuthoritativeLagCompensatedShot3D, validateDeterministicLagCompensatedShot3D } from './lag-compensation3d.js';
|
|
17
19
|
export { createDeterministicLocalAuthoritySnapshotChunkEnvelope, createDeterministicLocalAuthoritySnapshotCompleteEnvelope, createDeterministicLocalAuthoritySnapshotOfferEnvelope, createDeterministicLocalAuthoritySnapshotRequestEnvelope, decodeDeterministicLocalAuthorityWireEnvelope, deterministicLocalAuthorityWireProtocol, encodeDeterministicLocalAuthorityWireEnvelope } from './local-authority-wire.js';
|
|
@@ -39,6 +39,8 @@ export class DevSyncplayAuthorityRoom {
|
|
|
39
39
|
? undefined
|
|
40
40
|
: validateSyncplaySecretSystemsConfig(deterministic.secrets, playerCount);
|
|
41
41
|
const secretCrypto = secretConfig === undefined ? undefined : createEphemeralSecretCrypto();
|
|
42
|
+
const reconfigurePolicy = deterministic?.reconfigurePolicy;
|
|
43
|
+
const commandsPerSecond = deterministic?.commandsPerSecond;
|
|
42
44
|
// The server owns the seed: derive it deterministically from the room id so
|
|
43
45
|
// every client of this room receives the same seed (same scheme as prod).
|
|
44
46
|
const seed = deriveSeed(ctx.roomId);
|
|
@@ -55,6 +57,16 @@ export class DevSyncplayAuthorityRoom {
|
|
|
55
57
|
sessionConfigBytes: runtime.sessionConfigBytes,
|
|
56
58
|
sessionConfigDigest: runtime.sessionConfigDigest,
|
|
57
59
|
redundancyWindowTicks: readPositiveInt(config.redundancyWindowTicks, DEFAULT_REDUNDANCY_WINDOW_TICKS),
|
|
60
|
+
// Collaborative-editing knobs, read from the same `deterministic` block
|
|
61
|
+
// the production room server reads. Without these a `reconfigurePolicy`
|
|
62
|
+
// a creator declared in their realtime config is silently ignored under
|
|
63
|
+
// `npm run dev`, and every publish is dropped by the default 'none'.
|
|
64
|
+
...(reconfigurePolicy === undefined ? {} : { reconfigurePolicy }),
|
|
65
|
+
...(commandsPerSecond === undefined ? {} : { commandsPerSecond }),
|
|
66
|
+
// Slot 0 is the room's first seat — the creator in the create-and-share
|
|
67
|
+
// flow. The dev sidecar has no platform identity to consult, so this
|
|
68
|
+
// matches the production wrapper's choice.
|
|
69
|
+
creatorSlot: 0,
|
|
58
70
|
// Game-agnostic no-op input: the authority substitutes canonical null for
|
|
59
71
|
// a slot with no prior input; the client maps null to the game's default.
|
|
60
72
|
neutralInput: null,
|
|
@@ -14,8 +14,14 @@ export interface DeterministicEventEffectsAdapter<Payload, Effect> {
|
|
|
14
14
|
subscribe(listener: (dispatch: DeterministicEffectDispatch<Payload, Effect>) => void): () => void;
|
|
15
15
|
reset(): void;
|
|
16
16
|
}
|
|
17
|
+
export declare const DEFAULT_EFFECT_RETENTION_FRAMES = 1024;
|
|
17
18
|
export interface DeterministicEventEffectsAdapterConfig<Payload, Effect> {
|
|
18
19
|
readonly target: DeterministicEffectTarget;
|
|
19
20
|
readonly mapEvent: (record: DeterministicEventRecord<Payload>) => Effect | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* How far behind the newest dispatched frame a record is still remembered for
|
|
23
|
+
* dedupe, in frames. Defaults to {@link DEFAULT_EFFECT_RETENTION_FRAMES}.
|
|
24
|
+
*/
|
|
25
|
+
readonly retentionFrames?: number;
|
|
20
26
|
}
|
|
21
27
|
export declare function createDeterministicEventEffectsAdapter<Payload, Effect>(config: DeterministicEventEffectsAdapterConfig<Payload, Effect>): DeterministicEventEffectsAdapter<Payload, Effect>;
|