Avatar Motor
Generic client-authoritative avatar relay (Route B): the owning client simulates its avatar with its OWN controller and reports a full-3D pose (Vec3 + smallest-three quaternion + quantized velocity) plus an opaque game-state blob; the server validates (rate + gross-delta + optional IAvatarMoveValidator) and relays to observers with interpolation. Any bespoke-controller game (platformer/racer/ragdoll) gets multiplayer with near-zero netcode — it implements only IAvatarStateSource + IAvatarStateApplier and never defines a message or an id.
Seams you implement 3
Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayMotor(); and it wins.
IAvatarMoveValidator game SPI
Optional game seam for DEEP validation of a client-authoritative avatar report. The Motor already applies a generic envelope (per-message rate limiting via Core's RateLimitOptions, a gross-delta / max-speed teleport reject, and finite-value checks); register an IAvatarMoveValidator to add game-specific rules on top — a per-state max step, a kill-plane floor, a jump/dash budget, a "you cannot be here in this game-state" test. It is consulted AFTER the built-in checks pass, and its veto drops the report. Optional ([InjectOptional]): most co-op games need nothing beyond the built-in envelope and register no validator. Its absence is the default — the piece stays fully functional and removable without it.
-
bool Accept(long entityId, in Vec3 position, in Vec3 prevPosition, float dtMs)Returns true to accept the reported position for entityId, false to reject the whole report. prevPosition is the entity's previous authoritative position and dtMs the elapsed server milliseconds since the last accepted report (0 for the first report after spawn/teleport) — enough to bound a per-state step without the game tracking its own timing.
IMotorRelayAuthority game SPI
Optional game seam that decides which client may drive which server-spawned entity. Route B exists because the game has a bespoke controller the server cannot reproduce — which is equally true of its enemies, NPCs and host-driven props. Those are spawned server-side and have no owning session, so the game elects one client as the simulation host and that client relays their poses through the same MotorState report it already sends for its own avatar (MotorState.EntityId). The framework has no notion of a host — election is a game decision (lowest player entity id, the party leader, the lowest-latency peer, a lobby setting), and it changes at runtime when that client leaves. So the Motor asks: may THIS session move THAT entity, right now? Optional ([InjectOptional]) and closed by default: with no authority registered the server rejects every non-zero MotorState.EntityId, so a game that has not opted in behaves exactly as before. Registering one opens exactly the entities you approve — the Motor additionally refuses any target that is not server-spawned, so a relay can never touch another player's avatar even if this method wrongly returns true.
// The host (lowest player entity id) drives every server entity; nobody else drives any.
public sealed class HostRelayAuthority : IMotorRelayAuthority
{
private readonly IHostElection _election; // the game's own, already needed for its AI
public bool MayRelay(ISession session, long entityId) => _election.IsHost(session.ConnectionId);
}
// services.AddSingleton<IMotorRelayAuthority, HostRelayAuthority>(); -
bool MayRelay(ISession session, long entityId)Returns true to let session report the authoritative pose of entityId — a server-spawned entity it does not own. Called on every relayed report (a hot path: keep it a lookup, not a scan), AFTER the Motor has confirmed the entity exists and is server-spawned, and BEFORE the built-in rate / gross-delta / IAvatarMoveValidator checks, which then apply to the relayed entity exactly as they do to an avatar. Returning false drops the report silently — the entity simply keeps its last authoritative pose, which is also what happens while a host election is in flight.
IMotorSimulation game SPI
The ENTIRE per-game surface for server-authoritative motion: a pure, deterministic step the framework runs on BOTH tiers (server = authority, client = prediction). This IS the game's controller — gravity, jump, air-control, ground-snap, speed — expressed as portable math (no Physics.*, no Time.*, no engine calls), so the server can run it identically and prediction agrees. Contract:Step MUST be a pure function of its arguments — same state + input + dt + geometry ⇒ same result on both tiers. Any hidden state or non-determinism shows up as reconciliation correction. Gravity/jump constants live HERE, in the game — never in the framework (the Prime Directive). Portable only: a Unity-PhysX controller can't be re-run on the server; keep those on client-authoritative Motor. This serves a controller you can express as deterministic math.
-
MotorSimState Step(in MotorSimState state, in MotorInput input, float dt, IMotorGeometry geometry)Advances state by input over dt seconds, resolving against geometry (ground + walls), and returns the new state.
Services you call 4
Crossplay implements these. Resolve them from DI and call them from your own systems.
IMotorAuthority
Puts a Motor-driven avatar somewhere — PHYS-10, and the piece's whole server->avatar authority surface. The gap this closes.IMotorInputService was one method and IMotorService was one method, and both took input FROM the client. There was no teleport, no place, no launch. In ServerAuth the authoritative pose lives in a per-connection sim state seeded ONCE from the entity's spawn pose and never re-seeded, so writing the world entity — which is exactly what the default ITravelMover does — moved an avatar for precisely as long as it took the next input to arrive; the write was not refused, it was accepted and then undone. In ClientAuth the client owns the pose and nothing could tell it otherwise. What that blocked was already built.PhysicsEntityBodies hands a struck entity to the solver and takes the pose back when it settles, through IEntityPoseAuthority — the one implementation of which takes IMovementService. So a falling beam landed on a first-person player and nothing happened to their body: not a smaller effect than intended, none, because a null pose authority leaves an entity permanently kinematic and a kinematic has infinite mass. Respawn, zone travel and knockback were blocked through the same missing door. Answering false is how a composition decides. An id this piece is not driving is refused, so a Hosting bridge can ask Motor first and fall back to Movement without any configuration saying which one a game uses — a game that has always run Movement gets byte-identical behaviour, because Motor refuses every id it has never seen (rule 21).
-
long Placements { get; }Placements delivered since start — the diagnostic that separates "the number is wrong" from "nothing was ever a Motor avatar".
-
bool TryLaunch(long entityId, in Vec3 position, in Vec3 velocity, uint rotationQ = 0, byte reason = 0)Places an avatar IN MOTION — the physics handback: a player the solver has been throwing, given back mid-flight so they keep travelling instead of stopping dead in the air.
-
bool TryPlace(long entityId, in Vec3 position, uint rotationQ = 0, byte reason = 0)Places an avatar at rest: a respawn, a zone entry, a teleport into an instance.
IMotorGeometry
The tiny collision facade the game's IMotorSimulation step queries — the two primitives a character controller needs: "where is the ground" and "does my capsule hit a wall". Backed by the 3D query layer on BOTH tiers (World's IWorldQueries3D server-side, the client's IClientWorldQueries3D mirror client-side), so the SAME step, querying the SAME collision data, produces the SAME motion — the prediction-parity guarantee. Engine-free (only Vec3); the zone is already bound by the adapter the framework passes in.
-
bool CapsuleCast(in Vec3 from, in Vec3 to, float radius, float height, out float t, out Vec3 hitPoint, out Vec3 hitNormal)Sweeps the avatar's capsule (radius/height) from from to to and returns the first blocking contact (the wall test) — t is the fraction of the move completed before the hit.
-
bool GroundProbe(in Vec3 point, float maxDrop, out float floorY, out Vec3 normal)Highest solid surface at or below point within maxDrop (the landing test).
IMotorInputService
The ServerAuth Motor relay: accepts an input tick from the owning client, runs the game's deterministic IMotorSimulation step against the 3D collision layer, writes the authoritative pose, reconciles the owner, and broadcasts to observers. The server-authoritative sibling of IMotorService (which relays a client-reported pose). Routed the MotorInput message; exposed for tests and server tooling.
-
void ProcessInput(ISession session, MotorInput input)Integrates one input tick for the owning session's avatar (validates, steps, reconciles, broadcasts).
IMotorService
The Motor relay: accepts a client-authoritative avatar pose + opaque state blob from the owning session, validates it (rate + gross-delta + optional game validator), writes the authoritative 3D pose to the world, and batches an EntityMotorState fan-out to the entity's observers. A game rarely calls this directly — the piece routes MotorState here through its handler — but it is exposed so tests and server-side tooling can drive the relay.
-
void ProcessState(ISession session, MotorState state)Processes one owner report: validates it and, if accepted, updates the authoritative pose and records the entity for the next batched observer broadcast. Rejected reports (stale tick, non-finite fields, oversize blob, gross-delta, a validator veto) are dropped silently.
Configuration 1
Every tunable lives in an options object — there are no magic numbers to hunt for.
MotorOptions
Configurable Motor (server) parameters — defaults here, never inlined in logic (rule 4). The Motor relays a client-authoritative 3D avatar pose + an opaque game-state blob; these knobs bound the blob size, the built-in anti-cheat envelope, and the batched observer broadcast.
-
float EffectiveMarginRefillPerSecond { get; }The resolved refill rate: GrossDeltaMarginRefillPerSecond, else the whole margin per second.
-
int FrameBufferInitialBytes { get; set; }Initial capacity (bytes) of the reused outbound frame buffer. Grows to the high-water mark and never shrinks; sizing it past the largest frame keeps steady state allocation-free.
-
float GrossDeltaMarginRefillPerSecond { get; set; }How fast the gross-delta margin bucket refills, in world units per second. Default: the whole of GrossDeltaMarginUnits once per second (0 selects that default).
-
float GrossDeltaMarginUnits { get; set; }A distance (world units) allowed between two reports over and above MaxSpeed × elapsed time — it absorbs the first report after a spawn/teleport and clock jitter, so a legitimate warp or a slow first packet is never falsely rejected. The gross-delta test allows MaxSpeed × dt + the margin still in hand.
-
MotorHostElection HostElection { get; set; }Which built-in simulation-host election authorises relayed reports for server-spawned entities (enemies, NPCs, host-driven props). None — the default — registers no authority at all, so every relay is rejected and a game that has not opted in is unaffected. LowestPlayerId registers LowestPlayerIdRelayAuthority, whose matching client-side rule ships in com.crossplay.motor so both tiers reach the same answer with nothing on the wire.
-
float InputStepSeconds { get; set; }Seconds of motion one input represents in ServerAuth mode — the fixed step the server integrates and the client predicts (they MUST match, and normally equals 1 / client SendRateHz, so both tiers step identically). Ignored in ClientAuth mode.
-
int MaxBroadcastsPerPump { get; set; }Per-poll slice of the tick's broadcast batch, so one tick's burst never monopolizes a transport flush (a ping never waits behind a whole batch). 0 = emit the whole batch at tick time.
-
int MaxBroadcastsPerTick { get; set; }Cap on the number of moved entities whose EntityMotorState is broadcast per tick. Excess entities defer to later ticks (oldest first, freshest state) — the same graceful degradation the movement broadcast makes under a mass pile-up. 0 = unbounded.
-
int MaxReportIntervalMs { get; set; } -
float MaxSpeed { get; set; }Gross-delta anti-cheat ceiling: the maximum speed (world units/second) an avatar may travel between two reports before the report is rejected as a teleport. Sized generously above any real controller's top speed (dashes, launches, gravity falls) so it only catches blatant position jumps — deep, game-specific validation belongs in an IAvatarMoveValidator. 0 disables the built-in gross-delta check (rely on the validator / rate limit instead).
-
int MaxStateBytes { get; set; }Maximum bytes of the opaque game-state blob accepted on a MotorState. A larger blob is REJECTED (the whole update is dropped) so a hostile client cannot inflate the unreliable frame past the MTU. Keep it small — the blob is meant for an animation-state index + a few flags/counters.
-
int MinReportIntervalMs { get; set; }Accepted range (milliseconds) for the owner-reported MotorState.SendIntervalMs. A value outside it is ignored (that report is stamped with its arrival time), so a broken or hostile cadence claim cannot stretch or compress an entity's interpolation timeline. The defaults span roughly 5 Hz to 200 Hz of reporting.
-
MotorMode Mode { get; set; }Which authority model to run (see MotorMode). ClientAuth (default): the client reports the pose, the server relays. ServerAuth: the client sends input, the server integrates it via the game's IMotorSimulation — which MUST then be registered, or composition fails fast.
-
int SnapshotRebaselineTickGap { get; set; }Re-anchor an entity's timeline when its send tick advances by more than this in one step — a respawn, a reconnect or a long silence, where projecting from the old anchor would place the snapshot absurdly far in the future. 0 disables the check (not recommended).
-
int SnapshotTimingWindow { get; set; }How many recent (arrival − projection) observations back the per-entity offset estimate. The estimate is their MINIMUM (queueing delay is one-sided, so the minimum is the low-latency truth), and this window is also how the estimate recovers when a route genuinely gets slower: an old, better sample ages out. Bigger = steadier but slower to admit a worse path; ~2-4 seconds of reports is a sensible range. Ignored when TickDerivedSnapshotTimes is off.
-
int StatePoolMaxRetained { get; set; }Max retained pooled EntityMotorState instances (per-entity dirty-state pool).
-
bool TickDerivedSnapshotTimes { get; set; }Stamp each broadcast snapshot with the instant the owner SAMPLED the pose — reconstructed from its send tick (see SnapshotTimeline) — instead of the moment the packet was processed. On (the default) remote avatars stop jittering in proportion to their speed, because arrival-time stamping labels uniformly-sampled positions with jittered instants and the client then renders a correct position at the wrong time (error = speed × timing error). Turn it off only to reproduce the old behaviour: it needs no per-game tuning, and a client that does not report its cadence (MotorState.SendIntervalMs = 0, i.e. an older build) transparently falls back to arrival time.
Wire messages 5
The protocol this piece speaks. Ids are allocated per piece so they can never collide.
MotorState Client -> server (unreliable, sent at the motor's send rate): an authoritative pose plus an opaque, game-defined state Blob. This is the Route-B report — the client simulates with its bespoke controller and tells the server where it ended up; the server validates (rate + gross-delta + optional game validator) and relays. The framework never reads Blob — all game meaning (animation-state index, flags, jump counters, gear/rpm…) lives inside it, owned by the game's IAvatarStateSource. The report is normally about the sender's OWN avatar (EntityId = 0). A client elected by the game as the simulation host may also report for a SERVER-SPAWNED entity it does not own (an enemy, an NPC, a host-driven prop) by naming it in EntityId — see that field.
EntityMotorState Server -> observers (unreliable): a relayed avatar pose + opaque game-state blob for one entity, stamped for interpolation. The server writes the authoritative pose with IWorldService.UpdateEntity(..., swept:false) (full 3D) and fans this out to the entity's interest set. Clients buffer these on the ServerTimeMs timeline and interpolate, then hand the pose + blob to the game's IAvatarStateApplier. The blob passes through untouched.
MotorInput Client -> server (ServerAuth mode, unreliable): ONE input tick — desired move, buttons, and aim. The server integrates it by running the game's IMotorSimulation step (it owns the position, unlike client-auth mode where the client sends the finished pose). The client predicts with the same input + step, then reconciles against the server's MotorReconcile.
MotorReconcile Server -> self (ServerAuth mode, unreliable): the authoritative post-step simulation state at the acknowledged input. The owning client snaps its prediction to this state and REPLAYS every input still in flight through the same IMotorSimulation step — exact reconciliation with no round-trip wait, so the local avatar never lags and never rubber-bands (the correction decays out smoothly). Position/velocity ride full-precision; orientation packs smallest-three.
MotorPlace Server -> self (both modes, RELIABLE): you are now here — PHYS-10. A placement is not a correction, and the difference is the whole message. A MotorReconcile says "you were slightly wrong": the client snaps to it and REPLAYS the inputs still in flight, because those inputs really did happen. A placement says "you are somewhere else now" — respawned, teleported into an instance, or thrown across the room by a falling wall — and replaying a run that happened somewhere else is the classic bug where a respawned player slides back across the map. So the client discards its pending tail instead of replaying it. Reliable, unlike every other message in this piece. A lost reconcile is superseded a tick later and a lost pose is superseded by the next one. A lost placement is superseded by nothing: the player is simply left standing where they died. Why the gap needed a message at all. The piece had no server->owner authority path. In ServerAuth the authoritative pose lives in a per-connection sim state seeded once and never re-seeded, so writing the world entity moved an avatar only until its next input overwrote it; in ClientAuth the client owns the pose outright and nothing could tell it otherwise. Movement could be repositioned and cannot look up; Motor can look up and could not be repositioned.
Unity seams you implement 7
The client half's sockets. Bind your implementation in the client context and Crossplay's client calls it — this is where your models, animators, UI and controls plug in.
IAvatarStateApplier unity SPI
OBSERVER-side game seam — the entire per-game surface for rendering a remote avatar. Each frame the Motor samples every tracked remote entity's interpolated pose off the server timeline and calls Apply with that pose and the latest opaque blob. The game resolves the entity id to its own avatar (e.g. via IWorldEntities/ICharacterFactory), writes the transform, and reads the blob to drive its animator/VFX/state — all presentation, 100% the game's. The framework never interprets the blob; it only moved the bytes. Optional ([InjectOptional]): a headless / diagnostics-only client binds no applier and the Motor still receives and buffers the streams, rendering nothing. Bind one on any client that shows remote avatars.
-
void Apply(long entityId, in MotorPose pose, ReadOnlySpan<byte> blob, uint serverTimeMs)Renders entityId at an authoritative interpolated pose for this frame.
IAvatarStateSource unity SPI
OWNER-side game seam — the entire per-game surface for reporting the local avatar. The Motor calls Sample once per send tick; the game returns its avatar's current world-space pose and packs whatever game state observers need (animation-state index, flags, jump/dash counters, a car's gear/rpm…) into the supplied blob span. The framework never reads those bytes — it relays them verbatim. Because the game supplies the pose, its OWN controller stays the single source of truth for local motion (Route B / 1:1 feel); the Motor never simulates or overrides it. Optional ([InjectOptional]): a spectator or observer-only client binds no source and simply never sends. Bind one on any client that owns a controllable avatar.
-
void Sample(out MotorPose pose, Span<byte> blob, out int blobLength)Produces the current owner pose and writes the opaque game-state into blob.
IMotorClient unity SPI
Public facade / diagnostics seam for the Motor client loop. A game rarely needs this — the whole contract is the two SPIs (IAvatarStateSource / IAvatarStateApplier), plus the optional IRelayedAvatarStateSource on a simulation host — but it exposes lightweight counters an overlay or test can read.
-
int RemoteEntityCountNumber of remote entities currently being interpolated.
-
bool IsReportingOwnerTrue once the owner has sent at least one report this session (a local avatar is live).
-
int RelayedEntityCountNumber of server-spawned entities this client relayed on the last send tick (0 unless the game bound an IRelayedAvatarStateSource and this client is the elected simulation host) — the cheapest way for an overlay or a test to see whether relaying is actually live.
IMotorDiagnostics unity SPI
What the Motor client can tell you about remote playback quality, so "is the jitter fixed?" is a number rather than an impression. This exists because the absence of it cost real time: a game whose numeric harness had been lost saw three unrelated defects — proxy collision, snapshot arrival-time stamping, and an undersized jitter buffer — all present identically as "the jitter is back", which made genuine progress look circular. The framework owns the measurement now, so every game gets the separation for free. Every aggregate below describes the entities that actually STREAM — the ones that have received at least one Motor packet. The Motor opens a buffer for every non-self entity in the world, which in a game using authored level content is mostly props that will never send anything; averaging those in reports a delay no packet produced and a remote count two orders of magnitude off the number of players. See TrackedEntityCount for the other figure. Read-only and optional: implemented by the Motor client, consumed by the diagnostics overlay through [InjectOptional], so nothing depends on it being present.
-
int RemoteEntityCountRemote entities that are actually streaming — buffered AND having received at least one snapshot. This is the number to compare against "how many other players are here".
-
int TrackedEntityCountRemote entities the Motor holds a buffer for, streaming or not. Always at least RemoteEntityCount; the difference is the world's non-Motor entities (authored props, scenery), and a large one is normal, not a leak.
-
MotorInterpolationMode InterpolationModeWhether the delay is measured per source or authored (see MotorInterpolationMode).
-
float WorstDelaySecondsThe largest interpolation delay any remote is currently rendered at, in seconds — the worst link in the session, which is the one a player complains about.
-
float MeanDelaySecondsMean interpolation delay across remotes, in seconds.
-
float WorstJitterMsLargest smoothed arrival jitter across remotes, in milliseconds — WHY the worst delay is what it is.
-
float WorstGapMsLargest recent arrival gap across remotes, in milliseconds. A gap far above one send interval is consecutive packet loss, which is invisible in the jitter figure alone.
-
float WorstTransitMsLargest measured path-transit baseline across remotes, in milliseconds — how LATE snapshots arrive after their owner sampled them (the whole owner→server→here path), as opposed to how UNEVENLY (WorstJitterMs). This is the term that sets the SCALE of an adaptive delay; without it on display, a small WorstDelaySeconds is ambiguous between "correctly sized" and "ignoring latency" — precisely the confusion GAP-15 documents. Zero until the server timeline is in use (lateness needs a shared clock to measure).
-
bool TryGetRemote(long entityId, out float delaySeconds, out float jitterMs, out float worstGapMs, out int bufferedSnapshots)Per-entity readout, or false when that entity is not a buffered remote (it may be ours, host-driven by us, or gone). Unlike the aggregates this answers for a TRACKED entity, streaming or not — asked about a specific id the honest answer is its state, and a never-streamed one reports zero buffered snapshots, which is the tell.
-
bool TryGetRemoteTransit(long entityId, out float transitMs)Per-entity path-transit baseline (see WorstTransitMs), or false when that entity is not a tracked remote. Additive rather than a fifth out-param on TryGetRemote, so no existing game readout has to change to keep compiling.
IMotorInputSource unity SPI
OWNER-side game seam for ServerAuth mode — the game's INPUT (not a pose). The framework calls this each send tick to build a MotorInput; the server integrates it (authority) and the client predicts with the same input. The counterpart of client-auth mode's IAvatarStateSource (which reports a finished pose). Bind on any client that owns a server-auth avatar.
-
void Sample(out Vec3 move, out uint buttons, out Quat aim)Produces this tick's desired input: move direction, buttons (jump/dash/…), and aim.
IMotorPlacementHandler unity SPI
The game's hook for "you are now here" — PHYS-10's client half. What a placement IS. Not a correction. A reconcile says "you were slightly wrong": the predictor snaps and REPLAYS the inputs still in flight, because those inputs really happened. A placement says "you are somewhere else now" — respawned, teleported into an instance, or thrown across the room by a falling wall — and replaying a run that happened somewhere else is the classic bug where a respawned player slides back across the map. The pending tail is discarded, not replayed. In ServerAuth this is a NOTIFICATION. The predictor has already been reset before this is called, so the avatar is where the server says. Implement it to cut the camera, flash the screen, stop a footstep loop, or nothing at all. In ClientAuth this is an INSTRUCTION, and the only one there is. The client owns the pose in that mode, so nothing else will move the avatar: a game that does not implement this cannot be respawned or thrown. That is the honest division — a bespoke controller is the game's, and the framework will not reach into it. Optional: resolved through [InjectOptional], and absent nothing is called.
-
void OnPlaced(Vector3 position, Quaternion rotation, Vector3 velocity, byte reason)The server has placed this client's avatar.
IRelayedAvatarStateSource unity SPI
HOST-side game seam — report poses for entities this client does not own. Route B exists because the game has a bespoke controller the server cannot reproduce, and that applies just as much to its enemies, NPCs and host-driven props: they are spawned server-side, have no owning session, and nobody but a client can say where they are. So the game elects ONE client as the simulation host, runs their controllers there, and the Motor relays their poses on exactly the path it already uses for the local avatar — no game message, no game-allocated wire id, no per-entity interpolation bookkeeping. This is a second, optional source alongside IAvatarStateSource (which stays unchanged and keeps reporting the local avatar): bind both on the host, only IAvatarStateSource on everyone else, or neither on a spectator. Sampling happens on the same send tick and under the same MotorOptions.SendRateHz and MaxStateBytes budgets — relaying introduces no new rate of its own. The server must agree. A relayed report is accepted only if the game also registered an IMotorRelayAuthority server-side that approves this session for that entity, and the entity is genuinely server-spawned. Report an id you are not authorised for and the report is simply dropped — which is also the harmless steady state while a host election is changing hands. Host echo is suppressed. The host is itself an observer of what it relays, so the server fans the resulting EntityMotorState back to it. The Motor DROPS those echoes for every id this source is currently relaying, so the host renders its own live simulation while every other client renders the interpolated relay. That is the same asymmetry a game already has between its local player (real controller) and remote proxies (interpolated), so no new one is introduced — and the moment an id leaves RelayedEntityIds (host migration, the entity stops being host-driven) the echoes are accepted again and it becomes an ordinary interpolated remote.
-
IReadOnlyList<long> RelayedEntityIdsThe entities this client is currently driving, or an empty list / null when it is driving none (the normal state on every non-host). Read once per send tick and indexed, never retained, so returning a reused list is both allowed and preferred. Ids of 0 and the local avatar's own id are ignored — the local avatar belongs to IAvatarStateSource.
-
bool TrySample(long entityId, out MotorPose pose, Span<byte> blob, out int blobLength)Produces the current pose + opaque game state of entityId, exactly as Sample does for the local avatar. Return false to skip this entity for this tick — the honest answer when it just died, despawned, or has not been created locally yet — and nothing is sent for it.