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
+38 -8
View File
@@ -15,17 +15,31 @@ static func _mp():
# -- Decode: bytes from server → GDScript types --------------------------------
## Decode a raw MessagePack frame to its top-level value (or null on error).
## #960: lets receive_bytes decode a frame once and branch by shape before
## committing to the heavier snapshot decode.
static func decode_raw(bytes: PackedByteArray) -> Variant:
var result = _mp().decode(bytes)
if result.status != null:
push_error("Protocol: msgpack decode failed: %s" % result.status)
return null
return result.value
## Decode an ObserverSnapshot from MessagePack bytes.
## Returns decoded snapshot Dictionary or null on error.
## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty
## when decoding v1 snapshots for backward compatibility.
static func decode_snapshot(bytes: PackedByteArray) -> Variant:
var result = _mp().decode(bytes)
if result.status != null:
push_error("Protocol: msgpack decode failed: %s" % result.status)
var raw = decode_raw(bytes)
if raw == null:
return null
return _decode_snapshot_from_raw(raw)
var raw = result.value
## Build an ObserverSnapshot from an already-decoded raw value (the snapshot
## body, shared by decode_snapshot and the receive-side classifier).
static func _decode_snapshot_from_raw(raw: Variant) -> Variant:
if not raw is Dictionary or not raw.has("tick") or not raw.has("entities"):
push_error("Protocol: snapshot missing required fields")
return null
@@ -745,10 +759,12 @@ static func encode_atlas_layer_request(body_id: String, up_to: String = "Topogra
## name ("Ready"|"Pending"|"NotFound"|"Error"); error holds the message for the
## Error variant. layer1 is the raw decoded Layer1Output map, or null.
static func decode_atlas_layer_response(bytes: PackedByteArray) -> Variant:
var result = _mp().decode(bytes)
if result.status != null:
return null
var raw = result.value
return atlas_response_from_raw(decode_raw(bytes))
## Build an AtlasLayerResponse from an already-decoded raw value. Returns null
## if it is not an atlas response (no "status" key).
static func atlas_response_from_raw(raw: Variant) -> Variant:
if not raw is Dictionary or not raw.has("status"):
return null
var status_raw = raw["status"]
@@ -767,6 +783,20 @@ static func decode_atlas_layer_response(bytes: PackedByteArray) -> Variant:
}
## Decode + classify one inbound frame (#960, D-225). Returns {kind, value} with
## kind "snapshot" | "atlas" | "unknown" — both are msgpack maps, so they are
## told apart by field (a response has "status"; a snapshot has "entities").
## Lets receive_bytes decode the frame ONCE and branch, instead of double-decoding
## the 20 Hz snapshot path.
static func decode_inbound(bytes: PackedByteArray) -> Dictionary:
var raw = decode_raw(bytes)
if not raw is Dictionary:
return {"kind": "unknown", "value": null}
if raw.has("status") and not raw.has("entities"):
return {"kind": "atlas", "value": atlas_response_from_raw(raw)}
return {"kind": "snapshot", "value": _decode_snapshot_from_raw(raw)}
## Encode a ConfirmBookmark action (#614, #680).
## Struct variant with bookmark_id and starting_location_id.
static func encode_confirm_bookmark(bookmark_id: String, starting_location_id: String) -> PackedByteArray: