Work-Package: M02-GMP-TERRAIN-QUERY-001 Agent: sindo-main-codex Tests: terrain query, player input and movement regressions; asset-free scene; renderer dry-run; coordinate, streaming, documentation and coordination gates Fidelity: preserves existing ADT outer-grid interpolation and player ground snap; build-12340 collision parity remains unverified
13 KiB
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
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
GodotWorldPositionandCoordinateMappercontracts. - Existing native
ADTLoaderat the ADT adapter boundary. - Dynamic dictionaries only inside the native-format adapter.
- Player scene composition may inject any
TerrainQueryimplementation.
Forbidden dependencies:
TerrainQuerycontract 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
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
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
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
TerrainQueryreference. AdtTerrainQueryexclusively 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 PASSor 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.002units (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.05plus 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
TerrainQuerywithout 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.