260 lines
14 KiB
Markdown
260 lines
14 KiB
Markdown
# 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`.
|