From ce4f7ec0e276bd7992e4a666b97e883d92c4f29b Mon Sep 17 00:00:00 2001 From: sindoring Date: Mon, 13 Jul 2026 14:27:38 +0400 Subject: [PATCH] test(M01): add coordinate golden fixtures Work-Package: M01-QAR-COORD-FIXTURES-001 Agent: sindo-main-codex --- docs/modules/coordinate-mapping.md | 29 ++- .../fixtures/coordinate_golden_points.json | 99 +++++++++ .../verify_coordinate_golden_fixtures.gd | 197 ++++++++++++++++++ .../verify_coordinate_golden_fixtures.gd.uid | 1 + 4 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 src/tests/fixtures/coordinate_golden_points.json create mode 100644 src/tools/verify_coordinate_golden_fixtures.gd create mode 100644 src/tools/verify_coordinate_golden_fixtures.gd.uid diff --git a/docs/modules/coordinate-mapping.md b/docs/modules/coordinate-mapping.md index 3e69a70..e1ad379 100644 --- a/docs/modules/coordinate-mapping.md +++ b/docs/modules/coordinate-mapping.md @@ -5,9 +5,9 @@ | Field | Value | |---|---| | 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 | -| 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 | ## Purpose @@ -85,6 +85,7 @@ Forbidden dependencies: | 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 | +| 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: @@ -95,6 +96,10 @@ Side effects: ```mermaid 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] ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical 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 -This package adds no persisted format, cache or migration. Existing dictionaries, -native `Vector3` payloads and renderer conversion helpers remain compatible. +Production code adds no persisted format, cache or migration. Test schema 1 in +`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 conversions only after its producer/consumer tests pass. @@ -163,9 +171,12 @@ raw three-number arrays are not an accepted new persistence contract. ## Verification - 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. - 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. - Manual diagnostics: not required for the pure contract; renderer checkpoints 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 | | 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 | +| 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 @@ -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. - 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. +- 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 @@ -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/*_yaw.gd` | Immutable typed world-facing yaw values | | `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 | ## Related decisions and references diff --git a/src/tests/fixtures/coordinate_golden_points.json b/src/tests/fixtures/coordinate_golden_points.json new file mode 100644 index 0000000..89b3b33 --- /dev/null +++ b/src/tests/fixtures/coordinate_golden_points.json @@ -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] + } + ] +} diff --git a/src/tools/verify_coordinate_golden_fixtures.gd b/src/tools/verify_coordinate_golden_fixtures.gd new file mode 100644 index 0000000..cf7e95a --- /dev/null +++ b/src/tools/verify_coordinate_golden_fixtures.gd @@ -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) diff --git a/src/tools/verify_coordinate_golden_fixtures.gd.uid b/src/tools/verify_coordinate_golden_fixtures.gd.uid new file mode 100644 index 0000000..2cd8dd5 --- /dev/null +++ b/src/tools/verify_coordinate_golden_fixtures.gd.uid @@ -0,0 +1 @@ +uid://cr8f88swvk002