Net Messages
In short
Every message is a byte array whose first byte is a
MsgId. Ids 1-2 are NET TEST’s ping, 10-16 the lobby (NetSession), 30-38 the co-op run (37-38 the match intro) and 40-44 live spectating (CoopRun). Clients only talk to the host; the host relays a batter’s messages to the others. Everything is reliable exceptAtBatPose. Never renumber an id.
How it works
Encoding (NetWriter / NetReader): little-endian, no reflection. byte, bool (1 byte), ushort (2), int
(4), float (4, raw bits), double (8), Vec2 (2 floats), Vec3 (3 floats), string (ushort length + UTF-8).
A short message throws FormatException; NetRuntime catches it and logs “Malformed message from player n ignored”.
Routing.
- The transport raises
Received(from, bytes). NetRuntime.OnReceivedreads the id:Pingis answered withPong,Pongstores the round trip; everything else goes toNetSession.Handle.NetSessionhandles ids below 30 itself and passes 30 and up toGameMessage(→CoopRun.OnMessage).CoopRunqueues each message for the next frame.
Sending. NetSession.SendToAll (host: every admitted player; client: the host), SendTo(player),
SendToAllExcept(msg, player) (host relay). On a client, “to all” means “to the host”, which relays when needed.
Message table
Direction: c = client, h = host, B = the current batter (host or client). “Relay” means the host forwards the same bytes to every player except the sender.
| Id | Name | Direction | Reliable | Payload (wire order) |
|---|---|---|---|---|
| 1 | Ping | either (NET TEST) | yes | double send time (realtimeSinceStartupAsDouble) |
| 2 | Pong | either (NET TEST) | yes | double the Ping’s send time |
| 10 | Hello | c → h | yes | string protocol, string version, PlayerInfo |
| 11 | Welcome | h → c | yes | byte player id |
| 12 | Refuse | h → c | yes | string reason |
| 13 | Roster | h → all | yes | byte count, PlayerInfo × count |
| 14 | SetBatter | c → h | yes | string batterId, byte count, string cosmeticId × count |
| 15 | StartRun | h → all | yes | int seed, byte count, byte player id × count (batting order), string parkId, byte difficulty |
| 16 | Leave | either | yes | (none) |
| 30 | RoundStart | h → all | yes | byte inning index, byte pool |
| 31 | TurnStart | h → all | yes | byte batter, ushort turnIndex, int teamScore, byte outsLeft, byte pool |
| 32 | PlayFeed | B → h → relay | yes | byte batter, string headline, byte r, byte g, byte b, int teamScore, bool homeRun, bool wasOut, float distanceFt |
| 33 | TurnEnd | B → h | yes | int scoreGained, byte homeRuns, float longestFt |
| 34 | RoundEnd | h → all | yes | bool cleared, int teamScore, byte outsLeft |
| 35 | ShopReady | c → h | yes | (none) |
| 36 | ReadyState | h → all | yes | byte count, byte player id × count |
| 37 | IntroDone | c → h | yes | (none): this player finished, skipped, or has cutscenes off |
| 38 | IntroProgress | h → all | yes | byte done, byte total, byte count, byte player id × count (who is still reading) |
| 40 | AtBatBegin | B → h → relay | yes | byte batter, string batUpgradeId, byte outs, float ringRadius, int teamScore |
| 41 | AtBatPose | B → h → relay | no | float t, Vec2 aim, Vec3 hands, Vec3 direction, float twist |
| 42 | AtBatPitch | B → h → relay | yes | float at, string pitch type id, Vec3 release, Vec3 plateLocation, float speedMph, float releaseTime, float flightTime, float wobble, bool isInZone, byte effect, byte bossEffect, float arcAngle, byte decoy count, Vec3 decoy × count |
| 43 | AtBatSwing | B → h → relay | yes | float pressTime |
| 44 | AtBatResult | B → h → relay | yes | float at, byte outcome, float timingOffset, float timingQuality, float contactQuality, float quality, bool isGreen, byte greenStreak (clamped 0-255), float exitVelocityMph, float launchAngleDeg, float sprayAngleDeg, byte zone, Vec2 hitAim, float curveAccel, float gravityScale, bool madeContact; if madeContact: float contact time, Vec3 ballPosition, Vec3 batDirection, int score, bool isHomeRun, bool isFair, float distanceFt |
PlayerInfo (inside Hello and Roster): byte id, string name, string batterId, byte count, string cosmeticId ×
count, bool connected.
Times in 40-44 are the batter’s Time.time; the spectator converts them (Live Spectating).
Who handles what
Hello,SetBatter: only the host acts on them.Welcome,Refuse,Roster,StartRun: only clients.Leavefrom the host (id 0) ends a client’s session; on the host, the transport’sPeerLeftthat follows removes the player.RoundStart,TurnStart,RoundEnd,ReadyState: clients only (the host applies its own copy directly).PlayFeed: the host setsbatterto the sender, relays, and shows it; clients show it.TurnEnd,ShopReady,IntroDone: host only.TurnEnd’s home runs and longest are read and ignored.IntroProgress: clients only; the host shows its own copy directly. Both feed the WAITING FOR NAME line (Cutscenes).- 40-44: the host relays only while an inning is playing and only from
CoopRules.CurrentBatter; the relay keeps the sender’s reliability (pose unreliable, the rest reliable).
Transport framing (below these messages)
- Local: each UDP packet starts with a kind byte: Connect 1 (+ string
HRD-LOCAL-1), Accept 2 (+ byte peer id), Refuse 3 (+ string), Unreliable 4, Reliable 5 (+ int sequence), Ack 6 (+ int sequence), Heartbeat 7, Disconnect 8. - Steam: the host’s first message to a client is the 2 bytes
[0xFF, peer id]; everything else is a game message.
See Transports.
Numbers
| What | Value | Where to tune |
|---|---|---|
| Pose message | 41 bytes, 30 per second | SpectateMessages.Pose, CoopRun.PoseInterval |
| Biggest Local packet | 1200 bytes | LocalTransport.MaxPacket |
| Writer start size | 256 bytes (grows) | NetWriter |
In the code
| File | What it does |
|---|---|
Assets/_Project/Scripts/Net/NetMessages.cs | MsgId, PlayerInfo, NetMessages (10-38 writers / readers, ReadIntroWaiting), SpectateMessages (40-44, ReadDecoys), TurnInfo, PlayInfo |
Assets/_Project/Scripts/Net/NetBuffers.cs | NetWriter, NetReader (Source gives the whole message for relaying) |
Assets/_Project/Scripts/Net/NetRuntime.cs | OnReceived: ping / pong, then the session |
Assets/_Project/Scripts/Net/NetSession.cs | Handle for 10-16, GameMessage for 30+ |
Assets/_Project/Scripts/Run/CoopRun.cs | OnMessage, OnSpectateMessage |
Assets/_Project/Editor/NetSessionTest.cs | RoundTrips: every run message reads back what was written |
Assets/_Project/Editor/MatchIntroTest.cs | IntroGate and the IntroProgress round trip |
Gotchas
- Ids 17-29 and 39 are unused;
NetSessionsilently ignores unknown ids below 30. - The end-of-game cutscenes send nothing: every machine plays them from its own state.
- Adding or changing a payload must bump
NetSession.Protocol, or an old build reads garbage. See Protocol Versions. PR #46 broke this rule: it addedzone,hitAim,curveAccelandgravityScaletoAtBatResultand left the protocol atHRD-COOP-7. - Ids and counts are single bytes: player ids, inning index, pool and outs must stay under 256.
MULTIPLAYER-PLAN.mdsection 4 lists a different, older message set (ClockPing,RoundStarted,PitchThrown,Snapshot,Kick…). None of those exist; the table above is the code. Clock sync is not built: the spectator’s offset replaces it.
Related
- Lobby and Session: the 10-16 messages in context
- Co-op Run Flow: the 30-36 messages in context
- Live Spectating: the 40-44 messages in context
- Transports: how bytes travel
- Protocol Versions: when payloads changed
- Add a Net Message: step by step