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:
2026-07-25 16:10:27 +02:00
co-authored by Claude Fable 5
parent befdb689c1
commit 8da9670e0f
28 changed files with 1244 additions and 467 deletions
@@ -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