test(M01): add coordinate golden fixtures

Work-Package: M01-QAR-COORD-FIXTURES-001

Agent: sindo-main-codex
This commit is contained in:
2026-07-13 14:27:38 +04:00
parent 758b375e7e
commit ce4f7ec0e2
4 changed files with 320 additions and 6 deletions
+23 -6
View File
@@ -5,9 +5,9 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| Status | Implemented | | Status | Implemented |
| Target/work package | `M01-FND-COORDS-001`, `M01-FND-COORD-TILE-AXIS-002` | | Target/work package | `M01-FND-COORDS-001`, `M01-FND-COORD-TILE-AXIS-002`, `M01-QAR-COORD-FIXTURES-001` |
| Owners | Foundation/domain | | Owners | Foundation/domain |
| Last verified | Worktree `work/sindo-main-codex/m01-coordinate-tile-axis-fix`, 2026-07-13 | | Last verified | Worktree `work/sindo-main-codex/m01-coordinate-fixtures`, 2026-07-13 |
| Profiles/capabilities | All profiles; contract version 2 | | Profiles/capabilities | All profiles; contract version 2 |
## Purpose ## Purpose
@@ -85,6 +85,7 @@ Forbidden dependencies:
| Output | `CanonicalWowWorldPosition` | `CoordinateMapper` | Gameplay/content/adapters | New immutable value | Caller-owned reference | | 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 | `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 | | Output | `GodotWorldPosition`, `GodotWorldYaw` | `CoordinateMapper` | Future render/scene adapter | New immutable values | Caller-owned references |
| Test input | `coordinate_golden_points.json` schema 1 | Pinned public server row plus private numeric observations | Headless fixture verifier | Versioned repository fixture | Test-process lifetime |
Side effects: Side effects:
@@ -95,6 +96,10 @@ Side effects:
```mermaid ```mermaid
flowchart LR flowchart LR
ServerFixture[Pinned AzerothCore spawn] --> FixtureVerifier[Golden fixture verifier]
RawADTFixture[Raw ADT numeric metadata] --> FixtureVerifier
ClientFixture[Build 12340 observations] --> FixtureVerifier
FixtureVerifier --> Mapper
Server[ServerWorldPosition X,Y,Z] -->|identity components| Canonical[CanonicalWowWorldPosition] Server[ServerWorldPosition X,Y,Z] -->|identity components| Canonical[CanonicalWowWorldPosition]
ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical
Canonical -->|tileX from Y; tileY from X| Tile[AdtTileCoordinate] Canonical -->|tileX from Y; tileY from X| Tile[AdtTileCoordinate]
@@ -145,8 +150,11 @@ There is no cancellation because every conversion is constant-time scalar math.
## Persistence, cache and migration ## Persistence, cache and migration
This package adds no persisted format, cache or migration. Existing dictionaries, Production code adds no persisted format, cache or migration. Test schema 1 in
native `Vector3` payloads and renderer conversion helpers remain compatible. `coordinate_golden_points.json` persists explicit source provenance, coordinate
contract version 2, tolerances, source-space values and expected outputs. It
contains no proprietary bytes, database dump or original-client image. Existing
dictionaries, native `Vector3` payloads and renderer conversion helpers remain compatible.
Consumer migration is ordered after this contract and must remove old manual Consumer migration is ordered after this contract and must remove old manual
conversions only after its producer/consumer tests pass. conversions only after its producer/consumer tests pass.
@@ -163,9 +171,12 @@ raw three-number arrays are not an accepted new persistence contract.
## Verification ## Verification
- Unit/contract tests: `src/tools/verify_coordinate_mapper.gd`. - Unit/contract tests: `src/tools/verify_coordinate_mapper.gd`.
- Cross-source golden test: `src/tools/verify_coordinate_golden_fixtures.gd`.
- Integration/E2E: consumer migration and `StreamingFocus` packages remain pending. - Integration/E2E: consumer migration and `StreamingFocus` packages remain pending.
- Fidelity evidence: five build-12340 camera positions from the M00 manifest map - Fidelity evidence: five build-12340 camera positions from the M00 manifest map
within `0.002` yard; independent reference formulas are recorded in ADR 0001. within `0.002` yard; a pinned AzerothCore human-warrior spawn and one raw MCNK
header observation independently confirm tile `(32,48)` and chunk `(3,12)`;
independent reference formulas are recorded in ADR 0001.
- Performance budgets: constant-time arithmetic; no scene or file access. - Performance budgets: constant-time arithmetic; no scene or file access.
- Manual diagnostics: not required for the pure contract; renderer checkpoints - Manual diagnostics: not required for the pure contract; renderer checkpoints
are required when consumers migrate. are required when consumers migrate.
@@ -186,7 +197,9 @@ raw three-number arrays are not an accepted new persistence contract.
| ADT tile/local/chunk ownership | Implemented | Center/extent boundaries plus raw `Azeroth_31_49.adt` filename and MCNK direction evidence | Migrate streaming consumers | | ADT tile/local/chunk ownership | Implemented | Center/extent boundaries plus raw `Azeroth_31_49.adt` filename and MCNK direction evidence | Migrate streaming consumers |
| World-facing yaw mapping | Implemented | Cardinal/wrap round-trip fixtures | Placement Euler remains separate | | 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 | | 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 | | Cross-source coordinate fixture | Implemented | Pinned AzerothCore spawn, raw ADT MCNK metadata and five build-12340 points pass one headless verifier | Add live adapter/renderer integration |
| TrinityCore populated spawn equivalence | Not verified | The base development schema has no pinned populated row used by this fixture | Pin and verify a TDB snapshot before claiming parity |
| Visual/server-spawn integration | Planned | M01 target criteria | Route the fixture through migrated adapters and renderer |
## Known gaps and risks ## Known gaps and risks
@@ -197,6 +210,8 @@ raw three-number arrays are not an accepted new persistence contract.
this package deliberately does not generalize them into world yaw. this package deliberately does not generalize them into world yaw.
- Finite-number validation belongs to external format adapters and is not yet implemented. - 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. - Arithmetic agreement does not prove terrain, placement, camera direction or visual parity.
- The golden server row is verified against pinned AzerothCore data only; no
TrinityCore equivalence claim is made without a pinned populated TDB snapshot.
## Source map ## Source map
@@ -208,6 +223,8 @@ raw three-number arrays are not an accepted new persistence contract.
| `src/domain/coordinates/adt_chunk_coordinate.gd` | MCNK 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/domain/coordinates/*_yaw.gd` | Immutable typed world-facing yaw values |
| `src/tools/verify_coordinate_mapper.gd` | Headless fixtures and boundary regressions | | `src/tools/verify_coordinate_mapper.gd` | Headless fixtures and boundary regressions |
| `src/tests/fixtures/coordinate_golden_points.json` | Versioned cross-source values, provenance and tolerances |
| `src/tools/verify_coordinate_golden_fixtures.gd` | Cross-source fixture schema and mapper validation |
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision | | `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
## Related decisions and references ## Related decisions and references
+99
View File
@@ -0,0 +1,99 @@
{
"schema_version": 1,
"coordinate_contract_version": 2,
"tolerances": {
"position_yards": 0.002,
"yaw_radians": 0.000001
},
"sources": {
"azerothcore_playercreateinfo": {
"status": "verified",
"repository": "azerothcore/database-wotlk",
"revision": "68fcf0098b16388093989627f37be530fc91f07d",
"path": "sql/base/playercreateinfo.sql",
"url": "https://raw.githubusercontent.com/azerothcore/database-wotlk/68fcf0098b16388093989627f37be530fc91f07d/sql/base/playercreateinfo.sql"
},
"trinitycore_playercreateinfo": {
"status": "not_verified",
"repository": "TrinityCore/TrinityCore",
"branch": "3.3.5",
"reason": "The base development schema does not provide a pinned populated playercreateinfo row; verify against a pinned TDB snapshot before claiming equivalence."
},
"azeroth_32_48_adt": {
"status": "verified_private_metadata",
"file_name": "Azeroth_32_48.adt",
"sha256": "D66E27FD82B461AE38B78562952AED428E619777A3F3E35DD8FF97444644770A",
"distribution": "Numeric metadata only; proprietary file remains outside Git."
},
"original_client_build_12340": {
"status": "verified_private_observation",
"build": 12340,
"manifest": "res://src/tools/render_baseline_manifest.json",
"evidence_document": "docs/M00_FINAL_PAIRED_EVIDENCE.md"
}
},
"server_spawns": [
{
"name": "azerothcore_human_warrior_start",
"source": "azerothcore_playercreateinfo",
"backend": "AzerothCore",
"table": "playercreateinfo",
"key": {"race": 1, "class": 1},
"map_id": 0,
"map_directory": "Azeroth",
"zone_id": 12,
"server_position": [-8949.95, -132.493, 83.5312],
"server_yaw_radians": 0.0,
"expected_adt_placement": [26016.616666667, 83.5312, 17199.159666667],
"expected_adt_tile": [32, 48],
"expected_adt_chunk": [3, 12],
"expected_adt_tile_local": [132.493, 83.5312, 416.616666667],
"expected_godot_position": [17199.159666667, 83.5312, 26016.616666667],
"expected_godot_yaw_radians": 0.0
}
],
"raw_adt_chunks": [
{
"name": "azeroth_32_48_mcnk_3_12",
"source": "azeroth_32_48_adt",
"map_directory": "Azeroth",
"adt_tile": [32, 48],
"mcnk_index": [3, 12],
"raw_header_canonical_origin": [-8933.33203125, -100.0, 84.238929749],
"sample_position": "chunk_center",
"expected_godot_chunk_center": [17183.333333333, 84.238929749, 26016.665364583]
}
],
"original_client_points": [
{
"name": "elwynn_terrain_overview",
"source": "original_client_build_12340",
"client_position": [-9153.334, 386.666, 180.0],
"expected_godot_position": [16680.0, 180.0, 26220.0]
},
{
"name": "elwynn_adt_boundary",
"source": "original_client_build_12340",
"client_position": [-9523.334, 46.666, 105.0],
"expected_godot_position": [17020.0, 105.0, 26590.0]
},
{
"name": "goldshire_dense_m2",
"source": "original_client_build_12340",
"client_position": [-9400.0, 110.0, 150.0],
"expected_godot_position": [16956.666, 150.0, 26466.666]
},
{
"name": "goldshire_inn_large_wmo",
"source": "original_client_build_12340",
"client_position": [-9385.0, 53.0, 72.0],
"expected_godot_position": [17013.666, 72.0, 26451.666]
},
{
"name": "elwynn_waterfall_liquid",
"source": "original_client_build_12340",
"client_position": [-9300.0, 585.0, 190.0],
"expected_godot_position": [16481.666, 190.0, 26366.666]
}
]
}
@@ -0,0 +1,197 @@
extends SceneTree
## Validates the non-proprietary M01 coordinate fixture against coordinate
## contract version 2 and the existing M00 build-12340 manifest. The script does
## not read server databases, proprietary ADT bytes or original-client images.
const CoordinateMapperScript = preload("res://src/domain/coordinates/coordinate_mapper.gd")
const CanonicalWowWorldPositionScript = preload("res://src/domain/coordinates/canonical_wow_world_position.gd")
const ServerWorldPositionScript = preload("res://src/domain/coordinates/server_world_position.gd")
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")
const FIXTURE_PATH := "res://src/tests/fixtures/coordinate_golden_points.json"
const RENDER_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const SUPPORTED_FIXTURE_SCHEMA_VERSION := 1
const REQUIRED_COORDINATE_CONTRACT_VERSION := 2
func _initialize() -> void:
var failures: Array[String] = []
var fixture := _load_json_object(FIXTURE_PATH, failures)
var render_manifest := _load_json_object(RENDER_MANIFEST_PATH, failures)
if fixture.is_empty() or render_manifest.is_empty():
_report_and_quit(failures)
return
_expect_equal(fixture.get("schema_version", -1), SUPPORTED_FIXTURE_SCHEMA_VERSION, "fixture schema version", failures)
_expect_equal(fixture.get("coordinate_contract_version", -1), REQUIRED_COORDINATE_CONTRACT_VERSION, "coordinate contract version", failures)
var position_tolerance_yards := float(fixture.get("tolerances", {}).get("position_yards", 0.0))
var yaw_tolerance_radians := float(fixture.get("tolerances", {}).get("yaw_radians", 0.0))
_expect_true(position_tolerance_yards > 0.0, "positive position tolerance", failures)
_expect_true(yaw_tolerance_radians > 0.0, "positive yaw tolerance", failures)
_verify_source_provenance(fixture.get("sources", {}), failures)
_verify_server_spawns(fixture.get("server_spawns", []), position_tolerance_yards, yaw_tolerance_radians, failures)
_verify_raw_adt_chunks(fixture.get("raw_adt_chunks", []), position_tolerance_yards, failures)
_verify_original_client_points(fixture.get("original_client_points", []), render_manifest, position_tolerance_yards, failures)
if not failures.is_empty():
_report_and_quit(failures)
return
print("COORDINATE_GOLDEN_FIXTURES PASS server_spawns=1 raw_adt_chunks=1 original_client_points=5")
quit(0)
func _verify_source_provenance(sources: Dictionary, failures: Array[String]) -> void:
var azerothcore_source: Dictionary = sources.get("azerothcore_playercreateinfo", {})
_expect_equal(azerothcore_source.get("status", ""), "verified", "AzerothCore source status", failures)
_expect_equal(String(azerothcore_source.get("revision", "")).length(), 40, "AzerothCore pinned revision length", failures)
_expect_true(String(azerothcore_source.get("url", "")).contains(String(azerothcore_source.get("revision", ""))), "AzerothCore URL contains pinned revision", failures)
var trinitycore_source: Dictionary = sources.get("trinitycore_playercreateinfo", {})
_expect_equal(trinitycore_source.get("status", ""), "not_verified", "TrinityCore source status", failures)
_expect_true(not String(trinitycore_source.get("reason", "")).is_empty(), "TrinityCore gap reason", failures)
var adt_source: Dictionary = sources.get("azeroth_32_48_adt", {})
_expect_equal(adt_source.get("status", ""), "verified_private_metadata", "ADT source status", failures)
_expect_equal(String(adt_source.get("sha256", "")).length(), 64, "ADT SHA-256 length", failures)
func _verify_server_spawns(server_spawns: Array, position_tolerance_yards: float, yaw_tolerance_radians: float, failures: Array[String]) -> void:
_expect_equal(server_spawns.size(), 1, "server spawn count", failures)
for spawn_variant in server_spawns:
if not (spawn_variant is Dictionary):
failures.append("server spawn must be an object")
continue
var spawn: Dictionary = spawn_variant
var name := String(spawn.get("name", "server_spawn"))
var server_position_values := _require_three_numbers(spawn.get("server_position", null), "%s server_position" % name, failures)
if server_position_values.is_empty():
continue
var server_position = ServerWorldPositionScript.new(server_position_values[0], server_position_values[1], server_position_values[2])
var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
var adt_placement = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
var adt_tile = CoordinateMapperScript.canonical_to_adt_tile(canonical_position)
var adt_local = CoordinateMapperScript.canonical_to_adt_tile_local(canonical_position)
var adt_chunk = CoordinateMapperScript.adt_tile_local_to_chunk(adt_tile, adt_local)
_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], spawn.get("expected_godot_position", null), position_tolerance_yards, "%s Godot position" % name, failures)
_expect_three_near([adt_placement.x_offset_yards, adt_placement.height_yards, adt_placement.z_offset_yards], spawn.get("expected_adt_placement", null), position_tolerance_yards, "%s ADT placement" % name, failures)
_expect_three_near([adt_local.east_yards, adt_local.height_yards, adt_local.south_yards], spawn.get("expected_adt_tile_local", null), position_tolerance_yards, "%s ADT local" % name, failures)
_expect_pair([adt_tile.tile_x, adt_tile.tile_y], spawn.get("expected_adt_tile", null), "%s ADT tile" % name, failures)
_expect_pair([adt_chunk.chunk_x, adt_chunk.chunk_y], spawn.get("expected_adt_chunk", null), "%s ADT chunk" % name, failures)
var server_yaw = ServerWorldYawScript.new(float(spawn.get("server_yaw_radians", 0.0)))
var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(CoordinateMapperScript.server_to_canonical_yaw(server_yaw))
_expect_near(godot_yaw.radians, float(spawn.get("expected_godot_yaw_radians", 0.0)), yaw_tolerance_radians, "%s Godot yaw" % name, failures)
func _verify_raw_adt_chunks(raw_adt_chunks: Array, position_tolerance_yards: float, failures: Array[String]) -> void:
_expect_equal(raw_adt_chunks.size(), 1, "raw ADT chunk count", failures)
for chunk_variant in raw_adt_chunks:
if not (chunk_variant is Dictionary):
failures.append("raw ADT chunk must be an object")
continue
var chunk: Dictionary = chunk_variant
var name := String(chunk.get("name", "raw_adt_chunk"))
var origin_values := _require_three_numbers(chunk.get("raw_header_canonical_origin", null), "%s raw origin" % name, failures)
if origin_values.is_empty():
continue
var half_chunk_size_yards := CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 0.5
var chunk_center = CanonicalWowWorldPositionScript.new(
origin_values[0] - half_chunk_size_yards,
origin_values[1] - half_chunk_size_yards,
origin_values[2]
)
var tile_coordinate = CoordinateMapperScript.canonical_to_adt_tile(chunk_center)
var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(chunk_center)
var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, local_position)
_expect_pair([tile_coordinate.tile_x, tile_coordinate.tile_y], chunk.get("adt_tile", null), "%s ADT tile" % name, failures)
_expect_pair([chunk_coordinate.chunk_x, chunk_coordinate.chunk_y], chunk.get("mcnk_index", null), "%s MCNK index" % name, failures)
var godot_center = CoordinateMapperScript.canonical_to_godot(chunk_center)
_expect_three_near([godot_center.x_units, godot_center.y_units, godot_center.z_units], chunk.get("expected_godot_chunk_center", null), position_tolerance_yards, "%s Godot center" % name, failures)
func _verify_original_client_points(original_client_points: Array, render_manifest: Dictionary, position_tolerance_yards: float, failures: Array[String]) -> void:
_expect_equal(original_client_points.size(), 5, "original-client point count", failures)
var manifest_checkpoint_by_name: Dictionary = {}
for checkpoint_variant in render_manifest.get("checkpoints", []):
if checkpoint_variant is Dictionary:
manifest_checkpoint_by_name[String(checkpoint_variant.get("name", ""))] = checkpoint_variant
for point_variant in original_client_points:
if not (point_variant is Dictionary):
failures.append("original-client point must be an object")
continue
var point: Dictionary = point_variant
var name := String(point.get("name", "original_client_point"))
var client_position_values := _require_three_numbers(point.get("client_position", null), "%s client position" % name, failures)
if client_position_values.is_empty():
continue
var canonical_position = CanonicalWowWorldPositionScript.new(client_position_values[0], client_position_values[1], client_position_values[2])
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], point.get("expected_godot_position", null), position_tolerance_yards, "%s mapped position" % name, failures)
var manifest_checkpoint: Dictionary = manifest_checkpoint_by_name.get(name, {})
_expect_true(not manifest_checkpoint.is_empty(), "%s exists in render manifest" % name, failures)
if not manifest_checkpoint.is_empty():
_expect_three_near(client_position_values, manifest_checkpoint.get("reference_wow_camera", null), position_tolerance_yards, "%s manifest client position" % name, failures)
_expect_three_near(point.get("expected_godot_position", null), manifest_checkpoint.get("camera", null), position_tolerance_yards, "%s manifest Godot position" % name, failures)
func _require_three_numbers(value_variant, label: String, failures: Array[String]) -> Array[float]:
if not (value_variant is Array) or value_variant.size() != 3:
failures.append("%s must contain three numbers" % label)
return []
return [float(value_variant[0]), float(value_variant[1]), float(value_variant[2])]
func _expect_three_near(actual_values, expected_variant, tolerance: float, label: String, failures: Array[String]) -> void:
var expected_values := _require_three_numbers(expected_variant, label, failures)
if expected_values.is_empty() or not (actual_values is Array) or actual_values.size() != 3:
return
for component_index in range(3):
_expect_near(float(actual_values[component_index]), expected_values[component_index], tolerance, "%s[%d]" % [label, component_index], failures)
func _expect_pair(actual_values: Array, expected_variant, label: String, failures: Array[String]) -> void:
if not (expected_variant is Array) or expected_variant.size() != 2:
failures.append("%s must contain two integers" % label)
return
_expect_equal(actual_values[0], int(expected_variant[0]), "%s[0]" % label, failures)
_expect_equal(actual_values[1], int(expected_variant[1]), "%s[1]" % label, failures)
func _expect_near(actual_value: float, expected_value: float, tolerance: float, label: String, failures: Array[String]) -> void:
if absf(actual_value - expected_value) > tolerance:
failures.append("%s expected %.9f, got %.9f" % [label, expected_value, actual_value])
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open JSON fixture: %s" % path)
return {}
var parsed = JSON.parse_string(file.get_as_text())
if not (parsed is Dictionary):
failures.append("JSON fixture is not an object: %s" % path)
return {}
return parsed
func _report_and_quit(failures: Array[String]) -> void:
if failures.is_empty():
failures.append("fixture validation stopped without a diagnostic")
for failure in failures:
push_error("COORDINATE_GOLDEN_FIXTURES: %s" % failure)
quit(1)
@@ -0,0 +1 @@
uid://cr8f88swvk002