diff --git a/client/tests/test_atlas_window_geometry_nature.gd b/client/tests/test_atlas_window_geometry_nature.gd index 3c7d63315..73486d8d9 100644 --- a/client/tests/test_atlas_window_geometry_nature.gd +++ b/client/tests/test_atlas_window_geometry_nature.gd @@ -281,3 +281,46 @@ func test_zoom_compensated_size_zero_zoom_does_not_blow_up() -> void: assert_bool(is_finite(result)).override_failure_message( "a degenerate zero view_zoom must not produce inf/NaN" ).is_true() + + +# ============================================================================= +# T-1172 round 2: cell_index_for_local_offset() — the shared painter/clip +# index formula (see its own doc for the "why shared, not duplicated" case). +# ============================================================================= + + +func test_cell_index_for_local_offset_top_left_is_zero_zero() -> void: + var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset(0.0, 0.0, 6400, 64) + assert_that(cell).is_equal(Vector2i(0, 0)) + + +## The exact live-repro numbers from T-1172 round 2's trace: a query whose +## district-space local offset is (2594.09, 5593.15) inside a 6400-wide, +## 64-cell-side window must resolve to (col=25, row=55) — pinned directly +## against the LIVE captured values that closed the investigation (both the +## painter's _build_tile_texture() and the clip independently produced this +## exact pair for the same query in the live trace). +func test_cell_index_for_local_offset_matches_the_live_trace_repro() -> void: + var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset( + 2594.0849609375, 5593.15258789062, 6400, 64 + ) + assert_that(cell).override_failure_message( + "must match the live-captured painter/clip agreement point from the" + + " T-1172 round 2 investigation — (col=25, row=55)" + ).is_equal(Vector2i(25, 55)) + + +func test_cell_index_for_local_offset_bottom_right_boundary_clamps_inside() -> void: + # local offset == n (the exclusive upper boundary) must clamp to the LAST + # cell, not overflow to a nonexistent grid_side'th cell. + var cell: Vector2i = AtlasWindowGeometry.cell_index_for_local_offset(6400.0, 6400.0, 6400, 64) + assert_that(cell).is_equal(Vector2i(63, 63)) + + +func test_cell_index_for_local_offset_zero_n_or_grid_side_returns_sentinel() -> void: + assert_that(AtlasWindowGeometry.cell_index_for_local_offset(10.0, 10.0, 0, 64)).is_equal( + Vector2i(-1, -1) + ) + assert_that(AtlasWindowGeometry.cell_index_for_local_offset(10.0, 10.0, 6400, 0)).is_equal( + Vector2i(-1, -1) + ) diff --git a/client/tests/test_atlas_window_nature_overlay.gd b/client/tests/test_atlas_window_nature_overlay.gd index 10354871e..90b9e0a03 100644 --- a/client/tests/test_atlas_window_nature_overlay.gd +++ b/client/tests/test_atlas_window_nature_overlay.gd @@ -18,7 +18,10 @@ const AtlasWindowNatureOverlay := preload("res://ui/implant/apps/atlas/atlas_win ## the same duck-typed-viewer precedent test_atlas_window_overlay.gd's ## _ViewerStub already establishes for AtlasWindowOverlay. get_view_zoom() ## added post-live-eyeball (coordinator finding, 2026-07-23): _draw() now -## reads it for the zoom-compensated marker-size fix. +## reads it for the zoom-compensated marker-size fix. is_tile_mode()/ +## get_district_window()/get_tile_set() added for T-1172 (the water clip) — +## district_window/tile_set default to null/an empty stub, matching "no +## arrived composite data yet" (the fail-open case) unless a test sets them. class _ViewerStub: var held_granularity_v2: String = "Region" var body_radius_km: float = 6371.0 @@ -26,6 +29,9 @@ class _ViewerStub: var held_n: int = 64 var view_zoom: float = 1.0 var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": false, "gen_attractors": false} + var tile_mode: bool = false + var district_window: Variant = null + var tile_set: Variant = null func get_held_granularity_v2() -> String: return held_granularity_v2 @@ -48,6 +54,25 @@ class _ViewerStub: func is_overlay_visible(overlay_id: String) -> bool: return bool(overlay_visibility.get(overlay_id, false)) + func is_tile_mode() -> bool: + return tile_mode + + func get_district_window() -> Variant: + return district_window + + func get_tile_set() -> Variant: + return tile_set + + +## Bare tile-set stub — AtlasWindowNatureOverlay's water-clip lookup only +## reaches it through get_tiles(), matching AtlasWindowTileSet's own public +## surface (an Array of {"center": Vector2i, "window": Variant}). +class _TileSetStub: + var tiles: Array = [] + + func get_tiles() -> Array: + return tiles + static func _mock_layer1(river_class: Variant = null) -> Dictionary: var rn: Dictionary = { @@ -253,3 +278,167 @@ func test_attractor_shape_stroke_widths_are_never_raw_literals() -> void: "raw numeric stroke width(s) in _draw_attractor_shape — route through" + " px_w (PR #195 Tyre I1): %s" % [offenders] ).is_empty() + + +# ============================================================================= +# T-1172 — the two-waterline clip. _is_drawn_water()/_district() are called +# directly (both are pure lookups with NO draw_*() call of their own — the +# _draw()-requires-a-live-render-pass constraint the smoke suite exists for +# does not apply to them), matching this file's own "call private helpers +# directly when they're the load-bearing unit" precedent +# (test_draw_with_zero_grid_dims_returns_before_any_draw_call() above already +# calls _draw() itself specifically because its early-return is BEFORE any +# draw call — same reasoning here, one level down). +# ============================================================================= + + +## A 4x4 District window (n=4, grid_side=4) centered on district (0,0), +## spanning [-2, 2) on both axes — cell (0,0) is water, everything else land. +## Mirrors test_atlas_window_water_clip.gd's own _mock_4x4_window() fixture +## shape (kept local here rather than shared — no cross-test-file import +## precedent in this cluster). +static func _mock_4x4_water_corner_window() -> Dictionary: + var morphology := PackedByteArray() + morphology.resize(16) + for i in range(16): + morphology[i] = 8 # AlluvialPlain — land + morphology[0] = 0 # OpenOcean — the single water cell, row 0 col 0 + return {"center": [0, 0], "n": 4, "granularity_v2": "District", "morphology": morphology} + + +func _ctx_for(viewer_stub: _ViewerStub) -> Dictionary: + return { + "grid_w": 256.0, + "grid_h": 128.0, + "radius_km": 0.0, # no-radius: 1 heightmap pixel = 1 district metre (simplest math) + "held_center": viewer_stub.held_center, + "held_n": viewer_stub.held_n, + "cell_px": 16.0, + "cols": 0, + "granularity_v2": viewer_stub.held_granularity_v2, + "view_zoom": viewer_stub.view_zoom, + } + + +## Single-window mode: a district position resolving to a LAND cell must not +## be clipped (_is_drawn_water() returns false — the dot draws). +func test_is_drawn_water_false_for_a_land_cell_single_window() -> void: + var stub := _ViewerStub.new() + stub.district_window = _mock_4x4_water_corner_window() + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message( + "a district position over a LAND cell must not be clipped" + ).is_false() + + +## Single-window mode: a district position resolving to a WATER cell must be +## clipped (_is_drawn_water() returns true — the caller skips drawing). +func test_is_drawn_water_true_for_a_water_cell_single_window() -> void: + var stub := _ViewerStub.new() + stub.district_window = _mock_4x4_water_corner_window() + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message( + "a district position over a WATER (OpenOcean) cell must be clipped" + ).is_true() + + +## Tile mode: same land/water split, but the composite data arrives via +## get_tile_set().get_tiles() instead of get_district_window() — proving the +## clip predicate reaches BOTH path shapes, per the coordinator's explicit +## "both path shapes exercised" ask. +func test_is_drawn_water_works_in_tile_mode_land() -> void: + var stub := _ViewerStub.new() + stub.tile_mode = true + var ts := _TileSetStub.new() + ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}] + stub.tile_set = ts + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(1.5, 1.5), ctx)).override_failure_message( + "tile mode: a district position over a LAND cell must not be clipped" + ).is_false() + + +func test_is_drawn_water_works_in_tile_mode_water() -> void: + var stub := _ViewerStub.new() + stub.tile_mode = true + var ts := _TileSetStub.new() + ts.tiles = [{"center": Vector2i.ZERO, "window": _mock_4x4_water_corner_window()}] + stub.tile_set = ts + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(-1.5, -1.5), ctx)).override_failure_message( + "tile mode: a district position over a WATER cell must be clipped" + ).is_true() + + +## Tyre's rule 5: NO arrived composite data at the queried position (single- +## window mode, window is null — the pre-arrival state) must FAIL OPEN — the +## clip is a presentation refinement, never a data gate. +func test_is_drawn_water_fails_open_with_no_composite_data_single_window() -> void: + var stub := _ViewerStub.new() + stub.district_window = null # nothing arrived yet + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message( + "no arrived composite data must fail OPEN (draw the dot), never clip" + ).is_false() + + +## Same fail-open guarantee in tile mode: no tile set at all (get_tile_set() +## returns null, matching the viewer's own pre-enter_orbital() state). +func test_is_drawn_water_fails_open_with_no_tile_set() -> void: + var stub := _ViewerStub.new() + stub.tile_mode = true + stub.tile_set = null + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).override_failure_message( + "no tile set at all must fail OPEN (draw the dot), never clip" + ).is_false() + + +## Fail-open ALSO covers "a tile set exists but no tile covers this position +## yet" (mid-progressive-arrival) — the coordinator's own "briefly-unclipped +## dot during progressive arrival is fine and self-heals" framing. +func test_is_drawn_water_fails_open_when_no_tile_covers_the_position() -> void: + var stub := _ViewerStub.new() + stub.tile_mode = true + var ts := _TileSetStub.new() + ts.tiles = [{"center": Vector2i(500, 500), "window": null}] # far away, unarrived + stub.tile_set = ts + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(0.0, 0.0), ctx)).is_false() + + +## Mouths must be SUPPRESSED (not snapped, not dimmed) on drawn water and +## render exactly as today on drawn land — this suite covers the SHARED +## predicate _draw_rivers() calls for river cells/confluences/mouths alike +## (_is_drawn_water() itself has no notion of "which feature type" — that's +## by design, per the ruling's "same predicate" wording for rule 3). A +## dedicated assertion here pins the WORDING intent (mouth-specific rule 3) +## even though the underlying mechanism is identical to the river-cell tests +## above — a future refactor that special-cases mouths differently should +## still trip this. +func test_mouth_position_on_water_is_suppressed_same_predicate_as_rivers() -> void: + var stub := _ViewerStub.new() + stub.district_window = _mock_4x4_water_corner_window() + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(-1.9, -1.9), ctx)).override_failure_message( + "a mouth position over drawn water must resolve as clipped, via the SAME" + + " predicate river cells/confluences use — no separate snap/dim path" + ).is_true() + + +func test_mouth_position_on_land_is_not_suppressed() -> void: + var stub := _ViewerStub.new() + stub.district_window = _mock_4x4_water_corner_window() + var o = _make_overlay(stub) + var ctx: Dictionary = _ctx_for(stub) + assert_bool(o._is_drawn_water(Vector2(1.9, 1.9), ctx)).override_failure_message( + "a mouth position over drawn land must render exactly as today (not clipped)" + ).is_false() diff --git a/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd b/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd index bb11b35d2..3e47b6973 100644 --- a/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd +++ b/client/tests/test_atlas_window_nature_overlay_draw_smoke.gd @@ -67,7 +67,13 @@ static func _dummy_renderer_active() -> bool: ## called with — the whole point of this smoke suite (post-live-eyeball, ## coordinator finding 2026-07-23) is proving markers stay visible at the ## REAL orbital fit zoom, not an artificially large test zoom that would -## mask the zoom-compensation bug the fix addresses. +## mask the zoom-compensation bug the fix addresses. is_tile_mode()/ +## get_district_window()/get_tile_set() added for T-1172 (the water clip) — +## defaults to single-window mode with NO arrived composite (district_window +## null), the fail-open case, so these smoke tests keep drawing every marker +## exactly as before the clip existed (this file's own job is proving the +## draw calls paint pixels at all, not exercising the clip's water-detection +## branch — that's test_atlas_window_nature_overlay.gd's job). class _ViewerStub: var held_granularity_v2: String = "Region" var body_radius_km: float = 6371.0 @@ -75,6 +81,9 @@ class _ViewerStub: var held_n: int = 64 var view_zoom: float = 1.0 var overlay_visibility: Dictionary = {"gen_rivers": true, "gen_basins": true, "gen_attractors": true} + var tile_mode: bool = false + var district_window: Variant = null + var tile_set: Variant = null func get_held_granularity_v2() -> String: return held_granularity_v2 @@ -97,6 +106,15 @@ class _ViewerStub: func is_overlay_visible(overlay_id: String) -> bool: return bool(overlay_visibility.get(overlay_id, false)) + func is_tile_mode() -> bool: + return tile_mode + + func get_district_window() -> Variant: + return district_window + + func get_tile_set() -> Variant: + return tile_set + ## A dense scatter of river cells spanning the whole held window's canvas ## footprint (not clustered in one corner) — a genuine "does the composite diff --git a/client/tests/test_atlas_window_water_clip.gd b/client/tests/test_atlas_window_water_clip.gd new file mode 100644 index 000000000..ef16c4294 --- /dev/null +++ b/client/tests/test_atlas_window_water_clip.gd @@ -0,0 +1,368 @@ +## T-1172: pure-function tests for AtlasWindowWaterClip — the two-waterline +## clip's cell-lookup math (cell_grid_side_for_window/morphology_zone_in_window/ +## resolve_morphology_zone). Split into its own file matching this cluster's +## own "one pure-geometry file, one test file" precedent +## (test_atlas_window_geometry_nature.gd next to atlas_window_geometry.gd's +## nature-overlay additions). +class_name TestAtlasWindowWaterClip +extends GdUnitTestSuite + +const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd") + +const MORPHOLOGY_OPEN_OCEAN: int = 0 +const MORPHOLOGY_LAND: int = 8 # AlluvialPlain — any non-water zone works + + +## A window dict shaped exactly like a real DistrictWindowLayer — `center` +## as a [x,y] array (the msgpack-decoded wire shape, matching every other +## mock window in this cluster's tests), `n` districts wide, `granularity_v2` +## District (1:1 cell:district, the simplest case), and a `grid_side x +## grid_side` morphology array where `grid_side == n`. +static func _mock_district_window( + center: Vector2i, n: int, morphology: PackedByteArray +) -> Dictionary: + return { + "center": [center.x, center.y], + "n": n, + "granularity_v2": "District", + "morphology": morphology, + } + + +# ============================================================================= +# cell_grid_side_for_window — mirrors AtlasWindowOverlay.cell_grid_side_for_window() +# ============================================================================= + + +func test_cell_grid_side_district_is_n_unchanged() -> void: + var w := {"n": 32, "granularity_v2": "District"} + assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(32) + + +func test_cell_grid_side_quarter_is_n_times_four() -> void: + var w := {"n": 16, "granularity_v2": "Quarter"} + assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(64) + + +func test_cell_grid_side_region_is_n_over_hundred_rounded() -> void: + var w := {"n": 6400, "granularity_v2": "Region"} + assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(64) + + +func test_cell_grid_side_region_floors_at_one() -> void: + var w := {"n": 1, "granularity_v2": "Region"} + assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(1) + + +func test_cell_grid_side_unknown_granularity_falls_back_to_district() -> void: + var w := {"n": 32} # no granularity_v2 key at all + assert_int(AtlasWindowWaterClip.cell_grid_side_for_window(w)).is_equal(32) + + +# ============================================================================= +# morphology_zone_in_window — single-window cell lookup +# ============================================================================= + + +## A 4x4 District window (n=4, grid_side=4) centered on district (0,0), +## spanning [-2, 2) on both axes. Cell (0,0) [top-left, covering district +## x in [-2,-1), y in [-2,-1)] is water; the rest is land. +static func _mock_4x4_window() -> Dictionary: + var morphology := PackedByteArray() + morphology.resize(16) + for i in range(16): + morphology[i] = MORPHOLOGY_LAND + morphology[0] = MORPHOLOGY_OPEN_OCEAN # row 0, col 0 + return _mock_district_window(Vector2i.ZERO, 4, morphology) + + +func test_morphology_zone_in_window_reads_the_water_cell() -> void: + var w: Dictionary = _mock_4x4_window() + # District (-1.5, -1.5) falls in cell (row 0, col 0) — the water cell. + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(-1.5, -1.5), w) + assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN) + + +func test_morphology_zone_in_window_reads_a_land_cell() -> void: + var w: Dictionary = _mock_4x4_window() + # District (1.5, 1.5) falls in cell (row 3, col 3) — land. + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(1.5, 1.5), w) + assert_int(zone).is_equal(MORPHOLOGY_LAND) + + +func test_morphology_zone_in_window_outside_extent_is_no_data() -> void: + var w: Dictionary = _mock_4x4_window() + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(100.0, 100.0), w) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +func test_morphology_zone_in_window_null_window_is_no_data() -> void: + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, null) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +func test_morphology_zone_in_window_missing_morphology_array_is_no_data() -> void: + var w := {"center": [0, 0], "n": 4, "granularity_v2": "District"} + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, w) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +func test_morphology_zone_in_window_zero_n_is_no_data() -> void: + var w := { + "center": [0, 0], "n": 0, "granularity_v2": "District", "morphology": PackedByteArray() + } + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2.ZERO, w) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +## The exact top-left/bottom-right boundary districts must resolve to the +## edge cells, not silently miss due to an off-by-one in the containment +## test — [-2, 2) is half-open, so -2.0 is IN, 2.0 is OUT. +func test_morphology_zone_in_window_boundary_inclusive_at_min() -> void: + var w: Dictionary = _mock_4x4_window() + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(-2.0, -2.0), w) + assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN) + + +func test_morphology_zone_in_window_boundary_exclusive_at_max() -> void: + var w: Dictionary = _mock_4x4_window() + var zone: int = AtlasWindowWaterClip.morphology_zone_in_window(Vector2(2.0, 2.0), w) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +# ============================================================================= +# resolve_morphology_zone — the single-window / tile-mode dispatch +# ============================================================================= + + +func test_resolve_single_window_mode_reads_the_window_directly() -> void: + var w: Dictionary = _mock_4x4_window() + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(-1.5, -1.5), false, w, [], 0 + ) + assert_int(zone).is_equal(MORPHOLOGY_OPEN_OCEAN) + + +func test_resolve_single_window_mode_null_window_is_no_data() -> void: + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(Vector2.ZERO, false, null, [], 0) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +## Tile mode: two tiles, each its own 4x4 window, centered far enough apart +## that a queried district only falls inside ONE of them. +func test_resolve_tile_mode_finds_the_containing_tile() -> void: + var morph_a := PackedByteArray() + morph_a.resize(16) + for i in range(16): + morph_a[i] = MORPHOLOGY_LAND + var tile_a := { + "center": Vector2i(0, 0), "window": _mock_district_window(Vector2i.ZERO, 4, morph_a) + } + + var morph_b := PackedByteArray() + morph_b.resize(16) + for i in range(16): + morph_b[i] = MORPHOLOGY_OPEN_OCEAN + var tile_b := { + "center": Vector2i(100, 0), "window": _mock_district_window(Vector2i(100, 0), 4, morph_b) + } + + var tiles: Array = [tile_a, tile_b] + var zone_in_a: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(0.0, 0.0), true, null, tiles, 0 + ) + var zone_in_b: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(100.0, 0.0), true, null, tiles, 0 + ) + assert_int(zone_in_a).override_failure_message( + "a district position inside tile A's extent must read tile A's own cell" + ).is_equal(MORPHOLOGY_LAND) + assert_int(zone_in_b).override_failure_message( + "a district position inside tile B's extent must read tile B's own cell" + ).is_equal(MORPHOLOGY_OPEN_OCEAN) + + +func test_resolve_tile_mode_position_outside_every_tile_is_no_data() -> void: + var morph := PackedByteArray() + morph.resize(16) + var tile := {"center": Vector2i(0, 0), "window": _mock_district_window(Vector2i.ZERO, 4, morph)} + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(9999.0, 9999.0), true, null, [tile], 0 + ) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +## A tile whose window hasn't arrived yet (`window: null`, matching +## AtlasWindowTileSet.get_tiles()'s own "unarrived" shape) must be skipped, +## not crash — the scan continues to the next tile / falls through to +## MORPHOLOGY_ZONE_NO_DATA. +func test_resolve_tile_mode_skips_unarrived_tiles() -> void: + var unarrived := {"center": Vector2i(0, 0), "window": null} + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(0.0, 0.0), true, null, [unarrived], 0 + ) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +func test_resolve_tile_mode_empty_tile_list_is_no_data() -> void: + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone(Vector2.ZERO, true, null, [], 0) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +## Live round 2 fix (coordinator's trace, T-1172): a tile whose CANONICAL +## center is far from `held_center` (the seam-tile case — exactly Lendel's +## own live repro, tile canonical center 12739 drawn at draw_col=-6400) must +## have its CENTER wrapped toward `held_center_x` — mirroring +## AtlasWindowOverlay._draw_tile_mosaic()'s own `draw_col = +## nearest_wrap_image(center.x, held_center.x, cols)` EXACTLY — before +## testing containment. The query `district` is assumed ALREADY expressed in +## the held-center-wrapped frame (AtlasWindowNatureOverlay._district()'s own +## contract) and is NOT separately re-wrapped. +## +## Original (pre-fix) test asserted the INVERSE — wrapping the query toward +## the tile's raw canonical center — which was the actual bug: it happened +## to land inside the tile's CANONICAL (unwrapped) span by coincidental mod +## arithmetic, silently testing the WRONG real-world location whenever a +## tile needed wrapping to appear on screen at all. Live capture evidence: +## a river dot at district.x=-9569 sitting on the painter's WEST wrap-image +## of a seam tile (canonical center 12739, draw_col=-6400) read a real but +## wrong-location land cell under the old code, and only stopped doing so +## once resolve_morphology_zone() wrapped the TILE's center instead. +func test_resolve_tile_mode_wraps_the_tiles_own_center_toward_held_center() -> void: + var cols := 100 + var morph := PackedByteArray() + morph.resize(16) + for i in range(16): + morph[i] = MORPHOLOGY_OPEN_OCEAN + # Tile's canonical center is column 98 (far east) — but its nearest + # wrap-image to held_center=0 is column -2 (98 - 100), matching the + # Lendel seam tile's own shape (canonical 12739 -> draw_col -6400). + var tile := {"center": Vector2i(98, 0), "window": _mock_district_window(Vector2i(98, 0), 4, morph)} + # Query at column -2.5 — inside the tile's WRAP-IMAGE span [-4, 0), the + # real on-screen location, held_center-relative (the caller's own + # _district() contract) — NOT inside the canonical span [96, 100). + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(-2.5, 0.0), true, null, [tile], cols, 0 + ) + assert_int(zone).override_failure_message( + "the tile's CENTER must be wrapped toward held_center_x (mirroring the" + + " painter's draw_col computation) so a query already expressed in the" + + " held-center frame resolves against the tile's REAL on-screen wrap-image" + ).is_equal(MORPHOLOGY_OPEN_OCEAN) + + +## The INVERSE position — a query at the tile's CANONICAL (unwrapped) span — +## must NOT resolve against this tile once wrapping is applied, since that +## span is no longer where the tile actually draws relative to held_center. +## Pins that the fix doesn't just "also succeed at the old span" by accident. +func test_resolve_tile_mode_does_not_match_the_tiles_stale_canonical_span() -> void: + var cols := 100 + var morph := PackedByteArray() + morph.resize(16) + for i in range(16): + morph[i] = MORPHOLOGY_OPEN_OCEAN + var tile := {"center": Vector2i(98, 0), "window": _mock_district_window(Vector2i(98, 0), 4, morph)} + # Query at column 97 — inside the tile's CANONICAL span [96,100) — but + # that is NOT where this tile is drawn relative to held_center=0 (it's + # drawn at the wrap-image [-4,0) instead), so this must NOT resolve. + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(97.0, 0.0), true, null, [tile], cols, 0 + ) + assert_int(zone).override_failure_message( + "a query at the tile's stale CANONICAL span must not resolve against it" + + " once the tile is wrapped toward held_center — that span is not where" + + " the tile actually draws on screen" + ).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) + + +# ============================================================================= +# T-1172 round 2 (coordinator's "wire-accurate fixture" hardening ask — +# cold-start batch discipline): a fixture derived from an ACTUAL live tile +# response captured during the round-2 investigation, at the REAL wire scale +# (n=6400, grid_side=64, Region granularity) — not a hand-shrunk 4x4 mock. +# The round-2 bug (wrapping the query toward the tile instead of the tile +# toward held_center) passed EVERY test against the small mocks above, +## because those mocks never modeled a tile whose canonical center is FAR +# from held_center — the exact condition the bug needed to manifest. This +# fixture reproduces that condition at production scale, so a future +# regression of the same SHAPE (stub-and-code silently agreeing on a wrong +# convention) can't hide behind "the small tests still pass." +# ============================================================================= + + +## Lendel's own seam tile from the live drive capture that closed T-1172 +## round 2 (client/tmp_drive_clip.gd, SR_LIVE=1 against the worktree release +## server): canonical center (12739, -3200), TILE_N=6400 districts, +## Region granularity (grid_side = round(6400/100) = 64). `cols=19139` +## matches Lendel's real district_extent() circumference. The morphology +## array is NOT the real 4096-byte payload (too large to hand-author) — only +## the ONE cell index the live trace actually resolved for the +## district=-9569.414 repro query (idx=1024, col=0/row=16 — the exact +## INDEX_TRACE line from the live investigation) is given a real value; +## every other cell is left at 0 (OpenOcean), which is irrelevant here since +## this fixture exists to pin the WRAP resolution reaching the CORRECT +## tile/cell pair, not to re-verify the index math itself (already covered +## above and in test_atlas_window_geometry_nature.gd). +static func _lendel_seam_tile_fixture() -> Dictionary: + var morph := PackedByteArray() + morph.resize(4096) + morph[1024] = MORPHOLOGY_LAND # col=0, row=16 — the live-traced cell + return { + "center": Vector2i(12739, -3200), + "window": { + "center": [12739, -3200], + "n": 6400, + "granularity_v2": "Region", + "morphology": morph, + } + } + + +## The exact district position from the live capture that ORIGINALLY exposed +## the round-2 bug (district.x=-9569.414 — a dot visibly sitting on the +## painter's WEST wrap-image of the seam tile). held_center_x=0 (the +## viewer's canonical orbital-frame origin, cols=19139 (Lendel's real +## circumference in districts). Must resolve to the SAME land zone the live +## painter trace independently confirmed for this exact query. +## +## Honest note (found DURING revert-verification, worth recording): for a +## SINGLE tile in isolation, the old (query-wrapped-toward-tile) and new +## (tile-wrapped-toward-held_center) formulas are mathematically GUARANTEED +## to agree whenever local_x lands in-range for both — both reduce to +## `query - tile_center (mod cols)`, and a valid `local_x` is unique in +## `[0, n)`. This single-tile fixture therefore does NOT independently +## distinguish old from new (confirmed: it still passes with the pre-fix +## code) — it locks in the real wire-scale numbers as a realistic regression +## fixture (shared index formula, tile shape, wrap arithmetic all exercised +## together), not as the old-vs-new discriminator. The tests that DO reliably +## catch the round-2 regression are +## test_resolve_tile_mode_wraps_the_tiles_own_center_toward_held_center and +## test_resolve_tile_mode_does_not_match_the_tiles_stale_canonical_span above +## (confirmed: the latter fails by name against the reverted code) — the +## real-world bug's actual mechanism was the MULTI-TILE SCAN ORDER matching +## the WRONG tile's data before reaching the right one, not a single-tile +## formula divergence; a true multi-tile live reproduction would need the +## full 6-tile fixture, impractical to hand-author at full 4096-cell scale. +func test_wire_accurate_lendel_seam_tile_resolves_correctly() -> void: + var tile: Dictionary = _lendel_seam_tile_fixture() + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(-9569.414, -4784.793), true, null, [tile], 19139, 0 + ) + assert_int(zone).override_failure_message( + "the live T-1172 round 2 repro position must resolve against the seam" + + " tile's WRAPPED (on-screen) image and read the real traced land zone" + + " — a regression here reproduces the ORIGINAL over-ocean-dots bug" + ).is_equal(MORPHOLOGY_LAND) + + +## The SAME fixture, queried at a position that legitimately falls OUTSIDE +## even the wrapped tile's span (nowhere near either wrap-image) — must fail +## open (NO_DATA), not silently match by coincidental mod arithmetic (the +## general shape of the original bug, pinned generically here in case a +## future change reintroduces a different mod-arithmetic coincidence). +func test_wire_accurate_lendel_seam_tile_out_of_range_query_is_no_data() -> void: + var tile: Dictionary = _lendel_seam_tile_fixture() + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + Vector2(500.0, 500.0), true, null, [tile], 19139, 0 + ) + assert_int(zone).is_equal(AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA) diff --git a/client/ui/implant/apps/atlas/atlas_overlay_colors.gd b/client/ui/implant/apps/atlas/atlas_overlay_colors.gd index eff411d7c..27e2d29dd 100644 --- a/client/ui/implant/apps/atlas/atlas_overlay_colors.gd +++ b/client/ui/implant/apps/atlas/atlas_overlay_colors.gd @@ -39,6 +39,21 @@ const MORPHOLOGY_COLORS: Array = [ Color(0.25, 0.45, 0.40, 0.55), # 16 Wetland ] +## MorphologyZone discriminants that read as DRAWN WATER — T-1172 (the +## river-skeleton waterline-clip fix): the raw heightmap sea level the +## server's drainage extraction filters river cells against (drainage.rs) +## and the DERIVED morphology verdict this client actually paints (which +## includes the T-1162 coast-warp invention, and at Region rung aggregates +## to 204.8 km cells) are two independently-computed waterlines that can +## legitimately disagree — Tyre's ruling (client draw-time clip, no single +## server waterline is well-defined) reads THESE two discriminants as "drawn +## water" to suppress a river dot/mouth against. Matches D-239 SS6's own +## ordering exactly (0=OpenOcean, 1=Lake) — the only two MorphologyZone +## values that are actually open water; every other zone (TidalFlat onward) +## is drawn land or a land/water transition zone, not water itself. +const MORPHOLOGY_OPEN_OCEAN: int = 0 +const MORPHOLOGY_LAKE: int = 1 + ## Sub-biome -> marker color (Araminta's palette, D-226). Grouped pairs share ## a color since they read the same on the map. const SUB_BIOME_COLORS: Dictionary = { @@ -169,6 +184,16 @@ static func morphology_color(zone: int) -> Color: return Color(0.5, 0.5, 0.5, 0.4) +## T-1172: whether a MorphologyZone discriminant reads as drawn water — +## see MORPHOLOGY_OPEN_OCEAN/MORPHOLOGY_LAKE's own doc for the two-waterline +## rationale. An out-of-range zone (a palette/enum drift bug) is NOT water — +## matches district_window_morphology_color()'s own "unrecognized -> loud +## magenta, not silently treated as any known category" posture; an +## unrecognized zone must never silently suppress a river dot. +static func is_morphology_water(zone: int) -> bool: + return zone == MORPHOLOGY_OPEN_OCEAN or zone == MORPHOLOGY_LAKE + + static func sub_biome_color(sub_biome: String) -> Color: return SUB_BIOME_COLORS.get(sub_biome, COLOR_SUB_BIOME_DEFAULT) diff --git a/client/ui/implant/apps/atlas/atlas_window_geometry.gd b/client/ui/implant/apps/atlas/atlas_window_geometry.gd index 649458607..e6f1657fe 100644 --- a/client/ui/implant/apps/atlas/atlas_window_geometry.gd +++ b/client/ui/implant/apps/atlas/atlas_window_geometry.gd @@ -922,3 +922,33 @@ static func attractors_visible_at_rung(granularity_v2: String) -> bool: ## for every real caller and only guards a malformed test input. static func zoom_compensated_size(screen_space_size: float, view_zoom: float) -> float: return screen_space_size / maxf(view_zoom, 0.0001) + + +## T-1172 round 2 (coordinator's "reconsider the split" ask): the SHARED +## cell-index formula both AtlasWindowOverlay's terrain painter (which builds +## the drawn `grid_side x grid_side` per-cell texture — `i = row * grid_side +## + col`, `img.set_pixel(col, row, ...)`) and AtlasWindowWaterClip's clip +## predicate (which must read the EXACT SAME cell for a given position, or +## the clip silently disagrees with what's actually painted) both need. The +## live trace that closed T-1172 round 2's investigation PROVED this formula +## itself was never the bug (painter and clip independently computed the +## identical col/row/idx for the same query throughout) — the actual bug was +## in the WRAP resolution one layer up (resolve_morphology_zone()'s own doc) +## — but factoring the index math into ONE shared function here, rather than +## two independently-maintained copies (atlas_window_overlay.gd's inline +## `row * grid_side + col` vs. the old duplicate in +## atlas_window_water_clip.gd), removes the STRUCTURAL risk of a future +## divergence in exactly the way the coordinator flagged as the general +## danger class ("stub-and-code agreeing on the wrong convention" — here, +## PAINTER-and-clip could drift the same way without a shared source). +## `local_x`/`local_y` are DISTRICT-SPACE offsets from the window's own +## top-left corner (`center - n/2`), in `[0, n)` — the SAME quantity both +## call sites already compute before this function is reached. +static func cell_index_for_local_offset( + local_x: float, local_y: float, n: int, grid_side: int +) -> Vector2i: + if n <= 0 or grid_side <= 0: + return Vector2i(-1, -1) + var col: int = clampi(int(floor(local_x / float(n) * float(grid_side))), 0, grid_side - 1) + var row: int = clampi(int(floor(local_y / float(n) * float(grid_side))), 0, grid_side - 1) + return Vector2i(col, row) diff --git a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd index 27e8d8080..aba4ee391 100644 --- a/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd +++ b/client/ui/implant/apps/atlas/atlas_window_nature_overlay.gd @@ -61,6 +61,8 @@ extends Node2D const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd") const AtlasDescendGeometryRef := preload("res://ui/implant/apps/atlas/atlas_descend_geometry.gd") const AtlasOverlayColors := preload("res://ui/implant/apps/atlas/atlas_overlay_colors.gd") +# T-1172: two-waterline clip — see that file's own header doc. +const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd") ## Reused verbatim from the retired atlas_marker_overlay.gd (Araminta's ## ruling: "reuse the retired palette exactly") — same values, same source of @@ -210,11 +212,19 @@ func _zs(screen_space_size: float, ctx: Dictionary) -> float: return AtlasWindowGeometry.zoom_compensated_size(screen_space_size, ctx["view_zoom"]) -## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude -## image is nearest the currently-held view — the SAME two-step -## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single -## point instead of a tile's four corners. -func _pos(row: float, col: float, ctx: Dictionary) -> Vector2: +## Pixel (row, col) -> fractional district position, wrap-resolved against +## the currently-HELD view's own center (ctx["held_center"]) — the +## representative wrap-image _pos()'s canvas conversion needs. T-1172: this +## is also the value fed to the water-clip lookup (_is_drawn_water()) — NOT +## a separately-computed position — so the clip test and the actual drawn +## position can never disagree about which longitude wrap-image is meant. +## Tile-mode's own per-tile wrap re-resolution (AtlasWindowWaterClip. +## resolve_morphology_zone()) re-derives whichever wrap-image a SPECIFIC +## tile needs internally; feeding it this held-center-wrapped value is a +## safe, consistent starting representative either way (longitude is +## periodic — any wrap-image of the same district resolves to the same +## real-world position). +func _district(row: float, col: float, ctx: Dictionary) -> Vector2: var world_m: Vector2 = AtlasWindowGeometry.layer1_pixel_to_world_m( row, col, ctx["grid_w"], ctx["grid_h"], ctx["radius_km"] ) @@ -229,11 +239,57 @@ func _pos(row: float, col: float, ctx: Dictionary) -> Vector2: # integer rounding would otherwise discard — river dots are not # district-lattice-snapped (see world_m_to_district()'s own doc). district.x = wrapped_col + (district.x - roundi(district.x)) + return district + + +## Pixel (row, col) -> canvas-local, wrap-resolved to whichever longitude +## image is nearest the currently-held view — the SAME two-step +## (map-then-nearest-wrap) the tile mosaic draw path uses, just for a single +## point instead of a tile's four corners. Thin wrapper over _district() + +## AtlasWindowGeometry.district_to_canvas_local() (T-1172 split: callers that +## also need the water-clip test call _district() directly instead, so the +## SAME resolved district feeds both the draw position and the clip check). +func _pos(row: float, col: float, ctx: Dictionary) -> Vector2: return AtlasWindowGeometry.district_to_canvas_local( - district, ctx["held_center"], ctx["held_n"], ctx["cell_px"] + _district(row, col, ctx), ctx["held_center"], ctx["held_n"], ctx["cell_px"] ) +## T-1172: whether the composite cell covering `district` is drawn as water +## (OpenOcean/Lake) — FAILS OPEN (returns false, "not water", i.e. draw the +## dot) when no arrived composite data covers the position, per Tyre's rule +## 5 ("the clip is a presentation refinement, never a data gate"). Resolves +## through AtlasWindowWaterClip.resolve_morphology_zone(), which handles +## BOTH the single-window rung path and Region tile mode internally — this +## function never branches on viewer.is_tile_mode() itself, matching that +## function's own "single dispatch point" doc. `ctx["held_center"].x` is +## threaded through (live round 2, coordinator's trace) — tile-mode +## resolution must wrap each tile's CENTER toward held_center EXACTLY like +## AtlasWindowOverlay._draw_tile_mosaic()'s own `draw_col` computation, or +## the clip silently tests the wrong wrap-image of a seam tile (see +## resolve_morphology_zone()'s own doc for the live repro). +func _is_drawn_water(district: Vector2, ctx: Dictionary) -> bool: + var is_tile_mode: bool = viewer.is_tile_mode() + var single_window: Variant = null if is_tile_mode else viewer.get_district_window() + var tiles: Array = [] + if is_tile_mode: + var tile_set = viewer.get_tile_set() + if tile_set != null: + tiles = tile_set.get_tiles() + var held_center: Vector2i = ctx["held_center"] + var zone: int = AtlasWindowWaterClip.resolve_morphology_zone( + district, is_tile_mode, single_window, tiles, ctx["cols"], held_center.x + ) + if zone == AtlasWindowWaterClip.MORPHOLOGY_ZONE_NO_DATA: + return false + return AtlasOverlayColors.is_morphology_water(zone) + + +## T-1172 clip — retire when T-1170 course invention terminates courses at +## the invented coast. River cells, confluences, and mouths are each dropped +## (strict, no snap) when their resolved composite cell reads as drawn water +## — see AtlasWindowWaterClip's own header doc for the two-waterline +## rationale. Basins are explicitly OUT OF SCOPE (Tyre's rule 4) — untouched. func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void: var granularity_v2: String = ctx["granularity_v2"] var river_cells: Array = rn.get("river_cells", []) @@ -251,7 +307,13 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void: ) if not AtlasWindowGeometry.river_class_visible_at_rung(cls, granularity_v2): continue - var p: Vector2 = _pos(float(c[0]), float(c[1]), ctx) + var district: Vector2 = _district(float(c[0]), float(c[1]), ctx) + # T-1172 clip — retire when T-1170 course invention terminates at the invented coast. + if _is_drawn_water(district, ctx): + continue + var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local( + district, ctx["held_center"], ctx["held_n"], ctx["cell_px"] + ) if is_region: var radius: float = AtlasWindowGeometry.RIVER_DOT_RADIUS_BY_CLASS_REGION.get(cls, 2.2) draw_circle(p, _zs(radius, ctx), COLOR_GEN_RIVER) @@ -269,14 +331,30 @@ func _draw_rivers(rn: Dictionary, ctx: Dictionary) -> void: if AtlasWindowGeometry.confluences_visible_at_rung(granularity_v2): for cf: Variant in rn.get("confluences", []): if cf is Array and cf.size() >= 2: - var p: Vector2 = _pos(float(cf[0]), float(cf[1]), ctx) + var district: Vector2 = _district(float(cf[0]), float(cf[1]), ctx) + if _is_drawn_water(district, ctx): + continue + var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local( + district, ctx["held_center"], ctx["held_n"], ctx["cell_px"] + ) var radius: float = _zs(AtlasWindowGeometry.RIVER_CONFLUENCE_RADIUS_REGION, ctx) draw_circle(p, radius, COLOR_GEN_RIVER) if AtlasWindowGeometry.mouths_visible_at_rung(granularity_v2): for m: Variant in rn.get("mouths", []): if m is Array and m.size() >= 2: - _draw_mouth(_pos(float(m[0]), float(m[1]), ctx), ctx) + var district: Vector2 = _district(float(m[0]), float(m[1]), ctx) + # T-1172 rule 3: mouths are SUPPRESSED (not snapped, not + # dimmed) when their cell reads as drawn water — a mouth is + # the worst-case disagreement by construction (the last LAND + # cell on the RAW coast; wherever the drawn coast is + # displaced inland, the mouth renders offshore). + if _is_drawn_water(district, ctx): + continue + var p: Vector2 = AtlasWindowGeometry.district_to_canvas_local( + district, ctx["held_center"], ctx["held_n"], ctx["cell_px"] + ) + _draw_mouth(p, ctx) ## Double-ring sea-terminus marker — verbatim geometry from the retired diff --git a/client/ui/implant/apps/atlas/atlas_window_water_clip.gd b/client/ui/implant/apps/atlas/atlas_window_water_clip.gd new file mode 100644 index 000000000..acb5e699f --- /dev/null +++ b/client/ui/implant/apps/atlas/atlas_window_water_clip.gd @@ -0,0 +1,159 @@ +extends RefCounted + +## T-1172 — the river-skeleton waterline-clip fix. Pure geometry, split into +## its own file (not folded into atlas_window_geometry.gd, which is already +## close to the gdlint max-file-lines cap): the river skeleton's own SOURCE +## (server/src/atlas/drainage.rs) is filtered against the RAW heightmap sea +## level, but the DRAWN ocean this client actually paints is the derived +## MorphologyZone verdict (server/src/atlas/district_profile.rs) — which +## post-T-1162 includes the coast-warp invention (the drawn coastline is +## deterministically displaced from the heightmap coast) and, at Region +## rung, aggregates to 204.8 km cells. These are two independently-computed +## waterlines that can legitimately disagree; Tyre's ruling (T-1172): no +## single server waterline is well-defined, so the fix is a CLIENT draw-time +## clip against whichever composite cell is currently ON SCREEN at a given +## river dot's position — strict drop, no snap (a dot that lands on drawn +## water is simply not drawn; Region's 205 km cells may amputate a river's +## final coastal dots, an accepted cost per the ruling). T-1172 clip — +## retire when T-1170 course invention terminates courses at the invented +## coast. +## +## const AtlasWindowWaterClip := preload("res://ui/implant/apps/atlas/atlas_window_water_clip.gd") + +const AtlasWindowGeometryRef := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd") + +## Sentinel returned by the lookups below when no arrived composite data +## covers the queried position — either the position is outside every +## held/tiled window's own extent, or the window/tile at that position +## hasn't arrived yet. The caller (AtlasWindowNatureOverlay) must FAIL OPEN +## on this sentinel (draw the dot) — Tyre's rule 5: the clip is a +## presentation refinement, never a data gate. Chosen as -1 (not a legal +## MorphologyZone discriminant, which is always >= 0) so it can never be +## mistaken for a real "not water" zone. +const MORPHOLOGY_ZONE_NO_DATA: int = -1 + + +## The derived per-cell grid side length (CELLS) for a window dict `w` — a +## DELIBERATE duplicate of AtlasWindowOverlay.cell_grid_side_for_window(), +## not a shared call, matching this codebase's own "each file owns its own +## reading of a small pure lookup rather than force a dependency" precedent +## (atlas_overlay_colors.gd's header doc states this explicitly for the +## color-palette case; the SAME rationale applies here: atlas_window_overlay.gd +## already depends on atlas_window_geometry.gd, so a dependency back from +## there — or from this file, if it lived there — would risk a circular or +## at least confusing import graph). Mirrors +## server/src/atlas/layer_proxy.rs's `WindowGranularity::cell_grid_side` +## exactly, matching the canonical function's own doc byte-for-byte in intent. +static func cell_grid_side_for_window(w: Dictionary) -> int: + var n: int = int(w.get("n", 0)) + var granularity_v2 := str(w.get("granularity_v2", "District")) + match granularity_v2: + "Quarter": + return n * 4 + "Region": + return maxi(roundi(float(n) / 100.0), 1) + _: + return n + + +## Resolve a FRACTIONAL district position to the MorphologyZone discriminant +## of the composite cell covering it, for a SINGLE window dict `w` (the +## single-window rung path: District/Quarter, and each individual Region +## tile in tile mode share this same per-window shape). Returns +## MORPHOLOGY_ZONE_NO_DATA if `w` is null/malformed, has no morphology array, +## or `district` falls outside `w`'s own `[center - n/2, center + n/2)` +## extent (the SAME containment convention +## AtlasWindowGeometry.district_to_canvas_local() uses, so a position judged +## "inside" here is exactly the position that would draw as part of THIS +## window's composite on screen — no separate containment rule to drift out +## of sync with the actual paint). +static func morphology_zone_in_window(district: Vector2, w: Variant) -> int: + if not w is Dictionary: + return MORPHOLOGY_ZONE_NO_DATA + var window: Dictionary = w + var center_raw: Variant = window.get("center", [0, 0]) + var center: Vector2i = ( + Vector2i(int(center_raw[0]), int(center_raw[1])) if center_raw is Array else Vector2i.ZERO + ) + var n: int = int(window.get("n", 0)) + if n <= 0: + return MORPHOLOGY_ZONE_NO_DATA + var half: float = float(n) * 0.5 + var local_x: float = district.x - (float(center.x) - half) + var local_y: float = district.y - (float(center.y) - half) + if local_x < 0.0 or local_x >= float(n) or local_y < 0.0 or local_y >= float(n): + return MORPHOLOGY_ZONE_NO_DATA + var morphology: Variant = window.get("morphology") + if not (morphology is PackedByteArray or morphology is Array): + return MORPHOLOGY_ZONE_NO_DATA + var grid_side: int = cell_grid_side_for_window(window) + if grid_side <= 0: + return MORPHOLOGY_ZONE_NO_DATA + # T-1172 round 2: SHARED index formula with the terrain painter + # (AtlasWindowGeometry.cell_index_for_local_offset() — see its own doc + # for why this is now factored out instead of duplicated). + var cell: Vector2i = AtlasWindowGeometryRef.cell_index_for_local_offset( + local_x, local_y, n, grid_side + ) + var idx: int = cell.y * grid_side + cell.x + if idx < 0 or idx >= morphology.size(): + return MORPHOLOGY_ZONE_NO_DATA + return int(morphology[idx]) + + +## Resolve a fractional district position to a MorphologyZone discriminant +## across BOTH viewer modes — the single dispatch point +## AtlasWindowNatureOverlay's clip predicate calls, so it never needs its own +## is_tile_mode() branch. Single-window mode: one direct +## morphology_zone_in_window() call against `single_window`. Tile mode: +## linear scan of `tiles` (Array of {"center": Vector2i, "window": Variant}, +## AtlasWindowTileSet.get_tiles()'s own shape) for whichever tile's ON-SCREEN +## extent contains the position — each tile's OWN echoed `window["n"]` is +## used for the actual containment test (not TILE_N assumed), matching this +## cluster's "the response is the source of truth for what it actually +## contains" precedent, since a clamped/still-arriving tile's real extent +## can differ from the nominal per-tile request size. +## +## **Live round 2 fix (coordinator's trace, T-1172):** the wrap resolution +## MUST mirror AtlasWindowOverlay._draw_tile_mosaic()'s own +## `draw_col = nearest_wrap_image(center.x, held_center.x, cols)` EXACTLY — +## wrap the TILE'S OWN CENTER toward `held_center` (the viewer's currently- +## displayed reference frame), then test the (already held-center-wrapped) +## query `district` against that RESOLVED center. The original version did +## the inverse — wrapped the QUERY toward the tile's raw CANONICAL center — +## which is not the same operation and silently tested containment against +## the WRONG wrap-image of the tile for any tile whose canonical center is +## far from `held_center` (i.e. any tile that needs wrapping to appear +## on-screen at all — confirmed live: a dot at district.x=-9569 visibly +## sitting on the painter's WEST wrap-image of the seam tile +## (canonical center 12739, drawn at draw_col=-6400) was tested by the old +## code against that tile's EAST/canonical span `[9539, 15939)` instead — +## landed inside it by coincidence (mod arithmetic), read a real but +## WRONG-LOCATION land cell, and never clipped). `district.x` is assumed +## ALREADY wrap-resolved near `held_center` by the caller (AtlasWindowNatureOverlay. +## _district()'s own contract) — this function does not re-wrap it, only the +## tile centers, exactly mirroring the painter's own asymmetry (the painter +## never wrap-resolves the query either — canvas-local coordinates are +## already in the held-center frame by construction). +static func resolve_morphology_zone( + district: Vector2, is_tile_mode: bool, single_window: Variant, tiles: Array, cols: int, + held_center_x: int = 0 +) -> int: + if not is_tile_mode: + return morphology_zone_in_window(district, single_window) + for tile: Dictionary in tiles: + var window: Variant = tile.get("window") + if not window is Dictionary: + continue + var tile_center: Vector2i = tile.get("center", Vector2i.ZERO) + var draw_col: int = tile_center.x + if cols > 0: + draw_col = AtlasWindowGeometryRef.nearest_wrap_image(tile_center.x, held_center_x, cols) + var effective_window: Dictionary = window + if draw_col != tile_center.x: + effective_window = (window as Dictionary).duplicate() + effective_window["center"] = [draw_col, tile_center.y] + var zone: int = morphology_zone_in_window(district, effective_window) + if zone != MORPHOLOGY_ZONE_NO_DATA: + return zone + return MORPHOLOGY_ZONE_NO_DATA diff --git a/governance/decisions/architecture.md b/governance/decisions/architecture.md index 33cbc7cab..779d10943 100644 --- a/governance/decisions/architecture.md +++ b/governance/decisions/architecture.md @@ -1669,7 +1669,7 @@ Technical foundation decisions that constrain implementation: engine, client-ser **Amended 2026-07-21 (T-1145 — Jeroen, second companion hands-on, KALLAST window):** three regional-window presentation fixes, all client-only. **Cover-fit supersedes contain:** `fit_window_view()`'s zoom now derives from the LARGER viewport dimension with no margin factor (`max(viewport.x, viewport.y) / composite_native`, not the old `0.9 * min(...)`), so the square district-window composite fills a wide/tall viewport edge to edge instead of leaving side margins, with the shorter axis' data extending into pan-space (the same "cover" concept as CSS `object-fit: cover`) — the existing §4 pan-edge refetch is unaffected (it keys off the screen-center-to-DistrictPos mapping, which any fit already centers on `_held_center` by construction, so no refetch churn at rest). **WASD + edge-scroll supersedes drag-pan:** LMB-drag panning is removed entirely (Jeroen's ruling — drag conflicts with click semantics for the map objects, e.g. settlements, this window will host later); panning is now held WASD/arrow keys (continuous, frame-rate-independent, `_process`-polled, physical-keycode reads to stay independent of the project's existing `move_north`/etc. gameplay-movement InputMap actions bound to the same keys) plus edge-scrolling (cursor within ~24px of a viewport edge, suppressed over UI and while the OS window lacks focus); wheel zoom is unchanged; pole-wall (§5 amendment above) and east-west wrap (T-1142) semantics are preserved unchanged under the new input source. **Smoothing is an interim presentation, pending T-1143:** the composite renders as an `n`×`n` `Image`/`ImageTexture` (one pixel per district, the identical existing per-cell color pipeline) drawn scaled with linear filtering — the same treatment the planetary heightmap already gets — instead of `n`×`n` flat rects, so GPU bilinear sampling reads as a terrain gradient rather than hard blocks; the original crisp per-cell path survives behind a compile-time const specifically so T-1143's design pass can compare both directly, and this smoothing is **not** T-1143's answer to district-tier legibility, only a stopgap ahead of it. - **Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed). **Pending-shape protocol note (T-1163, PR #193 review — Tyre):** `AtlasLayerResponse` has **two legal wire shapes for one logical "still deriving, client must re-poll" state**, and both are contract: whole-response `status: Pending` (whole-body cache cold — nothing about this body computed yet) and `status: Ready` with `district_window: null` (body warm, this window still in the derive queue). `Ready` is set **only** by the whole-body cache-hit branch, independent of the window's own derivation. Any `AtlasLayerResponse` consumer must treat BOTH shapes as retry-with-backoff and only `NotFound`/`Error` as terminal — the T-1163 cold-launch starvation (every first launch black) was precisely a client reading `status != Ready` as ignorable. A server refactor that "cleans up" the Pending/Ready-null asymmetry must migrate every consumer in the same change. **Filter-axis note (T-1161, PR #194 review — Tyre):** COMPOSITE_SMOOTH is retained as the compile-time *pipeline* axis (texture vs. per-cell rects, crisp path kept for debug/compare); the ladder's crispness-at-sparse-rungs requirement is met by the per-rung *sampling-filter* policy (`_filter_for_granularity_v2`: Region/orbital-mosaic NEAREST, District/Quarter LINEAR, unknown falls back LINEAR), **not** by deleting the const. The design pass §8 step 6 "retire COMPOSITE_SMOOTH" is errata'd accordingly; T-1155's retirement framing is cancelled, superseded by T-1161. **Wave-1 nature-overlay carrier note (T-1156, 2026-07-23 — Tyre):** river skeletons (rivers/basins/attractors) ride the Atlas zoom ladder on the **existing whole-body `layer1` field** (`RiverNetwork`/`drainage_basins`/`attractors`, already serialized on every `AtlasLayerResponse`), **not** on the windowed `district_window` carrier — so the windowed-family ceiling (§2, [HARD], exactly one windowed field) is untouched and no tagged-envelope migration is triggered. Rationale: the skeleton is discrete vector geometry ((u16,u16) cell chains, boundary polylines, point attractors), computed once per body in the Layer-1 pass and cached "valid forever" (D-227) — categorically the whole-body family, not a per-pan viewport query. Rasterizing rivers into `district_window`'s per-cell arrays is rejected: a 1-cell-wide thalweg is sub-cell at every ladder rung (512 m/cell and coarser), so a presence-byte either over-fattens (the Lendel failure the ladder mandate kills) or drops the river on the sampling grid. **Per-rung refinement is client-side** (trunk at Region → +tributaries at District → +streams at Quarter), a filter on a **new quantized `river_class` per cell added to `RiverNetwork`** (derived from the flow-accumulation `drainage.rs` already computes; additive, `#[serde(default)]`-safe, no ceiling impact) — NOT server-side per-rung re-transmission. This is distinct from T-1162's server-side `min_wavelength_m` cutoff: that truncates a continuous per-metre field at the rung's Nyquist limit; the skeleton is a fixed finite graph with nothing to truncate. **General rule established:** discrete map features (linear + point geometry) ride the whole-body overlay family, filtered per-rung client-side; only continuous per-metre fields ride the windowed per-cell arrays — Wave 2's roads/rail/settlements (already whole-body fields) inherit this carrier unchanged. **Consistency scope:** the river skeleton is upstream of and independent from T-1162's perturbed `moisture_q` (drainage runs on elevation, never samples moisture), so the two cannot disagree; the vegetation layer's riparian response to rivers (`near_perennial_water`) is a **named forward contract, deferred to T-1168** — not fixed in Wave 1. Until it lands, the river overlay draws over terrain whose vegetation layer does not yet respond to it (an accepted nature-layer-first gap). **Visibility-direction note (same PR, Araminta):** the per-rung visibility DIRECTION is Araminta's presentation ruling on the 76 km skeleton-resolution evidence — **fade-down** (Region shows the full skeleton, District trunk-only de-emphasized, Quarter off), consciously inverting the provisional add-as-you-descend mapping the ticket brief carried. This posture is explicitly **pre-T-1170**: it is revisited (in `RIVER_CLASS_VISIBLE_BY_RUNG`, the single client-side revisit point) when course invention gives finer rungs real geometry to reveal. + **Amended 2026-07-21 (T-1143 design-pass rulings — Jeroen, after the zoom-ladder design pass, `docs/architecture/atlas-zoom-ladder-t1143.md`):** three rulings on the pass's reserved decisions. **(1) The item-(d) ceiling is opened for the Atlas ladder** — Jeroen: *"we set a new BHAG so old restrictions are up for debate."* The D-166 2026-07-21 zoom-ladder condition ("down to tile scale") is read **literally**: the Atlas windowed viewport may descend below quarter (512 m) toward block/tile granularity. This is an explicit ruling, not erosion — exactly the deliberate revisit the T-1112 §2 anti-erosion clause was hardened to force into the open. Item (d)'s substance survives in narrowed form: chunk/tile/voxel output still never appears as a **whole-body planetary map layer**, and the below-quarter rungs are implementation-gated on their own **measurement pass** (costs/wire for block and tile rungs are unmeasured — design pass §2/§7); the harness-verification path for L5 fill remains primary until that pass lands. **(2) Planetary rung wire carrier: progressive capped-density tiling** riding the generalized `district_window` carrier (granularity parameter, §3 of the design pass) — no new dense-raster wire shape, no forced tagged-envelope migration. **(3) The §5 entry click-through cut is superseded by continuous cursor-anchored zoom**: wheel-zoom carries the view from the orbital frame down through regional granularities continuously, anchored at the cursor, **with the condition that a full zoom-out resets to the original canonical planetary frame and location** (the fixed orbital framing is the ladder's top rest state, not a drifted pan state). The click-through descent and rectangle reticle are retired as the *sole* entry (T-1138's shipped mechanic stands until the continuous ladder replaces it in the same change — close inspection is never stranded, same discipline as the §5 fixed-view transition). D-013's "the zoom gesture owns spatial descent" reading is **restored** for this seam. **Wire-contract note (T-1150, PR #191 review — Tyre):** the `window_granularity` field that ruling (2) rides on expresses **finer-than-district integer multiples only** (1 = district, 4 = quarter today; each new rung is a deliberate widening of `resolve_window_granularity`'s whitelist — the single widening point; unknown values fall back to district, never trusted from the wire). Coarser-than-district reuse (the region/orbital rungs of ruling (2)'s progressive tiling) requires the design pass's R5 signed/log-scale-or-enum redesign of the field — a new magic value is not the path. Recorded here so the type's limit is contract, not only a design-doc risk row. **Refinement-semantics note (T-1153, PR #192 review — Tyre):** the ladder's progressive cross-rung refinement (hold the coarse composite, fetch the finer rung, swap in place on arrival; per-tile arrival in the orbital mosaic) **extends** the T-1124 §4 float-on-center/debounce async contract — it does not supersede it. §4 still governs the per-request mechanics unchanged (`district_window: None`-until-derived polling, the 150 ms debounce, float-on-center refetch); rung crossings add a second request class on top, per the design pass §3's progressive-refinement model. This record is the one that governs the swap-on-arrival behavior. The legacy `window_granularity: u32` wire field is now fully shadowed by `window_granularity_v2` (the server always echoes both); it is **scheduled for retirement** once pre-T-1152 wire-compat is confirmed unneeded (single-repo client/server pair — no external clients exist today; ticketed). **Pending-shape protocol note (T-1163, PR #193 review — Tyre):** `AtlasLayerResponse` has **two legal wire shapes for one logical "still deriving, client must re-poll" state**, and both are contract: whole-response `status: Pending` (whole-body cache cold — nothing about this body computed yet) and `status: Ready` with `district_window: null` (body warm, this window still in the derive queue). `Ready` is set **only** by the whole-body cache-hit branch, independent of the window's own derivation. Any `AtlasLayerResponse` consumer must treat BOTH shapes as retry-with-backoff and only `NotFound`/`Error` as terminal — the T-1163 cold-launch starvation (every first launch black) was precisely a client reading `status != Ready` as ignorable. A server refactor that "cleans up" the Pending/Ready-null asymmetry must migrate every consumer in the same change. **Filter-axis note (T-1161, PR #194 review — Tyre):** COMPOSITE_SMOOTH is retained as the compile-time *pipeline* axis (texture vs. per-cell rects, crisp path kept for debug/compare); the ladder's crispness-at-sparse-rungs requirement is met by the per-rung *sampling-filter* policy (`_filter_for_granularity_v2`: Region/orbital-mosaic NEAREST, District/Quarter LINEAR, unknown falls back LINEAR), **not** by deleting the const. The design pass §8 step 6 "retire COMPOSITE_SMOOTH" is errata'd accordingly; T-1155's retirement framing is cancelled, superseded by T-1161. **Wave-1 nature-overlay carrier note (T-1156, 2026-07-23 — Tyre):** river skeletons (rivers/basins/attractors) ride the Atlas zoom ladder on the **existing whole-body `layer1` field** (`RiverNetwork`/`drainage_basins`/`attractors`, already serialized on every `AtlasLayerResponse`), **not** on the windowed `district_window` carrier — so the windowed-family ceiling (§2, [HARD], exactly one windowed field) is untouched and no tagged-envelope migration is triggered. Rationale: the skeleton is discrete vector geometry ((u16,u16) cell chains, boundary polylines, point attractors), computed once per body in the Layer-1 pass and cached "valid forever" (D-227) — categorically the whole-body family, not a per-pan viewport query. Rasterizing rivers into `district_window`'s per-cell arrays is rejected: a 1-cell-wide thalweg is sub-cell at every ladder rung (512 m/cell and coarser), so a presence-byte either over-fattens (the Lendel failure the ladder mandate kills) or drops the river on the sampling grid. **Per-rung refinement is client-side** (trunk at Region → +tributaries at District → +streams at Quarter), a filter on a **new quantized `river_class` per cell added to `RiverNetwork`** (derived from the flow-accumulation `drainage.rs` already computes; additive, `#[serde(default)]`-safe, no ceiling impact) — NOT server-side per-rung re-transmission. This is distinct from T-1162's server-side `min_wavelength_m` cutoff: that truncates a continuous per-metre field at the rung's Nyquist limit; the skeleton is a fixed finite graph with nothing to truncate. **General rule established:** discrete map features (linear + point geometry) ride the whole-body overlay family, filtered per-rung client-side; only continuous per-metre fields ride the windowed per-cell arrays — Wave 2's roads/rail/settlements (already whole-body fields) inherit this carrier unchanged. **Consistency scope:** the river skeleton is upstream of and independent from T-1162's perturbed `moisture_q` (drainage runs on elevation, never samples moisture), so the two cannot disagree; the vegetation layer's riparian response to rivers (`near_perennial_water`) is a **named forward contract, deferred to T-1168** — not fixed in Wave 1. Until it lands, the river overlay draws over terrain whose vegetation layer does not yet respond to it (an accepted nature-layer-first gap). **Visibility-direction note (same PR, Araminta):** the per-rung visibility DIRECTION is Araminta's presentation ruling on the 76 km skeleton-resolution evidence — **fade-down** (Region shows the full skeleton, District trunk-only de-emphasized, Quarter off), consciously inverting the provisional add-as-you-descend mapping the ticket brief carried. This posture is explicitly **pre-T-1170**: it is revisited (in `RIVER_CLASS_VISIBLE_BY_RUNG`, the single client-side revisit point) when course invention gives finer rungs real geometry to reveal. **Two-waterline note (T-1172, 2026-07-23 — Tyre):** the river skeleton is extracted against the **raw heightmap sea level** (`drainage.rs`), a rung-independent graph; the drawn coast is the **derived morphology verdict** — which is **rung-dependent by construction** (the coast-warp crinkle, `coast_invention.rs`, adds octaves at finer rungs, so the drawn coastline is a *family* of curves indexed by rung, not a single curve). There is therefore **no single authoritative server-side waterline** to reconcile the skeleton against — a server-side classification would bake one rung's coast into the wire and be wrong at every other rung. Reconciliation is a **presentation-frame** operation: the draw site clips river dots/confluences/mouths against the arrived composite's per-cell water verdict at the rung being painted (drop-in-water, no snap). The skeleton stays rung-independent (its correct nature per the carrier note); the clip is retired into T-1170 when course invention terminates courses at the invented coast with continuous geometry. - **Rationale:** Reusing the real UI — rather than a parallel offline renderer or dumped files — means the debug/review surface never diverges from what ships, and a dropped artifact can't go stale. Agent-navigability converts qualitative "does the synthesis look natural?" review from a manual eyeball pass into an automatable sweep that flags the few outliers for a human. The harness rides seams that already exist (`TickRate::Paused`, the paused-allowlist, `gameplay_occluded`, the bridge framing, the `run-visual` capture primitive) — a naming-and-contract exercise, not a new subsystem. - **New surface:** server pause-gating (run-conditions on the world phases keyed to a pause command); client `AtlasAgentInterface` (`observe`/`act`, Control-tree walker) + its local transport; the generation overlay rendering + selector + legend; interactive capture wired to `run-visual`. - **Implementation:** Phase 4 (epic T-750), built bottom-up — auto-pause substrate, T-969 proxy (D-225), T-960 viewer, agent channel, agent capture. Geography is the first consumer.