feat(ci): protocol version handshake client + IPC benchmark (#556, #342)

#556: HANDSHAKING state in sim_bridge.gd — decodes first framed message
as HandshakeMessage, validates vs Protocol.PROTOCOL_VERSION, 5s timeout,
handshake_complete/handshake_failed signals.

#342: IPC benchmark now reads and validates HandshakeMessage before
starting the timing loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 12:57:59 +01:00
co-authored by Claude Opus 4.6
parent 1bd528e521
commit b6c4ecb302
2 changed files with 83 additions and 16 deletions
+65 -2
View File
@@ -1,7 +1,7 @@
extends Node
# Connection states
enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR }
enum ConnectionState { DISCONNECTED, CONNECTING, HANDSHAKING, CONNECTED, ERROR }
var state: ConnectionState = ConnectionState.DISCONNECTED
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
@@ -27,9 +27,15 @@ const CONNECT_RETRY_INTERVAL: float = 0.1 # Seconds between retry attempts
var _connect_retries: int = 0
var _retry_timer: float = 0.0
# Handshake state (#556)
const HANDSHAKE_TIMEOUT_USEC: int = 5_000_000 # 5 seconds
var _handshake_start_usec: int = 0
# Signals
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
signal snapshot_received(snapshot: Dictionary)
signal handshake_complete(protocol_version: int)
signal handshake_failed(reason: String)
func _ready() -> void:
if test_mode:
@@ -121,7 +127,8 @@ func _process(delta: float) -> void:
_bridge.poll()
match _bridge.get_status():
StreamPeerTCP.STATUS_CONNECTED:
_set_state(ConnectionState.CONNECTED)
_handshake_start_usec = Time.get_ticks_usec()
_set_state(ConnectionState.HANDSHAKING)
StreamPeerTCP.STATUS_CONNECTING:
pass # Still connecting, wait
StreamPeerTCP.STATUS_ERROR:
@@ -134,6 +141,62 @@ func _process(delta: float) -> void:
_bridge = null # Reset and retry
return
# HANDSHAKING state: read first framed message, validate HandshakeMessage (#556)
if state == ConnectionState.HANDSHAKING:
if _bridge == null:
_set_state(ConnectionState.ERROR)
return
_bridge.poll()
# Check connection dropped during handshake
var bridge_status := _bridge.get_status()
if bridge_status == StreamPeerTCP.STATUS_ERROR or bridge_status == StreamPeerTCP.STATUS_NONE:
var reason := "Connection dropped during handshake"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge = null
_set_state(ConnectionState.ERROR)
return
# Check timeout
if Time.get_ticks_usec() - _handshake_start_usec > HANDSHAKE_TIMEOUT_USEC:
var reason := "Handshake timeout: no message received within 5 seconds"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
# Try to read first message
var msg := _bridge.poll_message()
if msg.is_empty():
return # Not ready yet, continue polling
# Decode HandshakeMessage: { "protocol_version": N }
var decoded: Variant = Messagepack.decode(msg)
if decoded.status != null or not (decoded.value is Dictionary) \
or not decoded.value.has("protocol_version"):
var reason := "Handshake decode failed: malformed HandshakeMessage"
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
var server_version: int = decoded.value["protocol_version"]
if server_version != Protocol.PROTOCOL_VERSION:
var reason := "Protocol version mismatch: server=%d, client=%d" % [
server_version, Protocol.PROTOCOL_VERSION]
push_error("SimBridge: %s" % reason)
handshake_failed.emit(reason)
_bridge.disconnect_from_server()
_set_state(ConnectionState.ERROR)
return
handshake_complete.emit(server_version)
_set_state(ConnectionState.CONNECTED)
return
if _bridge == null:
return