Compare commits

..

13 Commits

Author SHA1 Message Date
sindoring 77cd17a5bc docs(M01): mark streaming focus ready
Work-Package: M01-RND-STREAMING-FOCUS-001

Agent: sindo-main-codex
2026-07-13 15:45:32 +04:00
sindoring 7815385b3b feat(M01): add explicit streaming focus
Work-Package: M01-RND-STREAMING-FOCUS-001

Agent: sindo-main-codex
2026-07-13 15:41:16 +04:00
sindoring fbef131bcb docs(M01): claim streaming focus contract
Work-Package: M01-RND-STREAMING-FOCUS-001

Agent: sindo-main-codex
2026-07-13 15:35:45 +04:00
sindoring 52e1b2ba67 docs(M01): mark coordinate fixtures ready
Work-Package: M01-QAR-COORD-FIXTURES-001

Agent: sindo-main-codex
2026-07-13 14:28:03 +04:00
sindoring ce4f7ec0e2 test(M01): add coordinate golden fixtures
Work-Package: M01-QAR-COORD-FIXTURES-001

Agent: sindo-main-codex
2026-07-13 14:27:38 +04:00
sindoring 758b375e7e docs(M01): claim coordinate golden fixtures
Work-Package: M01-QAR-COORD-FIXTURES-001
Agent: sindo-main-codex
Tests: not applicable (claim only)
Fidelity: pinned AzerothCore spawn, raw ADT metadata and build 12340 observations scoped
2026-07-13 14:23:23 +04:00
sindoring b55bfca2a7 docs(M01): mark ADT tile axis fix ready
Work-Package: M01-FND-COORD-TILE-AXIS-002
Agent: sindo-main-codex
Tests: coordination and documentation gates passed
Fidelity: handoff records raw Azeroth_31_49 MCNK evidence
2026-07-13 14:13:10 +04:00
sindoring a7072e3d14 fix(M01): correct ADT filename coordinate axes
Work-Package: M01-FND-COORD-TILE-AXIS-002
Agent: sindo-main-codex
Tests: coordinate mapper, M00 calibration, baseline manifest, coordination and documentation gates passed
Fidelity: Azeroth_31_49 raw MCNK origins and OpenRealm confirm filename X from world Y and filename Y from world X
2026-07-13 14:12:51 +04:00
sindoring f9c467b71f docs(M01): claim ADT tile axis correction
Work-Package: M01-FND-COORD-TILE-AXIS-002
Agent: sindo-main-codex
Tests: not applicable (claim only)
Fidelity: raw Azeroth_31_49 ADT and OpenRealm independently expose swapped filename axes
2026-07-13 14:09:20 +04:00
sindoring 1c0dfc761d docs(M01): mark coordinate contract handoff ready
Work-Package: M01-FND-COORDS-001
Agent: sindo-main-codex
Tests: coordination and documentation gates passed
Fidelity: handoff records five build 12340 golden coordinate points
2026-07-13 13:30:18 +04:00
sindoring f45695c26a fnd(M01): add canonical coordinate mapper
Work-Package: M01-FND-COORDS-001
Agent: sindo-main-codex
Tests: coordinate mapper, M00 coordinate calibration, baseline manifest, coordination and documentation gates passed
Fidelity: five build 12340 camera points map within 0.002 yard; no visual parity claim
2026-07-13 13:29:59 +04:00
sindoring c2be8ae95e docs(M01): claim canonical coordinate contract
Work-Package: M01-FND-COORDS-001
Agent: sindo-main-codex
Tests: not applicable (claim only)
Fidelity: scope references merged M00 build 12340 coordinate evidence
2026-07-13 13:22:02 +04:00
sindoring 8636427234 Merge pull request 'test(M00): close renderer baseline milestone' (#16) from work/sindo-main-codex/m00-integrator-closeout into master
Reviewed-on: #16
2026-07-13 12:17:51 +03:00
46 changed files with 2035 additions and 61 deletions
+13
View File
@@ -830,6 +830,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- A focused 8-second GUI checkpoint and a seven-checkpoint single-pass GUI run finish without Node, renderer resource or RID leak diagnostics. A timing-dependent anonymous zero-reference `RefCounted` ObjectDB warning can remain at engine teardown; it is tracked separately and is not suppressed.
- This is shutdown-only ownership behavior and does not alter checkpoint rendering, cache formats or the `Blizzlike335` compatibility profile.
## 2026-07-13 Explicit Streaming Focus
- `StreamingWorldLoader` consumes an immutable `StreamingFocus` containing a
typed `GodotWorldPosition`; runtime streaming no longer discovers the active
`Camera3D` from the viewport or child nodes.
- Eastern Kingdoms and Kalimdor runtime scenes explicitly use the player as the
streaming source. Checkpoint capture and terrain/occluder probes explicitly
use their dedicated camera through the same public refresh path.
- The editor viewport camera remains behind a dedicated editor-only adapter.
`camera_path` is retained solely for optional automatic overview positioning.
- Streaming radii, LOD decisions, cache formats and renderer quality profiles
are unchanged by this migration.
## 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,104 @@
# 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 -->
## Ownership
- Target: M01
- Program: FND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-coordinate-tile-axis-fix`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Correct the additive coordinate contract so `AdtTileCoordinate(tile_x, tile_y)`
matches actual `Map_tileX_tileY.adt` filenames and MCNK `indexX/indexY` axes.
## Non-goals
- Migrating renderer/native consumers.
- Changing canonical, server, Godot or ADT placement position arithmetic.
- Adding server-spawn fixtures or `StreamingFocus`.
- Committing proprietary ADT bytes.
## Paths
- Exclusive: `src/domain/coordinates/coordinate_mapper.gd`,
`src/domain/coordinates/adt_chunk_coordinate.gd`,
`src/tools/verify_coordinate_mapper.gd`
- Shared/hotspots: `docs/adr/0001-canonical-world-coordinates.md`,
`docs/modules/coordinate-mapping.md`, this claim
- Generated/ignored: local extracted ADT and Godot import metadata
## Contracts and data
- Public API/events: method names unchanged; corrected tile/chunk axis semantics
- Schema/format version: coordinate contract version increments from 1 to 2
- Migration/compatibility: no released consumer exists; contract was merged but
not yet consumed, so correction precedes consumer rollout
- Consumers: future renderer, network, gameplay and server-data adapters
## Dependencies
- Requires: merged `M01-FND-COORDS-001` at `1c0dfc7`
- Blocks: coordinate golden fixtures and all coordinate consumers
- External state: private `Azeroth_31_49.adt` used read-only for evidence
## Verification
- Commands: coordinate mapper probe, M00 calibration, manifest, documentation
and coordination gates
- Fixtures: build 12340 Elwynn point must resolve to `Azeroth_31_49.adt`;
MCNK `(0,0)` and `(15,15)` establish chunk-axis direction
- Fidelity evidence: local ADT SHA-256
`18185B5FAC9566D9C57A9C19ABD75F108370D1795639B6077167C0EFD3672FAE`;
OpenRealm independently indexes filename X from world Y and filename Y from world X
- Performance budget: unchanged constant-time scalar arithmetic
## Documentation deliverables
- Inline public API docs: corrected tile/chunk direction comments
- Module specification: contract version, formulas, capability evidence and risk
- Data-flow diagram: corrected filename/chunk axes
- ADR: corrected decision and evidence
- Sequence/state diagrams: not applicable; pure synchronous mapper
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: swap the two filename formulas and corresponding
local/chunk ownership calculations
- Rejected complexity/abstractions: compatibility flag for a contract with no consumers
- Unavoidable complexity and justification: ADT filename axes differ from client X/Y order
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: raw-corpus diagnosis, filename/local/chunk axis correction, contract
version 2 documentation and regression fixtures
- Next: integrator review and merge before coordinate consumer work
- Blocked by:
## Handoff
- Commit: `a7072e3`
- Branch: `work/sindo-main-codex/m01-coordinate-tile-axis-fix`
- Outcome: `tile_x/chunk_x` now grow east from canonical Y and
`tile_y/chunk_y` grow south from canonical X; public method names unchanged
- Contract/schema: coordinate contract version 2; no persisted format, cache or migration
- Verification: coordinate mapper PASS (5 build-12340 points, 8 boundaries,
2 recorded raw MCNK origins, 5 yaw cases); M00 calibration PASS with maximum
error `0.000015`; renderer manifest, coordination, documentation, dependency
and whitespace checks passed
- Fidelity: private `Azeroth_31_49.adt` hash and raw MCNK origins recorded in
ADR 0001; OpenRealm independently uses the same filename-axis ordering
- Rebuild: none; private/generated prerequisites remain outside Git
- Remaining risks: server spawn fixture, native placement conversion and
MDDF/MODF Euler semantics remain subsequent M01 packages
- Recommended merge order: merge this correction before golden fixture or consumer migration
- Documentation: ADR 0001 and coordinate-mapping module data flow, contract
version, capability evidence and known risks updated
+111
View File
@@ -0,0 +1,111 @@
# 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 -->
## Ownership
- Target: M01
- Program: FND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-coordinate-mapper`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Introduce scene-free value objects for canonical WoW, server, ADT placement,
ADT tile/chunk/local and Godot world coordinate spaces, plus the single pure
`CoordinateMapper` contract for position and world-yaw conversions.
## Non-goals
- Migrating `StreamingWorldLoader`, native parsers, sky, player or renderer code.
- Defining model-specific MDDF/MODF Euler rotation corrections.
- Introducing `StreamingFocus`, typed IDs or a test framework dependency.
- Claiming visual parity from arithmetic round trips.
## Paths
- Exclusive: `src/domain/coordinates/**`, `src/tools/verify_coordinate_mapper.gd`,
`docs/modules/coordinate-mapping.md`, `docs/adr/0001-canonical-world-coordinates.md`
- Shared/hotspots: `docs/modules/README.md`, this claim
- Generated/ignored: Godot import metadata and local test output
## Contracts and data
- Public API/events: immutable coordinate value objects and pure mapper methods
- Schema/format version: no persisted format; contract version 1 in module docs
- Migration/compatibility: additive contract; existing renderer conversions remain
unchanged until a later consumer migration package
- Consumers: future renderer, network, gameplay and server-data adapters
- Rollout order: contract and fixtures first, then native/parser and renderer consumers
## Dependencies
- Requires: merged M00 build 12340 coordinate calibration evidence
- Blocks: M01 renderer coordinate migration and `StreamingFocus`
- External state: no proprietary inputs; reference formulas are source-only evidence
## Verification
- Commands: headless coordinate contract probe, headless project load,
coordination and documentation gates
- Fixtures: five build 12340 camera points, ADT map center/edge/boundary points,
server/ADT/Godot round trips and yaw normalization cases
- Fidelity evidence: M00 observed build 12340 positions plus independent
OpenRealm/WoWee and TrinityCore grid formulas
- Performance budget: constant-time scalar arithmetic with no allocation beyond result values
## Documentation deliverables
- Inline public API docs: every public value object and mapper method
- Module specification: purpose, public API, inputs/outputs, ownership, errors and source map
- Data-flow diagram: server/ADT/canonical/Godot mappings
- Dependency diagram: canonical contract consumers and forbidden scene dependency
- Sequence/state diagrams: not applicable; mapper is synchronous and stateless
- ADR: coordinate axes, units, tile indexing and compatibility rollout
## Simplicity and naming
- Important names introduced: `CanonicalWowWorldPosition`, `ServerWorldPosition`,
`AdtPlacementPosition`, `AdtTileCoordinate`, `AdtChunkCoordinate`,
`AdtTileLocalPosition`, `GodotWorldPosition`, `WorldYaw`, `CoordinateMapper`
- Simplest considered solution: scalar immutable value objects and one pure mapper
- Rejected complexity/abstractions: generic vector-space framework, matrices,
dependency injection and a third-party test framework
- Unavoidable complexity and justification: distinct source spaces prevent silent axis swaps
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: repository/reference audit, ADR, immutable value objects, pure mapper,
build 12340/boundary/yaw fixtures, module specification and repository gates
- Next: integrator review and merge before renderer/native consumer migration
- Blocked by:
## Handoff
- Commit: `f45695c`
- Branch: `work/sindo-main-codex/m01-coordinate-mapper`
- Outcome: additive contract for canonical/server/ADT/Godot positions, ADT
tile/chunk/local ownership and world-facing yaw; no existing consumer changed
- Public contracts: contract version 1 documented in
`docs/modules/coordinate-mapping.md`; no persisted schema or cache change
- Verification: coordinate mapper PASS (5 golden points, 8 tile boundaries,
5 yaw cases); M00 coordinate calibration PASS (5 points, maximum error
`0.000015`); renderer manifest, coordination, documentation, dependency and
whitespace checks passed; headless project load completed with expected
missing proprietary-data diagnostics in the isolated worktree
- Fidelity: observed build 12340 positions match within `0.002` yard; formulas
cross-checked against TrinityCore, OpenRealm and WoWee; visual parity is not claimed
- Migration/rebuild: none for this additive package; consumers migrate only
after merge and must preserve renderer checkpoints
- Remaining risks: native loaders and renderer still use manual conversions;
MDDF/MODF Euler mapping is intentionally unresolved; server-spawn integration
and finite-value adapter validation remain later M01 packages
- Recommended merge order: this contract first, coordinate fixtures/server-spawn
audit second, renderer/native consumers third, manual-conversion search gate last
- Documentation: ADR 0001; coordinate-mapping module spec with data-flow and
dependency diagrams; inline API documentation on all public symbols
@@ -0,0 +1,109 @@
# 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 -->
## Ownership
- Target: M01
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-coordinate-fixtures`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Add a versioned, non-proprietary fixture and headless validator joining one
pinned AzerothCore player spawn, raw ADT/MCNK metadata and the five M00
original-client coordinate observations through coordinate contract version 2.
## Non-goals
- Committing ADT files, SQL dumps or original-client screenshots.
- Claiming TrinityCore spawn equivalence without a pinned populated TDB source.
- Migrating renderer/native consumers or rendering a live server spawn.
- Changing coordinate formulas, IDs or `StreamingFocus`.
## Paths
- Exclusive: `src/tests/fixtures/coordinate_golden_points.json`,
`src/tools/verify_coordinate_golden_fixtures.gd`
- Shared/hotspots: `docs/modules/coordinate-mapping.md`, this claim
- Generated/ignored: extracted ADT data, server database downloads and Godot metadata
## Contracts and data
- Public API/events: no new production API; fixture schema version 1
- Coordinate contract: version 2, unchanged
- Migration/compatibility: additive test data; fixture consumers reject unknown schema versions
- Consumers: M01 coordinate mapper, future native/renderer/server adapter migrations
## Dependencies
- Requires: merged coordinate contract version 2 at `b55bfca`
- Blocks: coordinate consumer migration and golden server-spawn renderer integration
- External state: source evidence reduced to pinned revision, numeric rows and SHA-256 hashes
## Verification
- Commands: fixture validator, coordinate mapper probe, M00 calibration,
manifest, coordination and documentation gates
- Fixtures: AzerothCore Human Warrior start; raw `Azeroth_32_48.adt` MCNK
`(3,12)`; five build 12340 camera points
- Fidelity evidence: exact source provenance and mapper outputs with bounded tolerances
- Performance budget: negligible headless JSON and scalar arithmetic
## Documentation deliverables
- Inline public API docs: fixture-validator header and field validation functions
- Module specification: fixture schema, provenance, verification and source map
- Data-flow diagram: server/raw/client fixtures into mapper assertions
- State/sequence/dependency diagrams: unchanged; synchronous stateless validation
## Simplicity and naming
- Important names introduced: `coordinate_golden_points`,
`verify_coordinate_golden_fixtures`
- Simplest considered solution: one JSON fixture and one existing-style headless script
- Rejected complexity/abstractions: test framework dependency, SQL parser, ADT bytes in Git
- Unavoidable complexity and justification: three independent source categories need explicit provenance
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: pinned source research, schema 1 fixture, headless validator, module
documentation and all package gates
- Next: integrator review and merge before coordinate consumer migration
- Blocked by:
## Handoff
- Commit: `ce4f7ec`
- Branch: `work/sindo-main-codex/m01-coordinate-fixtures`
- Outcome: one versioned fixture now joins a pinned AzerothCore Human Warrior
spawn, private raw MCNK metadata and five original-client build 12340 points
through coordinate contract version 2
- Contract/schema: additive test fixture schema 1; production coordinate API and
contract version 2 are unchanged; proprietary bytes remain outside Git
- Verification: golden fixture PASS (`1` server spawn, `1` raw ADT chunk, `5`
original-client points); CoordinateMapper PASS (`5` golden points, `8` tile
boundaries, `2` raw MCNK cases, `5` yaw cases); M00 calibration PASS with
maximum error `0.000015`; renderer manifest PASS (`7` checkpoints); coordination,
documentation, dependency and whitespace checks passed
- Fidelity: AzerothCore source pinned at revision
`68fcf0098b16388093989627f37be530fc91f07d`; private
`Azeroth_32_48.adt` SHA-256
`D66E27FD82B461AE38B78562952AED428E619777A3F3E35DD8FF97444644770A`;
server and raw data independently resolve tile `(32,48)` and chunk `(3,12)`
- Rebuild: no proprietary input is required to rerun the committed fixture;
local ignored renderer resources are still required by the inherited M00
manifest probe
- Remaining risks: TrinityCore equivalence needs a pinned populated TDB snapshot;
live server adapter, renderer integration and MDDF/MODF Euler semantics remain
subsequent M01 work
- Recommended merge order: after coordinate contract version 2 (`b55bfca`) and
before live coordinate consumer migration
- Documentation: coordinate-mapping module inputs/outputs, persistence, data-flow
diagram, verification, capability matrix, risks and source map updated
@@ -0,0 +1,120 @@
# 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 -->
## Ownership
- Target: M01
- Program: RND/FND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m01-streaming-focus`
- Lease expires UTC: 2026-07-15
- Integrator: M01 milestone integrator
## Outcome
Introduce a scene-free `StreamingFocus` value and make runtime/test streaming
consume it through an explicit source or public setter instead of discovering
the active `Camera3D` from a viewport.
## Non-goals
- Extracting `WorldRenderFacade` or `StreamingTargetPlanner` from the monolithic loader.
- Adding direction, velocity, frustum or multi-focus planning.
- Decomposing the player controller before M02.
- Changing terrain, M2, WMO, liquid, cache or LOD formulas.
## Paths
- Exclusive: `src/domain/streaming/`, `src/tools/verify_streaming_focus.gd`
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/scenes/streaming/eastern_kingdoms_streaming.tscn`,
`src/scenes/streaming/kalimdor_streaming.tscn`, renderer diagnostic tools,
`docs/modules/world-renderer.md`, this claim
- Generated/ignored: local extracted assets, renderer caches and Godot import metadata
## Contracts and data
- Public API: immutable `StreamingFocus`; loader `set_streaming_focus` and
`refresh_streaming_focus`
- Source adapter: explicit `streaming_focus_source_path` may reference any `Node3D`
- Migration: runtime scenes use player focus; capture/probe tools explicitly use
their dedicated camera as the source
- Compatibility: streaming radii/LOD/cache behavior and coordinate contract v2 unchanged
## Dependencies
- Requires: coordinate contract version 2 and golden fixtures at `52e1b2b`
- Blocks: M01 renderer/test focus exit criterion and later renderer facade extraction
- External state: no new dependency or proprietary input
## Verification
- Commands: streaming focus contract/integration verifier, renderer baseline
manifest/calibration, headless scene smoke, coordination/documentation gates
- Fixtures: Node3D player-style source and explicit capture-style setter
- Fidelity evidence: existing seven renderer checkpoints and build-12340 coordinate
calibration must remain unchanged
- Performance budget: one scalar focus snapshot per configured streaming interval;
no new per-tile or per-chunk work
## Documentation deliverables
- Inline public API docs: focus value and loader public methods
- Module specification: inputs/outputs, ownership, lifecycle, failure/recovery and source map
- Data-flow diagram: player/editor/capture source to focus contract to loader
- State/sequence diagram: focus sampling and refresh threshold sequence
## Simplicity and naming
- Important names introduced: `StreamingFocus`, `streaming_focus_source_path`,
`set_streaming_focus`, `refresh_streaming_focus`
- Simplest considered solution: one immutable value plus one explicit scene adapter path
- Rejected complexity/abstractions: provider interface hierarchy, service locator,
multi-focus registry and predictive motion fields
- Unavoidable complexity and justification: editor viewport remains a separate
adapter because its camera is owned by `EditorInterface`
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: contract, runtime/editor/capture migration, headless regression,
module diagrams, renderer documentation and cold/warm GUI capture
- Next: integrator review and merge before typed identity work
- Blocked by:
## Handoff
- Commit: `7815385`
- Branch: `work/sindo-main-codex/m01-streaming-focus`
- Outcome: runtime streaming consumes immutable `StreamingFocus`; Eastern
Kingdoms and Kalimdor scenes explicitly follow the player, while capture and
terrain/occluder probes explicitly follow their dedicated camera
- Contract/API: new scene-free `StreamingFocus(GodotWorldPosition)` plus loader
`set_streaming_focus`, `refresh_streaming_focus` and
`streaming_focus_source_path`; `camera_path` remains only for optional overview positioning
- Verification: StreamingFocus PASS (`1` contract, `2` runtime scenes, `3`
capture tools); coordinate golden fixtures and CoordinateMapper PASS; M00
calibration PASS with maximum error `0.000015`; manifest PASS (`7`
checkpoints); runtime-cache shutdown PASS; headless runtime scene exit `0`;
coordination, documentation, dependency and whitespace checks passed
- Renderer checkpoint: standard 8-second GUI capture completed `14` images
(`7` cold + `7` warm) and wrote schema-1 `report.json`; focus tiles changed
through the public contract; no Node/resource/RID leak diagnostic appeared
- Fidelity: checkpoint camera positions, coordinate contract v2, streaming radii,
LOD formulas, cache formats and quality profiles are unchanged; this is
regression evidence, not a new `1:1` parity claim
- Rebuild: none; GUI verification used local ignored extracted assets/cache
junctions. Missing worktree `.godot/imported` copies for several incidental M2
GLBs produced existing cache-load fallback diagnostics only
- Remaining risks: the monolithic loader still owns planning/jobs/presentation;
direction, velocity, frustum and multiple foci remain M03 planner work. The
known timing-dependent zero-reference `ObjectDB` shutdown warning occurred,
without Node/resource/RID leak diagnostics
- Recommended merge order: after coordinate fixtures at `52e1b2b`, before typed
identity and renderer-facade extraction
- Documentation: `world-renderer.md` inputs/outputs, public API, lifecycle,
data-flow and sequence diagrams, ownership, recovery, verification,
capability matrix, risks and source map updated; `RENDER.md` updated
@@ -0,0 +1,164 @@
# ADR 0001: Canonical world coordinate contract
- Status: Accepted for M01 contract implementation
- Date: 2026-07-13
- Work packages: `M01-FND-COORDS-001`, corrected by `M01-FND-COORD-TILE-AXIS-002`
- Decision owners: M01 foundation/integration owners
## Context
OpenWC currently performs coordinate conversion in native ADT/WDT loaders,
`StreamingWorldLoader`, sky, player and renderer diagnostic scripts. The same
numbers are frequently carried in `Vector3` or dictionaries, so the coordinate
space is implicit. Existing names also mix ADT filename indices, renderer X/Z
indices and MDDF/MODF placement fields.
M00 recorded five positions observed in WoW 3.3.5a build 12340. They verify the
current client-position to Godot-position arithmetic within `0.002` yard, but
they do not define a cross-layer contract and do not prove camera composition
or visual parity.
Independent references agree on a 64 by 64 ADT grid and an approximately
`533.3333` yard tile size. They do not all use the word “canonical” for the same
axis order. OpenWC therefore needs names tied to observable formats instead of
adopting an internal convention from one reference project.
## Decision
### Canonical position
`CanonicalWowWorldPosition(X, Y, Z)` preserves the numeric X/Y/Z order used by
the WoW client, movement protocol and TrinityCore/AzerothCore world positions.
X and Y are horizontal; Z is height. Units are WoW yards.
`ServerWorldPosition` is a distinct type but converts component-for-component
to canonical. The type boundary prevents a server record from being passed
directly to renderer code and leaves room for adapter validation without a
silent axis swap.
### World extent and ADT indices
The contract uses:
```text
ADT_TILE_SIZE_YARDS = 1600 / 3
ADT_TILES_PER_MAP_AXIS = 64
WOW_WORLD_HALF_EXTENT_YARDS = 32 * ADT_TILE_SIZE_YARDS
tileX = floor(32 - canonical.Y / ADT_TILE_SIZE_YARDS)
tileY = floor(32 - canonical.X / ADT_TILE_SIZE_YARDS)
```
`AdtTileCoordinate(tile_x, tile_y)` always means the indices in
`Map_tileX_tileY.adt`. Results are intentionally not clamped: the exact
south-east extent produces index 64 and remains diagnosable as out of bounds.
Normal valid indices are 0 through 63.
An `AdtTileLocalPosition` is measured east and south from the tile's
north-west corner. Filename `tile_x` and MCNK `chunk_x` grow east; filename
`tile_y` and MCNK `chunk_y` grow south, with valid indices 0 through 15. These
direction names avoid pretending that ADT file axes are Godot X/Z axes.
### ADT placement fields
MDDF/MODF position fields are represented in on-disk X/height/Z order:
```text
adt.X = halfExtent - canonical.X
adt.height = canonical.Z
adt.Z = halfExtent - canonical.Y
```
Model-local MDDF/MODF Euler corrections are not world orientation. They remain
outside this contract until a placement-orientation package has format- and
model-specific fixtures.
### Godot renderer basis
The compatibility renderer keeps one Godot unit equal to one WoW yard and uses
Y-up coordinates from the north-west map extent:
```text
godot.X = halfExtent - canonical.Y # east
godot.Y = canonical.Z # up
godot.Z = halfExtent - canonical.X # south
```
`GodotWorldPosition` contains scalars rather than `Vector3`. A scene/render
adapter may construct `Vector3` only at its boundary.
### World yaw
Server/canonical yaw zero faces increasing WoW X; positive yaw turns toward
increasing WoW Y. In the mapped Godot basis, Godot forward `-Z` and positive
Y-axis yaw produce the same numeric world angle. Mapper conversions therefore
normalize the value to `[-PI, PI)` without adding an offset.
This identity applies to world-facing yaw only. It does not apply to M2/WMO
asset axes or placement Euler triples.
### Contract version 2 correction evidence
Contract version 1 incorrectly calculated filename `tile_x` from canonical X
and `tile_y` from canonical Y. The private WotLK corpus contains the observed
Elwynn point in `Azeroth_31_49.adt`, SHA-256
`18185B5FAC9566D9C57A9C19ABD75F108370D1795639B6077167C0EFD3672FAE`; the
transposed `Azeroth_49_31.adt` does not exist. Raw MCNK headers in that file
show `indexX` decreasing world Y (east) and `indexY` decreasing world X (south).
OpenRealm independently calculates filename X from world Y and filename Y from
world X. Contract version 2 corrects the formulas before any consumer migration.
## Alternatives considered
### Make renderer coordinates canonical
Rejected because protocol, database and ADT adapters would silently inherit a
presentation-specific offset and Y-up basis.
### Adopt WoWee's internal canonical axis names unchanged
Rejected because that reference swaps server X/Y into its chosen north/west
ordering. OpenWC preserves observable client/server field names and makes the
render/ADT transformations explicit instead.
### Use `Vector3` plus comments
Rejected because comments cannot prevent server, ADT and Godot values from
being passed across the wrong boundary.
### Generic vector-space or matrix framework
Rejected as unnecessary complexity for fixed affine transforms and integer
grid ownership rules.
## Consequences
- Network and server-data adapters can expose typed positions without knowing
Godot.
- Renderer consumers must explicitly map and then create Godot vectors at their
own boundary.
- ADT filename indices can no longer be confused with renderer X/Z tile names.
- Existing native/parser and renderer conversions remain unchanged until
separate consumer migration packages merge after this contract.
- Manual axis conversions become review/test violations once migration is
complete; this ADR alone does not delete compatibility paths.
- Persisted schemas are unchanged. Future serialized positions must include an
explicit coordinate-space/version discriminator.
## Verification
- Five M00 build-12340 client camera positions map to the recorded Godot
checkpoints within `0.002` yard.
- Server, ADT placement, Godot and tile-local conversions round-trip.
- Exact center, tile, chunk and world-extent boundaries have explicit tests.
- 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.
## References
- [TrinityCore GridDefines](https://github.com/TrinityCore/TrinityCore/blob/master/src/server/game/Grids/GridDefines.h)
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_adt.c`
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_objects.c`
- `reference/WoWee/include/core/coordinates.hpp`
- [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
+1
View File
@@ -7,6 +7,7 @@
| Module | Current status | Primary specification |
|---|---|---|
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
| Coordinate mapping | Implemented | [`coordinate-mapping.md`](coordinate-mapping.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
+235
View File
@@ -0,0 +1,235 @@
# Coordinate Mapping
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| 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-fixtures`, 2026-07-13 |
| Profiles/capabilities | All profiles; contract version 2 |
## Purpose
Provide scene-free value objects and one pure `CoordinateMapper` for positions,
ADT grid ownership and world-facing yaw. The module makes every source space
explicit before renderer, network, gameplay and authoring consumers migrate.
## Non-goals
- Reading ADT, packet, SQL or scene data.
- Constructing `Vector3`, `Transform3D`, Node or Resource values.
- Correcting M2/WMO local axes or MDDF/MODF Euler rotations.
- Selecting maps, validating terrain height or owning streaming focus.
- Persisting coordinate values in this work package.
## Context and boundaries
```mermaid
flowchart LR
Server[Server / movement X,Y,Z] --> ServerPosition[ServerWorldPosition]
ADT[MDDF/MODF X,height,Z] --> AdtPosition[AdtPlacementPosition]
ServerPosition --> Mapper[CoordinateMapper]
AdtPosition --> Mapper
Mapper --> Canonical[CanonicalWowWorldPosition]
Canonical --> Tile[AdtTileCoordinate + AdtTileLocalPosition]
Canonical --> GodotPosition[GodotWorldPosition]
GodotPosition --> Adapter[Future scene/render adapter]
Adapter --> Vector[Vector3 at boundary]
Scene[Node / Camera3D / scene lookup] -. forbidden .-> Mapper
```
Allowed dependencies:
- GDScript scalar math and `RefCounted` lifetime;
- other value objects in `src/domain/coordinates`;
- callers in network, gameplay, render and server-data adapters.
Forbidden dependencies:
- `Node`, `Resource`, SceneTree, Camera3D and global scene lookup;
- packet buffers, SQL rows, asset parsers and renderer state;
- `Vector3` as a public mapper input or output;
- model-specific placement corrections.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `CanonicalWowWorldPosition` | Immutable value | Client/server X/Y/Z in yards | Any thread; caller-owned reference | No bounds validation |
| `ServerWorldPosition` | Immutable value | Server/wire X/Y/Z in yards | Any thread; caller-owned reference | No adapter validation |
| `AdtPlacementPosition` | Immutable value | MDDF/MODF X/height/Z fields | Any thread; caller-owned reference | No file validation |
| `GodotWorldPosition` | Immutable value | Godot east/up/south scalars | Any thread; caller-owned reference | Does not create `Vector3` |
| `AdtTileCoordinate` | Immutable value | `Map_tileX_tileY.adt` indices | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
| `AdtTileLocalPosition` | Immutable value | East/height/south offsets from tile north-west corner | Any thread; caller-owned reference | Values are intentionally unclamped |
| `AdtChunkCoordinate` | Immutable value | MCNK X-east/Y-south indices inside one tile | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
| `CanonicalWowWorldYaw` | Immutable value | Canonical world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
| `ServerWorldYaw` | Immutable value | Movement orientation in radians | Any thread; caller-owned reference | Constructor does not normalize |
| `GodotWorldYaw` | Immutable value | Godot Y-axis world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
| `CoordinateMapper.server_to_canonical` / inverse | Pure static methods | Preserve server X/Y/Z as canonical | Synchronous; new result value | Null input is a caller contract violation |
| `CoordinateMapper.canonical_to_godot` / inverse | Pure static methods | Apply Y-up renderer basis and map offset | Synchronous; new result value | Non-finite scalars propagate |
| `CoordinateMapper.canonical_to_adt_placement` / inverse | Pure static methods | Convert ADT placement field basis | Synchronous; new result value | Non-finite scalars propagate |
| `CoordinateMapper.canonical_to_adt_tile` | Pure static method | Compute unclamped filename indices | Synchronous; new result value | Out-of-map result remains invalid |
| `CoordinateMapper.canonical_to_adt_tile_local` / inverse | Pure static methods | Split/reconstruct tile-local position | Synchronous; new result value | Caller validates tile ownership |
| `CoordinateMapper.adt_tile_local_to_chunk` | Pure static method | Compute unclamped MCNK indices | Synchronous; new result value | Boundary/out-of-range remains diagnosable |
| World-yaw mapper methods | Pure static methods | Normalize and cross the world-basis boundary | Synchronous; new result value | Non-finite input is unsupported |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `ServerWorldPosition`, `ServerWorldYaw` | Future protocol/server adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
| Input | `AdtPlacementPosition` | Future ADT adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
| Input | `CanonicalWowWorldPosition`, `CanonicalWowWorldYaw` | Gameplay/content/domain | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
| Output | `CanonicalWowWorldPosition` | `CoordinateMapper` | Gameplay/content/adapters | New immutable value | Caller-owned reference |
| Output | `AdtTileCoordinate`, `AdtTileLocalPosition`, `AdtChunkCoordinate` | `CoordinateMapper` | Streaming/content adapters | New immutable values | Caller-owned references |
| Output | `GodotWorldPosition`, `GodotWorldYaw` | `CoordinateMapper` | Future render/scene adapter | New immutable values | Caller-owned references |
| 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:
- none;
- no scene mutation, file/cache write, network send, database access or logging.
## Data flow
```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]
Canonical -->|north-west edge offsets| Local[AdtTileLocalPosition]
Local -->|floor offset / chunk size| Chunk[AdtChunkCoordinate]
Canonical -->|half extent - Y; Z; half extent - X| Godot[GodotWorldPosition]
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
```
## Lifecycle/state
The module is stateless. Each call reads immutable scalar inputs and returns a
new value object. There is no initialization, shutdown, cancellation or retry
state machine.
## Main sequence
No cross-layer asynchronous sequence occurs inside this module. Producers parse
or validate their external format first, call the mapper synchronously, and
then pass only typed results across the next boundary.
## Ownership, threading and resources
- Value objects own only scalar fields and, for chunks, one immutable tile reference.
- Callers own returned `RefCounted` references.
- Methods do not mutate inputs or shared state and are safe for worker use.
- No Node, RID, file, socket, database or cache resource is acquired.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Tile/chunk outside standard grid | `is_within_map_grid()` | Unclamped indices are preserved | Caller reports source and map context | Reject or apply explicit map policy |
| Tile-local value outside half-open tile | Consumer boundary validation | Value is preserved | Caller reports source coordinate | Recompute ownership or reject malformed input |
| Null value object | Static typing/call failure | Programming contract violation | GDScript error with call site | Fix producer; no fallback value |
| NaN/infinite scalar | Adapter validation | Arithmetic propagates invalid value | Adapter must report source field | Reject malformed external input |
There is no cancellation because every conversion is constant-time scalar math.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Contract version | 2 | All | No | Corrected filename/chunk axes in ADR 0001 |
| Godot units per WoW yard | 1 | `Blizzlike335` and current tools | No | Preserves current renderer scale |
| ADT grid | 64 tiles/axis, 16 chunks/tile | WotLK 3.3.5a | No | Defines index and boundary math |
## Persistence, cache and migration
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.
Any future serialized coordinate must store a schema version and explicit space;
raw three-number arrays are not an accepted new persistence contract.
## Diagnostics and observability
- Logs: none in production mapper.
- Metrics: none; conversion cost is constant and outside frame-budget concern.
- Debug views: headless contract probe prints one PASS summary.
- Correlation IDs: supplied by external adapters if an invalid source is reported.
## Verification
- Unit/contract tests: `src/tools/verify_coordinate_mapper.gd`.
- 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; 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.
## Extension points
- Add typed map IDs around positions without changing axis semantics.
- Add a format-specific placement-orientation adapter with its own fixtures.
- Add serialization only with explicit space/version fields and migrations.
- Add consumer adapters that construct engine-native vectors at their boundary.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Canonical/server/Godot position mapping | Implemented | Headless round-trip and five M00 golden points | Migrate producers/consumers |
| ADT placement mapping | Implemented | Synthetic/direct round-trip fixtures | Compare native parsed placements during migration |
| ADT tile/local/chunk ownership | Implemented | Center/extent 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 |
| 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
- Existing native loaders and renderer scripts still contain manual conversions.
- 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;
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
| Path | Responsibility |
|---|---|
| `src/domain/coordinates/coordinate_mapper.gd` | Constants and all coordinate-axis conversions |
| `src/domain/coordinates/*_position.gd` | Immutable typed position values |
| `src/domain/coordinates/adt_tile_coordinate.gd` | ADT filename tile indices and bounds |
| `src/domain/coordinates/adt_chunk_coordinate.gd` | MCNK indices and bounds |
| `src/domain/coordinates/*_yaw.gd` | Immutable typed world-facing yaw values |
| `src/tools/verify_coordinate_mapper.gd` | Headless fixtures and boundary regressions |
| `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
- ADR: [`../adr/0001-canonical-world-coordinates.md`](../adr/0001-canonical-world-coordinates.md)
- M00 evidence: [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
- Testing rules: [`../TESTING.md`](../TESTING.md)
+43 -14
View File
@@ -5,9 +5,9 @@
| Field | Value |
|---|---|
| Status | Partial |
| Target/work package | M00 active; декомпозиция M01M03 |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; декомпозиция M02M03 |
| Owners | Renderer workstream / milestone integrator |
| Last verified | M00 visual-diff worktree, 2026-07-11 |
| Last verified | Worktree `work/sindo-main-codex/m01-streaming-focus`, 2026-07-13 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -26,7 +26,11 @@
```mermaid
flowchart LR
Focus[Camera/player/editor focus] --> Loader[StreamingWorldLoader]
Player[Player or spectator Node3D] --> Adapter[Explicit source adapter]
Editor[Editor viewport adapter] --> Focus[StreamingFocus]
Capture[Capture tool] --> Adapter
Adapter --> Focus
Focus --> Loader[StreamingWorldLoader]
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
Cache[Baked terrain/M2/WMO caches] --> Loader
Native --> Parsed[Parsed tile/model data]
@@ -60,7 +64,11 @@ Forbidden dependencies:
|---|---|---|---|---|
| `map_name` | Exported property | WDT/map directory name | Set before world load | Missing WDT produces diagnostic/empty world |
| `extracted_dir` | Exported property | Root of legally extracted client data | Session lifetime | Missing files reported per loader path |
| `camera_path` | Exported property | Current streaming focus compatibility path | Main thread, scene lifetime | Missing camera prevents normal focus update |
| `StreamingFocus` | Immutable value | Typed Godot-basis position without Node/Camera dependency | Any thread; caller-owned reference | Null position is a caller contract violation |
| `streaming_focus_source_path` | Exported adapter property | Samples any explicit `Node3D` source into `StreamingFocus` | Main thread, scene lifetime | Missing/wrong node warns once and retains prior focus |
| `set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Null focus is ignored by refresh until replaced |
| `refresh_streaming_focus` | Public method | Samples configured source and applies refresh thresholds | Main thread/session | Returns `false` when no valid focus exists |
| `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning |
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
@@ -71,7 +79,9 @@ Forbidden dependencies:
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Map/focus/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `StreamingWorldLoader` | Immutable caller value | Main thread/session |
| Input | Focus-source `NodePath` | Runtime/test scene composition | Loader source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
| 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 |
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
@@ -90,7 +100,12 @@ Side effects:
```mermaid
flowchart TD
F[Streaming focus] --> T[Compute desired ADT tiles]
P[Player/spectator Node3D] --> A[Explicit scene adapter]
C[Capture camera] --> A
E[Editor viewport] --> EA[Editor adapter]
A --> F[StreamingFocus]
EA --> F
F --> T[Compute desired ADT tiles]
WDT[WDT tile catalog] --> T
T --> Q[Tile load queue]
Q --> P[Worker parse/cache load]
@@ -113,13 +128,14 @@ flowchart TD
```mermaid
stateDiagram-v2
[*] --> Initializing
Initializing --> Ready: WDT/catalog/config ready
Initializing --> WaitingForFocus: WDT/catalog/config ready
Initializing --> Degraded: missing loaders/data/cache
Ready --> Streaming: focus update
WaitingForFocus --> Streaming: valid StreamingFocus
Streaming --> Streaming: load/finalize/evict ticks
Streaming --> WaitingForFocus: world reset without replacement focus
Streaming --> SwitchingMap: map/focus reset
SwitchingMap --> Initializing
Ready --> ShuttingDown
WaitingForFocus --> ShuttingDown
Degraded --> ShuttingDown
Streaming --> ShuttingDown
ShuttingDown --> [*]
@@ -129,12 +145,14 @@ stateDiagram-v2
```mermaid
sequenceDiagram
participant Focus as Camera/Player/Editor
participant Source as Player/Editor/Capture source
participant Focus as StreamingFocus adapter
participant Stream as StreamingWorldLoader
participant Worker as Worker task
participant Budget as Main-thread budget
participant Render as SceneTree/RenderingServer
Focus->>Stream: focus position changed
Source->>Focus: sample explicit Godot position
Focus->>Stream: set/refresh typed focus
Stream->>Stream: desired tiles and priorities
Stream->>Worker: parse/load tile and detail data
Worker-->>Stream: immutable result
@@ -150,6 +168,8 @@ sequenceDiagram
## Ownership, threading and resources
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
- Focus producers own the immutable `StreamingFocus` reference; the loader retains
the latest reference and converts it to `Vector3` only inside the render boundary.
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
- Parsed/grouped results передаются обратно через guarded result queues.
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
@@ -163,6 +183,8 @@ sequenceDiagram
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Missing WDT/ADT | File/catalog check | Tile/map unavailable | Loader warning/error | Fix extraction/profile, reload |
| Missing focus source | Explicit NodePath resolution | Retain current streamed content and prior valid focus | One warning until source recovers | Fix composition path or call `set_streaming_focus` |
| No focus supplied | Refresh contract | No target recomputation; queues continue draining | `refresh_streaming_focus()` returns `false` | Supply player/editor/capture focus |
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
@@ -184,6 +206,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Persistence, cache and migration
- `StreamingFocus` is runtime-only and is not serialized. Scene migration adds
`streaming_focus_source_path`; existing `camera_path` remains valid only for
optional automatic overview positioning.
- Renderer reads caches under `data/cache`; caches are not committed.
- Format changes require `FORMAT_VERSION`/required-version bump and rebuild instructions.
- Atomic cache publication and unified manifest are target-state work from FND/M03.
@@ -198,7 +223,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Verification
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
- 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.
- 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.
@@ -207,7 +232,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
## Extension points
- Target `WorldRenderFacade` for runtime/editor/capture callers.
- Target `StreamingFocus` independent of Camera3D.
- Extend `StreamingFocus` with measured direction/velocity needs when the pure planner is extracted.
- Extracted planner, budget scheduler, terrain, M2, WMO, liquid, sky and character services.
- Separate Blizzlike/Enhanced material and quality profiles.
@@ -221,12 +246,14 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| 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 |
| Stable renderer facade | Planned | M03 | Current caller uses monolithic Node API |
## Known gaps and risks
- Monolithic streamer mixes planning, jobs, caches and presentation.
- Direct camera path remains until M01/M03.
- `camera_path` remains only for optional automatic overview positioning; it no
longer drives runtime streaming or discovers the active viewport camera.
- Original-client paired fidelity evidence incomplete.
- Первый paired run выявил coordinate/placement mismatch: несколько совпадающих server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC.
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
@@ -247,6 +274,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Path | Responsibility |
|---|---|
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
@@ -0,0 +1,44 @@
class_name AdtChunkCoordinate
extends RefCounted
## Immutable MCNK chunk indices inside one ADT tile.
## `chunk_x` grows east and `chunk_y` grows south, matching the ADT header and
## tile conversion contract rather than Godot's X/Z member names.
const MINIMUM_CHUNK_INDEX: int = 0
const MAXIMUM_CHUNK_INDEX: int = 15
var tile_coordinate: AdtTileCoordinate:
get:
return _tile_coordinate
var chunk_x: int:
get:
return _chunk_x
var chunk_y: int:
get:
return _chunk_y
var _tile_coordinate: AdtTileCoordinate
var _chunk_x: int
var _chunk_y: int
## Creates chunk indices without clamping so invalid parser input is visible.
func _init(tile_coordinate_value: AdtTileCoordinate, chunk_x_value: int, chunk_y_value: int) -> void:
_tile_coordinate = tile_coordinate_value
_chunk_x = chunk_x_value
_chunk_y = chunk_y_value
## Returns whether the tile and both chunk indices are inside standard bounds.
func is_within_map_grid() -> bool:
return (
_tile_coordinate != null
and _tile_coordinate.is_within_map_grid()
and _chunk_x >= MINIMUM_CHUNK_INDEX
and _chunk_x <= MAXIMUM_CHUNK_INDEX
and _chunk_y >= MINIMUM_CHUNK_INDEX
and _chunk_y <= MAXIMUM_CHUNK_INDEX
)
@@ -0,0 +1 @@
uid://b1lf5w5p3b6ye
@@ -0,0 +1,29 @@
class_name AdtPlacementPosition
extends RefCounted
## Immutable MDDF/MODF position in ADT file field order.
## `x_offset_yards` and `z_offset_yards` are measured from the north-west map
## extent; `height_yards` is the vertical field stored between them.
var x_offset_yards: float:
get:
return _x_offset_yards
var height_yards: float:
get:
return _height_yards
var z_offset_yards: float:
get:
return _z_offset_yards
var _x_offset_yards: float
var _height_yards: float
var _z_offset_yards: float
## Creates an ADT placement position in on-disk X/height/Z field order.
func _init(x_offset_yards_value: float, height_yards_value: float, z_offset_yards_value: float) -> void:
_x_offset_yards = x_offset_yards_value
_height_yards = height_yards_value
_z_offset_yards = z_offset_yards_value
@@ -0,0 +1 @@
uid://cxxlbkjmdtg7m
@@ -0,0 +1,35 @@
class_name AdtTileCoordinate
extends RefCounted
## Immutable indices from an ADT filename `Map_tileX_tileY.adt`.
## Both axes use the inclusive range 0..63 for a normal WotLK world map.
const MINIMUM_TILE_INDEX: int = 0
const MAXIMUM_TILE_INDEX: int = 63
var tile_x: int:
get:
return _tile_x
var tile_y: int:
get:
return _tile_y
var _tile_x: int
var _tile_y: int
## Creates file indices without clamping so out-of-map input remains diagnosable.
func _init(tile_x_value: int, tile_y_value: int) -> void:
_tile_x = tile_x_value
_tile_y = tile_y_value
## Returns whether both file indices address the standard 64 by 64 ADT grid.
func is_within_map_grid() -> bool:
return (
_tile_x >= MINIMUM_TILE_INDEX
and _tile_x <= MAXIMUM_TILE_INDEX
and _tile_y >= MINIMUM_TILE_INDEX
and _tile_y <= MAXIMUM_TILE_INDEX
)
@@ -0,0 +1 @@
uid://bg0a286yty1jw
@@ -0,0 +1,29 @@
class_name AdtTileLocalPosition
extends RefCounted
## Immutable position relative to the north-west corner of one ADT tile.
## East and south offsets are normally in `[0, ADT_TILE_SIZE_YARDS)`; height
## remains in the canonical vertical space.
var east_yards: float:
get:
return _east_yards
var height_yards: float:
get:
return _height_yards
var south_yards: float:
get:
return _south_yards
var _east_yards: float
var _height_yards: float
var _south_yards: float
## Creates a tile-local position without clamping boundary values.
func _init(east_yards_value: float, height_yards_value: float, south_yards_value: float) -> void:
_east_yards = east_yards_value
_height_yards = height_yards_value
_south_yards = south_yards_value
@@ -0,0 +1 @@
uid://cpit2uvrdud6h
@@ -0,0 +1,29 @@
class_name CanonicalWowWorldPosition
extends RefCounted
## Immutable WoW 3.3.5a world position in client/server axis order and yards.
## X and Y are the horizontal coordinates reported by the client and world
## server; Z is height. This type deliberately does not expose a [Vector3].
var x_yards: float:
get:
return _x_yards
var y_yards: float:
get:
return _y_yards
var z_yards: float:
get:
return _z_yards
var _x_yards: float
var _y_yards: float
var _z_yards: float
## Creates a canonical position without changing precision or validating map bounds.
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
_x_yards = x_yards_value
_y_yards = y_yards_value
_z_yards = z_yards_value
@@ -0,0 +1 @@
uid://b75i20d4vnkyh
@@ -0,0 +1,16 @@
class_name CanonicalWowWorldYaw
extends RefCounted
## Immutable canonical world-facing yaw in radians.
## Zero faces increasing WoW X and positive rotation turns toward increasing Y.
var radians: float:
get:
return _radians
var _radians: float
## Creates a yaw without normalization; [CoordinateMapper] normalizes conversions.
func _init(radians_value: float) -> void:
_radians = radians_value
@@ -0,0 +1 @@
uid://ddu5eiobntlrm
+137
View File
@@ -0,0 +1,137 @@
class_name CoordinateMapper
extends RefCounted
## Pure, stateless conversions between OpenWC coordinate value objects.
## This is the only domain contract allowed to change coordinate axes. It does
## not depend on Node, Resource, scene state, map assets or renderer globals.
const ADT_TILES_PER_MAP_AXIS: int = 64
const ADT_CHUNKS_PER_TILE_AXIS: int = 16
const ADT_TILE_SIZE_YARDS: float = 1600.0 / 3.0
const ADT_CHUNK_SIZE_YARDS: float = ADT_TILE_SIZE_YARDS / float(ADT_CHUNKS_PER_TILE_AXIS)
const WOW_WORLD_HALF_EXTENT_YARDS: float = ADT_TILE_SIZE_YARDS * float(ADT_TILES_PER_MAP_AXIS) * 0.5
## Converts server/wire X/Y/Z to canonical WoW X/Y/Z without an axis swap.
static func server_to_canonical(server_position: ServerWorldPosition) -> CanonicalWowWorldPosition:
return CanonicalWowWorldPosition.new(
server_position.x_yards,
server_position.y_yards,
server_position.z_yards
)
## Converts canonical WoW X/Y/Z to server/wire X/Y/Z without an axis swap.
static func canonical_to_server(canonical_position: CanonicalWowWorldPosition) -> ServerWorldPosition:
return ServerWorldPosition.new(
canonical_position.x_yards,
canonical_position.y_yards,
canonical_position.z_yards
)
## Converts canonical WoW world axes to the Godot Y-up compatibility basis.
static func canonical_to_godot(canonical_position: CanonicalWowWorldPosition) -> GodotWorldPosition:
return GodotWorldPosition.new(
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards,
canonical_position.z_yards,
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards
)
## Converts the Godot Y-up compatibility basis to canonical WoW world axes.
static func godot_to_canonical(godot_position: GodotWorldPosition) -> CanonicalWowWorldPosition:
return CanonicalWowWorldPosition.new(
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.z_units,
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.x_units,
godot_position.y_units
)
## 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(
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards,
canonical_position.z_yards,
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards
)
## Converts MDDF/MODF X/height/Z file fields to canonical WoW coordinates.
static func adt_placement_to_canonical(adt_position: AdtPlacementPosition) -> CanonicalWowWorldPosition:
return CanonicalWowWorldPosition.new(
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.x_offset_yards,
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.z_offset_yards,
adt_position.height_yards
)
## Returns unclamped ADT filename indices for a canonical position.
## Exact south/east boundaries belong to the next tile by floor semantics.
static func canonical_to_adt_tile(canonical_position: CanonicalWowWorldPosition) -> AdtTileCoordinate:
return AdtTileCoordinate.new(
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.y_yards / ADT_TILE_SIZE_YARDS)),
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.x_yards / ADT_TILE_SIZE_YARDS))
)
## Converts a canonical position to east/south offsets inside its ADT tile.
static func canonical_to_adt_tile_local(canonical_position: CanonicalWowWorldPosition) -> AdtTileLocalPosition:
var tile_coordinate := canonical_to_adt_tile(canonical_position)
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
return AdtTileLocalPosition.new(
tile_west_edge_y_yards - canonical_position.y_yards,
canonical_position.z_yards,
tile_north_edge_x_yards - canonical_position.x_yards
)
## Reconstructs canonical coordinates from an ADT tile and tile-local offsets.
static func adt_tile_local_to_canonical(
tile_coordinate: AdtTileCoordinate,
local_position: AdtTileLocalPosition
) -> CanonicalWowWorldPosition:
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
return CanonicalWowWorldPosition.new(
tile_north_edge_x_yards - local_position.south_yards,
tile_west_edge_y_yards - local_position.east_yards,
local_position.height_yards
)
## Returns unclamped MCNK indices for a tile-local position.
static func adt_tile_local_to_chunk(
tile_coordinate: AdtTileCoordinate,
local_position: AdtTileLocalPosition
) -> AdtChunkCoordinate:
return AdtChunkCoordinate.new(
tile_coordinate,
int(floor(local_position.east_yards / ADT_CHUNK_SIZE_YARDS)),
int(floor(local_position.south_yards / ADT_CHUNK_SIZE_YARDS))
)
## Converts server movement yaw to canonical yaw and normalizes it to [-PI, PI).
static func server_to_canonical_yaw(server_yaw: ServerWorldYaw) -> CanonicalWowWorldYaw:
return CanonicalWowWorldYaw.new(_normalize_yaw(server_yaw.radians))
## Converts canonical yaw to server movement yaw and normalizes it to [-PI, PI).
static func canonical_to_server_yaw(canonical_yaw: CanonicalWowWorldYaw) -> ServerWorldYaw:
return ServerWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
## Converts canonical world yaw to Godot Y-axis yaw in the mapped world basis.
static func canonical_to_godot_yaw(canonical_yaw: CanonicalWowWorldYaw) -> GodotWorldYaw:
return GodotWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
## Converts Godot Y-axis world yaw to canonical yaw in the mapped world basis.
static func godot_to_canonical_yaw(godot_yaw: GodotWorldYaw) -> CanonicalWowWorldYaw:
return CanonicalWowWorldYaw.new(_normalize_yaw(godot_yaw.radians))
static func _normalize_yaw(radians: float) -> float:
return fposmod(radians + PI, TAU) - PI
@@ -0,0 +1 @@
uid://bg8p8rrkmg84d
@@ -0,0 +1,29 @@
class_name GodotWorldPosition
extends RefCounted
## Immutable Godot Y-up renderer position.
## One Godot unit equals one WoW yard in the compatibility renderer. X grows
## east, Y grows up and Z grows south from the north-west map extent.
var x_units: float:
get:
return _x_units
var y_units: float:
get:
return _y_units
var z_units: float:
get:
return _z_units
var _x_units: float
var _y_units: float
var _z_units: float
## Creates a renderer position without exposing it as a cross-layer [Vector3].
func _init(x_units_value: float, y_units_value: float, z_units_value: float) -> void:
_x_units = x_units_value
_y_units = y_units_value
_z_units = z_units_value
@@ -0,0 +1 @@
uid://tp7jo14w25nc
+17
View File
@@ -0,0 +1,17 @@
class_name GodotWorldYaw
extends RefCounted
## Immutable Godot Y-axis yaw in radians for the canonical world basis.
## With Godot forward `-Z`, the numeric angle equals WoW world yaw after the
## position basis conversion. Model-local MDDF/MODF corrections are separate.
var radians: float:
get:
return _radians
var _radians: float
## Creates a Godot world yaw without normalization.
func _init(radians_value: float) -> void:
_radians = radians_value
@@ -0,0 +1 @@
uid://cuy2skhdv3f6g
@@ -0,0 +1,29 @@
class_name ServerWorldPosition
extends RefCounted
## Immutable TrinityCore/AzerothCore world position in wire/database X/Y/Z order.
## Units are WoW yards. A distinct type prevents server positions from being
## passed directly to renderer APIs even though the scalar values are canonical.
var x_yards: float:
get:
return _x_yards
var y_yards: float:
get:
return _y_yards
var z_yards: float:
get:
return _z_yards
var _x_yards: float
var _y_yards: float
var _z_yards: float
## Creates a server position without changing precision or validating map bounds.
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
_x_yards = x_yards_value
_y_yards = y_yards_value
_z_yards = z_yards_value
@@ -0,0 +1 @@
uid://dilhr7ym3xr43
@@ -0,0 +1,16 @@
class_name ServerWorldYaw
extends RefCounted
## Immutable TrinityCore/AzerothCore movement orientation in radians.
## It uses the WoW client convention: zero faces +X and positive turns toward +Y.
var radians: float:
get:
return _radians
var _radians: float
## Creates a server yaw without normalization.
func _init(radians_value: float) -> void:
_radians = radians_value
@@ -0,0 +1 @@
uid://dr8k82t2qgewd
+17
View File
@@ -0,0 +1,17 @@
class_name StreamingFocus
extends RefCounted
## Immutable world-streaming point independent of cameras and the scene tree.
## Renderer adapters may obtain this position from a player, spectator, editor
## viewport or capture tool, but streaming consumers receive only this value.
var world_position: GodotWorldPosition:
get:
return _world_position
var _world_position: GodotWorldPosition
## Creates a focus at an explicit position in the Godot renderer basis.
func _init(world_position_value: GodotWorldPosition) -> void:
_world_position = world_position_value
@@ -0,0 +1 @@
uid://ehv78jvge0q2
@@ -53,6 +53,7 @@ fog_depth_end = 5200.0
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
script = ExtResource("1_stream")
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
quality_preset = "High"
update_interval = 0.1
@@ -54,6 +54,7 @@ fog_depth_end = 5200.0
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
script = ExtResource("1_sisqv")
map_name = "Kalimdor"
streaming_focus_source_path = NodePath("ThirdPersonPlayer")
camera_path = NodePath("ThirdPersonPlayer/CameraPivot/Camera3D")
quality_preset = "High"
update_interval = 0.1
+66 -38
View File
@@ -1,5 +1,5 @@
@tool
## Streams Azeroth terrain around the active camera with chunk-based LODs.
## Streams Azeroth terrain around an explicit [StreamingFocus] with chunk-based LODs.
extends Node3D
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
@@ -12,6 +12,8 @@ const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_buil
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
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 REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
@@ -29,6 +31,9 @@ const QUALITY_HIGH := "High"
@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
## Optional scene adapter source. Any Node3D may provide the runtime focus.
@export var streaming_focus_source_path: NodePath
## Camera used only by the optional automatic overview positioning feature.
@export var camera_path: NodePath
@export_enum("Custom", "Performance", "Balanced", "High") var quality_preset: String = QUALITY_CUSTOM
@@ -213,6 +218,8 @@ var _tile_min := Vector2i(63, 63)
var _tile_max := Vector2i(0, 0)
var _camera_initialized := false
var _editor_signature := ""
var _streaming_focus: StreamingFocus
var _missing_focus_source_reported := false
var _last_focus_pos := Vector3.ZERO
var _dbg_chunks_created := 0
var _dbg_chunks_null := 0
@@ -293,6 +300,8 @@ func _ready() -> void:
_tick_editor_streaming()
elif auto_position_camera:
_position_camera_over_world()
if not Engine.is_editor_hint():
_capture_streaming_focus_from_source()
set_process(true)
call_deferred("_refresh_streaming_targets_after_ready")
@@ -364,7 +373,7 @@ func _process(delta: float) -> void:
if Engine.is_editor_hint():
_tick_editor_streaming()
else:
_refresh_streaming_targets(false)
refresh_streaming_focus(false)
did_refresh = true
_profile_section(timings, "refresh", section_start, profile_enabled)
_log_hitch_profile(profile_start, timings, did_refresh, profile_enabled)
@@ -590,10 +599,11 @@ func _tick_editor_streaming() -> void:
var focus_world := _tile_center_to_world(editor_preview_center_x, editor_preview_center_y)
if editor_follow_view_camera:
var editor_camera := _get_stream_camera()
var editor_camera := _get_editor_view_camera()
if editor_camera:
focus_world += editor_camera.global_position
_refresh_editor_streaming_targets_at(focus_world, false)
_set_streaming_focus_from_vector3(focus_world)
_refresh_editor_streaming_targets_at(_streaming_focus_to_vector3(), false)
func _scan_available_tiles() -> void:
@@ -681,24 +691,28 @@ func _load_tiles_from_directory() -> void:
_tile_max.y = max(_tile_max.y, ty)
func _refresh_streaming_targets(force: bool) -> void:
var camera := _get_stream_camera()
if camera == null:
return
## Replaces the current streaming point without requiring a Node or Camera3D.
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
_streaming_focus = streaming_focus
## Samples the configured scene source, then refreshes streaming from the typed focus.
## Returns false when no valid focus is available; existing streamed content is retained.
func refresh_streaming_focus(force: bool = false) -> bool:
_capture_streaming_focus_from_source()
if _streaming_focus == null or _streaming_focus.world_position == null:
return false
_terrain_root.position = Vector3.ZERO
_refresh_streaming_targets_at(camera.global_position, force)
_refresh_streaming_targets_at(_streaming_focus_to_vector3(), force)
return true
func _refresh_streaming_targets_after_ready() -> void:
var camera := _get_stream_camera()
if camera == null:
return
_terrain_root.position = Vector3.ZERO
_has_refresh_focus = false
_refresh_streaming_targets_at(camera.global_position, false)
refresh_streaming_focus(false)
## Core streaming update — works for both game (camera pos) and editor (preview center pos).
## Core streaming update for runtime and editor focus positions.
func _refresh_editor_streaming_targets_at(focus_pos: Vector3, force: bool) -> void:
if not force and not _should_refresh_focus(focus_pos):
return
@@ -3048,32 +3062,46 @@ func _is_tile_queued(key: String) -> bool:
return false
func _get_stream_camera() -> Camera3D:
if Engine.is_editor_hint():
# The 3D editor viewport camera lives in EditorInterface, not get_viewport().
# get_viewport().get_camera_3d() from a @tool script returns whichever Camera3D
# belongs to the edited scene (often null), so editor-view movement is invisible
# unless we reach into EditorInterface.
var vp := EditorInterface.get_editor_viewport_3d(0)
if vp != null:
var cam := vp.get_camera_3d()
if cam:
return cam
func _capture_streaming_focus_from_source() -> void:
if streaming_focus_source_path == NodePath():
return
var focus_source := get_node_or_null(streaming_focus_source_path) as Node3D
if focus_source == null:
if not _missing_focus_source_reported:
push_warning("Streaming focus source is missing or is not Node3D: %s" % streaming_focus_source_path)
_missing_focus_source_reported = true
return
_missing_focus_source_reported = false
_set_streaming_focus_from_vector3(focus_source.global_position)
if camera_path != NodePath():
var from_path := get_node_or_null(camera_path)
if from_path is Camera3D:
return from_path
var viewport_camera := get_viewport().get_camera_3d()
if viewport_camera:
return viewport_camera
func _set_streaming_focus_from_vector3(world_position: Vector3) -> void:
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(
world_position.x,
world_position.y,
world_position.z
)
set_streaming_focus(STREAMING_FOCUS_SCRIPT.new(typed_world_position))
for child in get_children():
if child is Camera3D and child.current:
return child
return null
func _streaming_focus_to_vector3() -> Vector3:
var world_position: GodotWorldPosition = _streaming_focus.world_position
return Vector3(world_position.x_units, world_position.y_units, world_position.z_units)
func _get_editor_view_camera() -> Camera3D:
# EditorInterface owns this camera. This method is the explicit editor adapter;
# runtime streaming never discovers a viewport camera.
var editor_viewport := EditorInterface.get_editor_viewport_3d(0)
if editor_viewport == null:
return null
return editor_viewport.get_camera_3d()
func _get_auto_position_camera() -> Camera3D:
if camera_path == NodePath():
return null
return get_node_or_null(camera_path) as Camera3D
func _can_use_baked_tile_cache() -> bool:
@@ -5365,7 +5393,7 @@ func _position_camera_over_world() -> void:
if _camera_initialized:
return
var camera := _get_stream_camera()
var camera := _get_auto_position_camera()
if camera == null:
return
+99
View File
@@ -0,0 +1,99 @@
{
"schema_version": 1,
"coordinate_contract_version": 2,
"tolerances": {
"position_yards": 0.002,
"yaw_radians": 0.000001
},
"sources": {
"azerothcore_playercreateinfo": {
"status": "verified",
"repository": "azerothcore/database-wotlk",
"revision": "68fcf0098b16388093989627f37be530fc91f07d",
"path": "sql/base/playercreateinfo.sql",
"url": "https://raw.githubusercontent.com/azerothcore/database-wotlk/68fcf0098b16388093989627f37be530fc91f07d/sql/base/playercreateinfo.sql"
},
"trinitycore_playercreateinfo": {
"status": "not_verified",
"repository": "TrinityCore/TrinityCore",
"branch": "3.3.5",
"reason": "The base development schema does not provide a pinned populated playercreateinfo row; verify against a pinned TDB snapshot before claiming equivalence."
},
"azeroth_32_48_adt": {
"status": "verified_private_metadata",
"file_name": "Azeroth_32_48.adt",
"sha256": "D66E27FD82B461AE38B78562952AED428E619777A3F3E35DD8FF97444644770A",
"distribution": "Numeric metadata only; proprietary file remains outside Git."
},
"original_client_build_12340": {
"status": "verified_private_observation",
"build": 12340,
"manifest": "res://src/tools/render_baseline_manifest.json",
"evidence_document": "docs/M00_FINAL_PAIRED_EVIDENCE.md"
}
},
"server_spawns": [
{
"name": "azerothcore_human_warrior_start",
"source": "azerothcore_playercreateinfo",
"backend": "AzerothCore",
"table": "playercreateinfo",
"key": {"race": 1, "class": 1},
"map_id": 0,
"map_directory": "Azeroth",
"zone_id": 12,
"server_position": [-8949.95, -132.493, 83.5312],
"server_yaw_radians": 0.0,
"expected_adt_placement": [26016.616666667, 83.5312, 17199.159666667],
"expected_adt_tile": [32, 48],
"expected_adt_chunk": [3, 12],
"expected_adt_tile_local": [132.493, 83.5312, 416.616666667],
"expected_godot_position": [17199.159666667, 83.5312, 26016.616666667],
"expected_godot_yaw_radians": 0.0
}
],
"raw_adt_chunks": [
{
"name": "azeroth_32_48_mcnk_3_12",
"source": "azeroth_32_48_adt",
"map_directory": "Azeroth",
"adt_tile": [32, 48],
"mcnk_index": [3, 12],
"raw_header_canonical_origin": [-8933.33203125, -100.0, 84.238929749],
"sample_position": "chunk_center",
"expected_godot_chunk_center": [17183.333333333, 84.238929749, 26016.665364583]
}
],
"original_client_points": [
{
"name": "elwynn_terrain_overview",
"source": "original_client_build_12340",
"client_position": [-9153.334, 386.666, 180.0],
"expected_godot_position": [16680.0, 180.0, 26220.0]
},
{
"name": "elwynn_adt_boundary",
"source": "original_client_build_12340",
"client_position": [-9523.334, 46.666, 105.0],
"expected_godot_position": [17020.0, 105.0, 26590.0]
},
{
"name": "goldshire_dense_m2",
"source": "original_client_build_12340",
"client_position": [-9400.0, 110.0, 150.0],
"expected_godot_position": [16956.666, 150.0, 26466.666]
},
{
"name": "goldshire_inn_large_wmo",
"source": "original_client_build_12340",
"client_position": [-9385.0, 53.0, 72.0],
"expected_godot_position": [17013.666, 72.0, 26451.666]
},
{
"name": "elwynn_waterfall_liquid",
"source": "original_client_build_12340",
"client_position": [-9300.0, 585.0, 190.0],
"expected_godot_position": [16481.666, 190.0, 26366.666]
}
]
}
+3 -3
View File
@@ -64,7 +64,7 @@ func _capture_async() -> void:
camera.far = 50000.0
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
(world as Node3D).add_child(camera)
world.set("camera_path", NodePath("CheckpointCamera"))
world.set("streaming_focus_source_path", NodePath("CheckpointCamera"))
world.set("debug_streaming", true)
world.set("runtime_stats_enabled", true)
get_root().add_child(world)
@@ -126,8 +126,8 @@ func _capture_async() -> void:
if player != null:
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
if world.has_method("_refresh_streaming_targets_at"):
world.call("_refresh_streaming_targets_at", camera.global_position, true)
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
if dry_run:
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s yaw_offset=%.2f pitch_offset=%.2f time=%.2f" % [
+3 -3
View File
@@ -28,7 +28,7 @@ func _run_async() -> void:
camera.name = "OccluderProbeCamera"
camera.current = true
world.add_child(camera)
world.set("camera_path", NodePath(camera.name))
world.set("streaming_focus_source_path", NodePath(camera.name))
world.set("debug_streaming", false)
get_root().add_child(world)
await process_frame
@@ -47,8 +47,8 @@ func _run_async() -> void:
var camera_position := _vector3(checkpoint.get("camera", []))
var target_position := _vector3(checkpoint.get("target", []))
camera.global_position = camera_position
if world.has_method("_refresh_streaming_targets_at"):
world.call("_refresh_streaming_targets_at", camera_position, true)
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var geometry_nodes: Array[Node3D] = []
_collect_geometry_nodes(world, geometry_nodes)
+3 -3
View File
@@ -36,7 +36,7 @@ func _run_async() -> void:
var camera := Camera3D.new()
camera.current = true
world.add_child(camera)
world.set("camera_path", NodePath(camera.name))
world.set("streaming_focus_source_path", NodePath(camera.name))
world.set("debug_streaming", false)
get_root().add_child(world)
await process_frame
@@ -53,8 +53,8 @@ func _run_async() -> void:
continue
var camera_position := _vector3(checkpoint.get("camera", []))
camera.global_position = camera_position
if world.has_method("_refresh_streaming_targets_at"):
world.call("_refresh_streaming_targets_at", camera_position, true)
if world.has_method("refresh_streaming_focus"):
world.call("refresh_streaming_focus", true)
await create_timer(maxf(0.1, wait_seconds)).timeout
var terrain_sample := _sample_terrain(world, camera_position)
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
@@ -0,0 +1,197 @@
extends SceneTree
## Validates the non-proprietary M01 coordinate fixture against coordinate
## contract version 2 and the existing M00 build-12340 manifest. The script does
## not read server databases, proprietary ADT bytes or original-client images.
const CoordinateMapperScript = preload("res://src/domain/coordinates/coordinate_mapper.gd")
const CanonicalWowWorldPositionScript = preload("res://src/domain/coordinates/canonical_wow_world_position.gd")
const ServerWorldPositionScript = preload("res://src/domain/coordinates/server_world_position.gd")
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")
const FIXTURE_PATH := "res://src/tests/fixtures/coordinate_golden_points.json"
const RENDER_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const SUPPORTED_FIXTURE_SCHEMA_VERSION := 1
const REQUIRED_COORDINATE_CONTRACT_VERSION := 2
func _initialize() -> void:
var failures: Array[String] = []
var fixture := _load_json_object(FIXTURE_PATH, failures)
var render_manifest := _load_json_object(RENDER_MANIFEST_PATH, failures)
if fixture.is_empty() or render_manifest.is_empty():
_report_and_quit(failures)
return
_expect_equal(fixture.get("schema_version", -1), SUPPORTED_FIXTURE_SCHEMA_VERSION, "fixture schema version", failures)
_expect_equal(fixture.get("coordinate_contract_version", -1), REQUIRED_COORDINATE_CONTRACT_VERSION, "coordinate contract version", failures)
var position_tolerance_yards := float(fixture.get("tolerances", {}).get("position_yards", 0.0))
var yaw_tolerance_radians := float(fixture.get("tolerances", {}).get("yaw_radians", 0.0))
_expect_true(position_tolerance_yards > 0.0, "positive position tolerance", failures)
_expect_true(yaw_tolerance_radians > 0.0, "positive yaw tolerance", failures)
_verify_source_provenance(fixture.get("sources", {}), failures)
_verify_server_spawns(fixture.get("server_spawns", []), position_tolerance_yards, yaw_tolerance_radians, failures)
_verify_raw_adt_chunks(fixture.get("raw_adt_chunks", []), position_tolerance_yards, failures)
_verify_original_client_points(fixture.get("original_client_points", []), render_manifest, position_tolerance_yards, failures)
if not failures.is_empty():
_report_and_quit(failures)
return
print("COORDINATE_GOLDEN_FIXTURES PASS server_spawns=1 raw_adt_chunks=1 original_client_points=5")
quit(0)
func _verify_source_provenance(sources: Dictionary, failures: Array[String]) -> void:
var azerothcore_source: Dictionary = sources.get("azerothcore_playercreateinfo", {})
_expect_equal(azerothcore_source.get("status", ""), "verified", "AzerothCore source status", failures)
_expect_equal(String(azerothcore_source.get("revision", "")).length(), 40, "AzerothCore pinned revision length", failures)
_expect_true(String(azerothcore_source.get("url", "")).contains(String(azerothcore_source.get("revision", ""))), "AzerothCore URL contains pinned revision", failures)
var trinitycore_source: Dictionary = sources.get("trinitycore_playercreateinfo", {})
_expect_equal(trinitycore_source.get("status", ""), "not_verified", "TrinityCore source status", failures)
_expect_true(not String(trinitycore_source.get("reason", "")).is_empty(), "TrinityCore gap reason", failures)
var adt_source: Dictionary = sources.get("azeroth_32_48_adt", {})
_expect_equal(adt_source.get("status", ""), "verified_private_metadata", "ADT source status", failures)
_expect_equal(String(adt_source.get("sha256", "")).length(), 64, "ADT SHA-256 length", failures)
func _verify_server_spawns(server_spawns: Array, position_tolerance_yards: float, yaw_tolerance_radians: float, failures: Array[String]) -> void:
_expect_equal(server_spawns.size(), 1, "server spawn count", failures)
for spawn_variant in server_spawns:
if not (spawn_variant is Dictionary):
failures.append("server spawn must be an object")
continue
var spawn: Dictionary = spawn_variant
var name := String(spawn.get("name", "server_spawn"))
var server_position_values := _require_three_numbers(spawn.get("server_position", null), "%s server_position" % name, failures)
if server_position_values.is_empty():
continue
var server_position = ServerWorldPositionScript.new(server_position_values[0], server_position_values[1], server_position_values[2])
var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
var adt_placement = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
var adt_tile = CoordinateMapperScript.canonical_to_adt_tile(canonical_position)
var adt_local = CoordinateMapperScript.canonical_to_adt_tile_local(canonical_position)
var adt_chunk = CoordinateMapperScript.adt_tile_local_to_chunk(adt_tile, adt_local)
_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], spawn.get("expected_godot_position", null), position_tolerance_yards, "%s Godot position" % name, failures)
_expect_three_near([adt_placement.x_offset_yards, adt_placement.height_yards, adt_placement.z_offset_yards], spawn.get("expected_adt_placement", null), position_tolerance_yards, "%s ADT placement" % name, failures)
_expect_three_near([adt_local.east_yards, adt_local.height_yards, adt_local.south_yards], spawn.get("expected_adt_tile_local", null), position_tolerance_yards, "%s ADT local" % name, failures)
_expect_pair([adt_tile.tile_x, adt_tile.tile_y], spawn.get("expected_adt_tile", null), "%s ADT tile" % name, failures)
_expect_pair([adt_chunk.chunk_x, adt_chunk.chunk_y], spawn.get("expected_adt_chunk", null), "%s ADT chunk" % name, failures)
var server_yaw = ServerWorldYawScript.new(float(spawn.get("server_yaw_radians", 0.0)))
var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(CoordinateMapperScript.server_to_canonical_yaw(server_yaw))
_expect_near(godot_yaw.radians, float(spawn.get("expected_godot_yaw_radians", 0.0)), yaw_tolerance_radians, "%s Godot yaw" % name, failures)
func _verify_raw_adt_chunks(raw_adt_chunks: Array, position_tolerance_yards: float, failures: Array[String]) -> void:
_expect_equal(raw_adt_chunks.size(), 1, "raw ADT chunk count", failures)
for chunk_variant in raw_adt_chunks:
if not (chunk_variant is Dictionary):
failures.append("raw ADT chunk must be an object")
continue
var chunk: Dictionary = chunk_variant
var name := String(chunk.get("name", "raw_adt_chunk"))
var origin_values := _require_three_numbers(chunk.get("raw_header_canonical_origin", null), "%s raw origin" % name, failures)
if origin_values.is_empty():
continue
var half_chunk_size_yards := CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 0.5
var chunk_center = CanonicalWowWorldPositionScript.new(
origin_values[0] - half_chunk_size_yards,
origin_values[1] - half_chunk_size_yards,
origin_values[2]
)
var tile_coordinate = CoordinateMapperScript.canonical_to_adt_tile(chunk_center)
var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(chunk_center)
var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, local_position)
_expect_pair([tile_coordinate.tile_x, tile_coordinate.tile_y], chunk.get("adt_tile", null), "%s ADT tile" % name, failures)
_expect_pair([chunk_coordinate.chunk_x, chunk_coordinate.chunk_y], chunk.get("mcnk_index", null), "%s MCNK index" % name, failures)
var godot_center = CoordinateMapperScript.canonical_to_godot(chunk_center)
_expect_three_near([godot_center.x_units, godot_center.y_units, godot_center.z_units], chunk.get("expected_godot_chunk_center", null), position_tolerance_yards, "%s Godot center" % name, failures)
func _verify_original_client_points(original_client_points: Array, render_manifest: Dictionary, position_tolerance_yards: float, failures: Array[String]) -> void:
_expect_equal(original_client_points.size(), 5, "original-client point count", failures)
var manifest_checkpoint_by_name: Dictionary = {}
for checkpoint_variant in render_manifest.get("checkpoints", []):
if checkpoint_variant is Dictionary:
manifest_checkpoint_by_name[String(checkpoint_variant.get("name", ""))] = checkpoint_variant
for point_variant in original_client_points:
if not (point_variant is Dictionary):
failures.append("original-client point must be an object")
continue
var point: Dictionary = point_variant
var name := String(point.get("name", "original_client_point"))
var client_position_values := _require_three_numbers(point.get("client_position", null), "%s client position" % name, failures)
if client_position_values.is_empty():
continue
var canonical_position = CanonicalWowWorldPositionScript.new(client_position_values[0], client_position_values[1], client_position_values[2])
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
_expect_three_near([godot_position.x_units, godot_position.y_units, godot_position.z_units], point.get("expected_godot_position", null), position_tolerance_yards, "%s mapped position" % name, failures)
var manifest_checkpoint: Dictionary = manifest_checkpoint_by_name.get(name, {})
_expect_true(not manifest_checkpoint.is_empty(), "%s exists in render manifest" % name, failures)
if not manifest_checkpoint.is_empty():
_expect_three_near(client_position_values, manifest_checkpoint.get("reference_wow_camera", null), position_tolerance_yards, "%s manifest client position" % name, failures)
_expect_three_near(point.get("expected_godot_position", null), manifest_checkpoint.get("camera", null), position_tolerance_yards, "%s manifest Godot position" % name, failures)
func _require_three_numbers(value_variant, label: String, failures: Array[String]) -> Array[float]:
if not (value_variant is Array) or value_variant.size() != 3:
failures.append("%s must contain three numbers" % label)
return []
return [float(value_variant[0]), float(value_variant[1]), float(value_variant[2])]
func _expect_three_near(actual_values, expected_variant, tolerance: float, label: String, failures: Array[String]) -> void:
var expected_values := _require_three_numbers(expected_variant, label, failures)
if expected_values.is_empty() or not (actual_values is Array) or actual_values.size() != 3:
return
for component_index in range(3):
_expect_near(float(actual_values[component_index]), expected_values[component_index], tolerance, "%s[%d]" % [label, component_index], failures)
func _expect_pair(actual_values: Array, expected_variant, label: String, failures: Array[String]) -> void:
if not (expected_variant is Array) or expected_variant.size() != 2:
failures.append("%s must contain two integers" % label)
return
_expect_equal(actual_values[0], int(expected_variant[0]), "%s[0]" % label, failures)
_expect_equal(actual_values[1], int(expected_variant[1]), "%s[1]" % label, failures)
func _expect_near(actual_value: float, expected_value: float, tolerance: float, label: String, failures: Array[String]) -> void:
if absf(actual_value - expected_value) > tolerance:
failures.append("%s expected %.9f, got %.9f" % [label, expected_value, actual_value])
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
if actual_value != expected_value:
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open JSON fixture: %s" % path)
return {}
var parsed = JSON.parse_string(file.get_as_text())
if not (parsed is Dictionary):
failures.append("JSON fixture is not an object: %s" % path)
return {}
return parsed
func _report_and_quit(failures: Array[String]) -> void:
if failures.is_empty():
failures.append("fixture validation stopped without a diagnostic")
for failure in failures:
push_error("COORDINATE_GOLDEN_FIXTURES: %s" % failure)
quit(1)
@@ -0,0 +1 @@
uid://cr8f88swvk002
+219
View File
@@ -0,0 +1,219 @@
extends SceneTree
## Headless contract verification for M01 canonical coordinate value objects and
## [CoordinateMapper]. Uses synthetic boundaries plus five positions observed in
## the original WoW 3.3.5a build 12340 during the M00 paired renderer session.
## The Elwynn filename case regression-checks a raw `Azeroth_31_49.adt` corpus.
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 AdtPlacementPositionScript = preload("res://src/domain/coordinates/adt_placement_position.gd")
const AdtTileCoordinateScript = preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
const AdtTileLocalPositionScript = preload("res://src/domain/coordinates/adt_tile_local_position.gd")
const CanonicalWowWorldYawScript = preload("res://src/domain/coordinates/canonical_wow_world_yaw.gd")
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")
const RENDER_BASELINE_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const GOLDEN_POSITION_TOLERANCE_YARDS := 0.002
const ROUND_TRIP_TOLERANCE_YARDS := 0.000001
const BOUNDARY_EPSILON_YARDS := 0.001
func _initialize() -> void:
var failures: Array[String] = []
_verify_build_12340_golden_positions(failures)
_verify_server_and_adt_round_trips(failures)
_verify_tile_and_chunk_boundaries(failures)
_verify_recorded_raw_adt_chunk_axes(failures)
_verify_world_yaw_conversions(failures)
if not failures.is_empty():
for failure in failures:
push_error("COORDINATE_MAPPER: %s" % failure)
quit(1)
return
print("COORDINATE_MAPPER PASS golden_points=5 tile_boundaries=8 raw_mcnk=2 yaw_cases=5")
quit(0)
func _verify_build_12340_golden_positions(failures: Array[String]) -> void:
var manifest := _load_json_object(RENDER_BASELINE_MANIFEST_PATH, failures)
if manifest.is_empty():
return
var golden_point_count := 0
for checkpoint_variant in manifest.get("checkpoints", []):
if not (checkpoint_variant is Dictionary):
continue
var checkpoint: Dictionary = checkpoint_variant
var reference_wow_camera_variant = checkpoint.get("reference_wow_camera", null)
if not (reference_wow_camera_variant is Array) or reference_wow_camera_variant.size() != 3:
continue
var expected_godot_camera_variant = checkpoint.get("camera", null)
if not (expected_godot_camera_variant is Array) or expected_godot_camera_variant.size() != 3:
failures.append("%s has no three-component renderer camera" % checkpoint.get("name", "checkpoint"))
continue
var canonical_position = CanonicalWowWorldPositionScript.new(
float(reference_wow_camera_variant[0]),
float(reference_wow_camera_variant[1]),
float(reference_wow_camera_variant[2])
)
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
_expect_near(godot_position.x_units, float(expected_godot_camera_variant[0]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot X" % checkpoint_name, failures)
_expect_near(godot_position.y_units, float(expected_godot_camera_variant[1]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Y" % checkpoint_name, failures)
_expect_near(godot_position.z_units, float(expected_godot_camera_variant[2]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Z" % checkpoint_name, failures)
var round_trip_position = CoordinateMapperScript.godot_to_canonical(godot_position)
_expect_canonical_position(round_trip_position, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "%s Godot round trip" % checkpoint_name, failures)
golden_point_count += 1
_expect_equal(golden_point_count, 5, "build 12340 golden point count", failures)
func _verify_server_and_adt_round_trips(failures: Array[String]) -> void:
var server_position = ServerWorldPositionScript.new(-9153.334, 386.666, 180.0)
var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
_expect_near(canonical_position.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X remains canonical X", failures)
_expect_near(canonical_position.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y remains canonical Y", failures)
_expect_near(canonical_position.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z remains canonical Z", failures)
var round_trip_server = CoordinateMapperScript.canonical_to_server(canonical_position)
_expect_near(round_trip_server.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X round trip", failures)
_expect_near(round_trip_server.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y round trip", failures)
_expect_near(round_trip_server.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z round trip", failures)
var adt_position = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
_expect_near(adt_position.x_offset_yards, 26220.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement X offset", failures)
_expect_near(adt_position.height_yards, 180.0, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement height", failures)
_expect_near(adt_position.z_offset_yards, 16680.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement Z offset", failures)
var adt_round_trip = CoordinateMapperScript.adt_placement_to_canonical(adt_position)
_expect_canonical_position(adt_round_trip, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement round trip", failures)
var direct_adt_position = AdtPlacementPositionScript.new(26220.000666666667, 180.0, 16680.000666666667)
var direct_canonical = CoordinateMapperScript.adt_placement_to_canonical(direct_adt_position)
_expect_canonical_position(direct_canonical, canonical_position, GOLDEN_POSITION_TOLERANCE_YARDS, "direct ADT placement conversion", failures)
func _verify_tile_and_chunk_boundaries(failures: Array[String]) -> void:
var half_extent := CoordinateMapperScript.WOW_WORLD_HALF_EXTENT_YARDS
var tile_size := CoordinateMapperScript.ADT_TILE_SIZE_YARDS
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(half_extent, half_extent, 0.0)), 0, 0, true, "north-west extent", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(0.0, 0.0, 0.0)), 32, 32, true, "map center", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(BOUNDARY_EPSILON_YARDS, BOUNDARY_EPSILON_YARDS, 0.0)), 31, 31, true, "north-west of center boundary", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-BOUNDARY_EPSILON_YARDS, -BOUNDARY_EPSILON_YARDS, 0.0)), 32, 32, true, "south-east of center boundary", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent + BOUNDARY_EPSILON_YARDS, -half_extent + BOUNDARY_EPSILON_YARDS, 0.0)), 63, 63, true, "inside south-east extent", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent, -half_extent, 0.0)), 64, 64, false, "exclusive south-east extent", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(tile_size, -tile_size, 0.0)), 33, 31, true, "whole-tile offsets", failures)
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0)), 31, 49, true, "raw Azeroth_31_49 filename", failures)
var tile_coordinate = AdtTileCoordinateScript.new(31, 49)
var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0))
var local_round_trip = CoordinateMapperScript.adt_tile_local_to_canonical(tile_coordinate, local_position)
_expect_canonical_position(local_round_trip, CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0), ROUND_TRIP_TOLERANCE_YARDS, "ADT tile-local round trip", failures)
_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 chunk_local_position = AdtTileLocalPositionScript.new(
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 2.0,
180.0,
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS
)
var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, chunk_local_position)
_expect_equal(chunk_coordinate.chunk_x, 2, "exact east chunk boundary", failures)
_expect_equal(chunk_coordinate.chunk_y, 1, "exact south chunk boundary", failures)
_expect_true(chunk_coordinate.is_within_map_grid(), "chunk coordinate is inside map grid", failures)
func _verify_recorded_raw_adt_chunk_axes(failures: Array[String]) -> void:
var half_chunk_size_yards := CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 0.5
var recorded_chunk_origins := [
{
"label": "raw MCNK 0,0",
"chunk_x": 0,
"chunk_y": 0,
"canonical_x_origin": -9066.666015625,
"canonical_y_origin": 533.333984375,
},
{
"label": "raw MCNK 15,15",
"chunk_x": 15,
"chunk_y": 15,
"canonical_x_origin": -9566.666015625,
"canonical_y_origin": 33.333984375,
},
]
for recorded_chunk_origin in recorded_chunk_origins:
var chunk_center = CanonicalWowWorldPositionScript.new(
float(recorded_chunk_origin.canonical_x_origin) - half_chunk_size_yards,
float(recorded_chunk_origin.canonical_y_origin) - half_chunk_size_yards,
0.0
)
var tile_coordinate = CoordinateMapperScript.canonical_to_adt_tile(chunk_center)
_expect_tile(tile_coordinate, 31, 49, true, String(recorded_chunk_origin.label), failures)
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_equal(chunk_coordinate.chunk_x, int(recorded_chunk_origin.chunk_x), "%s chunk X" % recorded_chunk_origin.label, failures)
_expect_equal(chunk_coordinate.chunk_y, int(recorded_chunk_origin.chunk_y), "%s chunk Y" % recorded_chunk_origin.label, failures)
func _verify_world_yaw_conversions(failures: Array[String]) -> void:
var yaw_cases := [0.0, PI * 0.5, -PI * 0.5, PI, TAU + 0.25]
for yaw_radians in yaw_cases:
var server_yaw = ServerWorldYawScript.new(float(yaw_radians))
var canonical_yaw = CoordinateMapperScript.server_to_canonical_yaw(server_yaw)
var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_yaw)
var canonical_round_trip = CoordinateMapperScript.godot_to_canonical_yaw(godot_yaw)
var server_round_trip = CoordinateMapperScript.canonical_to_server_yaw(canonical_round_trip)
var expected_normalized_yaw := fposmod(float(yaw_radians) + PI, TAU) - PI
_expect_near(canonical_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "canonical yaw normalization", failures)
_expect_near(godot_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "Godot yaw basis", failures)
_expect_near(server_round_trip.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "server yaw round trip", failures)
var canonical_pi_yaw = CanonicalWowWorldYawScript.new(PI)
var normalized_godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_pi_yaw)
_expect_near(normalized_godot_yaw.radians, -PI, ROUND_TRIP_TOLERANCE_YARDS, "positive PI normalizes to negative PI", failures)
func _expect_tile(tile_coordinate, expected_x: int, expected_y: int, expected_valid: bool, label: String, failures: Array[String]) -> void:
_expect_equal(tile_coordinate.tile_x, expected_x, "%s tile X" % label, failures)
_expect_equal(tile_coordinate.tile_y, expected_y, "%s tile Y" % label, failures)
_expect_equal(tile_coordinate.is_within_map_grid(), expected_valid, "%s validity" % label, failures)
func _expect_canonical_position(actual_position, expected_position, tolerance: float, label: String, failures: Array[String]) -> void:
_expect_near(actual_position.x_yards, expected_position.x_yards, tolerance, "%s X" % label, failures)
_expect_near(actual_position.y_yards, expected_position.y_yards, tolerance, "%s Y" % label, failures)
_expect_near(actual_position.z_yards, expected_position.z_yards, tolerance, "%s Z" % 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
@@ -0,0 +1 @@
uid://bmxjtf1fsrl2x
+101
View File
@@ -0,0 +1,101 @@
extends SceneTree
## Headless M01 contract and wiring regression for camera-independent streaming.
const StreamingFocusScript = preload("res://src/domain/streaming/streaming_focus.gd")
const GodotWorldPositionScript = preload("res://src/domain/coordinates/godot_world_position.gd")
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const RUNTIME_SCENE_PATHS: Array[String] = [
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"res://src/scenes/streaming/kalimdor_streaming.tscn",
]
const CAPTURE_TOOL_PATHS: Array[String] = [
"res://src/tools/capture_render_checkpoints.gd",
"res://src/tools/probe_render_camera_occluders.gd",
"res://src/tools/probe_render_terrain_height.gd",
]
func _initialize() -> void:
var failures: Array[String] = []
_verify_scene_free_focus_value(failures)
_verify_loader_boundary(failures)
_verify_runtime_scene_wiring(failures)
_verify_capture_tool_wiring(failures)
if not failures.is_empty():
for failure in failures:
push_error("STREAMING_FOCUS: %s" % failure)
quit(1)
return
print("STREAMING_FOCUS PASS contract=1 runtime_scenes=2 capture_tools=3")
quit(0)
func _verify_scene_free_focus_value(failures: Array[String]) -> void:
var position = GodotWorldPositionScript.new(17199.159666667, 83.5312, 26016.616666667)
var focus = StreamingFocusScript.new(position)
_expect_true(focus.world_position == position, "focus retains typed position", failures)
_expect_near(focus.world_position.x_units, 17199.159666667, "focus X", failures)
_expect_near(focus.world_position.y_units, 83.5312, "focus Y", failures)
_expect_near(focus.world_position.z_units, 26016.616666667, "focus Z", failures)
var focus_source := _read_text("res://src/domain/streaming/streaming_focus.gd", failures)
_expect_true(not focus_source.contains("extends Node"), "focus does not inherit Node", failures)
_expect_true(not focus_source.contains("extends Resource"), "focus does not inherit Resource", failures)
_expect_true(not focus_source.contains("Vector3"), "focus does not expose Vector3", failures)
func _verify_loader_boundary(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
for required_text in [
"@export var streaming_focus_source_path: NodePath",
"func set_streaming_focus(streaming_focus: StreamingFocus) -> void:",
"func refresh_streaming_focus(force: bool = false) -> bool:",
]:
_expect_true(loader_source.contains(required_text), "loader contains %s" % required_text, failures)
for forbidden_text in [
"get_viewport().get_camera_3d()",
"child is Camera3D and child.current",
"func _get_stream_camera()",
]:
_expect_true(not loader_source.contains(forbidden_text), "loader omits %s" % forbidden_text, failures)
func _verify_runtime_scene_wiring(failures: Array[String]) -> void:
for scene_path in RUNTIME_SCENE_PATHS:
var scene_source := _read_text(scene_path, failures)
_expect_true(
scene_source.contains('streaming_focus_source_path = NodePath("ThirdPersonPlayer")'),
"%s uses player focus" % scene_path,
failures
)
func _verify_capture_tool_wiring(failures: Array[String]) -> void:
for tool_path in CAPTURE_TOOL_PATHS:
var tool_source := _read_text(tool_path, failures)
_expect_true(tool_source.contains('world.set("streaming_focus_source_path"'), "%s sets explicit focus source" % tool_path, failures)
_expect_true(tool_source.contains('world.call("refresh_streaming_focus", true)'), "%s uses public refresh" % tool_path, failures)
_expect_true(not tool_source.contains('world.call("_refresh_streaming_targets_at"'), "%s avoids private refresh" % tool_path, failures)
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_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
if absf(actual_value - expected_value) > 0.000001:
failures.append("%s expected %.9f, got %.9f" % [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)
+1
View File
@@ -0,0 +1 @@
uid://b7ayw5a0bn1lq