Compare commits

...

13 Commits

Author SHA1 Message Date
sindoring fc81059dfa docs(M01): mark milestone closeout ready
Work-Package: M01-QAR-INTEGRATOR-CLOSEOUT-001

Agent: sindo-main-codex
2026-07-14 12:14:39 +04:00
sindoring dd412e6d0d test(M01): close coordinates and seams milestone
Work-Package: M01-QAR-INTEGRATOR-CLOSEOUT-001

Agent: sindo-main-codex

Tests: all M01 domain gates, unified renderer dry-run, coordination and documentation gates

Fidelity: five build-12340 points, raw ADT and mapped AzerothCore spawn evidence; no 1:1 claim
2026-07-14 12:13:43 +04:00
sindoring 07f08044f4 docs(M01): claim milestone closeout
Work-Package: M01-QAR-INTEGRATOR-CLOSEOUT-001

Agent: sindo-main-codex
2026-07-14 12:01:49 +04:00
sindoring c8e99f28d6 Merge pull request 'chore(M01): remove local spawn capture logs' (#19) from work/sindo-main-codex/m01-server-spawn-renderer into master
Reviewed-on: #19
2026-07-14 10:58:37 +03:00
sindoring 48cc976ba0 docs(M01): mark server spawn renderer ready
Work-Package: M01-QAR-SERVER-SPAWN-RENDERER-001

Agent: sindo-main-codex

Tests: server spawn GUI cold/warm, unified renderer dry-run, domain and documentation gates
2026-07-14 11:56:39 +04:00
sindoring 318e6ae757 chore(M01): remove local spawn capture logs
Work-Package: M01-QAR-SERVER-SPAWN-RENDERER-001

Agent: sindo-main-codex

Tests: renderer dry-run, coordinate/domain gates, GUI cold/warm inspection
2026-07-14 11:55:05 +04:00
sindoring 747fae8e2e Merge pull request 'координаты' (#18) from work/sindo-main-codex/m01-server-spawn-renderer into master
Reviewed-on: #18
2026-07-14 10:48:53 +03:00
sindoring 24ddc99a7b координаты 2026-07-14 11:48:14 +04:00
sindoring 52b6fa2ebb Merge pull request 'docs(M01): claim server spawn renderer evidence' (#17) from work/sindo-main-codex/m01-server-spawn-renderer into master
Reviewed-on: #17
2026-07-14 10:42:14 +03:00
sindoring 3933dbfc47 docs(M01): claim server spawn renderer evidence
Work-Package: M01-QAR-SERVER-SPAWN-RENDERER-001

Agent: sindo-main-codex
2026-07-13 17:41:02 +04:00
sindoring a07a487368 docs(M01): mark coordinate boundary gate ready
Work-Package: M01-FND-COORD-BOUNDARY-GATE-001

Agent: sindo-main-codex
2026-07-13 16:40:07 +04:00
sindoring 47dec6c32c feat(M01): enforce coordinate conversion boundaries
Work-Package: M01-FND-COORD-BOUNDARY-GATE-001

Agent: sindo-main-codex
2026-07-13 16:37:29 +04:00
sindoring c8e62acf4c docs(M01): claim coordinate boundary gate
Work-Package: M01-FND-COORD-BOUNDARY-GATE-001

Agent: sindo-main-codex
2026-07-13 16:15:18 +04:00
32 changed files with 1026 additions and 96 deletions
+4
View File
@@ -35,3 +35,7 @@
# CMake build директория
/src/native/build/
# Local root-level diagnostic capture logs
/*.stdout.log
/*.stderr.log
+30
View File
@@ -843,6 +843,36 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Streaming radii, LOD decisions, cache formats and renderer quality profiles
are unchanged by this migration.
## 2026-07-13 Coordinate Conversion Boundary Gate
- Sky/area lookup, streaming tile ownership, player spawn/terrain sampling and
terrain diagnostics now cross world-coordinate spaces through typed
`CoordinateMapper` methods.
- Renderer distance, LOD, ray and model-local M2/WMO calculations remain in
their existing Godot/local spaces; they are not axis conversions.
- The repository gate rejects new manual world-center, WoW/Godot and
Godot-position-to-ADT formulas. The unified renderer baseline runs this gate.
- Native ADT/WDT parsers keep one shared world conversion in
`wow_chunk_reader.h`; the independent M00 calibration formula remains a test oracle.
- Coordinate contract version 2, renderer radii, cache formats and visual
quality profiles are unchanged.
## 2026-07-13 Golden Server Spawn Renderer Evidence
- The pinned AzerothCore Human Warrior start at server position
`(-8949.95, -132.493, 83.5312)` maps through `CoordinateMapper` to Godot
`(17199.159666667, 83.5312, 26016.616666667)`, ADT `(32,48)` and MCNK `(3,12)`.
- `server_spawn_render_manifest.json` drives the ordinary Eastern Kingdoms
checkpoint capture pipeline at that location; no live database or production
spawn entity is introduced.
- A renderer-native emissive marker starts at the exact mapped origin and keeps
depth testing enabled so GUI evidence exposes its relationship to terrain.
- `verify_server_spawn_renderer.gd` rejects drift between the pinned fixture,
manifest target/player coordinates and expected ADT ownership. The unified
renderer baseline runs this contract check before capture.
- This closes a coordinate/renderer integration criterion only. It does not
claim TrinityCore row equivalence or visual parity with the original client.
## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
@@ -0,0 +1,131 @@
# M01-FND-COORD-BOUNDARY-GATE-001 — Coordinate conversion boundary gate
<!-- OPENWC_CLAIM:M01-FND-COORD-BOUNDARY-GATE-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORD-BOUNDARY-GATE-001:c8e99f2 -->
## Ownership
- Target: M01
- Program: FND/QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-coordinate-boundary-gate`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Route remaining GDScript world/tile axis conversions through `CoordinateMapper`
and add a repository-wide gate that rejects new manual world-center, axis-swap
or world-to-ADT tile formulas outside explicit contract/native-oracle boundaries.
## Non-goals
- Rewriting model-local M2/WMO vertex or placement orientation transforms.
- Moving renderer distance/LOD geometry out of Godot space.
- Replacing the native parser boundary with GDScript calls from worker threads.
- Changing coordinate contract semantics, renderer radii or cache formats.
## Paths
- Exclusive: `src/tools/verify_coordinate_conversion_boundaries.gd`
- Shared/hotspots: `src/domain/coordinates/coordinate_mapper.gd`, coordinate
tests/docs/ADR, streamer/player/sky/terrain-probe consumers,
`docs/CODING_STANDARD.md`, this claim
- Generated/ignored: Godot import metadata, extracted assets and renderer caches
## Contracts and data
- Additive mapper methods: Godot position to ADT tile/local and ADT tile/local
to Godot position
- Native exception: one formula definition in `src/native/src/wow_chunk_reader.h`;
ADT/WDT loaders may call it but may not define copies
- Oracle exception: `verify_render_coordinate_calibration.gd` retains one
independent M00 formula to detect common-mode mapper regressions
- Model-local transforms are outside the world-coordinate gate
## Dependencies
- Requires: coordinate contract v2 and M01 seams through `7c64e8d`
- Blocks: M01 coordinate-centralization exit criterion and milestone evidence audit
- External state: no dependency or proprietary input
## Verification
- Commands: conversion-boundary gate, mapper/golden fixtures, streaming focus,
identities, renderer calibration/manifest and repository gates
- Fixtures: Godot/ADT tile center, local offsets and exact boundaries
- Fidelity evidence: existing build-12340 points and raw ADT/server fixture remain unchanged
- Performance budget: mapper allocations occur only at adapter/update boundaries;
renderer distance/LOD math remains in Godot scalars
## Documentation deliverables
- Inline public API docs: new mapper methods
- Module specification/ADR: boundary APIs, allowed exceptions and gate behavior
- Coding standard: mandatory review rule for cross-space conversions
- Data-flow diagram: existing coordinate module diagram updated for direct adapters
- State/sequence diagrams: unchanged; pure synchronous mapping
## Simplicity and naming
- Important names introduced: `godot_to_adt_tile`, `godot_to_adt_tile_local`,
`adt_tile_local_to_godot`, `verify_coordinate_conversion_boundaries`
- Simplest considered solution: extend the existing mapper and scan source text
for narrow high-signal formula signatures
- Rejected complexity/abstractions: generic coordinate algebra framework,
native-to-GDScript calls on parser workers and a broad numeric-literal ban
- Unavoidable complexity and justification: one native boundary and one
independent test oracle must remain explicit to avoid worker coupling and common-mode tests
- Measured optimization evidence: existing renderer distance/LOD scalar math is unchanged
## Status
- State: ready-for-review
- Done: mapper extensions, consumer migration, repository gate, review rule,
integrated renderer gate, regression fixtures and documentation
- Next: integrator review, merge and M01 completion-evidence audit
- Blocked by:
## Handoff
- Commit: `47dec6c`
- Branch: `work/sindo-main-codex/m01-coordinate-boundary-gate`
- Outcome: all audited GDScript world/tile conversions in sky, streaming,
player spawn/ground sampling and terrain probes route through typed
`CoordinateMapper` boundaries. A repository scan rejects new manual
world-center, legacy axis-conversion, world-to-tile, tile-local and
tile-center formulas.
- Contract/schema: additive `godot_to_adt_tile`,
`godot_to_adt_tile_local` and `adt_tile_local_to_godot` APIs; no persisted
format, cache version or renderer radius changes.
- Verification: CoordinateConversionBoundaries PASS (`83` files, one native
boundary, one independent oracle, four consumers, six classifier guards);
CoordinateMapper PASS (`5` golden points, `8` tile boundaries, `2` raw MCNK,
`5` yaw cases); renderer calibration PASS (`5` points, maximum error
`0.000015`); renderer manifest PASS (`7/7/7`); unified render baseline dry-run,
headless project load, runtime cache shutdown, coordination, documentation
and whitespace gates passed.
- GUI regression: final ADT-boundary capture compared `2/2`, missing `0`,
failed `0` (cold mean `0.0002324`, changed ratio `0.0013741`; warm mean
`0.0000226`, changed ratio `0.0001285`). Earlier full capture produced all
`14` images; `11/14` met the strict changed-pixel threshold. The three
threshold-only differences retained low mean error (`<= 0.00622`) and were
visually classified as cold M2 readiness and liquid animation phase, with
unchanged camera, geometry alignment, area and streaming focus.
- Fidelity: existing build-12340 calibrated points, raw ADT fixture and server
spawn fixture remain unchanged. Native parser workers retain exactly one
formula in `wow_chunk_reader.h`; the calibration verifier retains exactly one
independent formula so mapper and oracle cannot share a common-mode defect.
- Rebuild: none. Local ignored extracted assets/native binary/cache junctions
were used for runtime checks and are not part of the commit.
- Remaining risks: source scanning intentionally recognizes narrow high-signal
formula signatures rather than parsing arbitrary algebra; native formula
parity still relies on calibration/raw fixtures. Model-local M2/WMO basis
transforms and Godot-space LOD distance math are intentionally outside scope.
- Recommended merge order: after `7c64e8d`; this package closes the remaining
M01 manual axis-conversion checklist item, but only the milestone integrator
may update target Evidence/status or set `OPENWC_TARGET_DONE`.
- Documentation: coding review rule, coordinate-mapping module API/input-output/
data-flow/capability/risk/source-map sections, ADR 0001 and `RENDER.md` were
updated. Existing pure synchronous data-flow diagram remains authoritative;
no state/sequence diagram changed.
@@ -1,7 +1,7 @@
# M01-FND-COORD-TILE-AXIS-002 — ADT filename axis correction
<!-- OPENWC_CLAIM:M01-FND-COORD-TILE-AXIS-002:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_HANDOFF:READY:M01-FND-COORD-TILE-AXIS-002:a7072e3 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORD-TILE-AXIS-002:c8e99f2 -->
## Ownership
+1 -1
View File
@@ -1,7 +1,7 @@
# M01-FND-COORDS-001 — Canonical coordinate contracts
<!-- OPENWC_CLAIM:M01-FND-COORDS-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_HANDOFF:READY:M01-FND-COORDS-001:f45695c -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORDS-001:c8e99f2 -->
## Ownership
@@ -1,7 +1,7 @@
# M01-FND-DOMAIN-IDENTITIES-001 — Stable domain identity namespaces
<!-- OPENWC_CLAIM:M01-FND-DOMAIN-IDENTITIES-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_HANDOFF:READY:M01-FND-DOMAIN-IDENTITIES-001:c1dc07c -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-DOMAIN-IDENTITIES-001:c8e99f2 -->
## Ownership
@@ -1,7 +1,7 @@
# M01-QAR-COORD-FIXTURES-001 — Coordinate golden fixtures
<!-- OPENWC_CLAIM:M01-QAR-COORD-FIXTURES-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_HANDOFF:READY:M01-QAR-COORD-FIXTURES-001:ce4f7ec -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-QAR-COORD-FIXTURES-001:c8e99f2 -->
## Ownership
@@ -0,0 +1,101 @@
# M01-QAR-INTEGRATOR-CLOSEOUT-001 — M01 milestone closeout
<!-- OPENWC_CLAIM:M01-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex:2026-07-16 -->
<!-- OPENWC_HANDOFF:READY:M01-QAR-INTEGRATOR-CLOSEOUT-001:dd412e6 -->
## Ownership
- Target: M01
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-integrator-closeout`
- Lease expires UTC: 2026-07-16
- Integrator: sindo-main-codex, continuing the user-directed milestone workflow on 2026-07-14
## Outcome
Audit every merged M01 work package on current `master`, record final evidence,
close M01 and activate M02 only if all target verification and documentation
criteria pass.
## Non-goals
- Change coordinate, identity, streaming or renderer behavior.
- Start M02 implementation.
- Add new compatibility or visual-parity claims.
- Commit proprietary assets, generated UIDs, caches or capture output.
## Paths
- Exclusive: M01/M02 target status, M01 final Evidence and M01 claim acceptance markers
- Shared/hotspots: `targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, this claim
- Generated/ignored: local Godot imports, renderer corpus and `user://` reports
## Contracts and data
- Public APIs, schemas, cache formats and migrations: unchanged
- Coordinate contract remains version 2
- Fixture schema remains version 1
- Consumers: milestone sequence and M02 work packages
## Dependencies
- Requires: all seven M01 READY commits integrated through master `c8e99f2`
- Blocks: M02 activation
- External state: server-spawn GUI hashes and original-client numeric evidence are recorded; proprietary files remain outside Git
## Verification
- Commands: all M01 domain/fixture/boundary/focus/identity/spawn gates,
unified renderer dry-run, coordination/documentation checks and diff hygiene
- Fidelity evidence: five build-12340 points, raw ADT metadata, pinned
AzerothCore spawn and cache-complete GUI marker capture
- Performance: no runtime change; preserve recorded renderer cold/warm evidence
## Documentation deliverables
- M01 final target Evidence and checklist
- M01 claim integration-acceptance markers
- Target index and roadmap transition to M02
- API/module specifications and diagrams are audited, not changed unless a gap is found
## Simplicity and naming
- Important names introduced: none beyond the closeout work-package ID
- Simplest approach: evidence/status-only integrator patch
- Rejected complexity: new one-off closeout framework or behavioral changes
- Unavoidable complexity: none
## Status
- State: ready-for-review
- Done: accepted seven M01 packages, reran post-merge gates, recorded complete
target Evidence, closed M01 and activated M02
- Next: merge closeout commit before starting an M02 implementation claim
- Blocked by:
## Handoff
- Commit: `dd412e6`
- Branch: `work/sindo-main-codex/m01-integrator-closeout`
- Outcome: M01 is `DONE` with all seven checklist items and complete Evidence;
M02 is the only `ACTIVE` target and `targets/README.md` points to it.
- Integration audit: all seven M01 READY commits are ancestors of master
`c8e99f2`; their claims now carry `OPENWC_INTEGRATION:ACCEPTED` markers.
- Verification: unified renderer dry-run passed project load, material/dedupe/
shutdown gates, manifest `7/7/7`, calibration maximum error `0.000015`,
coordinate boundary (`84` files) and server-spawn renderer `(32,48)/(3,12)`;
coordinate mapper, golden fixtures, StreamingFocus and domain identity gates
passed. Coordination and documentation gates passed; coordination reports
only five already-expired M00 claim warnings.
- Fidelity: closeout preserves five build-12340 coordinate observations, raw ADT
evidence, pinned AzerothCore spawn mapping and cache-complete Northshire GUI
evidence without claiming original-client visual parity or TrinityCore row
equivalence.
- Rebuild/migration: none. Coordinate contract remains version 2; fixture schema
remains version 1; renderer cache formats are unchanged.
- Remaining risks: recorded in M01 Evidence—TrinityCore populated spawn,
placement Euler parity, native parser boundary and broader renderer parity.
- Documentation: M01 target Evidence/checklist, target index and roadmap current
state updated. Existing coordinate, identity and renderer module specs/API/
diagrams were audited and passed the documentation gate.
@@ -0,0 +1,135 @@
# M01-QAR-SERVER-SPAWN-RENDERER-001 — Golden server spawn renderer evidence
<!-- OPENWC_CLAIM:M01-QAR-SERVER-SPAWN-RENDERER-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-QAR-SERVER-SPAWN-RENDERER-001:c8e99f2 -->
## Ownership
- Target: M01
- Program: QAR/RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-server-spawn-renderer`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Route the pinned AzerothCore Human Warrior spawn fixture through
`CoordinateMapper` into the live Eastern Kingdoms renderer and produce
repeatable headless contract checks plus GUI screenshot evidence at the mapped
ADT tile/chunk.
## Non-goals
- Connecting to a live AzerothCore or TrinityCore database.
- Claiming TrinityCore spawn equivalence without a pinned populated TDB row.
- Changing coordinate formulas, streaming radii, terrain, player movement or
production character presentation.
- Adding proprietary assets or screenshots to Git.
## Paths
- Exclusive: `src/tools/server_spawn_render_manifest.json`,
`src/tools/verify_server_spawn_renderer.gd`
- Shared/hotspots: `src/tools/capture_render_checkpoints.gd`, renderer runner,
coordinate/world-renderer module documentation, `RENDER.md`, this claim
- Generated/ignored: Godot UIDs/import metadata and `user://` PNG/JSON evidence
## Contracts and data
- Fixture schema and coordinate contract version remain unchanged.
- Additive diagnostic checkpoint metadata may request a renderer-native marker;
existing M00 checkpoints remain byte-for-byte compatible.
- The spawn remains sourced from pinned AzerothCore revision
`68fcf0098b16388093989627f37be530fc91f07d`.
## Dependencies
- Requires: merged M01 packages through `a07a487`
- Blocks: M01 golden server-spawn renderer criterion and milestone closeout
- External state: GUI capture requires the existing local extracted/cached
renderer corpus; headless contract verification remains non-proprietary
## Verification
- Commands: dedicated spawn verifier, coordinate fixture/mapper/boundary gates,
renderer baseline dry-run, GUI checkpoint capture and human inspection
- Fixture: AzerothCore race `1`, class `1`, map `0`, server position
`(-8949.95, -132.493, 83.5312)`
- Expected renderer result: Godot position
`(17199.159666667, 83.5312, 26016.616666667)`, ADT `(32,48)`, MCNK `(3,12)`
- Fidelity classification: server/coordinate integration evidence only; it does
not establish full original-client visual parity
## Documentation deliverables
- Inline API/tool documentation for new verifier and marker metadata
- Coordinate-mapping module capability/evidence update
- World-renderer checkpoint input/output/source-map update when marker support is added
- Existing synchronous coordinate data-flow diagram updated only if flow changes
## Simplicity and naming
- Important names: `server_spawn_render_manifest`,
`verify_server_spawn_renderer`, `diagnostic_spawn_marker`
- Simplest approach: reuse the existing checkpoint capture pipeline with one
dedicated manifest and a renderer-native diagnostic marker
- Rejected complexity: live DB adapter, new scene-test framework and production
spawn entity implementation before the network/gameplay milestones
- Performance: one diagnostic checkpoint outside production runtime
## Status
- State: ready-for-review
- Done: fixture-to-renderer manifest/verifier, renderer-native marker, unified
baseline integration, cold/warm GUI evidence, documentation and log cleanup
- Next: integrator review, merge cleanup/handoff and M01 closeout audit
- Blocked by:
## Handoff
- Commits: implementation `24ddc99`; required cleanup `318e6ae`
- Branch: `work/sindo-main-codex/m01-server-spawn-renderer`
- Outcome: the pinned AzerothCore Human Warrior start is routed through
`CoordinateMapper` into the ordinary Eastern Kingdoms capture scene. A
depth-tested magenta marker begins at the exact mapped spawn origin.
- Contract/schema: additive diagnostic manifest schema 1 and marker metadata;
coordinate contract version 2, production renderer APIs, cache formats and
`Blizzlike335` behavior are unchanged.
- Verification: `SERVER_SPAWN_RENDERER PASS spawn=1 checkpoint=1 tile=32_48
chunk=3_12 profile=Blizzlike335`; unified renderer dry-run passed project,
materials, M2 dedupe, shutdown, `7/7/7` manifest, five-point calibration,
coordinate-boundary and server-spawn steps; coordinate golden fixtures,
mapper, StreamingFocus, domain identities, documentation and whitespace gates
passed. Coordination passed with five expired M00-claim warnings only.
- GUI evidence: two `1280x900` cold/warm PNGs captured under
`user://server_spawn_renderer_m01_gui_run2`; focus tile `(32,48)`, terrain
quality `100/100`, missing cache `0`, queues drained, report results `2`.
Human inspection confirms the marker is on rendered terrain beside the road
at Northshire Abbey rather than below terrain or in an adjacent tile. PNG
hashes: cold `c08c43bcb75e5b3277105afb0d4e5a76caae9db91a7db64488770ffa03e32589`,
warm `1b75237a090ea163836372a8e39e778c909773f4508063adc3f70a3e12a85a62`.
- Fidelity: source remains AzerothCore revision
`68fcf0098b16388093989627f37be530fc91f07d`; server
`(-8949.95,-132.493,83.5312)` maps to Godot
`(17199.159666667,83.5312,26016.616666667)`, ADT `(32,48)`, MCNK `(3,12)`.
This proves coordinate/renderer integration, not original-client visual parity
or TrinityCore populated-row equivalence.
- Runtime notes: D3D12 descriptor-heap initialization fell back to Vulkan;
optional ignored character/ambient GLB imports were unavailable. Neither
affected terrain/marker evidence. The known anonymous ObjectDB teardown
warning remained; no Node, renderer resource or RID leak diagnostic occurred
in the cache-complete run.
- Repository hygiene: four accidentally committed local GUI logs are removed by
`318e6ae`; root `*.stdout.log`/`*.stderr.log` files are now ignored. Generated
PNG/JSON evidence and proprietary corpus remain outside Git.
- Remaining risks: the diagnostic marker is not a production network entity;
live server adapters belong to M06/M08. Exact original-client character spawn
presentation and TrinityCore database equivalence remain unverified.
- Documentation: `RENDER.md`, renderer-baseline procedure, coordinate-mapping
and world-renderer module API/input-output/data-flow/capability/source-map
sections updated. No lifecycle/state diagram changed because the verifier and
mapper path are synchronous; capture reuses the documented renderer lifecycle.
- Recommended merge order: `318e6ae` after master merge `747fae8`. Then the M01
milestone integrator may accept all M01 claims, update target Evidence/checks
and evaluate `OPENWC_TARGET_DONE`.
@@ -1,7 +1,7 @@
# M01-RND-STREAMING-FOCUS-001 — Explicit streaming focus contract
<!-- OPENWC_CLAIM:M01-RND-STREAMING-FOCUS-001:sindo-main-codex:2026-07-15 -->
<!-- OPENWC_HANDOFF:READY:M01-RND-STREAMING-FOCUS-001:7815385 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-RND-STREAMING-FOCUS-001:c8e99f2 -->
## Ownership
+19
View File
@@ -237,6 +237,23 @@ TODO/FIXME обязан содержать target/work-package ID или diagnos
- Медленный понятный reference implementation полезен как oracle для optimized implementation.
- Сложная оптимизация удаляется или упрощается, если она не даёт измеримого результата.
## Coordinate conversion boundary
- Cross-space world conversions выполняются только через typed API
`CoordinateMapper`; ручные world-center offsets, swaps осей и вычисление ADT
filename indices из Godot position запрещены.
- Scene/render adapter МОЖЕТ создать `Vector3` только после получения
`GodotWorldPosition` или перевести engine vector в typed scalar value на своей границе.
- Единственная native world-position формула находится в
`src/native/src/wow_chunk_reader.h`, поскольку parser workers не вызывают
GDScript. Новая native копия требует отдельного boundary decision и parity fixture.
- `verify_render_coordinate_calibration.gd` остаётся независимым test oracle;
production code не копирует его формулы.
- Model-local M2/WMO vertex basis и placement orientation не являются world
coordinate conversion. Они документируются и тестируются своим format adapter.
- `verify_coordinate_conversion_boundaries.gd` обязателен при изменении
coordinates, native parsers, sky, player, streaming или renderer diagnostics.
## Review checklist
- Можно ли понять назначение сущности по имени?
@@ -245,6 +262,8 @@ TODO/FIXME обязан содержать target/work-package ID или diagnos
- Можно ли убрать abstraction без потери correctness/boundary?
- Есть ли скрытый mutable state или side effect?
- Не появился ли второй источник истины?
- Все ли cross-space world coordinates проходят через `CoordinateMapper` или
документированную native boundary?
- Измерена ли сложная optimization?
- Изолирована и документирована ли неизбежная сложность?
- Соответствуют ли API docs и diagrams фактическому коду?
+19 -1
View File
@@ -10,7 +10,10 @@
.\tools\run_render_baseline.ps1
```
Скрипт последовательно проверяет headless-загрузку проекта, renderer materials, M2 unique-id dedupe, regression manifest и затем снимает checkpoint-ы. Для CI или быстрой проверки контракта без PNG:
Скрипт последовательно проверяет headless-загрузку проекта, renderer materials,
M2 unique-id dedupe, regression manifest, coordinate boundaries и dedicated
server-spawn renderer contract, затем снимает checkpoint-ы. Для CI или быстрой
проверки контракта без PNG:
```powershell
.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1
@@ -18,6 +21,21 @@
Если `godot` отсутствует в `PATH`, скрипт использует локальный executable, указанный в `RENDER.md`. Другой executable задаётся через `-GodotPath`.
Отдельный M01 server-spawn checkpoint использует тот же capture pipeline, но не
расширяет семичастный M00 visual baseline:
```powershell
godot --headless --path . --script res://src/tools/verify_server_spawn_renderer.gd
godot --path . --script res://src/tools/capture_render_checkpoints.gd -- `
--manifest res://src/tools/server_spawn_render_manifest.json `
--output user://server_spawn_renderer
```
Manifest связывает pinned AzerothCore Human Warrior spawn с Godot position,
ADT tile/chunk и renderer-native diagnostic marker. Маркер начинается точно в
spawn origin и проходит обычный depth test, поэтому screenshot показывает его
отношение к terrain; это integration evidence, а не заявление visual parity.
Результат по умолчанию находится в `user://render_baseline`:
- `report.json` — Godot, OS, CPU, GPU/backend, viewport, revision, cache inventory, cache versions, параметры checkpoint-ов и результаты;
+23 -4
View File
@@ -108,6 +108,21 @@ 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
@@ -138,10 +153,12 @@ grid ownership rules.
- 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.
- 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.
@@ -154,6 +171,8 @@ grid ownership rules.
- 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
+28 -8
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | `M01-FND-COORDS-001`, `M01-FND-COORD-TILE-AXIS-002`, `M01-QAR-COORD-FIXTURES-001` |
| Target/work package | `M01-FND-COORDS-001`, `M01-FND-COORD-TILE-AXIS-002`, `M01-QAR-COORD-FIXTURES-001`, `M01-FND-COORD-BOUNDARY-GATE-001`, `M01-QAR-SERVER-SPAWN-RENDERER-001` |
| Owners | Foundation/domain |
| Last verified | Worktree `work/sindo-main-codex/m01-coordinate-fixtures`, 2026-07-13 |
| Last verified | Worktree `work/sindo-main-codex/m01-server-spawn-renderer`, 2026-07-13 |
| Profiles/capabilities | All profiles; contract version 2 |
## Purpose
@@ -69,6 +69,9 @@ Forbidden dependencies:
| `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.godot_to_adt_tile` | Pure static method | Resolve filename indices from typed renderer position | Synchronous; new result value | Result remains unclamped |
| `CoordinateMapper.godot_to_adt_tile_local` | Pure static method | Resolve east/height/south offsets from typed renderer position | Synchronous; new result value | Caller validates ownership/range |
| `CoordinateMapper.adt_tile_local_to_godot` | Pure static method | Compose typed renderer position from tile/local values | 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 |
@@ -100,12 +103,19 @@ flowchart LR
RawADTFixture[Raw ADT numeric metadata] --> FixtureVerifier
ClientFixture[Build 12340 observations] --> FixtureVerifier
FixtureVerifier --> Mapper
ServerFixture --> SpawnManifest[Server spawn render manifest]
SpawnManifest --> SpawnVerifier[Renderer checkpoint verifier]
SpawnVerifier --> Capture[Live renderer capture]
Capture --> Marker[Diagnostic marker at mapped spawn]
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]
Canonical -->|north-west edge offsets| Local[AdtTileLocalPosition]
Local -->|floor offset / chunk size| Chunk[AdtChunkCoordinate]
Canonical -->|half extent - Y; Z; half extent - X| Godot[GodotWorldPosition]
Godot -->|typed direct adapter| Tile
Godot -->|typed direct adapter| Local
Tile -->|tile plus local| Godot
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
```
@@ -172,7 +182,13 @@ raw three-number arrays are not an accepted new persistence contract.
- 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.
- Boundary enforcement: `src/tools/verify_coordinate_conversion_boundaries.gd`.
- Server-spawn renderer contract: `src/tools/verify_server_spawn_renderer.gd`
validates the dedicated manifest against the same pinned fixture before GUI capture.
- Integration/E2E: sky, player, streamer and terrain probe use mapper boundaries;
`StreamingFocus` provides typed renderer focus, and the pinned AzerothCore
Human Warrior spawn is captured in the Eastern Kingdoms renderer at ADT
`(32,48)`, chunk `(3,12)`.
- Fidelity evidence: five build-12340 camera positions from the M00 manifest map
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)`;
@@ -192,18 +208,19 @@ raw three-number arrays are not an accepted new persistence contract.
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Canonical/server/Godot position mapping | Implemented | Headless round-trip and five M00 golden points | Migrate producers/consumers |
| Canonical/server/Godot position mapping | Implemented | Headless round-trip, five M00 golden points and migrated GDScript consumers | Network/server adapters remain future consumers |
| ADT placement mapping | Implemented | Synthetic/direct round-trip fixtures | Compare native parsed placements during migration |
| 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, raw `Azeroth_31_49.adt` evidence and direct Godot adapter round-trip | Native parser remains explicit boundary |
| 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, headless tests and repository-wide conversion gate | Maintain gate with new consumers |
| 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 |
| Visual/server-spawn integration | Implemented | Dedicated manifest/verifier plus cold/warm GUI capture of the mapped AzerothCore spawn | This is coordinate/renderer integration evidence, not original-client visual parity |
## Known gaps and risks
- Existing native loaders and renderer scripts still contain manual conversions.
- Native ADT/WDT parsing retains one shared boundary formula because worker
parsers cannot call GDScript; the gate prevents additional definitions.
- Contract version 1 briefly transposed ADT filename/chunk axes; version 2 fixes
the defect before any consumer migration.
- MDDF/MODF rotation comments and implementations disagree across existing paths;
@@ -225,6 +242,9 @@ raw three-number arrays are not an accepted new persistence contract.
| `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 |
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
## Related decisions and references
+15 -3
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; декомпозиция M02–M03 |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; декомпозиция M02–M03 |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m01-streaming-focus`, 2026-07-13 |
| Last verified | Worktree `work/sindo-main-codex/m01-server-spawn-renderer`, 2026-07-13 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -29,6 +29,8 @@ flowchart LR
Player[Player or spectator Node3D] --> Adapter[Explicit source adapter]
Editor[Editor viewport adapter] --> Focus[StreamingFocus]
Capture[Capture tool] --> Adapter
SpawnFixture[Pinned server spawn fixture] --> SpawnManifest[Spawn render manifest]
SpawnManifest --> Capture
Adapter --> Focus
Focus --> Loader[StreamingWorldLoader]
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
@@ -84,9 +86,11 @@ Forbidden dependencies:
| Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Metrics/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
Side effects:
@@ -102,6 +106,9 @@ Side effects:
flowchart TD
P[Player/spectator Node3D] --> A[Explicit scene adapter]
C[Capture camera] --> A
SF[Pinned server spawn] --> SM[Validated spawn manifest]
SM --> C
SM --> Marker[Renderer-native origin marker]
E[Editor viewport] --> EA[Editor adapter]
A --> F[StreamingFocus]
EA --> F
@@ -119,6 +126,7 @@ flowchart TD
M2 --> World
WMO --> World
Liquid --> World
Marker --> World
F --> E[Eviction/retention decisions]
E --> World
```
@@ -224,7 +232,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: streaming-focus contract/wiring, material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
- Manual diagnostics: [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md) and [`../../RENDER.md`](../../RENDER.md).
@@ -247,6 +256,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
| Stable renderer facade | Planned | M03 | Current caller uses monolithic Node API |
## Known gaps and risks
@@ -287,6 +297,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Headless ownership regression for detached runtime prototypes, resource caches and empty liquid roots |
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
| `src/tools/server_spawn_render_manifest.json` | Dedicated mapped AzerothCore spawn checkpoint and diagnostic marker configuration |
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer checkpoint contract verification |
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
@@ -48,6 +48,36 @@ static func godot_to_canonical(godot_position: GodotWorldPosition) -> CanonicalW
)
## Returns ADT filename indices directly from a Godot renderer position.
static func godot_to_adt_tile(godot_position: GodotWorldPosition) -> AdtTileCoordinate:
return AdtTileCoordinate.new(
int(floor(godot_position.x_units / ADT_TILE_SIZE_YARDS)),
int(floor(godot_position.z_units / ADT_TILE_SIZE_YARDS))
)
## Returns east/height/south offsets inside the owning ADT tile.
static func godot_to_adt_tile_local(godot_position: GodotWorldPosition) -> AdtTileLocalPosition:
var tile_coordinate := godot_to_adt_tile(godot_position)
return AdtTileLocalPosition.new(
godot_position.x_units - float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS,
godot_position.y_units,
godot_position.z_units - float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS
)
## Converts an ADT tile/local pair directly to the Godot renderer basis.
static func adt_tile_local_to_godot(
tile_coordinate: AdtTileCoordinate,
local_position: AdtTileLocalPosition
) -> GodotWorldPosition:
return GodotWorldPosition.new(
float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS + local_position.east_yards,
local_position.height_yards,
float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS + local_position.south_yards
)
## Converts canonical WoW coordinates to MDDF/MODF X/height/Z file fields.
static func canonical_to_adt_placement(canonical_position: CanonicalWowWorldPosition) -> AdtPlacementPosition:
return AdtPlacementPosition.new(
@@ -3,9 +3,12 @@ extends CharacterBody3D
const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const UNIT_SIZE := CHUNK_SIZE / 8.0
@export var extracted_dir: String = "res://data/extracted"
@@ -62,8 +65,11 @@ func _ready() -> void:
_camera.current = true
_camera.far = 50000.0
if spawn_at_tile_center:
global_position.x = (float(spawn_tile_x) + 0.5) * TILE_SIZE
global_position.z = (float(spawn_tile_y) + 0.5) * TILE_SIZE
var spawn_tile = ADT_TILE_COORDINATE_SCRIPT.new(spawn_tile_x, spawn_tile_y)
var half_tile_size := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS * 0.5
var spawn_local = ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, global_position.y, half_tile_size)
var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local)
global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units)
_load_character_visual()
var ground := _sample_ground_height(global_position)
if is_finite(ground):
@@ -324,16 +330,17 @@ func _apply_camera_transform() -> void:
func _sample_ground_height(world_pos: Vector3) -> float:
var tx := int(floor(world_pos.x / TILE_SIZE))
var ty := int(floor(world_pos.z / TILE_SIZE))
var data := _load_adt(tx, ty)
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
var data := _load_adt(tile_coordinate.tile_x, tile_coordinate.tile_y)
if data.is_empty():
return NAN
var tile_origin := _get_tile_origin(data)
var local := world_pos - tile_origin
var chunk_x := clampi(int(floor(local.x / CHUNK_SIZE)), 0, 15)
var chunk_y := clampi(int(floor(local.z / CHUNK_SIZE)), 0, 15)
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
var chunk := _find_chunk(data, chunk_x, chunk_y, tile_origin)
if chunk.is_empty():
return NAN
+15 -13
View File
@@ -2,9 +2,9 @@
extends Node
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const TILE_SIZE := 533.33333
const WOW_WORLD_CENTER := 17066.666
const LIGHT_COORD_SCALE := 36.0
const HALF_MINUTES_PER_DAY := 2880
@@ -701,28 +701,30 @@ func _get_target_wow_position() -> Vector3:
var world_pos := Vector3.ZERO
if _target:
world_pos = _target.global_position
return Vector3(
WOW_WORLD_CENTER - world_pos.z,
WOW_WORLD_CENTER - world_pos.x,
world_pos.y)
var canonical_position = COORDINATE_MAPPER_SCRIPT.godot_to_canonical(_typed_godot_position(world_pos))
return Vector3(canonical_position.x_yards, canonical_position.y_yards, canonical_position.z_yards)
func _get_target_area_id() -> int:
if not _target or map_name.is_empty():
return 0
var world_pos := _target.global_position
var tile_x := int(floor(world_pos.x / TILE_SIZE))
var tile_y := int(floor(world_pos.z / TILE_SIZE))
var local_x := world_pos.x - float(tile_x) * TILE_SIZE
var local_y := world_pos.z - float(tile_y) * TILE_SIZE
var chunk_x := clampi(int(floor(local_x / (TILE_SIZE / 16.0))), 0, 15)
var chunk_y := clampi(int(floor(local_y / (TILE_SIZE / 16.0))), 0, 15)
var areas := _load_adt_area_grid(tile_x, tile_y)
var typed_world_position = _typed_godot_position(world_pos)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
var local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, local_position)
var areas := _load_adt_area_grid(tile_coordinate.tile_x, tile_coordinate.tile_y)
if areas.is_empty():
return 0
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
return int(areas[chunk_y * 16 + chunk_x])
func _typed_godot_position(world_position: Vector3):
return GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
func _load_adt_area_grid(tile_x: int, tile_y: int) -> PackedInt32Array:
var key := "%d_%d" % [tile_x, tile_y]
if _adt_area_cache.has(key):
+36 -25
View File
@@ -14,14 +14,17 @@ const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/l
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
const M2_MATERIAL_REFRESH_VERSION := 2
const WMO_MATERIAL_REFRESH_VERSION := 10
const TILE_SIZE := 533.33333
const CHUNK_SIZE := TILE_SIZE / 16.0
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const QUALITY_CUSTOM := "Custom"
@@ -786,10 +789,9 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
var parts: PackedStringArray = key.split("_")
var tx: int = int(parts[0])
var ty: int = int(parts[1])
var tcx: float = (tx + 0.5) * TILE_SIZE
var tcz: float = (ty + 0.5) * TILE_SIZE
var dx: float = tcx - focus_pos.x
var dz: float = tcz - focus_pos.z
var tile_center := _tile_center_to_world(tx, ty)
var dx: float = tile_center.x - focus_pos.x
var dz: float = tile_center.z - focus_pos.z
_tile_load_queue.append({
"key": key,
"tx": tx,
@@ -2909,8 +2911,10 @@ func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
if threshold <= 0.0:
return result
var local_x := fposmod(focus_pos.x, TILE_SIZE) / TILE_SIZE
var local_z := fposmod(focus_pos.z, TILE_SIZE) / TILE_SIZE
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
var local_x: float = tile_local_position.east_yards / TILE_SIZE
var local_z: float = tile_local_position.south_yards / TILE_SIZE
var x_offsets: Array[int] = [0]
var z_offsets: Array[int] = [0]
@@ -3033,10 +3037,17 @@ func _is_tile_chunk_set_ready(state: Dictionary) -> bool:
func _tile_dist_sq(state: Dictionary, cam_pos: Vector3) -> float:
var min_x: float = float(state["tx"]) * TILE_SIZE
var max_x: float = min_x + TILE_SIZE
var min_z: float = float(state["ty"]) * TILE_SIZE
var max_z: float = min_z + TILE_SIZE
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(int(state["tx"]), int(state["ty"]))
var north_west_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(0.0, 0.0, 0.0))
var south_east_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(TILE_SIZE, 0.0, TILE_SIZE))
var min_x: float = north_west_position.x_units
var max_x: float = south_east_position.x_units
var min_z: float = north_west_position.z_units
var max_z: float = south_east_position.z_units
var dx := 0.0
if cam_pos.x < min_x:
@@ -5397,9 +5408,9 @@ func _position_camera_over_world() -> void:
if camera == null:
return
var center_tx: float = (_tile_min.x + _tile_max.x) * 0.5
var center_ty: float = (_tile_min.y + _tile_max.y) * 0.5
var center := _tile_center_to_world(center_tx, center_ty)
var minimum_tile_center := _tile_center_to_world(_tile_min.x, _tile_min.y)
var maximum_tile_center := _tile_center_to_world(_tile_max.x, _tile_max.y)
var center := (minimum_tile_center + maximum_tile_center) * 0.5
var tile_span: int = int(max(_tile_max.x - _tile_min.x + 1, _tile_max.y - _tile_min.y + 1))
var height: float = max(2000.0, float(tile_span) * TILE_SIZE * 0.18)
@@ -5408,19 +5419,19 @@ func _position_camera_over_world() -> void:
_camera_initialized = true
## Tile (tx, ty) NW corner sits at Godot (tx*TILE_SIZE, 0, ty*TILE_SIZE).
## This matches wow_to_godot output: chunk origins ≈ (TX*TILE_SIZE, h, TY*TILE_SIZE).
func _tile_center_to_world(tile_x: float, tile_y: float) -> Vector3:
return Vector3(
(tile_x + 0.5) * TILE_SIZE,
0.0,
(tile_y + 0.5) * TILE_SIZE)
func _tile_center_to_world(tile_x: int, tile_y: int) -> Vector3:
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
var half_tile_size := TILE_SIZE * 0.5
var tile_center_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
tile_coordinate,
ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, 0.0, half_tile_size))
return Vector3(tile_center_position.x_units, tile_center_position.y_units, tile_center_position.z_units)
func _world_to_tile(world_pos: Vector3) -> Vector2i:
return Vector2i(
int(floor(world_pos.x / TILE_SIZE)),
int(floor(world_pos.z / TILE_SIZE)))
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
func _tile_key(tx: int, ty: int) -> String:
+39
View File
@@ -246,6 +246,8 @@ func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: flo
func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
if bool(checkpoint.get("diagnostic_spawn_marker", false)):
return null if dry_run else _create_diagnostic_spawn_marker(checkpoint)
var rel_path := String(checkpoint.get("probe_model", ""))
if rel_path.is_empty() or dry_run:
return null
@@ -265,6 +267,43 @@ func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
return M2_NATIVE_ANIMATED_BUILDER.build(data, extracted_dir)
## Creates a renderer-native marker whose origin is the exact mapped server
## spawn. The vertical mast grows upward from that origin, so terrain occlusion
## remains visible instead of being hidden by an always-on-top overlay.
func _create_diagnostic_spawn_marker(checkpoint: Dictionary) -> Node3D:
var marker_root := Node3D.new()
marker_root.name = "DiagnosticServerSpawnMarker"
var marker_height := maxf(float(checkpoint.get("diagnostic_marker_height", 6.0)), 0.5)
var marker_radius := maxf(float(checkpoint.get("diagnostic_marker_radius", 0.35)), 0.05)
var marker_material := StandardMaterial3D.new()
marker_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
marker_material.albedo_color = Color(1.0, 0.05, 0.7, 1.0)
marker_material.emission_enabled = true
marker_material.emission = Color(1.0, 0.01, 0.35, 1.0)
var mast_mesh := CylinderMesh.new()
mast_mesh.height = marker_height
mast_mesh.top_radius = marker_radius
mast_mesh.bottom_radius = marker_radius
mast_mesh.material = marker_material
var mast := MeshInstance3D.new()
mast.name = "SpawnOriginMast"
mast.mesh = mast_mesh
mast.position.y = marker_height * 0.5
marker_root.add_child(mast)
var origin_mesh := SphereMesh.new()
origin_mesh.radius = marker_radius * 1.8
origin_mesh.height = marker_radius * 3.6
origin_mesh.material = marker_material
var origin := MeshInstance3D.new()
origin.name = "SpawnOrigin"
origin.mesh = origin_mesh
marker_root.add_child(origin)
return marker_root
func _set_sky_time(world: Node, time_hours: float) -> void:
var sky := world.get_node_or_null("WowSkyController")
if sky != null:
+10 -7
View File
@@ -5,7 +5,8 @@ extends SceneTree
## [--wait 3.0] [--output user://render_terrain_height/report.json]
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const TILE_SIZE := 533.33333
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
const RAY_HEIGHT := 5000.0
@@ -90,9 +91,7 @@ func _run_async() -> void:
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
var tile_coordinate := Vector2i(
int(floor(world_position.x / TILE_SIZE)),
int(floor(world_position.z / TILE_SIZE)))
var tile_coordinate := _adt_tile_vector2i(world_position)
var tile_states: Dictionary = world.get("_tile_states")
var highest_terrain_height := -INF
var intersected_tile_key := ""
@@ -198,9 +197,7 @@ func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictiona
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
var sample_tile := Vector2i(
int(floor(sample_position.x / TILE_SIZE)),
int(floor(sample_position.z / TILE_SIZE)))
var sample_tile := _adt_tile_vector2i(sample_position)
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
if not tile_states.has(sample_key):
continue
@@ -214,6 +211,12 @@ func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictiona
return {"nearest_sample_distance": null}
func _adt_tile_vector2i(world_position: Vector3) -> Vector2i:
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
func _vector3_array(value: Vector3) -> Array[float]:
return [value.x, value.y, value.z]
@@ -0,0 +1,29 @@
{
"schema_version": 1,
"profile": "Blizzlike335",
"scene": "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"map": "Azeroth",
"quality_preset": "High",
"viewport": [1280, 900],
"camera_fov": 62.0,
"default_wait_seconds": 10.0,
"default_measure_seconds": 1.0,
"source_fixture": "res://src/tests/fixtures/coordinate_golden_points.json",
"source_spawn_name": "azerothcore_human_warrior_start",
"required_coverage": ["server_spawn"],
"checkpoints": [
{
"name": "azerothcore_human_warrior_start",
"coverage": ["server_spawn"],
"camera": [17168.0, 105.0, 25984.0],
"target": [17199.159666667, 83.5312, 26016.616666667],
"player": [17199.159666667, 83.5312, 26016.616666667],
"time_hours": 13.0,
"diagnostic_spawn_marker": true,
"diagnostic_marker_height": 6.0,
"diagnostic_marker_radius": 0.35,
"expected_adt_tile": [32, 48],
"expected_adt_chunk": [3, 12]
}
]
}
@@ -0,0 +1,140 @@
extends SceneTree
## Repository-wide M01 gate against manual cross-space world coordinate formulas.
## Model-local M2/WMO basis transforms and ordinary Godot-space geometry are not
## world-space conversions and are intentionally outside these signatures.
const SCAN_ROOTS: Array[String] = ["res://src", "res://addons"]
const SOURCE_EXTENSIONS: Array[String] = ["gd", "cpp", "h", "hpp"]
const EXCLUDED_DIRECTORY_PREFIXES: Array[String] = ["res://src/native/build/"]
const GATE_PATH := "res://src/tools/verify_coordinate_conversion_boundaries.gd"
const COORDINATE_MAPPER_PATH := "res://src/domain/coordinates/coordinate_mapper.gd"
const CALIBRATION_ORACLE_PATH := "res://src/tools/verify_render_coordinate_calibration.gd"
const NATIVE_BOUNDARY_PATH := "res://src/native/src/wow_chunk_reader.h"
const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
NATIVE_BOUNDARY_PATH,
"res://src/native/src/adt_loader.cpp",
"res://src/native/src/wdt_loader.cpp",
CALIBRATION_ORACLE_PATH,
]
const REQUIRED_MAPPER_CALLS := {
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
"res://src/scenes/player/third_person_wow_controller.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
}
func _initialize() -> void:
var failures: Array[String] = []
var source_paths: Array[String] = []
for scan_root in SCAN_ROOTS:
_collect_source_paths(scan_root, source_paths, failures)
var native_boundary_definition_count := 0
var independent_oracle_definition_count := 0
for source_path in source_paths:
if source_path == GATE_PATH:
continue
var source := _read_text(source_path, failures)
var source_lines := source.split("\n")
for line_index in range(source_lines.size()):
var source_line := String(source_lines[line_index])
var violation := _coordinate_violation(source_path, source_line)
if not violation.is_empty():
failures.append("%s:%d %s" % [source_path, line_index + 1, violation])
if source_path == NATIVE_BOUNDARY_PATH and source_line.contains("inline void wow_to_godot"):
native_boundary_definition_count += 1
if source_path == CALIBRATION_ORACLE_PATH and source_line.contains("func _wow_to_godot"):
independent_oracle_definition_count += 1
_expect_equal(native_boundary_definition_count, 1, "native world conversion definition count", failures)
_expect_equal(independent_oracle_definition_count, 1, "independent calibration oracle count", failures)
_verify_required_mapper_calls(failures)
_verify_classifier_guards(failures)
if not failures.is_empty():
for failure in failures:
push_error("COORDINATE_CONVERSION_BOUNDARIES: %s" % failure)
quit(1)
return
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=4 classifier_cases=6" % source_paths.size())
quit(0)
func _coordinate_violation(source_path: String, source_line: String) -> String:
var stripped_line := source_line.strip_edges()
if stripped_line.is_empty():
return ""
if (source_line.contains("wow_to_godot") or source_line.contains("godot_to_wow")) and source_path not in ALLOWED_LEGACY_NAME_PATHS:
return "manual WoW/Godot conversion name outside mapper boundary"
if (source_line.contains("17066.666") or source_line.contains("WOW_WORLD_CENTER")) and source_path not in [NATIVE_BOUNDARY_PATH, CALIBRATION_ORACLE_PATH]:
return "manual WoW world-center formula outside native boundary/oracle"
if source_line.contains("1600.0 / 3.0") and source_path != COORDINATE_MAPPER_PATH:
return "duplicated ADT tile-size formula outside CoordinateMapper"
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("floor(") and source_line.contains("TILE_SIZE") and (source_line.contains(".x") or source_line.contains(".z")):
return "manual Godot world-position to ADT tile formula"
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("fposmod(") and source_line.contains("TILE_SIZE"):
return "manual Godot world-position to ADT tile-local formula"
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("+ 0.5) * TILE_SIZE"):
return "manual ADT tile-center to Godot world-position formula"
return ""
func _verify_required_mapper_calls(failures: Array[String]) -> void:
for source_path in REQUIRED_MAPPER_CALLS:
var source := _read_text(source_path, failures)
for required_call in REQUIRED_MAPPER_CALLS[source_path]:
if not source.contains(String(required_call)):
failures.append("%s must call CoordinateMapper.%s" % [source_path, required_call])
func _verify_classifier_guards(failures: Array[String]) -> void:
_expect_true(not _coordinate_violation("res://src/example.gd", "const CENTER := 17066.666").is_empty(), "world-center classifier", failures)
_expect_true(not _coordinate_violation("res://src/example.gd", "func wow_to_godot():").is_empty(), "conversion-name classifier", failures)
_expect_true(not _coordinate_violation("res://src/example.gd", "floor(world_position.x / TILE_SIZE)").is_empty(), "world-to-tile classifier", failures)
_expect_true(not _coordinate_violation("res://src/example.gd", "fposmod(world_position.z, TILE_SIZE)").is_empty(), "world-to-tile-local classifier", failures)
_expect_true(not _coordinate_violation("res://src/example.gd", "(tile_x + 0.5) * TILE_SIZE").is_empty(), "tile-center classifier", failures)
_expect_true(_coordinate_violation(NATIVE_BOUNDARY_PATH, "inline void wow_to_godot(float wx) { gx = -(wx - 17066.666f); }").is_empty(), "native boundary exception", failures)
func _collect_source_paths(directory_path: String, source_paths: Array[String], failures: Array[String]) -> void:
var directory := DirAccess.open(directory_path)
if directory == null:
failures.append("cannot scan directory %s" % directory_path)
return
for entry_name in directory.get_files():
var source_path := directory_path.path_join(entry_name)
if entry_name.get_extension().to_lower() in SOURCE_EXTENSIONS:
source_paths.append(source_path)
for child_directory_name in directory.get_directories():
var child_directory_path := directory_path.path_join(child_directory_name) + "/"
if _has_excluded_prefix(child_directory_path):
continue
_collect_source_paths(child_directory_path.trim_suffix("/"), source_paths, failures)
func _has_excluded_prefix(path: String) -> bool:
for excluded_prefix in EXCLUDED_DIRECTORY_PREFIXES:
if path.begins_with(excluded_prefix):
return true
return false
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
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)
@@ -0,0 +1 @@
uid://bnj844sd2n6ln
+12
View File
@@ -119,6 +119,18 @@ func _verify_tile_and_chunk_boundaries(failures: Array[String]) -> void:
_expect_true(local_position.east_yards >= 0.0 and local_position.east_yards < tile_size, "tile-local east offset is half-open", failures)
_expect_true(local_position.south_yards >= 0.0 and local_position.south_yards < tile_size, "tile-local south offset is half-open", failures)
var godot_position = CoordinateMapperScript.canonical_to_godot(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0))
var direct_godot_tile = CoordinateMapperScript.godot_to_adt_tile(godot_position)
var direct_godot_local = CoordinateMapperScript.godot_to_adt_tile_local(godot_position)
var direct_godot_round_trip = CoordinateMapperScript.adt_tile_local_to_godot(direct_godot_tile, direct_godot_local)
_expect_equal(direct_godot_tile.tile_x, 31, "direct Godot ADT tile X", failures)
_expect_equal(direct_godot_tile.tile_y, 49, "direct Godot ADT tile Y", failures)
_expect_near(direct_godot_local.east_yards, 146.66677, GOLDEN_POSITION_TOLERANCE_YARDS, "direct Godot tile-local east", failures)
_expect_near(direct_godot_local.south_yards, 86.66677, GOLDEN_POSITION_TOLERANCE_YARDS, "direct Godot tile-local south", failures)
_expect_near(direct_godot_round_trip.x_units, godot_position.x_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local X round trip", failures)
_expect_near(direct_godot_round_trip.y_units, godot_position.y_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local Y round trip", failures)
_expect_near(direct_godot_round_trip.z_units, godot_position.z_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local Z round trip", failures)
var chunk_local_position = AdtTileLocalPositionScript.new(
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 2.0,
180.0,
+138
View File
@@ -0,0 +1,138 @@
extends SceneTree
## Verifies that the pinned AzerothCore spawn is routed through the coordinate
## contract into an executable renderer checkpoint. GUI capture is performed by
## capture_render_checkpoints.gd with the validated dedicated manifest.
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
const SERVER_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/server_world_position.gd")
const MANIFEST_PATH := "res://src/tools/server_spawn_render_manifest.json"
const EXPECTED_FIXTURE_PATH := "res://src/tests/fixtures/coordinate_golden_points.json"
const EXPECTED_SPAWN_NAME := "azerothcore_human_warrior_start"
const EXPECTED_PROFILE := "Blizzlike335"
const POSITION_TOLERANCE_YARDS := 0.002
func _initialize() -> void:
var failures: Array[String] = []
var manifest := _load_json_object(MANIFEST_PATH, failures)
var fixture_path := String(manifest.get("source_fixture", ""))
_expect_equal(fixture_path, EXPECTED_FIXTURE_PATH, "source fixture path", failures)
var fixture := _load_json_object(fixture_path, failures) if not fixture_path.is_empty() else {}
if manifest.is_empty() or fixture.is_empty():
_report_and_quit(failures)
return
_expect_equal(int(manifest.get("schema_version", -1)), 1, "manifest schema version", failures)
_expect_equal(String(manifest.get("profile", "")), EXPECTED_PROFILE, "renderer profile", failures)
_expect_equal(String(manifest.get("map", "")), "Azeroth", "renderer map", failures)
_expect_true(ResourceLoader.exists(String(manifest.get("scene", ""))), "renderer scene exists", failures)
_expect_equal(String(manifest.get("source_spawn_name", "")), EXPECTED_SPAWN_NAME, "source spawn name", failures)
var spawn := _find_named_dictionary(fixture.get("server_spawns", []), EXPECTED_SPAWN_NAME)
_expect_true(not spawn.is_empty(), "pinned server spawn exists", failures)
var checkpoints: Array = manifest.get("checkpoints", [])
_expect_equal(checkpoints.size(), 1, "renderer checkpoint count", failures)
if spawn.is_empty() or checkpoints.size() != 1 or not (checkpoints[0] is Dictionary):
_report_and_quit(failures)
return
var checkpoint: Dictionary = checkpoints[0]
var server_values := _three_floats(spawn.get("server_position", null), "server position", failures)
if server_values.is_empty():
_report_and_quit(failures)
return
var server_position = SERVER_WORLD_POSITION_SCRIPT.new(server_values[0], server_values[1], server_values[2])
var canonical_position = COORDINATE_MAPPER_SCRIPT.server_to_canonical(server_position)
var godot_position = COORDINATE_MAPPER_SCRIPT.canonical_to_godot(canonical_position)
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.canonical_to_adt_tile(canonical_position)
var tile_local_position = COORDINATE_MAPPER_SCRIPT.canonical_to_adt_tile_local(canonical_position)
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
var mapped_position := [godot_position.x_units, godot_position.y_units, godot_position.z_units]
_expect_equal(String(checkpoint.get("name", "")), EXPECTED_SPAWN_NAME, "checkpoint name", failures)
_expect_true("server_spawn" in checkpoint.get("coverage", []), "server-spawn coverage", failures)
_expect_true(bool(checkpoint.get("diagnostic_spawn_marker", false)), "diagnostic spawn marker enabled", failures)
_expect_three_near(checkpoint.get("target", null), mapped_position, "checkpoint target", failures)
_expect_three_near(checkpoint.get("player", null), mapped_position, "checkpoint player", failures)
_expect_pair(checkpoint.get("expected_adt_tile", null), [tile_coordinate.tile_x, tile_coordinate.tile_y], "ADT tile", failures)
_expect_pair(checkpoint.get("expected_adt_chunk", null), [chunk_coordinate.chunk_x, chunk_coordinate.chunk_y], "ADT chunk", failures)
_expect_three_near(spawn.get("expected_godot_position", null), mapped_position, "fixture Godot position", failures)
var camera_values := _three_floats(checkpoint.get("camera", null), "checkpoint camera", failures)
if not camera_values.is_empty():
_expect_true(camera_values[1] > godot_position.y_units, "checkpoint camera is above spawn", failures)
_expect_true(Vector3(camera_values[0], camera_values[1], camera_values[2]).distance_to(Vector3(mapped_position[0], mapped_position[1], mapped_position[2])) > 1.0, "checkpoint camera is distinct from spawn", failures)
if not failures.is_empty():
_report_and_quit(failures)
return
print("SERVER_SPAWN_RENDERER PASS spawn=1 checkpoint=1 tile=%d_%d chunk=%d_%d profile=%s" % [
tile_coordinate.tile_x,
tile_coordinate.tile_y,
chunk_coordinate.chunk_x,
chunk_coordinate.chunk_y,
EXPECTED_PROFILE,
])
quit(0)
func _find_named_dictionary(entries: Array, expected_name: String) -> Dictionary:
for entry_variant in entries:
if entry_variant is Dictionary and String(entry_variant.get("name", "")) == expected_name:
return entry_variant
return {}
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: %s" % path)
return {}
var parsed = JSON.parse_string(file.get_as_text())
if not (parsed is Dictionary):
failures.append("JSON is not an object: %s" % path)
return {}
return parsed
func _three_floats(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_variant, expected_values: Array, label: String, failures: Array[String]) -> void:
var actual_values := _three_floats(actual_variant, label, failures)
if actual_values.is_empty() or expected_values.size() != 3:
return
for component_index in range(3):
if absf(actual_values[component_index] - float(expected_values[component_index])) > POSITION_TOLERANCE_YARDS:
failures.append("%s[%d] expected %.9f, got %.9f" % [label, component_index, expected_values[component_index], actual_values[component_index]])
func _expect_pair(actual_variant, expected_values: Array, label: String, failures: Array[String]) -> void:
if not (actual_variant is Array) or actual_variant.size() != 2:
failures.append("%s must contain two integers" % label)
return
for component_index in range(2):
_expect_equal(int(actual_variant[component_index]), int(expected_values[component_index]), "%s[%d]" % [label, component_index], failures)
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 _report_and_quit(failures: Array[String]) -> void:
if failures.is_empty():
failures.append("verification stopped without a diagnostic")
for failure in failures:
push_error("SERVER_SPAWN_RENDERER: %s" % failure)
quit(1)
@@ -0,0 +1 @@
uid://c7f2rpfey0vrs
+41 -15
View File
@@ -1,6 +1,7 @@
# M01 — Coordinates and Architecture Seams
<!-- OPENWC_TARGET:ACTIVE -->
<!-- OPENWC_TARGET:DONE -->
<!-- OPENWC_TARGET_DONE:M01:work/sindo-main-codex/m01-integrator-closeout:2026-07-14 -->
## Outcome
@@ -8,13 +9,13 @@
## Steps
- [ ] Создать domain/value-object слой без `Node`/`Resource` dependencies.
- [ ] Определить canonical WoW, Godot, ADT tile/chunk и local coordinate types.
- [ ] Реализовать единственный `CoordinateMapper` для position и orientation.
- [ ] Собрать golden points из ADT, server spawn и оригинального клиента.
- [ ] Ввести `StreamingFocus` вместо неявной зависимости от active Camera3D.
- [ ] Ввести stable `ContentId`, `EntityId` и отличить их от WoW GUID/server entry.
- [ ] Запретить ручные axis conversion через code review rule/test search.
- [x] Создать domain/value-object слой без `Node`/`Resource` dependencies.
- [x] Определить canonical WoW, Godot, ADT tile/chunk и local coordinate types.
- [x] Реализовать единственный `CoordinateMapper` для position и orientation.
- [x] Собрать golden points из ADT, server spawn и оригинального клиента.
- [x] Ввести `StreamingFocus` вместо неявной зависимости от active Camera3D.
- [x] Ввести stable `ContentId`, `EntityId` и отличить их от WoW GUID/server entry.
- [x] Запретить ручные axis conversion через code review rule/test search.
## Verification
@@ -28,10 +29,35 @@ Renderer/test scene получают focus через контракт; коор
## Evidence
- Date:
- Revision/worktree:
- Commands:
- Results:
- Fidelity comparison:
- Changed files:
- Remaining risks:
- Date: 2026-07-14
- Revision/worktree: merged master `c8e99f2`; closeout
`work/sindo-main-codex/m01-integrator-closeout`
- Commands: `tools/run_render_baseline.ps1 -DryRun -WaitSeconds 0.1
-MeasureSeconds 0.1`; `verify_coordinate_mapper.gd`;
`verify_coordinate_golden_fixtures.gd`; `verify_streaming_focus.gd`;
`verify_domain_identities.gd`; coordination/documentation/diff gates.
- Results: CoordinateMapper PASS (`5` golden points, `8` tile boundaries,
`2` raw MCNK, `5` yaw); golden fixtures PASS (`1` server spawn, `1` raw ADT,
`5` original-client points); StreamingFocus PASS (`1` contract, `2` runtime
scenes, `3` capture tools); identities PASS (`5/5/6/5`); conversion boundary
PASS (`84` files, one native boundary, one oracle, four consumers, six
classifiers); server-spawn renderer PASS at ADT `(32,48)`, MCNK `(3,12)`;
renderer manifest PASS (`7/7/7`) and calibration maximum error `0.000015`.
- Fidelity comparison: five build-12340 camera observations, pinned AzerothCore
Human Warrior spawn and private raw ADT metadata agree through coordinate
contract version 2. Cache-complete cold/warm GUI capture places the mapped
spawn marker on rendered Northshire terrain with focus `(32,48)`, quality
`100/100`, missing cache `0`; PNG SHA-256 values are recorded in the server
spawn handoff. The final ADT-boundary regression passed `2/2`; the full M01
regression captured `14/14`, with `11` strict passes and three low-mean
timing/animation-phase differences rather than coordinate drift.
- Changed files: `src/domain/coordinates/`, `src/domain/identity/`,
`src/domain/streaming/`, migrated sky/player/streaming/terrain consumers,
coordinate/renderer fixtures and tools, runtime scenes, ADR 0001/0002,
coordinate/domain/renderer module specs, coding/render documentation and
M01 coordination claims.
- Remaining risks: TrinityCore populated spawn equivalence is not verified;
native parser conversion remains one separately calibrated boundary; MDDF/
MODF placement Euler parity is outside world-yaw scope; renderer visual parity
gaps and the known anonymous ObjectDB teardown warning continue. No `1:1`
compatibility claim is made by M01.
+1 -1
View File
@@ -1,6 +1,6 @@
# M02 — Player Decomposition
<!-- OPENWC_TARGET:OPEN -->
<!-- OPENWC_TARGET:ACTIVE -->
## Outcome
+1 -1
View File
@@ -21,7 +21,7 @@ OpenWC должен предоставить четыре согласованн
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. Текущая работа M01 вводит coordinate и architecture seams.
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; текущая работа M02 декомпозирует player input, movement, terrain query, camera и presentation без observable regression.
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
+3 -3
View File
@@ -10,7 +10,7 @@
## Current target
`M01` — [01-coordinates-and-seams.md](01-coordinates-and-seams.md)
`M02` — [02-player-decomposition.md](02-player-decomposition.md)
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
@@ -34,8 +34,8 @@
| ID | Цель | Зависит от | Статус |
|---|---|---|---|
| M00 | [Renderer baseline](00-render-baseline.md) | — | DONE |
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | ACTIVE |
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | OPEN |
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | DONE |
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | ACTIVE |
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | OPEN |
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
| M05 | [Content Project](05-content-project.md) | M04 | OPEN |
+2
View File
@@ -57,6 +57,8 @@ try {
Invoke-GodotStep "runtime-cache-shutdown" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_runtime_cache_shutdown.gd")
Invoke-GodotStep "baseline-manifest" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_baseline_manifest.gd")
Invoke-GodotStep "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
Invoke-GodotStep "coordinate-boundaries" @("--headless", "--path", ".", "--script", "res://src/tools/verify_coordinate_conversion_boundaries.gd")
Invoke-GodotStep "server-spawn-renderer" @("--headless", "--path", ".", "--script", "res://src/tools/verify_server_spawn_renderer.gd")
$captureArgs = @(
"--path", ".",