@irtio/client 0.6.0 → 0.8.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-6J6PKFUS.js → chunk-5Z4DHUA3.js} +5 -0
- package/dist/{chunk-TE66EDBB.js → chunk-XFD6WYQW.js} +251 -39
- package/dist/index.d.ts +705 -37
- package/dist/index.js +436 -11
- package/dist/{physics-S5LHL3HK.js → physics-4SGKNBAC.js} +26 -3
- package/dist/physics-rapier2d-TBOAJMVM.js +186 -0
- package/dist/{physics2d-ZV2IRRRZ.js → physics2d-CNSEWKP4.js} +51 -2
- package/package.json +8 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel,
|
|
1
|
+
import { AnySchema, PlainState, CollectionDesc, Delta, EntityCollection, PhysicsBodyChannel, ClientImplementations, ClientRpcs, SchemaRpc, RoleOf, VisibleKeys, SchemaDefs, EntityDef, ReadonlyCollection, DeepReadonly, InferFields, Owned, SingletonDef, ClientCallProxy, MessageChannels } from '@irtio/schema';
|
|
2
2
|
import * as _irtio_protocol from '@irtio/protocol';
|
|
3
3
|
import { PresenceRecord, ProfileLedger, ProfileSnapshot } from '@irtio/protocol';
|
|
4
|
+
import RAPIER2D from '@dimforge/rapier2d-compat';
|
|
4
5
|
import * as MATTER from 'matter-js';
|
|
5
6
|
import RAPIER from '@dimforge/rapier3d-compat';
|
|
6
7
|
|
|
@@ -280,6 +281,23 @@ interface EngineAdapter {
|
|
|
280
281
|
/** Factory call plus every engine-specific warning about what it returned. */
|
|
281
282
|
createBody(desc: CollectionDesc, id: string, record: AnyRecord$2, warn: (key: string, message: string) => void): EngineBody | undefined;
|
|
282
283
|
removeBody(body: EngineBody): void;
|
|
284
|
+
/**
|
|
285
|
+
* D71: turn a body the factory just built into a **kinematic proxy**. It keeps the collider the
|
|
286
|
+
* factory gave it, it is never moved by gravity, by contact or by any other local force, and it
|
|
287
|
+
* goes only where {@link EngineAdapter.moveKinematic} puts it. Called once, immediately after
|
|
288
|
+
* `createBody`, and never on a body the local world is simulating.
|
|
289
|
+
*/
|
|
290
|
+
makeKinematic(body: EngineBody): void;
|
|
291
|
+
/**
|
|
292
|
+
* D71: put a kinematic proxy at `pose` for the next step, so that contacts see the velocity the
|
|
293
|
+
* move implies and a predicted body resting on it is carried.
|
|
294
|
+
*
|
|
295
|
+
* Called before every local step. Asking for the same pose twice moves nothing and implies no
|
|
296
|
+
* velocity — which is what "held, never extrapolated" means at the engine seam: a proxy driven
|
|
297
|
+
* through a forty-tick rebase stands still for all of it rather than running the same delta forty
|
|
298
|
+
* times.
|
|
299
|
+
*/
|
|
300
|
+
moveKinematic(body: EngineBody, pose: Pose): void;
|
|
283
301
|
/** One fixed-timestep step of the local world. */
|
|
284
302
|
step(): void;
|
|
285
303
|
/** Server record → body state, in whatever order this engine's setters require. */
|
|
@@ -302,9 +320,20 @@ interface EngineAdapter {
|
|
|
302
320
|
*/
|
|
303
321
|
velocityTolerance(epsilon: number, timestepSeconds: number): number;
|
|
304
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* D71: where a kinematic proxy goes — the body-channel values the renderer is drawing for one
|
|
325
|
+
* instance at this instant, written into `into`. `false` when there is nothing drawn (the entity
|
|
326
|
+
* has not appeared at the render clock, or has left it).
|
|
327
|
+
*
|
|
328
|
+
* Backed by `RenderStore.drawn`, the D20 interpolation buffer: the authoritative pose at
|
|
329
|
+
* `interpDelayMs` behind arrival, **held** past the newest delta rather than extrapolated. The
|
|
330
|
+
* predictor never asks it for a time, so it can never be asked for the future.
|
|
331
|
+
*/
|
|
332
|
+
type DrawnReader = (desc: CollectionDesc, id: string, into: Record<string, number>) => boolean;
|
|
305
333
|
/** The engine-neutral tuning knobs; both engines' option objects carry these names. */
|
|
306
334
|
interface PredictorTuning {
|
|
307
335
|
readonly maxPredictedBodies?: number;
|
|
336
|
+
readonly maxProxyBodies?: number;
|
|
308
337
|
readonly epsilon?: number;
|
|
309
338
|
readonly smoothingHalfLifeMs?: number;
|
|
310
339
|
readonly smoothingSnapUnits?: number;
|
|
@@ -317,16 +346,42 @@ interface PredictionStats {
|
|
|
317
346
|
resimSteps: number;
|
|
318
347
|
/** Rebase passes (one per authoritative arrival batch). */
|
|
319
348
|
rebases: number;
|
|
320
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Rebases that rendered every body at authority. Since bugs.md #71 an outrun lead clamps
|
|
351
|
+
* instead of snapping (`leadClamped`), so nothing increments this today; the field stays
|
|
352
|
+
* because a zero here is a fence several suites hold, and a future hard-snap path would count
|
|
353
|
+
* here again.
|
|
354
|
+
*/
|
|
321
355
|
snaps: number;
|
|
356
|
+
/**
|
|
357
|
+
* Rebases whose lead outran `MAX_LEAD` and were re-stepped that many ticks instead (bugs.md
|
|
358
|
+
* #71). The local character still answers input; the anchor sits closer to authority than the
|
|
359
|
+
* round trip wants, so releases overshoot by the difference. A value that climbs and keeps
|
|
360
|
+
* climbing means the measured rtt is past the cap's horizon (667 ms at 60 Hz) — degraded
|
|
361
|
+
* prediction, where a snap would have been none.
|
|
362
|
+
*/
|
|
363
|
+
leadClamped: number;
|
|
322
364
|
/** Corrections whose values matched the local prediction within epsilon. */
|
|
323
365
|
suppressed: number;
|
|
324
366
|
/**
|
|
325
|
-
* Non-owned predicted instances currently over
|
|
326
|
-
*
|
|
327
|
-
*
|
|
367
|
+
* Non-owned predicted instances currently over `maxPredictedBodies`.
|
|
368
|
+
*
|
|
369
|
+
* Since D71 these are **proxied** rather than absent: each one still has a collider in the local
|
|
370
|
+
* world, moved to the pose the renderer draws, so a predicted body stands on it instead of
|
|
371
|
+
* falling through. What it no longer has is a simulation of its own — shove it and nothing
|
|
372
|
+
* happens locally until the server agrees. `stats.absent` is the number that means "you can fall
|
|
373
|
+
* through this"; this one means "you cannot push this".
|
|
328
374
|
*/
|
|
329
375
|
overCap: number;
|
|
376
|
+
/** D71: kinematic proxies in the local world right now. */
|
|
377
|
+
proxies: number;
|
|
378
|
+
/**
|
|
379
|
+
* D71: non-owned instances with **no body in the local world at all** — over `maxProxyBodies`,
|
|
380
|
+
* opted out with `proxy: false`, or with no body factory on the client. Predicted bodies pass
|
|
381
|
+
* straight through these until the next correction snaps them back, so a non-zero value on a
|
|
382
|
+
* collection anything stands on is a gameplay bug rather than a fidelity tradeoff.
|
|
383
|
+
*/
|
|
384
|
+
absent: number;
|
|
330
385
|
/** Microseconds spent in the last rebase's re-steps. */
|
|
331
386
|
lastResimMicros: number;
|
|
332
387
|
/**
|
|
@@ -389,11 +444,19 @@ declare class Predictor {
|
|
|
389
444
|
/** Seconds per step, fixed for the life of the world. */
|
|
390
445
|
private timestepSeconds;
|
|
391
446
|
private readonly bodies;
|
|
447
|
+
/** D71: kinematic proxies, keyed like `bodies`. The two maps are disjoint by construction. */
|
|
448
|
+
private readonly proxies;
|
|
392
449
|
/** Physics-backed entity collections, in schema order. D50: not `readonly`, see `swapSchema`. */
|
|
393
450
|
private collections;
|
|
394
451
|
private readonly warned;
|
|
395
452
|
/** Highest over-cap count warned about per collection, so growth re-warns and noise does not. */
|
|
396
453
|
private readonly overCapHigh;
|
|
454
|
+
/** The same, for the proxy cap: `absent` is the count a game has to act on (D71-d). */
|
|
455
|
+
private readonly absentHigh;
|
|
456
|
+
/** D71: where a proxy goes. Wired by the session; see {@link DrawnReader}. */
|
|
457
|
+
private drawnReader;
|
|
458
|
+
/** `drawnReader`'s output buffer, reused: it is called once per proxy per frame. */
|
|
459
|
+
private readonly drawnChannels;
|
|
397
460
|
/** Authority arrived since the last frame: rebase before free-running. */
|
|
398
461
|
private authorityDirty;
|
|
399
462
|
private accumulatorMs;
|
|
@@ -466,6 +529,11 @@ declare class Predictor {
|
|
|
466
529
|
* `PredictedBody.desc` and re-deriving collider shapes from the new descriptors.
|
|
467
530
|
*/
|
|
468
531
|
swapSchema(newExt: AnySchema): void;
|
|
532
|
+
/**
|
|
533
|
+
* D71: tell the predictor where its proxies go. The session calls this once, with a reader over
|
|
534
|
+
* the D20 interpolation buffer; without it a proxy holds the pose it was built at.
|
|
535
|
+
*/
|
|
536
|
+
setDrawnReader(reader: DrawnReader): void;
|
|
469
537
|
get epsilon(): number;
|
|
470
538
|
/** `true` once the engine is loaded and the local world exists. */
|
|
471
539
|
get ready(): boolean;
|
|
@@ -509,8 +577,19 @@ declare class Predictor {
|
|
|
509
577
|
* that is the lead, not this.
|
|
510
578
|
*/
|
|
511
579
|
noteWriteApplied(stampTick: number, appliedTick: number): void;
|
|
512
|
-
/**
|
|
580
|
+
/**
|
|
581
|
+
* Does the local world currently **simulate** `collection[id]`?
|
|
582
|
+
*
|
|
583
|
+
* Deliberately still false for a proxied instance (D71). Every caller of this asks it to decide
|
|
584
|
+
* whether the local world is the better answer than authority — the render read path, the
|
|
585
|
+
* correction classifier, `room.prediction.predicts` — and for a proxy it is not: a proxy is
|
|
586
|
+
* authority, one interpolation delay old, put into the world so that other bodies can touch it.
|
|
587
|
+
* Reading it back would be a round trip through the physics engine to learn what the render
|
|
588
|
+
* buffer already said. {@link proxied} is the question about proxies.
|
|
589
|
+
*/
|
|
513
590
|
has(collection: string, id: string): boolean;
|
|
591
|
+
/** D71: does `collection[id]` have a kinematic proxy in the local world right now? */
|
|
592
|
+
hasProxy(collection: string, id: string): boolean;
|
|
514
593
|
/**
|
|
515
594
|
* Is a correction's every value within the suppression tolerance of the prediction it judges?
|
|
516
595
|
* Position (and rotation) channels compare against `epsilon` directly; velocity and angular
|
|
@@ -615,24 +694,77 @@ declare class Predictor {
|
|
|
615
694
|
*/
|
|
616
695
|
predictedValues(desc: CollectionDesc, id: string, fields: readonly string[], atTick?: number): AnyRecord$2 | undefined;
|
|
617
696
|
/**
|
|
618
|
-
* Mirrors the local world
|
|
619
|
-
* instance it owns
|
|
620
|
-
*
|
|
621
|
-
*
|
|
697
|
+
* Mirrors the local world onto the instances this client can see: a **simulated** body for every
|
|
698
|
+
* physics instance it owns plus non-owned instances of `predicted: true` collections up to
|
|
699
|
+
* `maxPredictedBodies`, and a **kinematic proxy** (D71) for everything else that has a body
|
|
700
|
+
* factory and has not opted out with `proxy: false`, up to `maxProxyBodies`.
|
|
701
|
+
*
|
|
702
|
+
* Slot order is collection order then insertion order — the same iteration guarantee the server
|
|
703
|
+
* states — so which instances fall over either cap is deterministic. There is no relevance
|
|
704
|
+
* policy and no distance ordering: the ninth crate spawned is still the ninth crate, it is just
|
|
705
|
+
* now solid rather than missing.
|
|
622
706
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
707
|
+
* Three transitions happen here, and each is a removal before a creation so the world never holds
|
|
708
|
+
* two colliders for one id:
|
|
709
|
+
*
|
|
710
|
+
* - **Promotion.** A proxied instance of a predicted collection finds a free slot (a predicted
|
|
711
|
+
* body was removed, or the cap was raised). `createBody` applies the authority record, velocity
|
|
712
|
+
* channels included, so the body carries on rather than starting from rest.
|
|
713
|
+
* - **Demotion.** A simulated instance loses its slot to an earlier-collection newcomer. That is
|
|
714
|
+
* what this loop already did before proxies — it stopped marking the body live and the sweep
|
|
715
|
+
* below removed it — and now it becomes a proxy at the drawn pose in the same frame. It costs
|
|
716
|
+
* one visible pose step: the body was a lead ahead of authority and the proxy is an
|
|
717
|
+
* interpolation delay behind it.
|
|
718
|
+
* - **Removal.** An instance the server removed, or one that left this client's area of interest
|
|
719
|
+
* (D23), loses whichever of the two it had, the same frame.
|
|
720
|
+
*
|
|
721
|
+
* What is left over — over the proxy cap, `proxy: false`, or no factory — has no collider in the
|
|
722
|
+
* local world at all, is counted in `stats.absent`, and is the only case a predicted body still
|
|
723
|
+
* falls through.
|
|
626
724
|
*/
|
|
627
725
|
private reconcileBodies;
|
|
726
|
+
/** Drops one simulated body and everything the loop keeps per body. Safe on a missing key. */
|
|
727
|
+
private removePredicted;
|
|
728
|
+
/** Drops one proxy. Safe on a missing key. */
|
|
729
|
+
private removeProxy;
|
|
730
|
+
/**
|
|
731
|
+
* D71: builds one kinematic proxy through the **same factory call** the simulated path uses, so
|
|
732
|
+
* the collider is the server's collider by construction rather than by a second description of
|
|
733
|
+
* it, then hands it to the adapter to be made kinematic.
|
|
734
|
+
*
|
|
735
|
+
* The authority record is applied first — a proxy appears where the server last said it was, not
|
|
736
|
+
* at the factory's origin — and the drawn pose takes over on the next `refreshProxyTargets`.
|
|
737
|
+
*/
|
|
738
|
+
private createProxy;
|
|
739
|
+
/**
|
|
740
|
+
* Reads every proxy's drawn pose once per frame, and only once.
|
|
741
|
+
*
|
|
742
|
+
* Once, because a rebase re-steps the world up to `MAX_LEAD` times against a render clock that
|
|
743
|
+
* has not moved: asking again per step would return the same answer at a real cost. And once
|
|
744
|
+
* *because* of D71's rule — a proxy holds one pose for every re-step of a rebase. It cannot run
|
|
745
|
+
* ahead of the newest delta because the reader never extrapolates, and it cannot run ahead of the
|
|
746
|
+
* frame because it is only ever asked here.
|
|
747
|
+
*
|
|
748
|
+
* A proxy with nothing drawn (a delta gap, an entity not yet at the render clock) keeps the pose
|
|
749
|
+
* it had. That is the hold, and it is the difference between a crate that stays solid through a
|
|
750
|
+
* stall and a crate that slides off across the level.
|
|
751
|
+
*/
|
|
752
|
+
private refreshProxyTargets;
|
|
753
|
+
/** Writes every proxy to its target pose. Runs immediately before each `adapter.step()`. */
|
|
754
|
+
private driveProxies;
|
|
628
755
|
private createBody;
|
|
629
756
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
757
|
+
* The two cap notices, both on a new high-water mark per collection: a game that grows past a cap
|
|
758
|
+
* mid-session hears about it, and a count that oscillates around one level does not turn the
|
|
759
|
+
* console into a log.
|
|
760
|
+
*
|
|
761
|
+
* They say different things now, and only the second is an emergency (D71). Over the *prediction*
|
|
762
|
+
* cap an instance is still solid — it has a proxy — it just is not simulated, so a shove does
|
|
763
|
+
* nothing locally until the server agrees. Over the *proxy* cap it is genuinely not there, which
|
|
764
|
+
* is the failure `bugs.md` #3 was about, and the message names both numbers so it is obvious
|
|
765
|
+
* which one to raise.
|
|
634
766
|
*/
|
|
635
|
-
private
|
|
767
|
+
private warnCaps;
|
|
636
768
|
private warnOnce;
|
|
637
769
|
/**
|
|
638
770
|
* The client's lead over authority, in ticks: a full round trip, rounded to the nearest tick
|
|
@@ -670,8 +802,8 @@ declare class Predictor {
|
|
|
670
802
|
* written, so plain state holds exactly what the server said), then re-step the world by the
|
|
671
803
|
* client's lead, applying to each re-stepped tick the intent that was in force *at that tick*:
|
|
672
804
|
* the newest buffered unjudged write stamped at or before it, or the baseline (the newest
|
|
673
|
-
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead
|
|
674
|
-
*
|
|
805
|
+
* judged write) before the first of them. Bounded by `MAX_LEAD`: an outrun lead re-steps that
|
|
806
|
+
* many and counts (`stats.leadClamped`, bugs.md #71).
|
|
675
807
|
*/
|
|
676
808
|
private rebase;
|
|
677
809
|
/**
|
|
@@ -696,6 +828,11 @@ declare class Predictor {
|
|
|
696
828
|
* local body of a collection, owned or not, run after the whole intent pass rather than
|
|
697
829
|
* interleaved with it. It is what gives a non-owned predicted body its gravity on an engine
|
|
698
830
|
* whose world has none of its own.
|
|
831
|
+
*
|
|
832
|
+
* Both passes walk `this.bodies`, which is why neither ever reaches a proxy (D71): a proxy is not
|
|
833
|
+
* simulated, so an intent hook or a per-step gravity force on it would be a force on a body that
|
|
834
|
+
* cannot move, applied to a pose that is going to be overwritten before the next step anyway. The
|
|
835
|
+
* isolation is structural rather than a filter, which is the version that cannot rot.
|
|
699
836
|
*/
|
|
700
837
|
private applyIntents;
|
|
701
838
|
/** Server record → body channels (the same mapping the runtime's sync uses, inverted). */
|
|
@@ -703,6 +840,126 @@ declare class Predictor {
|
|
|
703
840
|
private isF32;
|
|
704
841
|
}
|
|
705
842
|
|
|
843
|
+
/**
|
|
844
|
+
* The rapier2d half of client-side physics prediction: `joinRoom({ physics2d: { engine:
|
|
845
|
+
* 'rapier2d', … } })`'s option type, the lazy `@dimforge/rapier2d-compat` load, and the adapter
|
|
846
|
+
* that is the only place in the client where a rapier2d type is touched.
|
|
847
|
+
*
|
|
848
|
+
* The loop is `predictor.ts`, shared byte for byte with the other two engines. This file is the
|
|
849
|
+
* seam list, and it is transcribed from `physics.ts` — the Rapier one — rather than from
|
|
850
|
+
* `physics2d.ts`, for the same reason `core/rapier2d.ts` is transcribed from `core/physics.ts`:
|
|
851
|
+
*
|
|
852
|
+
* - **The world applies gravity**, so there is no `settle` hook here and there must not be one.
|
|
853
|
+
* `EngineAdapter.settle` exists for matter2d, whose engine gravity is usually zero and whose
|
|
854
|
+
* rooms apply it per body; a rapier2d world that gained one would fall twice as fast on the
|
|
855
|
+
* client and be corrected every tick.
|
|
856
|
+
* - **Velocities are per second**, so `velocityTolerance` is `epsilon / dt`, the 3D answer, not
|
|
857
|
+
* matter's per-step one.
|
|
858
|
+
* - **The step is `world.step()`** against a `world.timestep` fixed once at start.
|
|
859
|
+
*
|
|
860
|
+
* What it takes from the 2D side is only the plane: `{ x, y }` gravity, a scalar rotation and a
|
|
861
|
+
* scalar spin, mapped onto the thirteen 3D pose numbers through `@irtio/schema`'s `channelOf2d` /
|
|
862
|
+
* `applyChannel2d` / `angleFrom2d`. That mapping is written once, in the schema package, and this
|
|
863
|
+
* file reads it rather than restating it — a second copy is a way for the two sides to disagree
|
|
864
|
+
* by a sign.
|
|
865
|
+
*
|
|
866
|
+
* There is no planar-lock warning here (bug 6's friction trap needs a third axis to lock, and this
|
|
867
|
+
* engine has none), and the engine is loaded lazily, so a game predicting with matter2d or rapier3d
|
|
868
|
+
* never pulls the 2D WASM in.
|
|
869
|
+
*/
|
|
870
|
+
|
|
871
|
+
type ClientRapier2dModule = typeof RAPIER2D;
|
|
872
|
+
type ClientRapier2dWorld = RAPIER2D.World;
|
|
873
|
+
type ClientRapier2dBody = RAPIER2D.RigidBody;
|
|
874
|
+
interface ClientVector2d {
|
|
875
|
+
readonly x: number;
|
|
876
|
+
readonly y: number;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* What a client-side rapier2d body factory returns — the same shape the room config's factories
|
|
880
|
+
* use (`Rapier2dBodySpec` in `@irtio/server`). Rapier bodies *have* colliders rather than being
|
|
881
|
+
* their geometry, which is the one structural difference from the matter2d spec.
|
|
882
|
+
*/
|
|
883
|
+
interface ClientRapier2dBodySpec {
|
|
884
|
+
readonly body: RAPIER2D.RigidBodyDesc;
|
|
885
|
+
readonly colliders?: readonly RAPIER2D.ColliderDesc[];
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Method-syntax members check bivariantly, so a builder's factory or intent hook written against
|
|
889
|
+
* its own instance type (`(body, ball: Ball) => …`) is accepted — the values really passed are
|
|
890
|
+
* the schema's records for that collection.
|
|
891
|
+
*/
|
|
892
|
+
type ClientRapier2dBodyFactory = {
|
|
893
|
+
factory(rapier: ClientRapier2dModule, instance: AnyRecord$2, id: string): ClientRapier2dBodySpec;
|
|
894
|
+
}['factory'];
|
|
895
|
+
/**
|
|
896
|
+
* The client's half of `Rapier2dIntentHook`. Same five parameters in the same order as the server
|
|
897
|
+
* type, so one hook exported from a shared world module typechecks against both sides without a
|
|
898
|
+
* cast.
|
|
899
|
+
*/
|
|
900
|
+
type ClientRapier2dIntentHook = {
|
|
901
|
+
hook(body: ClientRapier2dBody, instance: AnyRecord$2, rapier: ClientRapier2dModule, world: ClientRapier2dWorld, timestep: number): void;
|
|
902
|
+
}['hook'];
|
|
903
|
+
/**
|
|
904
|
+
* `joinRoom({ physics2d: { engine: 'rapier2d', … } })` — the client half of the shared
|
|
905
|
+
* world-builder contract for a rapier2d room.
|
|
906
|
+
*
|
|
907
|
+
* It rides inside the `physics2d` option rather than beside it because it is the same *option*:
|
|
908
|
+
* a planar world, a planar gravity, one 2D engine. `engine` is the discriminant, and it is
|
|
909
|
+
* required here and optional (defaulting to `'matter2d'`) on the matter2d member, so every
|
|
910
|
+
* matter2d room written before this engine existed still compiles unchanged.
|
|
911
|
+
*
|
|
912
|
+
* There is no `settle`. See the module docblock.
|
|
913
|
+
*/
|
|
914
|
+
interface ClientRapier2dOptions {
|
|
915
|
+
/** The discriminant. Required: `physics2d` without one is a matter2d world. */
|
|
916
|
+
readonly engine: 'rapier2d';
|
|
917
|
+
/** Must equal the room config's gravity. Rapier's convention is y-up; nothing flips it. */
|
|
918
|
+
readonly gravity: ClientVector2d;
|
|
919
|
+
/** Seconds per step. Defaults to the room's tick interval (from `WELCOME`). */
|
|
920
|
+
readonly timestep?: number;
|
|
921
|
+
/** The shared static-geometry builder (the room's `physics.setup`). */
|
|
922
|
+
readonly setup?: (world: ClientRapier2dWorld, rapier: ClientRapier2dModule) => void;
|
|
923
|
+
/** Shape factories for the collections this client predicts (the room's `physics.bodies`). */
|
|
924
|
+
readonly bodies?: Readonly<Record<string, ClientRapier2dBodyFactory>>;
|
|
925
|
+
/**
|
|
926
|
+
* Intent → force, applied before every predicted step for bodies this client owns — the same
|
|
927
|
+
* function the room's `tick()` calls per instance, shared so both simulations agree.
|
|
928
|
+
*/
|
|
929
|
+
readonly intents?: Readonly<Record<string, ClientRapier2dIntentHook>>;
|
|
930
|
+
/**
|
|
931
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
932
|
+
* Over-cap instances get a kinematic proxy instead (see `maxProxyBodies`), so a predicted body
|
|
933
|
+
* still stands on them. Counted as `stats.overCap`.
|
|
934
|
+
*/
|
|
935
|
+
readonly maxPredictedBodies?: number;
|
|
936
|
+
/**
|
|
937
|
+
* D71 cap: how many **kinematic proxies** this client keeps. Default `MAX_PROXY_BODIES`. Past
|
|
938
|
+
* it, instances are absent from the local world and predicted bodies pass through them (warned,
|
|
939
|
+
* counted as `stats.absent`). See `ClientPhysicsOptions.maxProxyBodies` for why it is its own
|
|
940
|
+
* number.
|
|
941
|
+
*/
|
|
942
|
+
readonly maxProxyBodies?: number;
|
|
943
|
+
/**
|
|
944
|
+
* A body-field correction whose every value is within this tolerance of the local prediction is
|
|
945
|
+
* *suppressed*: authority still applies, but it is not a misprediction. Positions compare
|
|
946
|
+
* against `epsilon` world units; velocity channels against `epsilon / timestep`, because a
|
|
947
|
+
* rapier2d velocity is per second. Default 0.05, which is sized for a metre-scale world.
|
|
948
|
+
*/
|
|
949
|
+
readonly epsilon?: number;
|
|
950
|
+
/**
|
|
951
|
+
* How fast the drawn position eases back onto the simulation after a re-simulation moved it, as
|
|
952
|
+
* a half-life in milliseconds. `0` turns the smoothing off. Default 70. See
|
|
953
|
+
* `ClientPhysicsOptions.smoothingHalfLifeMs` for why this smooths the error and not the motion.
|
|
954
|
+
*/
|
|
955
|
+
readonly smoothingHalfLifeMs?: number;
|
|
956
|
+
/**
|
|
957
|
+
* How far the drawn position may be held from the simulation while an offset eases away, in
|
|
958
|
+
* world units. Past it the offset is dropped and the body appears where it is. Default 4.
|
|
959
|
+
*/
|
|
960
|
+
readonly smoothingSnapUnits?: number;
|
|
961
|
+
}
|
|
962
|
+
|
|
706
963
|
/**
|
|
707
964
|
* The matter2d half of client-side physics prediction (D45, D57): `joinRoom({ physics2d })`'s
|
|
708
965
|
* option type, the lazy `matter-js` load, and the adapter that is the only place in the client
|
|
@@ -777,6 +1034,11 @@ type ClientIntent2dHook = {
|
|
|
777
1034
|
* Every function here should be the very export the room config imports.
|
|
778
1035
|
*/
|
|
779
1036
|
interface ClientPhysics2dOptions {
|
|
1037
|
+
/**
|
|
1038
|
+
* The `physics2d` discriminant. Optional and defaulting to `'matter2d'`, so every matter2d
|
|
1039
|
+
* client written before rapier2d existed keeps compiling and keeps predicting with matter.
|
|
1040
|
+
*/
|
|
1041
|
+
readonly engine?: 'matter2d';
|
|
780
1042
|
/** Must equal the room config's gravity, in matter's own convention (y is down). */
|
|
781
1043
|
readonly gravity: ClientVector2;
|
|
782
1044
|
/** Seconds per step. Defaults to the room's tick interval (from `WELCOME`). */
|
|
@@ -798,12 +1060,22 @@ interface ClientPhysics2dOptions {
|
|
|
798
1060
|
*/
|
|
799
1061
|
readonly settle?: Readonly<Record<string, ClientIntent2dHook>>;
|
|
800
1062
|
/**
|
|
801
|
-
* D21 cap: how many **non-owned** predicted bodies this client simulates.
|
|
802
|
-
*
|
|
803
|
-
*
|
|
804
|
-
*
|
|
1063
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
1064
|
+
*
|
|
1065
|
+
* Since D71 an over-cap instance is not gone from the local world — it gets a kinematic proxy
|
|
1066
|
+
* (see `maxProxyBodies`), so a predicted body still stands on it and is still blocked by it.
|
|
1067
|
+
* What it loses is being simulated: shove it and nothing happens locally until the server says
|
|
1068
|
+
* it moved. Counted as `stats.overCap`.
|
|
805
1069
|
*/
|
|
806
1070
|
readonly maxPredictedBodies?: number;
|
|
1071
|
+
/**
|
|
1072
|
+
* D71 cap: how many **kinematic proxies** this client keeps — colliders at the pose the renderer
|
|
1073
|
+
* draws, for the instances it does not simulate. Default `MAX_PROXY_BODIES`. Past it, instances
|
|
1074
|
+
* are **absent from the local world** and predicted bodies pass straight through them (warned,
|
|
1075
|
+
* counted as `stats.absent`). See `ClientPhysicsOptions.maxProxyBodies` for why it is its own
|
|
1076
|
+
* number rather than a share of the prediction cap.
|
|
1077
|
+
*/
|
|
1078
|
+
readonly maxProxyBodies?: number;
|
|
807
1079
|
/**
|
|
808
1080
|
* A body-field correction whose every value is within this tolerance of the local prediction
|
|
809
1081
|
* is *suppressed*: authority still applies, but it is not a misprediction. Positions compare
|
|
@@ -885,12 +1157,28 @@ interface ClientPhysicsOptions {
|
|
|
885
1157
|
*/
|
|
886
1158
|
readonly intents?: Readonly<Record<string, ClientIntentHook>>;
|
|
887
1159
|
/**
|
|
888
|
-
* D21 cap: how many **non-owned** predicted bodies this client simulates.
|
|
889
|
-
*
|
|
890
|
-
*
|
|
891
|
-
*
|
|
1160
|
+
* D21 cap: how many **non-owned** predicted bodies this client simulates ahead. Default 64.
|
|
1161
|
+
*
|
|
1162
|
+
* Since D71 an over-cap instance is not gone from the local world — it gets a kinematic proxy
|
|
1163
|
+
* (see `maxProxyBodies`), so a predicted body still stands on it and is still blocked by it.
|
|
1164
|
+
* What it loses is being simulated: shove it and nothing happens locally until the server says
|
|
1165
|
+
* it moved. Raise this for the bodies players push; leave it for the ones they only stand on.
|
|
1166
|
+
* Counted as `stats.overCap`.
|
|
892
1167
|
*/
|
|
893
1168
|
readonly maxPredictedBodies?: number;
|
|
1169
|
+
/**
|
|
1170
|
+
* D71 cap: how many **kinematic proxies** this client keeps — colliders at the pose the renderer
|
|
1171
|
+
* draws, for the instances it does not simulate (over `maxPredictedBodies`, or in a collection
|
|
1172
|
+
* that is not `predicted`). Default `MAX_PROXY_BODIES`.
|
|
1173
|
+
*
|
|
1174
|
+
* Its own number rather than a share of `maxPredictedBodies`, because a proxy is a different
|
|
1175
|
+
* cost: a pose write and a collider in the broad phase, not a body being integrated and solved.
|
|
1176
|
+
* Past this cap instances are **absent from the local world** — predicted bodies pass through
|
|
1177
|
+
* them until the next correction snaps them back (warned, counted as `stats.absent`). Turn
|
|
1178
|
+
* `proxy: false` on in the schema for the collections nothing collides with, so the budget goes
|
|
1179
|
+
* to the ones that matter.
|
|
1180
|
+
*/
|
|
1181
|
+
readonly maxProxyBodies?: number;
|
|
894
1182
|
/**
|
|
895
1183
|
* A body-field correction whose every value is within this tolerance of the local prediction
|
|
896
1184
|
* is *suppressed*: authority still applies, but it is not a misprediction — steady state stays
|
|
@@ -1356,7 +1644,7 @@ interface JoinOptions<S, Role extends string = string> {
|
|
|
1356
1644
|
* take different functions; passing both is an error at join. The wire carries no engine
|
|
1357
1645
|
* name, so passing the one that does not match the room is a game bug the client cannot see.
|
|
1358
1646
|
*/
|
|
1359
|
-
readonly physics2d?: ClientPhysics2dOptions;
|
|
1647
|
+
readonly physics2d?: ClientPhysics2dOptions | ClientRapier2dOptions;
|
|
1360
1648
|
/** @internal */
|
|
1361
1649
|
readonly transport?: Transport;
|
|
1362
1650
|
/** @internal */
|
|
@@ -1369,8 +1657,18 @@ interface JoinOptions<S, Role extends string = string> {
|
|
|
1369
1657
|
*/
|
|
1370
1658
|
readonly profile?: boolean;
|
|
1371
1659
|
}
|
|
1372
|
-
/**
|
|
1373
|
-
|
|
1660
|
+
/**
|
|
1661
|
+
* `joinRelay` options. A relay room has no state and no RPC; D70 adds the one thing a schema can
|
|
1662
|
+
* still describe on it — `schema` names the project's deployed schema, which lets this client
|
|
1663
|
+
* bring its real hash instead of the zero hash and send typed messages.
|
|
1664
|
+
*/
|
|
1665
|
+
interface JoinRelayOptions<S extends AnySchema = AnySchema> {
|
|
1666
|
+
/**
|
|
1667
|
+
* D70: the schema this project deployed with `irtio deploy` and no room file. The HELLO carries
|
|
1668
|
+
* its hash, the relay host checks it against what it was registered with, and `room.messages`
|
|
1669
|
+
* comes from it. Omitted ⇒ the zero hash and no typed messages, exactly as before.
|
|
1670
|
+
*/
|
|
1671
|
+
readonly schema?: S;
|
|
1374
1672
|
readonly room?: string;
|
|
1375
1673
|
readonly role?: string;
|
|
1376
1674
|
readonly name?: string;
|
|
@@ -1390,6 +1688,31 @@ interface JoinRelayOptions {
|
|
|
1390
1688
|
type MessageTarget = 'all' | string | {
|
|
1391
1689
|
readonly role: string;
|
|
1392
1690
|
};
|
|
1691
|
+
/**
|
|
1692
|
+
* D70: `room.stats.messages` — what this socket did with peer messages.
|
|
1693
|
+
*
|
|
1694
|
+
* `dropped` is the one to watch. It counts typed frames this client could not read: an index its
|
|
1695
|
+
* schema does not have, or a payload the codec refused. Every one of them is a peer running a
|
|
1696
|
+
* schema this client does not, so a number climbing here means a stale tab, a stale deploy, or
|
|
1697
|
+
* somebody probing — never a bug in the game reading it.
|
|
1698
|
+
*/
|
|
1699
|
+
interface RoomMessageStats {
|
|
1700
|
+
readonly sent: number;
|
|
1701
|
+
readonly received: number;
|
|
1702
|
+
readonly dropped: number;
|
|
1703
|
+
}
|
|
1704
|
+
/** D70: `room.stats`. One member today; the shape exists so later counters have somewhere to go. */
|
|
1705
|
+
interface RoomStats {
|
|
1706
|
+
readonly messages: RoomMessageStats;
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* D70: `room.messages` — one channel per shape the schema declares, `{}` when it declares none.
|
|
1710
|
+
*
|
|
1711
|
+
* `send` takes exactly the declared value and `on` hands back exactly the same, so a shape change
|
|
1712
|
+
* is a compile error on both sides at once. Fire and forget: there is no reply, no acknowledgement
|
|
1713
|
+
* and no ordering promise beyond one socket's own frames.
|
|
1714
|
+
*/
|
|
1715
|
+
type RoomMessages<S> = MessageChannels<S, MessageTarget, 'server' | string, Unsubscribe>;
|
|
1393
1716
|
/**
|
|
1394
1717
|
* D65: the client's view of where its bytes went, by collection and field.
|
|
1395
1718
|
*
|
|
@@ -1415,6 +1738,44 @@ interface RoomProfile {
|
|
|
1415
1738
|
* a predicted body passes through them.
|
|
1416
1739
|
*/
|
|
1417
1740
|
declare const MAX_PREDICTED_BODIES = 64;
|
|
1741
|
+
/**
|
|
1742
|
+
* D71: default cap on **kinematic proxies** per client — instances the local world does not
|
|
1743
|
+
* simulate but does have to be able to collide with (over `MAX_PREDICTED_BODIES`, or in a
|
|
1744
|
+
* collection that is not `predicted`).
|
|
1745
|
+
*
|
|
1746
|
+
* Its own number, measured rather than inherited from the prediction cap, because a proxy is a
|
|
1747
|
+
* different cost: a pose write and a collider in the broad phase, not a body being integrated and
|
|
1748
|
+
* solved. `packages/client/test/proxy-bench.test.ts` is the measurement and prints its whole table
|
|
1749
|
+
* on every run.
|
|
1750
|
+
*
|
|
1751
|
+
* Measured 2026-09-03 on the development machine (Windows 11, node 22), 64 predicted bodies plus N
|
|
1752
|
+
* proxies, all boxes in contact on a floor, median of 200 samples for a step and 20 for a
|
|
1753
|
+
* forty-step rebase:
|
|
1754
|
+
*
|
|
1755
|
+
* | N | rapier step | rapier rebase | matter step | matter rebase |
|
|
1756
|
+
* |---|---|---|---|---|
|
|
1757
|
+
* | 0 | 0.0055 ms | 6.66 ms | 0.0458 ms | 2.16 ms |
|
|
1758
|
+
* | 50 | 0.0146 | 7.71 | 0.0623 | 2.38 |
|
|
1759
|
+
* | 100 | 0.0096 | 8.01 | 0.0659 | 2.63 |
|
|
1760
|
+
* | 200 | 0.0118 | 7.22 | 0.0868 | 3.59 |
|
|
1761
|
+
* | 400 | 0.0217 | 7.56 | 0.1608 | 6.32 |
|
|
1762
|
+
*
|
|
1763
|
+
* So a proxy costs about 0.04 microseconds a step on Rapier and 0.29 on matter — three to four
|
|
1764
|
+
* orders of magnitude inside the 4 ms per step `games/dive/spike` budgets for the server's world,
|
|
1765
|
+
* which means the step budget does not bind at any count worth having. What binds is the rebase,
|
|
1766
|
+
* which runs on every authoritative arrival and has to fit inside a frame: at 200 both engines sit
|
|
1767
|
+
* at 3.6 to 7.2 ms against a 60 Hz frame's 16.7.
|
|
1768
|
+
*
|
|
1769
|
+
* The same bench inside a full `pnpm test`, with the rest of the suite running in parallel workers,
|
|
1770
|
+
* reads two to three times that: the step stays far inside its budget (0.023 ms rapier, 0.216 ms
|
|
1771
|
+
* 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
|
|
1772
|
+
* loaded reading is why 400 is not the default even though it fits on a quiet machine: a busy
|
|
1773
|
+
* device is the normal case, and the rebase is the number that stretches on one.
|
|
1774
|
+
*
|
|
1775
|
+
* Four times the prediction cap is also the honest ratio between the two costs. Raise it with
|
|
1776
|
+
* `maxProxyBodies` and watch `stats.lastResimMicros`, which is the rebase column measured live.
|
|
1777
|
+
*/
|
|
1778
|
+
declare const MAX_PROXY_BODIES = 256;
|
|
1418
1779
|
/** Default correction-suppression epsilon, world units (see `ClientPhysicsOptions.epsilon`). */
|
|
1419
1780
|
declare const PREDICTION_EPSILON = 0.05;
|
|
1420
1781
|
/**
|
|
@@ -1437,6 +1798,15 @@ interface PredictionStatus {
|
|
|
1437
1798
|
readonly active: boolean;
|
|
1438
1799
|
/** Is `collection[id]` currently simulated in the local world? */
|
|
1439
1800
|
predicts(collection: string, id: string): boolean;
|
|
1801
|
+
/**
|
|
1802
|
+
* D71: does `collection[id]` have a **kinematic proxy** in the local world — a collider at the
|
|
1803
|
+
* pose the renderer draws, which predicted bodies collide with and never move?
|
|
1804
|
+
*
|
|
1805
|
+
* Never true at the same time as `predicts`: an instance is simulated locally, or proxied, or
|
|
1806
|
+
* absent. `!predicts && !proxied` with the instance present is the absent case, and that is the
|
|
1807
|
+
* one a predicted body falls through.
|
|
1808
|
+
*/
|
|
1809
|
+
proxied(collection: string, id: string): boolean;
|
|
1440
1810
|
readonly stats: PredictionStats;
|
|
1441
1811
|
}
|
|
1442
1812
|
/**
|
|
@@ -1498,13 +1868,24 @@ interface Room<S, Role extends string = RoleOf<S> & string> {
|
|
|
1498
1868
|
requestOwnership(entity: string, id: string): Promise<boolean>;
|
|
1499
1869
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
1500
1870
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
1871
|
+
/** D70: the declared message shapes. A raw `onMessage` callback never sees one of these. */
|
|
1872
|
+
readonly messages: RoomMessages<S>;
|
|
1873
|
+
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1874
|
+
readonly stats: RoomStats;
|
|
1501
1875
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1502
1876
|
/** Sends any pending owned writes immediately instead of at the next flush window. */
|
|
1503
1877
|
flush(): void;
|
|
1504
1878
|
leave(): void;
|
|
1505
1879
|
}
|
|
1506
|
-
/**
|
|
1507
|
-
|
|
1880
|
+
/**
|
|
1881
|
+
* What `joinRelay` returns: presence and the message channel, nothing else.
|
|
1882
|
+
*
|
|
1883
|
+
* D70 gives it a type parameter. `joinRelay()` with no schema is `RelayRoom<never>` and behaves
|
|
1884
|
+
* exactly as it did — `messages` is `{}` — while `joinRelay({ schema })` against a project that
|
|
1885
|
+
* deployed one gets the same typed channels a coded room has. There is still no state and still
|
|
1886
|
+
* no RPC: a schema on a relay room describes messages, and lane K is what makes it describe more.
|
|
1887
|
+
*/
|
|
1888
|
+
interface RelayRoom<S = never> {
|
|
1508
1889
|
readonly me: string;
|
|
1509
1890
|
readonly id: string;
|
|
1510
1891
|
/**
|
|
@@ -1519,6 +1900,10 @@ interface RelayRoom {
|
|
|
1519
1900
|
readonly clients: readonly PresenceRecord[];
|
|
1520
1901
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
1521
1902
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
1903
|
+
/** D70: the declared message shapes; `{}` for a relay join that brought no schema. */
|
|
1904
|
+
readonly messages: RoomMessages<S>;
|
|
1905
|
+
/** D70: this socket's message counters, including typed frames it could not read. */
|
|
1906
|
+
readonly stats: RoomStats;
|
|
1522
1907
|
on<K extends keyof RoomEvents>(event: K, cb: (value: RoomEvents[K]) => void): Unsubscribe;
|
|
1523
1908
|
leave(): void;
|
|
1524
1909
|
}
|
|
@@ -1688,6 +2073,230 @@ declare function matchRoom<S extends AnySchema, Role extends string = RoleOf<S>
|
|
|
1688
2073
|
} & {
|
|
1689
2074
|
readonly identity?: boolean;
|
|
1690
2075
|
}): Promise<Room<S, Role>>;
|
|
2076
|
+
/** What a `mode: "public"` answer carries. */
|
|
2077
|
+
interface PublicTicket {
|
|
2078
|
+
readonly room: string;
|
|
2079
|
+
readonly queue: string;
|
|
2080
|
+
/** How many players the queue this lobby belongs to seats. */
|
|
2081
|
+
readonly size: number;
|
|
2082
|
+
/** True when this caller minted the room rather than joining one somebody else was waiting in. */
|
|
2083
|
+
readonly created: boolean;
|
|
2084
|
+
}
|
|
2085
|
+
/**
|
|
2086
|
+
* Asks the control plane for a public room of `queue`: the oldest open one, or a fresh code.
|
|
2087
|
+
*
|
|
2088
|
+
* The lower half of {@link joinPublic}, exported because the element's chooser is driven by events
|
|
2089
|
+
* rather than by a call, and an app that wires its own **Join a game** button may want the code
|
|
2090
|
+
* without the join.
|
|
2091
|
+
*
|
|
2092
|
+
* `exclude` names a room this caller already failed to join, so the second answer is a different
|
|
2093
|
+
* one. See `joinPublic` for when that happens.
|
|
2094
|
+
*/
|
|
2095
|
+
declare function findPublic(project: string, options?: Pick<MatchOptions, 'queue' | 'controlUrl' | 'fetch'> & {
|
|
2096
|
+
readonly identity?: string;
|
|
2097
|
+
readonly exclude?: string;
|
|
2098
|
+
}): Promise<PublicTicket>;
|
|
2099
|
+
/**
|
|
2100
|
+
* Join a game: land in the next open public lobby, or open one and wait in it.
|
|
2101
|
+
*
|
|
2102
|
+
* ```ts
|
|
2103
|
+
* const room = await joinPublic(schema); // the default queue
|
|
2104
|
+
* const room = await joinPublic(schema, { queue: '4p', identity: true });
|
|
2105
|
+
* ```
|
|
2106
|
+
*
|
|
2107
|
+
* This is the other shape of matchmaking and it is deliberately not `matchRoom`. `matchRoom`
|
|
2108
|
+
* holds a long poll until a whole party is there and answers `E_NO_MATCH` if one never is;
|
|
2109
|
+
* `joinPublic` resolves on the first round trip and puts the player in a room that says "1/4
|
|
2110
|
+
* players". Waiting in a lobby you can see is a different experience from waiting on a spinner,
|
|
2111
|
+
* and it is the one a **Join a game** button should give.
|
|
2112
|
+
*
|
|
2113
|
+
* Nothing about the join differs from a friend sharing a link — the room neither knows nor cares
|
|
2114
|
+
* that the registry sent you — which is the property every part of this feature is built on.
|
|
2115
|
+
*
|
|
2116
|
+
* **The one retry.** The registry answers from an occupancy reading a few seconds old, so the room
|
|
2117
|
+
* may have filled on the way there. That is refused with `E_ROOM_FULL`, and the honest recovery is
|
|
2118
|
+
* to ask once more, naming the room that did not work so the second answer cannot be the same one.
|
|
2119
|
+
* Once, and never a loop: a retry that kept going would turn a busy game into a client that
|
|
2120
|
+
* hammers control, and a second failure means something a third attempt will not fix.
|
|
2121
|
+
*/
|
|
2122
|
+
declare function joinPublic<S extends AnySchema, Role extends string = RoleOf<S> & string>(schema: S, options?: Pick<MatchOptions, 'queue' | 'controlUrl' | 'fetch'> & JoinOptions<S, Role> & {
|
|
2123
|
+
role?: RoleOf<S> & string;
|
|
2124
|
+
} & {
|
|
2125
|
+
readonly identity?: boolean;
|
|
2126
|
+
}): Promise<Room<S, Role>>;
|
|
2127
|
+
|
|
2128
|
+
/**
|
|
2129
|
+
* D75 (M6 lane H): the client's half of the lobby, and the glue that wires `<irt-lobby>` up.
|
|
2130
|
+
*
|
|
2131
|
+
* Two exports and a clean division between them.
|
|
2132
|
+
*
|
|
2133
|
+
* `lobbyOf(room)` is the **view**: it reads the lobby fragment out of ordinary room state and
|
|
2134
|
+
* answers the structural shape `@irtio/lobby`'s `AttachableRoom.lobby` asks for. It is a reader
|
|
2135
|
+
* plus one owner write (the ready flag), and it holds nothing the room does not already say.
|
|
2136
|
+
*
|
|
2137
|
+
* `attachLobby(element, schema, options)` is the **glue**: it listens for the panel's two chooser
|
|
2138
|
+
* events, makes the calls the panel deliberately cannot make itself, and attaches the room it gets
|
|
2139
|
+
* back. Sugar, not capability — the same rule `matchRoom` is held to. An app that wants to answer
|
|
2140
|
+
* the events itself keeps every part of this and needs none of it.
|
|
2141
|
+
*
|
|
2142
|
+
* ## Why the view polls
|
|
2143
|
+
*
|
|
2144
|
+
* `room.on(...)` has no "state changed" event: the client SDK publishes `status`, `clients`, `rtt`,
|
|
2145
|
+
* `error` and `correct`, and lobby state is ordinary entity state that arrives in a `DELTA` like
|
|
2146
|
+
* everything else. So `on('change')` samples — on every `clients` event, and on a cheap timer in
|
|
2147
|
+
* between — and fires only when a small signature actually differs.
|
|
2148
|
+
*
|
|
2149
|
+
* That is a real cost and it is written here rather than hidden: one shallow string build per
|
|
2150
|
+
* sample, over at most `maxClients` records, for as long as somebody is watching. It is the honest
|
|
2151
|
+
* price of the element importing nothing from this package. A `state` event on `Room` would replace
|
|
2152
|
+
* the timer with a subscription and is recorded as a debt in the lane report rather than smuggled
|
|
2153
|
+
* into this lane's protocol-free budget.
|
|
2154
|
+
*/
|
|
2155
|
+
|
|
2156
|
+
/**
|
|
2157
|
+
* The two collection names the lobby fragment declares.
|
|
2158
|
+
*
|
|
2159
|
+
* Literals here rather than an import from `@irtio/server`, which is where they are defined. The
|
|
2160
|
+
* client SDK ships to browsers and depends on `@irtio/schema` and `@irtio/protocol` alone; taking
|
|
2161
|
+
* a dependency on the room-file API to read two strings would put the whole of it in every game's
|
|
2162
|
+
* bundle. This is the same fence `@irtio/server`'s own `MAX_AWAKE_MAX` sits behind, and it is
|
|
2163
|
+
* pinned the same way: `test/lobby-view.test.ts` imports both and asserts the pairs equal, from a
|
|
2164
|
+
* package that legitimately depends on both, so a rename is a failing test rather than a lobby
|
|
2165
|
+
* that silently never appears.
|
|
2166
|
+
*/
|
|
2167
|
+
declare const LOBBY_STATE = "irtLobby";
|
|
2168
|
+
declare const LOBBY_MEMBERS = "irtLobbyMembers";
|
|
2169
|
+
/** How often the view samples for a change between `clients` events. */
|
|
2170
|
+
declare const LOBBY_POLL_MS = 120;
|
|
2171
|
+
/** One row of the lobby roster, in join order. */
|
|
2172
|
+
interface LobbyPlayer {
|
|
2173
|
+
readonly clientId: string;
|
|
2174
|
+
readonly ready: boolean;
|
|
2175
|
+
readonly connected: boolean;
|
|
2176
|
+
/** True for the row belonging to the client holding this view. */
|
|
2177
|
+
readonly me: boolean;
|
|
2178
|
+
}
|
|
2179
|
+
/** What `lobbyOf` answers: `@irtio/lobby`'s structural `lobby` member, with names. */
|
|
2180
|
+
interface LobbyView {
|
|
2181
|
+
readonly phase: 'lobby' | 'started';
|
|
2182
|
+
readonly players: readonly LobbyPlayer[];
|
|
2183
|
+
readonly public: boolean;
|
|
2184
|
+
readonly readyUi: boolean;
|
|
2185
|
+
/** How many the room starts at under `'when-full'`, or 0 when the room has not said. */
|
|
2186
|
+
readonly capacity: number;
|
|
2187
|
+
ready(value: boolean): void;
|
|
2188
|
+
setPublic?(value: boolean): void;
|
|
2189
|
+
on(event: 'change', cb: () => void): () => void;
|
|
2190
|
+
}
|
|
2191
|
+
interface StateLike {
|
|
2192
|
+
[collection: string]: unknown;
|
|
2193
|
+
}
|
|
2194
|
+
interface RoomLike {
|
|
2195
|
+
readonly me: string;
|
|
2196
|
+
readonly state: StateLike;
|
|
2197
|
+
readonly clients: readonly {
|
|
2198
|
+
clientId: string;
|
|
2199
|
+
connected: boolean;
|
|
2200
|
+
}[];
|
|
2201
|
+
on(event: 'clients', cb: (clients: readonly {
|
|
2202
|
+
clientId: string;
|
|
2203
|
+
connected: boolean;
|
|
2204
|
+
}[]) => void): () => void;
|
|
2205
|
+
flush(): void;
|
|
2206
|
+
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Does this room carry a lobby?
|
|
2209
|
+
*
|
|
2210
|
+
* A schema that did not spread `lobbyCollections` has neither collection, and the honest answer for
|
|
2211
|
+
* such a room is that it has no lobby rather than an empty one — `<irt-lobby>` renders no lobby UI
|
|
2212
|
+
* at all in that case, which is what keeps every existing consumer unchanged.
|
|
2213
|
+
*/
|
|
2214
|
+
declare function hasLobby(room: unknown): boolean;
|
|
2215
|
+
/**
|
|
2216
|
+
* Reads a joined room's lobby.
|
|
2217
|
+
*
|
|
2218
|
+
* ```ts
|
|
2219
|
+
* const room = await joinPublic(schema);
|
|
2220
|
+
* lobby.attach(room, { lobby: lobbyOf(room) });
|
|
2221
|
+
* ```
|
|
2222
|
+
*
|
|
2223
|
+
* `setPublic` is present only when the caller supplies one, and that is a consequence of there
|
|
2224
|
+
* being no built-in RPC for it. The platform never takes a public toggle from a client — that
|
|
2225
|
+
* would be a protocol change — so the room's own game code owns the door. A game that wants the
|
|
2226
|
+
* panel's toggle to work declares its own RPC and passes it here:
|
|
2227
|
+
*
|
|
2228
|
+
* ```ts
|
|
2229
|
+
* lobbyOf(room, { setPublic: (value) => void room.call.setPublic({ value }) })
|
|
2230
|
+
* ```
|
|
2231
|
+
*
|
|
2232
|
+
* Without one, the panel renders the room's public state and no toggle, which is the truthful
|
|
2233
|
+
* rendering of a room whose code has not offered the control.
|
|
2234
|
+
*/
|
|
2235
|
+
declare function lobbyOf(room: unknown, options?: {
|
|
2236
|
+
readonly setPublic?: (value: boolean) => void;
|
|
2237
|
+
}): LobbyView | undefined;
|
|
2238
|
+
/** What the panel's `quickmatch` event carries. */
|
|
2239
|
+
interface QuickMatchDetail {
|
|
2240
|
+
readonly queue?: string;
|
|
2241
|
+
}
|
|
2242
|
+
/** The element surface `attachLobby` needs. Structural, so a test can pass a stand-in. */
|
|
2243
|
+
interface LobbyElementLike {
|
|
2244
|
+
addEventListener(type: string, cb: (event: Event) => void): void;
|
|
2245
|
+
removeEventListener(type: string, cb: (event: Event) => void): void;
|
|
2246
|
+
setAttribute(name: string, value: string): void;
|
|
2247
|
+
removeAttribute(name: string): void;
|
|
2248
|
+
attach(room: unknown): () => void;
|
|
2249
|
+
}
|
|
2250
|
+
/**
|
|
2251
|
+
* The room, as `<irt-lobby>`'s structural `AttachableRoom` wants it: everything the panel already
|
|
2252
|
+
* read, plus the optional `lobby` member the design's §6 adds.
|
|
2253
|
+
*
|
|
2254
|
+
* A wrapper of getters rather than a spread, because a `Room`'s fields are accessors on a class:
|
|
2255
|
+
* `{ ...room }` would freeze `status`, `id` and `clients` at the moment of the copy, and the panel
|
|
2256
|
+
* re-reads all three on every render precisely so a reconnect that changes the code is picked up.
|
|
2257
|
+
* Mutating the room to hang a `lobby` on it was the other option and is worse — the SDK's own
|
|
2258
|
+
* object would then carry a field its type does not declare.
|
|
2259
|
+
*/
|
|
2260
|
+
declare function attachable(room: RoomLike & {
|
|
2261
|
+
id: string;
|
|
2262
|
+
link: string;
|
|
2263
|
+
status: string;
|
|
2264
|
+
maxClients?: number;
|
|
2265
|
+
rtt?: number;
|
|
2266
|
+
}, view: LobbyView | undefined): unknown;
|
|
2267
|
+
interface AttachLobbyOptions<S extends AnySchema, Role extends string> {
|
|
2268
|
+
/** Passed through to `joinRoom` / `joinPublic`. */
|
|
2269
|
+
readonly join?: JoinOptions<S, Role> & {
|
|
2270
|
+
role?: RoleOf<S> & string;
|
|
2271
|
+
};
|
|
2272
|
+
/** Passed through to the control-plane call. */
|
|
2273
|
+
readonly match?: Pick<MatchOptions, 'controlUrl' | 'fetch'> & {
|
|
2274
|
+
readonly identity?: boolean;
|
|
2275
|
+
};
|
|
2276
|
+
/** The room's own public-toggle RPC, if it has one. See `lobbyOf`. */
|
|
2277
|
+
readonly setPublic?: (room: Room<S, Role>, value: boolean) => void;
|
|
2278
|
+
/** Called with the room once it is joined, so game code can start drawing. */
|
|
2279
|
+
readonly onRoom?: (room: Room<S, Role>) => void;
|
|
2280
|
+
/** Called when a join fails. Default: the panel's status goes back to idle and the error is
|
|
2281
|
+
* rethrown on the microtask queue so it reaches `window.onerror` rather than vanishing. */
|
|
2282
|
+
readonly onError?: (err: unknown) => void;
|
|
2283
|
+
}
|
|
2284
|
+
/**
|
|
2285
|
+
* Wires `<irt-lobby>`'s chooser to the two calls it deliberately cannot make itself.
|
|
2286
|
+
*
|
|
2287
|
+
* ```ts
|
|
2288
|
+
* attachLobby(document.querySelector('irt-lobby')!, schema, { onRoom: (room) => start(room) });
|
|
2289
|
+
* ```
|
|
2290
|
+
*
|
|
2291
|
+
* The panel emits and this answers: `private` becomes an ordinary `joinRoom` with no code (which
|
|
2292
|
+
* creates one), `quickmatch` becomes `joinPublic`. Both then `attach` the room, exactly as an app
|
|
2293
|
+
* doing it by hand would. Returns an unsubscribe.
|
|
2294
|
+
*
|
|
2295
|
+
* `@irtio/lobby` imports nothing from this package and never will — that is what lets a React or
|
|
2296
|
+
* Svelte app drive the panel with attributes and no SDK. This function is the other side of that
|
|
2297
|
+
* rule rather than an exception to it: the dependency points this way.
|
|
2298
|
+
*/
|
|
2299
|
+
declare function attachLobby<S extends AnySchema, Role extends string = RoleOf<S> & string>(element: LobbyElementLike, schema: S, options?: AttachLobbyOptions<S, Role>): () => void;
|
|
1691
2300
|
|
|
1692
2301
|
/**
|
|
1693
2302
|
* The default `Scheduler`. In a browser the write batcher aligns to `requestAnimationFrame` (one
|
|
@@ -1805,6 +2414,25 @@ declare class RenderStore {
|
|
|
1805
2414
|
attachPredictor(predictor: Predictor): void;
|
|
1806
2415
|
/** The interpolated (or predicted, or authoritative) value of `collection[id]` right now. */
|
|
1807
2416
|
get(desc: CollectionDesc, id: string): unknown;
|
|
2417
|
+
/**
|
|
2418
|
+
* M6 lane D (D71): the body-channel values the renderer is drawing for one instance right now,
|
|
2419
|
+
* written into `into`. `true` when there is a pose to draw at all.
|
|
2420
|
+
*
|
|
2421
|
+
* This is what a kinematic proxy is moved to before every local step, and it deliberately does
|
|
2422
|
+
* **not** go through `get`. `get` asks the predictor first — which would recurse, since it calls
|
|
2423
|
+
* `frame()` — and for an owned or predicted instance it answers out of the local world, which is
|
|
2424
|
+
* the one answer a proxy must never be given (a proxy driven by the local world is a body driving
|
|
2425
|
+
* itself). So this is the D20 buffer and nothing else: the authoritative pose interpolated at
|
|
2426
|
+
* `interpDelayMs` behind arrival, held at the newest delta and never extrapolated past it.
|
|
2427
|
+
*
|
|
2428
|
+
* Allocation-free, and only the physics channels: it runs once per proxy per frame.
|
|
2429
|
+
*
|
|
2430
|
+
* Two things `get` does are left out on purpose. `starved` is not counted, because that number
|
|
2431
|
+
* belongs to the render path and a physics read landing in it would double it. And a buffer whose
|
|
2432
|
+
* entity has aged out is not `gc`'d here, because a read that drives physics must not decide when
|
|
2433
|
+
* the render path's buffers die.
|
|
2434
|
+
*/
|
|
2435
|
+
drawn(desc: CollectionDesc, id: string, into: Record<string, number>): boolean;
|
|
1808
2436
|
/** Is `collection[id]` visible at the render clock? */
|
|
1809
2437
|
has(desc: CollectionDesc, id: string): boolean;
|
|
1810
2438
|
/** The authoritative owner — ownership is fact, not a rendered value. */
|
|
@@ -1870,7 +2498,7 @@ interface SessionOptions {
|
|
|
1870
2498
|
/** The shared world-builder half the client predicts with (D22 part 2). */
|
|
1871
2499
|
readonly physics?: ClientPhysicsOptions | undefined;
|
|
1872
2500
|
/** The same, for a matter2d room (D57). Never both; `joinRoom` refuses that. */
|
|
1873
|
-
readonly physics2d?: ClientPhysics2dOptions | undefined;
|
|
2501
|
+
readonly physics2d?: ClientPhysics2dOptions | ClientRapier2dOptions | undefined;
|
|
1874
2502
|
readonly transport?: Transport | undefined;
|
|
1875
2503
|
readonly scheduler?: Scheduler | undefined;
|
|
1876
2504
|
readonly onFrame?: FrameHook | undefined;
|
|
@@ -1906,8 +2534,11 @@ declare class Session {
|
|
|
1906
2534
|
* whatever a `SCHEMA` frame replaced it with. Read instead of `options.schema` everywhere, so a
|
|
1907
2535
|
* swapped session announces its *current* hash when it reconnects rather than the one it was
|
|
1908
2536
|
* born with — otherwise a resume after a swap would be refused as a mismatch.
|
|
2537
|
+
*
|
|
2538
|
+
* D70 made it internally readable rather than private: `buildMessages` reads the declared
|
|
2539
|
+
* shapes off it when the room object is built. Still not on the public `Room` type.
|
|
1909
2540
|
*/
|
|
1910
|
-
|
|
2541
|
+
schema: AnySchema | undefined;
|
|
1911
2542
|
/** D50: how many times this session has swapped schema. Test seam and a diagnostic. */
|
|
1912
2543
|
schemaSwaps: number;
|
|
1913
2544
|
private readonly transport;
|
|
@@ -1969,6 +2600,24 @@ declare class Session {
|
|
|
1969
2600
|
* `INTERNAL_SESSION`, the same route the D50 tests use for the session itself.
|
|
1970
2601
|
*/
|
|
1971
2602
|
private readonly voiceListeners;
|
|
2603
|
+
/**
|
|
2604
|
+
* D70: typed-message listeners, by wire index, in their own map for the same structural reason
|
|
2605
|
+
* `voiceListeners` is its own set: a raw `onMessage` callback must never be handed a typed
|
|
2606
|
+
* frame and a typed callback must never be handed raw bytes. Splitting the registries makes
|
|
2607
|
+
* that a fact about the shape of the code rather than a filter somewhere in the fan-out.
|
|
2608
|
+
*/
|
|
2609
|
+
private readonly typedListeners;
|
|
2610
|
+
/** D70: one warning per session for dropped typed messages — a flood must stay one line. */
|
|
2611
|
+
private typedDropWarned;
|
|
2612
|
+
/**
|
|
2613
|
+
* D70: what `room.stats.messages` reads. `dropped` is the one worth watching: it counts typed
|
|
2614
|
+
* frames this client could not decode, which is a peer on a schema this one does not have.
|
|
2615
|
+
*/
|
|
2616
|
+
readonly messageCounts: {
|
|
2617
|
+
sent: number;
|
|
2618
|
+
received: number;
|
|
2619
|
+
dropped: number;
|
|
2620
|
+
};
|
|
1972
2621
|
private cancelFlush;
|
|
1973
2622
|
private cancelPing;
|
|
1974
2623
|
private cancelRetry;
|
|
@@ -2073,6 +2722,18 @@ declare class Session {
|
|
|
2073
2722
|
private onError;
|
|
2074
2723
|
private onPong;
|
|
2075
2724
|
private onMsg;
|
|
2725
|
+
/**
|
|
2726
|
+
* D70: decode one typed payload against the current schema and hand it to that shape's
|
|
2727
|
+
* listeners.
|
|
2728
|
+
*
|
|
2729
|
+
* Everything a hostile peer can do here ends in the same place: a counter and a dropped frame.
|
|
2730
|
+
* An index past the schema's list, a truncated payload, an oversize `str`, an over-max `list`,
|
|
2731
|
+
* an unknown enum member, or a well-formed value of the wrong shape — each is caught, none
|
|
2732
|
+
* throws out of the frame loop, none closes the socket, and the next frame is delivered
|
|
2733
|
+
* normally. The `console.warn` fires once per session so a flood stays one line.
|
|
2734
|
+
*/
|
|
2735
|
+
private deliverTyped;
|
|
2736
|
+
private dropTypedMessage;
|
|
2076
2737
|
private startTimers;
|
|
2077
2738
|
/**
|
|
2078
2739
|
* The write batcher: one window per animation frame in a browser, or per `writeIntervalMs`
|
|
@@ -2094,6 +2755,13 @@ declare class Session {
|
|
|
2094
2755
|
private rejectPending;
|
|
2095
2756
|
message(target: MessageTarget, bytes: Uint8Array): void;
|
|
2096
2757
|
onMessage(cb: (from: 'server' | string, bytes: Uint8Array) => void): Unsubscribe;
|
|
2758
|
+
/**
|
|
2759
|
+
* D70: send one typed message. `payload` is already `encodeFields`'d by the caller
|
|
2760
|
+
* (`buildMessages`), which is where the shape and the throw on a bad value belong.
|
|
2761
|
+
*/
|
|
2762
|
+
typedMessage(index: number, target: MessageTarget, payload: Uint8Array): void;
|
|
2763
|
+
/** D70: subscribe to one message shape by wire index. Raw listeners never see these frames. */
|
|
2764
|
+
onTypedMessage(index: number, cb: (from: 'server' | string, value: Record<string, unknown>) => void): Unsubscribe;
|
|
2097
2765
|
/**
|
|
2098
2766
|
* D51: write one voice signaling message. Internal — reached only through `INTERNAL_SESSION`,
|
|
2099
2767
|
* so it is not on the `Room` type and a game cannot call it.
|
|
@@ -2279,6 +2947,6 @@ declare function joinRoom<S extends AnySchema, Role extends string = RoleOf<S> &
|
|
|
2279
2947
|
* channel. A separate entry rather than a schema-less `joinRoom` overload, because everything a
|
|
2280
2948
|
* `Room` promises about state would be a lie here.
|
|
2281
2949
|
*/
|
|
2282
|
-
declare function joinRelay(options?: JoinRelayOptions): Promise<RelayRoom
|
|
2950
|
+
declare function joinRelay<S extends AnySchema = never>(options?: JoinRelayOptions<S extends AnySchema ? S : AnySchema>): Promise<RelayRoom<S>>;
|
|
2283
2951
|
|
|
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 };
|
|
2952
|
+
export { ACCOUNT_STORAGE_KEY, type AttachLobbyOptions, 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 ClientRapier2dBody, type ClientRapier2dBodyFactory, type ClientRapier2dBodySpec, type ClientRapier2dIntentHook, type ClientRapier2dModule, type ClientRapier2dOptions, type ClientRapier2dWorld, type ClientRapierBody, type ClientRapierModule, type ClientRapierWorld, type ClientState, ClientStore, type ClientVector2, type ClientVector2d, 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, LOBBY_MEMBERS, LOBBY_POLL_MS, LOBBY_STATE, type LobbyPlayer, type LobbyView, 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, type PublicTicket, type QuickMatchDetail, 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, attachLobby, attachable, createParty, defaultScheduler, findMatch, findPublic, hasLobby, identityStorageKey, joinPublic, joinRelay, joinRoom, joinVoice, linkForUrl, lobbyOf, matchRoom, resolveUrl, roomIdFrom, webSocketTransport };
|