@irtio/client 0.6.0 → 0.7.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-TE66EDBB.js → chunk-DABQDR3S.js} +245 -32
- package/dist/{chunk-6J6PKFUS.js → chunk-XWVXZRBS.js} +4 -0
- package/dist/index.d.ts +337 -32
- package/dist/index.js +181 -9
- package/dist/{physics-S5LHL3HK.js → physics-RT5T36P5.js} +26 -3
- package/dist/{physics2d-ZV2IRRRZ.js → physics2d-HOFMWPZV.js} +51 -2
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel, ReadonlyCollection, RoleOf, VisibleKeys, SchemaDefs, EntityDef, DeepReadonly, InferFields, Owned, SingletonDef, ClientImplementations, ClientRpcs, SchemaRpc, ClientCallProxy } from '@irtio/schema';
|
|
1
|
+
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel, ReadonlyCollection, RoleOf, VisibleKeys, SchemaDefs, EntityDef, DeepReadonly, InferFields, Owned, SingletonDef, ClientImplementations, ClientRpcs, SchemaRpc, MessageChannels, ClientCallProxy } from '@irtio/schema';
|
|
2
2
|
import * as _irtio_protocol from '@irtio/protocol';
|
|
3
3
|
import { PresenceRecord, ProfileLedger, ProfileSnapshot } from '@irtio/protocol';
|
|
4
4
|
import * as MATTER from 'matter-js';
|
|
@@ -280,6 +280,23 @@ interface EngineAdapter {
|
|
|
280
280
|
/** Factory call plus every engine-specific warning about what it returned. */
|
|
281
281
|
createBody(desc: CollectionDesc, id: string, record: AnyRecord$2, warn: (key: string, message: string) => void): EngineBody | undefined;
|
|
282
282
|
removeBody(body: EngineBody): void;
|
|
283
|
+
/**
|
|
284
|
+
* D71: turn a body the factory just built into a **kinematic proxy**. It keeps the collider the
|
|
285
|
+
* factory gave it, it is never moved by gravity, by contact or by any other local force, and it
|
|
286
|
+
* goes only where {@link EngineAdapter.moveKinematic} puts it. Called once, immediately after
|
|
287
|
+
* `createBody`, and never on a body the local world is simulating.
|
|
288
|
+
*/
|
|
289
|
+
makeKinematic(body: EngineBody): void;
|
|
290
|
+
/**
|
|
291
|
+
* D71: put a kinematic proxy at `pose` for the next step, so that contacts see the velocity the
|
|
292
|
+
* move implies and a predicted body resting on it is carried.
|
|
293
|
+
*
|
|
294
|
+
* Called before every local step. Asking for the same pose twice moves nothing and implies no
|
|
295
|
+
* velocity — which is what "held, never extrapolated" means at the engine seam: a proxy driven
|
|
296
|
+
* through a forty-tick rebase stands still for all of it rather than running the same delta forty
|
|
297
|
+
* times.
|
|
298
|
+
*/
|
|
299
|
+
moveKinematic(body: EngineBody, pose: Pose): void;
|
|
283
300
|
/** One fixed-timestep step of the local world. */
|
|
284
301
|
step(): void;
|
|
285
302
|
/** Server record → body state, in whatever order this engine's setters require. */
|
|
@@ -302,9 +319,20 @@ interface EngineAdapter {
|
|
|
302
319
|
*/
|
|
303
320
|
velocityTolerance(epsilon: number, timestepSeconds: number): number;
|
|
304
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* D71: where a kinematic proxy goes — the body-channel values the renderer is drawing for one
|
|
324
|
+
* instance at this instant, written into `into`. `false` when there is nothing drawn (the entity
|
|
325
|
+
* has not appeared at the render clock, or has left it).
|
|
326
|
+
*
|
|
327
|
+
* Backed by `RenderStore.drawn`, the D20 interpolation buffer: the authoritative pose at
|
|
328
|
+
* `interpDelayMs` behind arrival, **held** past the newest delta rather than extrapolated. The
|
|
329
|
+
* predictor never asks it for a time, so it can never be asked for the future.
|
|
330
|
+
*/
|
|
331
|
+
type DrawnReader = (desc: CollectionDesc, id: string, into: Record<string, number>) => boolean;
|
|
305
332
|
/** The engine-neutral tuning knobs; both engines' option objects carry these names. */
|
|
306
333
|
interface PredictorTuning {
|
|
307
334
|
readonly maxPredictedBodies?: number;
|
|
335
|
+
readonly maxProxyBodies?: number;
|
|
308
336
|
readonly epsilon?: number;
|
|
309
337
|
readonly smoothingHalfLifeMs?: number;
|
|
310
338
|
readonly smoothingSnapUnits?: number;
|
|
@@ -322,11 +350,24 @@ interface PredictionStats {
|
|
|
322
350
|
/** Corrections whose values matched the local prediction within epsilon. */
|
|
323
351
|
suppressed: number;
|
|
324
352
|
/**
|
|
325
|
-
* Non-owned predicted instances currently over
|
|
326
|
-
*
|
|
327
|
-
*
|
|
353
|
+
* Non-owned predicted instances currently over `maxPredictedBodies`.
|
|
354
|
+
*
|
|
355
|
+
* Since D71 these are **proxied** rather than absent: each one still has a collider in the local
|
|
356
|
+
* world, moved to the pose the renderer draws, so a predicted body stands on it instead of
|
|
357
|
+
* falling through. What it no longer has is a simulation of its own — shove it and nothing
|
|
358
|
+
* happens locally until the server agrees. `stats.absent` is the number that means "you can fall
|
|
359
|
+
* through this"; this one means "you cannot push this".
|
|
328
360
|
*/
|
|
329
361
|
overCap: number;
|
|
362
|
+
/** D71: kinematic proxies in the local world right now. */
|
|
363
|
+
proxies: number;
|
|
364
|
+
/**
|
|
365
|
+
* D71: non-owned instances with **no body in the local world at all** — over `maxProxyBodies`,
|
|
366
|
+
* opted out with `proxy: false`, or with no body factory on the client. Predicted bodies pass
|
|
367
|
+
* straight through these until the next correction snaps them back, so a non-zero value on a
|
|
368
|
+
* collection anything stands on is a gameplay bug rather than a fidelity tradeoff.
|
|
369
|
+
*/
|
|
370
|
+
absent: number;
|
|
330
371
|
/** Microseconds spent in the last rebase's re-steps. */
|
|
331
372
|
lastResimMicros: number;
|
|
332
373
|
/**
|
|
@@ -389,11 +430,19 @@ declare class Predictor {
|
|
|
389
430
|
/** Seconds per step, fixed for the life of the world. */
|
|
390
431
|
private timestepSeconds;
|
|
391
432
|
private readonly bodies;
|
|
433
|
+
/** D71: kinematic proxies, keyed like `bodies`. The two maps are disjoint by construction. */
|
|
434
|
+
private readonly proxies;
|
|
392
435
|
/** Physics-backed entity collections, in schema order. D50: not `readonly`, see `swapSchema`. */
|
|
393
436
|
private collections;
|
|
394
437
|
private readonly warned;
|
|
395
438
|
/** Highest over-cap count warned about per collection, so growth re-warns and noise does not. */
|
|
396
439
|
private readonly overCapHigh;
|
|
440
|
+
/** The same, for the proxy cap: `absent` is the count a game has to act on (D71-d). */
|
|
441
|
+
private readonly absentHigh;
|
|
442
|
+
/** D71: where a proxy goes. Wired by the session; see {@link DrawnReader}. */
|
|
443
|
+
private drawnReader;
|
|
444
|
+
/** `drawnReader`'s output buffer, reused: it is called once per proxy per frame. */
|
|
445
|
+
private readonly drawnChannels;
|
|
397
446
|
/** Authority arrived since the last frame: rebase before free-running. */
|
|
398
447
|
private authorityDirty;
|
|
399
448
|
private accumulatorMs;
|
|
@@ -466,6 +515,11 @@ declare class Predictor {
|
|
|
466
515
|
* `PredictedBody.desc` and re-deriving collider shapes from the new descriptors.
|
|
467
516
|
*/
|
|
468
517
|
swapSchema(newExt: AnySchema): void;
|
|
518
|
+
/**
|
|
519
|
+
* D71: tell the predictor where its proxies go. The session calls this once, with a reader over
|
|
520
|
+
* the D20 interpolation buffer; without it a proxy holds the pose it was built at.
|
|
521
|
+
*/
|
|
522
|
+
setDrawnReader(reader: DrawnReader): void;
|
|
469
523
|
get epsilon(): number;
|
|
470
524
|
/** `true` once the engine is loaded and the local world exists. */
|
|
471
525
|
get ready(): boolean;
|
|
@@ -509,8 +563,19 @@ declare class Predictor {
|
|
|
509
563
|
* that is the lead, not this.
|
|
510
564
|
*/
|
|
511
565
|
noteWriteApplied(stampTick: number, appliedTick: number): void;
|
|
512
|
-
/**
|
|
566
|
+
/**
|
|
567
|
+
* Does the local world currently **simulate** `collection[id]`?
|
|
568
|
+
*
|
|
569
|
+
* Deliberately still false for a proxied instance (D71). Every caller of this asks it to decide
|
|
570
|
+
* whether the local world is the better answer than authority — the render read path, the
|
|
571
|
+
* correction classifier, `room.prediction.predicts` — and for a proxy it is not: a proxy is
|
|
572
|
+
* authority, one interpolation delay old, put into the world so that other bodies can touch it.
|
|
573
|
+
* Reading it back would be a round trip through the physics engine to learn what the render
|
|
574
|
+
* buffer already said. {@link proxied} is the question about proxies.
|
|
575
|
+
*/
|
|
513
576
|
has(collection: string, id: string): boolean;
|
|
577
|
+
/** D71: does `collection[id]` have a kinematic proxy in the local world right now? */
|
|
578
|
+
hasProxy(collection: string, id: string): boolean;
|
|
514
579
|
/**
|
|
515
580
|
* Is a correction's every value within the suppression tolerance of the prediction it judges?
|
|
516
581
|
* Position (and rotation) channels compare against `epsilon` directly; velocity and angular
|
|
@@ -615,24 +680,77 @@ declare class Predictor {
|
|
|
615
680
|
*/
|
|
616
681
|
predictedValues(desc: CollectionDesc, id: string, fields: readonly string[], atTick?: number): AnyRecord$2 | undefined;
|
|
617
682
|
/**
|
|
618
|
-
* Mirrors the local world
|
|
619
|
-
* instance it owns
|
|
620
|
-
*
|
|
621
|
-
*
|
|
683
|
+
* Mirrors the local world onto the instances this client can see: a **simulated** body for every
|
|
684
|
+
* physics instance it owns plus non-owned instances of `predicted: true` collections up to
|
|
685
|
+
* `maxPredictedBodies`, and a **kinematic proxy** (D71) for everything else that has a body
|
|
686
|
+
* factory and has not opted out with `proxy: false`, up to `maxProxyBodies`.
|
|
687
|
+
*
|
|
688
|
+
* Slot order is collection order then insertion order — the same iteration guarantee the server
|
|
689
|
+
* states — so which instances fall over either cap is deterministic. There is no relevance
|
|
690
|
+
* policy and no distance ordering: the ninth crate spawned is still the ninth crate, it is just
|
|
691
|
+
* now solid rather than missing.
|
|
622
692
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
693
|
+
* Three transitions happen here, and each is a removal before a creation so the world never holds
|
|
694
|
+
* two colliders for one id:
|
|
695
|
+
*
|
|
696
|
+
* - **Promotion.** A proxied instance of a predicted collection finds a free slot (a predicted
|
|
697
|
+
* body was removed, or the cap was raised). `createBody` applies the authority record, velocity
|
|
698
|
+
* channels included, so the body carries on rather than starting from rest.
|
|
699
|
+
* - **Demotion.** A simulated instance loses its slot to an earlier-collection newcomer. That is
|
|
700
|
+
* what this loop already did before proxies — it stopped marking the body live and the sweep
|
|
701
|
+
* below removed it — and now it becomes a proxy at the drawn pose in the same frame. It costs
|
|
702
|
+
* one visible pose step: the body was a lead ahead of authority and the proxy is an
|
|
703
|
+
* interpolation delay behind it.
|
|
704
|
+
* - **Removal.** An instance the server removed, or one that left this client's area of interest
|
|
705
|
+
* (D23), loses whichever of the two it had, the same frame.
|
|
706
|
+
*
|
|
707
|
+
* What is left over — over the proxy cap, `proxy: false`, or no factory — has no collider in the
|
|
708
|
+
* local world at all, is counted in `stats.absent`, and is the only case a predicted body still
|
|
709
|
+
* falls through.
|
|
626
710
|
*/
|
|
627
711
|
private reconcileBodies;
|
|
712
|
+
/** Drops one simulated body and everything the loop keeps per body. Safe on a missing key. */
|
|
713
|
+
private removePredicted;
|
|
714
|
+
/** Drops one proxy. Safe on a missing key. */
|
|
715
|
+
private removeProxy;
|
|
716
|
+
/**
|
|
717
|
+
* D71: builds one kinematic proxy through the **same factory call** the simulated path uses, so
|
|
718
|
+
* the collider is the server's collider by construction rather than by a second description of
|
|
719
|
+
* it, then hands it to the adapter to be made kinematic.
|
|
720
|
+
*
|
|
721
|
+
* The authority record is applied first — a proxy appears where the server last said it was, not
|
|
722
|
+
* at the factory's origin — and the drawn pose takes over on the next `refreshProxyTargets`.
|
|
723
|
+
*/
|
|
724
|
+
private createProxy;
|
|
725
|
+
/**
|
|
726
|
+
* Reads every proxy's drawn pose once per frame, and only once.
|
|
727
|
+
*
|
|
728
|
+
* Once, because a rebase re-steps the world up to `MAX_LEAD` times against a render clock that
|
|
729
|
+
* has not moved: asking again per step would return the same answer at a real cost. And once
|
|
730
|
+
* *because* of D71's rule — a proxy holds one pose for every re-step of a rebase. It cannot run
|
|
731
|
+
* ahead of the newest delta because the reader never extrapolates, and it cannot run ahead of the
|
|
732
|
+
* frame because it is only ever asked here.
|
|
733
|
+
*
|
|
734
|
+
* A proxy with nothing drawn (a delta gap, an entity not yet at the render clock) keeps the pose
|
|
735
|
+
* it had. That is the hold, and it is the difference between a crate that stays solid through a
|
|
736
|
+
* stall and a crate that slides off across the level.
|
|
737
|
+
*/
|
|
738
|
+
private refreshProxyTargets;
|
|
739
|
+
/** Writes every proxy to its target pose. Runs immediately before each `adapter.step()`. */
|
|
740
|
+
private driveProxies;
|
|
628
741
|
private createBody;
|
|
629
742
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
743
|
+
* The two cap notices, both on a new high-water mark per collection: a game that grows past a cap
|
|
744
|
+
* mid-session hears about it, and a count that oscillates around one level does not turn the
|
|
745
|
+
* console into a log.
|
|
746
|
+
*
|
|
747
|
+
* They say different things now, and only the second is an emergency (D71). Over the *prediction*
|
|
748
|
+
* cap an instance is still solid — it has a proxy — it just is not simulated, so a shove does
|
|
749
|
+
* nothing locally until the server agrees. Over the *proxy* cap it is genuinely not there, which
|
|
750
|
+
* is the failure `bugs.md` #3 was about, and the message names both numbers so it is obvious
|
|
751
|
+
* which one to raise.
|
|
634
752
|
*/
|
|
635
|
-
private
|
|
753
|
+
private warnCaps;
|
|
636
754
|
private warnOnce;
|
|
637
755
|
/**
|
|
638
756
|
* The client's lead over authority, in ticks: a full round trip, rounded to the nearest tick
|
|
@@ -696,6 +814,11 @@ declare class Predictor {
|
|
|
696
814
|
* local body of a collection, owned or not, run after the whole intent pass rather than
|
|
697
815
|
* interleaved with it. It is what gives a non-owned predicted body its gravity on an engine
|
|
698
816
|
* whose world has none of its own.
|
|
817
|
+
*
|
|
818
|
+
* Both passes walk `this.bodies`, which is why neither ever reaches a proxy (D71): a proxy is not
|
|
819
|
+
* simulated, so an intent hook or a per-step gravity force on it would be a force on a body that
|
|
820
|
+
* cannot move, applied to a pose that is going to be overwritten before the next step anyway. The
|
|
821
|
+
* isolation is structural rather than a filter, which is the version that cannot rot.
|
|
699
822
|
*/
|
|
700
823
|
private applyIntents;
|
|
701
824
|
/** Server record → body channels (the same mapping the runtime's sync uses, inverted). */
|
|
@@ -798,12 +921,22 @@ interface ClientPhysics2dOptions {
|
|
|
798
921
|
*/
|
|
799
922
|
readonly settle?: Readonly<Record<string, ClientIntent2dHook>>;
|
|
800
923
|
/**
|
|
801
|
-
* D21 cap: how many **non-owned** predicted bodies this client simulates.
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
924
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
925
|
+
*
|
|
926
|
+
* Since D71 an over-cap instance is not gone from the local world — it gets a kinematic proxy
|
|
927
|
+
* (see `maxProxyBodies`), so a predicted body still stands on it and is still blocked by it.
|
|
928
|
+
* What it loses is being simulated: shove it and nothing happens locally until the server says
|
|
929
|
+
* it moved. Counted as `stats.overCap`.
|
|
805
930
|
*/
|
|
806
931
|
readonly maxPredictedBodies?: number;
|
|
932
|
+
/**
|
|
933
|
+
* D71 cap: how many **kinematic proxies** this client keeps — colliders at the pose the renderer
|
|
934
|
+
* draws, for the instances it does not simulate. Default `MAX_PROXY_BODIES`. Past it, instances
|
|
935
|
+
* are **absent from the local world** and predicted bodies pass straight through them (warned,
|
|
936
|
+
* counted as `stats.absent`). See `ClientPhysicsOptions.maxProxyBodies` for why it is its own
|
|
937
|
+
* number rather than a share of the prediction cap.
|
|
938
|
+
*/
|
|
939
|
+
readonly maxProxyBodies?: number;
|
|
807
940
|
/**
|
|
808
941
|
* A body-field correction whose every value is within this tolerance of the local prediction
|
|
809
942
|
* is *suppressed*: authority still applies, but it is not a misprediction. Positions compare
|
|
@@ -885,12 +1018,28 @@ interface ClientPhysicsOptions {
|
|
|
885
1018
|
*/
|
|
886
1019
|
readonly intents?: Readonly<Record<string, ClientIntentHook>>;
|
|
887
1020
|
/**
|
|
888
|
-
* D21 cap: how many **non-owned** predicted bodies this client simulates.
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
1021
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
1022
|
+
*
|
|
1023
|
+
* Since D71 an over-cap instance is not gone from the local world — it gets a kinematic proxy
|
|
1024
|
+
* (see `maxProxyBodies`), so a predicted body still stands on it and is still blocked by it.
|
|
1025
|
+
* What it loses is being simulated: shove it and nothing happens locally until the server says
|
|
1026
|
+
* it moved. Raise this for the bodies players push; leave it for the ones they only stand on.
|
|
1027
|
+
* Counted as `stats.overCap`.
|
|
892
1028
|
*/
|
|
893
1029
|
readonly maxPredictedBodies?: number;
|
|
1030
|
+
/**
|
|
1031
|
+
* D71 cap: how many **kinematic proxies** this client keeps — colliders at the pose the renderer
|
|
1032
|
+
* draws, for the instances it does not simulate (over `maxPredictedBodies`, or in a collection
|
|
1033
|
+
* that is not `predicted`). Default `MAX_PROXY_BODIES`.
|
|
1034
|
+
*
|
|
1035
|
+
* Its own number rather than a share of `maxPredictedBodies`, because a proxy is a different
|
|
1036
|
+
* cost: a pose write and a collider in the broad phase, not a body being integrated and solved.
|
|
1037
|
+
* Past this cap instances are **absent from the local world** — predicted bodies pass through
|
|
1038
|
+
* them until the next correction snaps them back (warned, counted as `stats.absent`). Turn
|
|
1039
|
+
* `proxy: false` on in the schema for the collections nothing collides with, so the budget goes
|
|
1040
|
+
* to the ones that matter.
|
|
1041
|
+
*/
|
|
1042
|
+
readonly maxProxyBodies?: number;
|
|
894
1043
|
/**
|
|
895
1044
|
* A body-field correction whose every value is within this tolerance of the local prediction
|
|
896
1045
|
* is *suppressed*: authority still applies, but it is not a misprediction — steady state stays
|
|
@@ -1369,8 +1518,18 @@ interface JoinOptions<S, Role extends string = string> {
|
|
|
1369
1518
|
*/
|
|
1370
1519
|
readonly profile?: boolean;
|
|
1371
1520
|
}
|
|
1372
|
-
/**
|
|
1373
|
-
|
|
1521
|
+
/**
|
|
1522
|
+
* `joinRelay` options. A relay room has no state and no RPC; D70 adds the one thing a schema can
|
|
1523
|
+
* still describe on it — `schema` names the project's deployed schema, which lets this client
|
|
1524
|
+
* bring its real hash instead of the zero hash and send typed messages.
|
|
1525
|
+
*/
|
|
1526
|
+
interface JoinRelayOptions<S extends AnySchema = AnySchema> {
|
|
1527
|
+
/**
|
|
1528
|
+
* D70: the schema this project deployed with `irtio deploy` and no room file. The HELLO carries
|
|
1529
|
+
* its hash, the relay host checks it against what it was registered with, and `room.messages`
|
|
1530
|
+
* comes from it. Omitted ⇒ the zero hash and no typed messages, exactly as before.
|
|
1531
|
+
*/
|
|
1532
|
+
readonly schema?: S;
|
|
1374
1533
|
readonly room?: string;
|
|
1375
1534
|
readonly role?: string;
|
|
1376
1535
|
readonly name?: string;
|
|
@@ -1390,6 +1549,31 @@ interface JoinRelayOptions {
|
|
|
1390
1549
|
type MessageTarget = 'all' | string | {
|
|
1391
1550
|
readonly role: string;
|
|
1392
1551
|
};
|
|
1552
|
+
/**
|
|
1553
|
+
* D70: `room.stats.messages` — what this socket did with peer messages.
|
|
1554
|
+
*
|
|
1555
|
+
* `dropped` is the one to watch. It counts typed frames this client could not read: an index its
|
|
1556
|
+
* schema does not have, or a payload the codec refused. Every one of them is a peer running a
|
|
1557
|
+
* schema this client does not, so a number climbing here means a stale tab, a stale deploy, or
|
|
1558
|
+
* somebody probing — never a bug in the game reading it.
|
|
1559
|
+
*/
|
|
1560
|
+
interface RoomMessageStats {
|
|
1561
|
+
readonly sent: number;
|
|
1562
|
+
readonly received: number;
|
|
1563
|
+
readonly dropped: number;
|
|
1564
|
+
}
|
|
1565
|
+
/** D70: `room.stats`. One member today; the shape exists so later counters have somewhere to go. */
|
|
1566
|
+
interface RoomStats {
|
|
1567
|
+
readonly messages: RoomMessageStats;
|
|
1568
|
+
}
|
|
1569
|
+
/**
|
|
1570
|
+
* D70: `room.messages` — one channel per shape the schema declares, `{}` when it declares none.
|
|
1571
|
+
*
|
|
1572
|
+
* `send` takes exactly the declared value and `on` hands back exactly the same, so a shape change
|
|
1573
|
+
* is a compile error on both sides at once. Fire and forget: there is no reply, no acknowledgement
|
|
1574
|
+
* and no ordering promise beyond one socket's own frames.
|
|
1575
|
+
*/
|
|
1576
|
+
type RoomMessages<S> = MessageChannels<S, MessageTarget, 'server' | string, Unsubscribe>;
|
|
1393
1577
|
/**
|
|
1394
1578
|
* D65: the client's view of where its bytes went, by collection and field.
|
|
1395
1579
|
*
|
|
@@ -1415,6 +1599,44 @@ interface RoomProfile {
|
|
|
1415
1599
|
* a predicted body passes through them.
|
|
1416
1600
|
*/
|
|
1417
1601
|
declare const MAX_PREDICTED_BODIES = 64;
|
|
1602
|
+
/**
|
|
1603
|
+
* D71: default cap on **kinematic proxies** per client — instances the local world does not
|
|
1604
|
+
* simulate but does have to be able to collide with (over `MAX_PREDICTED_BODIES`, or in a
|
|
1605
|
+
* collection that is not `predicted`).
|
|
1606
|
+
*
|
|
1607
|
+
* Its own number, measured rather than inherited from the prediction cap, because a proxy is a
|
|
1608
|
+
* different cost: a pose write and a collider in the broad phase, not a body being integrated and
|
|
1609
|
+
* solved. `packages/client/test/proxy-bench.test.ts` is the measurement and prints its whole table
|
|
1610
|
+
* on every run.
|
|
1611
|
+
*
|
|
1612
|
+
* Measured 2026-09-03 on the development machine (Windows 11, node 22), 64 predicted bodies plus N
|
|
1613
|
+
* proxies, all boxes in contact on a floor, median of 200 samples for a step and 20 for a
|
|
1614
|
+
* forty-step rebase:
|
|
1615
|
+
*
|
|
1616
|
+
* | N | rapier step | rapier rebase | matter step | matter rebase |
|
|
1617
|
+
* |---|---|---|---|---|
|
|
1618
|
+
* | 0 | 0.0055 ms | 6.66 ms | 0.0458 ms | 2.16 ms |
|
|
1619
|
+
* | 50 | 0.0146 | 7.71 | 0.0623 | 2.38 |
|
|
1620
|
+
* | 100 | 0.0096 | 8.01 | 0.0659 | 2.63 |
|
|
1621
|
+
* | 200 | 0.0118 | 7.22 | 0.0868 | 3.59 |
|
|
1622
|
+
* | 400 | 0.0217 | 7.56 | 0.1608 | 6.32 |
|
|
1623
|
+
*
|
|
1624
|
+
* So a proxy costs about 0.04 microseconds a step on Rapier and 0.29 on matter — three to four
|
|
1625
|
+
* orders of magnitude inside the 4 ms per step `games/dive/spike` budgets for the server's world,
|
|
1626
|
+
* which means the step budget does not bind at any count worth having. What binds is the rebase,
|
|
1627
|
+
* which runs on every authoritative arrival and has to fit inside a frame: at 200 both engines sit
|
|
1628
|
+
* at 3.6 to 7.2 ms against a 60 Hz frame's 16.7.
|
|
1629
|
+
*
|
|
1630
|
+
* The same bench inside a full `pnpm test`, with the rest of the suite running in parallel workers,
|
|
1631
|
+
* reads two to three times that: the step stays far inside its budget (0.023 ms rapier, 0.216 ms
|
|
1632
|
+
* matter at 200) but the rebase reaches 15.6 and 12.1 ms at 200 and 22.0 and 19.2 ms at 400. That
|
|
1633
|
+
* loaded reading is why 400 is not the default even though it fits on a quiet machine: a busy
|
|
1634
|
+
* device is the normal case, and the rebase is the number that stretches on one.
|
|
1635
|
+
*
|
|
1636
|
+
* Four times the prediction cap is also the honest ratio between the two costs. Raise it with
|
|
1637
|
+
* `maxProxyBodies` and watch `stats.lastResimMicros`, which is the rebase column measured live.
|
|
1638
|
+
*/
|
|
1639
|
+
declare const MAX_PROXY_BODIES = 256;
|
|
1418
1640
|
/** Default correction-suppression epsilon, world units (see `ClientPhysicsOptions.epsilon`). */
|
|
1419
1641
|
declare const PREDICTION_EPSILON = 0.05;
|
|
1420
1642
|
/**
|
|
@@ -1437,6 +1659,15 @@ interface PredictionStatus {
|
|
|
1437
1659
|
readonly active: boolean;
|
|
1438
1660
|
/** Is `collection[id]` currently simulated in the local world? */
|
|
1439
1661
|
predicts(collection: string, id: string): boolean;
|
|
1662
|
+
/**
|
|
1663
|
+
* D71: does `collection[id]` have a **kinematic proxy** in the local world — a collider at the
|
|
1664
|
+
* pose the renderer draws, which predicted bodies collide with and never move?
|
|
1665
|
+
*
|
|
1666
|
+
* Never true at the same time as `predicts`: an instance is simulated locally, or proxied, or
|
|
1667
|
+
* absent. `!predicts && !proxied` with the instance present is the absent case, and that is the
|
|
1668
|
+
* one a predicted body falls through.
|
|
1669
|
+
*/
|
|
1670
|
+
proxied(collection: string, id: string): boolean;
|
|
1440
1671
|
readonly stats: PredictionStats;
|
|
1441
1672
|
}
|
|
1442
1673
|
/**
|
|
@@ -1498,13 +1729,24 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
|
1498
1729
|
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
1499
1730
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
1500
1731
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
1732
|
+
/** D70: the declared message shapes. A raw `onMessage` callback never sees one of these. */
|
|
1733
|
+
readonly messages: RoomMessages<S>;
|
|
1734
|
+
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1735
|
+
readonly stats: RoomStats;
|
|
1501
1736
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1502
1737
|
/** Sends any pending owned writes immediately instead of at the next flush window. */
|
|
1503
1738
|
flush(): void;
|
|
1504
1739
|
leave(): void;
|
|
1505
1740
|
}
|
|
1506
|
-
/**
|
|
1507
|
-
|
|
1741
|
+
/**
|
|
1742
|
+
* What `joinRelay` returns: presence and the message channel, nothing else.
|
|
1743
|
+
*
|
|
1744
|
+
* D70 gives it a type parameter. `joinRelay()` with no schema is `RelayRoom<never>` and behaves
|
|
1745
|
+
* exactly as it did — `messages` is `{}` — while `joinRelay({ schema })` against a project that
|
|
1746
|
+
* deployed one gets the same typed channels a coded room has. There is still no state and still
|
|
1747
|
+
* no RPC: a schema on a relay room describes messages, and lane K is what makes it describe more.
|
|
1748
|
+
*/
|
|
1749
|
+
interface RelayRoom<S = never> {
|
|
1508
1750
|
readonly me: string;
|
|
1509
1751
|
readonly id: string;
|
|
1510
1752
|
/**
|
|
@@ -1519,6 +1761,10 @@ interface RelayRoom {
|
|
|
1519
1761
|
readonly clients: readonly PresenceRecord[];
|
|
1520
1762
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
1521
1763
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
1764
|
+
/** D70: the declared message shapes; `{}` for a relay join that brought no schema. */
|
|
1765
|
+
readonly messages: RoomMessages<S>;
|
|
1766
|
+
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1767
|
+
readonly stats: RoomStats;
|
|
1522
1768
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1523
1769
|
leave(): void;
|
|
1524
1770
|
}
|
|
@@ -1805,6 +2051,25 @@ declare class RenderStore {
|
|
|
1805
2051
|
attachPredictor(predictor: Predictor): void;
|
|
1806
2052
|
/** The interpolated (or predicted, or authoritative) value of `collection[id]` right now. */
|
|
1807
2053
|
get(desc: CollectionDesc, id: string): unknown;
|
|
2054
|
+
/**
|
|
2055
|
+
* M6 lane D (D71): the body-channel values the renderer is drawing for one instance right now,
|
|
2056
|
+
* written into `into`. `true` when there is a pose to draw at all.
|
|
2057
|
+
*
|
|
2058
|
+
* This is what a kinematic proxy is moved to before every local step, and it deliberately does
|
|
2059
|
+
* **not** go through `get`. `get` asks the predictor first — which would recurse, since it calls
|
|
2060
|
+
* `frame()` — and for an owned or predicted instance it answers out of the local world, which is
|
|
2061
|
+
* the one answer a proxy must never be given (a proxy driven by the local world is a body driving
|
|
2062
|
+
* itself). So this is the D20 buffer and nothing else: the authoritative pose interpolated at
|
|
2063
|
+
* `interpDelayMs` behind arrival, held at the newest delta and never extrapolated past it.
|
|
2064
|
+
*
|
|
2065
|
+
* Allocation-free, and only the physics channels: it runs once per proxy per frame.
|
|
2066
|
+
*
|
|
2067
|
+
* Two things `get` does are left out on purpose. `starved` is not counted, because that number
|
|
2068
|
+
* belongs to the render path and a physics read landing in it would double it. And a buffer whose
|
|
2069
|
+
* entity has aged out is not `gc`'d here, because a read that drives physics must not decide when
|
|
2070
|
+
* the render path's buffers die.
|
|
2071
|
+
*/
|
|
2072
|
+
drawn(desc: CollectionDesc, id: string, into: Record<string, number>): boolean;
|
|
1808
2073
|
/** Is `collection[id]` visible at the render clock? */
|
|
1809
2074
|
has(desc: CollectionDesc, id: string): boolean;
|
|
1810
2075
|
/** The authoritative owner — ownership is fact, not a rendered value. */
|
|
@@ -1906,8 +2171,11 @@ declare class Session {
|
|
|
1906
2171
|
* whatever a `SCHEMA` frame replaced it with. Read instead of `options.schema` everywhere, so a
|
|
1907
2172
|
* swapped session announces its *current* hash when it reconnects rather than the one it was
|
|
1908
2173
|
* born with — otherwise a resume after a swap would be refused as a mismatch.
|
|
2174
|
+
*
|
|
2175
|
+
* D70 made it internally readable rather than private: `buildMessages` reads the declared
|
|
2176
|
+
* shapes off it when the room object is built. Still not on the public `Room` type.
|
|
1909
2177
|
*/
|
|
1910
|
-
|
|
2178
|
+
schema: AnySchema | undefined;
|
|
1911
2179
|
/** D50: how many times this session has swapped schema. Test seam and a diagnostic. */
|
|
1912
2180
|
schemaSwaps: number;
|
|
1913
2181
|
private readonly transport;
|
|
@@ -1969,6 +2237,24 @@ declare class Session {
|
|
|
1969
2237
|
* `INTERNAL_SESSION`, the same route the D50 tests use for the session itself.
|
|
1970
2238
|
*/
|
|
1971
2239
|
private readonly voiceListeners;
|
|
2240
|
+
/**
|
|
2241
|
+
* D70: typed-message listeners, by wire index, in their own map for the same structural reason
|
|
2242
|
+
* `voiceListeners` is its own set: a raw `onMessage` callback must never be handed a typed
|
|
2243
|
+
* frame and a typed callback must never be handed raw bytes. Splitting the registries makes
|
|
2244
|
+
* that a fact about the shape of the code rather than a filter somewhere in the fan-out.
|
|
2245
|
+
*/
|
|
2246
|
+
private readonly typedListeners;
|
|
2247
|
+
/** D70: one warning per session for dropped typed messages — a flood must stay one line. */
|
|
2248
|
+
private typedDropWarned;
|
|
2249
|
+
/**
|
|
2250
|
+
* D70: what `room.stats.messages` reads. `dropped` is the one worth watching: it counts typed
|
|
2251
|
+
* frames this client could not decode, which is a peer on a schema this one does not have.
|
|
2252
|
+
*/
|
|
2253
|
+
readonly messageCounts: {
|
|
2254
|
+
sent: number;
|
|
2255
|
+
received: number;
|
|
2256
|
+
dropped: number;
|
|
2257
|
+
};
|
|
1972
2258
|
private cancelFlush;
|
|
1973
2259
|
private cancelPing;
|
|
1974
2260
|
private cancelRetry;
|
|
@@ -2073,6 +2359,18 @@ declare class Session {
|
|
|
2073
2359
|
private onError;
|
|
2074
2360
|
private onPong;
|
|
2075
2361
|
private onMsg;
|
|
2362
|
+
/**
|
|
2363
|
+
* D70: decode one typed payload against the current schema and hand it to that shape's
|
|
2364
|
+
* listeners.
|
|
2365
|
+
*
|
|
2366
|
+
* Everything a hostile peer can do here ends in the same place: a counter and a dropped frame.
|
|
2367
|
+
* An index past the schema's list, a truncated payload, an oversize `str`, an over-max `list`,
|
|
2368
|
+
* an unknown enum member, or a well-formed value of the wrong shape — each is caught, none
|
|
2369
|
+
* throws out of the frame loop, none closes the socket, and the next frame is delivered
|
|
2370
|
+
* normally. The `console.warn` fires once per session so a flood stays one line.
|
|
2371
|
+
*/
|
|
2372
|
+
private deliverTyped;
|
|
2373
|
+
private dropTypedMessage;
|
|
2076
2374
|
private startTimers;
|
|
2077
2375
|
/**
|
|
2078
2376
|
* The write batcher: one window per animation frame in a browser, or per `writeIntervalMs`
|
|
@@ -2094,6 +2392,13 @@ declare class Session {
|
|
|
2094
2392
|
private rejectPending;
|
|
2095
2393
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
2096
2394
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
2395
|
+
/**
|
|
2396
|
+
* D70: send one typed message. `payload` is already `encodeFields`'d by the caller
|
|
2397
|
+
* (`buildMessages`), which is where the shape and the throw on a bad value belong.
|
|
2398
|
+
*/
|
|
2399
|
+
typedMessage(index: number, target: MessageTarget, payload: Uint8Array): void;
|
|
2400
|
+
/** D70: subscribe to one message shape by wire index. Raw listeners never see these frames. */
|
|
2401
|
+
onTypedMessage(index: number, cb: (from: 'server' | string, value: Record<string, unknown>) => void): Unsubscribe;
|
|
2097
2402
|
/**
|
|
2098
2403
|
* D51: write one voice signaling message. Internal — reached only through `INTERNAL_SESSION`,
|
|
2099
2404
|
* so it is not on the `Room` type and a game cannot call it.
|
|
@@ -2279,6 +2584,6 @@ declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> &
|
|
|
2279
2584
|
* channel. A separate entry rather than a schema-less `joinRoom` overload, because everything a
|
|
2280
2585
|
* `Room` promises about state would be a lie here.
|
|
2281
2586
|
*/
|
|
2282
|
-
declare function joinRelay(options?: JoinRelayOptions): Promise<RelayRoom
|
|
2587
|
+
declare function joinRelay<S extends AnySchema = never>(options?: JoinRelayOptions<S extends AnySchema ? S : AnySchema>): Promise<RelayRoom<S>>;
|
|
2283
2588
|
|
|
2284
|
-
export { ACCOUNT_STORAGE_KEY, CALL_TIMEOUT_MS, type ClientBody2dFactory, type ClientBody2dSpec, type ClientBodySpec, type ClientCollection, type ClientIntent2dHook, type ClientMatterBody, type ClientMatterConstraint, type ClientMatterEngine, type ClientMatterModule, type ClientPhysics2dOptions, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector2, type ClientVector3, type Correction, DEFAULT_CONTROL_URL, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, E_IDENTITY_RATE_LIMITED, type FrameHook, INTERNAL_VOICE_TRACKS, Identity, IdentityError, type IdentityOptions, type IdentityStorage, type JoinOptions, type JoinRelayOptions, MAX_IDENTITY_RETRY_WAIT_MS, MAX_PREDICTED_BODIES, MatchError, type MatchOptions, type MatchTicket, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PartyTicket, type PredictionStats, type PredictionStatus, REGION_RE, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type RoomProfile, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, type VoiceHandle, type VoiceOptions, type VoicePeerState, type VoiceTrackAccess, createParty, defaultScheduler, findMatch, identityStorageKey, joinRelay, joinRoom, joinVoice, linkForUrl, matchRoom, resolveUrl, roomIdFrom, webSocketTransport };
|
|
2589
|
+
export { ACCOUNT_STORAGE_KEY, CALL_TIMEOUT_MS, type ClientBody2dFactory, type ClientBody2dSpec, type ClientBodySpec, type ClientCollection, type ClientIntent2dHook, type ClientMatterBody, type ClientMatterConstraint, type ClientMatterEngine, type ClientMatterModule, type ClientPhysics2dOptions, type ClientPhysicsOptions, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector2, type ClientVector3, type Correction, DEFAULT_CONTROL_URL, DEFAULT_REGION, DEFAULT_WRITE_INTERVAL_MS, DEV_PORT, E_CONNECT_FAILED, E_IDENTITY_RATE_LIMITED, type FrameHook, INTERNAL_VOICE_TRACKS, Identity, IdentityError, type IdentityOptions, type IdentityStorage, type JoinOptions, type JoinRelayOptions, MAX_IDENTITY_RETRY_WAIT_MS, MAX_PREDICTED_BODIES, MAX_PROXY_BODIES, MatchError, type MatchOptions, type MatchTicket, type MessageTarget, PING_INTERVAL_MS, PREDICTION_EPSILON, type PartyTicket, type PredictionStats, type PredictionStatus, REGION_RE, RESIM_DEPTH, type RelayRoom, type Room, type RoomCallProxy, type RoomError, type RoomEvents, type RoomMessageStats, type RoomMessages, type RoomProfile, type RoomStats, SMOOTHING_HALF_LIFE_MS, SMOOTHING_SNAP_UNITS, type Scheduler, Session, type Status, type Transport, type TransportSocket, type Unsubscribe, type VoiceHandle, type VoiceOptions, type VoicePeerState, type VoiceTrackAccess, createParty, defaultScheduler, findMatch, identityStorageKey, joinRelay, joinRoom, joinVoice, linkForUrl, matchRoom, resolveUrl, roomIdFrom, webSocketTransport };
|