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.
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> MountChangedRaised 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.
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.
MountResponse Server → actor: the verdict. The state itself replicates via EntityMountChanged; the speed change rides the ordinary movement wire.
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> ResultReceivedThe server's verdict on your mount/dismount.
-
event Action<EntityMountChanged> EntityMountChangedAn entity's mount state changed (or was revealed): render it.