Spatial

Mounts

A rideable STATE on an entity: mount id + opaque look blob + speed multiplier (applied through Movement's speed seam via a Hosting bridge). Horses, hoverboards, sprint stances, transformation forms — same state machine, the game's render. Ownership/eligibility = IMountPolicy; broadcast on change + replayed on interest reveal; empty catalog = inert.

Server
services.AddCrossplayMounts();
Unity package
com.crossplay.mounts
Depends on
world

Providers you can swap 1

Infrastructure seams. Crossplay ships a working implementation of each — replacing one changes where data lives or how it moves, never a game rule.

IMountPolicy provider

The game's mount rule: may THIS player ride THIS mount now? Ownership, unlocks, indoor bans, combat locks — the game's vocabulary; the default allows every configured mount. Register yours BEFORE AddCrossplayMounts (a TryAdd seam).

  • bool AllowMount(ISession session, long ownerId, ushort mountId, out ushort reasonCode)

    True when riding is allowed. On refusal set reasonCode (0 falls back to MountRefused).

Services you call 1

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

IMountService

Server-side Mounts API — what the wire handler calls, and what game code calls directly (dismount on combat entry, mount from a dialog action, force-dismount in a no-ride zone).

  • bool Dismount(long entityId)

    Force-dismounts an entity server-side (no policy — the server is authoritative). True when the entity was mounted.

  • event Action<long, ushort> MountChanged

    Raised after a COMMITTED state change with (entity id, mount id — 0 = dismounted).

  • bool TryGetMount(long entityId, out ushort mountId, out MountDefinition definition)

    The entity's active mount, if it rides one (the speed bridge reads this per move).

  • bool TrySetMount(ISession session, ushort mountId, out ushort reasonCode)

    Mounts (or, with id 0, dismounts) the session's entity: catalog gate → policy → state change → broadcast. Returns false with a code on refusal.

Configuration 1

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

MountOptions

Configurable mount parameters (defaults here; never inlined in logic).

  • Dictionary<ushort, MountDefinition> Mounts { get; set; }

    The mount catalog, keyed by game-defined mount id. Empty (the default) leaves the piece inert: every request answers UnknownMount.

Wire messages 3

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

6001 MountRequest

Client → server: "I mount X" (0 = dismount). WHAT a mount is — a horse, a hoverboard, a sprint stance, a transformation — is 100% the game's; the piece knows a number and a look blob.

6002 MountResponse

Server → actor: the verdict. The state itself replicates via EntityMountChanged; the speed change rides the ordinary movement wire.

6003 EntityMountChanged

Server → observers + actor: the visible mount state. One game swaps in a horse model, another tints a sprite, another plays a sound — the piece cannot tell the difference.

Unity services you inject 1

Crossplay binds these in the client context. Inject and call them from your own MonoBehaviours and presenters.

IMountClient

The Mounts piece, client side: mount/dismount, and observe EVERY entity's rideable state (id + opaque look blob, broadcast on change and replayed on interest reveal). One game swaps in a horse model, another tints a sprite — bind EntityMountChanged to your entity views. The speed change rides the ordinary movement wire, never this piece.

  • void Mount(ushort mountId)

    Ride mount X (the game's policy validates ownership/eligibility server-side).

  • void Dismount()

    Dismount.

  • event Action<MountResponse> ResultReceived

    The server's verdict on your mount/dismount.

  • event Action<EntityMountChanged> EntityMountChanged

    An entity's mount state changed (or was revealed): render it.