Files
open-wc/docs/modules/terrain-query.md
T
sindoring 0ce3cae208 docs(M02): publish terrain query handoff
Work-Package: M02-GMP-TERRAIN-QUERY-001
Agent: sindo-main-codex
Tests: coordination and documentation gates; diff check
Fidelity: documents inherited height/snap regression and remaining collision evidence gap
2026-07-14 23:11:23 +04:00

252 lines
13 KiB
Markdown

# 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 | `a45d521`, 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`.