Compare commits

...

16 Commits

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

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

Agent: sindo-main-codex

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

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

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

Agent: sindo-main-codex

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

Agent: sindo-main-codex

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

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

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

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

Agent: sindo-main-codex
2026-07-13 16:15:18 +04:00
sindoring 7c64e8d60a docs(M01): mark domain identities ready
Work-Package: M01-FND-DOMAIN-IDENTITIES-001

Agent: sindo-main-codex
2026-07-13 16:11:35 +04:00
sindoring c1dc07c705 feat(M01): add typed domain identities
Work-Package: M01-FND-DOMAIN-IDENTITIES-001

Agent: sindo-main-codex
2026-07-13 16:11:00 +04:00
sindoring d68786355c docs(M01): claim domain identity contracts
Work-Package: M01-FND-DOMAIN-IDENTITIES-001

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