# Terrain Query

## Metadata

| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001; M03 renderer consumer `M03-RND-FACADE-GROUND-QUERY-001` |
| Owners | Gameplay terrain-query contract; ADT adapter owns parsed tile cache |
| Last verified | `a45d521`, 2026-07-14 |
| Profiles/capabilities | Current sandbox ADT ground height; loaded-render-mesh diagnostic query; authoritative physics backend planned |

## Purpose

Provide a replaceable, scene-free ground-height query for player movement.
Isolate native ADT loading, per-tile caching, chunk selection and legacy outer
height-grid interpolation from the player scene behind a typed
`GodotWorldPosition → TerrainGroundSample` contract.

## Non-goals

- Own player transforms, movement velocity or ground-snap policy.
- Provide collision normals, slopes, holes, liquids, WMO/M2 collision or ray queries.
- Change ADT topology, coordinate conversion, renderer caches or native parser format.
- Claim original-client terrain-contact parity from height regression alone.
- Couple gameplay to the monolithic streaming renderer before a public backend exists.

## Context and boundaries

```mermaid
flowchart LR
    Scene[ThirdPersonWowController adapter] --> Position[GodotWorldPosition]
    Position --> Contract[TerrainQuery]
    Contract --> Adapter[AdtTerrainQuery]
    Adapter --> Mapper[CoordinateMapper]
    Adapter --> Native[ADTLoader]
    Native --> Tile[ADT tile Dictionary]
    Tile --> Cache[Owned tile cache]
    Cache --> Sample[TerrainGroundSample]
    Sample --> Scene
    Scene --> Snap[Existing ground-snap policy]
```

Allowed dependencies:

- Typed `GodotWorldPosition` and `CoordinateMapper` contracts.
- Existing native `ADTLoader` at the ADT adapter boundary.
- Dynamic dictionaries only inside the native-format adapter.
- Player scene composition may inject any `TerrainQuery` implementation.
- Renderer diagnostics reuse `GodotWorldPosition` through `WorldRenderFacade`
  but return renderer-owned `RenderedGroundSample`, preserving the render/gameplay boundary.

Forbidden dependencies:

- `TerrainQuery` contract depending on Nodes, cameras, input or renderer state.
- Player scene loading/parsing ADT data or owning a parsed tile cache.
- Manual WoW/Godot/ADT world-coordinate conversion outside `CoordinateMapper`.
- Terrain query mutating transforms, gameplay movement state or visual meshes.

## Public API

| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `TerrainGroundSample.available(height_units)` | Immutable value factory | Creates a finite Godot-world ground height | Any thread; caller-held value | Non-finite height becomes unavailable |
| `TerrainGroundSample.unavailable(failure_code)` | Immutable value factory | Creates explicit unavailable result | Any thread; caller-held value | Empty code normalizes to `terrain_unavailable` |
| `TerrainQuery.sample_ground_height(position)` | Replaceable boundary | Samples ground at typed Godot world position | Implementation-defined; current adapter main-thread owned | Base reports `terrain_query_not_implemented` |
| `AdtTerrainQuery.new(extracted_directory, map_directory_name, loader_override)` | Adapter constructor | Configures one map and optional test/data-source loader | Query lifetime | Production validates native loader and file presence lazily |
| `AdtTerrainQuery.sample_ground_height(position)` | Adapter method | Loads/caches tile and interpolates ADT outer heights | Main thread; cache lives with query | Returns stable failure code |
| `ThirdPersonWowController.set_terrain_query(query)` | Composition method | Replaces terrain backend independently of movement/presentation | Call before scene ready or on main thread | Null is rejected and current query retained |
| `WorldRenderFacade.sample_ground_height(position)` | Separate renderer diagnostic API | Samples already loaded terrain Mesh and returns `RenderedGroundSample` without exposing streamer state | Main thread; immutable sample | Stable `render_terrain_*` unavailable code; not injected into player by default |

## Inputs and outputs

| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `GodotWorldPosition` | Player scene adapter | `TerrainQuery` | Immutable caller-held value | One query |
| Input | Extracted directory/map name | Scene composition | `AdtTerrainQuery` | Copied strings | Query lifetime |
| Input | ADT file/dictionary | Filesystem/native loader or injected loader | `AdtTerrainQuery` | Adapter-owned cached dictionary | Query lifetime |
| Output | `TerrainGroundSample` | Terrain query | Player ground-snap adapter | Immutable caller-held value | One query |
| Output | Failure code | Terrain query | Diagnostics/tests | Immutable `StringName` | Sample lifetime |

Side effects:

- Production adapter performs lazy file existence check and native ADT parse on first tile query.
- Adapter mutates only its per-query tile/failure caches.
- No scene tree, transform, renderer RID, network, database or persisted setting mutation occurs.

## Data flow

```mermaid
flowchart LR
    Position[GodotWorldPosition] --> TileCoord[CoordinateMapper tile/local/chunk]
    TileCoord --> Lookup{Tile cached?}
    Lookup -->|no| Load[ADTLoader or injected loader]
    Load --> Cache[Cache Dictionary or unavailable result]
    Lookup -->|yes| Cache
    Cache --> Chunk[Select MCNK by origin]
    Chunk --> Heights[Validate 145 heights]
    Heights --> Bilinear[Interpolate 9x9 outer grid]
    Bilinear --> Available[TerrainGroundSample available]
    Cache -->|missing/invalid| Unavailable[TerrainGroundSample unavailable]
```

## Lifecycle/state

```mermaid
stateDiagram-v2
    [*] --> TileUnknown
    TileUnknown --> TileAvailable: first query / successful load
    TileUnknown --> TileUnavailable: first query / missing or failed
    TileAvailable --> TileAvailable: cached queries
    TileUnavailable --> TileUnavailable: cached queries
    TileAvailable --> [*]: query released
    TileUnavailable --> [*]: query released
```

Unavailable tiles are cached intentionally to avoid repeated filesystem/native
work each physics tick. Recovery after files appear requires replacing the query
instance; hot-reload invalidation is outside this M02 package.

## Main sequence

```mermaid
sequenceDiagram
    participant Player as ThirdPersonWowController
    participant Query as TerrainQuery
    participant ADT as AdtTerrainQuery
    participant Mapper as CoordinateMapper
    participant Native as ADTLoader
    Player->>Query: sample_ground_height(GodotWorldPosition)
    Query->>ADT: implementation dispatch
    ADT->>Mapper: tile/local/chunk coordinates
    alt tile not cached
        ADT->>Native: load_adt(absolute path)
        Native-->>ADT: ADT Dictionary
    end
    ADT->>ADT: select chunk and interpolate heights
    ADT-->>Player: TerrainGroundSample
    Player->>Player: apply existing offset/snap if available
```

## Ownership, threading and resources

- Player scene owns the active `TerrainQuery` reference.
- `AdtTerrainQuery` exclusively owns tile/failure dictionaries until released.
- Current production calls and cache mutations occur on the main physics thread.
- Native loader result dictionaries do not cross the adapter boundary.
- Samples and typed positions are immutable caller-held values.
- The module creates no Nodes, Resources, RIDs or background jobs.
- Future worker-backed queries require an explicit async/result contract rather
  than concurrent mutation of the current dictionaries.

## Errors, cancellation and recovery

| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Base contract used directly | Base method | Unavailable sample | `terrain_query_not_implemented` | Inject implementation |
| Native loader absent | ClassDB check | Cache unavailable tile | `adt_loader_unavailable` | Load extension and replace query |
| ADT file absent | File check | Cache unavailable tile | `adt_source_missing` | Restore corpus and replace query |
| Native parse empty/fails | Result validation | Cache unavailable tile | `adt_load_failed` | Inspect source/parser; replace query |
| Injected loader returns wrong type | Type check | Cache unavailable tile | `adt_loader_result_invalid` | Fix adapter |
| Chunk absent | Chunk lookup | Unavailable sample | `adt_chunk_missing` | Validate ADT dictionary |
| Height array shorter than 145 | Size check | Unavailable sample | `adt_chunk_heights_invalid` | Fix parser/source |
| Non-finite interpolated height | Sample invariant | Unavailable sample | `terrain_height_not_finite` | Inspect source values |
| Null query injection | Scene guard | Existing query retained | Player composition error | Pass valid query |

There are no asynchronous operations to cancel. Replacing/releasing the query
is the deterministic cache-reset and recovery path.

## Configuration and capabilities

| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Extracted directory | Player `extracted_dir` | Current sandbox | Captured at creation | ADT source root |
| Map directory name | Player `map_name` | Current sandbox | Captured at creation | ADT filename/path |
| Loader override | Invalid callable | Test/custom data source | Constructor only | Bypasses production ClassDB/file loading |
| Tile cache | Enabled | All current uses | Cleared by replacing query | Avoids repeated parse/failure work |
| Height policy | 9x9 outer-grid bilinear | Current sandbox | Fixed | Preserves pre-extraction behavior |

## Persistence, cache and migration

The cache is in-memory and unversioned because it holds native loader output for
one query lifetime. No disk cache, schema or migration changes are introduced.
Existing ADT/native/cache format versions remain unchanged.

## Diagnostics and observability

- Logs: dedicated verifier emits `TERRAIN_QUERY PASS` or named failures.
- Structured status: every unavailable sample has a stable `failure_code`.
- Metrics: synthetic verifier counts loader calls to prove one load per tile.
- Debug views: none.
- Correlation IDs: not applicable to synchronous local query.

## Verification

- Contract: available/unavailable/base/non-finite result invariants.
- Pure synthetic adapter: typed coordinate mapping, exact chunk selection,
  145-height layout and bilinear interpolation within `0.002` units (Vector3
  source precision), plus cached second query.
- Failures: empty tile and invalid height array produce stable codes.
- Integration: asset-free player scene accepts injected flat query and preserves
  spawn offset `0.05` plus physics ground snap.
- Boundary: source regression rejects ADT loader/cache/chunk helpers in player.
- Fidelity evidence: existing interpolation and snap behavior only; no
  original-client or collision parity claim.
- Performance budget: one dictionary cache lookup and scalar interpolation per
  cached query; native parse only on first tile access.

## Extension points

- Streaming renderer or physics backend may implement `TerrainQuery` without changing player/movement.
- The current facade intentionally uses a separate renderer-owned result and is
  not a `TerrainQuery` implementation. A future adapter must explicitly translate
  readiness/results and define gameplay authority before composition.
- Future result may grow separate slope, surface, liquid or collision contracts;
  incompatible additions require a versioned contract rather than Dictionary fields.
- Test/authoring data sources may use constructor loader override.
- Async streaming requires a separate availability/update model.

## Capability status

| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Typed replaceable ground-height query | Implemented | Contract and injected player regression | Integrate target checklist after review |
| Existing ADT height interpolation | Implemented | Synthetic fractional-grid fixture | Compare terrain contact with build 12340 |
| Per-tile success/failure cache | Implemented | Loader call-count regression | Add explicit invalidation only with authoring/hot reload |
| Renderer loaded-mesh diagnostic backend | Implemented | M03 facade typed sample and detached snapshot contract | Not composed into gameplay; triangle Mesh ray is diagnostic only |
| Authoritative renderer/physics backend | Planned | Boundary permits replacement | Define holes/slopes/collision/readiness semantics before gameplay composition |
| Holes/slopes/collision | Planned | Outside height-only contract | Later movement/physics package |
| Liquid/swim query | Planned | Outside contract | M09/M12 world gameplay |

## Known gaps and risks

- Bilinear outer-grid height is the inherited sandbox approximation; exact ADT
  triangle choice, holes and collision are not represented.
- Query is synchronous; first access may parse an ADT on the main thread exactly
  as before extraction. A streaming/backend replacement is future work.
- Unavailable results remain cached until query replacement.
- Player still owns ground offset/snap policy; only data access is extracted.
- Existing native dictionaries remain dynamic inside the adapter.

## Source map

| Path | Responsibility |
|---|---|
| `src/gameplay/terrain/terrain_ground_sample.gd` | Immutable available/unavailable result |
| `src/gameplay/terrain/terrain_query.gd` | Replaceable typed query boundary |
| `src/gameplay/terrain/adt_terrain_query.gd` | Native ADT adapter, cache, chunk selection and interpolation |
| `src/scenes/player/third_person_wow_controller.gd` | Query composition and existing ground-snap consumer |
| `src/render/world_render_facade.gd` | Read-only loaded-render-mesh diagnostic consumer of typed position/sample contracts |
| `src/tools/verify_terrain_query.gd` | Synthetic contract/cache/failure/injection regression |
| `src/tests/scenes/player_input_regression.tscn` | Asset-free player composition fixture |

## Related decisions and references

- ADR: none; no coordinate, format or authority decision changed.
- Upstream/reference: `targets/02-player-decomposition.md`,
  `targets/roadmap/02-rendering-and-graphics.md`,
  `targets/roadmap/04-gameplay-systems.md`, `docs/ARCHITECTURE.md`.
