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()