feat(simulation): feature-name pipeline wired + legacy window_granularity u32 retired (T-1169, T-1159)
One commit for two tickets whose changes share the bridge/plugin plumbing files. T-1169 connects the three dormant feature-name pieces: atlas_feature_names populated at regen (17,891 rows — 15,190 mountain, 2,701 river — via populate_atlas_feature_names mirroring the city-names importer; systems.db regenerated, stamp fresh), attach_feature_names wired into the cascade's Topography block with name pools threaded DB-free through AnalyzeBody (D-225 pattern) and assignments stored on Layer1Output/BodyWorldState for future consumers, and a FeatureNamesRequest/Response read proxy as the bridge's 7th tagged envelope (D-236 pattern, both SimBridge impls). Client label DRAW is deliberately NOT here — implementation proved both river and mountain labels need a wire-carried position (the pool is position-free; course polylines aren't correlated with the named attractors by construction) — deferred to T-1195's single design pass. cascade_layer1 golden re-pinned (additive feature_names field). T-1159 retires the legacy u32 granularity field fully shadowed by window_granularity_v2: AtlasLayerRequest.window_granularity, DistrictWindowLayer.granularity echo, the u32::MAX sentinel, and resolve_window_granularity are gone server-side; client encode paths and the caller-less atlas_window_cache legacy key component dropped; msgpack fixtures regenerated; the T-1150 aliasing regression test now drives through the surviving enum field. The district_window carrier itself survives byte-compatible per D-255(c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -434,21 +434,22 @@ func send_named_action(action_name: String, action_data: Variant = null) -> void
|
||||
## whole-body-layer caller (show_body()'s existing request), so their wire
|
||||
## traffic is byte-unchanged.
|
||||
##
|
||||
## window_granularity/window_min_wl_m (T-1150): struct/key plumbing for the
|
||||
## zoom-ladder quarter rung — district (0/omitted) stays the default for
|
||||
## every caller in this codebase today.
|
||||
## window_min_wl_m (T-1150): struct/key plumbing for the octave cutoff — 0
|
||||
## (omitted) stays the default for every caller in this codebase today.
|
||||
##
|
||||
## window_granularity_v2 (T-1152/T-1153): the R5-redesigned string-tag
|
||||
## granularity ("Quarter"/"District"/"Region") — the ONLY way to request the
|
||||
## coarser-than-district Region rung the legacy u32 field cannot express.
|
||||
## Empty string (omitted) is the default for every caller that doesn't pass
|
||||
## it, byte-compatible with every pre-T-1152 request.
|
||||
## window_granularity_v2 (T-1152/T-1153): the string-tag granularity
|
||||
## ("Quarter"/"District"/"Region") — the way to request the coarser-than-
|
||||
## district Region rung. Empty string (omitted) is the default for every
|
||||
## caller that doesn't pass it, resolving to District server-side.
|
||||
##
|
||||
## T-1159: the legacy `window_granularity: int` parameter this function used
|
||||
## to also forward is retired — see atlas_map_protocol.gd's
|
||||
## encode_atlas_layer_request doc for the full rationale.
|
||||
func request_atlas_layers(
|
||||
body_id: String,
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0,
|
||||
window_granularity_v2: String = ""
|
||||
) -> void:
|
||||
@@ -459,7 +460,6 @@ func request_atlas_layers(
|
||||
up_to,
|
||||
window_center,
|
||||
window_n,
|
||||
window_granularity,
|
||||
window_min_wl_m,
|
||||
window_granularity_v2
|
||||
)
|
||||
|
||||
@@ -32,29 +32,27 @@ class_name AtlasMapProtocol
|
||||
## client-side default/cap constants (DISTRICT_WINDOW_DEFAULT_N/MAX_N) live on
|
||||
## the regional-window viewer, not duplicated into the codec.
|
||||
##
|
||||
## `window_granularity`/`window_min_wl_m` (T-1150): the derivation-granularity
|
||||
## axis (district=1/omitted vs. quarter=4) and the octave cutoff, in whole
|
||||
## metres. Both OMITTED (not sent as 0) when at their default — this is
|
||||
## struct/key plumbing only (T-1150 scope): no caller in this codebase
|
||||
## requests quarter granularity yet (that's T-1153); this function just makes
|
||||
## it possible to ask, byte-compatible with every existing caller that
|
||||
## doesn't pass them.
|
||||
## `window_min_wl_m` (T-1150): the octave cutoff, in whole metres. OMITTED
|
||||
## (not sent as 0) when at its default — struct/key plumbing, byte-compatible
|
||||
## with every existing caller that doesn't pass it.
|
||||
##
|
||||
## `window_granularity_v2` (T-1152, R5 redesign — see
|
||||
## server/src/atlas/layer_proxy.rs's `WindowGranularity` doc): the ONLY way to
|
||||
## express a coarser-than-district rung (`"Region"`) the legacy `u32` field
|
||||
## cannot encode. A plain STRING variant tag ("Quarter" | "District" |
|
||||
## server/src/atlas/layer_proxy.rs's `WindowGranularity` doc): the derivation
|
||||
## granularity, a plain STRING variant tag ("Quarter" | "District" |
|
||||
## "Region"), matching `RoadNodeKind`'s existing wire precedent on this same
|
||||
## carrier (a bare `#[derive(Serialize, Deserialize)]` enum with no
|
||||
## `#[serde(rename_all)]` — rmp_serde encodes the Rust variant NAME verbatim,
|
||||
## not an integer discriminant). OMITTED (not sent as "") when
|
||||
## `window_granularity_v2` is the empty string — `#[serde(default)]` on the
|
||||
## Rust side decodes absence as `None`, falling back to the legacy `u32`
|
||||
## field's `resolve_window_granularity_v2()` precedence rule (that field wins
|
||||
## over the legacy one whenever present — see that Rust doc for the full
|
||||
## precedence contract). Every pre-T-1152 caller (and every T-1150 caller that
|
||||
## only ever sends `window_granularity`) omits this field entirely and stays
|
||||
## byte-compatible.
|
||||
## Rust side decodes absence as `None`, resolving to District (the default
|
||||
## rung).
|
||||
##
|
||||
## **T-1159:** the legacy `window_granularity: int` field this codec used to
|
||||
## also encode (T-1150's finer-than-district-only `u32` wire encoding) is
|
||||
## retired — `window_granularity_v2` fully shadowed it since T-1152, and no
|
||||
## caller in this codebase (nor any external client — single-repo
|
||||
## client/server pair) ever sent it as anything but the byte-compatible
|
||||
## default.
|
||||
##
|
||||
## **Quantization split (PR #191 review, Hoshe 1 / Tyre C3):** `window_min_wl_m`
|
||||
## is sent HERE as a raw, unquantized value — this codec does NOT snap it to
|
||||
@@ -71,7 +69,6 @@ static func encode_atlas_layer_request(
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0,
|
||||
window_granularity_v2: String = ""
|
||||
) -> PackedByteArray:
|
||||
@@ -80,8 +77,6 @@ static func encode_atlas_layer_request(
|
||||
var center: Vector2i = window_center
|
||||
msg["window_center"] = [center.x, center.y]
|
||||
msg["window_n"] = window_n
|
||||
if window_granularity != 0:
|
||||
msg["window_granularity"] = window_granularity
|
||||
if window_min_wl_m != 0:
|
||||
msg["window_min_wl_m"] = window_min_wl_m
|
||||
if not window_granularity_v2.is_empty():
|
||||
|
||||
@@ -786,17 +786,18 @@ static func encode_request_bookmark_catalog() -> PackedByteArray:
|
||||
## windowed district-resolution regional-map query — see
|
||||
## atlas_map_protocol.gd's encode_atlas_layer_request doc for the wire shape.
|
||||
## Omitted callers (every whole-body-layer call site predating T-1138) are
|
||||
## byte-unchanged. window_granularity/window_min_wl_m (T-1150): same
|
||||
## byte-compatibility contract, see atlas_map_protocol.gd.
|
||||
## window_granularity_v2 (T-1152): the R5-redesigned string-tag granularity
|
||||
## ("Quarter"/"District"/"Region") — the only way to request the Region rung.
|
||||
## Omitted (empty string) by every caller that doesn't pass it.
|
||||
## byte-unchanged. window_min_wl_m (T-1150): same byte-compatibility
|
||||
## contract, see atlas_map_protocol.gd.
|
||||
## window_granularity_v2 (T-1152): the string-tag granularity
|
||||
## ("Quarter"/"District"/"Region"). Omitted (empty string) by every caller
|
||||
## that doesn't pass it, resolving to District server-side. T-1159: the
|
||||
## legacy `window_granularity: int` parameter this used to also forward is
|
||||
## retired — see atlas_map_protocol.gd's encode_atlas_layer_request doc.
|
||||
static func encode_atlas_layer_request(
|
||||
body_id: String,
|
||||
up_to: String = "Topography",
|
||||
window_center: Variant = null,
|
||||
window_n: int = 0,
|
||||
window_granularity: int = 0,
|
||||
window_min_wl_m: int = 0,
|
||||
window_granularity_v2: String = ""
|
||||
) -> PackedByteArray:
|
||||
@@ -806,7 +807,6 @@ static func encode_atlas_layer_request(
|
||||
up_to,
|
||||
window_center,
|
||||
window_n,
|
||||
window_granularity,
|
||||
window_min_wl_m,
|
||||
window_granularity_v2
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -214,35 +214,32 @@ func test_encode_atlas_layer_request_carries_window_params() -> void:
|
||||
assert_that(decoded.value.get("window_n")).is_equal(32)
|
||||
|
||||
|
||||
## T-1150: window_granularity/window_min_wl_m are OMITTED (not sent as 0)
|
||||
## when at their default — a windowed request that doesn't pass them (every
|
||||
## pre-T-1150 window caller) is byte-identical to pre-T-1150 wire traffic,
|
||||
## same contract as window_center/window_n's own default-omission above.
|
||||
func test_encode_atlas_layer_request_omits_granularity_and_min_wl_by_default() -> void:
|
||||
## T-1150: window_min_wl_m is OMITTED (not sent as 0) when at its default —
|
||||
## a windowed request that doesn't pass it (every pre-T-1150 window caller)
|
||||
## is byte-identical to pre-T-1150 wire traffic, same contract as
|
||||
## window_center/window_n's own default-omission above.
|
||||
func test_encode_atlas_layer_request_omits_min_wl_by_default() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32
|
||||
)
|
||||
var decoded = Messagepack.decode(bytes)
|
||||
assert_bool(decoded.value.has("window_granularity")).is_false()
|
||||
assert_bool(decoded.value.has("window_min_wl_m")).is_false()
|
||||
|
||||
|
||||
## T-1150: a quarter-granularity request with an octave cutoff carries both
|
||||
## new fields verbatim, unclamped (the server owns
|
||||
## resolve_window_granularity()/clamp_window_n() — never trusted from the
|
||||
## wire, same posture as window_n).
|
||||
func test_encode_atlas_layer_request_carries_granularity_and_min_wl() -> void:
|
||||
## T-1150: a request with an octave cutoff carries it verbatim, unclamped
|
||||
## (the server owns quantize_min_wl_m() — never trusted from the wire, same
|
||||
## posture as window_n).
|
||||
func test_encode_atlas_layer_request_carries_min_wl() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32, 4, 512
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32, 512
|
||||
)
|
||||
var decoded = Messagepack.decode(bytes)
|
||||
assert_that(decoded.value.get("window_granularity")).is_equal(4)
|
||||
assert_that(decoded.value.get("window_min_wl_m")).is_equal(512)
|
||||
|
||||
|
||||
## T-1152/T-1153: window_granularity_v2 is OMITTED (not sent as "") when at
|
||||
## its empty-string default — same byte-compatibility contract as
|
||||
## window_granularity/window_min_wl_m's own default-omission.
|
||||
## window_min_wl_m's own default-omission.
|
||||
func test_encode_atlas_layer_request_omits_granularity_v2_by_default() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(140, 260), 32
|
||||
@@ -252,22 +249,20 @@ func test_encode_atlas_layer_request_omits_granularity_v2_by_default() -> void:
|
||||
|
||||
|
||||
## T-1152/T-1153: a Region-rung request carries window_granularity_v2 as the
|
||||
## bare string "Region" — the ONLY way to express the coarser-than-district
|
||||
## rung (WindowGranularity's Rust doc: "the ONLY way to actually request
|
||||
## Region is window_granularity_v2 = Some(WindowGranularity::Region)"), a
|
||||
## plain rmp_serde variant-name encoding matching RoadNodeKind's existing
|
||||
## wire precedent, NOT an integer discriminant.
|
||||
## bare string "Region" — the way to express the coarser-than-district rung
|
||||
## (WindowGranularity's Rust doc), a plain rmp_serde variant-name encoding
|
||||
## matching RoadNodeKind's existing wire precedent, NOT an integer
|
||||
## discriminant.
|
||||
##
|
||||
## T-1159: the legacy `window_granularity: int` param this call used to also
|
||||
## pass positionally (between window_n and window_min_wl_m) is retired — see
|
||||
## atlas_map_protocol.gd's encode_atlas_layer_request doc.
|
||||
func test_encode_atlas_layer_request_carries_granularity_v2_region() -> void:
|
||||
var bytes := Protocol.encode_atlas_layer_request(
|
||||
"GJ1c", "Topography", Vector2i(0, 0), 6400, 0, 0, "Region"
|
||||
"GJ1c", "Topography", Vector2i(0, 0), 6400, 0, "Region"
|
||||
)
|
||||
var decoded = Messagepack.decode(bytes)
|
||||
assert_that(decoded.value.get("window_granularity_v2")).is_equal("Region")
|
||||
# The legacy window_granularity field is independently omittable — a
|
||||
# Region request sends ONLY the v2 tag, never a legacy value pretending
|
||||
# to mean something for Region (WINDOW_GRANULARITY_REGION_KEY is a
|
||||
# key-space tag the SERVER echoes, never a legal wire INPUT).
|
||||
assert_bool(decoded.value.has("window_granularity")).is_false()
|
||||
|
||||
|
||||
## §2: district_window is a distinct payload (echoes center/n for the
|
||||
@@ -280,7 +275,7 @@ func test_atlas_response_district_window_passthrough() -> void:
|
||||
var window := {
|
||||
"center": [140, 260],
|
||||
"n": 32,
|
||||
"granularity": 4, # T-1150: quarter granularity, passed through same as every other field
|
||||
"granularity_v2": "Quarter",
|
||||
"min_wl_m": 512,
|
||||
"morphology": PackedByteArray([8, 14, 0, 5]),
|
||||
"elev_q": PackedByteArray([40, 62, 5, 88]),
|
||||
@@ -307,7 +302,6 @@ func test_atlas_response_district_window_passthrough_region_rung() -> void:
|
||||
var window := {
|
||||
"center": [0, 0],
|
||||
"n": 6400,
|
||||
"granularity": 4294967295, # WINDOW_GRANULARITY_REGION_KEY (u32::MAX) — key-space tag, not a real multiplier
|
||||
"granularity_v2": "Region",
|
||||
"min_wl_m": 0,
|
||||
"morphology": PackedByteArray([8, 14, 0, 5]),
|
||||
|
||||
@@ -122,100 +122,58 @@ func test_clear_empties_the_cache() -> void:
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# granularity / min_wl_m (T-1150, zoom ladder design doc §3 aliasing risk)
|
||||
# min_wl_m (T-1150, zoom ladder design doc §3 aliasing risk)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## **MANDATORY aliasing regression (client half, T-1150):** a granularity-4
|
||||
## (quarter) key and a granularity-1 (district) key at the IDENTICAL
|
||||
## (body_id, center, n) must be DISTINCT cache keys — this is what prevents a
|
||||
## quarter-spacing request from silently reading (or overwriting) a
|
||||
## district-spacing window's cache entry, and vice versa.
|
||||
func test_make_key_distinguishes_granularity_at_identical_body_center_n() -> void:
|
||||
var k_district := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY
|
||||
)
|
||||
var k_quarter := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 4)
|
||||
assert_str(k_district).is_not_equal(k_quarter)
|
||||
|
||||
|
||||
## Same aliasing risk, the other new axis: two requests identical except for
|
||||
## `min_wl_m` (the octave cutoff) must not collide either — different cutoffs
|
||||
## are different derived payloads (T-1149/T-1150).
|
||||
func test_make_key_distinguishes_min_wl_m_at_identical_body_center_n_granularity() -> void:
|
||||
var k_uncut := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
||||
)
|
||||
var k_cut := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 512
|
||||
)
|
||||
## **MANDATORY aliasing regression (client half, T-1150):** two requests
|
||||
## identical except for `min_wl_m` (the octave cutoff) must not collide —
|
||||
## different cutoffs are different derived payloads (T-1149/T-1150).
|
||||
func test_make_key_distinguishes_min_wl_m_at_identical_body_center_n() -> void:
|
||||
var k_uncut := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 0)
|
||||
var k_cut := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 512)
|
||||
assert_str(k_uncut).is_not_equal(k_cut)
|
||||
|
||||
|
||||
## Omitting granularity/min_wl_m (every pre-T-1150 call site) must produce the
|
||||
## SAME key as passing the explicit district/no-cutoff defaults — byte/string
|
||||
## compatibility for existing callers, not just "doesn't crash".
|
||||
func test_omitted_granularity_and_min_wl_m_match_explicit_district_defaults() -> void:
|
||||
## Omitting min_wl_m (every pre-T-1150 call site) must produce the SAME key
|
||||
## as passing the explicit no-cutoff default — byte/string compatibility for
|
||||
## existing callers, not just "doesn't crash".
|
||||
func test_omitted_min_wl_m_matches_explicit_default() -> void:
|
||||
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
||||
var k_explicit := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0
|
||||
)
|
||||
var k_explicit := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 0)
|
||||
assert_str(k_omitted).is_equal(k_explicit)
|
||||
|
||||
|
||||
## End-to-end through put()/get_window()/has() (not just make_key() in
|
||||
## isolation): a quarter-granularity window and a district-granularity window
|
||||
## at the identical (body, center, n) must both be independently retrievable,
|
||||
## neither one clobbering or masking the other.
|
||||
func test_district_and_quarter_windows_coexist_at_identical_body_center_n() -> void:
|
||||
var cache := AtlasWindowCache.new()
|
||||
var district_window := {"granularity": AtlasWindowCache.DISTRICT_GRANULARITY, "id": "district"}
|
||||
var quarter_window := {"granularity": 4, "id": "quarter"}
|
||||
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY)
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, quarter_window, 4)
|
||||
|
||||
assert_int(cache.size()).is_equal(2)
|
||||
assert_that(
|
||||
cache.get_window("GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY)
|
||||
).is_equal(district_window)
|
||||
assert_that(cache.get_window("GJ1c", Vector2i(10, 20), 32, 4)).is_equal(quarter_window)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# granularity_v2 (T-1152/T-1153): the string-tag axis — the ONLY thing that
|
||||
# distinguishes Region from District/Quarter, since Region has no legal
|
||||
# legacy-int representation (WindowGranularity::legacy_u32() returns None for
|
||||
# Region — see the server's own doc). This is the SAME mandatory-aliasing
|
||||
# regression class as the granularity/min_wl_m tests above, extended to the
|
||||
# new axis.
|
||||
# distinguishes Region from District/Quarter (WindowGranularity has no
|
||||
# integer representation at all — see the server's own doc). This is the
|
||||
# SAME mandatory-aliasing regression class as the min_wl_m tests above,
|
||||
# extended to the new axis.
|
||||
#
|
||||
# T-1159: these tests used to also carry a legacy int `granularity`
|
||||
# positional argument (mirroring the server's now-retired
|
||||
# `window_granularity: u32` wire field) — removed along with the cache's own
|
||||
# legacy key component (see atlas_window_cache.gd's doc).
|
||||
# =============================================================================
|
||||
|
||||
|
||||
## **MANDATORY aliasing regression (T-1152/T-1153):** a Region-rung key and a
|
||||
## District-rung key at the IDENTICAL (body_id, center, n, legacy
|
||||
## granularity, min_wl_m) must be DISTINCT cache keys — the legacy int slot
|
||||
## alone (both "District" and default-omitted callers pass
|
||||
## DISTRICT_GRANULARITY=1) cannot tell them apart; granularity_v2 is what
|
||||
## does.
|
||||
## District-rung key at the IDENTICAL (body_id, center, n, min_wl_m) must be
|
||||
## DISTINCT cache keys — granularity_v2 is the only axis that can tell them
|
||||
## apart.
|
||||
func test_make_key_distinguishes_granularity_v2_region_from_district() -> void:
|
||||
var k_district := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
||||
)
|
||||
var k_region := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
|
||||
)
|
||||
var k_district := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 0, "District")
|
||||
var k_region := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 0, "Region")
|
||||
assert_str(k_district).is_not_equal(k_region)
|
||||
|
||||
|
||||
## Omitting granularity_v2 (every pre-T-1152 call site) must produce the SAME
|
||||
## key as passing the explicit "District" default — byte/string
|
||||
## compatibility, same contract as the legacy granularity/min_wl_m defaults.
|
||||
## compatibility, same contract as the min_wl_m default above.
|
||||
func test_omitted_granularity_v2_matches_explicit_district_default() -> void:
|
||||
var k_omitted := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32)
|
||||
var k_explicit := AtlasWindowCache.make_key(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
||||
)
|
||||
var k_explicit := AtlasWindowCache.make_key("GJ1c", Vector2i(10, 20), 32, 0, "District")
|
||||
assert_str(k_omitted).is_equal(k_explicit)
|
||||
|
||||
|
||||
@@ -229,23 +187,13 @@ func test_region_and_district_windows_coexist_at_identical_body_center_n() -> vo
|
||||
var district_window := {"granularity_v2": "District", "id": "district"}
|
||||
var region_window := {"granularity_v2": "Region", "id": "region"}
|
||||
|
||||
cache.put(
|
||||
"GJ1c", Vector2i(10, 20), 32, district_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
|
||||
"District"
|
||||
)
|
||||
cache.put(
|
||||
"GJ1c", Vector2i(10, 20), 32, region_window, AtlasWindowCache.DISTRICT_GRANULARITY, 0,
|
||||
"Region"
|
||||
)
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, district_window, 0, "District")
|
||||
cache.put("GJ1c", Vector2i(10, 20), 32, region_window, 0, "Region")
|
||||
|
||||
assert_int(cache.size()).is_equal(2)
|
||||
assert_that(
|
||||
cache.get_window(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "District"
|
||||
)
|
||||
cache.get_window("GJ1c", Vector2i(10, 20), 32, 0, "District")
|
||||
).is_equal(district_window)
|
||||
assert_that(
|
||||
cache.get_window(
|
||||
"GJ1c", Vector2i(10, 20), 32, AtlasWindowCache.DISTRICT_GRANULARITY, 0, "Region"
|
||||
)
|
||||
cache.get_window("GJ1c", Vector2i(10, 20), 32, 0, "Region")
|
||||
).is_equal(region_window)
|
||||
|
||||
@@ -512,13 +512,17 @@ func test_decode_atlas_response_not_found() -> void:
|
||||
|
||||
|
||||
## PR #191 review, Hoshe 3: `atlas_response_ready_with_window.msgpack` had NO
|
||||
## consumer anywhere in client/tests — regenerated by the T-1150 `granularity`/
|
||||
## `min_wl_m` field additions but nothing decoded it through the real IPC path.
|
||||
## This is that consumer, matching the sibling `test_decode_atlas_response_*`
|
||||
## tests' style/fixture-dir convention above: full decode_atlas_layer_response()
|
||||
## consumer anywhere in client/tests — regenerated by the T-1150 `min_wl_m`
|
||||
## field addition but nothing decoded it through the real IPC path. This is
|
||||
## that consumer, matching the sibling `test_decode_atlas_response_*` tests'
|
||||
## style/fixture-dir convention above: full decode_atlas_layer_response()
|
||||
## round trip (not a hand-built Dictionary like test_atlas_data_delivery.gd's
|
||||
## passthrough tests), confirming `district_window.granularity`/`.min_wl_m`
|
||||
## (T-1150's two new echo fields) survive the real client decode path.
|
||||
## passthrough tests), confirming `district_window.granularity_v2`/`.min_wl_m`
|
||||
## survive the real client decode path.
|
||||
##
|
||||
## T-1159: the legacy `district_window.granularity` int echo this test used
|
||||
## to also assert is retired server-side — see
|
||||
## server/src/atlas/layer_proxy.rs's `DistrictWindowLayer` doc.
|
||||
func test_decode_atlas_response_ready_with_window() -> void:
|
||||
var bytes := _load_fixture("atlas_response_ready_with_window")
|
||||
var resp = Protocol.decode_atlas_layer_response(bytes)
|
||||
@@ -528,7 +532,7 @@ func test_decode_atlas_response_ready_with_window() -> void:
|
||||
var window: Dictionary = resp.district_window
|
||||
assert_that(window.get("center")).is_equal([10, -5])
|
||||
assert_that(int(window.get("n"))).is_equal(2)
|
||||
assert_that(int(window.get("granularity"))).is_equal(1)
|
||||
assert_that(str(window.get("granularity_v2"))).is_equal("District")
|
||||
assert_that(int(window.get("min_wl_m"))).is_equal(0)
|
||||
|
||||
|
||||
|
||||
@@ -2,27 +2,30 @@ extends RefCounted
|
||||
|
||||
## Client-side LRU cache for DistrictWindowLayer responses (T-1138, D-226
|
||||
## T-1124 amendment §4 "Client cache policy"; extended T-1150 for the
|
||||
## granularity/min_wl axes; extended T-1152/T-1153 for the granularity_v2
|
||||
## string-tag axis that makes Region representable at all).
|
||||
## min_wl axis; extended T-1152/T-1153 for the granularity_v2 string-tag
|
||||
## axis that makes Region representable at all).
|
||||
##
|
||||
## Keyed on (body_id, center, n, granularity, min_wl_m, granularity_v2) —
|
||||
## D-227's determinism guarantee (same seed + body + position + derivation
|
||||
## params -> same derived output, always) means a previously-fetched window is
|
||||
## valid FOREVER for that body+seed. This is an LRU-evict-only cache: no
|
||||
## freshness check, no TTL, no invalidation path at all. The only reason an
|
||||
## entry ever leaves is capacity pressure.
|
||||
## Keyed on (body_id, center, n, min_wl_m, granularity_v2) — D-227's
|
||||
## determinism guarantee (same seed + body + position + derivation params ->
|
||||
## same derived output, always) means a previously-fetched window is valid
|
||||
## FOREVER for that body+seed. This is an LRU-evict-only cache: no freshness
|
||||
## check, no TTL, no invalidation path at all. The only reason an entry ever
|
||||
## leaves is capacity pressure.
|
||||
##
|
||||
## granularity/min_wl_m/granularity_v2 default to
|
||||
## DISTRICT_GRANULARITY/0/DEFAULT_GRANULARITY_V2 ("District", district
|
||||
## spacing, no octave cutoff) — every pre-T-1150 caller that doesn't pass them
|
||||
## keeps its existing key shape and cache behavior unchanged. This is the
|
||||
## client half of the mandatory aliasing fix (T-1150 design doc §3, extended
|
||||
## min_wl_m/granularity_v2 default to 0/DEFAULT_GRANULARITY_V2 ("District",
|
||||
## no octave cutoff) — every pre-T-1150 caller that doesn't pass them keeps
|
||||
## its existing key shape and cache behavior unchanged. This is the client
|
||||
## half of the mandatory aliasing fix (T-1150 design doc §3, extended
|
||||
## T-1152): a quarter-granularity request, a district-granularity request,
|
||||
## and a REGION-granularity request all at the identical (body, center, n)
|
||||
## MUST NOT collide on the same cache slot — the legacy int alone cannot
|
||||
## distinguish Region (it has no legal legacy-int value, see
|
||||
## GRANULARITY_V2_REGION's doc), which is exactly why granularity_v2 is a
|
||||
## SEPARATE key component rather than a replacement for the legacy one.
|
||||
## MUST NOT collide on the same cache slot — granularity_v2 is what
|
||||
## distinguishes them.
|
||||
##
|
||||
## **T-1159:** the legacy int `granularity` key component (district=1 /
|
||||
## quarter=4, mirroring the server's now-retired `window_granularity: u32`
|
||||
## wire field) is retired — `granularity_v2` alone has always been sufficient
|
||||
## to distinguish every rung (it's the ONLY axis that can express Region at
|
||||
## all), so carrying both was redundant.
|
||||
##
|
||||
## Godot's Dictionary preserves insertion order, so "move to the end on
|
||||
## touch, evict from the front on overflow" is the whole LRU implementation —
|
||||
@@ -35,26 +38,16 @@ extends RefCounted
|
||||
|
||||
const DEFAULT_MAX_ENTRIES: int = 24
|
||||
|
||||
## Mirrors the server's WINDOW_GRANULARITY_DISTRICT (layer_proxy.rs) — the
|
||||
## default granularity every pre-T-1150 caller implicitly requests.
|
||||
const DISTRICT_GRANULARITY: int = 1
|
||||
|
||||
## Mirrors the server's WindowGranularity enum (T-1152, R5 redesign,
|
||||
## layer_proxy.rs) — the string-tag vocabulary rmp_serde encodes a bare
|
||||
## `#[derive(Serialize, Deserialize)]` enum's variant name as, verbatim (same
|
||||
## wire convention `RoadNodeKind` already established on this carrier). This
|
||||
## is the KEY-SPACE axis (T-1153): the legacy int `granularity` param below
|
||||
## stays wired for every existing District/Quarter caller (byte/behavior
|
||||
## compatible), but a cache slot is now ALSO qualified by this string so a
|
||||
## Region-rung window can never alias onto a District/Quarter slot at the
|
||||
## identical (body, center, n, legacy_granularity, min_wl_m) — the exact
|
||||
## aliasing risk the T-1150 design doc §3 flagged, extended to the new axis.
|
||||
## wire convention `RoadNodeKind` already established on this carrier).
|
||||
const GRANULARITY_V2_QUARTER: String = "Quarter"
|
||||
const GRANULARITY_V2_DISTRICT: String = "District"
|
||||
const GRANULARITY_V2_REGION: String = "Region"
|
||||
## Default v2 tag for every caller that doesn't pass one — matches
|
||||
## DISTRICT_GRANULARITY's own "district is the implicit default" contract, so
|
||||
## an omitted v2 tag and an explicit "District" tag key identically.
|
||||
## Default v2 tag for every caller that doesn't pass one — district spacing
|
||||
## is the implicit default, so an omitted v2 tag and an explicit "District"
|
||||
## tag key identically.
|
||||
const DEFAULT_GRANULARITY_V2: String = GRANULARITY_V2_DISTRICT
|
||||
|
||||
var _max_entries: int = DEFAULT_MAX_ENTRIES
|
||||
@@ -65,42 +58,37 @@ func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
|
||||
_max_entries = maxi(1, max_entries)
|
||||
|
||||
|
||||
## Build the cache key from the six fields D-227 + T-1150/T-1152 make
|
||||
## Build the cache key from the five fields D-227 + T-1150/T-1152 make
|
||||
## sufficient: body_id (which world+body), center (a [row, col] pair or
|
||||
## Vector2i), n (window extent in districts), granularity (the legacy int:
|
||||
## district=1 / quarter=4), min_wl_m (the octave cutoff, 0 = none), and
|
||||
## granularity_v2 (the T-1152 string tag: "Quarter"/"District"/"Region" — the
|
||||
## axis that actually distinguishes Region from every finer rung, since
|
||||
## Region has no legal legacy-int representation and the legacy slot alone
|
||||
## cannot tell a Region window's cache entry apart from a District one at the
|
||||
## same (center, n)). String-keyed rather than a nested Dictionary/Array key
|
||||
## — Godot Dictionary keys compare by value for primitives but a consistent
|
||||
## stringification sidesteps any Vector2i-vs-Array identity mismatch between
|
||||
## what a caller happens to hand in.
|
||||
## Vector2i), n (window extent in districts), min_wl_m (the octave cutoff,
|
||||
## 0 = none), and granularity_v2 (the T-1152 string tag:
|
||||
## "Quarter"/"District"/"Region"). String-keyed rather than a nested
|
||||
## Dictionary/Array key — Godot Dictionary keys compare by value for
|
||||
## primitives but a consistent stringification sidesteps any
|
||||
## Vector2i-vs-Array identity mismatch between what a caller happens to hand
|
||||
## in.
|
||||
static func make_key(
|
||||
body_id: String,
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> String:
|
||||
return "%s:%d,%d:%d:%d:%d:%s" % [
|
||||
body_id, center.x, center.y, n, granularity, min_wl_m, granularity_v2
|
||||
return "%s:%d,%d:%d:%d:%s" % [
|
||||
body_id, center.x, center.y, n, min_wl_m, granularity_v2
|
||||
]
|
||||
|
||||
|
||||
## True if a window is already cached for this exact (body, center, n,
|
||||
## granularity, min_wl_m, granularity_v2).
|
||||
## min_wl_m, granularity_v2).
|
||||
func has(
|
||||
body_id: String,
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> bool:
|
||||
return _entries.has(make_key(body_id, center, n, granularity, min_wl_m, granularity_v2))
|
||||
return _entries.has(make_key(body_id, center, n, min_wl_m, granularity_v2))
|
||||
|
||||
|
||||
## Fetch a cached window, touching it (move-to-most-recently-used). Returns
|
||||
@@ -112,11 +100,10 @@ func get_window(
|
||||
body_id: String,
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> Variant:
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
|
||||
var key := make_key(body_id, center, n, min_wl_m, granularity_v2)
|
||||
if not _entries.has(key):
|
||||
return null
|
||||
var value: Variant = _entries[key]
|
||||
@@ -133,11 +120,10 @@ func put(
|
||||
center: Vector2i,
|
||||
n: int,
|
||||
window: Dictionary,
|
||||
granularity: int = DISTRICT_GRANULARITY,
|
||||
min_wl_m: int = 0,
|
||||
granularity_v2: String = DEFAULT_GRANULARITY_V2
|
||||
) -> void:
|
||||
var key := make_key(body_id, center, n, granularity, min_wl_m, granularity_v2)
|
||||
var key := make_key(body_id, center, n, min_wl_m, granularity_v2)
|
||||
if _entries.has(key):
|
||||
_entries.erase(key)
|
||||
_entries[key] = window
|
||||
|
||||
Reference in New Issue
Block a user