fnd(M01): add canonical coordinate mapper
Work-Package: M01-FND-COORDS-001 Agent: sindo-main-codex Tests: coordinate mapper, M00 coordinate calibration, baseline manifest, coordination and documentation gates passed Fidelity: five build 12340 camera points map within 0.002 yard; no visual parity claim
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
# ADR 0001: Canonical world coordinate contract
|
||||
|
||||
- Status: Accepted for M01 contract implementation
|
||||
- Date: 2026-07-13
|
||||
- Work package: `M01-FND-COORDS-001`
|
||||
- Decision owners: M01 foundation/integration owners
|
||||
|
||||
## Context
|
||||
|
||||
OpenWC currently performs coordinate conversion in native ADT/WDT loaders,
|
||||
`StreamingWorldLoader`, sky, player and renderer diagnostic scripts. The same
|
||||
numbers are frequently carried in `Vector3` or dictionaries, so the coordinate
|
||||
space is implicit. Existing names also mix ADT filename indices, renderer X/Z
|
||||
indices and MDDF/MODF placement fields.
|
||||
|
||||
M00 recorded five positions observed in WoW 3.3.5a build 12340. They verify the
|
||||
current client-position to Godot-position arithmetic within `0.002` yard, but
|
||||
they do not define a cross-layer contract and do not prove camera composition
|
||||
or visual parity.
|
||||
|
||||
Independent references agree on a 64 by 64 ADT grid and an approximately
|
||||
`533.3333` yard tile size. They do not all use the word “canonical” for the same
|
||||
axis order. OpenWC therefore needs names tied to observable formats instead of
|
||||
adopting an internal convention from one reference project.
|
||||
|
||||
## Decision
|
||||
|
||||
### Canonical position
|
||||
|
||||
`CanonicalWowWorldPosition(X, Y, Z)` preserves the numeric X/Y/Z order used by
|
||||
the WoW client, movement protocol and TrinityCore/AzerothCore world positions.
|
||||
X and Y are horizontal; Z is height. Units are WoW yards.
|
||||
|
||||
`ServerWorldPosition` is a distinct type but converts component-for-component
|
||||
to canonical. The type boundary prevents a server record from being passed
|
||||
directly to renderer code and leaves room for adapter validation without a
|
||||
silent axis swap.
|
||||
|
||||
### World extent and ADT indices
|
||||
|
||||
The contract uses:
|
||||
|
||||
```text
|
||||
ADT_TILE_SIZE_YARDS = 1600 / 3
|
||||
ADT_TILES_PER_MAP_AXIS = 64
|
||||
WOW_WORLD_HALF_EXTENT_YARDS = 32 * ADT_TILE_SIZE_YARDS
|
||||
|
||||
tileX = floor(32 - canonical.X / ADT_TILE_SIZE_YARDS)
|
||||
tileY = floor(32 - canonical.Y / ADT_TILE_SIZE_YARDS)
|
||||
```
|
||||
|
||||
`AdtTileCoordinate(tile_x, tile_y)` always means the indices in
|
||||
`Map_tileX_tileY.adt`. Results are intentionally not clamped: the exact
|
||||
south-east extent produces index 64 and remains diagnosable as out of bounds.
|
||||
Normal valid indices are 0 through 63.
|
||||
|
||||
An `AdtTileLocalPosition` is measured east and south from the tile's
|
||||
north-west corner. An `AdtChunkCoordinate` uses `chunk_x` south and `chunk_y`
|
||||
east, with valid indices 0 through 15. These direction names avoid pretending
|
||||
that ADT file axes are Godot X/Z axes.
|
||||
|
||||
### ADT placement fields
|
||||
|
||||
MDDF/MODF position fields are represented in on-disk X/height/Z order:
|
||||
|
||||
```text
|
||||
adt.X = halfExtent - canonical.X
|
||||
adt.height = canonical.Z
|
||||
adt.Z = halfExtent - canonical.Y
|
||||
```
|
||||
|
||||
Model-local MDDF/MODF Euler corrections are not world orientation. They remain
|
||||
outside this contract until a placement-orientation package has format- and
|
||||
model-specific fixtures.
|
||||
|
||||
### Godot renderer basis
|
||||
|
||||
The compatibility renderer keeps one Godot unit equal to one WoW yard and uses
|
||||
Y-up coordinates from the north-west map extent:
|
||||
|
||||
```text
|
||||
godot.X = halfExtent - canonical.Y # east
|
||||
godot.Y = canonical.Z # up
|
||||
godot.Z = halfExtent - canonical.X # south
|
||||
```
|
||||
|
||||
`GodotWorldPosition` contains scalars rather than `Vector3`. A scene/render
|
||||
adapter may construct `Vector3` only at its boundary.
|
||||
|
||||
### World yaw
|
||||
|
||||
Server/canonical yaw zero faces increasing WoW X; positive yaw turns toward
|
||||
increasing WoW Y. In the mapped Godot basis, Godot forward `-Z` and positive
|
||||
Y-axis yaw produce the same numeric world angle. Mapper conversions therefore
|
||||
normalize the value to `[-PI, PI)` without adding an offset.
|
||||
|
||||
This identity applies to world-facing yaw only. It does not apply to M2/WMO
|
||||
asset axes or placement Euler triples.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Make renderer coordinates canonical
|
||||
|
||||
Rejected because protocol, database and ADT adapters would silently inherit a
|
||||
presentation-specific offset and Y-up basis.
|
||||
|
||||
### Adopt WoWee's internal canonical axis names unchanged
|
||||
|
||||
Rejected because that reference swaps server X/Y into its chosen north/west
|
||||
ordering. OpenWC preserves observable client/server field names and makes the
|
||||
render/ADT transformations explicit instead.
|
||||
|
||||
### Use `Vector3` plus comments
|
||||
|
||||
Rejected because comments cannot prevent server, ADT and Godot values from
|
||||
being passed across the wrong boundary.
|
||||
|
||||
### Generic vector-space or matrix framework
|
||||
|
||||
Rejected as unnecessary complexity for fixed affine transforms and integer
|
||||
grid ownership rules.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Network and server-data adapters can expose typed positions without knowing
|
||||
Godot.
|
||||
- Renderer consumers must explicitly map and then create Godot vectors at their
|
||||
own boundary.
|
||||
- ADT filename indices can no longer be confused with renderer X/Z tile names.
|
||||
- Existing native/parser and renderer conversions remain unchanged until
|
||||
separate consumer migration packages merge after this contract.
|
||||
- Manual axis conversions become review/test violations once migration is
|
||||
complete; this ADR alone does not delete compatibility paths.
|
||||
- Persisted schemas are unchanged. Future serialized positions must include an
|
||||
explicit coordinate-space/version discriminator.
|
||||
|
||||
## Verification
|
||||
|
||||
- Five M00 build-12340 client camera positions map to the recorded Godot
|
||||
checkpoints within `0.002` yard.
|
||||
- Server, ADT placement, Godot and tile-local conversions round-trip.
|
||||
- Exact center, tile, chunk and world-extent boundaries have explicit tests.
|
||||
- World-yaw normalization and round-trip cases cover cardinal angles and wrap.
|
||||
|
||||
## References
|
||||
|
||||
- [TrinityCore GridDefines](https://github.com/TrinityCore/TrinityCore/blob/master/src/server/game/Grids/GridDefines.h)
|
||||
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_adt.c`
|
||||
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_objects.c`
|
||||
- `reference/WoWee/include/core/coordinates.hpp`
|
||||
- [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||
@@ -7,6 +7,7 @@
|
||||
| Module | Current status | Primary specification |
|
||||
|---|---|---|
|
||||
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
|
||||
| Coordinate mapping | Implemented | [`coordinate-mapping.md`](coordinate-mapping.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
||||
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
# Coordinate Mapping
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target/work package | `M01-FND-COORDS-001` |
|
||||
| Owners | Foundation/domain |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m01-coordinate-mapper`, 2026-07-13 |
|
||||
| Profiles/capabilities | All profiles; contract version 1 |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide scene-free value objects and one pure `CoordinateMapper` for positions,
|
||||
ADT grid ownership and world-facing yaw. The module makes every source space
|
||||
explicit before renderer, network, gameplay and authoring consumers migrate.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Reading ADT, packet, SQL or scene data.
|
||||
- Constructing `Vector3`, `Transform3D`, Node or Resource values.
|
||||
- Correcting M2/WMO local axes or MDDF/MODF Euler rotations.
|
||||
- Selecting maps, validating terrain height or owning streaming focus.
|
||||
- Persisting coordinate values in this work package.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Server[Server / movement X,Y,Z] --> ServerPosition[ServerWorldPosition]
|
||||
ADT[MDDF/MODF X,height,Z] --> AdtPosition[AdtPlacementPosition]
|
||||
ServerPosition --> Mapper[CoordinateMapper]
|
||||
AdtPosition --> Mapper
|
||||
Mapper --> Canonical[CanonicalWowWorldPosition]
|
||||
Canonical --> Tile[AdtTileCoordinate + AdtTileLocalPosition]
|
||||
Canonical --> GodotPosition[GodotWorldPosition]
|
||||
GodotPosition --> Adapter[Future scene/render adapter]
|
||||
Adapter --> Vector[Vector3 at boundary]
|
||||
Scene[Node / Camera3D / scene lookup] -. forbidden .-> Mapper
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
|
||||
- GDScript scalar math and `RefCounted` lifetime;
|
||||
- other value objects in `src/domain/coordinates`;
|
||||
- callers in network, gameplay, render and server-data adapters.
|
||||
|
||||
Forbidden dependencies:
|
||||
|
||||
- `Node`, `Resource`, SceneTree, Camera3D and global scene lookup;
|
||||
- packet buffers, SQL rows, asset parsers and renderer state;
|
||||
- `Vector3` as a public mapper input or output;
|
||||
- model-specific placement corrections.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `CanonicalWowWorldPosition` | Immutable value | Client/server X/Y/Z in yards | Any thread; caller-owned reference | No bounds validation |
|
||||
| `ServerWorldPosition` | Immutable value | Server/wire X/Y/Z in yards | Any thread; caller-owned reference | No adapter validation |
|
||||
| `AdtPlacementPosition` | Immutable value | MDDF/MODF X/height/Z fields | Any thread; caller-owned reference | No file validation |
|
||||
| `GodotWorldPosition` | Immutable value | Godot east/up/south scalars | Any thread; caller-owned reference | Does not create `Vector3` |
|
||||
| `AdtTileCoordinate` | Immutable value | `Map_tileX_tileY.adt` indices | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
|
||||
| `AdtTileLocalPosition` | Immutable value | East/height/south offsets from tile north-west corner | Any thread; caller-owned reference | Values are intentionally unclamped |
|
||||
| `AdtChunkCoordinate` | Immutable value | MCNK chunk indices inside one tile | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
|
||||
| `CanonicalWowWorldYaw` | Immutable value | Canonical world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||
| `ServerWorldYaw` | Immutable value | Movement orientation in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||
| `GodotWorldYaw` | Immutable value | Godot Y-axis world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||
| `CoordinateMapper.server_to_canonical` / inverse | Pure static methods | Preserve server X/Y/Z as canonical | Synchronous; new result value | Null input is a caller contract violation |
|
||||
| `CoordinateMapper.canonical_to_godot` / inverse | Pure static methods | Apply Y-up renderer basis and map offset | Synchronous; new result value | Non-finite scalars propagate |
|
||||
| `CoordinateMapper.canonical_to_adt_placement` / inverse | Pure static methods | Convert ADT placement field basis | Synchronous; new result value | Non-finite scalars propagate |
|
||||
| `CoordinateMapper.canonical_to_adt_tile` | Pure static method | Compute unclamped filename indices | Synchronous; new result value | Out-of-map result remains invalid |
|
||||
| `CoordinateMapper.canonical_to_adt_tile_local` / inverse | Pure static methods | Split/reconstruct tile-local position | Synchronous; new result value | Caller validates tile ownership |
|
||||
| `CoordinateMapper.adt_tile_local_to_chunk` | Pure static method | Compute unclamped MCNK indices | Synchronous; new result value | Boundary/out-of-range remains diagnosable |
|
||||
| World-yaw mapper methods | Pure static methods | Normalize and cross the world-basis boundary | Synchronous; new result value | Non-finite input is unsupported |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | `ServerWorldPosition`, `ServerWorldYaw` | Future protocol/server adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||
| Input | `AdtPlacementPosition` | Future ADT adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||
| Input | `CanonicalWowWorldPosition`, `CanonicalWowWorldYaw` | Gameplay/content/domain | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||
| Output | `CanonicalWowWorldPosition` | `CoordinateMapper` | Gameplay/content/adapters | New immutable value | Caller-owned reference |
|
||||
| Output | `AdtTileCoordinate`, `AdtTileLocalPosition`, `AdtChunkCoordinate` | `CoordinateMapper` | Streaming/content adapters | New immutable values | Caller-owned references |
|
||||
| Output | `GodotWorldPosition`, `GodotWorldYaw` | `CoordinateMapper` | Future render/scene adapter | New immutable values | Caller-owned references |
|
||||
|
||||
Side effects:
|
||||
|
||||
- none;
|
||||
- no scene mutation, file/cache write, network send, database access or logging.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Server[ServerWorldPosition X,Y,Z] -->|identity components| Canonical[CanonicalWowWorldPosition]
|
||||
ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical
|
||||
Canonical -->|floor 32 - axis / tile size| Tile[AdtTileCoordinate]
|
||||
Canonical -->|north-west edge offsets| Local[AdtTileLocalPosition]
|
||||
Local -->|floor offset / chunk size| Chunk[AdtChunkCoordinate]
|
||||
Canonical -->|half extent - Y; Z; half extent - X| Godot[GodotWorldPosition]
|
||||
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
|
||||
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
The module is stateless. Each call reads immutable scalar inputs and returns a
|
||||
new value object. There is no initialization, shutdown, cancellation or retry
|
||||
state machine.
|
||||
|
||||
## Main sequence
|
||||
|
||||
No cross-layer asynchronous sequence occurs inside this module. Producers parse
|
||||
or validate their external format first, call the mapper synchronously, and
|
||||
then pass only typed results across the next boundary.
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Value objects own only scalar fields and, for chunks, one immutable tile reference.
|
||||
- Callers own returned `RefCounted` references.
|
||||
- Methods do not mutate inputs or shared state and are safe for worker use.
|
||||
- No Node, RID, file, socket, database or cache resource is acquired.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Tile/chunk outside standard grid | `is_within_map_grid()` | Unclamped indices are preserved | Caller reports source and map context | Reject or apply explicit map policy |
|
||||
| Tile-local value outside half-open tile | Consumer boundary validation | Value is preserved | Caller reports source coordinate | Recompute ownership or reject malformed input |
|
||||
| Null value object | Static typing/call failure | Programming contract violation | GDScript error with call site | Fix producer; no fallback value |
|
||||
| NaN/infinite scalar | Adapter validation | Arithmetic propagates invalid value | Adapter must report source field | Reject malformed external input |
|
||||
|
||||
There is no cancellation because every conversion is constant-time scalar math.
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Contract version | 1 | All | No | Axis, unit and boundary semantics in ADR 0001 |
|
||||
| Godot units per WoW yard | 1 | `Blizzlike335` and current tools | No | Preserves current renderer scale |
|
||||
| ADT grid | 64 tiles/axis, 16 chunks/tile | WotLK 3.3.5a | No | Defines index and boundary math |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
This package adds no persisted format, cache or migration. Existing dictionaries,
|
||||
native `Vector3` payloads and renderer conversion helpers remain compatible.
|
||||
Consumer migration is ordered after this contract and must remove old manual
|
||||
conversions only after its producer/consumer tests pass.
|
||||
|
||||
Any future serialized coordinate must store a schema version and explicit space;
|
||||
raw three-number arrays are not an accepted new persistence contract.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs: none in production mapper.
|
||||
- Metrics: none; conversion cost is constant and outside frame-budget concern.
|
||||
- Debug views: headless contract probe prints one PASS summary.
|
||||
- Correlation IDs: supplied by external adapters if an invalid source is reported.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit/contract tests: `src/tools/verify_coordinate_mapper.gd`.
|
||||
- Integration/E2E: consumer migration and `StreamingFocus` packages remain pending.
|
||||
- Fidelity evidence: five build-12340 camera positions from the M00 manifest map
|
||||
within `0.002` yard; independent reference formulas are recorded in ADR 0001.
|
||||
- Performance budgets: constant-time arithmetic; no scene or file access.
|
||||
- Manual diagnostics: not required for the pure contract; renderer checkpoints
|
||||
are required when consumers migrate.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Add typed map IDs around positions without changing axis semantics.
|
||||
- Add a format-specific placement-orientation adapter with its own fixtures.
|
||||
- Add serialization only with explicit space/version fields and migrations.
|
||||
- Add consumer adapters that construct engine-native vectors at their boundary.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Canonical/server/Godot position mapping | Implemented | Headless round-trip and five M00 golden points | Migrate producers/consumers |
|
||||
| ADT placement mapping | Implemented | Synthetic/direct round-trip fixtures | Compare native parsed placements during migration |
|
||||
| ADT tile/local/chunk ownership | Implemented | Center, extent, tile and chunk boundary fixtures | Migrate streaming names/ownership |
|
||||
| World-facing yaw mapping | Implemented | Cardinal/wrap round-trip fixtures | Placement Euler remains separate |
|
||||
| Scene-free production contract | Implemented | Source boundary and headless test | Add dependency search gate later in M01 |
|
||||
| Visual/server-spawn integration | Planned | M01 target criteria | Add golden server spawn after adapter migration |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Existing native loaders and renderer scripts still contain manual conversions.
|
||||
- Current `StreamingWorldLoader` tile names may represent Godot X/Z ordering rather
|
||||
than ADT filename X/Y; migration must preserve visible baseline behavior.
|
||||
- MDDF/MODF rotation comments and implementations disagree across existing paths;
|
||||
this package deliberately does not generalize them into world yaw.
|
||||
- Finite-number validation belongs to external format adapters and is not yet implemented.
|
||||
- Arithmetic agreement does not prove terrain, placement, camera direction or visual parity.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/domain/coordinates/coordinate_mapper.gd` | Constants and all coordinate-axis conversions |
|
||||
| `src/domain/coordinates/*_position.gd` | Immutable typed position values |
|
||||
| `src/domain/coordinates/adt_tile_coordinate.gd` | ADT filename tile indices and bounds |
|
||||
| `src/domain/coordinates/adt_chunk_coordinate.gd` | MCNK indices and bounds |
|
||||
| `src/domain/coordinates/*_yaw.gd` | Immutable typed world-facing yaw values |
|
||||
| `src/tools/verify_coordinate_mapper.gd` | Headless fixtures and boundary regressions |
|
||||
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- ADR: [`../adr/0001-canonical-world-coordinates.md`](../adr/0001-canonical-world-coordinates.md)
|
||||
- M00 evidence: [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||
- Testing rules: [`../TESTING.md`](../TESTING.md)
|
||||
Reference in New Issue
Block a user