feat(client): integrate LocalBridge transport and 8-directional movement
Wire SimBridge to use LocalBridge for TCP transport in non-test mode: _process() polls for incoming snapshots and flushes outbound inputs. Add 4 diagonal movement variants (NE, SE, SW, NW) to InputMapper and wire mapping, ordered clockwise. Register diagonal input actions in project.godot. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user