extends Node # Connection states enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR } 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 # Transport layer (non-test mode) var _bridge: LocalBridge = null var _server: ServerProcess = null var server_port: int = 9800 var server_path: String = "" # Path to server binary — set before connect_to_sim() # Signals signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState) signal snapshot_received(snapshot: Dictionary) func _ready() -> void: if test_mode: print("SimBridge: Running in test mode (hardcoded snapshot)") # Change connection state and emit signal func _set_state(new_state: ConnectionState) -> void: if state != new_state: var old_state = state state = new_state connection_state_changed.emit(old_state, new_state) # Connect to simulation server. # In test mode, immediately transitions to CONNECTED. # In live mode, spawns server subprocess and connects via TCP. func connect_to_sim() -> void: _set_state(ConnectionState.CONNECTING) if test_mode: _set_state(ConnectionState.CONNECTED) return # Spawn server subprocess if not server_path.is_empty(): _server = ServerProcess.new() var pid := _server.start(server_path, ["--port", str(server_port)]) if pid <= 0: push_error("SimBridge: failed to start server") _set_state(ConnectionState.ERROR) return # Connect TCP bridge _bridge = LocalBridge.new() var err := _bridge.connect_to_server("127.0.0.1", server_port) if err != OK: push_error("SimBridge: failed to initiate TCP connection: %s" % error_string(err)) _set_state(ConnectionState.ERROR) return # State transitions to CONNECTED in _process() when TCP handshake completes # Disconnect from simulation server func disconnect_from_sim() -> void: if _bridge != null: _bridge.disconnect_from_server() _bridge = null if _server != null: _server.stop() _server = null _set_state(ConnectionState.DISCONNECTED) # Poll transport layer every frame (non-test mode only) func _process(_delta: float) -> void: if test_mode or _bridge == null: return _bridge.poll() match _bridge.get_status(): StreamPeerTCP.STATUS_CONNECTED: if state != ConnectionState.CONNECTED: _set_state(ConnectionState.CONNECTED) # Receive: drain all complete messages from the bridge var msg := _bridge.poll_message() while msg.size() > 0: receive_bytes(msg) msg = _bridge.poll_message() # Send: flush outbound buffer through the bridge 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)) StreamPeerTCP.STATUS_CONNECTING: pass # Still connecting, wait StreamPeerTCP.STATUS_ERROR: if state != ConnectionState.ERROR: push_error("SimBridge: TCP connection error") _set_state(ConnectionState.ERROR) StreamPeerTCP.STATUS_NONE: if state == ConnectionState.CONNECTED: push_warning("SimBridge: connection lost") _set_state(ConnectionState.DISCONNECTED) # Send input to simulation server. # player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec". # In test mode, inputs are silently dropped. In live mode, encoded and buffered for transport. func send_input(player_input: Dictionary) -> void: if state != ConnectionState.CONNECTED: return if test_mode: return var action_name := _action_enum_to_wire(player_input.get("action", -1)) if action_name.is_empty(): return 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 _outbound_buffer.append(encoded) # Poll for snapshot from simulation. # In test mode returns hardcoded data. In live mode, returns the last decoded snapshot (if any). func poll_snapshot() -> Variant: if state != ConnectionState.CONNECTED: return null if test_mode: var snapshot = _test_snapshot() snapshot_received.emit(snapshot) return snapshot if _last_snapshot != null: var snapshot = _last_snapshot _last_snapshot = null snapshot_received.emit(snapshot) return snapshot return null # Called by transport layer when raw bytes arrive from the server. # Latest-wins semantics: newer snapshots replace unconsumed ones. This is correct # for real-time rendering (stale frames are worthless). Upgrade to queue if needed. func receive_bytes(bytes: PackedByteArray) -> void: var snapshot = Protocol.decode_snapshot(bytes) if snapshot != null: if _last_snapshot != null: 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() _outbound_buffer.clear() return messages # 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. static func _action_enum_to_wire(action: int) -> String: match action: InputMapper.Action.MOVE_NORTH: return "MoveNorth" InputMapper.Action.MOVE_NORTHEAST: return "MoveNortheast" InputMapper.Action.MOVE_EAST: return "MoveEast" InputMapper.Action.MOVE_SOUTHEAST: return "MoveSoutheast" InputMapper.Action.MOVE_SOUTH: return "MoveSouth" InputMapper.Action.MOVE_SOUTHWEST: return "MoveSouthwest" InputMapper.Action.MOVE_WEST: return "MoveWest" InputMapper.Action.MOVE_NORTHWEST: return "MoveNorthwest" InputMapper.Action.INTERACT: return "Interact" InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode" InputMapper.Action.PAUSE: return "Pause" InputMapper.Action.OPEN_MENU: # Client-only action, not part of wire protocol push_warning("SimBridge: OPEN_MENU is client-only, not sent to server") return "" _: push_warning("SimBridge: unknown action enum %s" % action) return "" # Hardcoded test snapshot matching Protocol format (deterministic per D-010 principle 4). # Uses the same {tick, entities} schema as Protocol.decode_snapshot() returns. func _test_snapshot() -> Dictionary: _test_tick += 1 return { "tick": _test_tick, "entities": [ { "entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": { "variant": "Npc", "data": null }, }, ], }