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 # 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 (real implementation comes later) func connect_to_sim() -> void: _set_state(ConnectionState.CONNECTING) # TODO: Actual connection logic when IPC/MessagePack is implemented if test_mode: _set_state(ConnectionState.CONNECTED) else: _set_state(ConnectionState.ERROR) # Disconnect from simulation server func disconnect_from_sim() -> void: _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. Transport (ticket #79) will send the bytes. 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) _outbound_buffer.append(Protocol.encode_player_input(tick, action_name)) # 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 (ticket #79) when raw bytes arrive from the server. func receive_bytes(bytes: PackedByteArray) -> void: var snapshot = Protocol.decode_snapshot(bytes) if snapshot != null: _last_snapshot = snapshot # Drain the outbound buffer. Called by transport layer (ticket #79) to get encoded messages. 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). static func _action_enum_to_wire(action: int) -> String: match action: 0: return "MoveNorth" # InputMapper.Action.MOVE_NORTH 1: return "MoveSouth" # InputMapper.Action.MOVE_SOUTH 2: return "MoveEast" # InputMapper.Action.MOVE_EAST 3: return "MoveWest" # InputMapper.Action.MOVE_WEST 4: return "Interact" # InputMapper.Action.INTERACT 5: return "UsePerceptionMode" # InputMapper.Action.USE_PERCEPTION_MODE 7: return "Pause" # InputMapper.Action.PAUSE _: push_warning("SimBridge: unknown action enum %s" % action) return "" # Hardcoded test snapshot for development (deterministic per D-010 principle 4) func _test_snapshot() -> Dictionary: _test_tick += 1 return { "tick": _test_tick, "player": { "position": [10, 10], "health": 100 }, "entities": [ { "id": 1, "type": "npc", "position": [12, 8], "name": "Test NPC" }, ], "fog": { "radius": 8 }, "hud": { "perception_mode": "baseline", "time": "08:00" } }