@irtio/client 0.1.0 → 0.3.0
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/dist/chunk-7UTJ7RSF.js +568 -0
- package/dist/index.d.ts +828 -137
- package/dist/index.js +450 -478
- package/dist/physics-H2VDQLAU.js +1059 -0
- package/package.json +14 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,644 @@
|
|
|
1
|
-
import { ReadonlyCollection, RoleOf, VisibleKeys, SchemaDefs, EntityDef, DeepReadonly, InferFields, Owned, SingletonDef, ClientImplementations, ClientRpcs, SchemaRpc, ClientCallProxy
|
|
1
|
+
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, ReadonlyCollection, RoleOf, VisibleKeys, SchemaDefs, EntityDef, DeepReadonly, InferFields, Owned, SingletonDef, ClientImplementations, ClientRpcs, SchemaRpc, ClientCallProxy } from '@irtio/schema';
|
|
2
|
+
import RAPIER from '@dimforge/rapier3d-compat';
|
|
2
3
|
import { PresenceRecord } from '@irtio/protocol';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
6
|
+
* The client's state layer.
|
|
7
|
+
*
|
|
8
|
+
* One plain `PlainState` (decoded from the join snapshot, advanced by every `DELTA`) with two
|
|
9
|
+
* views over it:
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* - **owned instances** → the `track()` proxy for that record, so a write normalizes exactly like
|
|
12
|
+
* the server would (f32 quantized, ranges and string sizes enforced by a throw) and marks a
|
|
13
|
+
* dirty set the flush window turns into one `WRITE`;
|
|
14
|
+
* - **everything else** → `frozenProxy`, which ignores writes and warns once with the ownership
|
|
15
|
+
* hint.
|
|
16
|
+
*
|
|
17
|
+
* Which one you get is decided per `get()`, so an ownership change needs no bookkeeping — but the
|
|
18
|
+
* object identity is not stable across it, which is why `state.x.get(id)` must be re-read after a
|
|
19
|
+
* grab (documented in the README).
|
|
20
|
+
*
|
|
21
|
+
* The server-wins rule lives here too: a `DELTA`'s update ops are dropped field-wise for
|
|
22
|
+
* instances this client owns (adds, removes and owner changes still apply), while a `CORRECT`
|
|
23
|
+
* applies in full and clears the matching local dirty marks.
|
|
11
24
|
*/
|
|
12
25
|
|
|
26
|
+
type AnyRecord$3 = Record<string, unknown>;
|
|
27
|
+
/**
|
|
28
|
+
* Resimulation depth cap (D19): how many flushed-but-unjudged `WRITE`s the client retains for
|
|
29
|
+
* replay after a `CORRECT`. A correction older than the retained window snaps everything it
|
|
30
|
+
* corrects and counts it (`snapped`); buffer memory is bounded by the same window.
|
|
31
|
+
*/
|
|
32
|
+
declare const RESIM_DEPTH = 20;
|
|
33
|
+
/** One correction op, as the `'correct'` event sees it. */
|
|
34
|
+
interface CorrectionOp {
|
|
35
|
+
readonly collection: string;
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly fields: readonly string[];
|
|
38
|
+
readonly patch: AnyRecord$3;
|
|
39
|
+
/** The local (predicted) values of `fields` right before the snap — what `patch` overrode. */
|
|
40
|
+
readonly previous: AnyRecord$3;
|
|
41
|
+
readonly tick: number;
|
|
42
|
+
/**
|
|
43
|
+
* The client write tick the server judged (D19), absent against a pre-week-8 server. Local
|
|
44
|
+
* writes newer than it were re-applied on top of the correction.
|
|
45
|
+
*/
|
|
46
|
+
readonly clientTick?: number;
|
|
47
|
+
/** How many pending local writes were re-applied over this correction's values. */
|
|
48
|
+
readonly replayed: number;
|
|
49
|
+
/** `true` when the correction outran the resim window and everything it named snapped. */
|
|
50
|
+
readonly snapped: boolean;
|
|
51
|
+
}
|
|
52
|
+
declare class ClientStore {
|
|
53
|
+
readonly ext: AnySchema;
|
|
54
|
+
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
55
|
+
private readonly meOf;
|
|
56
|
+
plain: PlainState;
|
|
57
|
+
private tracked;
|
|
58
|
+
private readonly descs;
|
|
59
|
+
private readonly frozen;
|
|
60
|
+
/** The object handed out as `room.state`; identity survives a resync. */
|
|
61
|
+
readonly view: AnyRecord$3;
|
|
62
|
+
/** Flushed-but-unjudged writes, oldest first, at most `RESIM_DEPTH` entries (D19). */
|
|
63
|
+
private readonly pendingWrites;
|
|
64
|
+
/** The newest write tick evicted from `pendingWrites` — corrections older than it must snap. */
|
|
65
|
+
private evictedThroughTick;
|
|
66
|
+
/**
|
|
67
|
+
* The newest fields to leave `pendingWrites` per instance — judged into a correction, or
|
|
68
|
+
* evicted past the resim depth. This is the intent in force at the *start* of a resim window:
|
|
69
|
+
* the server holds a write's values until the next `WRITE` replaces them, so a replay that
|
|
70
|
+
* walks the window tick by tick starts from here, not from the newest local value.
|
|
71
|
+
*/
|
|
72
|
+
private readonly baselineIntents;
|
|
73
|
+
constructor(ext: AnySchema,
|
|
74
|
+
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
75
|
+
meOf: () => string);
|
|
76
|
+
loadSnapshot(bytes: Uint8Array): void;
|
|
77
|
+
/**
|
|
78
|
+
* Captures the field values of every pending owned write, keyed by collection then id then
|
|
79
|
+
* field name — call this **before** `loadSnapshot` on a resync, or the edit is gone once the
|
|
80
|
+
* old `plain` it lives in is replaced. Paired with `applyPendingWrites`.
|
|
81
|
+
*/
|
|
82
|
+
capturePendingWrites(): Map<string, Map<string, AnyRecord$3>>;
|
|
83
|
+
/**
|
|
84
|
+
* Writes a `capturePendingWrites` snapshot into the freshly loaded `plain` (still the raw
|
|
85
|
+
* decoded values — no validation, they were already validated at the time of the original
|
|
86
|
+
* local write) and marks every owned field dirty so the next flush resends the real edit, not
|
|
87
|
+
* whatever the resync snapshot happened to carry for that field.
|
|
88
|
+
*/
|
|
89
|
+
applyPendingWrites(pending: Map<string, Map<string, AnyRecord$3>>): void;
|
|
90
|
+
private buildView;
|
|
91
|
+
/** The writable tracked proxy when this client owns `id`, otherwise a frozen one. */
|
|
92
|
+
instance(desc: CollectionDesc, id: string): unknown;
|
|
93
|
+
private freeze;
|
|
94
|
+
private entityHint;
|
|
95
|
+
private singletonHint;
|
|
96
|
+
/**
|
|
97
|
+
* Applies a server `DELTA`. Update ops for instances this client owns are dropped field-wise
|
|
98
|
+
* (server wins only through `CORRECT`); adds, removes and owner changes always apply.
|
|
99
|
+
*/
|
|
100
|
+
applyServerDelta(delta: Delta): void;
|
|
101
|
+
/**
|
|
102
|
+
* The flushed-write half of the §7.2 in-flight own-write fix: an `add` op for an instance this
|
|
103
|
+
* client owns replaces the whole record, so any write already *flushed* (dirty set consumed —
|
|
104
|
+
* `preserveLocalWrites` cannot see it) but not yet judged would be reverted by the echo.
|
|
105
|
+
* Re-apply the retained pending writes in flush order; the server's eventual `CORRECT` (if the
|
|
106
|
+
* write is clamped) still wins through the snap+replay path.
|
|
107
|
+
*/
|
|
108
|
+
private replayOverAddEchoes;
|
|
109
|
+
private withoutOwnedUpdates;
|
|
110
|
+
/**
|
|
111
|
+
* An `add` for an instance this client will own, with its unflushed local field values folded
|
|
112
|
+
* back in. Returns `op` unchanged when there is nothing to preserve.
|
|
113
|
+
*/
|
|
114
|
+
private preserveLocalWrites;
|
|
115
|
+
/**
|
|
116
|
+
* Applies a `CORRECT`: snap the named fields to the server's values, then re-apply every local
|
|
117
|
+
* write newer than the judged `clientTick` in order (D19 snap + replay) — flushed writes from
|
|
118
|
+
* the retained buffer first, the still-unflushed in-window edit last (it is the newest, and its
|
|
119
|
+
* dirty mark survives so the next flush re-sends the *local* value, not the server's).
|
|
120
|
+
*
|
|
121
|
+
* Without a `clientTick` (a pre-week-8 server) — or when the correction outran the
|
|
122
|
+
* `RESIM_DEPTH` window (`snapped`) — the pre-D19 semantics apply: the correction wins in full
|
|
123
|
+
* and the local dirty marks it supersedes are cleared.
|
|
124
|
+
*/
|
|
125
|
+
applyCorrection(delta: Delta, clientTick?: number): CorrectionOp[];
|
|
126
|
+
private desc;
|
|
127
|
+
/** The raw plain collection — authoritative values plus local writes (`track()` writes through). */
|
|
128
|
+
plainCollection(name: string): EntityCollection;
|
|
129
|
+
/**
|
|
130
|
+
* The flushed-but-unjudged write patches for one instance, oldest first, each keyed by the
|
|
131
|
+
* tick its `WRITE` was stamped with — for a physics entity these are exactly its
|
|
132
|
+
* unacknowledged intent frames (only intents are writable). The predictor's rebase walks its
|
|
133
|
+
* resim window tick by tick and applies the newest patch stamped at or before each tick
|
|
134
|
+
* (D22 part 2), starting from `baselineIntent` for the ticks before the first of them.
|
|
135
|
+
*/
|
|
136
|
+
pendingWritePatches(collection: string, id: string): readonly {
|
|
137
|
+
tick: number;
|
|
138
|
+
patch: AnyRecord$3;
|
|
139
|
+
}[];
|
|
140
|
+
/**
|
|
141
|
+
* The intent in force for one instance at the start of the resim window: the newest fields
|
|
142
|
+
* that have left the pending buffer (judged into a correction's values, or evicted past the
|
|
143
|
+
* resim depth). `undefined` until the instance's first write leaves the buffer.
|
|
144
|
+
*/
|
|
145
|
+
baselineIntent(collection: string, id: string): AnyRecord$3 | undefined;
|
|
146
|
+
/** Folds a write that left the pending buffer into the per-instance baseline, newest wins. */
|
|
147
|
+
private noteBaseline;
|
|
148
|
+
/** Cheap check for the flush loop: has anything been written locally since the last flush? */
|
|
149
|
+
get hasLocalWrites(): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* The `WRITE` payload for everything dirty, or `undefined` when nothing qualifies. Consumes the
|
|
152
|
+
* dirty set either way (a write to an instance that has since been removed or handed away is
|
|
153
|
+
* dropped, not retried forever).
|
|
154
|
+
*/
|
|
155
|
+
takeWrite(tick: number): Uint8Array | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* Captures the field values a flushed `WRITE` carried, keyed by its write tick, so a later
|
|
158
|
+
* `CORRECT` can re-apply exactly the writes the server had not judged yet (D19 snap + replay).
|
|
159
|
+
* Bounded to `RESIM_DEPTH` entries; eviction is remembered so an outrun correction snaps.
|
|
160
|
+
*/
|
|
161
|
+
private retainPendingWrite;
|
|
162
|
+
/** Re-applies one retained write's fields for `collection[id]` onto the plain state. */
|
|
163
|
+
private replayWrite;
|
|
164
|
+
/**
|
|
165
|
+
* Re-marks every field of every instance this client owns, so a resync `WELCOME` (wake or
|
|
166
|
+
* worker restart) does not silently drop writes that were in flight.
|
|
167
|
+
*/
|
|
168
|
+
remarkOwned(): void;
|
|
169
|
+
/** Only update ops, only instances that still exist and are still mine, never the owner bit. */
|
|
170
|
+
private ownedDirty;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Client-side physics prediction (D22 part 2, D21): a local Rapier world that simulates this
|
|
175
|
+
* client's own bodies — and any `predicted: true` collection's bodies — ahead of the server.
|
|
176
|
+
*
|
|
177
|
+
* ## The contract
|
|
178
|
+
*
|
|
179
|
+
* The world is built from the **shared world-builder** the game passes to
|
|
180
|
+
* `joinRoom({ physics })`: the same `setup` (static geometry), `bodies` (shape factories) and
|
|
181
|
+
* `intents` (intent → force, per step) functions the room's config uses, imported by both sides
|
|
182
|
+
* from one module (`irtio/world.ts` by convention — a convention, not a mechanism). The builder
|
|
183
|
+
* must be pure over synced inputs: no `Math.random()`, no clock, seed/level params read from
|
|
184
|
+
* synced state — or the two worlds are not the same world.
|
|
185
|
+
*
|
|
186
|
+
* ## The loop
|
|
187
|
+
*
|
|
188
|
+
* The local world **free-runs** on a fixed-timestep accumulator driven by render reads: each
|
|
189
|
+
* step applies the owner's current intent values through the `intents` hook, then steps. When
|
|
190
|
+
* authoritative body state arrives (a `CORRECT` for an owned body, a `DELTA` for a non-owned
|
|
191
|
+
* predicted one), every predicted body is **rebased** to the server's values and the world
|
|
192
|
+
* re-steps the client's lead — one step per tick of estimated one-way latency, replaying the
|
|
193
|
+
* buffered unjudged intent frames oldest-first (the write buffer holds exactly the server's
|
|
194
|
+
* input frames — only intents are writable on a physics entity). The lead is bounded by the
|
|
195
|
+
* shared resimulation depth (20, D19): beyond it the body snaps to authority and the snap is
|
|
196
|
+
* counted.
|
|
197
|
+
*
|
|
198
|
+
* ## What the renderer sees
|
|
199
|
+
*
|
|
200
|
+
* Not the head. The head moves once per server tick — and only when an arrival rebases the world
|
|
201
|
+
* onto it — while a display asks two to four times as often, so handing back the head draws a
|
|
202
|
+
* character running at a steady nine units a second as standing still for half the frames and
|
|
203
|
+
* moving at twenty for the rest. `read()` therefore keeps a short ring of recent poses per body
|
|
204
|
+
* and draws between the two either side of a render clock that runs on wall time, steered to sit
|
|
205
|
+
* about a tick behind the head. Correctness is unaffected: `room.state`, the per-tick history a
|
|
206
|
+
* correction is judged against, and `predictedValues` all still see the head. See bug 7 in
|
|
207
|
+
* `docs/bugs.md` for the measurement.
|
|
208
|
+
*
|
|
209
|
+
* ## What is deliberately absent
|
|
210
|
+
*
|
|
211
|
+
* Non-predicted dynamic bodies do not exist in the local world, and neither do `predicted: true`
|
|
212
|
+
* instances that fall over `maxPredictedBodies`. A predicted body that collides with either on
|
|
213
|
+
* the server mispredicts here — it had nothing to collide with, so it passes through and is
|
|
214
|
+
* snapped back by the next correction. That is the documented constraint: predict both
|
|
215
|
+
* (`predicted: true`, within the cap) or interpolate both.
|
|
216
|
+
*
|
|
217
|
+
* The engine itself is loaded lazily via dynamic `import()`, so a game with no physics option —
|
|
218
|
+
* or a bundler that code-splits — pays nothing for it.
|
|
219
|
+
*/
|
|
220
|
+
|
|
221
|
+
type AnyRecord$2 = Record<string, unknown>;
|
|
222
|
+
type ClientRapierModule = typeof RAPIER;
|
|
223
|
+
type ClientRapierWorld = RAPIER.World;
|
|
224
|
+
type ClientRapierBody = RAPIER.RigidBody;
|
|
225
|
+
interface ClientVector3 {
|
|
226
|
+
readonly x: number;
|
|
227
|
+
readonly y: number;
|
|
228
|
+
readonly z: number;
|
|
229
|
+
}
|
|
230
|
+
/** What a client-side body factory returns — the same shape the room config's factories use. */
|
|
231
|
+
interface ClientBodySpec {
|
|
232
|
+
readonly body: RAPIER.RigidBodyDesc;
|
|
233
|
+
readonly colliders?: readonly RAPIER.ColliderDesc[];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Method-syntax members check bivariantly, so a builder's factory or intent hook written against
|
|
237
|
+
* its own instance type (`(body, ball: Ball) => …`) is accepted — the values really passed are
|
|
238
|
+
* the schema's records for that collection.
|
|
239
|
+
*/
|
|
240
|
+
type ClientBodyFactory = {
|
|
241
|
+
factory(rapier: ClientRapierModule, instance: AnyRecord$2, id: string): ClientBodySpec;
|
|
242
|
+
}['factory'];
|
|
243
|
+
type ClientIntentHook = {
|
|
244
|
+
hook(body: ClientRapierBody, instance: AnyRecord$2, rapier: ClientRapierModule, world: ClientRapierWorld): void;
|
|
245
|
+
}['hook'];
|
|
246
|
+
/**
|
|
247
|
+
* `joinRoom({ physics })` — the client half of the shared world-builder contract. Every function
|
|
248
|
+
* here should be the very export the room config imports, so "same code both sides" stays
|
|
249
|
+
* literally true.
|
|
250
|
+
*/
|
|
251
|
+
interface ClientPhysicsOptions {
|
|
252
|
+
/** Must equal the room config's gravity (put it in the shared module). */
|
|
253
|
+
readonly gravity: ClientVector3;
|
|
254
|
+
/** Seconds per step. Defaults to the room's tick interval (from `WELCOME`). */
|
|
255
|
+
readonly timestep?: number;
|
|
256
|
+
/** The shared static-geometry builder (the room's `physics.setup`). */
|
|
257
|
+
readonly setup?: (world: ClientRapierWorld, rapier: ClientRapierModule) => void;
|
|
258
|
+
/** Shape factories for the collections this client predicts (the room's `physics.bodies`). */
|
|
259
|
+
readonly bodies?: Readonly<Record<string, ClientBodyFactory>>;
|
|
260
|
+
/**
|
|
261
|
+
* Intent → force, applied before every predicted step for bodies this client owns — the same
|
|
262
|
+
* function the room's `tick()` calls per instance, shared so both simulations agree.
|
|
263
|
+
*/
|
|
264
|
+
readonly intents?: Readonly<Record<string, ClientIntentHook>>;
|
|
265
|
+
/**
|
|
266
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates. Over-cap instances
|
|
267
|
+
* are **absent from the local world** — they render by interpolation, but predicted bodies
|
|
268
|
+
* pass straight through them, so anything a predicted body stands on or is blocked by has to
|
|
269
|
+
* be under the cap (warned, counted as `stats.overCap`). Default 64.
|
|
270
|
+
*/
|
|
271
|
+
readonly maxPredictedBodies?: number;
|
|
272
|
+
/**
|
|
273
|
+
* A body-field correction whose every value is within this tolerance of the local prediction
|
|
274
|
+
* is *suppressed*: authority still applies, but it is not a misprediction — steady state stays
|
|
275
|
+
* quiet. Positions compare against `epsilon` world units directly; velocity channels compare
|
|
276
|
+
* against `epsilon / timestep` (a velocity disagreement matters by what it moves in one tick —
|
|
277
|
+
* an input frame landing one tick late on the server is invisible, not a storm). Default 0.05.
|
|
278
|
+
*/
|
|
279
|
+
readonly epsilon?: number;
|
|
280
|
+
/**
|
|
281
|
+
* How fast the drawn position eases back onto the simulation after a re-simulation moved it,
|
|
282
|
+
* as a half-life in milliseconds. `0` turns the smoothing off. Default 70.
|
|
283
|
+
*
|
|
284
|
+
* A rebase can move a body that has already been drawn — because the server disagreed, or
|
|
285
|
+
* because a newly-flushed intent changed what the last few ticks should have been. Handing
|
|
286
|
+
* that straight to the renderer is a step, and a step in the middle of steady motion is what
|
|
287
|
+
* a player calls a yank. Instead the jump is taken out of the drawn pose and put into a
|
|
288
|
+
* per-body offset that decays: the character keeps moving smoothly and arrives at the truth a
|
|
289
|
+
* moment later.
|
|
290
|
+
*
|
|
291
|
+
* This smooths the **error**, not the motion. A rate limiter on the drawn position — the
|
|
292
|
+
* obvious version, and the one a game writes for itself — cannot tell a correction from the
|
|
293
|
+
* character running, so it lags real movement too. Nothing here touches motion the simulation
|
|
294
|
+
* actually produced.
|
|
295
|
+
*/
|
|
296
|
+
readonly smoothingHalfLifeMs?: number;
|
|
297
|
+
/**
|
|
298
|
+
* How far the drawn position may be held from the simulation while an offset eases away, in
|
|
299
|
+
* world units. Past it the offset is dropped and the body appears where it is. Default 4.
|
|
300
|
+
*
|
|
301
|
+
* This is the teleport case: a respawn, an area change, a resync. Easing across one of those
|
|
302
|
+
* draws the body sliding through the level, which is worse than the step it avoids. It is the
|
|
303
|
+
* one number here that depends on how big a world unit is in your game.
|
|
304
|
+
*/
|
|
305
|
+
readonly smoothingSnapUnits?: number;
|
|
306
|
+
}
|
|
307
|
+
/** Counters for the report and `room.prediction.stats`. */
|
|
308
|
+
interface PredictionStats {
|
|
309
|
+
/** Steps taken free-running ahead of authority. */
|
|
310
|
+
freeSteps: number;
|
|
311
|
+
/** Steps taken re-simulating after a rebase. */
|
|
312
|
+
resimSteps: number;
|
|
313
|
+
/** Rebase passes (one per authoritative arrival batch). */
|
|
314
|
+
rebases: number;
|
|
315
|
+
/** Rebases whose lead outran the resim depth: the body snapped to authority. */
|
|
316
|
+
snaps: number;
|
|
317
|
+
/** Corrections whose values matched the local prediction within epsilon. */
|
|
318
|
+
suppressed: number;
|
|
319
|
+
/**
|
|
320
|
+
* Non-owned predicted instances currently over the cap. They render by interpolation but have
|
|
321
|
+
* no body in the local world, so predicted bodies pass through them: a non-zero value on a
|
|
322
|
+
* collection anything stands on is a gameplay bug, not a fidelity tradeoff.
|
|
323
|
+
*/
|
|
324
|
+
overCap: number;
|
|
325
|
+
/** Microseconds spent in the last rebase's re-steps. */
|
|
326
|
+
lastResimMicros: number;
|
|
327
|
+
/**
|
|
328
|
+
* The largest render-error offset currently being eased away, in world units — how far the
|
|
329
|
+
* furthest-off predicted body is being drawn from where the simulation puts it, so that a
|
|
330
|
+
* re-simulation did not arrive as a step (`smoothingHalfLifeMs`). It rises on a correction and
|
|
331
|
+
* falls back towards zero; what is unhealthy is a value that *stays* up, which means the
|
|
332
|
+
* client is being corrected as fast as it can absorb it.
|
|
333
|
+
*/
|
|
334
|
+
smoothing: number;
|
|
335
|
+
/**
|
|
336
|
+
* Ticks between the stamp a `WRITE` carries and the server tick it was applied at, smoothed
|
|
337
|
+
* (bug 1). The client stamps on its predicted clock, which leads authority, so this is how far
|
|
338
|
+
* *early* a naive replay would put every pending intent. 0 until a server that reports the
|
|
339
|
+
* applied tick judges a stamped write.
|
|
340
|
+
*/
|
|
341
|
+
stampGap: number;
|
|
342
|
+
}
|
|
343
|
+
declare class PhysicsPredictor {
|
|
344
|
+
private readonly store;
|
|
345
|
+
private readonly options;
|
|
346
|
+
private readonly meOf;
|
|
347
|
+
private readonly rttOf;
|
|
348
|
+
private readonly tickIntervalOf;
|
|
349
|
+
private readonly log;
|
|
350
|
+
readonly stats: PredictionStats;
|
|
351
|
+
private rapier;
|
|
352
|
+
private world;
|
|
353
|
+
private readonly bodies;
|
|
354
|
+
/** Physics-backed entity collections, in schema order. */
|
|
355
|
+
private readonly collections;
|
|
356
|
+
private readonly warned;
|
|
357
|
+
/** Highest over-cap count warned about per collection, so growth re-warns and noise does not. */
|
|
358
|
+
private readonly overCapHigh;
|
|
359
|
+
/** Authority arrived since the last frame: rebase before free-running. */
|
|
360
|
+
private authorityDirty;
|
|
361
|
+
private accumulatorMs;
|
|
362
|
+
private lastNow;
|
|
363
|
+
private lastFrameNow;
|
|
364
|
+
private failed;
|
|
365
|
+
/**
|
|
366
|
+
* The local world's head, on the server's tick timeline: the authoritative tick the last
|
|
367
|
+
* rebase started from plus the lead it re-stepped, then one per free-run step.
|
|
368
|
+
*/
|
|
369
|
+
private headTick;
|
|
370
|
+
/** The newest server tick any authority has been seen for. `headTick`'s base. */
|
|
371
|
+
private authorityTick;
|
|
372
|
+
/** The newest tick a pose has been stored for. `-1` until the first capture. */
|
|
373
|
+
private curTick;
|
|
374
|
+
/**
|
|
375
|
+
* Where the renderer is, as a fractional tick. Advanced by wall time every frame and steered
|
|
376
|
+
* to sit about a tick behind `curTick`; `read()` draws between the stored poses either side
|
|
377
|
+
* of it.
|
|
378
|
+
*/
|
|
379
|
+
private renderTick;
|
|
380
|
+
/** Scratch for one interpolated pose. `read()` is synchronous, so one is enough. */
|
|
381
|
+
private readonly scratch;
|
|
382
|
+
/** Scratch quaternions, so the per-body per-frame offset maths allocates nothing. */
|
|
383
|
+
private readonly qa;
|
|
384
|
+
private readonly qb;
|
|
385
|
+
private readonly qc;
|
|
386
|
+
/**
|
|
387
|
+
* Per owned body: the server tick its authoritative record is *from* (the tick of the last
|
|
388
|
+
* `CORRECT` that touched it), and the per-tick prediction history — after every local step,
|
|
389
|
+
* the body's channels are recorded under the tick that step predicted. A correction for tick T
|
|
390
|
+
* is then judged against the client's prediction **for tick T**, not against the current head
|
|
391
|
+
* of the simulation (which legitimately leads authority by the whole latency — comparing
|
|
392
|
+
* against it would report the lead as misprediction).
|
|
393
|
+
*/
|
|
394
|
+
private readonly authorityTicks;
|
|
395
|
+
/** Smoothed stamp-minus-applied gap; see `noteWriteApplied`. */
|
|
396
|
+
private stampGap;
|
|
397
|
+
private gapMeasured;
|
|
398
|
+
/** The newest stamp already folded into `stampGap`, so a repeated echo is not re-weighted. */
|
|
399
|
+
private gapSampledThrough;
|
|
400
|
+
private readonly predictedTicks;
|
|
401
|
+
private readonly history;
|
|
402
|
+
/** History kept per body — comfortably past the resim depth. */
|
|
403
|
+
private static readonly HISTORY_TICKS;
|
|
404
|
+
constructor(ext: AnySchema, store: ClientStore, options: ClientPhysicsOptions, meOf: () => string, rttOf: () => number, tickIntervalOf: () => number, log?: (message: string) => void);
|
|
405
|
+
get epsilon(): number;
|
|
406
|
+
/** `true` once the engine is loaded and the local world exists. */
|
|
407
|
+
get ready(): boolean;
|
|
408
|
+
/**
|
|
409
|
+
* Kicks off the async engine load. Idempotent. Until it resolves, every read falls back to
|
|
410
|
+
* interpolation/authority — joining is never blocked on 2.9 MB of WASM.
|
|
411
|
+
*/
|
|
412
|
+
start(): Promise<void>;
|
|
413
|
+
free(): void;
|
|
414
|
+
/** A resync (`WELCOME`) replaced the authority wholesale: rebase everything, drop banked time. */
|
|
415
|
+
reset(): void;
|
|
416
|
+
/**
|
|
417
|
+
* Authoritative body state arrived (`DELTA` on a non-owned body, `CORRECT` on an owned one).
|
|
418
|
+
*
|
|
419
|
+
* `tick` is the server tick it carried, and it is the base of the predictor's own head clock —
|
|
420
|
+
* which the renderer's interpolation is paced against. It has to be the *world's* clock rather
|
|
421
|
+
* than any one body's: a client with nothing of its own to own still draws a world full of
|
|
422
|
+
* predicted bodies.
|
|
423
|
+
*/
|
|
424
|
+
noteAuthority(tick?: number): void;
|
|
425
|
+
/** The session tells us which server tick one body's authoritative record is from. */
|
|
426
|
+
noteAuthorityTick(collection: string, id: string, tick: number): void;
|
|
427
|
+
/**
|
|
428
|
+
* A `CORRECT` reported both halves of a judged write: the stamp the client put on it and the
|
|
429
|
+
* server tick it was actually applied at (bug 1). The gap between them is the systematic error
|
|
430
|
+
* in the rebase's replay, and it is not zero even on a loopback: the stamp is the predictor's
|
|
431
|
+
* *head* (authority + lead), while the server applies on arrival, at a tick barely past the
|
|
432
|
+
* authority the client is replaying from. Replaying at the stamp therefore held the previous
|
|
433
|
+
* intent for `gap` ticks the server had already simulated under the new one — the overshoot on
|
|
434
|
+
* every key release, sized `gap x speed`.
|
|
435
|
+
*
|
|
436
|
+
* Smoothed, because it rides on delivery jitter and the measurement is one sample per judged
|
|
437
|
+
* write. Clamped into `[0, RESIM_DEPTH]`: a stamp taken before this client had any physics
|
|
438
|
+
* authority is on the session's bare write counter rather than the server's tick stream, and
|
|
439
|
+
* differencing the two clocks is meaningless — 0 is the old behaviour and the honest default.
|
|
440
|
+
*/
|
|
441
|
+
noteWriteApplied(stampTick: number, appliedTick: number): void;
|
|
442
|
+
/** Does the local world currently simulate `collection[id]`? */
|
|
443
|
+
has(collection: string, id: string): boolean;
|
|
444
|
+
/**
|
|
445
|
+
* Is a correction's every value within the suppression tolerance of the prediction it judges?
|
|
446
|
+
* Position (and rotation) channels compare against `epsilon` directly; velocity and angular
|
|
447
|
+
* channels against `epsilon / timestep`, because a velocity disagreement matters by what it
|
|
448
|
+
* moves in one tick.
|
|
449
|
+
*/
|
|
450
|
+
withinEpsilon(desc: CollectionDesc, fields: readonly string[], patch: AnyRecord$2, predicted: AnyRecord$2): boolean;
|
|
451
|
+
/** Is `collection` one this client would predict at all (owned always; non-owned per D21)? */
|
|
452
|
+
predictsCollection(name: string): boolean;
|
|
453
|
+
/**
|
|
454
|
+
* Advances the local world to `now`. Driven by render reads (one pass per timestamp), so a
|
|
455
|
+
* draw loop — or a bot's render sampling — is the clock; there is no timer.
|
|
456
|
+
*/
|
|
457
|
+
frame(now: number): void;
|
|
458
|
+
/**
|
|
459
|
+
* The head moved: store every predicted body's pose under the tick it now holds.
|
|
460
|
+
*
|
|
461
|
+
* A tick can be simulated more than once — a `DELTA` and a `CORRECT` both arrive for the same
|
|
462
|
+
* tick and each rebases — so the slot is simply rewritten with the better answer. It can also
|
|
463
|
+
* move backwards, when a free-run step is discarded by the rebase behind it; `curTick` does
|
|
464
|
+
* not follow it down, because moving the newest label backwards would drag the segment the
|
|
465
|
+
* renderer is crossing backwards with it.
|
|
466
|
+
*/
|
|
467
|
+
private capturePoses;
|
|
468
|
+
/**
|
|
469
|
+
* Advance the render clock by one frame of wall time, steering it towards a target distance
|
|
470
|
+
* behind the head rather than clamping it there.
|
|
471
|
+
*
|
|
472
|
+
* `renderTick` and `curTick` agree on rate — one tick per tick period — but not on phase, and
|
|
473
|
+
* the phase error is delivery jitter: authority arrives when the network hands it over, not
|
|
474
|
+
* every 33 ms however steady the server is. Left alone the two would wander apart, so the
|
|
475
|
+
* clock is steered, by rate rather than by position. What the network did to a packet is not
|
|
476
|
+
* something the player's character should be seen doing.
|
|
477
|
+
*/
|
|
478
|
+
private advanceRenderClock;
|
|
479
|
+
/**
|
|
480
|
+
* Keep the render clock inside the span there are poses for.
|
|
481
|
+
*
|
|
482
|
+
* Both ends are real. At `curTick` there is nothing further to draw towards, so a late arrival
|
|
483
|
+
* holds the pose rather than extrapolating motion the next tick would have to take back — the
|
|
484
|
+
* same trade that sized the prediction lead (bug 1). At the far end the ring has recycled the
|
|
485
|
+
* slot and the pose is genuinely gone. Between them the clock floats, and the rate trim above
|
|
486
|
+
* is what keeps it from spending its time against either stop.
|
|
487
|
+
*/
|
|
488
|
+
private holdRenderTick;
|
|
489
|
+
/** `smoothingHalfLifeMs`, or 0 when the game turned the smoothing off. */
|
|
490
|
+
private halfLifeMs;
|
|
491
|
+
/** Remember where every body is being drawn, before this frame re-simulates anything. */
|
|
492
|
+
private snapshotDrawn;
|
|
493
|
+
/**
|
|
494
|
+
* Take whatever the re-simulation moved the drawn pose by and put it in the offset instead.
|
|
495
|
+
*
|
|
496
|
+
* This is the whole of correction smoothing, and it is smoothing the *error*: the difference
|
|
497
|
+
* measured here is between two answers to the same question — where is this body at the render
|
|
498
|
+
* clock — asked either side of a rebase. Motion the simulation produced is not in it, because
|
|
499
|
+
* the render clock advanced before the snapshot. So real movement is never slowed, which is
|
|
500
|
+
* the thing a rate limiter on the drawn position cannot promise: it sees a position that moved
|
|
501
|
+
* and has no way to know whether the body ran or was corrected.
|
|
502
|
+
*
|
|
503
|
+
* It absorbs more than server disagreement. A newly flushed intent changes what the last few
|
|
504
|
+
* ticks should have been, and the replay hands that over as a jump the same way; so does the
|
|
505
|
+
* render clock being pulled back inside the poses it has. All of it is the same defect from
|
|
506
|
+
* the player's side — the character was drawn somewhere it now turns out it was not — and all
|
|
507
|
+
* of it eases away here.
|
|
508
|
+
*/
|
|
509
|
+
private absorbJump;
|
|
510
|
+
/**
|
|
511
|
+
* Ease every offset towards zero. Exponential, so the rate is proportional to the error and
|
|
512
|
+
* there is no world-scale speed to pick: a correction worth a tenth of a unit is worked off
|
|
513
|
+
* gently and one worth a whole unit is not left hanging around for a second.
|
|
514
|
+
*/
|
|
515
|
+
private decayOffsets;
|
|
516
|
+
private clearOffset;
|
|
517
|
+
/**
|
|
518
|
+
* The pose to draw one body at: the stored poses either side of the render clock, blended.
|
|
519
|
+
* Falls back to the body's live transform when the ring cannot bracket it — before the first
|
|
520
|
+
* capture, and for a body that appeared this frame.
|
|
521
|
+
*/
|
|
522
|
+
private rawPose;
|
|
523
|
+
/** `rawPose` with the render-error offset applied — what the draw loop actually gets. */
|
|
524
|
+
private renderPose;
|
|
525
|
+
/**
|
|
526
|
+
* The predicted record for `collection[id]`: the authoritative record (which already carries
|
|
527
|
+
* local intent writes — `track()` writes through to plain state) with the body-mapped channels
|
|
528
|
+
* replaced by the local world's values, `Math.fround`ed for f32 fields so what the draw loop
|
|
529
|
+
* reads is what the wire would carry.
|
|
530
|
+
*
|
|
531
|
+
* Drawn *between* two simulated ticks, not at the head. The local world steps at the room's
|
|
532
|
+
* tick rate, and only when authority arrives to rebase it; a display runs at two to four times
|
|
533
|
+
* that and asks on every frame. Handing back the head means the answer changes on some frames
|
|
534
|
+
* and not others, so a character running at a steady nine units a second is drawn standing
|
|
535
|
+
* still for half of them and moving at twenty for the rest. It is the player's own character
|
|
536
|
+
* answering their own key, and it reads as the game hitching.
|
|
537
|
+
*
|
|
538
|
+
* This is a *render* read. `room.state` and every correctness path — `predictedValues`, the
|
|
539
|
+
* per-tick history a correction is judged against — still see the head exactly as before.
|
|
540
|
+
*/
|
|
541
|
+
read(desc: CollectionDesc, id: string, base: AnyRecord$2): AnyRecord$2;
|
|
542
|
+
/**
|
|
543
|
+
* The local world's current values for `fields` of one body — what a correction is judged
|
|
544
|
+
* against (`previous` on the `correct` event, and the epsilon-suppression comparison).
|
|
545
|
+
*/
|
|
546
|
+
predictedValues(desc: CollectionDesc, id: string, fields: readonly string[], atTick?: number): AnyRecord$2 | undefined;
|
|
547
|
+
/**
|
|
548
|
+
* Mirrors the local world's bodies onto the instances this client predicts: every physics
|
|
549
|
+
* instance it owns, plus non-owned instances of `predicted: true` collections up to the cap
|
|
550
|
+
* (collection order, then insertion order — the same stated iteration guarantee the server
|
|
551
|
+
* follows, so which bodies fall over the cap is deterministic).
|
|
552
|
+
*
|
|
553
|
+
* Everything else — non-`predicted` collections, and `predicted` instances over the cap — gets
|
|
554
|
+
* no body and no collider here. Those instances still render (the interpolation path reads
|
|
555
|
+
* authoritative state directly), but nothing in the local world can touch them.
|
|
556
|
+
*/
|
|
557
|
+
private reconcileBodies;
|
|
558
|
+
private createBody;
|
|
559
|
+
/**
|
|
560
|
+
* Over-cap is not a one-time tuning notice: it means those instances are missing from the local
|
|
561
|
+
* world right now, so a predicted body walks through them. Warn on every new high-water mark
|
|
562
|
+
* per collection — a game that grows past the cap mid-session hears about it, and a count that
|
|
563
|
+
* oscillates around one level does not turn the console into a log.
|
|
564
|
+
*/
|
|
565
|
+
private warnOverCap;
|
|
566
|
+
private warnOnce;
|
|
567
|
+
/**
|
|
568
|
+
* The client's lead over authority, in ticks: the one-way transit the authority in hand has
|
|
569
|
+
* already spent in flight, rounded to the nearest tick. Nothing more.
|
|
570
|
+
*
|
|
571
|
+
* Every tick of lead beyond that transit is a tick of motion the client draws on the
|
|
572
|
+
* assumption that the input it is holding will still be held when the server gets there. On a
|
|
573
|
+
* release that assumption is wrong by construction, and the invented travel is handed back as
|
|
574
|
+
* a backwards yank — bug 1's "keeps moving after key up and then snaps back". It was
|
|
575
|
+
* `ceil(owd) + 1`, which on a fast link is two whole ticks of margin over a transit of nearly
|
|
576
|
+
* zero. Measured on the arena fixture at no injected latency (release overshoot of a 6 u/s
|
|
577
|
+
* held-input character, `physics-predict.test.ts`):
|
|
578
|
+
*
|
|
579
|
+
* | lead | drawn overshoot past the server's stop | worst backwards step |
|
|
580
|
+
* | --- | --- | --- |
|
|
581
|
+
* | `ceil(owd) + 1` (was) | 0.585 u | -0.585 u |
|
|
582
|
+
* | `max(1, round(owd))` (is) | 0.293 u | -0.292 u |
|
|
583
|
+
* | `round(owd)`, floor 0 | 0.0008 u | -0.0008 u |
|
|
584
|
+
*
|
|
585
|
+
* One tick of running is 0.3 u there: the overshoot is the lead, in ticks, and nothing else.
|
|
586
|
+
* On a link with no transit to cover, every tick of lead is a tick of guessing that the key is
|
|
587
|
+
* still held.
|
|
588
|
+
*
|
|
589
|
+
* The floor of 1 is not margin, it is the smallest lead that is still prediction. At 0 the
|
|
590
|
+
* rebase takes no steps: the local world is pinned to the authority it just received, which is
|
|
591
|
+
* already a tick old, so the client responds to its own input only when the next `CORRECT`
|
|
592
|
+
* arrives and every moving body reads as mispredicted by one tick of motion. Measured: the
|
|
593
|
+
* bot suites' correction-storm invariant fires immediately (21 corrections/s per bot against a
|
|
594
|
+
* threshold of 5 in `packages/cli/test/simulate-physics.test.ts`). One tick of speculation is
|
|
595
|
+
* the price of predicting at all; two was the bug.
|
|
596
|
+
*
|
|
597
|
+
* Erring low is also the cheap direction — a lead shorter than the transit means authority
|
|
598
|
+
* arrives slightly *ahead* of the prediction, and a correction that pulls the character the way
|
|
599
|
+
* it is already going is the one nobody can see.
|
|
600
|
+
*
|
|
601
|
+
* Measured earlier the same day (dive e2e): a *full*-round-trip lead was worse again
|
|
602
|
+
* (-1.4 to -1.8 u worst backwards step vs -0.6 at half), which is the same finding from the
|
|
603
|
+
* other end. `stats.stampGap` is the running check on all of this — it reports how far ahead
|
|
604
|
+
* of the server's application the stamps still land, and it should sit at 0.
|
|
605
|
+
*/
|
|
606
|
+
private leadTicks;
|
|
607
|
+
private timestepMs;
|
|
608
|
+
/**
|
|
609
|
+
* Rebase + re-step: snap every predicted body to the authoritative record (server values —
|
|
610
|
+
* `DELTA` for non-owned bodies, `CORRECT` for owned ones; body fields are never client-
|
|
611
|
+
* written, so plain state holds exactly what the server said), then re-step the world by the
|
|
612
|
+
* client's lead, applying to each re-stepped tick the intent that was in force *at that tick*:
|
|
613
|
+
* the newest buffered unjudged write stamped at or before it, or the baseline (the newest
|
|
614
|
+
* judged write) before the first of them. Bounded by the shared resim depth: an outrun lead
|
|
615
|
+
* snaps to authority and counts (`stats.snaps`).
|
|
616
|
+
*/
|
|
617
|
+
private rebase;
|
|
618
|
+
/**
|
|
619
|
+
* The tick a `WRITE` flushed right now should be stamped with: one past the newest predicted
|
|
620
|
+
* head across owned bodies — the first tick the new intent can affect. That puts the stamp on
|
|
621
|
+
* the same clock as `authorityTicks` and the resim window (the server's tick stream), which is
|
|
622
|
+
* what lets the rebase walk pending writes by tick instead of by array index. `undefined`
|
|
623
|
+
* until the engine runs or while nothing is owned; the session falls back to its counter.
|
|
624
|
+
*/
|
|
625
|
+
stampTick(): number | undefined;
|
|
626
|
+
/** After every step: advance each owned body's prediction clock and remember its channels. */
|
|
627
|
+
private recordStep;
|
|
628
|
+
/** Returns the number of steps taken, so the caller knows whether the head moved. */
|
|
629
|
+
private freeRun;
|
|
630
|
+
/**
|
|
631
|
+
* Applies intents for every owned predicted body before one step: the intent in force at the
|
|
632
|
+
* tick this step predicts (during a rebase — see `rebase`'s replay walk), else the instance's
|
|
633
|
+
* current values (free-run: plain state carries the newest local intent writes, which is
|
|
634
|
+
* correct there because free-run steps are the ticks *after* every buffered write).
|
|
635
|
+
*/
|
|
636
|
+
private applyIntents;
|
|
637
|
+
/** Server record → body channels (the same mapping the runtime's sync uses, inverted). */
|
|
638
|
+
private applyRecord;
|
|
639
|
+
private isF32;
|
|
640
|
+
}
|
|
641
|
+
|
|
13
642
|
/**
|
|
14
643
|
* A client-side entity collection: the read API of `@irtio/schema`'s `ReadonlyCollection`, plus
|
|
15
644
|
* index sugar so `state.players[room.me]` reads the same as `state.players.get(room.me)`.
|
|
@@ -56,6 +685,11 @@ interface Correction {
|
|
|
56
685
|
readonly fields: readonly string[];
|
|
57
686
|
/** The server's values for those fields (already applied to `room.state`). */
|
|
58
687
|
readonly patch: Readonly<Record<string, unknown>>;
|
|
688
|
+
/**
|
|
689
|
+
* The local (predicted) values of `fields` right before the snap. Misprediction magnitude is
|
|
690
|
+
* the numeric distance between `previous` and `patch`, per field.
|
|
691
|
+
*/
|
|
692
|
+
readonly previous: Readonly<Record<string, unknown>>;
|
|
59
693
|
readonly tick: number;
|
|
60
694
|
/**
|
|
61
695
|
* The client write tick the server judged (D19, week 8): every local write through it is
|
|
@@ -70,6 +704,19 @@ interface Correction {
|
|
|
70
704
|
* writes) — everything it named snapped to the server's values, nothing was replayed.
|
|
71
705
|
*/
|
|
72
706
|
readonly snapped: boolean;
|
|
707
|
+
/**
|
|
708
|
+
* D22: every corrected field is a simulation-owned body field this client does **not**
|
|
709
|
+
* predict — not a disagreement, just server-authoritative body state reaching its owner (once
|
|
710
|
+
* per tick, by design). When the client predicts the body, this is `false` and the correction
|
|
711
|
+
* is a real misprediction: `previous` holds the local world's predicted values.
|
|
712
|
+
*/
|
|
713
|
+
readonly simulation: boolean;
|
|
714
|
+
/**
|
|
715
|
+
* D22 part 2: a predicted body's correction whose every value matched the local prediction
|
|
716
|
+
* within the epsilon — authority applied, but there was nothing to disagree about. Suppressed
|
|
717
|
+
* corrections keep a healthy predicted body's steady state quiet.
|
|
718
|
+
*/
|
|
719
|
+
readonly suppressed: boolean;
|
|
73
720
|
}
|
|
74
721
|
interface RoomEvents {
|
|
75
722
|
status: Status;
|
|
@@ -134,9 +781,30 @@ interface JoinOptions<S, Role extends string = string> {
|
|
|
134
781
|
* (which is what `irtio dev` uses when there is no `irtio.json`).
|
|
135
782
|
*/
|
|
136
783
|
readonly key?: string;
|
|
784
|
+
/**
|
|
785
|
+
* Week 13 (D27): a JWT asserting who this player is, minted by your server with the project's
|
|
786
|
+
* signing secret. `ctx.playerId` becomes the token's `sub`, and a `role` claim overrides
|
|
787
|
+
* `role` here. A function is called before every HELLO — reconnects included — so it can hand
|
|
788
|
+
* back a fresh token when the old one nears expiry. Omitted ⇒ the key-only join, unchanged.
|
|
789
|
+
*/
|
|
790
|
+
readonly token?: string | (() => string | Promise<string>);
|
|
137
791
|
readonly onStatus?: (status: Status) => void;
|
|
138
792
|
/** Hard cap on the owned-write flush window, in ms. Default 50. */
|
|
139
793
|
readonly writeIntervalMs?: number;
|
|
794
|
+
/**
|
|
795
|
+
* How far behind arrival `room.render` draws non-owned entities, in ms (D20). Default:
|
|
796
|
+
* `2 × tickIntervalMs` (the room's tick interval, learned from `WELCOME`), floored at 50 ms.
|
|
797
|
+
*/
|
|
798
|
+
readonly interpDelayMs?: number;
|
|
799
|
+
/**
|
|
800
|
+
* D22 part 2: the client half of the shared world-builder contract — the same `setup`,
|
|
801
|
+
* `bodies` and `intents` functions the room's `physics` config imports (by convention from
|
|
802
|
+
* `irtio/world.ts`), plus `gravity`. With it, this client simulates its own bodies (and any
|
|
803
|
+
* `predicted: true` collection's, capped) ahead in a local world; without it, body-backed
|
|
804
|
+
* entities interpolate like everything else. The engine loads lazily — a bundler that
|
|
805
|
+
* code-splits keeps Rapier out of the critical path entirely.
|
|
806
|
+
*/
|
|
807
|
+
readonly physics?: ClientPhysicsOptions;
|
|
140
808
|
/** @internal */
|
|
141
809
|
readonly transport?: Transport;
|
|
142
810
|
/** @internal */
|
|
@@ -163,6 +831,41 @@ interface JoinRelayOptions {
|
|
|
163
831
|
type MessageTarget = 'all' | string | {
|
|
164
832
|
readonly role: string;
|
|
165
833
|
};
|
|
834
|
+
/**
|
|
835
|
+
* Default D21 cap on non-owned predicted bodies per client.
|
|
836
|
+
*
|
|
837
|
+
* Sized against measurement, not caution: 41 dynamic bodies stacked in contact step in 0.005 to
|
|
838
|
+
* 0.007 ms (`games/dive/spike`, 2026-08-26), and a rebase re-steps the local world once per lead
|
|
839
|
+
* tick per authoritative tick — about 0.5 ms per second of wall clock at 30Hz with a 3-tick lead.
|
|
840
|
+
* Body count is not what makes prediction expensive, and the cost of a cap set too low is not
|
|
841
|
+
* saved CPU: over-cap instances are **absent from the local world** (see `reconcileBodies`), so
|
|
842
|
+
* a predicted body passes through them.
|
|
843
|
+
*/
|
|
844
|
+
declare const MAX_PREDICTED_BODIES = 64;
|
|
845
|
+
/** Default correction-suppression epsilon, world units (see `ClientPhysicsOptions.epsilon`). */
|
|
846
|
+
declare const PREDICTION_EPSILON = 0.05;
|
|
847
|
+
/**
|
|
848
|
+
* Default half-life for render-error smoothing, milliseconds (see
|
|
849
|
+
* `ClientPhysicsOptions.smoothingHalfLifeMs`). About two ticks at 30Hz: long enough that a
|
|
850
|
+
* correction is a settle rather than a step, short enough that the drawn position is back on the
|
|
851
|
+
* simulation within a fifth of a second.
|
|
852
|
+
*/
|
|
853
|
+
declare const SMOOTHING_HALF_LIFE_MS = 70;
|
|
854
|
+
/**
|
|
855
|
+
* Default distance past which a render-error offset is dropped rather than eased away, world
|
|
856
|
+
* units (see `ClientPhysicsOptions.smoothingSnapUnits`). A respawn, an area change or a session
|
|
857
|
+
* resync moves a body somewhere else entirely, and sliding into it is worse than arriving.
|
|
858
|
+
* Four units is a guess at world scale and the one number here a game may need to change.
|
|
859
|
+
*/
|
|
860
|
+
declare const SMOOTHING_SNAP_UNITS = 4;
|
|
861
|
+
/** `room.prediction` (D22 part 2). */
|
|
862
|
+
interface PredictionStatus {
|
|
863
|
+
/** The engine is loaded and the local world exists. */
|
|
864
|
+
readonly active: boolean;
|
|
865
|
+
/** Is `collection[id]` currently simulated in the local world? */
|
|
866
|
+
predicts(collection: string, id: string): boolean;
|
|
867
|
+
readonly stats: PredictionStats;
|
|
868
|
+
}
|
|
166
869
|
/**
|
|
167
870
|
* `room.call.<rpc>(params)` — every server RPC the builder declared, plus the one built-in.
|
|
168
871
|
* `requestOwnership` takes the entity/id pair positionally and unwraps `{ granted }`.
|
|
@@ -176,7 +879,10 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
|
176
879
|
readonly me: string;
|
|
177
880
|
/** The room code (from `WELCOME`). */
|
|
178
881
|
readonly id: string;
|
|
179
|
-
/**
|
|
882
|
+
/**
|
|
883
|
+
* Shareable URL carrying `?room=<id>`, with per-client params (`role`) stripped so it is safe
|
|
884
|
+
* to hand to somebody else. The address bar keeps them.
|
|
885
|
+
*/
|
|
180
886
|
readonly link: string;
|
|
181
887
|
/** The last server tick this client saw. */
|
|
182
888
|
readonly tick: number;
|
|
@@ -184,8 +890,25 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
|
184
890
|
/** Round-trip time in ms from the last `PING`/`PONG`, or 0 before the first one. */
|
|
185
891
|
readonly rtt: number;
|
|
186
892
|
readonly state: ClientState<S, Role>;
|
|
893
|
+
/**
|
|
894
|
+
* The render read path (D20): same shapes as `room.state`, but non-owned entities are
|
|
895
|
+
* interpolated at `now − interpDelayMs` (numeric fields lerp, everything else steps) while
|
|
896
|
+
* predicted bodies are drawn between the two most recent ticks of the local physics world.
|
|
897
|
+
* Either way the value advances with the frame clock rather than with arrivals or with the
|
|
898
|
+
* tick rate. A draw loop opts in by one substitution —
|
|
899
|
+
* `room.state.players` → `room.render.players`. Tests and game logic keep reading
|
|
900
|
+
* `room.state`, which stays authoritative.
|
|
901
|
+
*/
|
|
902
|
+
readonly render: ClientState<S, Role>;
|
|
187
903
|
/** Built-in presence, ordered by join. */
|
|
188
904
|
readonly clients: readonly PresenceRecord[];
|
|
905
|
+
/**
|
|
906
|
+
* Physics-prediction status, present when the join passed `physics` and the schema has
|
|
907
|
+
* body-backed collections (D22 part 2). `active` flips true once the engine loads; `predicts`
|
|
908
|
+
* says whether one instance currently reads from the local world; `stats` carries the
|
|
909
|
+
* counters the bot runtime and the demos report.
|
|
910
|
+
*/
|
|
911
|
+
readonly prediction?: PredictionStatus;
|
|
189
912
|
readonly call: RoomCallProxy<S>;
|
|
190
913
|
/** Convenience alias for `room.call.requestOwnership`. */
|
|
191
914
|
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
@@ -260,141 +983,60 @@ declare function defaultScheduler(): Scheduler;
|
|
|
260
983
|
declare const webSocketTransport: Transport;
|
|
261
984
|
|
|
262
985
|
/**
|
|
263
|
-
* The
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* dirty set the flush window turns into one `WRITE`;
|
|
271
|
-
* - **everything else** → `frozenProxy`, which ignores writes and warns once with the ownership
|
|
272
|
-
* hint.
|
|
986
|
+
* The interpolated render read path (D20, week 8): `room.render.<collection>` mirrors
|
|
987
|
+
* `room.state`'s shapes, but non-owned entities are rendered at `now − interpDelayMs`, lerping
|
|
988
|
+
* numeric fields between the two bracketing DELTAs and stepping everything else (strings, bools,
|
|
989
|
+
* enums, refs, lists). Predicted bodies — owned or `predicted: true` — come from the local
|
|
990
|
+
* physics world instead, drawn between the two most recent simulated ticks rather than at the
|
|
991
|
+
* simulation head, so they too advance with the frame clock and not with whatever rate their
|
|
992
|
+
* source happens to update at (`physics.ts`, `read`). One read path serves the whole draw loop.
|
|
273
993
|
*
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
* The server-wins rule lives here too: a `DELTA`'s update ops are dropped field-wise for
|
|
279
|
-
* instances this client owns (adds, removes and owner changes still apply), while a `CORRECT`
|
|
280
|
-
* applies in full and clears the matching local dirty marks.
|
|
994
|
+
* `room.state` stays authoritative and untouched: tests, bots and existing code see exactly what
|
|
995
|
+
* they saw before. Entities appear and disappear at the buffered time; past the newest delta the
|
|
996
|
+
* value holds (no extrapolation) and the starvation is counted. A collection declared
|
|
997
|
+
* `interpolate: false` (and every singleton) reads straight through to the authoritative view.
|
|
281
998
|
*/
|
|
282
999
|
|
|
283
1000
|
type AnyRecord$1 = Record<string, unknown>;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
* corrects and counts it (`snapped`); buffer memory is bounded by the same window.
|
|
288
|
-
*/
|
|
289
|
-
declare const RESIM_DEPTH = 20;
|
|
290
|
-
/** One correction op, as the `'correct'` event sees it. */
|
|
291
|
-
interface CorrectionOp {
|
|
292
|
-
readonly collection: string;
|
|
293
|
-
readonly id: string;
|
|
294
|
-
readonly fields: readonly string[];
|
|
295
|
-
readonly patch: AnyRecord$1;
|
|
296
|
-
readonly tick: number;
|
|
297
|
-
/**
|
|
298
|
-
* The client write tick the server judged (D19), absent against a pre-week-8 server. Local
|
|
299
|
-
* writes newer than it were re-applied on top of the correction.
|
|
300
|
-
*/
|
|
301
|
-
readonly clientTick?: number;
|
|
302
|
-
/** How many pending local writes were re-applied over this correction's values. */
|
|
303
|
-
readonly replayed: number;
|
|
304
|
-
/** `true` when the correction outran the resim window and everything it named snapped. */
|
|
305
|
-
readonly snapped: boolean;
|
|
306
|
-
}
|
|
307
|
-
declare class ClientStore {
|
|
308
|
-
readonly ext: AnySchema;
|
|
309
|
-
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
1001
|
+
declare class RenderStore {
|
|
1002
|
+
private readonly ext;
|
|
1003
|
+
private readonly store;
|
|
310
1004
|
private readonly meOf;
|
|
311
|
-
|
|
312
|
-
private
|
|
1005
|
+
private readonly now;
|
|
1006
|
+
private readonly delayMs;
|
|
1007
|
+
/** collection → id → buffered keyframes. Only interpolating entity collections have entries. */
|
|
1008
|
+
private readonly buffers;
|
|
313
1009
|
private readonly descs;
|
|
314
|
-
|
|
315
|
-
/** The object handed out as `room.state`; identity survives a resync. */
|
|
1010
|
+
/** The object handed out as `room.render`; identity survives a resync. */
|
|
316
1011
|
readonly view: AnyRecord$1;
|
|
317
|
-
/**
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
private
|
|
321
|
-
constructor(ext: AnySchema,
|
|
322
|
-
/** Reads the current client id — it is not known until the first `WELCOME`. */
|
|
323
|
-
meOf: () => string);
|
|
324
|
-
loadSnapshot(bytes: Uint8Array): void;
|
|
325
|
-
/**
|
|
326
|
-
* Captures the field values of every pending owned write, keyed by collection then id then
|
|
327
|
-
* field name — call this **before** `loadSnapshot` on a resync, or the edit is gone once the
|
|
328
|
-
* old `plain` it lives in is replaced. Paired with `applyPendingWrites`.
|
|
329
|
-
*/
|
|
330
|
-
capturePendingWrites(): Map<string, Map<string, AnyRecord$1>>;
|
|
1012
|
+
/** Render reads that ran past the newest delta and held it (buffer starvation, D20). */
|
|
1013
|
+
starved: number;
|
|
1014
|
+
/** D22 part 2: set when the session predicts physics bodies; render reads consult it first. */
|
|
1015
|
+
private predictor;
|
|
1016
|
+
constructor(ext: AnySchema, store: ClientStore, meOf: () => string, now: () => number, delayMs: () => number);
|
|
331
1017
|
/**
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* whatever the resync snapshot happened to carry for that field.
|
|
1018
|
+
* Seeds one keyframe per existing entity from a `WELCOME` snapshot, backdated by the delay so
|
|
1019
|
+
* the world is visible immediately (the join snapshot is the render baseline, not a change to
|
|
1020
|
+
* ease towards).
|
|
336
1021
|
*/
|
|
337
|
-
|
|
1022
|
+
seedSnapshot(): void;
|
|
1023
|
+
/** Buffers every entity a `DELTA` touched, stamped with its arrival time. Call after apply. */
|
|
1024
|
+
recordDelta(delta: Delta): void;
|
|
1025
|
+
private bufferFor;
|
|
1026
|
+
private renderTime;
|
|
1027
|
+
attachPredictor(predictor: PhysicsPredictor): void;
|
|
1028
|
+
/** The interpolated (or predicted, or authoritative) value of `collection[id]` right now. */
|
|
1029
|
+
get(desc: CollectionDesc, id: string): unknown;
|
|
1030
|
+
/** Is `collection[id]` visible at the render clock? */
|
|
1031
|
+
has(desc: CollectionDesc, id: string): boolean;
|
|
1032
|
+
/** The authoritative owner — ownership is fact, not a rendered value. */
|
|
1033
|
+
ownerOf(desc: CollectionDesc, id: string): string | undefined;
|
|
1034
|
+
/** Ids visible at the render clock: owned (predicted) plus buffered-and-appeared. */
|
|
1035
|
+
ids(desc: CollectionDesc): IterableIterator<string>;
|
|
1036
|
+
/** Drops frames the render clock has passed, keeping the newest one at-or-before `renderT`. */
|
|
1037
|
+
private prune;
|
|
1038
|
+
private gc;
|
|
338
1039
|
private buildView;
|
|
339
|
-
/** The writable tracked proxy when this client owns `id`, otherwise a frozen one. */
|
|
340
|
-
instance(desc: CollectionDesc, id: string): unknown;
|
|
341
|
-
private freeze;
|
|
342
|
-
private entityHint;
|
|
343
|
-
private singletonHint;
|
|
344
|
-
/**
|
|
345
|
-
* Applies a server `DELTA`. Update ops for instances this client owns are dropped field-wise
|
|
346
|
-
* (server wins only through `CORRECT`); adds, removes and owner changes always apply.
|
|
347
|
-
*/
|
|
348
|
-
applyServerDelta(delta: Delta): void;
|
|
349
|
-
/**
|
|
350
|
-
* The flushed-write half of the §7.2 in-flight own-write fix: an `add` op for an instance this
|
|
351
|
-
* client owns replaces the whole record, so any write already *flushed* (dirty set consumed —
|
|
352
|
-
* `preserveLocalWrites` cannot see it) but not yet judged would be reverted by the echo.
|
|
353
|
-
* Re-apply the retained pending writes in flush order; the server's eventual `CORRECT` (if the
|
|
354
|
-
* write is clamped) still wins through the snap+replay path.
|
|
355
|
-
*/
|
|
356
|
-
private replayOverAddEchoes;
|
|
357
|
-
private withoutOwnedUpdates;
|
|
358
|
-
/**
|
|
359
|
-
* An `add` for an instance this client will own, with its unflushed local field values folded
|
|
360
|
-
* back in. Returns `op` unchanged when there is nothing to preserve.
|
|
361
|
-
*/
|
|
362
|
-
private preserveLocalWrites;
|
|
363
|
-
/**
|
|
364
|
-
* Applies a `CORRECT`: snap the named fields to the server's values, then re-apply every local
|
|
365
|
-
* write newer than the judged `clientTick` in order (D19 snap + replay) — flushed writes from
|
|
366
|
-
* the retained buffer first, the still-unflushed in-window edit last (it is the newest, and its
|
|
367
|
-
* dirty mark survives so the next flush re-sends the *local* value, not the server's).
|
|
368
|
-
*
|
|
369
|
-
* Without a `clientTick` (a pre-week-8 server) — or when the correction outran the
|
|
370
|
-
* `RESIM_DEPTH` window (`snapped`) — the pre-D19 semantics apply: the correction wins in full
|
|
371
|
-
* and the local dirty marks it supersedes are cleared.
|
|
372
|
-
*/
|
|
373
|
-
applyCorrection(delta: Delta, clientTick?: number): CorrectionOp[];
|
|
374
|
-
private desc;
|
|
375
|
-
/** Cheap check for the flush loop: has anything been written locally since the last flush? */
|
|
376
|
-
get hasLocalWrites(): boolean;
|
|
377
|
-
/**
|
|
378
|
-
* The `WRITE` payload for everything dirty, or `undefined` when nothing qualifies. Consumes the
|
|
379
|
-
* dirty set either way (a write to an instance that has since been removed or handed away is
|
|
380
|
-
* dropped, not retried forever).
|
|
381
|
-
*/
|
|
382
|
-
takeWrite(tick: number): Uint8Array | undefined;
|
|
383
|
-
/**
|
|
384
|
-
* Captures the field values a flushed `WRITE` carried, keyed by its write tick, so a later
|
|
385
|
-
* `CORRECT` can re-apply exactly the writes the server had not judged yet (D19 snap + replay).
|
|
386
|
-
* Bounded to `RESIM_DEPTH` entries; eviction is remembered so an outrun correction snaps.
|
|
387
|
-
*/
|
|
388
|
-
private retainPendingWrite;
|
|
389
|
-
/** Re-applies one retained write's fields for `collection[id]` onto the plain state. */
|
|
390
|
-
private replayWrite;
|
|
391
|
-
/**
|
|
392
|
-
* Re-marks every field of every instance this client owns, so a resync `WELCOME` (wake or
|
|
393
|
-
* worker restart) does not silently drop writes that were in flight.
|
|
394
|
-
*/
|
|
395
|
-
remarkOwned(): void;
|
|
396
|
-
/** Only update ops, only instances that still exist and are still mine, never the owner bit. */
|
|
397
|
-
private ownedDirty;
|
|
398
1040
|
}
|
|
399
1041
|
|
|
400
1042
|
/**
|
|
@@ -409,8 +1051,13 @@ type AnyRecord = Record<string, unknown>;
|
|
|
409
1051
|
type ClientImpl = (params: AnyRecord) => unknown;
|
|
410
1052
|
/** Default hard cap on the owned-write flush window. */
|
|
411
1053
|
declare const DEFAULT_WRITE_INTERVAL_MS = 50;
|
|
412
|
-
/**
|
|
413
|
-
|
|
1054
|
+
/**
|
|
1055
|
+
* How often the client pings for `room.rtt`. Two seconds rather than ten (bug 4): the prediction
|
|
1056
|
+
* lead is re-derived from `rtt` on every rebase, so the interval is not just how fresh a
|
|
1057
|
+
* diagnostic number is — it is how long one measurement gets to size the client's simulation.
|
|
1058
|
+
* A PING is a 4-byte payload; five of them a minute is not the reason a room costs anything.
|
|
1059
|
+
*/
|
|
1060
|
+
declare const PING_INTERVAL_MS = 2000;
|
|
414
1061
|
/** How long an outbound RPC waits for its `REPLY` before rejecting. */
|
|
415
1062
|
declare const CALL_TIMEOUT_MS = 10000;
|
|
416
1063
|
interface SessionOptions {
|
|
@@ -419,10 +1066,21 @@ interface SessionOptions {
|
|
|
419
1066
|
readonly url: string;
|
|
420
1067
|
readonly roomId: string;
|
|
421
1068
|
readonly key: string;
|
|
1069
|
+
/**
|
|
1070
|
+
* Week 13 (D27): a JWT asserting who this player is, minted by YOUR server with the project's
|
|
1071
|
+
* signing secret (`irtio keys jwt-secret`). A function is called before every HELLO — every
|
|
1072
|
+
* reconnect included — so a fresh token can be supplied when the old one nears its `exp`; a
|
|
1073
|
+
* resume that outlives its token would otherwise be refused with `E_TOKEN_EXPIRED`.
|
|
1074
|
+
*/
|
|
1075
|
+
readonly token?: string | (() => string | Promise<string>) | undefined;
|
|
422
1076
|
readonly role?: string | undefined;
|
|
423
1077
|
readonly name?: string | undefined;
|
|
424
1078
|
readonly rpc?: Readonly<Record<string, ClientImpl>> | undefined;
|
|
425
1079
|
readonly writeIntervalMs?: number | undefined;
|
|
1080
|
+
/** Render delay for `room.render` (D20). Default `max(50, 2 × tickIntervalMs)`. */
|
|
1081
|
+
readonly interpDelayMs?: number | undefined;
|
|
1082
|
+
/** The shared world-builder half the client predicts with (D22 part 2). */
|
|
1083
|
+
readonly physics?: ClientPhysicsOptions | undefined;
|
|
426
1084
|
readonly transport?: Transport | undefined;
|
|
427
1085
|
readonly scheduler?: Scheduler | undefined;
|
|
428
1086
|
readonly onFrame?: FrameHook | undefined;
|
|
@@ -434,6 +1092,14 @@ declare class Session {
|
|
|
434
1092
|
private readonly options;
|
|
435
1093
|
readonly ext: AnySchema;
|
|
436
1094
|
readonly store: ClientStore;
|
|
1095
|
+
readonly render: RenderStore;
|
|
1096
|
+
/** `true` when the join passed `physics` and the schema has body-backed collections. */
|
|
1097
|
+
readonly predictionRequested: boolean;
|
|
1098
|
+
/**
|
|
1099
|
+
* Present once `./physics.js` has loaded (dynamic import — the predictor code, like the
|
|
1100
|
+
* engine, costs a non-physics game zero bytes). Reads fall back to interpolation until then.
|
|
1101
|
+
*/
|
|
1102
|
+
predictor: PhysicsPredictor | undefined;
|
|
437
1103
|
/** The schema the `CALL`/`REPLY` rpc id space indexes into. */
|
|
438
1104
|
private readonly rpcSchema;
|
|
439
1105
|
private readonly transport;
|
|
@@ -445,9 +1111,11 @@ declare class Session {
|
|
|
445
1111
|
roomId: string;
|
|
446
1112
|
tick: number;
|
|
447
1113
|
/**
|
|
448
|
-
* The
|
|
1114
|
+
* The stamp of the newest flushed `WRITE`, carried in its delta header (D19). With physics
|
|
1115
|
+
* prediction live this is the predictor's head tick + 1 — the server's tick clock, which is
|
|
1116
|
+
* what lets the rebase replay pending writes by tick — and otherwise a bare counter.
|
|
449
1117
|
* Monotonic for the life of the session, across reconnects — the server's `lastClientTick`
|
|
450
|
-
* for this client survives the grace window too.
|
|
1118
|
+
* for this client survives the grace window too, and only ever moves forward.
|
|
451
1119
|
*/
|
|
452
1120
|
writeTick: number;
|
|
453
1121
|
/** The room's tick interval from `WELCOME`, or 0 when unknown (relay / pre-week-8 server). */
|
|
@@ -455,6 +1123,19 @@ declare class Session {
|
|
|
455
1123
|
rtt: number;
|
|
456
1124
|
status: Status;
|
|
457
1125
|
private socket;
|
|
1126
|
+
/**
|
|
1127
|
+
* Whether `socket` has actually opened. `TransportSocket` deliberately has no `readyState` —
|
|
1128
|
+
* `@irtio/testing` injects an in-process pair — so the session tracks it from `onopen`.
|
|
1129
|
+
*
|
|
1130
|
+
* Needed because a reconnecting session holds a socket that exists and is *not* sendable:
|
|
1131
|
+
* `onSocketClosed` clears `socket` but keeps `joined` true (that is what makes resume work), and
|
|
1132
|
+
* `connect()` then assigns a fresh socket that is still CONNECTING. Every send guard here used to
|
|
1133
|
+
* test `this.socket` for presence alone, so the ping timer and the write flush would both reach a
|
|
1134
|
+
* connecting socket and throw `InvalidStateError: Sent before connected.` out of a timer callback,
|
|
1135
|
+
* where nothing catches it. Found on staging under 40+ bots, where joins are slow enough for the
|
|
1136
|
+
* window to be wide; it never opens against a local dev server.
|
|
1137
|
+
*/
|
|
1138
|
+
private socketOpen;
|
|
458
1139
|
private resumeToken;
|
|
459
1140
|
private joined;
|
|
460
1141
|
private left;
|
|
@@ -470,6 +1151,11 @@ declare class Session {
|
|
|
470
1151
|
private settleJoin;
|
|
471
1152
|
private failJoin;
|
|
472
1153
|
constructor(options: SessionOptions);
|
|
1154
|
+
/**
|
|
1155
|
+
* The `room.render` delay (D20): the explicit option, else twice the room's tick interval
|
|
1156
|
+
* (from `WELCOME`), floored at 50 ms — also the fallback when the interval is unknown.
|
|
1157
|
+
*/
|
|
1158
|
+
get interpDelayMs(): number;
|
|
473
1159
|
/** Connects and resolves on the first `WELCOME`; a fatal `ERROR` before it rejects. */
|
|
474
1160
|
start(): Promise<Session>;
|
|
475
1161
|
private connect;
|
|
@@ -487,6 +1173,10 @@ declare class Session {
|
|
|
487
1173
|
private onWelcome;
|
|
488
1174
|
private onDelta;
|
|
489
1175
|
private onCorrect;
|
|
1176
|
+
private descsByName;
|
|
1177
|
+
private descOfCollection;
|
|
1178
|
+
/** Pre-rebase predicted values per corrected instance, keyed `collection id`. */
|
|
1179
|
+
private capturePredicted;
|
|
490
1180
|
private onError;
|
|
491
1181
|
private onPong;
|
|
492
1182
|
private onMsg;
|
|
@@ -497,6 +1187,7 @@ declare class Session {
|
|
|
497
1187
|
* inside a window leaves as a single `WRITE`.
|
|
498
1188
|
*/
|
|
499
1189
|
private armFlush;
|
|
1190
|
+
private sendPing;
|
|
500
1191
|
private armPing;
|
|
501
1192
|
/** Sends every pending owned write now. */
|
|
502
1193
|
flush(): void;
|
|
@@ -553,4 +1244,4 @@ declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> &
|
|
|
553
1244
|
*/
|
|
554
1245
|
declare function joinRelay(options?: JoinRelayOptions): Promise<RelayRoom>;
|
|
555
1246
|
|
|
556
|
-
export { CALL_TIMEOUT_MS, type ClientCollection, type ClientState, ClientStore, type Correction, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, type FrameHook, type JoinOptions, type JoinRelayOptions, type MessageTarget, PING_INTERVAL_MS, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, defaultScheduler, joinRelay, joinRoom, linkForUrl, resolveUrl, roomIdFrom, webSocketTransport };
|
|
1247
|
+
export { CALL_TIMEOUT_MS, type ClientBodySpec, type ClientCollection, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector3, type Correction, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, type FrameHook, type JoinOptions, type JoinRelayOptions, MAX_PREDICTED_BODIES, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PredictionStats, type PredictionStatus, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, defaultScheduler, joinRelay, joinRoom, linkForUrl, resolveUrl, roomIdFrom, webSocketTransport };
|