transit

Transit (Timed Journeys)

Timed movements between named nodes that survive restarts: an army marching on a village, a caravan carrying goods, a scout, a monk, a delivery drone. Start one with an opaque private payload and public data, recall it (it returns taking as long as it had travelled), redirect it, cancel it; the game's ITransitArrival resolves what arriving means (a siege, a trade, a drop), and may start the trip home. Owner and target see every change (ITransitAudience adds watchers), a since-id cursor lists what is in flight, and nothing streams while it travels - a client draws the journey from its two timestamps. Nodes are game-defined strings: a village on a 2D map, a city on a globe, a stop on a route.

Category Meta Seams 5 Services 1 Options 1 Wire ids 5 Unity types 1
Server
services.AddCrossplayTransit();
Unity package
com.crossplay.transit
Depends on
core

Seams you implement 3

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

ITransitArrival game SPI

The game's answer to "what happens when it gets there?" - one registration per kind it cares about (or kind 0 for every kind). Called once per arrival, after the transit left the store, so a handler may start a new transit (the survivors marching home) without the old one in the way.

  • ushort Kind { get; }

    The kind this handles (0 = every kind).

  • void OnArrived(TransitRecord transit)

    The transit arrived.

ITransitAudience game SPI

Who sees a transit: the accounts it is pushed to on every change, and whether one viewer may list it. The default is its owner and its target; a game adds watchers (allies, anyone watching that region of the map).

  • bool CanSee(long viewerAccountId, TransitRecord transit)

    True when viewerAccountId may see transit.

  • void CollectViewers(TransitRecord transit, List<long> accounts)

    Adds the accounts that see transit to accounts (reused list).

ITransitRecallPolicy game SPI

Who may recall a transit from the client. The default: its owner, while it is still outbound.

  • bool MayRecall(ISession session, TransitRecord transit, out ushort reasonCode)

    True when session may turn transit back.

Providers you can swap 2

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

ITransitClock provider

Wall clock seam (unix ms UTC) so journeys are testable.

  • long UtcNowMs { get; }

    Now, unix milliseconds UTC.

ITransitStore provider

Where transits live between restarts. The default is in memory; a database-backed one is composed with persistence.

  • void Delete(long id)

    Removes one transit.

  • IReadOnlyList<TransitRecord> LoadAll()

    Every stored transit (read once at boot).

  • long NextId()

    The next id (ever increasing, across restarts for a durable store).

  • void Save(TransitRecord transit)

    Saves (inserts or replaces) one transit.

Services you call 1

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

ITransitService

Timed movements between named nodes: start one, turn it back, send it elsewhere, cancel it, and be called when it arrives. Durable through ITransitStore: a restart reloads every transit, and the ones that came due while the server was down arrive on the first sweeps. What a node, a kind or an arrival MEANS is the game's.

  • bool Cancel(long transitId)

    Removes it without arriving.

  • event Action<TransitRecord, byte> Changed

    (transit, change): raised after every change (a TransitChange value), for game glue.

  • int CopyOwnedBy(long ownerAccountId, List<TransitRecord> into)

    Copies every in-flight transit owned by ownerAccountId into into.

  • int Count { get; }

    How many are in flight.

  • bool Recall(long transitId)

    Turns an outbound transit back for its origin; the return takes as long as it had travelled.

  • bool Redirect(long transitId, string newTo, double secondsFromNow)

    Sends it to newTo instead, arriving secondsFromNow from now (the game computes the new journey). This leg now starts at the moment of the redirect.

  • long Start(TransitStart start)

    Starts a transit and returns its id (0 when refused: empty node, oversized data, bad duration).

  • bool TryGet(long transitId, out TransitRecord transit)

    Looks one up.

Configuration 1

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

TransitOptions

Transit tuning (defaults here; never inlined in logic).

  • bool AllowClientRecall { get; set; }

    Handle the client recall request (the recall policy still decides who may). On by default: turning back your own is the common case; a game where nothing can be recalled turns it off.

  • int MaxArrivalsPerSweep { get; set; }

    Most arrivals resolved in one sweep (a restart after downtime catches up over several sweeps rather than in one long frame).

  • int MaxListCount { get; set; }

    Most transits one list reply carries (the client asks again from the last id for more).

  • int MaxNodeLength { get; set; }

    Longest node key, in characters.

  • int MaxPayloadBytes { get; set; }

    Largest payload (server-only) and public data, in bytes.

  • float SweepIntervalSeconds { get; set; }

    How often arrivals are checked, in seconds.

Wire messages 5

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

7201 TransitListRequest

Client -> server: list the transits I may see with an id greater than SinceId.

7202 TransitList

Server -> client: the in-flight transits the requester may see, oldest first.

7203 TransitChanged

Server -> client: a transit this client may see changed (see TransitChange).

7204 TransitRecallRequest

Client -> server: turn my transit back home.

7205 TransitRecallResponse

Server -> client: the verdict on a recall (the TransitChanged push follows on success).

Unity services you inject 1

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

ITransitClient

Client SDK for Transit: the journeys this client may see (its own, those coming at it, and whatever the game's audience adds), kept current from the server's pushes, a since-id list for a late join or a reconnect, and recalling your own. Nothing streams while a journey travels: draw it from DepartUtcMs / ArriveUtcMs.

  • IReadOnlyDictionary<long, TransitView> InFlight

    The in-flight journeys known to this client, by id.

  • long HighestId

    The highest transit id seen - the cursor to list from after a reconnect.

  • event Action<TransitChanged> Changed

    Raised on every pushed change (started, redirected, recalled, arrived, cancelled).

  • UniTask<TransitList> ListAsync(long sinceId = 0, CancellationToken ct = default)

    Lists the journeys newer than sinceId (0 = all I may see) and merges them in.

  • UniTask<TransitRecallResponse> RecallAsync(long transitId, CancellationToken ct = default)

    Turns one of my journeys back (the server's recall policy decides).