Co-op Run Flow

In short

CoopRun is the co-op IRunDriver. Every machine builds the same map from the host’s seed and keeps its own RunState. The host starts each inning and announces each turn; the batter plays a mini-round on their own machine (1 out, 2 with a second life, starting from the team score) and reports the score it added. After a cleared inning everyone shops on their own and presses READY. The run ends for all when the pool runs out or inning 9 is cleared.

How it works

  1. Start. Host presses START RUN in the lobby (Lobby and Session). Every machine runs CoopRun.Start: it builds RunState from the seed, the host’s map (RunParkId) and difficulty, its own batter (from the roster, else the selected batter) and its batter level bonus. ShopSalt = LocalId + 1 gives each player different shop offers from the same seed. The host also creates CoopRules. Everyone lands on the run map.
  2. Inning start. Only the host can play (CanPlayRound); its button reads PLAY BALL! (ALL READY). Clients see “Waiting for {host} to press PLAY BALL.” The host sends RoundStart (inning, pool) and then TurnStart.
    • Inning 1 only: the match intro. On RoundStart every machine plays the intro with the whole lineup (cards built from the roster; everyone’s build is still their starting kit). The host does not send the first TurnStart yet: it makes an IntroGate of every player and waits. Each player who finishes, skips (Esc) or has cutscenes off counts as done (client → IntroDone; host → everyone IntroProgress with who is still reading). Players who are done hold on the face-off with WAITING FOR NAME (or ALEX & SAM, 3 PLAYERS (1/4), WAITING FOR TEAM…). When nobody is left reading, or after the intro’s camera time + 90 s, the host sends the first TurnStart. A player who leaves counts as done.
    • The first turn opens with the walk-up: the batter’s machine walks up under the intro’s tracking camera, then YOUR TURN! and the 3-2-1 come after the camera lands. Spectators keep the face-off until the batter’s AtBatBegin arrives, then watch the same walk-up. See Cutscenes.
  3. A turn. On TurnStart:
    • The batter’s machine (BeginMyTurn): stats = own stats + the park’s and pitcher’s batter modifiers; outs = OutsForTurn(ExtraOuts); pitch seed = RunState.TurnSeed(node, turnIndex); startingScore = team score; isTurn = true; a 3 s pre-roll for YOUR TURN! and a 3-2-1. It plays AtBatController.BeginRound like a solo inning and sends AtBatBegin, then streams the at-bat (Live Spectating). Each finished play goes out as a PlayFeed line. When the mini-round ends (“TURN OVER”, or the goal is reached) it sends TurnEnd with the score added, or, on the host, applies it directly.
    • Everyone else switches to GameState.CoopWatch and spectates.
  4. Next turn or inning end. The host feeds TurnEnd to CoopRules.CompleteTurn (Co-op Rules). Still playing → next TurnStart. Cleared or failed → RoundEnd (cleared, team score, outs left).
  5. Inning end (OnRoundEnd, every machine): pays this player’s coins, records the team score on the node and the run total, shows INNING n CLEARED! or OUT OF OUTS…. Failed → run over. Cleared and it was the last inning → run won. A cleared boss inning plays BOSS DOWN! first (the phase is already Shop and READY is cleared, so the ready gate waits for everyone’s cutscene). Otherwise the shop opens.
  6. Shop. Each player shops with their own coins and offers. The button is READY (then READY! while waiting). The shop lists each player as READY or SHOPPING. Pressing it sends ShopReady; the host broadcasts ReadyState. When everyone still in is ready, all go back to the map (“Everyone is ready.”).
  7. Run over (EndRun, every machine on its own): the phase becomes Over, everything is awarded and saved (see “Meta, unlocks and XP” below), then MAP BEATEN! or GAME OVER plays, then the results. The results screen has only MAIN MENU.
  8. Outros in co-op need no messages: each machine builds its own OutroCard and clicks through at its own pace. The figure at the plate is whoever batted last (their batter, cosmetics, and the bat seen in their AtBatBegin). The winning hit is replayed only if this machine saw it (it batted or watched it live since the turn began).
sequenceDiagram
    participant H as Host (CoopRules)
    participant B as Batter
    participant S as Spectators
    H->>B: StartRun (seed, order, map, difficulty)
    H->>S: StartRun
    Note over H,S: everyone on the run map
    H->>B: RoundStart (inning, pool)
    H->>S: RoundStart
    Note over H,S: inning 1: match intro on every screen
    B->>H: IntroDone
    S->>H: IntroDone
    H->>B: IntroProgress (done, total, still reading)
    H->>S: IntroProgress
    loop each turn
        H->>B: TurnStart (batter, turn, team score, outs left, pool)
        H->>S: TurnStart
        B->>H: AtBatBegin, AtBatPose..., AtBatPitch, AtBatSwing, AtBatResult
        H->>S: relayed live at-bat
        B->>H: PlayFeed (each play)
        H->>S: PlayFeed (relayed)
        B->>H: TurnEnd (score gained)
        H->>H: CompleteTurn
    end
    H->>B: RoundEnd (cleared, score, outs left)
    H->>S: RoundEnd
    Note over B,S: own coins, own shop
    B->>H: ShopReady
    S->>H: ShopReady
    H->>B: ReadyState (all ready → map)
    H->>S: ReadyState

Meta, unlocks and XP in co-op

CoopRun.EndRun runs on every machine for its own player:

AwardHowDiffers from solo
Map progressMeta.RecordMap(park, difficulty, won, TotalScore)Uses the team total; a team win counts toward everyone’s unlocks
Meta pointsMeta.AwardRun(summary, countsForBest: false): (inningsCleared × 10 + score / 500 + (won ? 50 : 0)) × difficulty (1 / 1.5 / 2.5)Does not touch your solo best score
XPMeta.AwardXp(summary): (inningsCleared × 25 + homeRuns × 5 + (won ? 150 : 0)) × difficultyhomeRuns is your own, not the team’s
LeaderboardnoneSolo submits to the local board; co-op does not
Run savenoneCanSaveAndQuit is false; the solo run.json is never touched

See Meta Progression and XP and Levels.

Leaving

  • LEAVE GAME is on the run map, the pause menu (instead of ABANDON RUN) and the watch screen. It sends Leave and returns to the title.
  • A client leaving: everyone sees “{NAME} LEFT THE GAME” in the feed; the host drops them from the order (their turn passes without an out).
  • The host leaving: every client’s session ends with “The host left the game” and goes to the title. If the run is already over, clients stay on the results.
  • Host leaving mid-run = no rewards CoopRun.OnSessionEnded calls Stop() without AwardRun / AwardXp, so clients get no meta points or XP for a run the host abandons. Only a run that reaches its end pays out.

  • A host leaving during MAP BEATEN! or GAME OVER changes nothing: the run is already Over and paid, so clients finish the cutscene and stay on the results. A host leaving during BOSS DOWN! (phase Shop) still ends the run for everyone.

Numbers

WhatValueWhere to tune
YOUR TURN! pre-roll3 sCoopRun.TurnPreRoll
Pose stream rate30 per secondCoopRun.PoseInterval = 1/30
Feed lines kept8CoopRun.MaxFeed
Messages handled per frame (max)256CoopRun.Tick guard
Big cheer on a watched home run≥ 450 ftCoopRun.OnPlayFeed
Intro gate deadlineintro camera time + 90 sCoopRun.IntroReadingAllowance
Innings per run9RunConfig.inningsPerRun

In the code

FileWhat it does
Assets/_Project/Scripts/Run/CoopRun.csStart, PlayCurrentRound, LeaveShop, AbandonRun, OnMessage, OnRoundStart, OnTurnStart, BeginMyTurn, OnMyPlay, OnMyTurnFinished, OnRoundEnd, OnReadyState, EndRun, Watch; intro: PlayMatchIntro, IntroFinishedHere, HostIntroProgress, ShowIntroProgress; outros: PlayOutro; CoopTicker drains the message queue and checks the intro gate each frame
Assets/_Project/Scripts/Intro/IntroGate.csHost: who is still reading the intro; Ready(now)
Assets/_Project/Scripts/Run/IRunDriver.csIsCoop, CanPlayRound, WaitingFor, CanSaveAndQuit, LeaveShop (co-op: ready), AbandonRun (co-op: leave)
Assets/_Project/Scripts/UI/RunMapView.csPLAY BALL! (ALL READY), LEAVE GAME
Assets/_Project/Scripts/UI/ShopView.csREADY / READY!, per-player READY / SHOPPING chips
Assets/_Project/Scripts/UI/HudView.csCo-op strip: CO-OP TEAM POOL n OUTS LEFT NEXT UP: name, SECOND LIFE, YOUR TURN! 3-2-1
Assets/_Project/Scripts/UI/CoopWatchView.csWatch screen (see Live Spectating)

Gotchas

  • Messages are queued and run on the next frame (Defer), including the host’s messages to itself, so nothing runs re-entrantly inside the at-bat coroutine or the network poll.
  • The batter’s machine decides its own plays; the host only adds up scoreGained. TurnEnd also carries home runs and longest hit, but the host ignores them.
  • The host’s PlayFeed relay overwrites the batter id with the sender’s id.
  • MULTIPLAYER-PLAN.md mentions a 90 s shop timer and a host map vote; neither is built. The map is a straight line of 9 innings, so there is no path to pick.
  • The intro gate holds only the first TurnStart: RoundStart has already gone out, so the inning’s pool is set.
  • Settings > CUTSCENES off on one machine just marks that player done at once; the others still watch.
  • Alt-tabbing pauses a window that is batting (UIRoot.OnApplicationFocus). NetRuntime keeps polling while paused and turns on Application.runInBackground when a session starts.