feat(client): atlas layer-stream codec — encode request / decode response (#960, D-225)
Client half of the layer-stream protocol (codec only; transport wiring next).
- protocol.gd: encode_atlas_layer_request (bare {body_id, up_to} map so the
server demux routes it to the proxy, not the PlayerInput array) and
decode_atlas_layer_response (-> {body_id, status, error, layer1}; returns null
for non-atlas frames, e.g. a snapshot, so receive_bytes can disambiguate).
- gen_fixtures.rs: cross-language fixtures (atlas_response_ready/pending/
not_found) from real rmp_serde output, matching the test_protocol.gd pattern.
- test_protocol.gd: 5 tests decode the fixtures + verify a snapshot is not
mistaken for a response + the request encodes to the right shape. 68/68 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -726,6 +726,47 @@ static func encode_request_bookmark_catalog() -> PackedByteArray:
|
|||||||
return result.value
|
return result.value
|
||||||
|
|
||||||
|
|
||||||
|
## Encode an AtlasLayerRequest (#969, D-225) for the layer-stream proxy.
|
||||||
|
## A bare map {body_id, up_to} — NOT the Vec<PlayerInput> array — so the server's
|
||||||
|
## frame demux routes it to the atlas proxy. up_to is a CascadeLayer unit variant
|
||||||
|
## (bare string: "Heightmap" | "Topography").
|
||||||
|
static func encode_atlas_layer_request(body_id: String, up_to: String = "Topography") -> PackedByteArray:
|
||||||
|
var msg := {"body_id": body_id, "up_to": up_to}
|
||||||
|
var result = _mp().encode(msg)
|
||||||
|
if result.status != null:
|
||||||
|
push_error("Protocol: encode_atlas_layer_request failed: %s" % result.status)
|
||||||
|
return PackedByteArray()
|
||||||
|
return result.value
|
||||||
|
|
||||||
|
|
||||||
|
## Decode an AtlasLayerResponse (#969, D-225). Returns a Dictionary
|
||||||
|
## {body_id, status, error, layer1}, or null if the bytes are not an atlas
|
||||||
|
## response (no "status" key — e.g. an ObserverSnapshot). status is the variant
|
||||||
|
## 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
|
||||||
|
if not raw is Dictionary or not raw.has("status"):
|
||||||
|
return null
|
||||||
|
var status_raw = raw["status"]
|
||||||
|
var status := ""
|
||||||
|
var error := ""
|
||||||
|
if status_raw is String:
|
||||||
|
status = status_raw
|
||||||
|
elif status_raw is Dictionary and status_raw.has("Error"):
|
||||||
|
status = "Error"
|
||||||
|
error = str(status_raw["Error"])
|
||||||
|
return {
|
||||||
|
"body_id": raw.get("body_id", ""),
|
||||||
|
"status": status,
|
||||||
|
"error": error,
|
||||||
|
"layer1": raw.get("layer1"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
## Encode a ConfirmBookmark action (#614, #680).
|
## Encode a ConfirmBookmark action (#614, #680).
|
||||||
## Struct variant with bookmark_id and starting_location_id.
|
## Struct variant with bookmark_id and starting_location_id.
|
||||||
static func encode_confirm_bookmark(bookmark_id: String, starting_location_id: String) -> PackedByteArray:
|
static func encode_confirm_bookmark(bookmark_id: String, starting_location_id: String) -> PackedByteArray:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ƒ§body_id¥ghost¦status¨NotFound¦layer1À
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
�body_id二J1c存tatus判ending奸ayer1�
|
||||||
Binary file not shown.
@@ -463,3 +463,51 @@ func test_encode_confirm_bookmark_roundtrip() -> void:
|
|||||||
var data: Dictionary = entry["action_data"]
|
var data: Dictionary = entry["action_data"]
|
||||||
assert_that(data["bookmark_id"]).is_equal("bm_tycoon_arion")
|
assert_that(data["bookmark_id"]).is_equal("bm_tycoon_arion")
|
||||||
assert_that(data["starting_location_id"]).is_equal("loc_arion_prime")
|
assert_that(data["starting_location_id"]).is_equal("loc_arion_prime")
|
||||||
|
|
||||||
|
|
||||||
|
# -- Atlas layer-stream protocol (#969, D-225) ---------------------------------
|
||||||
|
|
||||||
|
func test_decode_atlas_response_ready() -> void:
|
||||||
|
var bytes := _load_fixture("atlas_response_ready")
|
||||||
|
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||||
|
assert_that(resp).is_not_null()
|
||||||
|
assert_that(resp.body_id).is_equal("GJ1c")
|
||||||
|
assert_that(resp.status).is_equal("Ready")
|
||||||
|
assert_that(resp.layer1).is_not_null()
|
||||||
|
assert_that(resp.layer1.river_network.river_cells.size()).is_equal(2)
|
||||||
|
assert_that(resp.layer1.attractors.size()).is_equal(1)
|
||||||
|
assert_that(resp.layer1.attractors[0].attractor_type).is_equal("CoastalAccess")
|
||||||
|
assert_that(resp.layer1.attractors[0].sub_biome).is_equal("CoastalLowland")
|
||||||
|
|
||||||
|
|
||||||
|
func test_decode_atlas_response_pending() -> void:
|
||||||
|
var bytes := _load_fixture("atlas_response_pending")
|
||||||
|
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||||
|
assert_that(resp).is_not_null()
|
||||||
|
assert_that(resp.status).is_equal("Pending")
|
||||||
|
assert_that(resp.layer1).is_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_decode_atlas_response_not_found() -> void:
|
||||||
|
var bytes := _load_fixture("atlas_response_not_found")
|
||||||
|
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||||
|
assert_that(resp.status).is_equal("NotFound")
|
||||||
|
|
||||||
|
|
||||||
|
func test_snapshot_is_not_decoded_as_atlas_response() -> void:
|
||||||
|
# Disambiguation: an ObserverSnapshot has no "status" key, so the atlas
|
||||||
|
# decoder rejects it. receive_bytes relies on this to route correctly.
|
||||||
|
var bytes := _load_fixture("snapshot_empty")
|
||||||
|
assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null()
|
||||||
|
|
||||||
|
|
||||||
|
func test_encode_atlas_request_shape() -> void:
|
||||||
|
var bytes := Protocol.encode_atlas_layer_request("GJ1c", "Topography")
|
||||||
|
assert_that(bytes.size()).is_greater(0)
|
||||||
|
var raw: Variant = Messagepack.decode(bytes)
|
||||||
|
assert_that(raw.status).is_null()
|
||||||
|
assert_that(raw.value is Dictionary).is_true()
|
||||||
|
assert_that(raw.value["body_id"]).is_equal("GJ1c")
|
||||||
|
assert_that(raw.value["up_to"]).is_equal("Topography")
|
||||||
|
# A request is a map with no "status" — must not be mistaken for a response.
|
||||||
|
assert_that(Protocol.decode_atlas_layer_response(bytes)).is_null()
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
//! Generate MessagePack fixture files for cross-language testing (D-030 Layer 1).
|
//! Generate MessagePack fixture files for cross-language testing (D-030 Layer 1).
|
||||||
//! Run with: cargo test --test gen_fixtures -- --ignored
|
//! Run with: cargo test --test gen_fixtures -- --ignored
|
||||||
|
|
||||||
|
use settled_reach_server::atlas::body_world_state::{DrainageBasin, RiverNetwork};
|
||||||
|
use settled_reach_server::atlas::layer1::Layer1Output;
|
||||||
|
use settled_reach_server::atlas::layer_proxy::{AtlasLayerResponse, AtlasLayerStatus};
|
||||||
use settled_reach_server::bridge::types::*;
|
use settled_reach_server::bridge::types::*;
|
||||||
|
use settled_reach_server::simulation::generator::{
|
||||||
|
AttractorType, GeographicAttractor, SubBiomeVariant,
|
||||||
|
};
|
||||||
use settled_reach_server::simulation::poi::PoiCategory;
|
use settled_reach_server::simulation::poi::PoiCategory;
|
||||||
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
use settled_reach_server::simulation::time::{DayPhase, TickRate};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -529,3 +535,60 @@ fn generate_snapshot_with_bookmark_catalog() {
|
|||||||
rmp_serde::to_vec_named(&snapshot).expect("serialize snapshot_with_bookmark_catalog");
|
rmp_serde::to_vec_named(&snapshot).expect("serialize snapshot_with_bookmark_catalog");
|
||||||
write_fixture("snapshot_with_bookmark_catalog", &bytes);
|
write_fixture("snapshot_with_bookmark_catalog", &bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Atlas layer-stream responses (#969, D-225) — the client decodes these to
|
||||||
|
/// render the per-layer Atlas overlays (#960). Covers Ready (with a small
|
||||||
|
/// Layer1Output), Pending, and NotFound.
|
||||||
|
#[test]
|
||||||
|
#[ignore] // Run manually: cargo test --test gen_fixtures -- --ignored
|
||||||
|
fn generate_atlas_layer_response_fixtures() {
|
||||||
|
let layer1 = Layer1Output {
|
||||||
|
body_id: "GJ1c".into(),
|
||||||
|
river_network: RiverNetwork {
|
||||||
|
river_cells: vec![(12, 58), (12, 59)],
|
||||||
|
confluences: vec![],
|
||||||
|
mouths: vec![(12, 58)],
|
||||||
|
},
|
||||||
|
drainage_basins: vec![DrainageBasin {
|
||||||
|
basin_id: 1,
|
||||||
|
boundary: vec![(0, 0), (0, 10), (10, 10), (10, 0)],
|
||||||
|
area_pct: 0.42,
|
||||||
|
}],
|
||||||
|
attractors: vec![GeographicAttractor {
|
||||||
|
position: (12, 58),
|
||||||
|
attractor_type: AttractorType::CoastalAccess,
|
||||||
|
strength: 0.9,
|
||||||
|
sub_biome: SubBiomeVariant::CoastalLowland,
|
||||||
|
terrain_modification_cost: 1.7,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
let ready = AtlasLayerResponse {
|
||||||
|
body_id: "GJ1c".into(),
|
||||||
|
status: AtlasLayerStatus::Ready,
|
||||||
|
layer1: Some(layer1),
|
||||||
|
};
|
||||||
|
write_fixture(
|
||||||
|
"atlas_response_ready",
|
||||||
|
&rmp_serde::to_vec_named(&ready).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let pending = AtlasLayerResponse {
|
||||||
|
body_id: "GJ1c".into(),
|
||||||
|
status: AtlasLayerStatus::Pending,
|
||||||
|
layer1: None,
|
||||||
|
};
|
||||||
|
write_fixture(
|
||||||
|
"atlas_response_pending",
|
||||||
|
&rmp_serde::to_vec_named(&pending).unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let not_found = AtlasLayerResponse {
|
||||||
|
body_id: "ghost".into(),
|
||||||
|
status: AtlasLayerStatus::NotFound,
|
||||||
|
layer1: None,
|
||||||
|
};
|
||||||
|
write_fixture(
|
||||||
|
"atlas_response_not_found",
|
||||||
|
&rmp_serde::to_vec_named(¬_found).unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user