vehicles

Vehicles

Server-authoritative heading+speed vehicle motion (cars/karts/boats/hover/tanks): sampled steer/drive/drift axes in through one validated path (wire AND AI), substepped shared-integrator sim over baked per-class LUT tables, swept through the world bounds every substep (no wall tunneling), full-state owner ack for client prediction. Wall + vehicle-vs-vehicle contact events (car-vs-car via the config-gated Contact bridge); per-entity multiplier SPI (nitro / upgrades). Drivable from CONFIG ALONE: AutoRegister says which entities entering which zone become vehicles of which class, and SurfaceHandling maps the world's baked per-cell surface ids to handling multipliers (surface 0 and unmapped ids stay neutral) — no server code required for either. Motion rides the Movement broadcast; requires the Movement piece composed. The client package predicts the local vehicle through the same shared integrator and reconciles on the owner ack.

Category Spatial Seams 2 Services 5 Options 1 Wire ids 2 Unity types 4
Server
services.AddCrossplayVehicles();
Unity package
com.crossplay.vehicles
Depends on
worldmovement

Seams you implement 2

Crossplay calls these; your game supplies them. Each ships an inert or permissive default, so register yours before services.AddCrossplayVehicles(); and it wins.

IVehicleDriver game SPI

Server-side DRIVER seam — the other half of SetInput, which until now had exactly one caller (the client wire handler). Each tick the service asks it for the intent axes of every registered vehicle that has no live client driving it, and integrates the answer through the identical validated path a player's packet takes. That single-path property is the point: one simulation, one anti-cheat surface, and an AI that cannot express anything a player could not. "No live client" means either of two things, which is what makes this one seam cover three separate needs: a SERVER entity (no owning session at all) — AI opponents, bots filling an underfull session; or a PLAYER entity whose client has stopped sending for InputStaleTicks — an AFK player, a dropped connection. Without a driver those vehicles simply coast to a stop, exactly as before; with one they keep being driven and the human takes back over the instant a fresh input arrives (the client's own intent always wins). Absent (no game or composition registers one), nothing changes at all: every vehicle without a live client coasts. Genre-agnostic throughout — "something server-side steers this vehicle" is a racing bot, a patrolling tank, a delivery van and a ferry on a timetable alike.

Example
sealed class CircleDriver : IVehicleDriver
{
    public bool TryGetIntent(long entityId, out VehicleInputAxes axes, out byte flags)
    {
        axes = new VehicleInputAxes { Steer = 0.4f, Drive = 1f };
        flags = 0;
        return true; // steady left-hand throttle
    }
}
services.AddSingleton<IVehicleDriver, CircleDriver>(); // before AddCrossplayVehicles
  • bool TryGetIntent(long entityId, out VehicleInputAxes axes, out byte flags)

    The intent this vehicle should be driven with right now, or false to let it coast.

IVehicleParamsProvider game SPI

Per-entity handling overlay seam — the vehicle twin of Movement's IMovementSpeedProvider: each substep the integrator asks it for THIS vehicle's multiplier set (VehicleParams: max speed, accel, grip, brake). This is where surface grip, a nitro status effect, or owned upgrade tiers plug in — without the piece knowing what a "surface", a "nitro" or an "upgrade" is. Absent (no game registers one), every vehicle runs its class table verbatim (all multipliers 1).

Example
sealed class SurfaceGripParams : IVehicleParamsProvider
{
    public bool TryGetParams(long entityId, out VehicleParams p)
    {
        p = VehicleParams.Default;
        p.GripMult = _surfaceGrip[CurrentSurfaceOf(entityId)];
        return true;
    }
}
services.AddSingleton<IVehicleParamsProvider, SurfaceGripParams>(); // before AddCrossplayVehicles
  • bool TryGetParams(long entityId, out VehicleParams params)

    The multiplier overlay for this vehicle right now, or false for the neutral all-1s overlay.

Services you call 5

Crossplay implements these. Resolve them from DI and call them from your own systems.

IVehicleDisplacement

Repositions a vehicle from OUTSIDE its own simulation — the receiving half of a swept contact rewind (GAP-19), and of anything else that legitimately says "you were never there".

  • bool ApplyExternalDisplacement(long entityId, float deltaX, float deltaZ)

    Offsets a registered vehicle by an exact world-space delta, updating its simulation state and its world entity together. Velocity, yaw and every other part of the sim state are untouched — this answers "where", not "how fast": a contact rewind pairs it with an impulse, which is what carries the change of motion.

IVehicleGroundSampler

Where the ground is under a vehicle — the seam that gives the XZ-planar sim a vertical axis, and the reason it is declared in CONTRACTS rather than server-side: both tiers must ground a car the same way or the client predicts a height the server disagrees with, and every reconciliation is a visible vertical jump. The server implements it over the world's 3D geometry (IWorldQueries3D.GroundProbe); the client implements it over its mirror of the same baked mesh, exactly as IWorldSurfaceMap / IClientWorldSurfaceMap already pair up. The failure this closes. Height used to be a value copied around a loop and never computed: the sim handed the mover the Y it already had and read the same number straight back. A car therefore kept its SPAWN height for its whole life — floating over every dip and buried under every rise. On a circuit that climbs 57 world units the error reached thirteen car lengths, and no game could fix it from outside: only one system may own an entity's transform, and touching the server's Y is authority. Absent (no sampler registered) nothing changes at all — the height passes through as before, which is exactly right for a flat arena, a top-down game, or any world with no baked geometry. Genre-agnostic: karts, tanks, boats and hovercraft all need "sit on the ground and tilt with it", and a game whose arena is a plane never notices this exists.

  • bool TrySampleGround(string zone, float x, float fromY, float z, float maxDrop, out float floorY, out float normalX, out float normalY, out float normalZ)

    The ground under (x, z) in a zone, searched from fromY downward.

IVehicleNeighbourSource

Where the predictor's neighbours come from — implemented by the GAME, because only the game knows which interpolated entities are vehicles and what bodies they carry (it is already rendering them). The framework ships no default: absent, prediction computes exactly the numbers it always did, which is the removable-piece rule applied to a seam.

  • int GetNeighbours(long selfEntityId, string zone, float nearX, float nearZ, float radius, Span<VehicleNeighbour> into)

    Writes the vehicles near (nearX, nearZ) into into, excluding selfEntityId, ordered by entity id ascending.

IVehicleObstacleSampler

What a vehicle would HIT on its way from one place to the next — the lateral sibling of IVehicleGroundSampler, and declared in CONTRACTS for exactly the same reason: both tiers must stop a car at the same rock, or the client predicts straight through something the server refused and every reconciliation is a visible shove. The failure this closes. Vehicles only ever asked geometry a DOWNWARD question. A substep probed for the floor under each wheel, adopted that height, and then moved horizontally with nothing consulted at all — so a car drove through stone, trees and fences, and the world's solid geometry existed only as something to stand on. Worse, it degraded as terrain improved: a boulder in the bake is a thing the car CLIMBS, because a downward probe on top of it reports a perfectly good floor. The game's own note that adding vegetation to its bake "would turn trees into ramps the car climbs rather than obstacles it hits" is this gap stated from the outside. Why a sweep and not a point test. A point test misses at speed for the same reason discrete contact did: at 56 units/s a 60 Hz substep travels nearly a metre, so a fence post fits entirely between two samples. Asking "what does the body touch along this segment" cannot tunnel, and it is a query the shared MeshCollision.SweepCapsule already answers on both tiers over the same baked triangles. Why a CAPSULE, and why the signature grew (GAP-30). This seam used to carry a radius and nothing else, so a 4.2 × 1.8 m car was swept as a ball on both tiers. That was never a judgement about cars: a sphere was the only swept volume the server provider and the Unity client mirror could SHARE, and matching each other mattered more than matching the silhouette. Both tiers now share MeshCollision.SweepCapsule over the same bake, so the compromise is gone and the axis travels through the seam. The parameters were ADDED to the existing method rather than hidden behind a defaulted overload deliberately: a game with its own sampler gets a compile error and one line to write, instead of a car that silently keeps being swept as a ball however its class is configured. A zero axis is exactly a sphere, so a sampler that ignores it is honest, just less faithful. Absent (no sampler registered) nothing changes at all — and so is a class that leaves ObstacleRadius at 0, which never asks. A flat arena, a top-down game, or any world whose only solid is its floor behaves exactly as it did before this existed.

  • bool TrySweepBody(string zone, float fromX, float fromY, float fromZ, float toX, float toY, float toZ, float radius, float axisHalfX, float axisHalfY, float axisHalfZ, uint mask, out float hitFraction, out float normalX, out float normalY, out float normalZ)

    Sweeps the vehicle's body volume along a segment and reports the FIRST thing it touches.

IVehicleService

Server-authoritative vehicle motion for heading+speed vehicles (cars, karts, boats, hovercraft, tanks — the piece never knows which): register a live world entity under a configured class and the per-tick substepped VehicleIntegrator drives it through the swept movement pipeline — walls gate every substep, observers get the ordinary batched EntityMove broadcast, and the owning client (for a player entity) gets a full-state VehicleState ack for prediction. Registering a PLAYER entity flips the Movement piece's motion authority (its raw MoveInput path drops; this sim is its motion); unregister — or despawn — hands it back. SetInput is THE one validated path: the wire handler and a game's AI drivers both go through the same ladder.

  • bool ApplyExternalImpulse(long entityId, float deltaVelocityX, float deltaVelocityZ, float deltaSpinRate)

    Applies an external impulse to a registered vehicle: a change in world velocity, and a spin.

  • int Count { get; }

    Number of registered vehicles (diagnostics).

  • void NotifyExternalContact(long entityIdA, long entityIdB, float magnitude)

    The vehicle-vs-vehicle contact intake: reports that two entities touched with the given magnitude. When BOTH are registered vehicles, raises VehicleContact (ids ordered lowest first); anything else — unknown ids, a non-finite or negative magnitude — is dropped harmlessly. Fed by the Hosting Contact bridge or the game's own detection.

  • bool Register(long entityId, ushort classId)

    Registers a live world entity as a vehicle of a configured class (Classes): seeds the sim from the entity's current position/yaw and, for a PLAYER entity, takes motion authority (IMotionAuthority.SetServerDriven). Registering an already-registered entity re-seeds it under the (possibly new) class. The registration dies with the entity.

  • bool SetDriver(long entityId, ISession driver)

    Seats a session in a vehicle's DRIVER seat — the session whose VehicleInput steers this vehicle, and the session its full-state ack is sent to. Pass null to empty the seat.

  • bool SetInput(long entityId, uint tick, float steer, float drive, byte flags)

    THE one validated intent path — the wire handler and a game's AI both call it. The ladder: registered id; finite axes (NaN/Inf dropped); axes clamped to [-1, 1]; monotonic tick (stale/duplicate dropped); tick within the ahead window (MaxInputAheadTicks). Accepted intent is HELD (latest wins) and integrated every substep until replaced — or until it goes stale (InputStaleTicks) and the vehicle coasts.

  • bool SetLocomotion(long entityId, byte locoState)

    Sets the locomotion byte broadcast with the vehicle's EntityMove stream — pure presentation vocabulary (the game maps sim state to e.g. idle/rolling/boosting ids and the client maps them onto its own visuals; the framework never reads it).

  • bool SetSurface(long entityId, byte surfaceId)

    Sets the opaque surface id stamped on the vehicle's sim state and echoed on the owner ack (game vocabulary — pair it with an IVehicleParamsProvider for surface-dependent handling).

  • bool TryGetClass(long entityId, out ushort classId)

    Reads the class id an entity registered under.

  • bool TryGetDrivenVehicle(ISession session, out long entityId)

    Reads the vehicle a session currently drives through a seat — NOT its own avatar, which needs no seat. The inverse of SetDriver.

  • bool TryGetState(long entityId, out VehicleSimState state)

    Reads a snapshot of a vehicle's current integrator state (position, velocity, yaw, smoothed axes, drift, surface).

  • bool Unregister(long entityId)

    Unregisters a vehicle and, for a player entity, returns motion authority (the input-driven path resumes). Automatic when the entity leaves the world.

  • event Action<long, long, float> VehicleContact

    Raised when two REGISTERED vehicles make contact: (lower entity id, higher entity id, magnitude — the overlap depth or impact measure the reporter supplied). Fed by NotifyExternalContact (the Hosting Contact bridge, or the game's own detection); the piece performs no vehicle-vs-vehicle detection itself.

  • event Action<long, ushort> VehicleRegistered

    Raised after an entity registers (or re-registers under a new class): (entityId, classId). The Hosting Contact bridge profiles the vehicle from this.

  • event Action<long> VehicleUnregistered

    Raised after an entity unregisters (explicitly, or automatically on despawn).

  • event Action<long, float> WallContact

    Raised when a vehicle's intended substep motion was blocked (fully or partly) by the world bounds: (entityId, blocked approach speed in units/s). Cooldown-gated per vehicle (WallContactCooldownSeconds) and thresholded (WallContactMinSpeed) — SFX/damage hooks, the game's meaning.

Configuration 1

Every tunable lives in an options object — there are no magic numbers to hunt for.

VehicleOptions

Vehicle simulation tuning (defaults here, never inline). Per-CLASS tuning is not options — it is the Classes table of VehicleClassSpec entries.

  • Dictionary<string, VehicleAutoRegisterRule> AutoRegister { get; set; }

    Composition-bridge table (used by the Hosting layer, never by this piece): which entities entering which zone BECOME vehicles, keyed by zone id — or by an instance PREFIX, resolved with the same rule every per-zone table uses. Register is otherwise reachable only from game C#, which makes the whole piece unreachable from a game that ships configuration rather than server code; this table is that switch, expressed as data. Empty (the default) = no bridge is composed and nothing auto-registers — a game driving registration from its own code sees byte-for-byte the previous behaviour. Genre-agnostic: "the entities in this zone drive" is a kart track, a tank battle, a boat race and a delivery sim alike; the piece still never learns which.

  • bool BridgeAgentDrivers { get; set; }

    Composition-bridge knob (used by the Hosting layer, never by this piece): when true, an entity that is BOTH a registered vehicle and a brain-driven agent is driven rather than slid — the Hosting bridge converts wherever its brain wants to go into steer/drive intent and feeds it through the IVehicleDriver seam, i.e. the same validated path a player's packet takes. Without it the two systems fight: the brain writes positions while the sim integrates permanently-zero axes underneath, and ResyncDistance makes them alternate. False (the default) = no bridge is composed and agents move exactly as they always have. This is what turns any existing config-driven brain (wander / patrol / chase / state machine) into a driver, so bots need no new steering vocabulary and no server C#.

  • bool BridgeContact { get; set; }

    Composition-bridge knob (used by the Hosting layer, never by this piece): when true, the Hosting Contact bridge profiles every registered vehicle on the Contact piece (its class's OBB half-extents, weight, layer — Hard resolution) and relays vehicle-vs-vehicle ContactBegan pairs into NotifyExternalContact. False (the default) = no bridge is composed and vehicles do not collide with each other.

  • Dictionary<ushort, VehicleClassSpec> Classes { get; set; }

    The vehicle class table, keyed by the class id passed to Register — config/content-bindable (Crossplay:Vehicles:Classes:{id}). Empty (the default) = nothing can register and the piece is inert.

  • VehicleDriverTuning DriverTuning { get; set; }

    Composition-bridge tuning (used by the Hosting layer, never by this piece): how a "go to this point" intent becomes steer/drive axes. Ignored unless BridgeAgentDrivers is on.

  • int FrameBufferInitialBytes { get; set; }

    Initial capacity (bytes) of the reused outbound frame buffer (the owner VehicleState ack). Grows to the high-water mark and never shrinks. Default 128.

  • bool GroundVehicles { get; set; }

    Whether vehicles are GROUNDED on the world's 3D geometry each substep — height and slope taken from IWorldQueries3D.GroundProbe through the shared IVehicleGroundSampler seam, rather than a car keeping its spawn height for its whole life. Default true, and inert without geometry: a zone with nothing baked answers "no ground", the probe returns false and the height passes through exactly as it always did, so a flat arena or a top-down game never notices. Set false for vehicles that are deliberately not on the ground — a flying racer, a hover craft holding a fixed altitude, a game that drives Y from its own system. Per-class probe distances and ride height live in the class table (both tiers must agree on them), not here.

  • int InputBurst { get; set; }

    Burst allowance (token-bucket capacity) for the VehicleInput rate seed — absorbs a client's post-hiccup catch-up burst. Default 90.

  • int InputStaleTicks { get; set; }

    Server ticks without a NEWLY accepted input after which the held axes are treated as zero — the vehicle coasts (drag slows it) instead of driving forever on the last packet of a disconnected or stalled client. Default 10 (0.5 s at a 20 Hz tick).

  • double InputsPerSecond { get; set; }

    Sustained per-session rate cap (messages/second) seeded into Core's per-message rate limits for VehicleInput — comfortably above the ~20-30 Hz send cadence, far below a flood. A composer's configureRateLimit entry or Crossplay:RateLimit:PerMessage config wins over the seed. Default 60.

  • uint MaxInputAheadTicks { get; set; }

    How far past the newest accepted input tick a new input's tick may jump (the ahead window). Rejects absurd tick leaps (a hostile uint.MaxValue would otherwise lock every later legitimate input out as "stale"). Default 300 (~15 s of 20 Hz input ticks).

  • int MaxSubstepsPerTick { get; set; }

    Most integrator substeps run in one tick (the spiral-of-death guard): a long stall's accumulated time past this is dropped rather than simulated in a burst. Default 8.

  • int MaxVehicles { get; set; }

    Most entities registered as vehicles at once; Register refuses beyond it. Default 1024.

  • float ResyncDistance { get; set; }

    If the entity's world position drifts farther than this (world units) from the sim's position — a Teleport, a zone move, some other system repositioning it — the sim re-seeds from the entity instead of fighting it. Default 2.

  • float SpeedEnvelopeSlack { get; set; }

    The absolute speed-envelope slack: each substep the vehicle's horizontal speed is hard- clamped to MaxSpeed × MaxSpeedMult × this whatever the integrator (or a buggy params provider) produced, and the excess increments the EnvelopeClamps metric. Above 1 so legitimate drift (forward cap + lateral carry) never trips it. Default 1.25.

  • float StateSendRateHz { get; set; }

    Owner VehicleState ack rate, Hz, while inputs flow (0 or less = every tick). The ack is one small unreliable frame to ONE recipient; 20 matches the default input cadence so the predictor reconciles once per sent input. Default 20.

  • float SubstepHz { get; set; }

    Integrator substep rate, Hz. Each server tick runs SubstepHz / TickHz substeps and every substep routes through the swept movement pipeline, so the per-substep travel — not the per-tick travel — is what the wall gate sees: 60 Hz keeps a top-speed vehicle from tunneling a thin wall that a whole 20 Hz step would bridge. Default 60.

  • Dictionary<byte, SurfaceHandlingSpec> SurfaceHandling { get; set; }

    Composition-bridge table (used by the Hosting layer, never by this piece): the handling overlay each opaque SURFACE id imposes — the config-only implementation of IVehicleParamsProvider over the world's baked surface plane (IWorldSurfaceMap). Keys are the ids stamped into the bake; what a value MEANS (dirt, sand, ice, water) stays entirely the game's vocabulary. Surface 0 — the conventional default/unknown id, and the answer for every zone without a plane — and any id with no row here run NEUTRAL (every multiplier 1), so an unbaked track drives like plain road rather than like whatever row happened to sort first. Empty (the default) = no bridge is composed at all.

  • float WallContactCooldownSeconds { get; set; }

    Minimum seconds between WallContact events for one vehicle, so grinding along a wall is one event, not one per substep. Default 0.25.

  • float WallContactMinSpeed { get; set; }

    Blocked-approach speed (units/s) at or above which a wall hit raises WallContact — grazes below it just slide. Default 1.

  • float WallContactSpeedLoss { get; set; }

    Fraction of the vehicle's speed lost when a wall-contact event fires (the crash cost; the blocked velocity component is always removed regardless). Default 0.25.

Wire messages 2

The protocol this piece speaks. Ids are allocated per piece so they can never collide.

6301 VehicleInput

Client -> server vehicle intent (unreliable, sent at a fixed cadence, ~20-30 Hz). Sampled intent, never a command: the newest accepted packet overwrites the held axes and the server integrates from them every substep, so packet count or timing can never change simulation outcomes — a lost packet just means the previous intent is held a beat longer. The server is authoritative; this only expresses desired axes, never a position.

6302 VehicleState

Server -> the OWNING client only (unreliable, sent while inputs flow): the FULL integrator state acknowledging an input tick — everything a client predictor needs to rewind to AckTick, adopt this state, and replay its unacked inputs through the shared VehicleIntegrator so prediction matches the server exactly. Observers never receive this; remote vehicles render from the Movement piece's ordinary EntityMove interpolation stream.

Unity seams you implement 4

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.

IVehicleClassTableProvider unity SPI

SPI the game implements to give the CLIENT the same per-class integrator tuning the server's Crossplay:Vehicles:Classes:{id} config carries — the vehicles twin of IClientWorldBounds: identical tables on both tiers are what make a predicted step and its authoritative echo compute the same numbers. Optional: without a binding, tables come from the VehicleClientOptions asset's serialized class list instead. Bind this when the tables are data-driven (remote config / content) rather than authored in the options asset.

  • bool TryGetTable(ushort classId, out VehicleClassTable table)

    The baked tuning of classId, or false when the class is unknown (the client then falls back to the options asset, and failing that refuses to drive).

IVehicleClientParamsProvider unity SPI

SPI the game implements to mirror the server's per-entity IVehicleParamsProvider overlay (surface grip, nitro, upgrade tiers) for the LOCAL vehicle, so prediction applies the same multipliers the authority applies. Optional: without a binding the predictor runs the class table verbatim (VehicleParams.Default) — correct, just briefly divergent while a server overlay is active, which the state ack reconciles away.

  • bool TryGetParams(ushort classId, out VehicleParams overlay)

    The current multiplier overlay for the local vehicle of classId; false to run the class table verbatim this frame.

  • bool TryGetParams(ushort classId, long entityId, out VehicleParams overlay)

    The overlay for the vehicle being DRIVEN, which is the local avatar only until a driver seat (BeginDriving(classId, entityId)) names another entity — a server-spawned ambulance stands on its own surface, not on the driver's. Passed by the caller because a provider must not depend on the vehicles client that consumes it (a construction cycle). The default forwards to the one-argument overload, so an existing provider keeps compiling and keeps sampling the avatar.

IVehicleGameplayInput unity SPI

SPI the game implements to feed vehicle intent from any input source (new Input System, AI, replay, a rhythm chart, …) — the vehicle twin of the movement piece's IGameplayInput. Crossplay samples it once per frame and forwards the resolved axes as VehicleInput at the configured send rate. Optional: without a binding the vehicles client never sends or predicts (the local vehicle still renders from the server's ordinary movement broadcast).

  • VehicleIntent Sample()

    Returns the current desired vehicle intent for this frame.

IVehiclesClient unity SPI

The vehicles piece's client surface: the game tells it when the local player starts/stops driving (the server registered its entity as a vehicle through the game's own flow — an Interactions verb, a match start, a spawn rule), and reads back the predicted pose for its own view binding. Remote vehicles need nothing here — they render from the Movement piece's ordinary EntityMove interpolation, exactly like any other entity.

  • bool IsDriving

    True while the local player is driving (between BeginDriving and StopDriving).

  • ushort ClassId

    The class id being driven (0 when not driving — but 0 is also a legal class id; gate on IsDriving, never on this value).

  • VehicleSimState PredictedState

    The full predicted integrator state "now" — including the slewed axes, the drift bit and the surface id the server last stamped (opaque game vocabulary).

  • Vector3 PredictedPosition

    The predicted position "now" (server truth + replayed unacked inputs).

  • float PredictedYawRadians

    The predicted heading, radians (0 faces +Z).

  • float LastCorrectionMeters

    Distance (metres) the last server reconciliation corrected the predicted vehicle by — ~0 when prediction agrees with authority; spikes reveal desync/latency events. Never judge this number without the speed beside it (GAP-37a): a healthy one-interval reconciliation is 0.4 m at 8 u/s and 2.8 m at 56 u/s — same health, 7× the metres. Statistics over raw metres are partly statistics about how fast the car was going; use LastCorrectionSeconds for the speed-invariant form.

  • float LastCorrectionSeconds

    The last correction expressed as TIME: LastCorrectionMeters divided by the acked (authoritative) speed at that reconciliation — i.e. how much travel time the server rewound, which is the number that means the same thing at every speed (GAP-37a). 0 when the acked state was stationary: a nonzero correction at zero speed cannot be latency and is its own, different signal. How to read it: divide by StateIntervalSeconds. ≈1 interval is the irreducible floor of prediction (authority simply cannot speak more often); ≥3 intervals is a genuine rewind worth investigating. A consuming game measured its collision path from 5.6 intervals down to 1.0 with exactly this arithmetic — after two investigations were misled by the metres form.

  • float StateIntervalSeconds

    One owner-ack interval, seconds — 1 / VehicleClientOptions.StateSendRateHz, the floor LastCorrectionSeconds is compared against. Exposed here because the piece holds the rate and every consumer is badly placed to re-derive it.

  • int PendingInputCount

    Inputs sent but not yet acknowledged by the server (a diagnostics signal).

  • event Action<VehicleState> StateAcked

    Raised for every owner VehicleState ack applied, AFTER reconciliation — the game's hook for the echoed game-reserved flag bits (4-7) and the server-stamped surface id. The instance is not pooled; reading it after the call is safe.

  • bool BeginDriving(ushort classId)

    Starts driving: resolves the class table (the game's IVehicleClassTableProvider first, then the VehicleClientOptions asset), anchors prediction at the current self pose, and begins sampling/sending/predicting on the next frame. Call it when the server registered the local entity as a vehicle of this class (the game knows through its own flow).

  • bool BeginDriving(ushort classId, long entityId)

    Starts driving a vehicle that is NOT the local avatar — the entity the server seated this session in (IVehicleService.SetDriver). Everything else is identical to BeginDriving: same table resolution, same predictor, same reconciliation against the owner ack; only the entity whose view is predicted and written changes.

  • long DrivenEntityId

    The entity currently being predicted and driven — the local avatar for the ordinary case, or the seated vehicle. Meaningless unless IsDriving.

  • void StopDriving()

    Stops driving: no more sends or prediction; the local entity's transform is the game's (or the movement piece's) again. Call it when the server unregistered the vehicle.