Entry per Jeroen's 2026-07-21 revision: planetary heightmap is now FIXED — all drag-pan/wheel-zoom input removed (set_view/get_view_* capture API survives for the golden harness); hover shows a not-to-scale bracket reticle with the real extent labeled (a true n=32 rectangle is sub-pixel on the planetary canvas — the honest representation given the morph transition is deferred), and a click that misses every city marker descends (city-click wins — one gesture, two contextual reads, no modifier). Descent pushes a new 'district' nav screen centered on the click point's DistrictPos via atlas_descend_geometry.district_pos_at (the pixel-to-district inverse of the server mapping, verified against scale.rs). Regional mode: atlas_window_viewer draws the composite (morphology x elev_q lightness base; temp/moisture/veg toggles — temp reuses the region-ramp colorizer exactly; Marine=6 transparent; glaciation always-on tint matching apply_ice_tint's REAL gate, None|Light no-op, over the amendment's looser prose — documented); pan-on-held-composite with edge-crossing refetch + border-fade during the queue-based derive wait; zoom never refetches. atlas_window_cache: LRU keyed (body_id, center, n), touch-on-read, evict-only, no freshness (D-227). atlas_window_request mirrors the generation-proxy pending-retry shape for None-until-derived. Codec: window params omitted from the wire when absent — byte-identical for every existing caller. Live-verified against the T-1137 server in-worktree: real round-trip on a GJ380c coastal district (6 fields x 1024 cells), echo staleness guard, genuine ~1.4s background-derive wait, pan-edge refetch to an adjacent window, cache-hit on re-descent with zero network. Full client suite 3194/3194; gdlint clean on all 18 files. Open follow-ups flagged in-code: header location label always falls back to coordinates (nearest-settlement needs a join the district window does not carry); atlas_standalone.gd's 'atlas_app.gd is never modified' doc line is now imprecise. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
3.1 KiB
GDScript
80 lines
3.1 KiB
GDScript
extends RefCounted
|
|
|
|
## Client-side LRU cache for DistrictWindowLayer responses (T-1138, D-226
|
|
## T-1124 amendment §4 "Client cache policy").
|
|
##
|
|
## Keyed on (body_id, center, n) — D-227's determinism guarantee (same seed +
|
|
## body + position -> 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.
|
|
##
|
|
## 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
|
|
|
|
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 three fields D-227 makes sufficient:
|
|
## body_id (which world+body), center (a [row, col] pair or Vector2i), and n
|
|
## (window side length). 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) -> String:
|
|
return "%s:%d,%d:%d" % [body_id, center.x, center.y, n]
|
|
|
|
|
|
## True if a window is already cached for this exact (body, center, n).
|
|
func has(body_id: String, center: Vector2i, n: int) -> bool:
|
|
return _entries.has(make_key(body_id, center, n))
|
|
|
|
|
|
## 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) -> Variant:
|
|
var key := make_key(body_id, center, n)
|
|
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) -> void:
|
|
var key := make_key(body_id, center, n)
|
|
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()
|