# Terrain Query ## Metadata | Field | Value | |---|---| | Status | Implemented | | Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001 | | Owners | Gameplay terrain-query contract; ADT adapter owns parsed tile cache | | Last verified | `working tree`, 2026-07-14 | | Profiles/capabilities | Current sandbox ADT ground height; renderer/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. 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 | ## 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. - 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/streaming cache backend | Planned | Boundary permits replacement | M03 renderer facade/public terrain backend | | 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/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`.