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:
2026-02-11 20:59:40 +01:00
co-authored by Claude Opus 4.6
parent 6dfaf2831f
commit 2ecafff2ed
3 changed files with 107 additions and 14 deletions
+12 -3
View File
@@ -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"):
+75 -7
View File
@@ -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"