Files
settled-reach/client/scripts/autoloads/sim_bridge.gd
T
jpmschweitzerandClaude Opus 4.6 5a539991e4 fix(client): address PR #4 round 2 review feedback
- decode_snapshot() reports dropped entities via push_error and returns
  decode_errors count so callers can detect partial data (D-010
  information boundary compliance)
- receive_bytes() warns when overwriting unconsumed snapshot, documents
  latest-wins semantics
- Rename misleading test to test_encode_produces_nonempty_bytes
- Fix tick rate comment: 10 ticks/game-minute per D-031

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:12:39 +01:00

128 lines
4.5 KiB
GDScript

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)
var encoded := Protocol.encode_player_input(tick, action_name)
if encoded.size() == 0:
push_error("SimBridge: failed to encode player input (action=%s)" % action_name)
return
_outbound_buffer.append(encoded)
# 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.
# 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:
var snapshot = Protocol.decode_snapshot(bytes)
if snapshot != null:
if _last_snapshot != null:
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.
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).
# OPEN_MENU is client-only — no Rust equivalent, not sent over the wire.
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_EAST: return "MoveEast"
InputMapper.Action.MOVE_WEST: return "MoveWest"
InputMapper.Action.INTERACT: return "Interact"
InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode"
InputMapper.Action.PAUSE: return "Pause"
InputMapper.Action.OPEN_MENU:
# Client-only action, not part of wire protocol
push_warning("SimBridge: OPEN_MENU is client-only, not sent to server")
return ""
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""
# Hardcoded test snapshot matching Protocol format (deterministic per D-010 principle 4).
# Uses the same {tick, entities} schema as Protocol.decode_snapshot() returns.
func _test_snapshot() -> Dictionary:
_test_tick += 1
return {
"tick": _test_tick,
"entities": [
{
"entity_id": 1,
"x": 10.0,
"y": 10.0,
"z": 0,
"kind": { "variant": "Npc", "data": null },
},
],
}