diff --git a/client/project.godot b/client/project.godot index 74765696f..6085c83a7 100644 --- a/client/project.godot +++ b/client/project.godot @@ -39,11 +39,9 @@ move_north={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194320,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } -move_south={ +move_northeast={ "deadzone": 0.5, -"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) -, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) -] +"events": [] } move_east={ "deadzone": 0.5, @@ -51,12 +49,30 @@ move_east={ , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194321,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +move_southeast={ +"deadzone": 0.5, +"events": [] +} +move_south={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +move_southwest={ +"deadzone": 0.5, +"events": [] +} move_west={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) , Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194319,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) ] } +move_northwest={ +"deadzone": 0.5, +"events": [] +} interact={ "deadzone": 0.5, "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null) diff --git a/client/scripts/autoloads/input_mapper.gd b/client/scripts/autoloads/input_mapper.gd index d53412797..faf10eebc 100644 --- a/client/scripts/autoloads/input_mapper.gd +++ b/client/scripts/autoloads/input_mapper.gd @@ -2,7 +2,8 @@ extends Node # Semantic actions — NO raw key codes cross the bridge enum Action { - MOVE_NORTH, MOVE_SOUTH, MOVE_EAST, MOVE_WEST, + MOVE_NORTH, MOVE_NORTHEAST, MOVE_EAST, MOVE_SOUTHEAST, + MOVE_SOUTH, MOVE_SOUTHWEST, MOVE_WEST, MOVE_NORTHWEST, INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE } @@ -15,12 +16,20 @@ func _unhandled_input(event: InputEvent) -> void: # is_action_pressed handles press detection for all input types (key, gamepad, etc.) if event.is_action_pressed("move_north"): action = Action.MOVE_NORTH - elif event.is_action_pressed("move_south"): - action = Action.MOVE_SOUTH + elif event.is_action_pressed("move_northeast"): + action = Action.MOVE_NORTHEAST elif event.is_action_pressed("move_east"): action = Action.MOVE_EAST + elif event.is_action_pressed("move_southeast"): + action = Action.MOVE_SOUTHEAST + elif event.is_action_pressed("move_south"): + action = Action.MOVE_SOUTH + elif event.is_action_pressed("move_southwest"): + action = Action.MOVE_SOUTHWEST elif event.is_action_pressed("move_west"): action = Action.MOVE_WEST + elif event.is_action_pressed("move_northwest"): + action = Action.MOVE_NORTHWEST elif event.is_action_pressed("interact"): action = Action.INTERACT elif event.is_action_pressed("perception_mode"): diff --git a/client/scripts/autoloads/sim_bridge.gd b/client/scripts/autoloads/sim_bridge.gd index aa3794948..3f8e02b82 100644 --- a/client/scripts/autoloads/sim_bridge.gd +++ b/client/scripts/autoloads/sim_bridge.gd @@ -9,6 +9,12 @@ 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) @@ -24,22 +30,80 @@ func _set_state(new_state: ConnectionState) -> void: state = new_state connection_state_changed.emit(old_state, new_state) -# Connect to simulation server (real implementation comes later) +# 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) - # TODO: Actual connection logic when IPC/MessagePack is implemented + if test_mode: _set_state(ConnectionState.CONNECTED) - else: + 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. Transport (ticket #79) will send the bytes. +# 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 @@ -74,7 +138,7 @@ func poll_snapshot() -> Variant: return null -# Called by transport layer (ticket #79) when raw bytes arrive from the server. +# 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: @@ -84,7 +148,7 @@ 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. Called by transport layer (ticket #79) to get encoded messages. +# Drain the outbound buffer. Returns encoded messages for transport. func drain_outbound() -> Array[PackedByteArray]: var messages = _outbound_buffer.duplicate() _outbound_buffer.clear() @@ -95,9 +159,13 @@ func drain_outbound() -> Array[PackedByteArray]: static func _action_enum_to_wire(action: int) -> String: match action: InputMapper.Action.MOVE_NORTH: return "MoveNorth" - InputMapper.Action.MOVE_SOUTH: return "MoveSouth" + 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"