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:
+20
-4
@@ -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)
|
, 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,
|
"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)
|
"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":4194322,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
move_east={
|
move_east={
|
||||||
"deadzone": 0.5,
|
"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)
|
, 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={
|
move_west={
|
||||||
"deadzone": 0.5,
|
"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)
|
"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)
|
, 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={
|
interact={
|
||||||
"deadzone": 0.5,
|
"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)
|
"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)
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ extends Node
|
|||||||
|
|
||||||
# Semantic actions — NO raw key codes cross the bridge
|
# Semantic actions — NO raw key codes cross the bridge
|
||||||
enum Action {
|
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
|
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.)
|
# is_action_pressed handles press detection for all input types (key, gamepad, etc.)
|
||||||
if event.is_action_pressed("move_north"):
|
if event.is_action_pressed("move_north"):
|
||||||
action = Action.MOVE_NORTH
|
action = Action.MOVE_NORTH
|
||||||
elif event.is_action_pressed("move_south"):
|
elif event.is_action_pressed("move_northeast"):
|
||||||
action = Action.MOVE_SOUTH
|
action = Action.MOVE_NORTHEAST
|
||||||
elif event.is_action_pressed("move_east"):
|
elif event.is_action_pressed("move_east"):
|
||||||
action = Action.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"):
|
elif event.is_action_pressed("move_west"):
|
||||||
action = Action.MOVE_WEST
|
action = Action.MOVE_WEST
|
||||||
|
elif event.is_action_pressed("move_northwest"):
|
||||||
|
action = Action.MOVE_NORTHWEST
|
||||||
elif event.is_action_pressed("interact"):
|
elif event.is_action_pressed("interact"):
|
||||||
action = Action.INTERACT
|
action = Action.INTERACT
|
||||||
elif event.is_action_pressed("perception_mode"):
|
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 _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
|
||||||
var _outbound_buffer: Array[PackedByteArray] = [] # Encoded inputs awaiting transport
|
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
|
# Signals
|
||||||
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
|
||||||
signal snapshot_received(snapshot: Dictionary)
|
signal snapshot_received(snapshot: Dictionary)
|
||||||
@@ -24,22 +30,80 @@ func _set_state(new_state: ConnectionState) -> void:
|
|||||||
state = new_state
|
state = new_state
|
||||||
connection_state_changed.emit(old_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:
|
func connect_to_sim() -> void:
|
||||||
_set_state(ConnectionState.CONNECTING)
|
_set_state(ConnectionState.CONNECTING)
|
||||||
# TODO: Actual connection logic when IPC/MessagePack is implemented
|
|
||||||
if test_mode:
|
if test_mode:
|
||||||
_set_state(ConnectionState.CONNECTED)
|
_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)
|
_set_state(ConnectionState.ERROR)
|
||||||
|
return
|
||||||
|
# State transitions to CONNECTED in _process() when TCP handshake completes
|
||||||
|
|
||||||
# Disconnect from simulation server
|
# Disconnect from simulation server
|
||||||
func disconnect_from_sim() -> void:
|
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)
|
_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.
|
# Send input to simulation server.
|
||||||
# player_input: Dictionary with "action" (int from InputMapper.Action enum) and "timestamp_msec".
|
# 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:
|
func send_input(player_input: Dictionary) -> void:
|
||||||
if state != ConnectionState.CONNECTED:
|
if state != ConnectionState.CONNECTED:
|
||||||
return
|
return
|
||||||
@@ -74,7 +138,7 @@ func poll_snapshot() -> Variant:
|
|||||||
|
|
||||||
return null
|
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
|
# Latest-wins semantics: newer snapshots replace unconsumed ones. This is correct
|
||||||
# for real-time rendering (stale frames are worthless). Upgrade to queue if needed.
|
# for real-time rendering (stale frames are worthless). Upgrade to queue if needed.
|
||||||
func receive_bytes(bytes: PackedByteArray) -> void:
|
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])
|
push_warning("SimBridge: overwriting unconsumed snapshot (tick %s replaced by %s)" % [_last_snapshot.tick, snapshot.tick])
|
||||||
_last_snapshot = snapshot
|
_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]:
|
func drain_outbound() -> Array[PackedByteArray]:
|
||||||
var messages = _outbound_buffer.duplicate()
|
var messages = _outbound_buffer.duplicate()
|
||||||
_outbound_buffer.clear()
|
_outbound_buffer.clear()
|
||||||
@@ -95,9 +159,13 @@ func drain_outbound() -> Array[PackedByteArray]:
|
|||||||
static func _action_enum_to_wire(action: int) -> String:
|
static func _action_enum_to_wire(action: int) -> String:
|
||||||
match action:
|
match action:
|
||||||
InputMapper.Action.MOVE_NORTH: return "MoveNorth"
|
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_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_WEST: return "MoveWest"
|
||||||
|
InputMapper.Action.MOVE_NORTHWEST: return "MoveNorthwest"
|
||||||
InputMapper.Action.INTERACT: return "Interact"
|
InputMapper.Action.INTERACT: return "Interact"
|
||||||
InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode"
|
InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode"
|
||||||
InputMapper.Action.PAUSE: return "Pause"
|
InputMapper.Action.PAUSE: return "Pause"
|
||||||
|
|||||||
Reference in New Issue
Block a user