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>
141 lines
5.4 KiB
GDScript
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()
|