Skip to content

switch live gameplay websocket traffic to binary#3514

Draft
scamiv wants to merge 7 commits intoopenfrontio:mainfrom
scamiv:feat-ws-binary-gameplay-v1
Draft

switch live gameplay websocket traffic to binary#3514
scamiv wants to merge 7 commits intoopenfrontio:mainfrom
scamiv:feat-ws-binary-gameplay-v1

Conversation

@scamiv
Copy link
Copy Markdown
Contributor

@scamiv scamiv commented Mar 25, 2026

Description:

Switch live gameplay WebSocket traffic from JSON to binary while keeping setup and reconnect messages as JSON.

JSON remains for:

  • join, rejoin
  • lobby_info, prestart, start
  • error, winner

Binary is now used for:

  • client -> server: intent, hash, ping
  • server -> client: turn, desync

Changes

  • Added a shared binary codec in src/core/BinaryProtocol.ts and src/core/BinaryCodec.ts
  • Updated client transport to send/receive binary gameplay frames after start
  • Updated server gameplay handling to decode binary client frames and emit binary turns/desyncs
  • Preserved existing lockstep behavior, auth stamping, reconnect flow, rate limiting, and turn cadence
  • Fixed binary player refs to support bot/nation runtime IDs, matching the old JSON behavior

Testing

  • npx tsc --noEmit
  • npx vitest run tests/core/BinaryCodec.test.ts tests/server/GameServerBinaryProtocol.test.ts

Out of scope/todo:

  • binary start
  • reconnect "catch-up" payloads
  • WebTransport / WebRTC migration

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced

Please put your Discord username so you can be contacted if a bug or regression is found:

DISCORD_USERNAME

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 25, 2026

Walkthrough

A binary protocol layer was added for gameplay messages. Transport and server now detect JSON vs binary at runtime, initialize a binary context on game start, and encode/decode gameplay intents/turns in compact binary frames alongside existing JSON control messages.

Changes

Cohort / File(s) Summary
Binary Protocol Core
src/core/BinaryProtocol.ts
New protocol constants, enums (message & intent opcodes), player-index sentinels, conversion helpers, and BinaryProtocolContext construction.
Binary Codec Implementation
src/core/BinaryCodec.ts
New binary reader/writer, frame validation, encoding/decoding functions for client/server gameplay messages, and helpers to build/use a binary context. Exports many encode/decode functions and type guard.
Client Transport Changes
src/client/Transport.ts
Buffer now holds `string
Server Game Logic
src/server/GameServer.ts
Handles WebSocket raw/binary messages, decodes binary client gameplay messages, enforces binary-only gameplay after start, creates server-side binaryContext on start, and sends encoded binary turn/desync messages. Rate-limit and dispatch refactoring into helpers.
Unit Tests — Codec
tests/core/BinaryCodec.test.ts
Round-trip tests for encoding/decoding client intents, hash, ping, and server turn/desync; validation/error cases for protocol version, player indexes, flags, and truncated frames.
Integration Tests — Server Binary
tests/server/GameServerBinaryProtocol.test.ts
End-to-end tests: JSON start then binary intents, server emits binary turn messages, validation of decoded intent fields and client mapping, handling of malformed binary messages, and JSON rejoin behavior.

Sequence Diagram

sequenceDiagram
    participant ClientApp as Client App
    participant ClientTrans as Client Transport
    participant WSNet as WebSocket Network
    participant ServerGame as Game Server

    rect rgba(0,150,255,0.5)
    Note over ClientApp,ServerGame: Game start via JSON
    ServerGame->>WSNet: send JSON "start" (gameStartInfo)
    WSNet->>ClientTrans: deliver "start" (string)
    ClientTrans->>ClientTrans: activateBinaryGameplay(gameStartInfo)
    ClientTrans->>ClientApp: binary mode active
    end

    rect rgba(0,200,100,0.5)
    Note over ClientApp,ServerGame: Gameplay (binary)
    ClientApp->>ClientTrans: ClientIntentMessage
    ClientTrans->>ClientTrans: encodeBinaryClientGameplayMessage()
    ClientTrans->>WSNet: send Uint8Array (binary)
    WSNet->>ServerGame: receive binary intent
    ServerGame->>ServerGame: decodeBinaryClientGameplayMessage()
    ServerGame->>ServerGame: process intent
    end

    rect rgba(200,100,0,0.5)
    Note over ClientApp,ServerGame: Turn result (binary)
    ServerGame->>ServerGame: endTurn()
    ServerGame->>ServerGame: encodeBinaryServerGameplayMessage()
    ServerGame->>WSNet: send Uint8Array (turn)
    WSNet->>ClientTrans: deliver binary turn
    ClientTrans->>ClientTrans: decodeBinaryServerGameplayMessage()
    ClientTrans->>ClientApp: update game state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

Bits and frames now dance in line,
JSON greets start, then binary shines,
Intents in bytes, turns swift and small,
Packets hum — the game hears all. 🎮✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'switch live gameplay websocket traffic to binary' clearly and concisely describes the main change: converting WebSocket traffic for live gameplay from JSON to binary format.
Description check ✅ Passed The description is well-structured, related to the changeset, and clearly explains what is being changed (JSON to binary), what remains JSON, and provides implementation details and testing instructions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/BinaryCodec.ts`:
- Around line 823-831: The helper is incorrectly classifying every "intent" as
binary which causes Transport.serializeMessage to later throw for intents that
have no opcode (e.g., kick_player and update_game_config); update
isBinaryGameplayClientMessage to only return true for intent messages that map
to a binary opcode (use intentTypeToOpcode(message.intent.type) !== undefined or
explicitly exclude "kick_player" and "update_game_config"), so that
isBinaryGameplayClientMessage, BinaryClientGameplayMessage, ClientMessage,
intentTypeToOpcode, Transport.serializeMessage, and callers like
start/GameServer behave correctly.

In `@src/server/GameServer.ts`:
- Around line 326-347: The binary handler currently lets
decodeBinaryClientGameplayMessage throw into the outer catch (which only logs)
so malformed binary frames bypass checkRateLimit and don't get disconnected;
wrap the decodeBinaryClientGameplayMessage call in its own try/catch inside the
client.ws.on("message",...) branch, and on any decode/parsing error call
this.kickClient(client.clientID, KICK_REASON_INVALID_MESSAGE) and return
(optionally log the parse error), ensuring checkRateLimit is only called for
successfully decoded messages and malformed binary frames are kicked
immediately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1e55bffb-89ed-4fc2-a9d5-60b0dea54660

📥 Commits

Reviewing files that changed from the base of the PR and between 4e126c2 and 315e98f.

📒 Files selected for processing (6)
  • src/client/Transport.ts
  • src/core/BinaryCodec.ts
  • src/core/BinaryProtocol.ts
  • src/server/GameServer.ts
  • tests/core/BinaryCodec.test.ts
  • tests/server/GameServerBinaryProtocol.test.ts

@github-project-automation github-project-automation bot moved this from Triage to Development in OpenFront Release Management Mar 25, 2026
@scamiv
Copy link
Copy Markdown
Contributor Author

scamiv commented Mar 25, 2026

Follow-up

  • 69db6c1ac fix(server): kick malformed binary gameplay frames

Tighten binary gameplay error handling to match the old JSON path.

Malformed binary gameplay frames are now treated as invalid protocol messages and kicked with kick_reason.invalid_message, instead of only being logged.

Changes

  • wrap binary gameplay decode in an explicit parse failure path on the server
  • kick malformed binary frames the same way malformed JSON messages are kicked
  • add a regression test covering malformed binary traffic after start

Follow-up Limit Suggestion

Binary gameplay frames are about 3.75x to 4.9x smaller than the old JSON hot path in representative samples.

If we want byte-based rate limits to apply roughly the same effective message count pressure as before reduce the byte ceilings by about 4x:

  • change MAX_INTENT_SIZE from 500 to 128
  • change TOTAL_BYTES from 2 MB to 512 KB

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
tests/server/GameServerBinaryProtocol.test.ts (2)

141-144: Add a comment explaining the magic number.

message[1] === 2 checks for the binary turn message type, but this is not obvious. A quick comment would help future readers.

📝 Suggested comment
     const binaryTurn = clientA.ws.sent.find(
       (message): message is Uint8Array =>
-        message instanceof Uint8Array && message[1] === 2,
+        // byte[1] === 2 is the binary "turn" message type
+        message instanceof Uint8Array && message[1] === 2,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/server/GameServerBinaryProtocol.test.ts` around lines 141 - 144, Add a
short explanatory comment next to the predicate in the find call that checks
message[1] === 2 (the arrow function used to locate binaryTurn in
clientA.ws.sent) to state that index 1 encodes the message type and that value 2
represents the "binary turn" message type; update the comment near the
binaryTurn declaration or inside the predicate to clarify the magic number for
future readers.

262-263: Add a comment explaining why this payload is malformed.

The byte sequence [99, 1, 0, 0] triggers a decode failure, but the reason is not clear. A brief comment helps readers understand what makes this invalid (wrong magic byte? invalid message type?).

📝 Suggested comment
     clientA.ws.sent.length = 0;
+    // Invalid magic byte (99) causes decode to fail
     clientA.ws.emit("message", new Uint8Array([99, 1, 0, 0]), true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/server/GameServerBinaryProtocol.test.ts` around lines 262 - 263, Add a
one-line inline comment above the clientA.ws.emit call explaining why the
payload [99, 1, 0, 0] is malformed: note which field is invalid (e.g., the first
byte (99) is not the expected magic/version/header value or the header indicates
an invalid/unsupported message type/length), so the decoder fails; reference the
exact payload and the clientA.ws.emit call so readers immediately see why this
test triggers a decode error.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/server/GameServerBinaryProtocol.test.ts`:
- Around line 141-144: Add a short explanatory comment next to the predicate in
the find call that checks message[1] === 2 (the arrow function used to locate
binaryTurn in clientA.ws.sent) to state that index 1 encodes the message type
and that value 2 represents the "binary turn" message type; update the comment
near the binaryTurn declaration or inside the predicate to clarify the magic
number for future readers.
- Around line 262-263: Add a one-line inline comment above the clientA.ws.emit
call explaining why the payload [99, 1, 0, 0] is malformed: note which field is
invalid (e.g., the first byte (99) is not the expected magic/version/header
value or the header indicates an invalid/unsupported message type/length), so
the decoder fails; reference the exact payload and the clientA.ws.emit call so
readers immediately see why this test triggers a decode error.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23a19b12-8b7e-4a54-a91c-a99e795f4ff9

📥 Commits

Reviewing files that changed from the base of the PR and between 315e98f and 69db6c1.

📒 Files selected for processing (2)
  • src/server/GameServer.ts
  • tests/server/GameServerBinaryProtocol.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/server/GameServer.ts

@github-project-automation github-project-automation bot moved this from Development to Final Review in OpenFront Release Management Mar 25, 2026
@scamiv scamiv marked this pull request as draft March 27, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Final Review

Development

Successfully merging this pull request may close these issues.

1 participant