feat(client): atlas request/response transport on the bridge (#960, D-225)

Client transport half of the layer-stream protocol.
- SimBridge.request_atlas_layers(body_id) sends an AtlasLayerRequest frame
  (live mode only; no-op in test mode); responses arrive via a new
  atlas_layers_received signal.
- receive_bytes now decodes each frame ONCE via Protocol.decode_inbound and
  branches by shape (snapshot vs atlas response) — avoids double-decoding the
  20 Hz snapshot path. decode_snapshot is split into decode_raw +
  _decode_snapshot_from_raw (public decode_snapshot unchanged, so the 70 protocol
  tests stay the regression guard); decode_inbound returns {kind, value}.

70/70 protocol tests pass, including the new decode_inbound classifier test.
(Pre-existing client-suite failures in server-dependent e2e/roundtrip + unrelated
audio/fog/dialogue suites are unchanged — verified identical at baseline.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-24 18:36:39 +02:00
co-authored by Claude Opus 4.7
parent a9ba8ba90c
commit 03d0264eb0
3 changed files with 77 additions and 10 deletions
+28 -2
View File
@@ -3,6 +3,7 @@ extends Node
# Signals
signal connection_state_changed(old_state: ConnectionState, new_state: ConnectionState)
signal snapshot_received(snapshot: Dictionary)
signal atlas_layers_received(response: Dictionary)
signal handshake_complete
signal handshake_failed(reason: String)
@@ -387,6 +388,22 @@ func send_named_action(action_name: String, action_data: Variant = null) -> void
_outbound_buffer.append(entry)
## Request a body's generation-cascade layers from the server (#960, D-225).
## Live mode only — sends an AtlasLayerRequest frame; the response arrives via the
## atlas_layers_received signal. No-op in test mode (no server connection).
func request_atlas_layers(body_id: String, up_to: String = "Topography") -> void:
if test_mode or _bridge == null or state != ConnectionState.CONNECTED:
return
var bytes := Protocol.encode_atlas_layer_request(body_id, up_to)
if bytes.is_empty():
return
var err: int = _bridge.send_message(bytes)
if err != OK:
push_error(
"SimBridge: failed to send atlas layer request for %s: %s" % [body_id, error_string(err)]
)
# Poll for snapshot from simulation.
# In test mode delegates to test harness. In live mode, returns the last decoded snapshot.
func poll_snapshot() -> Variant:
@@ -412,9 +429,18 @@ func poll_snapshot() -> Variant:
# events (monologue, dialogue) are carried forward from overwritten snapshots
# so they aren't silently dropped when server ticks faster than client consumes.
func receive_bytes(bytes: PackedByteArray) -> void:
var snapshot = Protocol.decode_snapshot(bytes)
# Decode once, branch by frame shape (#960, D-225): atlas layer responses and
# snapshots are both msgpack maps, told apart by field.
var inbound := Protocol.decode_inbound(bytes)
if inbound.kind == "atlas":
atlas_layers_received.emit(inbound.value)
return
if inbound.kind != "snapshot":
push_warning("SimBridge: undecodable frame (%d bytes)" % bytes.size())
return
var snapshot = inbound.value
if snapshot == null:
push_warning("SimBridge: decode_snapshot returned null for %d bytes" % bytes.size())
push_warning("SimBridge: snapshot decode returned null for %d bytes" % bytes.size())
return
if _last_snapshot != null:
# Carry forward one-shot events the client hasn't consumed yet.