# ADR 0001: Canonical world coordinate contract

- Status: Accepted for M01 contract implementation
- Date: 2026-07-13
- Work packages: `M01-FND-COORDS-001`, corrected by `M01-FND-COORD-TILE-AXIS-002`
- 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.Y / ADT_TILE_SIZE_YARDS)
tileY = floor(32 - canonical.X / 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. Filename `tile_x` and MCNK `chunk_x` grow east; filename
`tile_y` and MCNK `chunk_y` grow south, 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.

### Contract version 2 correction evidence

Contract version 1 incorrectly calculated filename `tile_x` from canonical X
and `tile_y` from canonical Y. The private WotLK corpus contains the observed
Elwynn point in `Azeroth_31_49.adt`, SHA-256
`18185B5FAC9566D9C57A9C19ABD75F108370D1795639B6077167C0EFD3672FAE`; the
transposed `Azeroth_49_31.adt` does not exist. Raw MCNK headers in that file
show `indexX` decreasing world Y (east) and `indexY` decreasing world X (south).
OpenRealm independently calculates filename X from world Y and filename Y from
world X. Contract version 2 corrects the formulas before any consumer migration.

### Enforced conversion boundaries

GDScript production consumers use `CoordinateMapper` for canonical/Godot and
Godot/ADT tile-local conversion. `StreamingWorldLoader`, player spawn/terrain
sampling, sky/area lookup and terrain diagnostics may construct `Vector3` only
at their scene/tool boundary.

The native ADT/WDT parser path retains one equivalent formula definition in
`src/native/src/wow_chunk_reader.h`; worker parsers cannot call GDScript. The
M00 renderer calibration keeps one independent formula as a test oracle so a
shared mapper defect cannot make both implementation and evidence agree.
Repository verification rejects additional world-center, world/tile or named
WoW/Godot formulas outside those explicit exceptions. Model-local M2/WMO basis
transforms are separate format contracts and are not classified as world-space mapping.

## 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.
- GDScript world/tile consumers use mapper APIs; renderer distance and LOD math
  remains ordinary Godot-space geometry.
- Native ADT/WDT loaders share one boundary formula and the M00 calibration
  keeps one independent non-production oracle.
- New manual axis conversions are review/test violations enforced by the
  repository-wide source gate.
- 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.
- The observed Elwynn position resolves to the real `Azeroth_31_49.adt` file;
  MCNK `(0,0)` to `(15,15)` origins confirm X-east and Y-south chunk growth.
- World-yaw normalization and round-trip cases cover cardinal angles and wrap.
- Direct Godot/ADT tile-local conversions round-trip and production consumers
  are checked by `verify_coordinate_conversion_boundaries.gd`.

## 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)
