Tyre's APPROVE items (T-1163): record the two-legal-wire-shapes-for-one- logical-state protocol invariant (whole-response Pending AND Ready+null district_window both mean 're-poll'; only NotFound/Error are terminal) as a D-226 note under the T-1124 §4 amendment area, so a future server refactor of the asymmetry must migrate every consumer in the same change. Drop the duplicated 5-line stagger comment in atlas_window_tile_set.gd. Follow-up tickets filed on main: T-1164 (tiled terminal-recovery), T-1165 (queue_redraw edge root-cause), T-1166 (cold-launch test tier). Tickets: T-1163 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
204 lines
9.2 KiB
GDScript
204 lines
9.2 KiB
GDScript
extends Node
|
|
|
|
## Orbital rest-state TILE-SET orchestration (T-1153, live round 3 — Jeroen's
|
|
## ruling, design doc §4: "the top rest state is the WHOLE body, served as
|
|
## progressive capped-density TILING"). A single wire-capped Region window
|
|
## (AtlasWindowGeometry.MAX_COVERAGE_M["Region"] = 13,107,200 m) covers only a
|
|
## fraction of a real body's circumference (Lendel: ~39,197,023 m — a single
|
|
## window is ~a third of the body, the exact live-round finding: shot 01's
|
|
## own header read "13107.2 x 13107.2 km" against a 39,198 km circumference).
|
|
##
|
|
## Owns N independent `AtlasWindowRequest` child instances — one per tile —
|
|
## reusing 100% of the EXISTING, already-tested single-window request/cache/
|
|
## debounce/retry machinery (atlas_window_request.gd) rather than
|
|
## reinventing multi-window orchestration from scratch. Each tile is just a
|
|
## Region-granularity window request at its own canonicalized center
|
|
## (AtlasWindowGeometry.compute_tile_grid()); distinct centers are already
|
|
## distinct cache/coalescing keys (T-1150/T-1152's own aliasing discipline),
|
|
## so nothing about the request/cache LAYER needed to change for tiling to
|
|
## work — only the ORCHESTRATION (issue N requests instead of one) and the
|
|
## DRAWING (a mosaic instead of one composite) are new.
|
|
##
|
|
## No `class_name` on purpose, matching every other viewer-owned helper in
|
|
## this cluster (atlas_window_request.gd/atlas_overlay_bar.gd/
|
|
## atlas_legend_panel.gd, review #8 precedent): the owner (AtlasWindowViewer)
|
|
## passes itself to `_init()`.
|
|
##
|
|
## Progressive arrival (design doc §4's own "with visible refinement as
|
|
## tiles complete"): each tile's `AtlasWindowRequest.window_ready` connects
|
|
## independently — a tile's own `_tiles[i]["window"]` updates the moment
|
|
## THAT tile's response lands, with no dependency on any other tile's
|
|
## arrival. The viewer/overlay reads `get_tiles()` every draw and renders
|
|
## whichever tiles have arrived so far — an empty/border-fade gap for the
|
|
## rest, exactly the same "hold what's there, sharpen in place" contract
|
|
## single-window progressive refinement already has (§6 "no mode flip"),
|
|
## just per-tile instead of per-composite.
|
|
|
|
signal tile_ready(index: int) # a single tile's window arrived/updated — the viewer redraws
|
|
|
|
const AtlasWindowRequest := preload("res://ui/implant/apps/atlas/atlas_window_request.gd")
|
|
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
|
|
|
var _owner = null # AtlasWindowViewer (untyped to avoid cyclic ref)
|
|
var _body_id: String = ""
|
|
var _tile_n: int = AtlasWindowGeometry.TILE_N
|
|
|
|
## Array[Dictionary]: {"center": Vector2i, "request": AtlasWindowRequest,
|
|
## "window": Variant (null until arrived)} — one entry per tile, in the SAME
|
|
## deterministic order compute_tile_grid() produces (stable fill order, see
|
|
## that function's own doc).
|
|
var _tiles: Array = []
|
|
|
|
|
|
func _init(owner_ref = null) -> void:
|
|
_owner = owner_ref
|
|
|
|
|
|
## Unlike an individual AtlasWindowRequest (which has no signal connection of
|
|
## its own — the OWNING viewer forwards responses to it, per that class'
|
|
## own doc), the tile set DOES connect directly to
|
|
## SimBridge.atlas_layers_received itself and fans a single response out to
|
|
## EVERY tile's own `on_response()` — each tile's OWN staleness guard
|
|
## (center/n/granularity_v2) decides whether that particular response is
|
|
## the one IT was waiting for; only the matching tile ever adopts it. This
|
|
## is the same "one shared inbound signal, N independent consumers filtering
|
|
## by their own criteria" shape the design already uses elsewhere (every
|
|
## AtlasWindowRequest instance filters on its own state from a common
|
|
## broadcast — tiling just means N instances share the broadcast instead of
|
|
## one).
|
|
func _ready() -> void:
|
|
SimBridge.atlas_layers_received.connect(_on_atlas_layers_received)
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
if SimBridge.atlas_layers_received.is_connected(_on_atlas_layers_received):
|
|
SimBridge.atlas_layers_received.disconnect(_on_atlas_layers_received)
|
|
|
|
|
|
func _on_atlas_layers_received(response: Dictionary) -> void:
|
|
for tile: Dictionary in _tiles:
|
|
var request = tile["request"]
|
|
if is_instance_valid(request):
|
|
request.on_response(response)
|
|
|
|
|
|
## Enter tile mode for `body_id`/`body_radius_km` — computes the tile grid,
|
|
## tears down any PREVIOUS tile set's child request nodes (a fresh
|
|
## enter_orbital() on a DIFFERENT body must not leave stale tile requests
|
|
## from the old body wired up), and issues one request per tile immediately
|
|
## (no debounce — matching AtlasWindowRequest.request_now()'s own "first
|
|
## window" contract, §5: entry is never debounced, only pan/rung-reselect
|
|
## refetches are).
|
|
func enter(body_id: String, body_radius_km: float) -> void:
|
|
_teardown()
|
|
_body_id = body_id
|
|
var centers: Array = AtlasWindowGeometry.compute_tile_grid(body_radius_km)
|
|
for i in range(centers.size()):
|
|
var center: Vector2i = centers[i]
|
|
var request = AtlasWindowRequest.new(self)
|
|
request.name = "Tile%d" % i
|
|
# Cold-start dossier round 3 hardening: deterministic per-tile retry
|
|
# stagger (STAGGER_STEP*i) — without it, all 6 tiles go pending in the
|
|
# same frame and retry in perfect lockstep, a request pulse every
|
|
# RETRY_DELAY instead of a spread trickle. Set BEFORE request_now()
|
|
# so it's already in place for the very first retry, if one fires.
|
|
request._stagger_index = i
|
|
add_child(request)
|
|
var tile_index := i # capture by value for the lambda below
|
|
request.window_ready.connect(
|
|
func(window: Dictionary) -> void: _on_tile_window_ready(tile_index, window)
|
|
)
|
|
_tiles.append({"center": center, "request": request, "window": null})
|
|
request.request_now(body_id, center, _tile_n, AtlasWindowRequest.GRANULARITY_V2_REGION)
|
|
|
|
|
|
func _on_tile_window_ready(index: int, window: Dictionary) -> void:
|
|
if index < 0 or index >= _tiles.size():
|
|
return # a stale signal from a torn-down tile set (shouldn't happen — disconnected on teardown)
|
|
_tiles[index]["window"] = window
|
|
tile_ready.emit(index)
|
|
|
|
|
|
## Tear down every tile's request node — disconnects nothing explicitly
|
|
## (queue_free() on a Node disconnects all its own signal connections
|
|
## automatically, Godot's documented behavior) but DOES clear `_tiles` so a
|
|
## stale index from an in-flight-but-now-orphaned request's eventual
|
|
## response can never reach `_on_tile_window_ready()` with a now-meaningless
|
|
## index (guarded there too, belt-and-suspenders).
|
|
func _teardown() -> void:
|
|
for tile: Dictionary in _tiles:
|
|
var request = tile["request"]
|
|
if is_instance_valid(request):
|
|
request.queue_free()
|
|
_tiles.clear()
|
|
|
|
|
|
## The current tile set, for the viewer/overlay to draw — an Array of
|
|
## {"center": Vector2i, "window": Variant} (the "request" key is internal,
|
|
## not exposed here; callers only need center + arrived-or-null window).
|
|
func get_tiles() -> Array:
|
|
var result: Array = []
|
|
for tile: Dictionary in _tiles:
|
|
result.append({"center": tile["center"], "window": tile["window"]})
|
|
return result
|
|
|
|
|
|
## True while at least one tile's `window` hasn't arrived yet — the viewer's
|
|
## cold-start self-healing redraw (see AtlasWindowViewer._process()'s own
|
|
## doc) polls this every frame so a mosaic's paint can never silently wedge
|
|
## behind a lost/late queue_redraw() no matter which signal edge it was
|
|
## supposed to ride in on. Also the source of truth for whether the §4
|
|
## pending treatment should show.
|
|
func has_pending_tiles() -> bool:
|
|
for tile: Dictionary in _tiles:
|
|
if tile["window"] == null:
|
|
return true
|
|
return false
|
|
|
|
|
|
## True once at least ONE tile has a real window — distinct from
|
|
## has_pending_tiles()'s "at least one MISSING" (both can be true at once,
|
|
## mid-arrival). PR #192 cold-start round 2: a cold server's first
|
|
## AnalyzeBody can take >10s with ZERO tiles landed the whole time — the
|
|
## per-tile border-fade wash alone (subtle, same color as every OTHER
|
|
## no-data-yet state) read as broken darkness in a live cold capture, not
|
|
## loading. The viewer uses this to gate an unmistakable "DERIVING
|
|
## TERRAIN…" label: shown while this is false (nothing has arrived at all —
|
|
## the reassurance is needed most), dropped the moment even one tile lands
|
|
## (per-tile washes alone read fine once real content is visibly filling in
|
|
## around the gaps).
|
|
func has_any_tile_arrived() -> bool:
|
|
for tile: Dictionary in _tiles:
|
|
if tile["window"] != null:
|
|
return true
|
|
return false
|
|
|
|
|
|
## True once tiling is active for the current body — a body whose whole
|
|
## circumference fits in ONE Region window's own coverage ceiling produces
|
|
## exactly one tile (compute_tile_grid()'s own degenerate-case doc), so
|
|
## `is_multi_tile()` distinguishes "tile set with 1 entry" (still tiling
|
|
## machinery, technically) from "genuinely multiple tiles" — the viewer uses
|
|
## this to decide whether the tile-set draw path or the ORIGINAL
|
|
## single-window draw path is simpler/preferred for a small body (both are
|
|
## correct; single-window avoids the extra Node/signal overhead when there's
|
|
## only ever going to be one tile).
|
|
func is_multi_tile() -> bool:
|
|
return _tiles.size() > 1
|
|
|
|
|
|
func get_tile_count() -> int:
|
|
return _tiles.size()
|
|
|
|
|
|
## True if every tile currently has an arrived window — the viewer/legend
|
|
## chrome can use this to know when the mosaic is "complete" vs. still
|
|
## progressively filling in.
|
|
func is_fully_arrived() -> bool:
|
|
if _tiles.is_empty():
|
|
return false
|
|
for tile: Dictionary in _tiles:
|
|
if tile["window"] == null:
|
|
return false
|
|
return true
|