fix(client): the Atlas was replaying a cache from a build that no longer existed (T-1239)
Ferrath's Global map drew no rivers at native resolution: 375 courses arrived and 0 were drawn. The report suspected the D-261 length cull or the water truncation. Both were innocent, and so was the renderer. The client served the canvas from its own disk cache (T-1183). Every payload for GJ820Bc predated T-1237 (4e503c356), which replaced one-course-per-D8-hop with one-course-per-river -- so the map was drawing 375 hop fragments whose longest run was 106 km, all of them under D-261's read-as-a-line floor. Same build, same scenario, same 3440x1440, cache the only difference: stale courses=375 runs=180 longest=6.0px (~106 km) drawn=0 cold courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18 It looked resolution-dependent because it wasn't a resolution at all: 960x540 resolves to an 814x407 canvas, a key never cached, so it missed and re-derived correctly. 3440x1440 resolves to 1080x540, which had an entry from 2026-08-06. During the stale capture the server logged no course production whatsoever -- the canvas never came from it. The cache's only invalidation signal is project.yaml's version, and4e503c356changed how canvases are generated without touching it, so hop-shaped entries stayed valid. All 13 stale entries are stamped 0.4.5. 0.4.6 forces them to miss; that, not clearing a local directory, is what repairs a player's Atlas. The harness let this hide for eight days, in two ways now fixed. It ran against the developer's persistent user:// cache, so a capture could render a canvas built by a build that no longer existed -- and any golden shot in that window silently inherited it; user:// is now isolated per run. And it sent server stderr to /dev/null via an already-unlinked mktemp file, so no tracing from a capture was ever reachable; the log now lives at .cache/visual-server.log. The capture readout gained runs= and longest= between courses= and drawn=, because "375 arrived, 0 drawn" is not one fact but three stages, and telling them apart is what turned a guess between two suspects into a measurement. Follow-ups filed: T-1241 (current_schema_version() returns its ?.?.? fallback in an exported build, so a shipped game never invalidates on version at all) and T-1242 (nothing enforces the generation-change/version-bump pairing -- this is the fourth bump forced after the fact). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -335,7 +335,7 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void:
|
||||
(
|
||||
"visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s "
|
||||
+ "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d "
|
||||
+ "courses=%d drawn=%d settlements=%d"
|
||||
+ "courses=%d runs=%d longest=%.1fpx drawn=%d settlements=%d"
|
||||
)
|
||||
% [
|
||||
scenario_name,
|
||||
@@ -348,6 +348,8 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void:
|
||||
int(summary.get("canvas_width", 0)),
|
||||
int(summary.get("canvas_height", 0)),
|
||||
int(summary.get("course_count", 0)),
|
||||
int(summary.get("runs_built", 0)),
|
||||
float(summary.get("longest_run_px", 0.0)),
|
||||
int(summary.get("drawn_course_count", 0)),
|
||||
int(summary.get("settlement_count", 0)),
|
||||
]
|
||||
|
||||
@@ -206,6 +206,16 @@ var _body_radius_km: float = 0.0
|
||||
## (D-261). Entries: {points: PackedVector2Array, terminus: String, taper: bool}.
|
||||
var _prepared_courses: Array = []
|
||||
|
||||
## Preparation stage counts, for telling apart the three ways a river can fail
|
||||
## to appear (T-1239). "375 courses arrived and 0 were drawn" is not one fact
|
||||
## but a chain: courses arrive, the water clip splits them into runs, the length
|
||||
## cull drops the short runs. Without the intermediate counts every failure
|
||||
## looks identical from a capture, which is how T-1239 sat as "suspect the cull
|
||||
## OR the water truncation" instead of a measurement. Cheap: three integers
|
||||
## written once per canvas adoption, never per frame.
|
||||
var _runs_built: int = 0
|
||||
var _longest_run_px: float = 0.0
|
||||
|
||||
|
||||
## Adopt a new canvas + its request frame (world center, rung, extent, body
|
||||
## radius) — the world->screen projection for every drawn feature depends on
|
||||
@@ -249,6 +259,8 @@ func clear_frame() -> void:
|
||||
## dropped entirely rather than drawn as specks.
|
||||
func _prepare_courses() -> void:
|
||||
_prepared_courses.clear()
|
||||
_runs_built = 0
|
||||
_longest_run_px = 0.0
|
||||
if not _canvas is Dictionary:
|
||||
return
|
||||
var d: Dictionary = _canvas
|
||||
@@ -298,6 +310,8 @@ func _flush_run(
|
||||
) -> void:
|
||||
if run.size() < 2:
|
||||
return
|
||||
_runs_built += 1
|
||||
_longest_run_px = maxf(_longest_run_px, _polyline_length_px(run))
|
||||
_prepared_courses.append(
|
||||
{
|
||||
"points": run,
|
||||
@@ -351,6 +365,21 @@ func get_drawn_course_count() -> int:
|
||||
return _prepared_courses.size()
|
||||
|
||||
|
||||
## Runs the water clip produced, before the length cull (T-1239 diagnostic).
|
||||
## Read with get_drawn_course_count(): runs_built == 0 means the water clip ate
|
||||
## everything; runs_built high with drawn 0 means the cull did.
|
||||
func get_runs_built() -> int:
|
||||
return _runs_built
|
||||
|
||||
|
||||
## The longest run's on-screen length in px, before the cull (T-1239). Compared
|
||||
## against MIN_COURSE_LENGTH_PX it says whether the threshold was near-miss or
|
||||
## nowhere close — the difference between "tune the constant" and "the geometry
|
||||
## is wrong".
|
||||
func get_longest_run_px() -> float:
|
||||
return _longest_run_px
|
||||
|
||||
|
||||
static func _polyline_length_px(pts: PackedVector2Array) -> float:
|
||||
var total: float = 0.0
|
||||
for i in range(1, pts.size()):
|
||||
|
||||
@@ -434,6 +434,10 @@ func get_current_canvas_summary() -> Dictionary:
|
||||
"drawn_course_count": (
|
||||
_annotation_layer.get_drawn_course_count() if _annotation_layer else 0
|
||||
),
|
||||
# T-1239: the two stages between "arrived" and "drawn", so a capture can
|
||||
# say WHICH stage dropped the rivers instead of only that they are gone.
|
||||
"runs_built": _annotation_layer.get_runs_built() if _annotation_layer else 0,
|
||||
"longest_run_px": _annotation_layer.get_longest_run_px() if _annotation_layer else 0.0,
|
||||
"course_count_by_class": course_count_by_class,
|
||||
"cliff_count": (d.get("cliffs", []) as Array).size(),
|
||||
"settlement_count": _count_distinct_settlements(d),
|
||||
|
||||
Reference in New Issue
Block a user