Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc81059dfa | |||
| dd412e6d0d | |||
| 07f08044f4 | |||
| c8e99f28d6 | |||
| 48cc976ba0 | |||
| 318e6ae757 | |||
| 747fae8e2e | |||
| 24ddc99a7b | |||
| 52b6fa2ebb | |||
| 3933dbfc47 | |||
| a07a487368 | |||
| 47dec6c32c | |||
| c8e62acf4c | |||
| 7c64e8d60a | |||
| c1dc07c705 | |||
| d68786355c | |||
| 77cd17a5bc | |||
| 7815385b3b | |||
| fbef131bcb | |||
| 52e1b2ba67 | |||
| ce4f7ec0e2 | |||
| 758b375e7e | |||
| b55bfca2a7 | |||
| a7072e3d14 | |||
| f9c467b71f | |||
| 1c0dfc761d | |||
| f45695c26a | |||
| c2be8ae95e | |||
| 8636427234 | |||
| 5627180fd5 | |||
| ba81098797 | |||
| f19c0c45a7 |
@@ -35,3 +35,7 @@
|
||||
|
||||
# CMake build директория
|
||||
/src/native/build/
|
||||
|
||||
# Local root-level diagnostic capture logs
|
||||
/*.stdout.log
|
||||
/*.stderr.log
|
||||
|
||||
@@ -830,6 +830,49 @@ $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.
|
||||
|
||||
## 2026-07-13 Coordinate Conversion Boundary Gate
|
||||
|
||||
- Sky/area lookup, streaming tile ownership, player spawn/terrain sampling and
|
||||
terrain diagnostics now cross world-coordinate spaces through typed
|
||||
`CoordinateMapper` methods.
|
||||
- Renderer distance, LOD, ray and model-local M2/WMO calculations remain in
|
||||
their existing Godot/local spaces; they are not axis conversions.
|
||||
- The repository gate rejects new manual world-center, WoW/Godot and
|
||||
Godot-position-to-ADT formulas. The unified renderer baseline runs this gate.
|
||||
- Native ADT/WDT parsers keep one shared world conversion in
|
||||
`wow_chunk_reader.h`; the independent M00 calibration formula remains a test oracle.
|
||||
- Coordinate contract version 2, renderer radii, cache formats and visual
|
||||
quality profiles are unchanged.
|
||||
|
||||
## 2026-07-13 Golden Server Spawn Renderer Evidence
|
||||
|
||||
- The pinned AzerothCore Human Warrior start at server position
|
||||
`(-8949.95, -132.493, 83.5312)` maps through `CoordinateMapper` to Godot
|
||||
`(17199.159666667, 83.5312, 26016.616666667)`, ADT `(32,48)` and MCNK `(3,12)`.
|
||||
- `server_spawn_render_manifest.json` drives the ordinary Eastern Kingdoms
|
||||
checkpoint capture pipeline at that location; no live database or production
|
||||
spawn entity is introduced.
|
||||
- A renderer-native emissive marker starts at the exact mapped origin and keeps
|
||||
depth testing enabled so GUI evidence exposes its relationship to terrain.
|
||||
- `verify_server_spawn_renderer.gd` rejects drift between the pinned fixture,
|
||||
manifest target/player coordinates and expected ADT ownership. The unified
|
||||
renderer baseline runs this contract check before capture.
|
||||
- This closes a coordinate/renderer integration criterion only. It does not
|
||||
claim TrinityCore row equivalence or visual parity with the original client.
|
||||
|
||||
## Practical Rule For Future Work
|
||||
|
||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-ANIMATION-SKY-EVIDENCE-001 — Animation and sky evidence
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-ANIMATION-SKY-EVIDENCE-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-ANIMATION-SKY-EVIDENCE-001:d1df794 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-CAMERA-FRAMING-REFINEMENT-001 — Goldshire pitch/FOV refinement
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-FRAMING-REFINEMENT-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-FRAMING-REFINEMENT-001:3172f41 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-CAMERA-OCCLUDERS-001 — Camera occluder diagnostic
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-OCCLUDERS-001:sindo-main-codex:2026-07-13 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-OCCLUDERS-001:9d34a47 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-CAMERA-POSE-EVIDENCE-001 — Goldshire camera pose evidence
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-POSE-EVIDENCE-001:eca76c5 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-CAMERA-POSE-SWEEP-001 — Empirical camera pose sweep
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-POSE-SWEEP-001:657a1d8 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAPTURE-SHUTDOWN-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAPTURE-SHUTDOWN-001:8d4641a -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-COORD-CALIBRATION-001 — Renderer coordinate calibration
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-COORD-CALIBRATION-001:sindo-main-codex:2026-07-13 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-COORD-CALIBRATION-001:dfc1031 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-FINAL-PAIRED-EVIDENCE-001 — Final paired evidence audit
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-FINAL-PAIRED-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-FINAL-PAIRED-EVIDENCE-001:239cb24 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-FOV-SWEEP-001 — Empirical camera FOV sweep
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-FOV-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-FOV-SWEEP-001:f548507 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-GUI-RID-SHUTDOWN-001 — GUI capture RID shutdown
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-GUI-RID-SHUTDOWN-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-GUI-RID-SHUTDOWN-001:f19c0c4 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# M00-QAR-INTEGRATOR-CLOSEOUT-001 — M00 milestone closeout
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_HANDOFF:READY:M00-QAR-INTEGRATOR-CLOSEOUT-001:ba81098 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-integrator-closeout`
|
||||
- Lease expires UTC: 2026-07-15
|
||||
- Integrator: sindo-main-codex, assigned by the user on 2026-07-13
|
||||
|
||||
## Outcome
|
||||
|
||||
Audit merged M00 work packages, refresh final Evidence, close the milestone and activate M01.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change renderer behavior or compatibility claims.
|
||||
- Start M01 implementation.
|
||||
- Commit proprietary references or generated capture output.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: M00/M01 target status and final M00 Evidence
|
||||
- Shared/hotspots: `targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, completed M00 claims
|
||||
- Generated/ignored: `user://` post-merge reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: unchanged
|
||||
- Schema/format version: unchanged
|
||||
- Migration/compatibility: none
|
||||
- Consumers: milestone integrator and M01 work packages
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: merged M00 PRs #1 through #15 and final paired evidence
|
||||
- Blocks: M01 activation
|
||||
- External state: private reference images remain outside Git; their hashes and metrics are already recorded
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: post-merge render baseline dry-run, coordination gate, documentation gate
|
||||
- Fixtures: seven-checkpoint non-proprietary manifest and recorded private-reference hashes/metrics
|
||||
- Fidelity evidence: closeout preserves the explicit non-parity classification in `docs/M00_FINAL_PAIRED_EVIDENCE.md`
|
||||
- Performance budget: no runtime change
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: unchanged
|
||||
- Module specification: unchanged
|
||||
- Data-flow diagram: unchanged
|
||||
- Sequence/state/dependency diagrams: unchanged
|
||||
- Source map/status updates: target index, roadmap current state and M00 Evidence
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: none
|
||||
- Simplest considered solution: status and Evidence-only integrator patch
|
||||
- Rejected complexity/abstractions: new closeout tooling for a one-time milestone transition
|
||||
- Unavoidable complexity and justification: none
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready-for-review
|
||||
- Done: verified all M00 PRs and merge commits, refreshed final Evidence, closed M00, activated M01 and passed transition gates
|
||||
- Next: review and merge the closeout PR before starting an M01 implementation claim
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `ba81098`
|
||||
- Results: exactly one ACTIVE target (M01); M00 DONE marker accepted; post-merge baseline, comparator self-test, coordination and documentation gates passed
|
||||
- Remaining risks: documented renderer fidelity gaps and D3D12 fallback continue beyond M00
|
||||
- Documentation updated: target status/index, development-roadmap current state and final M00 Evidence
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-LANDMARK-REGION-DIFF-001 — Landmark-region visual diff
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-LANDMARK-REGION-DIFF-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-LANDMARK-REGION-DIFF-001:3d90c6d -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-TERRAIN-HEIGHT-001 — Terrain height diagnostic
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-TERRAIN-HEIGHT-001:sindo-main-codex:2026-07-13 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-TERRAIN-HEIGHT-001:f8538ba -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-TILE-OWNERSHIP-001 — Waterfall terrain tile ownership
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-TILE-OWNERSHIP-001:sindo-main-codex:2026-07-13 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-TILE-OWNERSHIP-001:2306200 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-VISUAL-DIFF-001 — Renderer checkpoint visual diff
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-VISUAL-DIFF-001:sindo-main-codex:2026-07-13 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-VISUAL-DIFF-001:19df5b7 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00-QAR-WMO-READINESS-001 — Goldshire WMO readiness
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-WMO-READINESS-001:sindo-main-codex:2026-07-14 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-WMO-READINESS-001:5092995 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# M01-FND-COORD-BOUNDARY-GATE-001 — Coordinate conversion boundary gate
|
||||
|
||||
<!-- OPENWC_CLAIM:M01-FND-COORD-BOUNDARY-GATE-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORD-BOUNDARY-GATE-001:c8e99f2 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M01
|
||||
- Program: FND/QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m01-coordinate-boundary-gate`
|
||||
- Lease expires UTC: 2026-07-15
|
||||
- Integrator: M01 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Route remaining GDScript world/tile axis conversions through `CoordinateMapper`
|
||||
and add a repository-wide gate that rejects new manual world-center, axis-swap
|
||||
or world-to-ADT tile formulas outside explicit contract/native-oracle boundaries.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Rewriting model-local M2/WMO vertex or placement orientation transforms.
|
||||
- Moving renderer distance/LOD geometry out of Godot space.
|
||||
- Replacing the native parser boundary with GDScript calls from worker threads.
|
||||
- Changing coordinate contract semantics, renderer radii or cache formats.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/verify_coordinate_conversion_boundaries.gd`
|
||||
- Shared/hotspots: `src/domain/coordinates/coordinate_mapper.gd`, coordinate
|
||||
tests/docs/ADR, streamer/player/sky/terrain-probe consumers,
|
||||
`docs/CODING_STANDARD.md`, this claim
|
||||
- Generated/ignored: Godot import metadata, extracted assets and renderer caches
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Additive mapper methods: Godot position to ADT tile/local and ADT tile/local
|
||||
to Godot position
|
||||
- Native exception: one formula definition in `src/native/src/wow_chunk_reader.h`;
|
||||
ADT/WDT loaders may call it but may not define copies
|
||||
- Oracle exception: `verify_render_coordinate_calibration.gd` retains one
|
||||
independent M00 formula to detect common-mode mapper regressions
|
||||
- Model-local transforms are outside the world-coordinate gate
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: coordinate contract v2 and M01 seams through `7c64e8d`
|
||||
- Blocks: M01 coordinate-centralization exit criterion and milestone evidence audit
|
||||
- External state: no dependency or proprietary input
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: conversion-boundary gate, mapper/golden fixtures, streaming focus,
|
||||
identities, renderer calibration/manifest and repository gates
|
||||
- Fixtures: Godot/ADT tile center, local offsets and exact boundaries
|
||||
- Fidelity evidence: existing build-12340 points and raw ADT/server fixture remain unchanged
|
||||
- Performance budget: mapper allocations occur only at adapter/update boundaries;
|
||||
renderer distance/LOD math remains in Godot scalars
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: new mapper methods
|
||||
- Module specification/ADR: boundary APIs, allowed exceptions and gate behavior
|
||||
- Coding standard: mandatory review rule for cross-space conversions
|
||||
- Data-flow diagram: existing coordinate module diagram updated for direct adapters
|
||||
- State/sequence diagrams: unchanged; pure synchronous mapping
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `godot_to_adt_tile`, `godot_to_adt_tile_local`,
|
||||
`adt_tile_local_to_godot`, `verify_coordinate_conversion_boundaries`
|
||||
- Simplest considered solution: extend the existing mapper and scan source text
|
||||
for narrow high-signal formula signatures
|
||||
- Rejected complexity/abstractions: generic coordinate algebra framework,
|
||||
native-to-GDScript calls on parser workers and a broad numeric-literal ban
|
||||
- Unavoidable complexity and justification: one native boundary and one
|
||||
independent test oracle must remain explicit to avoid worker coupling and common-mode tests
|
||||
- Measured optimization evidence: existing renderer distance/LOD scalar math is unchanged
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready-for-review
|
||||
- Done: mapper extensions, consumer migration, repository gate, review rule,
|
||||
integrated renderer gate, regression fixtures and documentation
|
||||
- Next: integrator review, merge and M01 completion-evidence audit
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `47dec6c`
|
||||
- Branch: `work/sindo-main-codex/m01-coordinate-boundary-gate`
|
||||
- Outcome: all audited GDScript world/tile conversions in sky, streaming,
|
||||
player spawn/ground sampling and terrain probes route through typed
|
||||
`CoordinateMapper` boundaries. A repository scan rejects new manual
|
||||
world-center, legacy axis-conversion, world-to-tile, tile-local and
|
||||
tile-center formulas.
|
||||
- Contract/schema: additive `godot_to_adt_tile`,
|
||||
`godot_to_adt_tile_local` and `adt_tile_local_to_godot` APIs; no persisted
|
||||
format, cache version or renderer radius changes.
|
||||
- Verification: CoordinateConversionBoundaries PASS (`83` files, one native
|
||||
boundary, one independent oracle, four consumers, six classifier guards);
|
||||
CoordinateMapper PASS (`5` golden points, `8` tile boundaries, `2` raw MCNK,
|
||||
`5` yaw cases); renderer calibration PASS (`5` points, maximum error
|
||||
`0.000015`); renderer manifest PASS (`7/7/7`); unified render baseline dry-run,
|
||||
headless project load, runtime cache shutdown, coordination, documentation
|
||||
and whitespace gates passed.
|
||||
- GUI regression: final ADT-boundary capture compared `2/2`, missing `0`,
|
||||
failed `0` (cold mean `0.0002324`, changed ratio `0.0013741`; warm mean
|
||||
`0.0000226`, changed ratio `0.0001285`). Earlier full capture produced all
|
||||
`14` images; `11/14` met the strict changed-pixel threshold. The three
|
||||
threshold-only differences retained low mean error (`<= 0.00622`) and were
|
||||
visually classified as cold M2 readiness and liquid animation phase, with
|
||||
unchanged camera, geometry alignment, area and streaming focus.
|
||||
- Fidelity: existing build-12340 calibrated points, raw ADT fixture and server
|
||||
spawn fixture remain unchanged. Native parser workers retain exactly one
|
||||
formula in `wow_chunk_reader.h`; the calibration verifier retains exactly one
|
||||
independent formula so mapper and oracle cannot share a common-mode defect.
|
||||
- Rebuild: none. Local ignored extracted assets/native binary/cache junctions
|
||||
were used for runtime checks and are not part of the commit.
|
||||
- Remaining risks: source scanning intentionally recognizes narrow high-signal
|
||||
formula signatures rather than parsing arbitrary algebra; native formula
|
||||
parity still relies on calibration/raw fixtures. Model-local M2/WMO basis
|
||||
transforms and Godot-space LOD distance math are intentionally outside scope.
|
||||
- Recommended merge order: after `7c64e8d`; this package closes the remaining
|
||||
M01 manual axis-conversion checklist item, but only the milestone integrator
|
||||
may update target Evidence/status or set `OPENWC_TARGET_DONE`.
|
||||
- Documentation: coding review rule, coordinate-mapping module API/input-output/
|
||||
data-flow/capability/risk/source-map sections, ADR 0001 and `RENDER.md` were
|
||||
updated. Existing pure synchronous data-flow diagram remains authoritative;
|
||||
no state/sequence diagram changed.
|
||||
@@ -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_INTEGRATION:ACCEPTED:M01-FND-COORD-TILE-AXIS-002:c8e99f2 -->
|
||||
|
||||
## 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
|
||||
@@ -0,0 +1,111 @@
|
||||
# M01-FND-COORDS-001 — Canonical coordinate contracts
|
||||
|
||||
<!-- OPENWC_CLAIM:M01-FND-COORDS-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORDS-001:c8e99f2 -->
|
||||
|
||||
## 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,112 @@
|
||||
# M01-FND-DOMAIN-IDENTITIES-001 — Stable domain identity namespaces
|
||||
|
||||
<!-- OPENWC_CLAIM:M01-FND-DOMAIN-IDENTITIES-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-DOMAIN-IDENTITIES-001:c8e99f2 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M01
|
||||
- Program: FND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m01-domain-identities`
|
||||
- Lease expires UTC: 2026-07-15
|
||||
- Integrator: M01 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Add scene-free, immutable identity values for stable authored content,
|
||||
session-local entities, opaque WoW wire GUIDs and namespaced server entries so
|
||||
callers cannot silently interchange their numeric/string representations.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Allocating Content IDs, Entity IDs or server entries.
|
||||
- Decoding WoW GUID high types before the build-12340 protocol package.
|
||||
- Defining map/spell/item/quest/display/sound IDs in this package.
|
||||
- Migrating gameplay, network or Content Project consumers that do not exist yet.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/domain/identity/`, `src/tools/verify_domain_identities.gd`,
|
||||
`docs/modules/domain-identities.md`, `docs/adr/0002-domain-identity-namespaces.md`
|
||||
- Shared/hotspots: `docs/ARCHITECTURE.md`, this claim
|
||||
- Generated/ignored: Godot import metadata
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- `ContentId`: canonical lowercase UUID text persisted with schema-owned content
|
||||
- `EntityId`: positive session-local integer, never a persisted content/server identity
|
||||
- `WowGuid`: opaque high/low unsigned 32-bit words preserving wire bits
|
||||
- `ServerEntryId`: positive integer plus non-empty adapter/table namespace
|
||||
- Migration: additive types only; no existing persisted data changes
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: M01 coordinate/focus seams through `77cd17a`
|
||||
- Blocks: Content Project schemas, server adapters, network entity mapping and gameplay registry
|
||||
- External state: no dependency or proprietary input
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: identity contract/boundary verifier, dependency search,
|
||||
coordination/documentation gates and headless project load
|
||||
- Fixtures: valid/invalid UUIDs, session IDs, full unsigned GUID words and
|
||||
duplicate numeric server entries in different namespaces
|
||||
- Fidelity evidence: GUID words remain opaque and lossless until build-12340
|
||||
packet fixtures establish semantic decomposition
|
||||
- Performance budget: constant-time scalar/string validation outside frame loops
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: every value and validation/equality method
|
||||
- Module specification: persistence, ownership, namespace mapping and recovery
|
||||
- Data-flow diagram: authored/runtime/wire/server identities into adapters
|
||||
- State/sequence diagrams: lifecycle diagram for content/session/wire/server identity
|
||||
- ADR: namespace separation and UUID persistence decision
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `ContentId`, `EntityId`, `WowGuid`, `ServerEntryId`
|
||||
- Simplest considered solution: four independent values with explicit fields
|
||||
- Rejected complexity/abstractions: generic ID base class, registry service,
|
||||
UUID generator and speculative GUID high-type enum
|
||||
- Unavoidable complexity and justification: server entry namespace and split GUID
|
||||
words prevent collisions and signed 64-bit loss
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready-for-review
|
||||
- Done: four identity values, boundary verifier, ADR, module specification,
|
||||
architecture mapping and repository gates
|
||||
- Next: integrator review and merge before the manual axis-conversion gate
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `c1dc07c`
|
||||
- Branch: `work/sindo-main-codex/m01-domain-identities`
|
||||
- Outcome: authored content, runtime entities, WoW wire GUIDs and namespaced
|
||||
server entries now have separate immutable scene-free value objects
|
||||
- Contract/schema: identity contract version 1; `ContentId` uses canonical
|
||||
lowercase UUID text. No existing schema/cache is migrated; containing future
|
||||
schemas still require their own version and duplicate/reference policy
|
||||
- Verification: DomainIdentities PASS (`5` ContentId, `5` EntityId, `6` WowGuid,
|
||||
`5` ServerEntryId cases); StreamingFocus, coordinate golden fixtures and
|
||||
CoordinateMapper PASS; M00 calibration PASS with maximum error `0.000015`;
|
||||
renderer manifest PASS (`7` checkpoints); headless project load exit `0`;
|
||||
coordination, documentation, naming/dependency and whitespace checks passed
|
||||
- Fidelity: `WowGuid` preserves two complete unsigned 32-bit wire words and
|
||||
explicitly does not claim build-12340 high-type semantics without packet
|
||||
fixtures; zero GUID remains an uninterpreted observable sentinel
|
||||
- Rebuild: none; no dependency or proprietary input. Clean-worktree project
|
||||
load reports expected missing ignored extracted assets/character resources
|
||||
- Remaining risks: no ID allocator, mapping registry or server namespace
|
||||
vocabulary exists yet; UUID shape does not enforce generator version/variant;
|
||||
GDScript callers must use domain `equals` or canonical keys rather than
|
||||
reference `==`
|
||||
- Recommended merge order: after StreamingFocus at `77cd17a`, before the M01
|
||||
manual axis-conversion rule/search gate
|
||||
- Documentation: ADR 0002, domain-identities module API/input-output/data-flow/
|
||||
lifecycle/persistence/recovery/capability/source-map sections, module registry
|
||||
and architecture Domain mapping added or updated
|
||||
@@ -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_INTEGRATION:ACCEPTED:M01-QAR-COORD-FIXTURES-001:c8e99f2 -->
|
||||
|
||||
## 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,101 @@
|
||||
# M01-QAR-INTEGRATOR-CLOSEOUT-001 — M01 milestone closeout
|
||||
|
||||
<!-- OPENWC_CLAIM:M01-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex:2026-07-16 -->
|
||||
<!-- OPENWC_HANDOFF:READY:M01-QAR-INTEGRATOR-CLOSEOUT-001:dd412e6 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M01
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m01-integrator-closeout`
|
||||
- Lease expires UTC: 2026-07-16
|
||||
- Integrator: sindo-main-codex, continuing the user-directed milestone workflow on 2026-07-14
|
||||
|
||||
## Outcome
|
||||
|
||||
Audit every merged M01 work package on current `master`, record final evidence,
|
||||
close M01 and activate M02 only if all target verification and documentation
|
||||
criteria pass.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change coordinate, identity, streaming or renderer behavior.
|
||||
- Start M02 implementation.
|
||||
- Add new compatibility or visual-parity claims.
|
||||
- Commit proprietary assets, generated UIDs, caches or capture output.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: M01/M02 target status, M01 final Evidence and M01 claim acceptance markers
|
||||
- Shared/hotspots: `targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, this claim
|
||||
- Generated/ignored: local Godot imports, renderer corpus and `user://` reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public APIs, schemas, cache formats and migrations: unchanged
|
||||
- Coordinate contract remains version 2
|
||||
- Fixture schema remains version 1
|
||||
- Consumers: milestone sequence and M02 work packages
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: all seven M01 READY commits integrated through master `c8e99f2`
|
||||
- Blocks: M02 activation
|
||||
- External state: server-spawn GUI hashes and original-client numeric evidence are recorded; proprietary files remain outside Git
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: all M01 domain/fixture/boundary/focus/identity/spawn gates,
|
||||
unified renderer dry-run, coordination/documentation checks and diff hygiene
|
||||
- Fidelity evidence: five build-12340 points, raw ADT metadata, pinned
|
||||
AzerothCore spawn and cache-complete GUI marker capture
|
||||
- Performance: no runtime change; preserve recorded renderer cold/warm evidence
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- M01 final target Evidence and checklist
|
||||
- M01 claim integration-acceptance markers
|
||||
- Target index and roadmap transition to M02
|
||||
- API/module specifications and diagrams are audited, not changed unless a gap is found
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: none beyond the closeout work-package ID
|
||||
- Simplest approach: evidence/status-only integrator patch
|
||||
- Rejected complexity: new one-off closeout framework or behavioral changes
|
||||
- Unavoidable complexity: none
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready-for-review
|
||||
- Done: accepted seven M01 packages, reran post-merge gates, recorded complete
|
||||
target Evidence, closed M01 and activated M02
|
||||
- Next: merge closeout commit before starting an M02 implementation claim
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `dd412e6`
|
||||
- Branch: `work/sindo-main-codex/m01-integrator-closeout`
|
||||
- Outcome: M01 is `DONE` with all seven checklist items and complete Evidence;
|
||||
M02 is the only `ACTIVE` target and `targets/README.md` points to it.
|
||||
- Integration audit: all seven M01 READY commits are ancestors of master
|
||||
`c8e99f2`; their claims now carry `OPENWC_INTEGRATION:ACCEPTED` markers.
|
||||
- Verification: unified renderer dry-run passed project load, material/dedupe/
|
||||
shutdown gates, manifest `7/7/7`, calibration maximum error `0.000015`,
|
||||
coordinate boundary (`84` files) and server-spawn renderer `(32,48)/(3,12)`;
|
||||
coordinate mapper, golden fixtures, StreamingFocus and domain identity gates
|
||||
passed. Coordination and documentation gates passed; coordination reports
|
||||
only five already-expired M00 claim warnings.
|
||||
- Fidelity: closeout preserves five build-12340 coordinate observations, raw ADT
|
||||
evidence, pinned AzerothCore spawn mapping and cache-complete Northshire GUI
|
||||
evidence without claiming original-client visual parity or TrinityCore row
|
||||
equivalence.
|
||||
- Rebuild/migration: none. Coordinate contract remains version 2; fixture schema
|
||||
remains version 1; renderer cache formats are unchanged.
|
||||
- Remaining risks: recorded in M01 Evidence—TrinityCore populated spawn,
|
||||
placement Euler parity, native parser boundary and broader renderer parity.
|
||||
- Documentation: M01 target Evidence/checklist, target index and roadmap current
|
||||
state updated. Existing coordinate, identity and renderer module specs/API/
|
||||
diagrams were audited and passed the documentation gate.
|
||||
@@ -0,0 +1,135 @@
|
||||
# M01-QAR-SERVER-SPAWN-RENDERER-001 — Golden server spawn renderer evidence
|
||||
|
||||
<!-- OPENWC_CLAIM:M01-QAR-SERVER-SPAWN-RENDERER-001:sindo-main-codex:2026-07-15 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-QAR-SERVER-SPAWN-RENDERER-001:c8e99f2 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M01
|
||||
- Program: QAR/RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m01-server-spawn-renderer`
|
||||
- Lease expires UTC: 2026-07-15
|
||||
- Integrator: M01 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Route the pinned AzerothCore Human Warrior spawn fixture through
|
||||
`CoordinateMapper` into the live Eastern Kingdoms renderer and produce
|
||||
repeatable headless contract checks plus GUI screenshot evidence at the mapped
|
||||
ADT tile/chunk.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Connecting to a live AzerothCore or TrinityCore database.
|
||||
- Claiming TrinityCore spawn equivalence without a pinned populated TDB row.
|
||||
- Changing coordinate formulas, streaming radii, terrain, player movement or
|
||||
production character presentation.
|
||||
- Adding proprietary assets or screenshots to Git.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/server_spawn_render_manifest.json`,
|
||||
`src/tools/verify_server_spawn_renderer.gd`
|
||||
- Shared/hotspots: `src/tools/capture_render_checkpoints.gd`, renderer runner,
|
||||
coordinate/world-renderer module documentation, `RENDER.md`, this claim
|
||||
- Generated/ignored: Godot UIDs/import metadata and `user://` PNG/JSON evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Fixture schema and coordinate contract version remain unchanged.
|
||||
- Additive diagnostic checkpoint metadata may request a renderer-native marker;
|
||||
existing M00 checkpoints remain byte-for-byte compatible.
|
||||
- The spawn remains sourced from pinned AzerothCore revision
|
||||
`68fcf0098b16388093989627f37be530fc91f07d`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: merged M01 packages through `a07a487`
|
||||
- Blocks: M01 golden server-spawn renderer criterion and milestone closeout
|
||||
- External state: GUI capture requires the existing local extracted/cached
|
||||
renderer corpus; headless contract verification remains non-proprietary
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated spawn verifier, coordinate fixture/mapper/boundary gates,
|
||||
renderer baseline dry-run, GUI checkpoint capture and human inspection
|
||||
- Fixture: AzerothCore race `1`, class `1`, map `0`, server position
|
||||
`(-8949.95, -132.493, 83.5312)`
|
||||
- Expected renderer result: Godot position
|
||||
`(17199.159666667, 83.5312, 26016.616666667)`, ADT `(32,48)`, MCNK `(3,12)`
|
||||
- Fidelity classification: server/coordinate integration evidence only; it does
|
||||
not establish full original-client visual parity
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API/tool documentation for new verifier and marker metadata
|
||||
- Coordinate-mapping module capability/evidence update
|
||||
- World-renderer checkpoint input/output/source-map update when marker support is added
|
||||
- Existing synchronous coordinate data-flow diagram updated only if flow changes
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names: `server_spawn_render_manifest`,
|
||||
`verify_server_spawn_renderer`, `diagnostic_spawn_marker`
|
||||
- Simplest approach: reuse the existing checkpoint capture pipeline with one
|
||||
dedicated manifest and a renderer-native diagnostic marker
|
||||
- Rejected complexity: live DB adapter, new scene-test framework and production
|
||||
spawn entity implementation before the network/gameplay milestones
|
||||
- Performance: one diagnostic checkpoint outside production runtime
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready-for-review
|
||||
- Done: fixture-to-renderer manifest/verifier, renderer-native marker, unified
|
||||
baseline integration, cold/warm GUI evidence, documentation and log cleanup
|
||||
- Next: integrator review, merge cleanup/handoff and M01 closeout audit
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commits: implementation `24ddc99`; required cleanup `318e6ae`
|
||||
- Branch: `work/sindo-main-codex/m01-server-spawn-renderer`
|
||||
- Outcome: the pinned AzerothCore Human Warrior start is routed through
|
||||
`CoordinateMapper` into the ordinary Eastern Kingdoms capture scene. A
|
||||
depth-tested magenta marker begins at the exact mapped spawn origin.
|
||||
- Contract/schema: additive diagnostic manifest schema 1 and marker metadata;
|
||||
coordinate contract version 2, production renderer APIs, cache formats and
|
||||
`Blizzlike335` behavior are unchanged.
|
||||
- Verification: `SERVER_SPAWN_RENDERER PASS spawn=1 checkpoint=1 tile=32_48
|
||||
chunk=3_12 profile=Blizzlike335`; unified renderer dry-run passed project,
|
||||
materials, M2 dedupe, shutdown, `7/7/7` manifest, five-point calibration,
|
||||
coordinate-boundary and server-spawn steps; coordinate golden fixtures,
|
||||
mapper, StreamingFocus, domain identities, documentation and whitespace gates
|
||||
passed. Coordination passed with five expired M00-claim warnings only.
|
||||
- GUI evidence: two `1280x900` cold/warm PNGs captured under
|
||||
`user://server_spawn_renderer_m01_gui_run2`; focus tile `(32,48)`, terrain
|
||||
quality `100/100`, missing cache `0`, queues drained, report results `2`.
|
||||
Human inspection confirms the marker is on rendered terrain beside the road
|
||||
at Northshire Abbey rather than below terrain or in an adjacent tile. PNG
|
||||
hashes: cold `c08c43bcb75e5b3277105afb0d4e5a76caae9db91a7db64488770ffa03e32589`,
|
||||
warm `1b75237a090ea163836372a8e39e778c909773f4508063adc3f70a3e12a85a62`.
|
||||
- Fidelity: source remains AzerothCore revision
|
||||
`68fcf0098b16388093989627f37be530fc91f07d`; server
|
||||
`(-8949.95,-132.493,83.5312)` maps to Godot
|
||||
`(17199.159666667,83.5312,26016.616666667)`, ADT `(32,48)`, MCNK `(3,12)`.
|
||||
This proves coordinate/renderer integration, not original-client visual parity
|
||||
or TrinityCore populated-row equivalence.
|
||||
- Runtime notes: D3D12 descriptor-heap initialization fell back to Vulkan;
|
||||
optional ignored character/ambient GLB imports were unavailable. Neither
|
||||
affected terrain/marker evidence. The known anonymous ObjectDB teardown
|
||||
warning remained; no Node, renderer resource or RID leak diagnostic occurred
|
||||
in the cache-complete run.
|
||||
- Repository hygiene: four accidentally committed local GUI logs are removed by
|
||||
`318e6ae`; root `*.stdout.log`/`*.stderr.log` files are now ignored. Generated
|
||||
PNG/JSON evidence and proprietary corpus remain outside Git.
|
||||
- Remaining risks: the diagnostic marker is not a production network entity;
|
||||
live server adapters belong to M06/M08. Exact original-client character spawn
|
||||
presentation and TrinityCore database equivalence remain unverified.
|
||||
- Documentation: `RENDER.md`, renderer-baseline procedure, coordinate-mapping
|
||||
and world-renderer module API/input-output/data-flow/capability/source-map
|
||||
sections updated. No lifecycle/state diagram changed because the verifier and
|
||||
mapper path are synchronous; capture reuses the documented renderer lifecycle.
|
||||
- Recommended merge order: `318e6ae` after master merge `747fae8`. Then the M01
|
||||
milestone integrator may accept all M01 claims, update target Evidence/checks
|
||||
and evaluate `OPENWC_TARGET_DONE`.
|
||||
@@ -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_INTEGRATION:ACCEPTED:M01-RND-STREAMING-FOCUS-001:c8e99f2 -->
|
||||
|
||||
## 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
|
||||
@@ -36,7 +36,12 @@ TrinityCore/AzerothCore ◄── Network Adapter ◄── Runtime Client
|
||||
|
||||
### Domain
|
||||
|
||||
Содержит идентификаторы, value objects, команды и события: `EntityId`, `WowGuid`, `MapPosition`, `SpellId`, `MoveIntent`, `EntityMoved`. Не содержит side effects.
|
||||
Содержит идентификаторы, value objects, команды и события: stable authored
|
||||
`ContentId`, session-local `EntityId`, opaque wire `WowGuid`, namespaced
|
||||
`ServerEntryId`, typed coordinates, `SpellId`, `MoveIntent`, `EntityMoved`.
|
||||
Эти identity namespaces не конвертируются неявно и не содержат side effects;
|
||||
их lifecycle и mapping определены в
|
||||
[`modules/domain-identities.md`](modules/domain-identities.md).
|
||||
|
||||
### Gameplay
|
||||
|
||||
|
||||
@@ -237,6 +237,23 @@ TODO/FIXME обязан содержать target/work-package ID или diagnos
|
||||
- Медленный понятный reference implementation полезен как oracle для optimized implementation.
|
||||
- Сложная оптимизация удаляется или упрощается, если она не даёт измеримого результата.
|
||||
|
||||
## Coordinate conversion boundary
|
||||
|
||||
- Cross-space world conversions выполняются только через typed API
|
||||
`CoordinateMapper`; ручные world-center offsets, swaps осей и вычисление ADT
|
||||
filename indices из Godot position запрещены.
|
||||
- Scene/render adapter МОЖЕТ создать `Vector3` только после получения
|
||||
`GodotWorldPosition` или перевести engine vector в typed scalar value на своей границе.
|
||||
- Единственная native world-position формула находится в
|
||||
`src/native/src/wow_chunk_reader.h`, поскольку parser workers не вызывают
|
||||
GDScript. Новая native копия требует отдельного boundary decision и parity fixture.
|
||||
- `verify_render_coordinate_calibration.gd` остаётся независимым test oracle;
|
||||
production code не копирует его формулы.
|
||||
- Model-local M2/WMO vertex basis и placement orientation не являются world
|
||||
coordinate conversion. Они документируются и тестируются своим format adapter.
|
||||
- `verify_coordinate_conversion_boundaries.gd` обязателен при изменении
|
||||
coordinates, native parsers, sky, player, streaming или renderer diagnostics.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- Можно ли понять назначение сущности по имени?
|
||||
@@ -245,6 +262,8 @@ TODO/FIXME обязан содержать target/work-package ID или diagnos
|
||||
- Можно ли убрать abstraction без потери correctness/boundary?
|
||||
- Есть ли скрытый mutable state или side effect?
|
||||
- Не появился ли второй источник истины?
|
||||
- Все ли cross-space world coordinates проходят через `CoordinateMapper` или
|
||||
документированную native boundary?
|
||||
- Измерена ли сложная optimization?
|
||||
- Изолирована и документирована ли неизбежная сложность?
|
||||
- Соответствуют ли API docs и diagrams фактическому коду?
|
||||
|
||||
+19
-1
@@ -10,7 +10,10 @@
|
||||
.\tools\run_render_baseline.ps1
|
||||
```
|
||||
|
||||
Скрипт последовательно проверяет headless-загрузку проекта, renderer materials, M2 unique-id dedupe, regression manifest и затем снимает checkpoint-ы. Для CI или быстрой проверки контракта без PNG:
|
||||
Скрипт последовательно проверяет headless-загрузку проекта, renderer materials,
|
||||
M2 unique-id dedupe, regression manifest, coordinate boundaries и dedicated
|
||||
server-spawn renderer contract, затем снимает checkpoint-ы. Для CI или быстрой
|
||||
проверки контракта без PNG:
|
||||
|
||||
```powershell
|
||||
.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1
|
||||
@@ -18,6 +21,21 @@
|
||||
|
||||
Если `godot` отсутствует в `PATH`, скрипт использует локальный executable, указанный в `RENDER.md`. Другой executable задаётся через `-GodotPath`.
|
||||
|
||||
Отдельный M01 server-spawn checkpoint использует тот же capture pipeline, но не
|
||||
расширяет семичастный M00 visual baseline:
|
||||
|
||||
```powershell
|
||||
godot --headless --path . --script res://src/tools/verify_server_spawn_renderer.gd
|
||||
godot --path . --script res://src/tools/capture_render_checkpoints.gd -- `
|
||||
--manifest res://src/tools/server_spawn_render_manifest.json `
|
||||
--output user://server_spawn_renderer
|
||||
```
|
||||
|
||||
Manifest связывает pinned AzerothCore Human Warrior spawn с Godot position,
|
||||
ADT tile/chunk и renderer-native diagnostic marker. Маркер начинается точно в
|
||||
spawn origin и проходит обычный depth test, поэтому screenshot показывает его
|
||||
отношение к terrain; это integration evidence, а не заявление visual parity.
|
||||
|
||||
Результат по умолчанию находится в `user://render_baseline`:
|
||||
|
||||
- `report.json` — Godot, OS, CPU, GPU/backend, viewport, revision, cache inventory, cache versions, параметры checkpoint-ов и результаты;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# ADR 0001: Canonical world coordinate contract
|
||||
|
||||
- Status: Accepted for M01 contract implementation
|
||||
- Date: 2026-07-13
|
||||
- Work packages: `M01-FND-COORDS-001`, corrected by `M01-FND-COORD-TILE-AXIS-002`
|
||||
- Decision owners: M01 foundation/integration owners
|
||||
|
||||
## Context
|
||||
|
||||
OpenWC currently performs coordinate conversion in native ADT/WDT loaders,
|
||||
`StreamingWorldLoader`, sky, player and renderer diagnostic scripts. The same
|
||||
numbers are frequently carried in `Vector3` or dictionaries, so the coordinate
|
||||
space is implicit. Existing names also mix ADT filename indices, renderer X/Z
|
||||
indices and MDDF/MODF placement fields.
|
||||
|
||||
M00 recorded five positions observed in WoW 3.3.5a build 12340. They verify the
|
||||
current client-position to Godot-position arithmetic within `0.002` yard, but
|
||||
they do not define a cross-layer contract and do not prove camera composition
|
||||
or visual parity.
|
||||
|
||||
Independent references agree on a 64 by 64 ADT grid and an approximately
|
||||
`533.3333` yard tile size. They do not all use the word “canonical” for the same
|
||||
axis order. OpenWC therefore needs names tied to observable formats instead of
|
||||
adopting an internal convention from one reference project.
|
||||
|
||||
## Decision
|
||||
|
||||
### Canonical position
|
||||
|
||||
`CanonicalWowWorldPosition(X, Y, Z)` preserves the numeric X/Y/Z order used by
|
||||
the WoW client, movement protocol and TrinityCore/AzerothCore world positions.
|
||||
X and Y are horizontal; Z is height. Units are WoW yards.
|
||||
|
||||
`ServerWorldPosition` is a distinct type but converts component-for-component
|
||||
to canonical. The type boundary prevents a server record from being passed
|
||||
directly to renderer code and leaves room for adapter validation without a
|
||||
silent axis swap.
|
||||
|
||||
### World extent and ADT indices
|
||||
|
||||
The contract uses:
|
||||
|
||||
```text
|
||||
ADT_TILE_SIZE_YARDS = 1600 / 3
|
||||
ADT_TILES_PER_MAP_AXIS = 64
|
||||
WOW_WORLD_HALF_EXTENT_YARDS = 32 * ADT_TILE_SIZE_YARDS
|
||||
|
||||
tileX = floor(32 - canonical.Y / ADT_TILE_SIZE_YARDS)
|
||||
tileY = floor(32 - canonical.X / ADT_TILE_SIZE_YARDS)
|
||||
```
|
||||
|
||||
`AdtTileCoordinate(tile_x, tile_y)` always means the indices in
|
||||
`Map_tileX_tileY.adt`. Results are intentionally not clamped: the exact
|
||||
south-east extent produces index 64 and remains diagnosable as out of bounds.
|
||||
Normal valid indices are 0 through 63.
|
||||
|
||||
An `AdtTileLocalPosition` is measured east and south from the tile's
|
||||
north-west corner. Filename `tile_x` and MCNK `chunk_x` grow east; filename
|
||||
`tile_y` and MCNK `chunk_y` grow south, with valid indices 0 through 15. These
|
||||
direction names avoid pretending that ADT file axes are Godot X/Z axes.
|
||||
|
||||
### ADT placement fields
|
||||
|
||||
MDDF/MODF position fields are represented in on-disk X/height/Z order:
|
||||
|
||||
```text
|
||||
adt.X = halfExtent - canonical.X
|
||||
adt.height = canonical.Z
|
||||
adt.Z = halfExtent - canonical.Y
|
||||
```
|
||||
|
||||
Model-local MDDF/MODF Euler corrections are not world orientation. They remain
|
||||
outside this contract until a placement-orientation package has format- and
|
||||
model-specific fixtures.
|
||||
|
||||
### Godot renderer basis
|
||||
|
||||
The compatibility renderer keeps one Godot unit equal to one WoW yard and uses
|
||||
Y-up coordinates from the north-west map extent:
|
||||
|
||||
```text
|
||||
godot.X = halfExtent - canonical.Y # east
|
||||
godot.Y = canonical.Z # up
|
||||
godot.Z = halfExtent - canonical.X # south
|
||||
```
|
||||
|
||||
`GodotWorldPosition` contains scalars rather than `Vector3`. A scene/render
|
||||
adapter may construct `Vector3` only at its boundary.
|
||||
|
||||
### World yaw
|
||||
|
||||
Server/canonical yaw zero faces increasing WoW X; positive yaw turns toward
|
||||
increasing WoW Y. In the mapped Godot basis, Godot forward `-Z` and positive
|
||||
Y-axis yaw produce the same numeric world angle. Mapper conversions therefore
|
||||
normalize the value to `[-PI, PI)` without adding an offset.
|
||||
|
||||
This identity applies to world-facing yaw only. It does not apply to M2/WMO
|
||||
asset axes or placement Euler triples.
|
||||
|
||||
### Contract version 2 correction evidence
|
||||
|
||||
Contract version 1 incorrectly calculated filename `tile_x` from canonical X
|
||||
and `tile_y` from canonical Y. The private WotLK corpus contains the observed
|
||||
Elwynn point in `Azeroth_31_49.adt`, SHA-256
|
||||
`18185B5FAC9566D9C57A9C19ABD75F108370D1795639B6077167C0EFD3672FAE`; the
|
||||
transposed `Azeroth_49_31.adt` does not exist. Raw MCNK headers in that file
|
||||
show `indexX` decreasing world Y (east) and `indexY` decreasing world X (south).
|
||||
OpenRealm independently calculates filename X from world Y and filename Y from
|
||||
world X. Contract version 2 corrects the formulas before any consumer migration.
|
||||
|
||||
### Enforced conversion boundaries
|
||||
|
||||
GDScript production consumers use `CoordinateMapper` for canonical/Godot and
|
||||
Godot/ADT tile-local conversion. `StreamingWorldLoader`, player spawn/terrain
|
||||
sampling, sky/area lookup and terrain diagnostics may construct `Vector3` only
|
||||
at their scene/tool boundary.
|
||||
|
||||
The native ADT/WDT parser path retains one equivalent formula definition in
|
||||
`src/native/src/wow_chunk_reader.h`; worker parsers cannot call GDScript. The
|
||||
M00 renderer calibration keeps one independent formula as a test oracle so a
|
||||
shared mapper defect cannot make both implementation and evidence agree.
|
||||
Repository verification rejects additional world-center, world/tile or named
|
||||
WoW/Godot formulas outside those explicit exceptions. Model-local M2/WMO basis
|
||||
transforms are separate format contracts and are not classified as world-space mapping.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Make renderer coordinates canonical
|
||||
|
||||
Rejected because protocol, database and ADT adapters would silently inherit a
|
||||
presentation-specific offset and Y-up basis.
|
||||
|
||||
### Adopt WoWee's internal canonical axis names unchanged
|
||||
|
||||
Rejected because that reference swaps server X/Y into its chosen north/west
|
||||
ordering. OpenWC preserves observable client/server field names and makes the
|
||||
render/ADT transformations explicit instead.
|
||||
|
||||
### Use `Vector3` plus comments
|
||||
|
||||
Rejected because comments cannot prevent server, ADT and Godot values from
|
||||
being passed across the wrong boundary.
|
||||
|
||||
### Generic vector-space or matrix framework
|
||||
|
||||
Rejected as unnecessary complexity for fixed affine transforms and integer
|
||||
grid ownership rules.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Network and server-data adapters can expose typed positions without knowing
|
||||
Godot.
|
||||
- Renderer consumers must explicitly map and then create Godot vectors at their
|
||||
own boundary.
|
||||
- ADT filename indices can no longer be confused with renderer X/Z tile names.
|
||||
- GDScript world/tile consumers use mapper APIs; renderer distance and LOD math
|
||||
remains ordinary Godot-space geometry.
|
||||
- Native ADT/WDT loaders share one boundary formula and the M00 calibration
|
||||
keeps one independent non-production oracle.
|
||||
- New manual axis conversions are review/test violations enforced by the
|
||||
repository-wide source gate.
|
||||
- Persisted schemas are unchanged. Future serialized positions must include an
|
||||
explicit coordinate-space/version discriminator.
|
||||
|
||||
## Verification
|
||||
|
||||
- Five M00 build-12340 client camera positions map to the recorded Godot
|
||||
checkpoints within `0.002` yard.
|
||||
- Server, ADT placement, Godot and tile-local conversions round-trip.
|
||||
- Exact center, tile, chunk and world-extent boundaries have explicit tests.
|
||||
- The observed Elwynn position resolves to the real `Azeroth_31_49.adt` file;
|
||||
MCNK `(0,0)` to `(15,15)` origins confirm X-east and Y-south chunk growth.
|
||||
- World-yaw normalization and round-trip cases cover cardinal angles and wrap.
|
||||
- Direct Godot/ADT tile-local conversions round-trip and production consumers
|
||||
are checked by `verify_coordinate_conversion_boundaries.gd`.
|
||||
|
||||
## References
|
||||
|
||||
- [TrinityCore GridDefines](https://github.com/TrinityCore/TrinityCore/blob/master/src/server/game/Grids/GridDefines.h)
|
||||
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_adt.c`
|
||||
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_objects.c`
|
||||
- `reference/WoWee/include/core/coordinates.hpp`
|
||||
- [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||
@@ -0,0 +1,122 @@
|
||||
# ADR 0002: Separate domain identity namespaces
|
||||
|
||||
- Status: Accepted for M01 contract implementation
|
||||
- Date: 2026-07-13
|
||||
- Work package: `M01-FND-DOMAIN-IDENTITIES-001`
|
||||
- Decision owners: M01 foundation/integration owners
|
||||
|
||||
## Context
|
||||
|
||||
OpenWC must relate authored content, runtime objects, WoW protocol objects and
|
||||
TrinityCore/AzerothCore rows. All four domains can expose an integer or string,
|
||||
but their identity lifetimes and collision rules are different. A raw `int`, a
|
||||
generic `id` field or one universal ID class would allow an entity sequence to
|
||||
be persisted as content identity, a database entry to be sent as a wire GUID,
|
||||
or equal entry numbers from different tables to collide.
|
||||
|
||||
The Content Project requires stable references before server entries are
|
||||
allocated. Runtime entities require cheap session-local identity even when no
|
||||
server object exists. WoW 3.3.5a GUIDs carry opaque 64 wire bits whose semantic
|
||||
decomposition must be established by protocol fixtures. Server templates use
|
||||
numeric entries scoped by an adapter/table namespace.
|
||||
|
||||
## Decision
|
||||
|
||||
### ContentId
|
||||
|
||||
`ContentId` stores canonical lowercase UUID text in the `8-4-4-4-12` shape.
|
||||
It is the only identity in this decision intended for stable authored-content
|
||||
references and serialization. Construction normalizes whitespace and letter
|
||||
case; `is_valid()` reports malformed input without crashing an import.
|
||||
|
||||
The value validates text shape, not UUID version or variant bits. A future
|
||||
Content Project allocator chooses the UUID generation policy and owns duplicate
|
||||
detection. Every persisted schema that contains a `ContentId` still declares
|
||||
its own schema version and field migration.
|
||||
|
||||
### EntityId
|
||||
|
||||
`EntityId` stores a positive integer allocated by one runtime session owner.
|
||||
It is valid only inside that session and must not be persisted as content,
|
||||
protocol or server identity. Sequence reuse policy belongs to the future entity
|
||||
registry, not the value object.
|
||||
|
||||
### WowGuid
|
||||
|
||||
`WowGuid` stores opaque `high_32_bits` and `low_32_bits` integers. Each word is
|
||||
validated against `0..0xffffffff`, preserving every unsigned wire bit without
|
||||
combining it into GDScript's signed 64-bit integer. The all-zero value remains
|
||||
an observable protocol sentinel and receives no gameplay interpretation here.
|
||||
|
||||
High-type masks, packed-GUID codecs and object-kind extraction are explicitly
|
||||
deferred to the build-12340 protocol package and require packet fixtures.
|
||||
|
||||
### ServerEntryId
|
||||
|
||||
`ServerEntryId` stores a positive numeric entry together with a normalized,
|
||||
non-empty adapter/table namespace. Equal numbers in different namespaces are
|
||||
different identities. Mapping a server entry to a `ContentId` is explicit
|
||||
adapter/snapshot data; neither value derives the other.
|
||||
|
||||
### Equality and keys
|
||||
|
||||
The types expose domain-named `equals` methods because `RefCounted` `==`
|
||||
compares object references. Stable keys are available only where their lifetime
|
||||
allows them: `ContentId.to_key()`, `WowGuid.to_hex_string()` and
|
||||
`ServerEntryId.to_key()`. `EntityId` exposes only `to_debug_key()` to discourage
|
||||
persistence.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### One generic `Id<T>` abstraction
|
||||
|
||||
Rejected because GDScript has no compile-time generic type distinction and the
|
||||
shared API would obscure different validation, lifetime and persistence rules.
|
||||
|
||||
### Use strings everywhere
|
||||
|
||||
Rejected because prefixes are a convention rather than a type boundary and
|
||||
would still permit accidental cross-layer assignment.
|
||||
|
||||
### Store WowGuid in one integer
|
||||
|
||||
Rejected because unsigned 64-bit GUIDs can exceed GDScript's positive signed
|
||||
integer range. Split words preserve the wire representation losslessly.
|
||||
|
||||
### Use server entries as ContentId
|
||||
|
||||
Rejected because entries are allocated by a particular backend/table, can
|
||||
collide between namespaces and do not exist while authoring offline content.
|
||||
|
||||
### Generate UUIDs inside ContentId
|
||||
|
||||
Rejected because randomness/allocation is a side effect requiring duplicate,
|
||||
fixture and deterministic-build policy. This value object only normalizes and
|
||||
validates an allocator-provided value.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Cross-layer contracts must name the identity type they accept.
|
||||
- Content references remain stable before and after server deployment.
|
||||
- Runtime entity registries need explicit mappings to wire GUIDs/content IDs.
|
||||
- Server adapters must supply a namespace and maintain entry-to-content maps.
|
||||
- Malformed external values remain representable long enough for adapters to
|
||||
produce diagnostics, but consumers must call the relevant validation method.
|
||||
- Additional typed IDs such as map, spell, item and quest remain separate
|
||||
follow-up contracts rather than aliases of these four values.
|
||||
|
||||
## Verification
|
||||
|
||||
- UUID normalization, shape validation and value equality are fixture-tested.
|
||||
- Zero/positive session sequences establish `EntityId` validity boundaries.
|
||||
- Maximum unsigned GUID words round-trip to exactly sixteen hexadecimal digits;
|
||||
overflow is rejected and zero remains detectable.
|
||||
- Equal server entry numbers in creature/gameobject namespaces do not compare equal.
|
||||
- Dependency search verifies that identity values do not inherit Node/Resource
|
||||
or expose `Vector3`.
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 9562: Universally Unique IDentifiers](https://www.rfc-editor.org/rfc/rfc9562.html)
|
||||
- [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||
- [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)
|
||||
@@ -7,6 +7,8 @@
|
||||
| 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) |
|
||||
| Domain identities | Implemented contract | [`domain-identities.md`](domain-identities.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) |
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
# 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`, `M01-FND-COORD-BOUNDARY-GATE-001`, `M01-QAR-SERVER-SPAWN-RENDERER-001` |
|
||||
| Owners | Foundation/domain |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m01-server-spawn-renderer`, 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.godot_to_adt_tile` | Pure static method | Resolve filename indices from typed renderer position | Synchronous; new result value | Result remains unclamped |
|
||||
| `CoordinateMapper.godot_to_adt_tile_local` | Pure static method | Resolve east/height/south offsets from typed renderer position | Synchronous; new result value | Caller validates ownership/range |
|
||||
| `CoordinateMapper.adt_tile_local_to_godot` | Pure static method | Compose typed renderer position from tile/local values | Synchronous; new result value | Non-finite scalars propagate |
|
||||
| `CoordinateMapper.canonical_to_adt_placement` / inverse | Pure static methods | Convert ADT placement field basis | Synchronous; new result value | Non-finite scalars propagate |
|
||||
| `CoordinateMapper.canonical_to_adt_tile` | Pure static method | Compute unclamped filename indices | Synchronous; new result value | Out-of-map result remains invalid |
|
||||
| `CoordinateMapper.canonical_to_adt_tile_local` / inverse | Pure static methods | Split/reconstruct tile-local position | Synchronous; new result value | Caller validates tile ownership |
|
||||
| `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
|
||||
ServerFixture --> SpawnManifest[Server spawn render manifest]
|
||||
SpawnManifest --> SpawnVerifier[Renderer checkpoint verifier]
|
||||
SpawnVerifier --> Capture[Live renderer capture]
|
||||
Capture --> Marker[Diagnostic marker at mapped spawn]
|
||||
Server[ServerWorldPosition X,Y,Z] -->|identity components| Canonical[CanonicalWowWorldPosition]
|
||||
ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical
|
||||
Canonical -->|tileX from Y; tileY from X| Tile[AdtTileCoordinate]
|
||||
Canonical -->|north-west edge offsets| Local[AdtTileLocalPosition]
|
||||
Local -->|floor offset / chunk size| Chunk[AdtChunkCoordinate]
|
||||
Canonical -->|half extent - Y; Z; half extent - X| Godot[GodotWorldPosition]
|
||||
Godot -->|typed direct adapter| Tile
|
||||
Godot -->|typed direct adapter| Local
|
||||
Tile -->|tile plus local| Godot
|
||||
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
|
||||
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
|
||||
```
|
||||
|
||||
## 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`.
|
||||
- Boundary enforcement: `src/tools/verify_coordinate_conversion_boundaries.gd`.
|
||||
- Server-spawn renderer contract: `src/tools/verify_server_spawn_renderer.gd`
|
||||
validates the dedicated manifest against the same pinned fixture before GUI capture.
|
||||
- Integration/E2E: sky, player, streamer and terrain probe use mapper boundaries;
|
||||
`StreamingFocus` provides typed renderer focus, and the pinned AzerothCore
|
||||
Human Warrior spawn is captured in the Eastern Kingdoms renderer at ADT
|
||||
`(32,48)`, chunk `(3,12)`.
|
||||
- Fidelity evidence: five build-12340 camera positions from the M00 manifest map
|
||||
within `0.002` yard; a pinned AzerothCore human-warrior spawn and one raw MCNK
|
||||
header observation independently confirm tile `(32,48)` and chunk `(3,12)`;
|
||||
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, five M00 golden points and migrated GDScript consumers | Network/server adapters remain future consumers |
|
||||
| ADT placement mapping | Implemented | Synthetic/direct round-trip fixtures | Compare native parsed placements during migration |
|
||||
| ADT tile/local/chunk ownership | Implemented | Center/extent boundaries, raw `Azeroth_31_49.adt` evidence and direct Godot adapter round-trip | Native parser remains explicit boundary |
|
||||
| World-facing yaw mapping | Implemented | Cardinal/wrap round-trip fixtures | Placement Euler remains separate |
|
||||
| Scene-free production contract | Implemented | Source boundary, headless tests and repository-wide conversion gate | Maintain gate with new consumers |
|
||||
| Cross-source coordinate fixture | Implemented | Pinned AzerothCore spawn, raw ADT MCNK metadata and five build-12340 points pass one headless verifier | Add live adapter/renderer integration |
|
||||
| TrinityCore populated spawn equivalence | Not verified | The base development schema has no pinned populated row used by this fixture | Pin and verify a TDB snapshot before claiming parity |
|
||||
| Visual/server-spawn integration | Implemented | Dedicated manifest/verifier plus cold/warm GUI capture of the mapped AzerothCore spawn | This is coordinate/renderer integration evidence, not original-client visual parity |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Native ADT/WDT parsing retains one shared boundary formula because worker
|
||||
parsers cannot call GDScript; the gate prevents additional definitions.
|
||||
- Contract version 1 briefly transposed ADT filename/chunk axes; version 2 fixes
|
||||
the defect before any consumer migration.
|
||||
- MDDF/MODF rotation comments and implementations disagree across existing paths;
|
||||
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 |
|
||||
| `src/tools/verify_coordinate_conversion_boundaries.gd` | Repository-wide manual conversion and required-consumer gate |
|
||||
| `src/tools/server_spawn_render_manifest.json` | Pinned server-spawn camera, player, marker and expected ADT ownership |
|
||||
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer manifest contract verifier |
|
||||
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- 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)
|
||||
@@ -0,0 +1,232 @@
|
||||
# Domain Identities
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target/work package | `M01-FND-DOMAIN-IDENTITIES-001` |
|
||||
| Owners | Foundation/domain |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m01-domain-identities`, 2026-07-13 |
|
||||
| Profiles/capabilities | All profiles; identity contract version 1 |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide non-interchangeable, scene-free identity values for authored content,
|
||||
runtime entities, WoW wire objects and namespaced server entries. Each value
|
||||
states its lifetime and serialization rules instead of exposing a generic `id`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Allocating, reserving or globally registering IDs.
|
||||
- Parsing packet bytes or server database rows.
|
||||
- Mapping GUID high types or server entries to gameplay object kinds.
|
||||
- Defining Content Project, snapshot or replay schemas.
|
||||
- Replacing future typed map/spell/item/quest/display/sound IDs.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Authoring[Content Project allocator] --> ContentId[ContentId]
|
||||
Session[Entity registry] --> EntityId[EntityId]
|
||||
Wire[Build 12340 packet adapter] --> WowGuid[WowGuid]
|
||||
Server[Core snapshot adapter] --> ServerEntry[ServerEntryId]
|
||||
ContentId --> Mapping[Explicit identity mapping records]
|
||||
EntityId --> Mapping
|
||||
WowGuid --> Mapping
|
||||
ServerEntry --> Mapping
|
||||
Mapping --> Consumers[Gameplay / network / authoring consumers]
|
||||
Raw[Raw int or string] -. forbidden across boundary .-> Consumers
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
|
||||
- GDScript scalar/string operations and `RefCounted` lifetime;
|
||||
- callers in content, gameplay, network and server adapter layers.
|
||||
|
||||
Forbidden dependencies:
|
||||
|
||||
- Node, Resource, SceneTree, renderer and UI state;
|
||||
- packet buffers, SQL connections or database table implementations;
|
||||
- implicit conversion between identity namespaces;
|
||||
- random/global allocation inside the value objects.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `ContentId` | Immutable value | Stable authored identity as canonical lowercase UUID text | Any thread; persisted content lifetime | `is_valid()` reports malformed shape |
|
||||
| `ContentId.equals` | Pure method | Value equality across references | Call duration | Null compares false |
|
||||
| `ContentId.to_key` | Pure method | Canonical serialization/dictionary key | Stable across schema lifetime | Caller must validate first |
|
||||
| `EntityId` | Immutable value | Positive session-local runtime sequence | One runtime session | `is_valid()` rejects zero/negative |
|
||||
| `EntityId.equals` | Pure method | Equality within an explicitly shared session | Call duration | Cross-session comparison is a caller error |
|
||||
| `EntityId.to_debug_key` | Pure method | Non-persistent diagnostic label | Session/log lifetime | Must not be serialized as content |
|
||||
| `WowGuid` | Immutable value | Opaque unsigned high/low wire words | Packet/replay/entity mapping lifetime | `has_valid_word_ranges()` reports overflow |
|
||||
| `WowGuid.is_empty` | Pure method | Detect all-zero wire sentinel | Call duration | Does not assign semantic meaning |
|
||||
| `WowGuid.to_hex_string` | Pure method | Sixteen-digit diagnostic/replay key | Wire/replay lifetime | Caller validates ranges first |
|
||||
| `ServerEntryId` | Immutable value | Adapter/table namespace plus positive entry | Snapshot/deployment mapping lifetime | `is_valid()` rejects missing namespace/non-positive entry |
|
||||
| `ServerEntryId.to_key` | Pure method | Namespaced mapping key | Snapshot/schema lifetime | Caller validates first |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | UUID text | Future Content Project allocator/parser | `ContentId` | Copied normalized string | Build/content lifetime |
|
||||
| Input | Session sequence | Future entity registry | `EntityId` | Copied integer | One runtime session |
|
||||
| Input | Two unsigned 32-bit words | Future build-12340 packet codec | `WowGuid` | Copied integers | Packet/replay/entity lifetime |
|
||||
| Input | Namespace and numeric entry | Future core snapshot adapter | `ServerEntryId` | Copied normalized string/integer | Snapshot/deployment lifetime |
|
||||
| Output | Validation/equality/key results | Identity values | Adapters/registries/schemas | Returned scalar/string | Call duration |
|
||||
|
||||
Side effects:
|
||||
|
||||
- none;
|
||||
- no allocation registry, random generation, file/cache write, packet read,
|
||||
database query, scene mutation or logging.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
ContentSource[Authored document] -->|UUID text| ContentId
|
||||
ServerRow[Backend table row] -->|adapter + table + entry| ServerEntryId
|
||||
Packet[World packet] -->|high/low wire words| WowGuid
|
||||
Registry[Session entity allocator] -->|positive sequence| EntityId
|
||||
ServerEntryId --> DeployMap[Explicit deployment mapping]
|
||||
ContentId --> DeployMap
|
||||
WowGuid --> RuntimeMap[Explicit runtime mapping]
|
||||
EntityId --> RuntimeMap
|
||||
DeployMap --> Snapshot[Versioned snapshot/change set]
|
||||
RuntimeMap --> WorldState[Session world state]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state ContentIdentity {
|
||||
[*] --> Parsed
|
||||
Parsed --> Persisted: valid ContentId
|
||||
Persisted --> Referenced
|
||||
}
|
||||
state RuntimeIdentity {
|
||||
[*] --> Allocated
|
||||
Allocated --> Active
|
||||
Active --> Released: session owner removes entity
|
||||
Released --> [*]
|
||||
}
|
||||
state ExternalIdentity {
|
||||
[*] --> Observed
|
||||
Observed --> Mapped: WowGuid or ServerEntryId
|
||||
Mapped --> Stale: disconnect or snapshot replacement
|
||||
Stale --> [*]
|
||||
}
|
||||
```
|
||||
|
||||
The values themselves are stateless and immutable. Allocation, release,
|
||||
mapping and stale-state ownership belong to future registries/adapters.
|
||||
|
||||
## Main sequence
|
||||
|
||||
Identity construction and validation are synchronous. An external adapter reads
|
||||
its format, constructs the matching value, validates it, then publishes an
|
||||
explicit mapping or typed domain event. No value object performs I/O or retries.
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Values own only copied strings/integers and are caller-owned `RefCounted` references.
|
||||
- Public methods do not mutate values or shared state and are worker-safe.
|
||||
- Content/session/wire/server mapping records are owned by their future layer,
|
||||
not by these values.
|
||||
- No Node, RID, file, socket, database, lock or cache resource is acquired.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Malformed ContentId | `is_valid()` | Invalid value remains inspectable | Source field/path supplied by parser | Regenerate or repair authored ID |
|
||||
| Invalid EntityId sequence | `is_valid()` | Registry must reject publication | Session/correlation context | Fix allocator/session mapping |
|
||||
| GUID word overflow | `has_valid_word_ranges()` | Codec must reject packet/replay value | Packet offset/opcode context | Reject malformed input; do not truncate |
|
||||
| Empty GUID | `is_empty()` | Preserved as protocol sentinel | Adapter decides whether field permits empty | Apply opcode/field-specific rule |
|
||||
| Missing server namespace/entry | `is_valid()` | Snapshot adapter rejects mapping | Backend/table/row context | Correct adapter schema mapping |
|
||||
| Unknown/stale mapping | Registry/snapshot lookup | No implicit numeric fallback | Typed IDs in diagnostic | Refresh snapshot/session or reject event |
|
||||
|
||||
There is no cancellation inside constant-time value operations.
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Identity contract version | 1 | All | No | Four separate namespaces and validation rules |
|
||||
| ContentId text form | Lowercase `8-4-4-4-12` UUID | Authoring/runtime/tools | No | Stable persisted key |
|
||||
| GUID decomposition | Opaque two-word storage | `wotlk_335a_build_12340` | No | Prevents unverified high-type assumptions |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
- `ContentId.uuid_text` is the only stable persisted identity introduced here.
|
||||
- Containing schemas must store their own schema version and duplicate/reference policy.
|
||||
- `EntityId` must never be persisted across sessions.
|
||||
- `WowGuid` may appear in versioned packet replays as explicit high/low words or
|
||||
the exact sixteen-digit hex form; a replay schema owns that choice.
|
||||
- `ServerEntryId` may appear in snapshots only with both namespace and entry.
|
||||
- This package is additive and migrates no existing data or cache.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Stable diagnostics use `ContentId.to_key`, `WowGuid.to_hex_string` and
|
||||
`ServerEntryId.to_key`.
|
||||
- Runtime diagnostics may use `EntityId.to_debug_key` with a session correlation ID.
|
||||
- Production metrics/logging belong to registries and adapters; values emit none.
|
||||
|
||||
## Verification
|
||||
|
||||
- Contract test: `src/tools/verify_domain_identities.gd`.
|
||||
- Fixtures cover UUID normalization/malformed shape, positive/zero session IDs,
|
||||
maximum/overflow GUID words, zero GUID and cross-table server entry collisions.
|
||||
- Dependency gate confirms no Node/Resource/Vector3 exposure.
|
||||
- Fidelity boundary: GUID bit meaning remains unclaimed until build-12340 packet fixtures.
|
||||
- Performance: constant-time integer checks and a bounded 36-character UUID scan.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Content Project allocator and duplicate/reference validation.
|
||||
- Session-owned entity registry and explicit `WowGuid ↔ EntityId` mappings.
|
||||
- Backend capability adapters that define server entry namespaces.
|
||||
- Separate typed map/spell/item/quest/display/sound values.
|
||||
- Fixture-backed build-12340 GUID high-type decoding in the protocol layer.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Stable authored ContentId | Implemented contract | UUID normalization/validation fixtures | Allocator and schema arrive with Content Project |
|
||||
| Session-local EntityId | Implemented contract | Positive/zero/equality fixtures | Registry lifecycle arrives with gameplay world state |
|
||||
| Lossless opaque WowGuid | Implemented contract | Full unsigned words and hex fixture | Packet codec/high-type semantics need build-12340 fixtures |
|
||||
| Namespaced ServerEntryId | Implemented contract | Cross-table collision fixture | Backend namespace capability mapping arrives with server adapters |
|
||||
| Cross-namespace mappings | Planned | Architecture/ADR boundary | Implement only in owning registries/adapters |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- No allocator or duplicate registry exists yet.
|
||||
- GDScript `==` compares references; consumers must use domain `equals` methods
|
||||
or canonical keys until a registry owns keying.
|
||||
- UUID shape validation deliberately does not enforce generation version/variant.
|
||||
- Server namespace vocabulary is adapter-owned and not yet centrally catalogued.
|
||||
- GUID high-type/object-kind semantics remain unverified rather than guessed.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/domain/identity/content_id.gd` | Stable canonical UUID content identity |
|
||||
| `src/domain/identity/entity_id.gd` | Session-local runtime identity |
|
||||
| `src/domain/identity/wow_guid.gd` | Opaque unsigned WoW wire identity |
|
||||
| `src/domain/identity/server_entry_id.gd` | Namespaced backend entry identity |
|
||||
| `src/tools/verify_domain_identities.gd` | Headless value/boundary regression |
|
||||
| `docs/adr/0002-domain-identity-namespaces.md` | Normative namespace/lifetime decision |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- ADR: [`../adr/0002-domain-identity-namespaces.md`](../adr/0002-domain-identity-namespaces.md)
|
||||
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||
- Foundation plan: [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md)
|
||||
@@ -5,9 +5,9 @@
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 active; декомпозиция M01–M03 |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; декомпозиция M02–M03 |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | M00 visual-diff worktree, 2026-07-11 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m01-server-spawn-renderer`, 2026-07-13 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -26,7 +26,13 @@
|
||||
|
||||
```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
|
||||
SpawnFixture[Pinned server spawn fixture] --> SpawnManifest[Spawn render manifest]
|
||||
SpawnManifest --> Capture
|
||||
Adapter --> Focus
|
||||
Focus --> Loader[StreamingWorldLoader]
|
||||
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
|
||||
Cache[Baked terrain/M2/WMO caches] --> Loader
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
@@ -60,7 +66,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,12 +81,16 @@ 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 |
|
||||
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
|
||||
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
|
||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||
| Output | Metrics/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
|
||||
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
|
||||
|
||||
Side effects:
|
||||
|
||||
@@ -90,7 +104,15 @@ 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
|
||||
SF[Pinned server spawn] --> SM[Validated spawn manifest]
|
||||
SM --> C
|
||||
SM --> Marker[Renderer-native origin marker]
|
||||
E[Editor viewport] --> EA[Editor adapter]
|
||||
A --> F[StreamingFocus]
|
||||
EA --> F
|
||||
F --> T[Compute desired ADT tiles]
|
||||
WDT[WDT tile catalog] --> T
|
||||
T --> Q[Tile load queue]
|
||||
Q --> P[Worker parse/cache load]
|
||||
@@ -104,6 +126,7 @@ flowchart TD
|
||||
M2 --> World
|
||||
WMO --> World
|
||||
Liquid --> World
|
||||
Marker --> World
|
||||
F --> E[Eviction/retention decisions]
|
||||
E --> World
|
||||
```
|
||||
@@ -113,13 +136,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 +153,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 +176,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 +191,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 +214,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,8 +231,9 @@ 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.
|
||||
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
|
||||
- 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, seven M00
|
||||
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
|
||||
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
||||
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
|
||||
- Manual diagnostics: [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md) and [`../../RENDER.md`](../../RENDER.md).
|
||||
@@ -207,7 +241,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 +255,15 @@ 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 |
|
||||
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
|
||||
| Stable renderer facade | Planned | M03 | Current caller uses monolithic Node API |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- 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 +284,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 |
|
||||
@@ -258,6 +297,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
|
||||
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Headless ownership regression for detached runtime prototypes, resource caches and empty liquid roots |
|
||||
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
|
||||
| `src/tools/server_spawn_render_manifest.json` | Dedicated mapped AzerothCore spawn checkpoint and diagnostic marker configuration |
|
||||
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer checkpoint contract verification |
|
||||
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
|
||||
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
|
||||
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,167 @@
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
## Returns ADT filename indices directly from a Godot renderer position.
|
||||
static func godot_to_adt_tile(godot_position: GodotWorldPosition) -> AdtTileCoordinate:
|
||||
return AdtTileCoordinate.new(
|
||||
int(floor(godot_position.x_units / ADT_TILE_SIZE_YARDS)),
|
||||
int(floor(godot_position.z_units / ADT_TILE_SIZE_YARDS))
|
||||
)
|
||||
|
||||
|
||||
## Returns east/height/south offsets inside the owning ADT tile.
|
||||
static func godot_to_adt_tile_local(godot_position: GodotWorldPosition) -> AdtTileLocalPosition:
|
||||
var tile_coordinate := godot_to_adt_tile(godot_position)
|
||||
return AdtTileLocalPosition.new(
|
||||
godot_position.x_units - float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS,
|
||||
godot_position.y_units,
|
||||
godot_position.z_units - float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS
|
||||
)
|
||||
|
||||
|
||||
## Converts an ADT tile/local pair directly to the Godot renderer basis.
|
||||
static func adt_tile_local_to_godot(
|
||||
tile_coordinate: AdtTileCoordinate,
|
||||
local_position: AdtTileLocalPosition
|
||||
) -> GodotWorldPosition:
|
||||
return GodotWorldPosition.new(
|
||||
float(tile_coordinate.tile_x) * ADT_TILE_SIZE_YARDS + local_position.east_yards,
|
||||
local_position.height_yards,
|
||||
float(tile_coordinate.tile_y) * ADT_TILE_SIZE_YARDS + local_position.south_yards
|
||||
)
|
||||
|
||||
|
||||
## Converts canonical WoW coordinates to MDDF/MODF X/height/Z file fields.
|
||||
static func canonical_to_adt_placement(canonical_position: CanonicalWowWorldPosition) -> AdtPlacementPosition:
|
||||
return AdtPlacementPosition.new(
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,43 @@
|
||||
class_name ContentId
|
||||
extends RefCounted
|
||||
|
||||
## Stable authored-content identity serialized as canonical lowercase UUID text.
|
||||
## It is independent of runtime entities, server database entries and wire GUIDs.
|
||||
|
||||
var uuid_text: String:
|
||||
get:
|
||||
return _uuid_text
|
||||
|
||||
var _uuid_text: String
|
||||
|
||||
|
||||
## Normalizes surrounding whitespace and hexadecimal letter case.
|
||||
func _init(uuid_text_value: String) -> void:
|
||||
_uuid_text = uuid_text_value.strip_edges().to_lower()
|
||||
|
||||
|
||||
## Reports whether the value has the canonical 8-4-4-4-12 UUID text shape.
|
||||
func is_valid() -> bool:
|
||||
if _uuid_text.length() != 36:
|
||||
return false
|
||||
for character_index in range(_uuid_text.length()):
|
||||
if character_index in [8, 13, 18, 23]:
|
||||
if _uuid_text[character_index] != "-":
|
||||
return false
|
||||
elif not _is_lowercase_hexadecimal_character(_uuid_text[character_index]):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
## Compares stable content identity by normalized UUID text.
|
||||
func equals(other_content_id: ContentId) -> bool:
|
||||
return other_content_id != null and _uuid_text == other_content_id.uuid_text
|
||||
|
||||
|
||||
## Returns the canonical dictionary/serialization key.
|
||||
func to_key() -> String:
|
||||
return _uuid_text
|
||||
|
||||
|
||||
func _is_lowercase_hexadecimal_character(character: String) -> bool:
|
||||
return (character >= "0" and character <= "9") or (character >= "a" and character <= "f")
|
||||
@@ -0,0 +1 @@
|
||||
uid://dk6splyui2an1
|
||||
@@ -0,0 +1,31 @@
|
||||
class_name EntityId
|
||||
extends RefCounted
|
||||
|
||||
## Session-local runtime entity identity.
|
||||
## This sequence is never a stable content ID, server entry or WoW wire GUID.
|
||||
|
||||
var session_sequence: int:
|
||||
get:
|
||||
return _session_sequence
|
||||
|
||||
var _session_sequence: int
|
||||
|
||||
|
||||
## Stores an allocator-issued session sequence without persistence semantics.
|
||||
func _init(session_sequence_value: int) -> void:
|
||||
_session_sequence = session_sequence_value
|
||||
|
||||
|
||||
## Reports whether this is a positive allocator-issued sequence.
|
||||
func is_valid() -> bool:
|
||||
return _session_sequence > 0
|
||||
|
||||
|
||||
## Compares identity inside the same explicitly owned runtime session.
|
||||
func equals(other_entity_id: EntityId) -> bool:
|
||||
return other_entity_id != null and _session_sequence == other_entity_id.session_sequence
|
||||
|
||||
|
||||
## Returns a diagnostic key; it must not be persisted across sessions.
|
||||
func to_debug_key() -> String:
|
||||
return "entity:%d" % _session_sequence
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqdd8el4me5ng
|
||||
@@ -0,0 +1,41 @@
|
||||
class_name ServerEntryId
|
||||
extends RefCounted
|
||||
|
||||
## Adapter-namespaced numeric entry from a server database or canonical snapshot.
|
||||
## Equal numbers in different template tables are intentionally different IDs.
|
||||
|
||||
var entry_namespace: String:
|
||||
get:
|
||||
return _entry_namespace
|
||||
|
||||
var entry_value: int:
|
||||
get:
|
||||
return _entry_value
|
||||
|
||||
var _entry_namespace: String
|
||||
var _entry_value: int
|
||||
|
||||
|
||||
## Normalizes a table/adapter namespace and preserves its positive entry value.
|
||||
func _init(entry_namespace_value: String, entry_value_value: int) -> void:
|
||||
_entry_namespace = entry_namespace_value.strip_edges().to_lower()
|
||||
_entry_value = entry_value_value
|
||||
|
||||
|
||||
## Reports whether both collision-prevention fields are present.
|
||||
func is_valid() -> bool:
|
||||
return not _entry_namespace.is_empty() and _entry_value > 0
|
||||
|
||||
|
||||
## Compares both namespace and numeric entry.
|
||||
func equals(other_server_entry_id: ServerEntryId) -> bool:
|
||||
return (
|
||||
other_server_entry_id != null
|
||||
and _entry_namespace == other_server_entry_id.entry_namespace
|
||||
and _entry_value == other_server_entry_id.entry_value
|
||||
)
|
||||
|
||||
|
||||
## Returns the stable adapter key without implying a ContentId mapping.
|
||||
func to_key() -> String:
|
||||
return "%s:%d" % [_entry_namespace, _entry_value]
|
||||
@@ -0,0 +1 @@
|
||||
uid://cg3mht5bhgsya
|
||||
@@ -0,0 +1,53 @@
|
||||
class_name WowGuid
|
||||
extends RefCounted
|
||||
|
||||
## Opaque 64-bit WoW wire GUID stored as two lossless unsigned 32-bit words.
|
||||
## Build-specific high-type decoding belongs to the protocol adapter and must be
|
||||
## fixture-backed before becoming a domain contract.
|
||||
|
||||
const MAXIMUM_UNSIGNED_32_BIT_VALUE := 0xffffffff
|
||||
|
||||
var high_32_bits: int:
|
||||
get:
|
||||
return _high_32_bits
|
||||
|
||||
var low_32_bits: int:
|
||||
get:
|
||||
return _low_32_bits
|
||||
|
||||
var _high_32_bits: int
|
||||
var _low_32_bits: int
|
||||
|
||||
|
||||
## Preserves both wire words without signed 64-bit conversion.
|
||||
func _init(high_32_bits_value: int, low_32_bits_value: int) -> void:
|
||||
_high_32_bits = high_32_bits_value
|
||||
_low_32_bits = low_32_bits_value
|
||||
|
||||
|
||||
## Reports whether both words fit their unsigned wire ranges.
|
||||
func has_valid_word_ranges() -> bool:
|
||||
return _is_unsigned_32_bit_word(_high_32_bits) and _is_unsigned_32_bit_word(_low_32_bits)
|
||||
|
||||
|
||||
## Reports the all-zero protocol sentinel without assigning gameplay meaning.
|
||||
func is_empty() -> bool:
|
||||
return _high_32_bits == 0 and _low_32_bits == 0
|
||||
|
||||
|
||||
## Compares all 64 opaque wire bits.
|
||||
func equals(other_wow_guid: WowGuid) -> bool:
|
||||
return (
|
||||
other_wow_guid != null
|
||||
and _high_32_bits == other_wow_guid.high_32_bits
|
||||
and _low_32_bits == other_wow_guid.low_32_bits
|
||||
)
|
||||
|
||||
|
||||
## Returns exactly sixteen lowercase hexadecimal digits for diagnostics/replays.
|
||||
func to_hex_string() -> String:
|
||||
return "%08x%08x" % [_high_32_bits, _low_32_bits]
|
||||
|
||||
|
||||
func _is_unsigned_32_bit_word(word_value: int) -> bool:
|
||||
return word_value >= 0 and word_value <= MAXIMUM_UNSIGNED_32_BIT_VALUE
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmwlyacooq2lc
|
||||
@@ -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
|
||||
@@ -3,9 +3,12 @@ extends CharacterBody3D
|
||||
const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
|
||||
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
|
||||
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||
|
||||
const TILE_SIZE := 533.33333
|
||||
const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
const UNIT_SIZE := CHUNK_SIZE / 8.0
|
||||
|
||||
@export var extracted_dir: String = "res://data/extracted"
|
||||
@@ -62,8 +65,11 @@ func _ready() -> void:
|
||||
_camera.current = true
|
||||
_camera.far = 50000.0
|
||||
if spawn_at_tile_center:
|
||||
global_position.x = (float(spawn_tile_x) + 0.5) * TILE_SIZE
|
||||
global_position.z = (float(spawn_tile_y) + 0.5) * TILE_SIZE
|
||||
var spawn_tile = ADT_TILE_COORDINATE_SCRIPT.new(spawn_tile_x, spawn_tile_y)
|
||||
var half_tile_size := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS * 0.5
|
||||
var spawn_local = ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, global_position.y, half_tile_size)
|
||||
var spawn_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(spawn_tile, spawn_local)
|
||||
global_position = Vector3(spawn_position.x_units, spawn_position.y_units, spawn_position.z_units)
|
||||
_load_character_visual()
|
||||
var ground := _sample_ground_height(global_position)
|
||||
if is_finite(ground):
|
||||
@@ -324,16 +330,17 @@ func _apply_camera_transform() -> void:
|
||||
|
||||
|
||||
func _sample_ground_height(world_pos: Vector3) -> float:
|
||||
var tx := int(floor(world_pos.x / TILE_SIZE))
|
||||
var ty := int(floor(world_pos.z / TILE_SIZE))
|
||||
var data := _load_adt(tx, ty)
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
|
||||
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
|
||||
var data := _load_adt(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
if data.is_empty():
|
||||
return NAN
|
||||
|
||||
var tile_origin := _get_tile_origin(data)
|
||||
var local := world_pos - tile_origin
|
||||
var chunk_x := clampi(int(floor(local.x / CHUNK_SIZE)), 0, 15)
|
||||
var chunk_y := clampi(int(floor(local.z / CHUNK_SIZE)), 0, 15)
|
||||
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
|
||||
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
|
||||
var chunk := _find_chunk(data, chunk_x, chunk_y, tile_origin)
|
||||
if chunk.is_empty():
|
||||
return NAN
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
extends Node
|
||||
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
|
||||
const TILE_SIZE := 533.33333
|
||||
const WOW_WORLD_CENTER := 17066.666
|
||||
const LIGHT_COORD_SCALE := 36.0
|
||||
const HALF_MINUTES_PER_DAY := 2880
|
||||
|
||||
@@ -701,28 +701,30 @@ func _get_target_wow_position() -> Vector3:
|
||||
var world_pos := Vector3.ZERO
|
||||
if _target:
|
||||
world_pos = _target.global_position
|
||||
return Vector3(
|
||||
WOW_WORLD_CENTER - world_pos.z,
|
||||
WOW_WORLD_CENTER - world_pos.x,
|
||||
world_pos.y)
|
||||
var canonical_position = COORDINATE_MAPPER_SCRIPT.godot_to_canonical(_typed_godot_position(world_pos))
|
||||
return Vector3(canonical_position.x_yards, canonical_position.y_yards, canonical_position.z_yards)
|
||||
|
||||
|
||||
func _get_target_area_id() -> int:
|
||||
if not _target or map_name.is_empty():
|
||||
return 0
|
||||
var world_pos := _target.global_position
|
||||
var tile_x := int(floor(world_pos.x / TILE_SIZE))
|
||||
var tile_y := int(floor(world_pos.z / TILE_SIZE))
|
||||
var local_x := world_pos.x - float(tile_x) * TILE_SIZE
|
||||
var local_y := world_pos.z - float(tile_y) * TILE_SIZE
|
||||
var chunk_x := clampi(int(floor(local_x / (TILE_SIZE / 16.0))), 0, 15)
|
||||
var chunk_y := clampi(int(floor(local_y / (TILE_SIZE / 16.0))), 0, 15)
|
||||
var areas := _load_adt_area_grid(tile_x, tile_y)
|
||||
var typed_world_position = _typed_godot_position(world_pos)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
||||
var local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_world_position)
|
||||
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, local_position)
|
||||
var areas := _load_adt_area_grid(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
if areas.is_empty():
|
||||
return 0
|
||||
var chunk_x := clampi(chunk_coordinate.chunk_x, 0, 15)
|
||||
var chunk_y := clampi(chunk_coordinate.chunk_y, 0, 15)
|
||||
return int(areas[chunk_y * 16 + chunk_x])
|
||||
|
||||
|
||||
func _typed_godot_position(world_position: Vector3):
|
||||
return GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
|
||||
|
||||
|
||||
func _load_adt_area_grid(tile_x: int, tile_y: int) -> PackedInt32Array:
|
||||
var key := "%d_%d" % [tile_x, tile_y]
|
||||
if _adt_area_cache.has(key):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,14 +12,19 @@ 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 COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||
const M2_MATERIAL_REFRESH_VERSION := 2
|
||||
const WMO_MATERIAL_REFRESH_VERSION := 10
|
||||
|
||||
const TILE_SIZE := 533.33333
|
||||
const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
const QUALITY_CUSTOM := "Custom"
|
||||
@@ -29,6 +34,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 +221,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 +303,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 +376,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 +602,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 +694,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
|
||||
@@ -772,10 +789,9 @@ func _apply_streaming_target(wanted_tiles: Dictionary, retained_tiles: Dictionar
|
||||
var parts: PackedStringArray = key.split("_")
|
||||
var tx: int = int(parts[0])
|
||||
var ty: int = int(parts[1])
|
||||
var tcx: float = (tx + 0.5) * TILE_SIZE
|
||||
var tcz: float = (ty + 0.5) * TILE_SIZE
|
||||
var dx: float = tcx - focus_pos.x
|
||||
var dz: float = tcz - focus_pos.z
|
||||
var tile_center := _tile_center_to_world(tx, ty)
|
||||
var dx: float = tile_center.x - focus_pos.x
|
||||
var dz: float = tile_center.z - focus_pos.z
|
||||
_tile_load_queue.append({
|
||||
"key": key,
|
||||
"tx": tx,
|
||||
@@ -2895,8 +2911,10 @@ func _predictive_focus_tiles(focus_pos: Vector3) -> Array[Vector2i]:
|
||||
if threshold <= 0.0:
|
||||
return result
|
||||
|
||||
var local_x := fposmod(focus_pos.x, TILE_SIZE) / TILE_SIZE
|
||||
var local_z := fposmod(focus_pos.z, TILE_SIZE) / TILE_SIZE
|
||||
var typed_focus_position = GODOT_WORLD_POSITION_SCRIPT.new(focus_pos.x, focus_pos.y, focus_pos.z)
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile_local(typed_focus_position)
|
||||
var local_x: float = tile_local_position.east_yards / TILE_SIZE
|
||||
var local_z: float = tile_local_position.south_yards / TILE_SIZE
|
||||
var x_offsets: Array[int] = [0]
|
||||
var z_offsets: Array[int] = [0]
|
||||
|
||||
@@ -3019,10 +3037,17 @@ func _is_tile_chunk_set_ready(state: Dictionary) -> bool:
|
||||
|
||||
|
||||
func _tile_dist_sq(state: Dictionary, cam_pos: Vector3) -> float:
|
||||
var min_x: float = float(state["tx"]) * TILE_SIZE
|
||||
var max_x: float = min_x + TILE_SIZE
|
||||
var min_z: float = float(state["ty"]) * TILE_SIZE
|
||||
var max_z: float = min_z + TILE_SIZE
|
||||
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(int(state["tx"]), int(state["ty"]))
|
||||
var north_west_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
|
||||
tile_coordinate,
|
||||
ADT_TILE_LOCAL_POSITION_SCRIPT.new(0.0, 0.0, 0.0))
|
||||
var south_east_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
|
||||
tile_coordinate,
|
||||
ADT_TILE_LOCAL_POSITION_SCRIPT.new(TILE_SIZE, 0.0, TILE_SIZE))
|
||||
var min_x: float = north_west_position.x_units
|
||||
var max_x: float = south_east_position.x_units
|
||||
var min_z: float = north_west_position.z_units
|
||||
var max_z: float = south_east_position.z_units
|
||||
|
||||
var dx := 0.0
|
||||
if cam_pos.x < min_x:
|
||||
@@ -3048,32 +3073,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,13 +5404,13 @@ 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
|
||||
|
||||
var center_tx: float = (_tile_min.x + _tile_max.x) * 0.5
|
||||
var center_ty: float = (_tile_min.y + _tile_max.y) * 0.5
|
||||
var center := _tile_center_to_world(center_tx, center_ty)
|
||||
var minimum_tile_center := _tile_center_to_world(_tile_min.x, _tile_min.y)
|
||||
var maximum_tile_center := _tile_center_to_world(_tile_max.x, _tile_max.y)
|
||||
var center := (minimum_tile_center + maximum_tile_center) * 0.5
|
||||
var tile_span: int = int(max(_tile_max.x - _tile_min.x + 1, _tile_max.y - _tile_min.y + 1))
|
||||
var height: float = max(2000.0, float(tile_span) * TILE_SIZE * 0.18)
|
||||
|
||||
@@ -5380,19 +5419,19 @@ func _position_camera_over_world() -> void:
|
||||
_camera_initialized = true
|
||||
|
||||
|
||||
## Tile (tx, ty) NW corner sits at Godot (tx*TILE_SIZE, 0, ty*TILE_SIZE).
|
||||
## This matches wow_to_godot output: chunk origins ≈ (TX*TILE_SIZE, h, TY*TILE_SIZE).
|
||||
func _tile_center_to_world(tile_x: float, tile_y: float) -> Vector3:
|
||||
return Vector3(
|
||||
(tile_x + 0.5) * TILE_SIZE,
|
||||
0.0,
|
||||
(tile_y + 0.5) * TILE_SIZE)
|
||||
func _tile_center_to_world(tile_x: int, tile_y: int) -> Vector3:
|
||||
var tile_coordinate = ADT_TILE_COORDINATE_SCRIPT.new(tile_x, tile_y)
|
||||
var half_tile_size := TILE_SIZE * 0.5
|
||||
var tile_center_position = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_godot(
|
||||
tile_coordinate,
|
||||
ADT_TILE_LOCAL_POSITION_SCRIPT.new(half_tile_size, 0.0, half_tile_size))
|
||||
return Vector3(tile_center_position.x_units, tile_center_position.y_units, tile_center_position.z_units)
|
||||
|
||||
|
||||
func _world_to_tile(world_pos: Vector3) -> Vector2i:
|
||||
return Vector2i(
|
||||
int(floor(world_pos.x / TILE_SIZE)),
|
||||
int(floor(world_pos.z / TILE_SIZE)))
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_pos.x, world_pos.y, world_pos.z)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
||||
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
|
||||
|
||||
func _tile_key(tx: int, ty: int) -> String:
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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" % [
|
||||
@@ -246,6 +246,8 @@ func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: flo
|
||||
|
||||
|
||||
func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
|
||||
if bool(checkpoint.get("diagnostic_spawn_marker", false)):
|
||||
return null if dry_run else _create_diagnostic_spawn_marker(checkpoint)
|
||||
var rel_path := String(checkpoint.get("probe_model", ""))
|
||||
if rel_path.is_empty() or dry_run:
|
||||
return null
|
||||
@@ -265,6 +267,43 @@ func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
|
||||
return M2_NATIVE_ANIMATED_BUILDER.build(data, extracted_dir)
|
||||
|
||||
|
||||
## Creates a renderer-native marker whose origin is the exact mapped server
|
||||
## spawn. The vertical mast grows upward from that origin, so terrain occlusion
|
||||
## remains visible instead of being hidden by an always-on-top overlay.
|
||||
func _create_diagnostic_spawn_marker(checkpoint: Dictionary) -> Node3D:
|
||||
var marker_root := Node3D.new()
|
||||
marker_root.name = "DiagnosticServerSpawnMarker"
|
||||
var marker_height := maxf(float(checkpoint.get("diagnostic_marker_height", 6.0)), 0.5)
|
||||
var marker_radius := maxf(float(checkpoint.get("diagnostic_marker_radius", 0.35)), 0.05)
|
||||
|
||||
var marker_material := StandardMaterial3D.new()
|
||||
marker_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
marker_material.albedo_color = Color(1.0, 0.05, 0.7, 1.0)
|
||||
marker_material.emission_enabled = true
|
||||
marker_material.emission = Color(1.0, 0.01, 0.35, 1.0)
|
||||
|
||||
var mast_mesh := CylinderMesh.new()
|
||||
mast_mesh.height = marker_height
|
||||
mast_mesh.top_radius = marker_radius
|
||||
mast_mesh.bottom_radius = marker_radius
|
||||
mast_mesh.material = marker_material
|
||||
var mast := MeshInstance3D.new()
|
||||
mast.name = "SpawnOriginMast"
|
||||
mast.mesh = mast_mesh
|
||||
mast.position.y = marker_height * 0.5
|
||||
marker_root.add_child(mast)
|
||||
|
||||
var origin_mesh := SphereMesh.new()
|
||||
origin_mesh.radius = marker_radius * 1.8
|
||||
origin_mesh.height = marker_radius * 3.6
|
||||
origin_mesh.material = marker_material
|
||||
var origin := MeshInstance3D.new()
|
||||
origin.name = "SpawnOrigin"
|
||||
origin.mesh = origin_mesh
|
||||
marker_root.add_child(origin)
|
||||
return marker_root
|
||||
|
||||
|
||||
func _set_sky_time(world: Node, time_hours: float) -> void:
|
||||
var sky := world.get_node_or_null("WowSkyController")
|
||||
if sky != null:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -5,7 +5,8 @@ extends SceneTree
|
||||
## [--wait 3.0] [--output user://render_terrain_height/report.json]
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const TILE_SIZE := 533.33333
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||
const RAY_HEIGHT := 5000.0
|
||||
|
||||
|
||||
@@ -36,7 +37,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 +54,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")
|
||||
@@ -90,9 +91,7 @@ func _run_async() -> void:
|
||||
|
||||
|
||||
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
|
||||
var tile_coordinate := Vector2i(
|
||||
int(floor(world_position.x / TILE_SIZE)),
|
||||
int(floor(world_position.z / TILE_SIZE)))
|
||||
var tile_coordinate := _adt_tile_vector2i(world_position)
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
var highest_terrain_height := -INF
|
||||
var intersected_tile_key := ""
|
||||
@@ -198,9 +197,7 @@ func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictiona
|
||||
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
|
||||
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
|
||||
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
|
||||
var sample_tile := Vector2i(
|
||||
int(floor(sample_position.x / TILE_SIZE)),
|
||||
int(floor(sample_position.z / TILE_SIZE)))
|
||||
var sample_tile := _adt_tile_vector2i(sample_position)
|
||||
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
|
||||
if not tile_states.has(sample_key):
|
||||
continue
|
||||
@@ -214,6 +211,12 @@ func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictiona
|
||||
return {"nearest_sample_distance": null}
|
||||
|
||||
|
||||
func _adt_tile_vector2i(world_position: Vector3) -> Vector2i:
|
||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
||||
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||
|
||||
|
||||
func _vector3_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"profile": "Blizzlike335",
|
||||
"scene": "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
"map": "Azeroth",
|
||||
"quality_preset": "High",
|
||||
"viewport": [1280, 900],
|
||||
"camera_fov": 62.0,
|
||||
"default_wait_seconds": 10.0,
|
||||
"default_measure_seconds": 1.0,
|
||||
"source_fixture": "res://src/tests/fixtures/coordinate_golden_points.json",
|
||||
"source_spawn_name": "azerothcore_human_warrior_start",
|
||||
"required_coverage": ["server_spawn"],
|
||||
"checkpoints": [
|
||||
{
|
||||
"name": "azerothcore_human_warrior_start",
|
||||
"coverage": ["server_spawn"],
|
||||
"camera": [17168.0, 105.0, 25984.0],
|
||||
"target": [17199.159666667, 83.5312, 26016.616666667],
|
||||
"player": [17199.159666667, 83.5312, 26016.616666667],
|
||||
"time_hours": 13.0,
|
||||
"diagnostic_spawn_marker": true,
|
||||
"diagnostic_marker_height": 6.0,
|
||||
"diagnostic_marker_radius": 0.35,
|
||||
"expected_adt_tile": [32, 48],
|
||||
"expected_adt_chunk": [3, 12]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
extends SceneTree
|
||||
|
||||
## Repository-wide M01 gate against manual cross-space world coordinate formulas.
|
||||
## Model-local M2/WMO basis transforms and ordinary Godot-space geometry are not
|
||||
## world-space conversions and are intentionally outside these signatures.
|
||||
|
||||
const SCAN_ROOTS: Array[String] = ["res://src", "res://addons"]
|
||||
const SOURCE_EXTENSIONS: Array[String] = ["gd", "cpp", "h", "hpp"]
|
||||
const EXCLUDED_DIRECTORY_PREFIXES: Array[String] = ["res://src/native/build/"]
|
||||
const GATE_PATH := "res://src/tools/verify_coordinate_conversion_boundaries.gd"
|
||||
const COORDINATE_MAPPER_PATH := "res://src/domain/coordinates/coordinate_mapper.gd"
|
||||
const CALIBRATION_ORACLE_PATH := "res://src/tools/verify_render_coordinate_calibration.gd"
|
||||
const NATIVE_BOUNDARY_PATH := "res://src/native/src/wow_chunk_reader.h"
|
||||
const ALLOWED_LEGACY_NAME_PATHS: Array[String] = [
|
||||
NATIVE_BOUNDARY_PATH,
|
||||
"res://src/native/src/adt_loader.cpp",
|
||||
"res://src/native/src/wdt_loader.cpp",
|
||||
CALIBRATION_ORACLE_PATH,
|
||||
]
|
||||
const REQUIRED_MAPPER_CALLS := {
|
||||
"res://src/scenes/sky/wow_sky_controller.gd": ["godot_to_canonical", "godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||
"res://src/scenes/streaming/streaming_world_loader.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_godot"],
|
||||
"res://src/scenes/player/third_person_wow_controller.gd": ["godot_to_adt_tile", "adt_tile_local_to_godot"],
|
||||
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
||||
}
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
var source_paths: Array[String] = []
|
||||
for scan_root in SCAN_ROOTS:
|
||||
_collect_source_paths(scan_root, source_paths, failures)
|
||||
|
||||
var native_boundary_definition_count := 0
|
||||
var independent_oracle_definition_count := 0
|
||||
for source_path in source_paths:
|
||||
if source_path == GATE_PATH:
|
||||
continue
|
||||
var source := _read_text(source_path, failures)
|
||||
var source_lines := source.split("\n")
|
||||
for line_index in range(source_lines.size()):
|
||||
var source_line := String(source_lines[line_index])
|
||||
var violation := _coordinate_violation(source_path, source_line)
|
||||
if not violation.is_empty():
|
||||
failures.append("%s:%d %s" % [source_path, line_index + 1, violation])
|
||||
if source_path == NATIVE_BOUNDARY_PATH and source_line.contains("inline void wow_to_godot"):
|
||||
native_boundary_definition_count += 1
|
||||
if source_path == CALIBRATION_ORACLE_PATH and source_line.contains("func _wow_to_godot"):
|
||||
independent_oracle_definition_count += 1
|
||||
|
||||
_expect_equal(native_boundary_definition_count, 1, "native world conversion definition count", failures)
|
||||
_expect_equal(independent_oracle_definition_count, 1, "independent calibration oracle count", failures)
|
||||
_verify_required_mapper_calls(failures)
|
||||
_verify_classifier_guards(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("COORDINATE_CONVERSION_BOUNDARIES: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=4 classifier_cases=6" % source_paths.size())
|
||||
quit(0)
|
||||
|
||||
|
||||
func _coordinate_violation(source_path: String, source_line: String) -> String:
|
||||
var stripped_line := source_line.strip_edges()
|
||||
if stripped_line.is_empty():
|
||||
return ""
|
||||
if (source_line.contains("wow_to_godot") or source_line.contains("godot_to_wow")) and source_path not in ALLOWED_LEGACY_NAME_PATHS:
|
||||
return "manual WoW/Godot conversion name outside mapper boundary"
|
||||
if (source_line.contains("17066.666") or source_line.contains("WOW_WORLD_CENTER")) and source_path not in [NATIVE_BOUNDARY_PATH, CALIBRATION_ORACLE_PATH]:
|
||||
return "manual WoW world-center formula outside native boundary/oracle"
|
||||
if source_line.contains("1600.0 / 3.0") and source_path != COORDINATE_MAPPER_PATH:
|
||||
return "duplicated ADT tile-size formula outside CoordinateMapper"
|
||||
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("floor(") and source_line.contains("TILE_SIZE") and (source_line.contains(".x") or source_line.contains(".z")):
|
||||
return "manual Godot world-position to ADT tile formula"
|
||||
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("fposmod(") and source_line.contains("TILE_SIZE"):
|
||||
return "manual Godot world-position to ADT tile-local formula"
|
||||
if source_path != COORDINATE_MAPPER_PATH and source_line.contains("+ 0.5) * TILE_SIZE"):
|
||||
return "manual ADT tile-center to Godot world-position formula"
|
||||
return ""
|
||||
|
||||
|
||||
func _verify_required_mapper_calls(failures: Array[String]) -> void:
|
||||
for source_path in REQUIRED_MAPPER_CALLS:
|
||||
var source := _read_text(source_path, failures)
|
||||
for required_call in REQUIRED_MAPPER_CALLS[source_path]:
|
||||
if not source.contains(String(required_call)):
|
||||
failures.append("%s must call CoordinateMapper.%s" % [source_path, required_call])
|
||||
|
||||
|
||||
func _verify_classifier_guards(failures: Array[String]) -> void:
|
||||
_expect_true(not _coordinate_violation("res://src/example.gd", "const CENTER := 17066.666").is_empty(), "world-center classifier", failures)
|
||||
_expect_true(not _coordinate_violation("res://src/example.gd", "func wow_to_godot():").is_empty(), "conversion-name classifier", failures)
|
||||
_expect_true(not _coordinate_violation("res://src/example.gd", "floor(world_position.x / TILE_SIZE)").is_empty(), "world-to-tile classifier", failures)
|
||||
_expect_true(not _coordinate_violation("res://src/example.gd", "fposmod(world_position.z, TILE_SIZE)").is_empty(), "world-to-tile-local classifier", failures)
|
||||
_expect_true(not _coordinate_violation("res://src/example.gd", "(tile_x + 0.5) * TILE_SIZE").is_empty(), "tile-center classifier", failures)
|
||||
_expect_true(_coordinate_violation(NATIVE_BOUNDARY_PATH, "inline void wow_to_godot(float wx) { gx = -(wx - 17066.666f); }").is_empty(), "native boundary exception", failures)
|
||||
|
||||
|
||||
func _collect_source_paths(directory_path: String, source_paths: Array[String], failures: Array[String]) -> void:
|
||||
var directory := DirAccess.open(directory_path)
|
||||
if directory == null:
|
||||
failures.append("cannot scan directory %s" % directory_path)
|
||||
return
|
||||
for entry_name in directory.get_files():
|
||||
var source_path := directory_path.path_join(entry_name)
|
||||
if entry_name.get_extension().to_lower() in SOURCE_EXTENSIONS:
|
||||
source_paths.append(source_path)
|
||||
for child_directory_name in directory.get_directories():
|
||||
var child_directory_path := directory_path.path_join(child_directory_name) + "/"
|
||||
if _has_excluded_prefix(child_directory_path):
|
||||
continue
|
||||
_collect_source_paths(child_directory_path.trim_suffix("/"), source_paths, failures)
|
||||
|
||||
|
||||
func _has_excluded_prefix(path: String) -> bool:
|
||||
for excluded_prefix in EXCLUDED_DIRECTORY_PREFIXES:
|
||||
if path.begins_with(excluded_prefix):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bnj844sd2n6ln
|
||||
@@ -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
|
||||
@@ -0,0 +1,231 @@
|
||||
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 godot_position = CoordinateMapperScript.canonical_to_godot(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0))
|
||||
var direct_godot_tile = CoordinateMapperScript.godot_to_adt_tile(godot_position)
|
||||
var direct_godot_local = CoordinateMapperScript.godot_to_adt_tile_local(godot_position)
|
||||
var direct_godot_round_trip = CoordinateMapperScript.adt_tile_local_to_godot(direct_godot_tile, direct_godot_local)
|
||||
_expect_equal(direct_godot_tile.tile_x, 31, "direct Godot ADT tile X", failures)
|
||||
_expect_equal(direct_godot_tile.tile_y, 49, "direct Godot ADT tile Y", failures)
|
||||
_expect_near(direct_godot_local.east_yards, 146.66677, GOLDEN_POSITION_TOLERANCE_YARDS, "direct Godot tile-local east", failures)
|
||||
_expect_near(direct_godot_local.south_yards, 86.66677, GOLDEN_POSITION_TOLERANCE_YARDS, "direct Godot tile-local south", failures)
|
||||
_expect_near(direct_godot_round_trip.x_units, godot_position.x_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local X round trip", failures)
|
||||
_expect_near(direct_godot_round_trip.y_units, godot_position.y_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local Y round trip", failures)
|
||||
_expect_near(direct_godot_round_trip.z_units, godot_position.z_units, ROUND_TRIP_TOLERANCE_YARDS, "direct Godot tile-local Z round trip", failures)
|
||||
|
||||
var chunk_local_position = AdtTileLocalPositionScript.new(
|
||||
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 2.0,
|
||||
180.0,
|
||||
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
|
||||
@@ -0,0 +1,87 @@
|
||||
extends SceneTree
|
||||
|
||||
## Headless M01 contract verification for non-interchangeable identity values.
|
||||
|
||||
const ContentIdScript = preload("res://src/domain/identity/content_id.gd")
|
||||
const EntityIdScript = preload("res://src/domain/identity/entity_id.gd")
|
||||
const WowGuidScript = preload("res://src/domain/identity/wow_guid.gd")
|
||||
const ServerEntryIdScript = preload("res://src/domain/identity/server_entry_id.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_content_id(failures)
|
||||
_verify_entity_id(failures)
|
||||
_verify_wow_guid(failures)
|
||||
_verify_server_entry_id(failures)
|
||||
_verify_scene_free_sources(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("DOMAIN_IDENTITIES: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("DOMAIN_IDENTITIES PASS content=5 entity=5 wow_guid=6 server_entry=5")
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_content_id(failures: Array[String]) -> void:
|
||||
var normalized = ContentIdScript.new(" 123E4567-E89B-42D3-A456-426614174000 ")
|
||||
_expect_true(normalized.is_valid(), "normalized ContentId is valid", failures)
|
||||
_expect_equal(normalized.to_key(), "123e4567-e89b-42d3-a456-426614174000", "ContentId canonical key", failures)
|
||||
_expect_true(normalized.equals(ContentIdScript.new("123e4567-e89b-42d3-a456-426614174000")), "equal ContentIds", failures)
|
||||
_expect_true(not normalized.equals(ContentIdScript.new("123e4567-e89b-42d3-a456-426614174001")), "different ContentIds", failures)
|
||||
_expect_true(not ContentIdScript.new("123e4567e89b42d3a456426614174000").is_valid(), "malformed ContentId", failures)
|
||||
|
||||
|
||||
func _verify_entity_id(failures: Array[String]) -> void:
|
||||
var entity_id = EntityIdScript.new(42)
|
||||
_expect_true(entity_id.is_valid(), "positive EntityId", failures)
|
||||
_expect_true(not EntityIdScript.new(0).is_valid(), "zero EntityId", failures)
|
||||
_expect_true(not EntityIdScript.new(-1).is_valid(), "negative EntityId", failures)
|
||||
_expect_true(entity_id.equals(EntityIdScript.new(42)), "equal EntityIds", failures)
|
||||
_expect_equal(entity_id.to_debug_key(), "entity:42", "EntityId diagnostic key", failures)
|
||||
|
||||
|
||||
func _verify_wow_guid(failures: Array[String]) -> void:
|
||||
var guid = WowGuidScript.new(0xf1300000, 0xffffffff)
|
||||
_expect_true(guid.has_valid_word_ranges(), "maximum GUID low word", failures)
|
||||
_expect_equal(guid.to_hex_string(), "f1300000ffffffff", "GUID hexadecimal form", failures)
|
||||
_expect_true(guid.equals(WowGuidScript.new(0xf1300000, 0xffffffff)), "equal GUID words", failures)
|
||||
_expect_true(WowGuidScript.new(0, 0).is_empty(), "zero GUID sentinel", failures)
|
||||
_expect_true(not WowGuidScript.new(-1, 0).has_valid_word_ranges(), "negative GUID word", failures)
|
||||
_expect_true(not WowGuidScript.new(0x100000000, 0).has_valid_word_ranges(), "overflow GUID word", failures)
|
||||
|
||||
|
||||
func _verify_server_entry_id(failures: Array[String]) -> void:
|
||||
var creature_entry = ServerEntryIdScript.new(" Creature_Template ", 123)
|
||||
var gameobject_entry = ServerEntryIdScript.new("gameobject_template", 123)
|
||||
_expect_true(creature_entry.is_valid(), "namespaced server entry", failures)
|
||||
_expect_equal(creature_entry.to_key(), "creature_template:123", "server entry key", failures)
|
||||
_expect_true(not creature_entry.equals(gameobject_entry), "cross-table entry collision prevented", failures)
|
||||
_expect_true(not ServerEntryIdScript.new("", 123).is_valid(), "missing entry namespace", failures)
|
||||
_expect_true(not ServerEntryIdScript.new("creature_template", 0).is_valid(), "zero server entry", failures)
|
||||
|
||||
|
||||
func _verify_scene_free_sources(failures: Array[String]) -> void:
|
||||
for file_name in ["content_id.gd", "entity_id.gd", "wow_guid.gd", "server_entry_id.gd"]:
|
||||
var path := "res://src/domain/identity/%s" % file_name
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
continue
|
||||
var source := file.get_as_text()
|
||||
_expect_true(not source.contains("extends Node"), "%s does not inherit Node" % file_name, failures)
|
||||
_expect_true(not source.contains("extends Resource"), "%s does not inherit Resource" % file_name, failures)
|
||||
_expect_true(not source.contains("Vector3"), "%s does not expose Vector3" % file_name, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bmig1rwaur44r
|
||||
@@ -0,0 +1,138 @@
|
||||
extends SceneTree
|
||||
|
||||
## Verifies that the pinned AzerothCore spawn is routed through the coordinate
|
||||
## contract into an executable renderer checkpoint. GUI capture is performed by
|
||||
## capture_render_checkpoints.gd with the validated dedicated manifest.
|
||||
|
||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||
const SERVER_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/server_world_position.gd")
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/server_spawn_render_manifest.json"
|
||||
const EXPECTED_FIXTURE_PATH := "res://src/tests/fixtures/coordinate_golden_points.json"
|
||||
const EXPECTED_SPAWN_NAME := "azerothcore_human_warrior_start"
|
||||
const EXPECTED_PROFILE := "Blizzlike335"
|
||||
const POSITION_TOLERANCE_YARDS := 0.002
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
var manifest := _load_json_object(MANIFEST_PATH, failures)
|
||||
var fixture_path := String(manifest.get("source_fixture", ""))
|
||||
_expect_equal(fixture_path, EXPECTED_FIXTURE_PATH, "source fixture path", failures)
|
||||
var fixture := _load_json_object(fixture_path, failures) if not fixture_path.is_empty() else {}
|
||||
if manifest.is_empty() or fixture.is_empty():
|
||||
_report_and_quit(failures)
|
||||
return
|
||||
|
||||
_expect_equal(int(manifest.get("schema_version", -1)), 1, "manifest schema version", failures)
|
||||
_expect_equal(String(manifest.get("profile", "")), EXPECTED_PROFILE, "renderer profile", failures)
|
||||
_expect_equal(String(manifest.get("map", "")), "Azeroth", "renderer map", failures)
|
||||
_expect_true(ResourceLoader.exists(String(manifest.get("scene", ""))), "renderer scene exists", failures)
|
||||
_expect_equal(String(manifest.get("source_spawn_name", "")), EXPECTED_SPAWN_NAME, "source spawn name", failures)
|
||||
|
||||
var spawn := _find_named_dictionary(fixture.get("server_spawns", []), EXPECTED_SPAWN_NAME)
|
||||
_expect_true(not spawn.is_empty(), "pinned server spawn exists", failures)
|
||||
var checkpoints: Array = manifest.get("checkpoints", [])
|
||||
_expect_equal(checkpoints.size(), 1, "renderer checkpoint count", failures)
|
||||
if spawn.is_empty() or checkpoints.size() != 1 or not (checkpoints[0] is Dictionary):
|
||||
_report_and_quit(failures)
|
||||
return
|
||||
|
||||
var checkpoint: Dictionary = checkpoints[0]
|
||||
var server_values := _three_floats(spawn.get("server_position", null), "server position", failures)
|
||||
if server_values.is_empty():
|
||||
_report_and_quit(failures)
|
||||
return
|
||||
var server_position = SERVER_WORLD_POSITION_SCRIPT.new(server_values[0], server_values[1], server_values[2])
|
||||
var canonical_position = COORDINATE_MAPPER_SCRIPT.server_to_canonical(server_position)
|
||||
var godot_position = COORDINATE_MAPPER_SCRIPT.canonical_to_godot(canonical_position)
|
||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.canonical_to_adt_tile(canonical_position)
|
||||
var tile_local_position = COORDINATE_MAPPER_SCRIPT.canonical_to_adt_tile_local(canonical_position)
|
||||
var chunk_coordinate = COORDINATE_MAPPER_SCRIPT.adt_tile_local_to_chunk(tile_coordinate, tile_local_position)
|
||||
var mapped_position := [godot_position.x_units, godot_position.y_units, godot_position.z_units]
|
||||
|
||||
_expect_equal(String(checkpoint.get("name", "")), EXPECTED_SPAWN_NAME, "checkpoint name", failures)
|
||||
_expect_true("server_spawn" in checkpoint.get("coverage", []), "server-spawn coverage", failures)
|
||||
_expect_true(bool(checkpoint.get("diagnostic_spawn_marker", false)), "diagnostic spawn marker enabled", failures)
|
||||
_expect_three_near(checkpoint.get("target", null), mapped_position, "checkpoint target", failures)
|
||||
_expect_three_near(checkpoint.get("player", null), mapped_position, "checkpoint player", failures)
|
||||
_expect_pair(checkpoint.get("expected_adt_tile", null), [tile_coordinate.tile_x, tile_coordinate.tile_y], "ADT tile", failures)
|
||||
_expect_pair(checkpoint.get("expected_adt_chunk", null), [chunk_coordinate.chunk_x, chunk_coordinate.chunk_y], "ADT chunk", failures)
|
||||
_expect_three_near(spawn.get("expected_godot_position", null), mapped_position, "fixture Godot position", failures)
|
||||
var camera_values := _three_floats(checkpoint.get("camera", null), "checkpoint camera", failures)
|
||||
if not camera_values.is_empty():
|
||||
_expect_true(camera_values[1] > godot_position.y_units, "checkpoint camera is above spawn", failures)
|
||||
_expect_true(Vector3(camera_values[0], camera_values[1], camera_values[2]).distance_to(Vector3(mapped_position[0], mapped_position[1], mapped_position[2])) > 1.0, "checkpoint camera is distinct from spawn", failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
_report_and_quit(failures)
|
||||
return
|
||||
print("SERVER_SPAWN_RENDERER PASS spawn=1 checkpoint=1 tile=%d_%d chunk=%d_%d profile=%s" % [
|
||||
tile_coordinate.tile_x,
|
||||
tile_coordinate.tile_y,
|
||||
chunk_coordinate.chunk_x,
|
||||
chunk_coordinate.chunk_y,
|
||||
EXPECTED_PROFILE,
|
||||
])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _find_named_dictionary(entries: Array, expected_name: String) -> Dictionary:
|
||||
for entry_variant in entries:
|
||||
if entry_variant is Dictionary and String(entry_variant.get("name", "")) == expected_name:
|
||||
return entry_variant
|
||||
return {}
|
||||
|
||||
|
||||
func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open JSON: %s" % path)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
failures.append("JSON is not an object: %s" % path)
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
func _three_floats(value_variant, label: String, failures: Array[String]) -> Array[float]:
|
||||
if not (value_variant is Array) or value_variant.size() != 3:
|
||||
failures.append("%s must contain three numbers" % label)
|
||||
return []
|
||||
return [float(value_variant[0]), float(value_variant[1]), float(value_variant[2])]
|
||||
|
||||
|
||||
func _expect_three_near(actual_variant, expected_values: Array, label: String, failures: Array[String]) -> void:
|
||||
var actual_values := _three_floats(actual_variant, label, failures)
|
||||
if actual_values.is_empty() or expected_values.size() != 3:
|
||||
return
|
||||
for component_index in range(3):
|
||||
if absf(actual_values[component_index] - float(expected_values[component_index])) > POSITION_TOLERANCE_YARDS:
|
||||
failures.append("%s[%d] expected %.9f, got %.9f" % [label, component_index, expected_values[component_index], actual_values[component_index]])
|
||||
|
||||
|
||||
func _expect_pair(actual_variant, expected_values: Array, label: String, failures: Array[String]) -> void:
|
||||
if not (actual_variant is Array) or actual_variant.size() != 2:
|
||||
failures.append("%s must contain two integers" % label)
|
||||
return
|
||||
for component_index in range(2):
|
||||
_expect_equal(int(actual_variant[component_index]), int(expected_values[component_index]), "%s[%d]" % [label, component_index], failures)
|
||||
|
||||
|
||||
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
|
||||
|
||||
func _report_and_quit(failures: Array[String]) -> void:
|
||||
if failures.is_empty():
|
||||
failures.append("verification stopped without a diagnostic")
|
||||
for failure in failures:
|
||||
push_error("SERVER_SPAWN_RENDERER: %s" % failure)
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7f2rpfey0vrs
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b7ayw5a0bn1lq
|
||||
@@ -1,6 +1,7 @@
|
||||
# M00 — Renderer Baseline
|
||||
|
||||
<!-- OPENWC_TARGET:ACTIVE -->
|
||||
<!-- OPENWC_TARGET:DONE -->
|
||||
<!-- OPENWC_TARGET_DONE:M00:work/sindo-main-codex/m00-integrator-closeout:2026-07-13 -->
|
||||
|
||||
## Outcome
|
||||
|
||||
@@ -38,10 +39,10 @@ godot --headless --path . --script res://src/tools/capture_render_checkpoints.gd
|
||||
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-10
|
||||
- Revision/worktree: `93bfe11` + worktree baseline changes.
|
||||
- Commands: `.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1 -Output user://render_baseline_runner_test`; `.\tools\run_render_baseline.ps1 -Output user://render_baseline_m00_2026-07-10 -CacheState existing`.
|
||||
- Results: headless load, renderer material smoke, M2 unique-id dedupe and manifest contract passed. Full run produced 14 PNGs and 14 result records for 7 cold/warm checkpoints. Actual renderer was Godot 4.6.1, Vulkan Forward+, NVIDIA RTX 5070, 1280x900, High profile, with all seven cache families present. Cold p95 range was 31.585..86.537 ms with max hitch 258.456 ms; warm p95 range was 18.824..38.085 ms with max hitch 163.363 ms. The final streaming snapshots had an empty tile queue. Report: `user://render_baseline_m00_2026-07-10/report.json`.
|
||||
- Fidelity comparison: fixed positions/time/profile and current visual gaps are recorded, but no approved paired screenshots from the original WoW 3.3.5a build 12340 were available. This baseline does not claim parity.
|
||||
- Changed files: `RENDER.md`, `docs/RENDER_BASELINE.md`, `src/scenes/streaming/streaming_world_loader.gd`, `src/tools/capture_render_checkpoints.gd`, `src/tools/render_baseline_manifest.json`, `src/tools/verify_render_baseline_manifest.gd`, `tools/run_render_baseline.ps1`, `targets/00-render-baseline.md`.
|
||||
- Remaining risks: original-client paired comparison is still required before DONE; D3D12 descriptor heap creation failed and the measured run fell back to Vulkan; shutdown reported leaked renderer RIDs/resources; current captures intentionally preserve visible terrain/placement/material/animation gaps, including the synthetic native animated M2 probe; perceptual image diff/tolerance is documented but not automated yet.
|
||||
- Date: 2026-07-13
|
||||
- Revision/worktree: merged M00 implementation through `f19c0c4`; closeout worktree `work/sindo-main-codex/m00-integrator-closeout`.
|
||||
- Commands: `.\tools\run_render_baseline.ps1 -Output user://render_baseline_m00_complete_2026-07-13 -CacheState existing -WaitSeconds 8 -MeasureSeconds 0.5 -ReferenceCheckpointDirectory .\sources\OpenWCReferenceCheckpoints -VisualComparisonReport user://render_baseline_m00_complete_2026-07-13/visual_comparison.json -VisualComparisonWidth 2560 -VisualComparisonHeight 1440`; `.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1 -Output user://render_baseline_m00_post_merge`; `.\tools\verify_coordination.ps1`; `.\tools\verify_documentation.ps1`.
|
||||
- Results: the final GUI run produced fourteen cold/warm captures for seven checkpoints and all seven required coverage categories, with `compared=14` and `missing=0`. The post-merge dry-run passed project load, material, M2 unique-id dedupe, runtime-cache shutdown, manifest and five-point coordinate calibration checks. Coordination and documentation gates passed. Renderer shutdown follow-up removed retained Node/resource/RID diagnostics in focused and seven-checkpoint GUI runs.
|
||||
- Fidelity comparison: private original-client WoW 3.3.5a build 12340 references cover terrain, ADT boundary, dense M2, large WMO, liquid, animated M2 and sky transition. Every measured pair exceeds the strict visual tolerance and is recorded as a current gap rather than parity. Sky framing is approximately paired; animated M2 is asset-paired but placement-unpaired because the OpenWC Goldshire probe is synthetic. Full metrics and provenance are recorded in `docs/M00_FINAL_PAIRED_EVIDENCE.md`.
|
||||
- Changed files: `RENDER.md`, `docs/RENDER_BASELINE.md`, `docs/M00_FINAL_PAIRED_EVIDENCE.md`, `docs/modules/world-renderer.md`, renderer baseline scripts/verifiers, manifest/capture tooling, streaming shutdown ownership and M00 coordination claims.
|
||||
- Remaining risks: renderer parity is not claimed; documented terrain/material/placement/animation/lighting/liquid/culling gaps remain future renderer work. D3D12 descriptor heap creation can fall back to Vulkan. Godot can emit a timing-dependent anonymous zero-reference `RefCounted` teardown warning without retained Node/resource/RID ownership.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# M01 — Coordinates and Architecture Seams
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
<!-- OPENWC_TARGET:DONE -->
|
||||
<!-- OPENWC_TARGET_DONE:M01:work/sindo-main-codex/m01-integrator-closeout:2026-07-14 -->
|
||||
|
||||
## Outcome
|
||||
|
||||
@@ -8,13 +9,13 @@
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] Создать domain/value-object слой без `Node`/`Resource` dependencies.
|
||||
- [ ] Определить canonical WoW, Godot, ADT tile/chunk и local coordinate types.
|
||||
- [ ] Реализовать единственный `CoordinateMapper` для position и orientation.
|
||||
- [ ] Собрать golden points из ADT, server spawn и оригинального клиента.
|
||||
- [ ] Ввести `StreamingFocus` вместо неявной зависимости от active Camera3D.
|
||||
- [ ] Ввести stable `ContentId`, `EntityId` и отличить их от WoW GUID/server entry.
|
||||
- [ ] Запретить ручные axis conversion через code review rule/test search.
|
||||
- [x] Создать domain/value-object слой без `Node`/`Resource` dependencies.
|
||||
- [x] Определить canonical WoW, Godot, ADT tile/chunk и local coordinate types.
|
||||
- [x] Реализовать единственный `CoordinateMapper` для position и orientation.
|
||||
- [x] Собрать golden points из ADT, server spawn и оригинального клиента.
|
||||
- [x] Ввести `StreamingFocus` вместо неявной зависимости от active Camera3D.
|
||||
- [x] Ввести stable `ContentId`, `EntityId` и отличить их от WoW GUID/server entry.
|
||||
- [x] Запретить ручные axis conversion через code review rule/test search.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -28,10 +29,35 @@ Renderer/test scene получают focus через контракт; коор
|
||||
|
||||
## Evidence
|
||||
|
||||
- Date:
|
||||
- Revision/worktree:
|
||||
- Commands:
|
||||
- Results:
|
||||
- Fidelity comparison:
|
||||
- Changed files:
|
||||
- Remaining risks:
|
||||
- Date: 2026-07-14
|
||||
- Revision/worktree: merged master `c8e99f2`; closeout
|
||||
`work/sindo-main-codex/m01-integrator-closeout`
|
||||
- Commands: `tools/run_render_baseline.ps1 -DryRun -WaitSeconds 0.1
|
||||
-MeasureSeconds 0.1`; `verify_coordinate_mapper.gd`;
|
||||
`verify_coordinate_golden_fixtures.gd`; `verify_streaming_focus.gd`;
|
||||
`verify_domain_identities.gd`; coordination/documentation/diff gates.
|
||||
- Results: CoordinateMapper PASS (`5` golden points, `8` tile boundaries,
|
||||
`2` raw MCNK, `5` yaw); golden fixtures PASS (`1` server spawn, `1` raw ADT,
|
||||
`5` original-client points); StreamingFocus PASS (`1` contract, `2` runtime
|
||||
scenes, `3` capture tools); identities PASS (`5/5/6/5`); conversion boundary
|
||||
PASS (`84` files, one native boundary, one oracle, four consumers, six
|
||||
classifiers); server-spawn renderer PASS at ADT `(32,48)`, MCNK `(3,12)`;
|
||||
renderer manifest PASS (`7/7/7`) and calibration maximum error `0.000015`.
|
||||
- Fidelity comparison: five build-12340 camera observations, pinned AzerothCore
|
||||
Human Warrior spawn and private raw ADT metadata agree through coordinate
|
||||
contract version 2. Cache-complete cold/warm GUI capture places the mapped
|
||||
spawn marker on rendered Northshire terrain with focus `(32,48)`, quality
|
||||
`100/100`, missing cache `0`; PNG SHA-256 values are recorded in the server
|
||||
spawn handoff. The final ADT-boundary regression passed `2/2`; the full M01
|
||||
regression captured `14/14`, with `11` strict passes and three low-mean
|
||||
timing/animation-phase differences rather than coordinate drift.
|
||||
- Changed files: `src/domain/coordinates/`, `src/domain/identity/`,
|
||||
`src/domain/streaming/`, migrated sky/player/streaming/terrain consumers,
|
||||
coordinate/renderer fixtures and tools, runtime scenes, ADR 0001/0002,
|
||||
coordinate/domain/renderer module specs, coding/render documentation and
|
||||
M01 coordination claims.
|
||||
- Remaining risks: TrinityCore populated spawn equivalence is not verified;
|
||||
native parser conversion remains one separately calibrated boundary; MDDF/
|
||||
MODF placement Euler parity is outside world-yaw scope; renderer visual parity
|
||||
gaps and the known anonymous ObjectDB teardown warning continue. No `1:1`
|
||||
compatibility claim is made by M01.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# M02 — Player Decomposition
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
<!-- OPENWC_TARGET:ACTIVE -->
|
||||
|
||||
## Outcome
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ OpenWC должен предоставить четыре согласованн
|
||||
|
||||
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
|
||||
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
|
||||
- Renderer baseline M00 создан; до `DONE` не хватает paired comparison с оригинальным клиентом и закрытия известных diagnostic gaps.
|
||||
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; текущая работа M02 декомпозирует player input, movement, terrain query, camera и presentation без observable regression.
|
||||
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
|
||||
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
|
||||
|
||||
|
||||
+4
-4
@@ -10,7 +10,7 @@
|
||||
|
||||
## Current target
|
||||
|
||||
`M00` — [00-render-baseline.md](00-render-baseline.md)
|
||||
`M02` — [02-player-decomposition.md](02-player-decomposition.md)
|
||||
|
||||
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
|
||||
| ID | Цель | Зависит от | Статус |
|
||||
|---|---|---|---|
|
||||
| M00 | [Renderer baseline](00-render-baseline.md) | — | ACTIVE |
|
||||
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | OPEN |
|
||||
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | OPEN |
|
||||
| M00 | [Renderer baseline](00-render-baseline.md) | — | DONE |
|
||||
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | DONE |
|
||||
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | ACTIVE |
|
||||
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | OPEN |
|
||||
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
|
||||
| M05 | [Content Project](05-content-project.md) | M04 | OPEN |
|
||||
|
||||
@@ -57,6 +57,8 @@ try {
|
||||
Invoke-GodotStep "runtime-cache-shutdown" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_runtime_cache_shutdown.gd")
|
||||
Invoke-GodotStep "baseline-manifest" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_baseline_manifest.gd")
|
||||
Invoke-GodotStep "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
|
||||
Invoke-GodotStep "coordinate-boundaries" @("--headless", "--path", ".", "--script", "res://src/tools/verify_coordinate_conversion_boundaries.gd")
|
||||
Invoke-GodotStep "server-spawn-renderer" @("--headless", "--path", ".", "--script", "res://src/tools/verify_server_spawn_renderer.gd")
|
||||
|
||||
$captureArgs = @(
|
||||
"--path", ".",
|
||||
|
||||
Reference in New Issue
Block a user