Files
settled-reach/client/ui/implant/apps/atlas/atlas_window_cache.gd
T
jpmschweitzerandClaude Fable 5 8da9670e0f 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>
2026-07-25 16:10:27 +02:00

141 lines
5.4 KiB
GDScript

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
## 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, 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.
##
## 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 — 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 —
## no separate linked-list/counter bookkeeping needed.
##
## Consumed via explicit load() by path (no class_name), matching
## atlas_overlay_bar.gd/atlas_legend_panel.gd (review #8 precedent
## elsewhere in this app): the owner constructs one instance and holds it,
## same shape as those two.
const DEFAULT_MAX_ENTRIES: int = 24
## 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).
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 — 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
var _entries: Dictionary = {} # key String -> DistrictWindowLayer Dictionary
func _init(max_entries: int = DEFAULT_MAX_ENTRIES) -> void:
_max_entries = maxi(1, max_entries)
## 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), 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,
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> String:
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,
## min_wl_m, granularity_v2).
func has(
body_id: String,
center: Vector2i,
n: int,
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> bool:
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
## null on a miss — callers must not confuse this with a real "None" server
## response, which is a different concept (§1: an as-yet-underived window is
## carried as `district_window: None` inside a `Ready` AtlasLayerResponse,
## not a cache state).
func get_window(
body_id: String,
center: Vector2i,
n: int,
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> Variant:
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]
# Touch: erase + re-insert moves the key to the end (most-recently-used).
_entries.erase(key)
_entries[key] = value
return value
## Store a window, evicting the least-recently-used entry(ies) if over
## capacity. Overwriting an existing key also counts as a touch.
func put(
body_id: String,
center: Vector2i,
n: int,
window: Dictionary,
min_wl_m: int = 0,
granularity_v2: String = DEFAULT_GRANULARITY_V2
) -> void:
var key := make_key(body_id, center, n, min_wl_m, granularity_v2)
if _entries.has(key):
_entries.erase(key)
_entries[key] = window
while _entries.size() > _max_entries:
var oldest_key: String = _entries.keys()[0]
_entries.erase(oldest_key)
func size() -> int:
return _entries.size()
func clear() -> void:
_entries.clear()