From 2338941721329d7c5fe592cea51035f1972a99eb Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 11 Feb 2026 21:44:14 +0100 Subject: [PATCH] fix(client): batch-encode inputs as Vec per server wire format Server expects a MessagePack array of PlayerInput objects in one framed message per tick, not individual inputs per frame. Added Protocol.encode_player_inputs() for batch encoding. Changed SimBridge to buffer raw input dicts and batch-encode in _process(). Also fixed server port default (9876) and positional arg format to match server CLI. Co-Authored-By: Claude Opus 4.6 --- client/scripts/autoloads/sim_bridge.gd | 40 +++++++++++++++----------- client/scripts/protocol/protocol.gd | 26 +++++++++++++++++ 2 files changed, 49 insertions(+), 17 deletions(-) diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index 9bda09f94..d5a64b83f 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -7,12 +7,12 @@ var state: ConnectionState = ConnectionState.DISCONNECTED var test_mode: bool = true # Enable test mode for development without Rust server var _test_tick: int = 0 var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot) -var _outbound_buffer: Array[PackedByteArray] = [] # Encoded inputs awaiting transport +var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport # Transport layer (non-test mode) var _bridge: LocalBridge = null var _server: ServerProcess = null -var server_port: int = 9800 +var server_port: int = 9876 # Default matches server's default bind address var server_path: String = "" # Path to server binary — set before connect_to_sim() # Connection retry state — handles server startup delay (Critical fix #1) @@ -50,7 +50,8 @@ func connect_to_sim() -> void: # Spawn server subprocess if not server_path.is_empty(): _server = ServerProcess.new() - var pid := _server.start(server_path, ["--port", str(server_port)]) + # Server reads first positional arg as bind address (e.g. "127.0.0.1:9876") + var pid := _server.start(server_path, ["127.0.0.1:" + str(server_port)]) if pid <= 0: push_error("SimBridge: failed to start server") _set_state(ConnectionState.ERROR) @@ -129,12 +130,16 @@ func _process(delta: float) -> void: while msg.size() > 0: receive_bytes(msg) msg = _bridge.poll_message() - # Send: flush outbound buffer through the bridge + # Send: batch-encode and flush outbound buffer as one frame (Vec) var outbound := drain_outbound() - for payload in outbound: - var err := _bridge.send_message(payload) - if err != OK: - push_error("SimBridge: failed to send message: %s" % error_string(err)) + if outbound.size() > 0: + var encoded := Protocol.encode_player_inputs(outbound) + if encoded.size() > 0: + var err := _bridge.send_message(encoded) + if err != OK: + push_error("SimBridge: failed to send message: %s" % error_string(err)) + else: + push_error("SimBridge: failed to batch-encode %d inputs" % outbound.size()) StreamPeerTCP.STATUS_CONNECTING: pass # Should not happen in CONNECTED state StreamPeerTCP.STATUS_ERROR: @@ -160,11 +165,12 @@ func send_input(player_input: Dictionary) -> Error: # _action_enum_to_wire already emits push_warning for invalid actions return ERR_INVALID_PARAMETER var tick: int = player_input.get("timestamp_msec", 0) - var encoded := Protocol.encode_player_input(tick, action_name) - if encoded.size() == 0: - push_error("SimBridge: failed to encode player input (action=%s)" % action_name) - return ERR_CANT_CREATE - _outbound_buffer.append(encoded) + var entry: Dictionary = { "tick": tick, "action_name": action_name } + # Data variants (e.g. UsePerceptionMode) carry payload + var action_data: Variant = player_input.get("action_data") + if action_data != null: + entry["action_data"] = action_data + _outbound_buffer.append(entry) return OK # Poll for snapshot from simulation. @@ -196,11 +202,11 @@ func receive_bytes(bytes: PackedByteArray) -> void: push_warning("SimBridge: overwriting unconsumed snapshot (tick %s replaced by %s)" % [_last_snapshot.tick, snapshot.tick]) _last_snapshot = snapshot -# Drain the outbound buffer. Returns encoded messages for transport. -func drain_outbound() -> Array[PackedByteArray]: - var messages = _outbound_buffer.duplicate() +# Drain the outbound buffer. Returns raw input entries for batch encoding. +func drain_outbound() -> Array[Dictionary]: + var inputs = _outbound_buffer.duplicate() _outbound_buffer.clear() - return messages + return inputs # Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction). # OPEN_MENU is client-only — no Rust equivalent, not sent over the wire. diff --git a/client/scripts/protocol/protocol.gd b/client/scripts/protocol/protocol.gd index be31cece7..319737f52 100644 --- a/client/scripts/protocol/protocol.gd +++ b/client/scripts/protocol/protocol.gd @@ -107,6 +107,32 @@ static func encode_player_input(tick: int, action_name: String, action_data: Var return result.value +## Encode an array of PlayerInputs to MessagePack bytes (Vec wire format). +## Server expects one framed message per tick containing all inputs as a msgpack array. +## Each entry: { "tick": int, "action_name": String, "action_data": Variant (optional) } +static func encode_player_inputs(inputs: Array) -> PackedByteArray: + var wire_inputs: Array = [] + for input in inputs: + var action_name: String = input["action_name"] + var action_data: Variant = input.get("action_data") + var action_value: Variant + if action_data != null: + action_value = { action_name: action_data } + else: + action_value = action_name + wire_inputs.append({ + "tick": input["tick"], + "action": action_value, + }) + + var result = Messagepack.encode(wire_inputs) + if result.status != null: + push_error("Protocol: msgpack encode failed: %s" % result.status) + return PackedByteArray() + + return result.value + + ## Decode a PlayerInput from MessagePack bytes (used in tests / echo scenarios). ## Returns { "tick": int, "action": { "variant": String, "data": Variant } } or null. static func decode_player_input(bytes: PackedByteArray) -> Variant: