`relief_q` is the one field with signal below District — elev_q's 80 m steps
quantise sub-district detail away, which is precisely why relief_q was invented.
The server has encoded it since 5eb394b36 and the terrain layer has asked for it
by name ever since. step_canvas_protocol.gd's decode dictionary never listed the
key, so `canvas.get("relief_q")` was always null and the plane arrived nowhere.
The server half of that change landed; the protocol half did not.
That is the whole reason Region and below rendered as a flat wash. Measured plane
variety at District before the fix:
{morphology: 1, elev_q: 11, relief_q: 0, moisture_q: 25, vegetation: 3}
A 0 there means ABSENT, not constant — a distinction the capture could not make
until this commit adds it, and the reason two earlier sessions read the flatness
as a missing generator rather than a missing key.
Also spends the field properly. It drove a stipple PROBABILITY only, so a ridge
and a plain differed in dot density, which at one pixel per cell reads as noise;
and `_ruggedness()` took absf(relief_q - 50), discarding the sign the server
deliberately preserved ("a hollow and a rise are different ground... the reverse
is not recoverable"). Relief now shades continuously and signed — rises lighten,
hollows darken — UNDER the stipple rather than instead of it. Ruggedness
(unsigned) and elevation (signed) are different questions and both are worth
asking.
Ladder, before -> after (tooling/atlas-flatness, lum p1-p99):
Global 145.69 -> 145.69 unchanged, correct: relief_q is flat 50 at
orbital rungs by construction
Region 33.59 -> 71.01 2.1x
District 13.72 -> 77.01 5.6x
Quarter 11.01 -> 42.56 3.9x
Structure retention Global->Quarter: 7.6% -> 29%.
NOT finished, and the ticket says so: Region now reads as heavy speckle, because
ruggedness is real data instead of an elev_q-gradient fallback and far more cells
earn a mark than the T-1194 tuning assumed; District reads as soft blobby relief,
form without directionality. Both are grammar/tuning follow-ups on a channel that
finally carries signal.
0.4.9 is a REQUIRED bump. The disk cache stores the DECODED canvas, so every
earlier entry physically lacks the field and would keep rendering flat against a
build that reads it — the first bump in this series where a warm cache is wrong
about CONTENT, not merely stale. tooling/canvas_sources.py gains
step_canvas_protocol.gd for the same reason: it decides which planes exist, the
cache stores its output, and the T-1242 gate would not have flagged this fix
while the registry stopped at ui/.../step_canvas/.
Regression cover: every protocol test passed throughout the weeks the plane was
missing, because each asserted a field it already knew about and none asserted
the SET. There is now a test walking all eight dense planes of EncodedStepCanvas,
verified by disabling the fix and watching it fail by name.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
296 lines
11 KiB
GDScript
296 lines
11 KiB
GDScript
## T-1182 tests: StepCanvasRequest/StepCanvasResponse wire codec
|
||
## (step_canvas_protocol.gd, delegated via protocol.gd) — the D-255(c)
|
||
## tagged-envelope carrier. Mirrors test_atlas_data_delivery.gd's own
|
||
## encode/decode round-trip + decode_inbound classification conventions.
|
||
class_name TestStepCanvasProtocol
|
||
extends GdUnitTestSuite
|
||
|
||
|
||
# =============================================================================
|
||
# Encode
|
||
# =============================================================================
|
||
|
||
|
||
func test_encode_step_canvas_request_carries_discriminator_and_fields() -> void:
|
||
var bytes := Protocol.encode_step_canvas_request(
|
||
"GJ380c", "District", Vector2i(10, 20), Vector2i(64, 64), 512
|
||
)
|
||
assert_int(bytes.size()).is_greater(0)
|
||
var decoded = Messagepack.decode(bytes)
|
||
assert_that(decoded.status == null).is_true()
|
||
var raw: Dictionary = decoded.value
|
||
assert_bool(raw.get("step_canvas")).is_true()
|
||
assert_str(raw.get("body_id")).is_equal("GJ380c")
|
||
assert_str(raw.get("rung")).is_equal("District")
|
||
assert_that(raw.get("center")).is_equal([10, 20])
|
||
assert_that(raw.get("extent")).is_equal([64, 64])
|
||
assert_int(raw.get("min_wl_m")).is_equal(512)
|
||
|
||
|
||
## The rung is sent as a bare string (rmp_serde's unit-variant convention),
|
||
## never a raw integer — a Global request must carry the literal tag
|
||
## "Global", matching step_canvas.rs's own StepCanvasRung enum encoding.
|
||
func test_encode_step_canvas_request_global_rung_is_bare_string() -> void:
|
||
var bytes := Protocol.encode_step_canvas_request(
|
||
"GJ380c", "Global", Vector2i.ZERO, Vector2i.ZERO, 0
|
||
)
|
||
var decoded = Messagepack.decode(bytes)
|
||
assert_str(decoded.value.get("rung")).is_equal("Global")
|
||
|
||
|
||
# =============================================================================
|
||
# Decode — status variants
|
||
# =============================================================================
|
||
|
||
|
||
func test_step_canvas_response_from_raw_decodes_ready_status() -> void:
|
||
var raw := {
|
||
"body_id": "GJ380c",
|
||
"rung": "District",
|
||
"center": [10, 20],
|
||
"extent": [64, 64],
|
||
"min_wl_m": 0,
|
||
"status": "Ready",
|
||
"canvas": null,
|
||
}
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
assert_str(decoded["status"]).is_equal("Ready")
|
||
assert_str(decoded["error"]).is_equal("")
|
||
assert_str(decoded["rung"]).is_equal("District")
|
||
assert_that(decoded["center"]).is_equal(Vector2i(10, 20))
|
||
assert_that(decoded["extent"]).is_equal(Vector2i(64, 64))
|
||
|
||
|
||
func test_step_canvas_response_from_raw_decodes_pending_status() -> void:
|
||
var raw := {"body_id": "GJ380c", "rung": "Chunk", "status": "Pending"}
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
assert_str(decoded["status"]).is_equal("Pending")
|
||
assert_that(decoded["canvas"]).is_null()
|
||
|
||
|
||
func test_step_canvas_response_from_raw_decodes_error_status() -> void:
|
||
var raw := {
|
||
"body_id": "GJ380c", "rung": "Region", "status": {"Error": "no heightmap"}
|
||
}
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
assert_str(decoded["status"]).is_equal("Error")
|
||
assert_str(decoded["error"]).is_equal("no heightmap")
|
||
|
||
|
||
## Not a step-canvas response (no "rung" key) -> null, so decode_inbound's
|
||
## dispatch doesn't misroute a plain AtlasLayerResponse here.
|
||
func test_step_canvas_response_from_raw_returns_null_without_rung_key() -> void:
|
||
var raw := {"body_id": "GJ380c", "status": "Ready"}
|
||
assert_that(Protocol.step_canvas_response_from_raw(raw)).is_null()
|
||
|
||
|
||
# =============================================================================
|
||
# decode_inbound classification — the "rung" discriminator must win BEFORE
|
||
# the generic "status"-only AtlasLayerResponse fallback (a step-canvas
|
||
# response also carries "status").
|
||
# =============================================================================
|
||
|
||
|
||
func test_decode_inbound_classifies_step_canvas() -> void:
|
||
var raw := {"body_id": "GJ380c", "rung": "District", "status": "Pending"}
|
||
var encoded = Messagepack.encode(raw)
|
||
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
|
||
assert_str(inbound["kind"]).is_equal("step_canvas")
|
||
|
||
|
||
func test_decode_inbound_still_classifies_plain_atlas_response() -> void:
|
||
var raw := {"body_id": "GJ380c", "status": "Ready"}
|
||
var encoded = Messagepack.encode(raw)
|
||
var inbound: Dictionary = Protocol.decode_inbound(encoded.value)
|
||
assert_str(inbound["kind"]).is_equal("atlas")
|
||
|
||
|
||
# =============================================================================
|
||
# EncodedStepCanvas — the PNG-per-field array-of-int wire shape
|
||
# (step_canvas.rs's png_bytes: Vec<u8> with NO serde_bytes anywhere in this
|
||
# codebase serializes via serialize_seq, a msgpack ARRAY of ints, never a
|
||
# `bin` blob — decode_png_field()/_decode_encoded_canvas() must repack that
|
||
# Array into a PackedByteArray, not expect messagepack.gd's bin_8/16/32 path).
|
||
# =============================================================================
|
||
|
||
|
||
func test_decode_png_field_repacks_array_of_ints_to_packed_byte_array() -> void:
|
||
# A 1x1 all-black L8 PNG's real byte stream, as it would arrive already
|
||
# decoded off the wire (a plain Array of ints, one per byte) — using real
|
||
# PNG magic bytes so a downstream Image.load_png_from_buffer() call
|
||
# would also succeed, not just this repack step in isolation.
|
||
var png_bytes := PackedByteArray([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
|
||
var as_array: Array = []
|
||
for b in png_bytes:
|
||
as_array.append(b)
|
||
var field_raw := {"png_bytes": as_array}
|
||
var result: PackedByteArray = Protocol.step_canvas_response_from_raw(
|
||
{
|
||
"body_id": "GJ380c",
|
||
"rung": "Chunk",
|
||
"status": "Ready",
|
||
"canvas":
|
||
{
|
||
"width": 1,
|
||
"height": 1,
|
||
"morphology": field_raw,
|
||
"elev_q": {},
|
||
"temp_dc": {"values": []},
|
||
"moisture_q": {},
|
||
"vegetation": {},
|
||
"settlement_id": {"values": []},
|
||
"glaciation": {},
|
||
"flooded_q": {},
|
||
"courses": [],
|
||
"cliffs": [],
|
||
},
|
||
}
|
||
)["canvas"]["morphology"]
|
||
assert_that(result).is_equal(png_bytes)
|
||
|
||
|
||
func test_decode_encoded_canvas_passes_through_temp_dc_and_settlement_id_as_arrays() -> void:
|
||
var raw := {
|
||
"body_id": "GJ380c",
|
||
"rung": "Chunk",
|
||
"status": "Ready",
|
||
"canvas":
|
||
{
|
||
"width": 2,
|
||
"height": 1,
|
||
"morphology": {},
|
||
"elev_q": {},
|
||
"temp_dc": {"values": [120, -32768]},
|
||
"moisture_q": {},
|
||
"vegetation": {},
|
||
"settlement_id": {"values": [0, 7]},
|
||
"glaciation": {},
|
||
"flooded_q": {},
|
||
"courses": [],
|
||
"cliffs": [],
|
||
},
|
||
}
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
var canvas: Dictionary = decoded["canvas"]
|
||
assert_that(canvas["temp_dc"]).is_equal([120, -32768])
|
||
assert_that(canvas["settlement_id"]).is_equal([0, 7])
|
||
|
||
|
||
## T-1213 REGRESSION. `relief_q` shipped server-side, the terrain layer asked for
|
||
## it by name, and this decode never listed the key — so the plane the deep rungs
|
||
## exist to draw arrived nowhere for weeks, and District rendered as a flat wash.
|
||
## Every test here passed throughout, because each asserted a field it already
|
||
## knew about; none asserted the SET.
|
||
##
|
||
## So this one is written against the wire contract rather than against a list of
|
||
## fields someone remembered: every dense plane the server encodes must survive
|
||
## the decode. Adding a plane to EncodedStepCanvas without adding it here now
|
||
## fails loudly instead of rendering as "that rung is just flat".
|
||
func test_decode_encoded_canvas_carries_every_dense_plane() -> void:
|
||
# The eight PNG-per-field planes of EncodedStepCanvas (step_canvas.rs).
|
||
var planes := [
|
||
"morphology",
|
||
"elev_q",
|
||
"moisture_q",
|
||
"vegetation",
|
||
"lake_margin_q",
|
||
"relief_q",
|
||
"glaciation",
|
||
"flooded_q",
|
||
]
|
||
var canvas_in := {"width": 1, "height": 1, "temp_dc": {"values": [0]}}
|
||
for plane: String in planes:
|
||
# A one-pixel L8 PNG payload in the array-of-ints shape the wire uses.
|
||
canvas_in[plane] = {"png_bytes": [137, 80, 78, 71]}
|
||
var raw := {
|
||
"body_id": "GJ380c",
|
||
"rung": "District",
|
||
"status": "Ready",
|
||
"canvas": canvas_in,
|
||
}
|
||
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
var canvas: Dictionary = decoded["canvas"]
|
||
for plane: String in planes:
|
||
assert_bool(canvas.has(plane)).override_failure_message(
|
||
(
|
||
"decoded canvas is missing the '%s' plane — the server encodes it and "
|
||
+ "the renderer reads it, so a missing key here renders as a flat rung "
|
||
+ "rather than as an error (T-1213)"
|
||
)
|
||
% plane
|
||
).is_true()
|
||
assert_that(canvas[plane]).override_failure_message(
|
||
"plane '%s' decoded to null rather than bytes" % plane
|
||
).is_not_null()
|
||
|
||
|
||
func test_decode_encoded_canvas_passes_through_courses_and_cliffs_unshaped() -> void:
|
||
var courses := [{"edge_id": 1, "class": 2, "points": [[0, 0], [100, 100]], "terminus": "Mouth"}]
|
||
var cliffs := [{"point": [5, 5], "channel_depth_dm": 10, "cliff_edge": true}]
|
||
var raw := {
|
||
"body_id": "GJ380c",
|
||
"rung": "Chunk",
|
||
"status": "Ready",
|
||
"canvas":
|
||
{
|
||
"width": 1,
|
||
"height": 1,
|
||
"morphology": {},
|
||
"elev_q": {},
|
||
"temp_dc": {"values": []},
|
||
"moisture_q": {},
|
||
"vegetation": {},
|
||
"settlement_id": {"values": []},
|
||
"glaciation": {},
|
||
"flooded_q": {},
|
||
"courses": courses,
|
||
"cliffs": cliffs,
|
||
},
|
||
}
|
||
var decoded = Protocol.step_canvas_response_from_raw(raw)
|
||
var canvas: Dictionary = decoded["canvas"]
|
||
assert_that(canvas["courses"]).is_equal(courses)
|
||
assert_that(canvas["cliffs"]).is_equal(cliffs)
|
||
|
||
|
||
func test_decode_png_field_malformed_input_returns_empty_packed_byte_array() -> void:
|
||
assert_that(Protocol._scp().decode_png_field(null)).is_equal(PackedByteArray())
|
||
assert_that(Protocol._scp().decode_png_field({"png_bytes": "not an array"})).is_equal(
|
||
PackedByteArray()
|
||
)
|
||
|
||
|
||
## PR #203 review (Hoshe finding 1) — the genuine msgpack `bin_8`/`bin_16`/
|
||
## `bin_32` decode path. messagepack.gd's own decoder returns
|
||
## `StreamPeerBuffer.get_partial_data()`'s `[Error, PackedByteArray]` pair for
|
||
## a bin-typed field — which passes decode_png_field()'s `is Array` guard
|
||
## just as readily as the real (today's) array-of-ints shape, so this must be
|
||
## exercised with the REAL bin-shaped decode output, not a hand-built
|
||
## Dictionary, to prove the fix actually detects it. Built by round-tripping
|
||
## a genuine PackedByteArray value through Messagepack.encode()/decode()
|
||
## directly (bypassing step_canvas_protocol.gd's own encoder, which never
|
||
## sends a PackedByteArray for png_bytes today) — this is exactly the shape
|
||
## a future serde_bytes-annotated server would produce.
|
||
func test_decode_png_field_handles_the_genuine_bin_type_shape() -> void:
|
||
var real_png_bytes := PackedByteArray([0x89, 0x50, 0x4e, 0x47, 1, 2, 3, 4])
|
||
var encoded = Messagepack.encode(real_png_bytes)
|
||
assert_that(encoded.status).is_null()
|
||
var decoded = Messagepack.decode(encoded.value)
|
||
assert_that(decoded.status).is_null()
|
||
# Sanity: this really is the [error, data] pair shape, not the plain-array
|
||
# shape the rest of this suite exercises — if messagepack.gd's own
|
||
# behavior ever changes, this assertion fails loudly instead of the test
|
||
# silently exercising the wrong code path.
|
||
var bin_shape: Variant = decoded.value
|
||
assert_bool(bin_shape is Array).is_true()
|
||
assert_int((bin_shape as Array).size()).is_equal(2)
|
||
assert_bool((bin_shape as Array)[0] is int).is_true()
|
||
assert_bool((bin_shape as Array)[1] is PackedByteArray).is_true()
|
||
|
||
var result: PackedByteArray = Protocol._scp().decode_png_field({"png_bytes": bin_shape})
|
||
assert_that(result).override_failure_message(
|
||
"bin-shaped png_bytes must decode to the REAL inner bytes, not silently"
|
||
+ " collapse to [0, 0] via PackedByteArray([int, PackedByteArray]) coercion"
|
||
).is_equal(real_png_bytes)
|