Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5103eed014 | |||
| 9d34a47765 | |||
| aea7787b9b | |||
| 230620089f | |||
| 42fdf40282 | |||
| f8538ba2cf | |||
| d233a41ce8 | |||
| dfc10312f8 | |||
| 8e8ea32ba3 | |||
| 19df5b7968 | |||
| d467ffee7f | |||
| a4f60dcb06 | |||
| fbe22f6a94 | |||
| 0eef72a2c2 | |||
| 01953cc25e | |||
| b199876ce6 |
@@ -12,9 +12,14 @@
|
||||
|
||||
1. Прочитать [`targets/README.md`](targets/README.md).
|
||||
2. Прочитать файл текущей цели, указанный там как `Current target`.
|
||||
3. Прочитать [`docs/README.md`](docs/README.md) и связанные с задачей нормативные документы.
|
||||
4. Для renderer-задач прочитать [`RENDER.md`](RENDER.md).
|
||||
5. Проверить рабочее дерево и не перезаписывать чужие изменения.
|
||||
3. Прочитать [`targets/DEVELOPMENT_ROADMAP.md`](targets/DEVELOPMENT_ROADMAP.md) и соответствующий subsystem plan из `targets/roadmap/`.
|
||||
4. Прочитать [`docs/README.md`](docs/README.md) и связанные с задачей нормативные документы.
|
||||
5. Прочитать обязательный [`docs/CODING_STANDARD.md`](docs/CODING_STANDARD.md): явные имена и KISS имеют приоритет.
|
||||
6. Прочитать обязательный [`docs/DOCUMENTATION_STANDARD.md`](docs/DOCUMENTATION_STANDARD.md) и определить, какие API docs, module spec и diagrams затрагивает задача.
|
||||
7. Для renderer-задач прочитать [`RENDER.md`](RENDER.md).
|
||||
8. Проверить рабочее дерево и не перезаписывать чужие изменения.
|
||||
9. Перед созданием сложной подсистемы или добавлением dependency проверить [`docs/TOOLING_CATALOG.md`](docs/TOOLING_CATALOG.md), зарегистрировать найденные готовые решения и провести bounded evaluation вместо немедленного вендоринга.
|
||||
10. При командной работе прочитать [`docs/TEAM_WORKFLOW.md`](docs/TEAM_WORKFLOW.md), проверить активные claims/draft PRs и объявить стабильный Agent ID до изменений.
|
||||
|
||||
Если задача пользователя противоречит target-порядку, прямое указание пользователя имеет приоритет, но агент обязан сохранить архитектурные границы и отметить влияние на roadmap.
|
||||
|
||||
@@ -28,10 +33,20 @@
|
||||
- Интерактивный authoring UI реализуется только через Godot `EditorPlugin`.
|
||||
- Любой новый формат, публичный контракт или несовместимое решение требует обновления документации и при необходимости ADR.
|
||||
- Исправление дефекта должно получать regression test, когда это технически возможно.
|
||||
- Не писать крупную подсистему с нуля до поиска готовых libraries, reference implementations, fixtures и tooling; результат исследования фиксируется в `docs/TOOLING_CATALOG.md`.
|
||||
- Не начинать claimed work package другого агента и не редактировать его exclusive paths без явного handoff.
|
||||
- Не работать двум агентам в одном worktree. Каждый агент использует собственную ветку и clone/worktree.
|
||||
- Статус milestone, Evidence и `OPENWC_TARGET_DONE` изменяет только назначенный интегратор.
|
||||
- Каждый новый/существенно изменённый модуль обязан иметь актуальную specification в `docs/modules/`, inline documentation публичного API и data-flow diagram по `docs/DOCUMENTATION_STANDARD.md`.
|
||||
- Stateful/async/cross-layer работа обязана документировать state/sequence diagram, ownership, threading, failure и recovery paths.
|
||||
- Документация обновляется в том же work package, что код. Handoff без обязательной документации имеет статус только `PARTIAL`.
|
||||
- Не сокращать имена ради краткости. Variable/function/type должен явно описывать назначение, domain context, units и coordinate space, когда они неоднозначны.
|
||||
- Следовать KISS: использовать самое простое корректное решение, не вводить premature abstractions/frameworks/optimizations и изолировать неизбежную сложность.
|
||||
- Имена `manager`, `helper`, `utils`, `data`, `info`, `process`, `handle` без точного контекста считаются design smell и требуют переименования или обоснования.
|
||||
|
||||
## Метки состояния
|
||||
|
||||
В каждом `targets/*.md` присутствует ровно одна метка:
|
||||
В каждом исполняемом target-файле `targets/[0-9][0-9]-*.md` присутствует ровно одна метка. Справочные документы `targets/DEVELOPMENT_ROADMAP.md` и `targets/roadmap/*.md` меток состояния не имеют:
|
||||
|
||||
- `<!-- OPENWC_TARGET:OPEN -->` — цель ещё не начата.
|
||||
- `<!-- OPENWC_TARGET:ACTIVE -->` — единственная текущая цель.
|
||||
@@ -51,6 +66,8 @@
|
||||
3. В Evidence записаны команды, результаты и изменённые файлы.
|
||||
4. Не осталось обязательных незакрытых пунктов или скрытых заглушек.
|
||||
5. Обновлены `targets/README.md` и матрица функций, если применимо.
|
||||
6. Публичный API, module spec, input/output tables и обязательные diagrams соответствуют фактической реализации.
|
||||
7. Код соответствует `docs/CODING_STANDARD.md`: понятные имена, простой data flow и обоснованная сложность.
|
||||
|
||||
Агент НЕ ДОЛЖЕН ставить `DONE` только потому, что код компилируется. Если пользователь явно принял частичный результат, статус остаётся `ACTIVE`, а принятое ограничение записывается в Evidence.
|
||||
|
||||
@@ -62,4 +79,5 @@
|
||||
- что изменено;
|
||||
- какие проверки выполнены;
|
||||
- fidelity evidence для поведения 3.3.5a;
|
||||
- оставшиеся риски или следующий незакрытый checklist item.
|
||||
- оставшиеся риски или следующий незакрытый checklist item;
|
||||
- какие API/module docs и diagrams созданы или обновлены.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Renderer Notes
|
||||
|
||||
Воспроизводимый renderer baseline для текущей цели M00, regression manifest, метрики и правила сравнения описаны в [`docs/RENDER_BASELINE.md`](docs/RENDER_BASELINE.md). Baseline фиксирует текущее поведение и известные расхождения, но не заявляет parity с WoW 3.3.5a.
|
||||
|
||||
Парные checkpoint PNG можно автоматически проверить через `src/tools/compare_render_checkpoints.gd`; reference-снимки оригинального клиента остаются вне Git, а автоматический tolerance не заменяет human fidelity approval.
|
||||
|
||||
Paired run 2026-07-11 подтвердил крупный coordinate/placement gap: некоторые server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC. До исправления этого расхождения perceptual metrics измеряют также несовпадение композиции, а не только материалы и свет.
|
||||
|
||||
Цель renderer-работы в этом проекте: добиться ощущения производительности оригинального клиента WoW 3.3.5a в Godot, без видимых фризов при переходе ADT -> ADT и без постоянного отката видимых участков к низкому качеству.
|
||||
|
||||
Этот документ фиксирует текущее состояние рендера, сделанные оптимизации и практические правила дальнейшей работы.
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# OpenWC Coordination
|
||||
|
||||
Основные правила находятся в [`../docs/TEAM_WORKFLOW.md`](../docs/TEAM_WORKFLOW.md).
|
||||
|
||||
Предпочтительно вести claims в issue и draft PR/MR общего Git forge. Каталог `claims/` — fallback, если issue tracker недоступен. Один claim — один файл; не использовать общий изменяемый board.
|
||||
|
||||
## Создание fallback claim
|
||||
|
||||
1. Скопировать [`TASK_TEMPLATE.md`](TASK_TEMPLATE.md) в `claims/<work-package-id>.md`.
|
||||
2. Заполнить owner, lease, paths, contracts и tests.
|
||||
3. Опубликовать claim до production edits.
|
||||
4. После интеграции заменить состояние на `accepted` или удалить claim отдельным coordination commit, сохранив историю в Git.
|
||||
|
||||
Claim-файлы не заменяют target Evidence и не меняют milestone status.
|
||||
@@ -0,0 +1,74 @@
|
||||
# <WORK-PACKAGE-ID> — <Title>
|
||||
|
||||
<!-- OPENWC_CLAIM:<WORK-PACKAGE-ID>:<AGENT-ID>:<YYYY-MM-DD> -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target:
|
||||
- Program:
|
||||
- Owner/Agent ID:
|
||||
- Branch:
|
||||
- Lease expires UTC:
|
||||
- Integrator:
|
||||
|
||||
## Outcome
|
||||
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive:
|
||||
- Shared/hotspots:
|
||||
- Generated/ignored:
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events:
|
||||
- Schema/format version:
|
||||
- Migration/compatibility:
|
||||
- Consumers:
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires:
|
||||
- Blocks:
|
||||
- External state:
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands:
|
||||
- Fixtures:
|
||||
- Fidelity evidence:
|
||||
- Performance budget:
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs:
|
||||
- Module specification:
|
||||
- Data-flow diagram:
|
||||
- Sequence/state/dependency diagrams:
|
||||
- Source map/status updates:
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced:
|
||||
- Simplest considered solution:
|
||||
- Rejected complexity/abstractions:
|
||||
- Unavoidable complexity and justification:
|
||||
- Measured optimization evidence:
|
||||
|
||||
## Status
|
||||
|
||||
- State: claimed
|
||||
- Done:
|
||||
- Next:
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit:
|
||||
- Results:
|
||||
- Remaining risks:
|
||||
- Documentation updated:
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-CAMERA-OCCLUDERS-001 — Camera occluder diagnostic
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-OCCLUDERS-001:sindo-main-codex:2026-07-13 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-camera-occluders`
|
||||
- Lease expires UTC: 2026-07-13
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Report scene-tree M2/WMO geometry that contains calibrated cameras or intersects each camera-to-target segment.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing placements, cameras, FOV, culling, or renderer behavior.
|
||||
- Claiming full coverage of RenderingServer RID-only instances.
|
||||
- Implementing a production visibility or collision service.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/probe_render_camera_occluders.gd`
|
||||
- Shared/hotspots: renderer baseline documentation
|
||||
- Generated/ignored: local JSON probe reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: headless diagnostic CLI only
|
||||
- Schema/format version: report schema 1
|
||||
- Migration/compatibility: none
|
||||
- Consumers: M00 fidelity workflow
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: calibrated five-point manifest and streaming scene
|
||||
- Blocks: placement versus camera-composition classification
|
||||
- External state: local extracted/cache data
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: camera occluder probe, coordination and documentation gates
|
||||
- Fixtures: five calibrated camera/target segments
|
||||
- Fidelity evidence: scene-tree bounds at paired build 12340 viewpoints
|
||||
- Performance budget: offline diagnostic
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: probe header and output fields
|
||||
- Module specification: verification/source map
|
||||
- Data-flow diagram: occluder probe flow
|
||||
- Sequence/state/dependency diagrams: synchronous diagnostic; not applicable
|
||||
- Source map/status updates: baseline findings
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `camera_containing_geometry`, `segment_intersecting_geometry`
|
||||
- Simplest considered solution: transformed published AABBs
|
||||
- Rejected complexity/abstractions: GPU visibility readback or new BVH
|
||||
- Unavoidable complexity and justification: RID-only geometry cannot be named by this probe
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: five-point scene-tree AABB probe, containment/intersection classification and documentation
|
||||
- Next: integrator review; calibrate reproducible reference camera direction/FOV separately
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: zero containing geometry at all five cameras; expected WMO/liquid target intersections; ADT/dense segments unobstructed
|
||||
- Remaining risks: RID-only instances are excluded; manual reference direction and FOV were not recorded exactly
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-COORD-CALIBRATION-001 — Renderer coordinate calibration
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-COORD-CALIBRATION-001:sindo-main-codex:2026-07-13 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-coordinate-calibration`
|
||||
- Lease expires UTC: 2026-07-13
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Record the five observed build 12340 camera positions as non-proprietary golden coordinates and determine whether the existing WoW/Godot position formula round-trips them.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Introducing the M01 production `CoordinateMapper` contract.
|
||||
- Changing terrain, placement, WMO, liquid, or streaming behavior.
|
||||
- Claiming camera composition parity from coordinate round-trip alone.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/verify_render_coordinate_calibration.gd`
|
||||
- Shared/hotspots: `src/tools/render_baseline_manifest.json`, renderer baseline documentation
|
||||
- Generated/ignored: original-client screenshots and local reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: none; headless diagnostic only
|
||||
- Schema/format version: additive checkpoint calibration metadata, manifest schema remains 1
|
||||
- Migration/compatibility: existing capture consumers ignore additive fields
|
||||
- Consumers: M00 fidelity workflow and future M01 golden fixtures
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: five accepted original-client build 12340 viewpoints
|
||||
- Blocks: diagnosis of paired camera/placement mismatch
|
||||
- External state: screenshots remain outside Git
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: coordinate calibration probe, baseline manifest, coordination and documentation gates
|
||||
- Fixtures: five `reference_wow_camera` values in renderer manifest
|
||||
- Fidelity evidence: positions were observed in build 12340 during the paired session
|
||||
- Performance budget: negligible headless arithmetic
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: diagnostic script header
|
||||
- Module specification: renderer verification/source map update
|
||||
- Data-flow diagram: baseline calibration flow update
|
||||
- Sequence/state/dependency diagrams: not applicable; stateless synchronous probe
|
||||
- Source map/status updates: renderer module and baseline document
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `reference_wow_camera`, `maximum_round_trip_error`
|
||||
- Simplest considered solution: direct formula probe over manifest fixtures
|
||||
- Rejected complexity/abstractions: production domain mapper before M01
|
||||
- Unavoidable complexity and justification: none
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: five golden camera points, headless round-trip probe, runner integration and documentation
|
||||
- Next: integrator review; diagnose terrain height/placement/composition separately
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: five points passed with maximum mapping/round-trip error 0.000015
|
||||
- Remaining risks: correct position arithmetic does not prove terrain height, placement or camera direction parity
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-FOV-SWEEP-001 — Empirical camera FOV sweep
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-FOV-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-fov-sweep`
|
||||
- Lease expires UTC: 2026-07-14
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Support a bounded empirical FOV sweep when build 12340 does not expose a readable camera FOV CVar.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Claiming exact WoW FOV from a single manually aimed screenshot.
|
||||
- Changing runtime player camera defaults.
|
||||
- Automating yaw/pitch registration or image warping.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: FOV/filter CLI additions in checkpoint tools
|
||||
- Shared/hotspots: renderer baseline documentation
|
||||
- Generated/ignored: local sweep PNGs and JSON reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: additive `--camera-fov` and comparator `--only` CLI options
|
||||
- Schema/format version: unchanged
|
||||
- Migration/compatibility: existing commands retain defaults
|
||||
- Consumers: M00 fidelity workflow
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: build 12340 reference JPG and paired-image comparator
|
||||
- Blocks: empirical projection ranking
|
||||
- External state: original screenshots remain outside Git
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: synthetic comparator test, capture dry-run with override, bounded real sweep
|
||||
- Fixtures: `elwynn_adt_boundary` reference
|
||||
- Fidelity evidence: ranked perceptual metrics with manual-direction limitation
|
||||
- Performance budget: offline diagnostic
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: CLI headers
|
||||
- Module specification: verification/source map if needed
|
||||
- Data-flow diagram: FOV sweep flow
|
||||
- Sequence/state/dependency diagrams: not applicable
|
||||
- Source map/status updates: baseline findings
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `camera_fov_override`, `only_filter`
|
||||
- Simplest considered solution: reuse capture and comparator
|
||||
- Rejected complexity/abstractions: computer vision registration framework
|
||||
- Unavoidable complexity and justification: multiple captures are required because client FOV is inaccessible
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: client FOV access audit, capture override, comparator filter/regression, dedicated-camera ownership fix and bounded sweep
|
||||
- Next: integrator review; obtain reproducible yaw/pitch/zoom metadata before changing normative FOV
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: corrected ranking 26=0.079588, 38=0.079633, 50=0.084353, 62=0.088360, 86=0.097993; plateau is inconclusive
|
||||
- Remaining risks: build 12340 FOV is inaccessible through tested APIs; manual direction/framing dominates metrics
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-TERRAIN-HEIGHT-001 — Terrain height diagnostic
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-TERRAIN-HEIGHT-001:sindo-main-codex:2026-07-13 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-terrain-height`
|
||||
- Lease expires UTC: 2026-07-13
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Measure rendered terrain height and camera clearance at the five build 12340 golden checkpoints without changing renderer behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Adding a runtime terrain-query API or collision system.
|
||||
- Changing terrain geometry, coordinate mapping, placements, or cameras.
|
||||
- Implementing the M01 CoordinateMapper.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/probe_render_terrain_height.gd`
|
||||
- Shared/hotspots: renderer baseline documentation and runner
|
||||
- Generated/ignored: local JSON probe reports
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: headless diagnostic CLI only
|
||||
- Schema/format version: report schema 1
|
||||
- Migration/compatibility: none
|
||||
- Consumers: M00 fidelity diagnosis
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: calibrated renderer manifest and active terrain meshes
|
||||
- Blocks: classification of under-terrain camera gaps
|
||||
- External state: local extracted/cache data
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: terrain probe, M00 dry-run, coordination and documentation gates
|
||||
- Fixtures: five calibrated manifest checkpoints
|
||||
- Fidelity evidence: camera clearance against rendered OpenWC terrain
|
||||
- Performance budget: offline diagnostic only
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: script CLI header
|
||||
- Module specification: verification/source map
|
||||
- Data-flow diagram: terrain probe flow
|
||||
- Sequence/state/dependency diagrams: synchronous diagnostic; not applicable
|
||||
- Source map/status updates: baseline and renderer module
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `terrain_height`, `camera_clearance`
|
||||
- Simplest considered solution: CPU ray against already loaded mesh
|
||||
- Rejected complexity/abstractions: new parser, physics collision, runtime query service
|
||||
- Unavoidable complexity and justification: tile-local ray transform is required by mesh ownership
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: active-mesh terrain probe, four clearance measurements, isolated waterfall missing-mesh confirmation and documentation
|
||||
- Next: integrator review; placement/composition diagnosis for four points and tile 30_49 ownership diagnosis remain separate packages
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: four cameras are 12.034..90.178 units above terrain; waterfall tile has no accessible terrain mesh after 10-second settle
|
||||
- Remaining risks: mesh ray does not measure WMO/M2 occlusion; waterfall missing mesh requires streaming ownership investigation
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-TILE-OWNERSHIP-001 — Waterfall terrain tile ownership
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-TILE-OWNERSHIP-001:sindo-main-codex:2026-07-13 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-waterfall-tile-ownership`
|
||||
- Lease expires UTC: 2026-07-13
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Identify the runtime transition that prevents waterfall tile `30_49` from exposing a terrain mesh to the height probe.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing streaming budgets or terrain behavior.
|
||||
- Rebuilding caches or modifying extracted assets.
|
||||
- Adding a production terrain query service.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: terrain probe runtime ownership diagnostics
|
||||
- Shared/hotspots: renderer baseline documentation
|
||||
- Generated/ignored: local probe reports and caches
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: additive diagnostic JSON fields
|
||||
- Schema/format version: terrain report remains schema 1
|
||||
- Migration/compatibility: additive fields only
|
||||
- Consumers: M00 fidelity diagnosis
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: merged terrain-height probe and local cache inventory
|
||||
- Blocks: waterfall terrain ownership classification
|
||||
- External state: local extracted/cache data
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: isolated waterfall probe, coordination and documentation gates
|
||||
- Fixtures: checkpoint tile `30_49`
|
||||
- Fidelity evidence: runtime state correlated with build 12340 waterfall viewpoint
|
||||
- Performance budget: offline diagnostic
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: diagnostic output fields
|
||||
- Module specification: verification/source map if behavior changes
|
||||
- Data-flow diagram: update ownership transition if needed
|
||||
- Sequence/state/dependency diagrams: document observed tile transition
|
||||
- Source map/status updates: baseline findings
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `available`, `queued_index`, `loading`, `state_present`, `mesh_source`
|
||||
- Simplest considered solution: inspect existing loader registries read-only
|
||||
- Rejected complexity/abstractions: new tracing framework
|
||||
- Unavoidable complexity and justification: none
|
||||
- Measured optimization evidence: not applicable
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: raw/cache inventory, isolated runtime state, mesh AABB/local probe and nearby sampling diagnosis
|
||||
- Next: integrator review; placement/composition remains the actual paired-camera gap
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: tile 30_49 ownership and meshes are healthy; exact ray misses a triangle seam/edge, while a 2-unit offset samples terrain at 113.872
|
||||
- Remaining risks: nearby estimate is diagnostic and must not become a gameplay terrain-query contract
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`
|
||||
@@ -0,0 +1,78 @@
|
||||
# M00-QAR-VISUAL-DIFF-001 — Renderer checkpoint visual diff
|
||||
|
||||
<!-- OPENWC_CLAIM:M00-QAR-VISUAL-DIFF-001:sindo-main-codex:2026-07-13 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M00
|
||||
- Program: QAR
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m00-checkpoint-diff`
|
||||
- Lease expires UTC: 2026-07-13
|
||||
- Integrator: milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Provide a deterministic, headless perceptual comparison for paired renderer checkpoint PNGs, with machine-readable results and synthetic regression coverage.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Supplying or committing original-client screenshots.
|
||||
- Claiming visual parity or replacing human approval.
|
||||
- Image registration, camera correction, or temporal animation matching.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/tools/compare_render_checkpoints.gd`
|
||||
- Shared/hotspots: `src/tools/render_baseline_manifest.json`, `tools/run_render_baseline.ps1`, `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`, `RENDER.md`
|
||||
- Generated/ignored: comparison reports, diff PNGs, original-client captures
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Public API/events: headless CLI arguments and JSON report documented in renderer baseline docs
|
||||
- Schema/format version: comparison report schema 1; baseline manifest schema remains 1
|
||||
- Migration/compatibility: additive manifest budget fields
|
||||
- Consumers: developers and CI
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: Godot `Image` API and M00 checkpoint naming contract
|
||||
- Blocks: automated portion of original-client paired comparison
|
||||
- External state: approved build 12340 screenshots remain unavailable
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: headless self-test; manifest verification; documentation and coordination gates
|
||||
- Fixtures: tool-generated identical and changed 2x2 PNGs
|
||||
- Fidelity evidence: algorithm is deterministic; real-client evidence remains a manual/external input
|
||||
- Performance budget: offline checkpoint operation, linear in pixel count
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs: CLI usage in script header
|
||||
- Module specification: update renderer verification and source map
|
||||
- Data-flow diagram: update baseline documentation
|
||||
- Sequence/state/dependency diagrams: not stateful or asynchronous; not applicable
|
||||
- Source map/status updates: renderer module and RENDER notes
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `mean_perceptual_error`, `changed_pixel_ratio`
|
||||
- Simplest considered solution: direct Godot Image iteration with no dependency
|
||||
- Rejected complexity/abstractions: external image library, SSIM window pipeline, image registration
|
||||
- Unavoidable complexity and justification: sRGB linearization avoids comparing encoded channel values directly
|
||||
- Measured optimization evidence: not required for offline seven-checkpoint comparison
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: comparator, runner integration, manifest tolerance contract, synthetic regression, five local build 12340 references, three viewpoint calibrations, corrected visual capture and first paired report
|
||||
- Next: integrator review; coordinate/placement gap should be resolved before tolerance calibration
|
||||
- Blocked by: real paired screenshots only for final human fidelity approval
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: branch HEAD
|
||||
- Results: ten pairs compared with no missing candidates; mean error 0.0707..0.1746 and changed ratio 0.5504..0.8187; human review identified under-terrain/inside-geometry camera mismatches
|
||||
- Remaining risks: coordinate/placement mismatch blocks meaningful tolerance calibration; synthetic animation, dusk and full human approval remain incomplete
|
||||
- Documentation updated: `docs/RENDER_BASELINE.md`, `docs/modules/world-renderer.md`, `RENDER.md`
|
||||
@@ -0,0 +1,5 @@
|
||||
# Fallback claims
|
||||
|
||||
Этот каталог используется только когда общий issue/PR tracker недоступен. Каждый активный work package создаёт отдельный Markdown-файл по [`../TASK_TEMPLATE.md`](../TASK_TEMPLATE.md).
|
||||
|
||||
Файл без валидного `OPENWC_CLAIM` marker не считается claim. Не хранить здесь secrets, credentials, proprietary data или большие логи.
|
||||
@@ -56,6 +56,8 @@ Renderer получает `StreamingFocus`, `WorldVisualSnapshot` и presentatio
|
||||
|
||||
Владеет login/realm/character screens, HUD и FrameXML/Lua-compatible presentation. UI читает immutable view models и отправляет intents. Lua API получает capability-based facade; прямой доступ к network, filesystem и editor API запрещён.
|
||||
|
||||
Lua VM подключается через внутренний `LuaRuntime` interface. `rilua` и PUC-Rio Lua 5.1.1 рассматриваются как заменяемые backend-кандидаты; TOC, FrameXML, WoW API, event dispatch, SavedVariables, secure actions и taint не зависят от конкретного VM API. Rust/C++ типы не выходят за runtime adapter boundary.
|
||||
|
||||
### Audio
|
||||
|
||||
Владеет music, ambience, positional SFX, UI sounds и voice playback. Получает zone/combat/weather events и DBC-backed descriptors. Gameplay не управляет AudioStreamPlayer напрямую.
|
||||
@@ -86,6 +88,12 @@ Godot `EditorPlugin` регистрирует workspace, docks, inspectors, gizm
|
||||
|
||||
Pipeline выполняет validate, allocate IDs, bake, diff, package, generate SQL, dry-run, apply-to-dev и verification. Production deployment является отдельной явно подтверждаемой операцией.
|
||||
|
||||
### Navigation tooling
|
||||
|
||||
Navigation build является частью authoring/build pipeline, а не renderer или authoritative client gameplay. Optional backend `recast-rs` получает каноническую triangle/collision geometry и создаёт OpenWC navigation cache для editor preview, reachability validation и path diagnostics. Первая интеграция должна быть process/CLI boundary; Rust GDExtension допускается только после стабильного формата обмена и измеренной необходимости интерактивных запросов.
|
||||
|
||||
Server `mmaps` остаются отдельным артефактом. Экспорт в TrinityCore/AzerothCore формат запрещено считать поддержанным, пока compatibility tests не докажут совпадение coordinates, Recast parameters, tile layout и server query behavior.
|
||||
|
||||
## Направление зависимостей
|
||||
|
||||
```text
|
||||
@@ -129,6 +137,7 @@ src/
|
||||
editor/
|
||||
server_data/
|
||||
build_pipeline/
|
||||
navigation/
|
||||
shared/
|
||||
tests/
|
||||
addons/
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
- Packed GUIDs, update fields, create/update/out-of-range/destroy.
|
||||
- Movement flags, heartbeat, jump/fall/swim/fly, transports, knockback и spline movement.
|
||||
- Server time, tutorial flags, account data и feature capability detection.
|
||||
- Warden handshake/module compatibility для явно поддерживаемых test/private server profiles с sandbox и без ориентации на official servers.
|
||||
|
||||
## Player и camera
|
||||
|
||||
@@ -92,6 +93,7 @@
|
||||
- HUD: unit frames, target/focus, action bars, buffs, cast bars, bags, quest tracking, chat и combat text.
|
||||
- FrameXML: templates, inheritance, anchors, strata, layers, scripts и events.
|
||||
- Lua 5.1-compatible API surface, secure boundaries, SavedVariables и addon lifecycle.
|
||||
- Заменяемый Lua 5.1.1 backend; `rilua` и PUC-Rio проходят единый WoW-specific compatibility corpus до выбора production VM.
|
||||
- Widgets: Frame, Button, CheckButton, EditBox, Slider, StatusBar, ScrollFrame, Tooltip, Cooldown, Model, Texture, FontString, Minimap.
|
||||
- Macro conditionals, keybindings, slash commands, localization и UI scaling.
|
||||
|
||||
@@ -110,6 +112,7 @@
|
||||
- Quests, gossip, conditions, loot, vendors, trainers и localization.
|
||||
- Zones, areas, graveyards, teleports, taxi, triggers и phase rules.
|
||||
- Dungeon packages, encounters, doors, bosses, scripts и validation.
|
||||
- Navigation authoring: walkability overlay, reachability, patrol/escort validation, off-mesh links и dynamic doors; optional `recast-rs` backend.
|
||||
- Import, diff, package, SQL/change-set generation, dev deployment и playtest.
|
||||
|
||||
## Требование к расширяемости
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
# OpenWC Coding Standard
|
||||
|
||||
## Главные принципы
|
||||
|
||||
1. Код читается значительно чаще, чем пишется.
|
||||
2. Явное и точное имя предпочтительнее короткого и двусмысленного.
|
||||
3. KISS применяется по умолчанию: выбирается самое простое решение, которое корректно выполняет текущую задачу и сохраняет архитектурные границы.
|
||||
4. Сложность допускается только там, где она следует из предметной области, формата, протокола, производительности или совместимости 3.3.5a.
|
||||
5. Неизбежная сложность должна быть локализована, протестирована и документирована.
|
||||
|
||||
## Имена
|
||||
|
||||
Не нужно бояться длинных имён переменных, функций, типов, сигналов и файлов. Имя должно позволять понять назначение сущности без поиска всех мест использования.
|
||||
|
||||
Хорошо:
|
||||
|
||||
```gdscript
|
||||
var terrain_control_splat_cache_finalize_queue: Array = []
|
||||
var maximum_concurrent_terrain_upgrade_tasks: int = 1
|
||||
|
||||
func enqueue_terrain_control_splat_cache_finalize(tile_coordinate: Vector2i) -> void:
|
||||
pass
|
||||
|
||||
func should_retain_streaming_tile_outside_visible_radius(tile_state: TileState) -> bool:
|
||||
return false
|
||||
```
|
||||
|
||||
Плохо:
|
||||
|
||||
```gdscript
|
||||
var q = []
|
||||
var max_t = 1
|
||||
|
||||
func do_ctl(p):
|
||||
pass
|
||||
|
||||
func chk(s):
|
||||
return false
|
||||
```
|
||||
|
||||
Длина не является самоцелью. Не повторять контекст, который уже однозначно задаётся типом или модулем:
|
||||
|
||||
```gdscript
|
||||
# Внутри TerrainStreamingService:
|
||||
func enqueue_finalize(tile: TerrainTile) -> void:
|
||||
pass
|
||||
```
|
||||
|
||||
Такое имя допустимо, если `finalize` в этом классе имеет только одно значение. Если существуют terrain mesh, material и liquid finalization, имя должно уточнять конкретную операцию.
|
||||
|
||||
## Naming rules
|
||||
|
||||
- Типы/classes: `PascalCase`.
|
||||
- GDScript functions/variables/signals: `snake_case`.
|
||||
- Constants: `UPPER_SNAKE_CASE`.
|
||||
- Private implementation members: `_leading_underscore` по Godot convention.
|
||||
- C++ следует Godot/GDExtension convention внутри существующего native module; новый API не смешивает стили в одной области.
|
||||
- Boolean: `is_`, `has_`, `can_`, `should_`, `needs_`, `was_`.
|
||||
- Collections называются во множественном числе или по роли: `loaded_tiles`, `pending_packet_events`, `entity_by_guid`.
|
||||
- Functions начинаются с действия: `load_`, `parse_`, `build_`, `enqueue_`, `apply_`, `validate_`, `resolve_`, `convert_`, `release_`.
|
||||
- Units указываются в имени: `_milliseconds`, `_seconds`, `_bytes`, `_meters`, `_tiles`, `_radians`, `_degrees`.
|
||||
- Coordinate space указывается, если возможна путаница: `canonical_wow_position`, `godot_world_position`, `adt_tile_coordinate`.
|
||||
- Версия/build/profile указывается в типе или namespace, если semantics различаются: `Wotlk335ProtocolProfile`.
|
||||
|
||||
Допустимые устоявшиеся доменные сокращения:
|
||||
|
||||
- WoW, MPQ, BLP, DBC;
|
||||
- ADT, WDT, WDL;
|
||||
- M2, WMO;
|
||||
- GUID, SRP, API, UI;
|
||||
- LOD, RID, GPU, CPU;
|
||||
- SQL, TCP.
|
||||
|
||||
Новые локальные сокращения из двух-трёх букв без общеизвестного значения запрещены.
|
||||
|
||||
## Запрещённые размытые имена
|
||||
|
||||
Избегать без уточнения:
|
||||
|
||||
```text
|
||||
data
|
||||
info
|
||||
item
|
||||
object
|
||||
value
|
||||
temp
|
||||
result
|
||||
state
|
||||
manager
|
||||
helper
|
||||
utils
|
||||
process
|
||||
handle
|
||||
do_work
|
||||
```
|
||||
|
||||
Они допустимы только когда тип и ближайший scope делают значение полностью однозначным. Например, `parse_packet() -> PacketParseResult` может использовать локальную переменную `result`, если функция мала и другого результата нет.
|
||||
|
||||
`Manager`, `Helper`, `Utils` почти всегда скрывают несколько обязанностей. Предпочтительны названия по роли:
|
||||
|
||||
```text
|
||||
WorldSession
|
||||
StreamingTargetPlanner
|
||||
RenderBudgetScheduler
|
||||
CoordinateMapper
|
||||
AssetRepository
|
||||
QuestValidator
|
||||
ServerSchemaAdapter
|
||||
```
|
||||
|
||||
## KISS decision order
|
||||
|
||||
При выборе реализации двигаться по порядку:
|
||||
|
||||
1. Простая typed function с явными inputs/outputs.
|
||||
2. Маленький value object или immutable result.
|
||||
3. Небольшой service с одной ответственностью.
|
||||
4. Composition нескольких services.
|
||||
5. Interface/strategy только при реальной вариативности или boundary.
|
||||
6. Generic framework только после нескольких подтверждённых use cases.
|
||||
7. Native/low-level optimization только после измеренного bottleneck.
|
||||
|
||||
Не переходить на следующий уровень, если предыдущий решает задачу без нарушения correctness и architecture.
|
||||
|
||||
## Простота функций
|
||||
|
||||
- Одна функция выполняет одну логическую операцию.
|
||||
- Название функции описывает observable effect.
|
||||
- Inputs и outputs явные; hidden global state минимален.
|
||||
- Guard clauses предпочтительнее глубокой вложенности.
|
||||
- Happy path должен читаться сверху вниз.
|
||||
- Side effects не смешиваются с pure calculation без необходимости.
|
||||
- Parsing, validation, state mutation и presentation отделяются.
|
||||
- Функция не принимает набор boolean flags, меняющих её смысл.
|
||||
|
||||
Плохо:
|
||||
|
||||
```gdscript
|
||||
func load_tile(path: String, high: bool, async: bool, editor: bool):
|
||||
pass
|
||||
```
|
||||
|
||||
Лучше:
|
||||
|
||||
```gdscript
|
||||
func request_streaming_tile_load(request: StreamingTileLoadRequest) -> void:
|
||||
pass
|
||||
```
|
||||
|
||||
При этом `StreamingTileLoadRequest` оправдан только если параметры действительно образуют устойчивый contract. Для двух простых аргументов не нужно создавать объект только ради архитектурности.
|
||||
|
||||
## Простота data flow
|
||||
|
||||
- Один владелец mutable state.
|
||||
- Один канонический тип для одной концепции.
|
||||
- Один mapper для coordinate conversion.
|
||||
- Один authoritative source для content/runtime state.
|
||||
- Изменения передаются commands/events/read models, а не чтением чужих внутренних dictionaries.
|
||||
- Не создавать второй cache, registry или event bus без описанной отдельной ответственности.
|
||||
- Не использовать строки и `call()` как основной type system.
|
||||
- Dictionary допустим на binary/dynamic boundary; внутри domain данные переводятся в typed representation.
|
||||
|
||||
## Простота архитектуры
|
||||
|
||||
- Не вводить interface только потому, что «когда-нибудь может появиться второй backend».
|
||||
- Interface обязателен на реальной внешней границе: renderer facade, protocol profile, Lua VM, server schema adapter, asset repository.
|
||||
- Не создавать base class с одним наследником без конкретного boundary/invariant.
|
||||
- Предпочитать composition inheritance.
|
||||
- Не строить универсальную ECS, service container, message broker или plugin framework до подтверждённых требований OpenWC.
|
||||
- Не переносить архитектуру reference-проекта целиком; извлекать только проверенное решение конкретной проблемы.
|
||||
|
||||
## Работа со сложностью
|
||||
|
||||
Сложность считается оправданной для:
|
||||
|
||||
- binary formats и build-specific layouts;
|
||||
- encrypted/framed network protocol;
|
||||
- incremental world streaming;
|
||||
- animation/material compatibility;
|
||||
- Lua/FrameXML/taint semantics;
|
||||
- cross-core schema mappings;
|
||||
- async jobs, cancellation и deterministic cleanup;
|
||||
- performance-critical batching после profiling.
|
||||
|
||||
Если сложность неизбежна:
|
||||
|
||||
1. Изолировать её в одном модуле.
|
||||
2. Дать точные имена промежуточным этапам.
|
||||
3. Разбить pipeline на небольшие transformations.
|
||||
4. Добавить data-flow/sequence/state diagram.
|
||||
5. Зафиксировать invariants и ownership.
|
||||
6. Создать fixtures для edge cases.
|
||||
7. Объяснить, почему более простое решение недостаточно.
|
||||
|
||||
## Conditions and branching
|
||||
|
||||
- Предпочитать именованные predicates длинным boolean expressions.
|
||||
- Branch по capability/profile должен быть локальным и типизированным.
|
||||
- Не распространять `if core == "azerothcore"` по проекту; использовать adapter/capability.
|
||||
- Не смешивать Blizzlike и Enhanced branches в каждом shader/service: выбирать profile/strategy на boundary.
|
||||
- Pattern/table-driven mapping предпочтительнее сотен одинаковых `if`, если таблица остаётся читаемой и валидируемой.
|
||||
|
||||
## Comments
|
||||
|
||||
Комментарий объясняет `why`, invariant, source behavior или ограничение. Код и имя объясняют `what`.
|
||||
|
||||
Хорошо:
|
||||
|
||||
```gdscript
|
||||
# WoW M2 bone indices are relative to the skin-section bone palette,
|
||||
# not directly to the model-wide bone array.
|
||||
```
|
||||
|
||||
Плохо:
|
||||
|
||||
```gdscript
|
||||
# Increment index by one.
|
||||
index += 1
|
||||
```
|
||||
|
||||
TODO/FIXME обязан содержать target/work-package ID или diagnostic code.
|
||||
|
||||
## Error handling
|
||||
|
||||
- Ошибка содержит operation, subject и actionable context.
|
||||
- Не возвращать пустой Dictionary как неразличимое сочетание `not found`, `invalid` и `unsupported`.
|
||||
- Expected failure моделируется typed result/status; programming invariant может использовать assert в подходящем profile.
|
||||
- Не проглатывать ошибку ради продолжения renderer/editor: degraded behavior должен быть виден diagnostics.
|
||||
- Recovery path проще и предпочтительнее сложной попытки угадать состояние.
|
||||
|
||||
## Performance and KISS
|
||||
|
||||
- Не оптимизировать по предположению.
|
||||
- Сначала baseline/profile, затем smallest measured fix.
|
||||
- Batch/buffer/RenderingServer/native path вводится после доказательства bottleneck.
|
||||
- Оптимизированный path обязан иметь простой contract и reference tests.
|
||||
- Медленный понятный reference implementation полезен как oracle для optimized implementation.
|
||||
- Сложная оптимизация удаляется или упрощается, если она не даёт измеримого результата.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- Можно ли понять назначение сущности по имени?
|
||||
- Есть ли двусмысленные сокращения?
|
||||
- Выполняет ли функция одну операцию?
|
||||
- Можно ли убрать abstraction без потери correctness/boundary?
|
||||
- Есть ли скрытый mutable state или side effect?
|
||||
- Не появился ли второй источник истины?
|
||||
- Измерена ли сложная optimization?
|
||||
- Изолирована и документирована ли неизбежная сложность?
|
||||
- Соответствуют ли API docs и diagrams фактическому коду?
|
||||
|
||||
Если reviewer не может объяснить data flow модуля после чтения имён, public API и module spec, work package не готов к интеграции.
|
||||
@@ -91,6 +91,15 @@ Custom locations и mechanics могут требовать согласован
|
||||
|
||||
`ContentBundleManifest` связывает client и server artifacts общей версией. Клиент не входит в мир при несовместимой обязательной версии пакета без понятной диагностики.
|
||||
|
||||
## Navigation artifacts
|
||||
|
||||
Content Project хранит navigation profile и authoring hints, но не generated navmesh tiles. Build pipeline может создать:
|
||||
|
||||
- OpenWC navigation cache через pinned `recast-rs` для Editor preview и validation;
|
||||
- server mmap artifact через отдельный TrinityCore/AzerothCore-compatible exporter, когда его совместимость доказана.
|
||||
|
||||
Артефакты имеют разные type/version IDs и не подменяют друг друга. Manifest фиксирует source geometry hash, coordinate profile, backend/version, Recast parameters и tile checksums. Любое изменение этих входов инвалидирует cache.
|
||||
|
||||
## Overlay и модификации
|
||||
|
||||
Оригинальные извлечённые файлы не изменяются in-place. Пользовательские пакеты образуют ordered overlay:
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
# OpenWC Documentation Standard
|
||||
|
||||
## Цель
|
||||
|
||||
Документация является частью реализации. Код без актуального объяснения публичного API, данных, ownership и поведения считается незавершённым, даже если компилируется и проходит тесты.
|
||||
|
||||
Документация должна позволить человеку ответить:
|
||||
|
||||
- зачем существует модуль;
|
||||
- какие данные он получает и откуда;
|
||||
- что он создаёт или изменяет;
|
||||
- кто владеет состоянием и ресурсами;
|
||||
- какие зависимости допустимы;
|
||||
- что происходит по шагам;
|
||||
- как модуль завершается, отменяется и восстанавливается после ошибки;
|
||||
- как проверить fidelity, correctness и performance;
|
||||
- какие части реализованы, запланированы или известны как gap.
|
||||
|
||||
Правила именования, KISS и локализации неизбежной сложности определены в [`CODING_STANDARD.md`](CODING_STANDARD.md). Документация и ясные имена дополняют друг друга: комментарий не должен компенсировать неудачное имя.
|
||||
|
||||
## Уровни документации
|
||||
|
||||
### 1. Inline API documentation
|
||||
|
||||
Находится рядом с кодом и является источником истины для точной сигнатуры:
|
||||
|
||||
- публичные classes/interfaces;
|
||||
- public methods/functions;
|
||||
- signals/events;
|
||||
- exported properties/configuration;
|
||||
- commands, events и read models;
|
||||
- ownership/lifetime/threading requirements;
|
||||
- error/return semantics;
|
||||
- non-obvious invariants.
|
||||
|
||||
### 2. Module specification
|
||||
|
||||
Находится в `docs/modules/<module>.md` и объясняет назначение, архитектуру, data flow, state/lifecycle, extension points и verification. Шаблон: [`modules/TEMPLATE.md`](modules/TEMPLATE.md).
|
||||
|
||||
### 3. Data/schema documentation
|
||||
|
||||
Описывает persisted/wire/content formats: fields, types, units, optionality, version, migration, provenance и compatibility. Нельзя объяснять schema только примером JSON или SQL.
|
||||
|
||||
### 4. ADR
|
||||
|
||||
Фиксирует значимое решение, варианты и последствия. ADR не заменяет актуальное описание модуля: после решения module spec обновляется до текущего состояния.
|
||||
|
||||
### 5. Operational documentation
|
||||
|
||||
Команды build/test/import/bake/deploy/recovery, expected outputs и diagnostics. Команда без условий запуска и интерпретации результата недостаточна.
|
||||
|
||||
## Обязательная module specification
|
||||
|
||||
Новый самостоятельный модуль или существенно выделенная подсистема обязаны иметь документ со следующими разделами:
|
||||
|
||||
1. Metadata/status.
|
||||
2. Purpose и non-goals.
|
||||
3. Context и architecture boundaries.
|
||||
4. Public API.
|
||||
5. Inputs и outputs.
|
||||
6. Data flow diagram.
|
||||
7. State/lifecycle или sequence diagram, если применимо.
|
||||
8. Ownership, threading и resource lifetime.
|
||||
9. Errors, cancellation и recovery.
|
||||
10. Configuration/capabilities/profiles.
|
||||
11. Persistence/cache/migrations.
|
||||
12. Diagnostics и observability.
|
||||
13. Tests, fidelity evidence и performance budgets.
|
||||
14. Extension points.
|
||||
15. Known gaps.
|
||||
16. Source map.
|
||||
|
||||
`Status` принимает только:
|
||||
|
||||
- `Planned`;
|
||||
- `Prototype`;
|
||||
- `Partial`;
|
||||
- `Implemented`;
|
||||
- `Verified`;
|
||||
- `Deprecated`.
|
||||
|
||||
`Implemented` не означает fidelity; `Verified` требует evidence и ссылок на тесты.
|
||||
|
||||
## Обязательные схемы
|
||||
|
||||
### Data flow diagram
|
||||
|
||||
Обязательна для каждого module spec. Показывает реальные имена contracts и направление данных:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Source[ADT bytes] --> Parser[ADTLoader]
|
||||
Parser --> Data[AdtTileData]
|
||||
Data --> Planner[StreamingTargetPlanner]
|
||||
Planner --> Queue[RenderFinalizeCommand]
|
||||
Queue --> Renderer[TerrainRenderer]
|
||||
```
|
||||
|
||||
Диаграмма должна показывать:
|
||||
|
||||
- внешние источники;
|
||||
- входные contracts;
|
||||
- преобразования;
|
||||
- выходные contracts/side effects;
|
||||
- persistence/cache, если участвует;
|
||||
- запрещённое направление, если оно является важной границей.
|
||||
|
||||
### Sequence diagram
|
||||
|
||||
Обязательна, если операция пересекает два и более из следующих boundary:
|
||||
|
||||
- thread/job queue;
|
||||
- network;
|
||||
- gameplay;
|
||||
- renderer/UI/audio;
|
||||
- Editor;
|
||||
- server/database;
|
||||
- filesystem/build pipeline.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Net as WorldSession
|
||||
participant Game as WorldState
|
||||
participant View as EntityPresenter
|
||||
participant Render as WorldRenderFacade
|
||||
Net->>Game: EntityCreated
|
||||
Game-->>View: EntityReadModel change
|
||||
View->>Render: spawn_entity(descriptor)
|
||||
```
|
||||
|
||||
### State diagram
|
||||
|
||||
Обязательна для lifecycle/state machine с тремя и более состояниями, reconnect/retry или rollback:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Disconnected
|
||||
Disconnected --> Connecting
|
||||
Connecting --> Authenticated
|
||||
Connecting --> Failed
|
||||
Authenticated --> Disconnected
|
||||
Failed --> Connecting: retry
|
||||
```
|
||||
|
||||
### Dependency diagram
|
||||
|
||||
Обязательна при изменении архитектурной границы или модуле с тремя и более downstream consumers. Она показывает compile/runtime dependencies, а не случайное расположение Node в сцене.
|
||||
|
||||
## Public API documentation
|
||||
|
||||
Для каждого public symbol указываются:
|
||||
|
||||
- краткое назначение;
|
||||
- параметры с единицами и coordinate space;
|
||||
- return value или emitted event;
|
||||
- preconditions/postconditions;
|
||||
- ownership и mutability;
|
||||
- допустимый thread;
|
||||
- failure behavior;
|
||||
- profile/capability restrictions;
|
||||
- пример только если контракт остаётся неочевидным.
|
||||
|
||||
### GDScript
|
||||
|
||||
- Использовать `##` documentation comments для class, public methods, signals и exported properties.
|
||||
- Всегда указывать типы аргументов/return, где Godot API позволяет.
|
||||
- `@export` property объясняет units/range/profile и runtime mutability.
|
||||
- Signal документирует момент emission, arguments и ordering guarantees.
|
||||
- Internal helper получает комментарий, если содержит сложный invariant, coordinate conversion, ownership или performance assumption.
|
||||
|
||||
### C++/GDExtension
|
||||
|
||||
- Public headers используют Doxygen-compatible `///`/`/** */`.
|
||||
- Документировать ownership указателей/references/buffers, thread safety и error transport.
|
||||
- Binary structs указывают source format/build, packing/endianness и validation.
|
||||
- Native/Godot boundary описывает allocation и lifetime Variant/PackedArray/Resource.
|
||||
- Implementation comments объясняют `why` и invariant, а не пересказывают строку кода.
|
||||
|
||||
### Shaders/materials
|
||||
|
||||
- Uniform/global parameter: coordinate/color space, range, units и producer.
|
||||
- Material profile: supported WoW shader/blend modes и approximations.
|
||||
- Expensive branch/texture dependency имеет cost/fallback note.
|
||||
- Blizzlike и Enhanced behavior документируются отдельно.
|
||||
|
||||
### Network codecs
|
||||
|
||||
- Opcode/direction/session phase.
|
||||
- Exact build/profile.
|
||||
- Field order/type/endianness/conditional presence.
|
||||
- Domain events на выходе.
|
||||
- Malformed/unknown handling.
|
||||
- Fixture/reference source без sensitive payload.
|
||||
|
||||
### Content/server schemas
|
||||
|
||||
- Canonical field и mapping на каждый core adapter.
|
||||
- Unit, default, null/optional semantics и flags.
|
||||
- Numeric ID allocation и references.
|
||||
- Migration/rollback и unsupported degradation.
|
||||
|
||||
## Документация потока данных
|
||||
|
||||
Inputs/outputs оформляются таблицей:
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | `EntityMoved` | `WorldSession` | `WorldState` | Immutable event | Game-thread apply |
|
||||
| Output | `EntityReadModel` | `WorldState` | Presenter/UI | Snapshot | Until next revision |
|
||||
|
||||
Side effects перечисляются отдельно:
|
||||
|
||||
- scene tree mutation;
|
||||
- RenderingServer RID creation/free;
|
||||
- filesystem/cache write;
|
||||
- network send;
|
||||
- DB query/apply;
|
||||
- settings/SavedVariables mutation.
|
||||
|
||||
Скрытый side effect является дефектом документации и API.
|
||||
|
||||
## Статус реализации
|
||||
|
||||
Документ не должен выдавать roadmap за существующий код. Каждый важный capability помечается:
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
|
||||
После изменения кода статус и `Last verified` обновляются в том же work package. Если документацию нельзя подтвердить, ставится `Unknown`/`Planned`, а не предположение.
|
||||
|
||||
## Documentation change rules
|
||||
|
||||
Документация обновляется в том же work package, если изменились:
|
||||
|
||||
- public API/signal/event;
|
||||
- data flow или ownership;
|
||||
- state/lifecycle;
|
||||
- thread boundary;
|
||||
- format/schema/cache version;
|
||||
- configuration/capability;
|
||||
- diagnostics/error/recovery;
|
||||
- fidelity или performance behavior;
|
||||
- extension point.
|
||||
|
||||
Переименование symbol/path требует обновить source map, diagrams и ссылки. Stale documentation считается regression.
|
||||
|
||||
## Code comments
|
||||
|
||||
Хороший комментарий объясняет:
|
||||
|
||||
- почему решение отличается от очевидного;
|
||||
- форматное или protocol ограничение;
|
||||
- ownership/thread invariant;
|
||||
- причину batching/budget;
|
||||
- источник fidelity behavior;
|
||||
- временный compatibility path и условие удаления.
|
||||
|
||||
Плохой комментарий:
|
||||
|
||||
- пересказывает код;
|
||||
- обещает будущую работу без target/work package ID;
|
||||
- содержит устаревшую сигнатуру;
|
||||
- маскирует magic number без source/unit;
|
||||
- утверждает `Blizzlike` без evidence.
|
||||
|
||||
TODO/FIXME должен содержать target/work package или diagnostic code.
|
||||
|
||||
## Review gate
|
||||
|
||||
Reviewer/integrator проверяет:
|
||||
|
||||
- public symbols имеют inline docs;
|
||||
- module spec существует или обновлён;
|
||||
- input/output и side effects перечислены;
|
||||
- diagrams соответствуют реальным contracts;
|
||||
- state/error/recovery paths показаны;
|
||||
- tests/evidence и known gaps честны;
|
||||
- документы не дублируют противоречивые источники истины.
|
||||
|
||||
Если хотя бы один обязательный пункт отсутствует, handoff остаётся `PARTIAL`, а target не может получить `DONE`.
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
Graph validator обнаруживает циклы без намеренного repeat policy, недостижимые узлы, отсутствующие targets и dead ends.
|
||||
|
||||
Spatial validator на основе navigation backend (первоначально optional `recast-rs`) отдельно проверяет физическую достижимость giver, objectives и turn-in для заявленного movement profile. Результат является authoring diagnostic: он не заменяет server pathfinding и требует server-integrated playtest для окончательного подтверждения.
|
||||
|
||||
## Локации
|
||||
|
||||
Локация проектируется слоями:
|
||||
@@ -51,6 +53,8 @@ Graph validator обнаруживает циклы без намеренног
|
||||
|
||||
Редактор должен показывать heatmaps плотности spawn, quest travel paths, level/difficulty bands, line-of-sight blockers и приблизительную стоимость рендера.
|
||||
|
||||
Для подземелий navigation graph должен проверять связность входа, encounter rooms, дверей, recovery route и graveyard. Dynamic obstacles и off-mesh connections описываются явно, чтобы закрытая дверь или специальный переход не давали ложный положительный результат.
|
||||
|
||||
## Подземелья и encounters
|
||||
|
||||
`DungeonPackage` объединяет входы, difficulty, encounters, doors, triggers, trash groups, bosses, loot, quests, graveyards, scripts и localization.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Godot Best Practices for OpenWC
|
||||
|
||||
## Статус
|
||||
|
||||
Это нормативные правила применения Godot в OpenWC. Они дополняют общую архитектуру и основаны на официальных Godot practices с учётом большого streaming world, native parsers и встроенного Editor tooling.
|
||||
|
||||
## Nodes только на границах
|
||||
|
||||
- Domain, packet codecs, validators, coordinate math, build graph и canonical content не должны быть Nodes.
|
||||
- Node/Control используются для lifecycle, scene composition, input, rendering/audio presentation и Editor UI.
|
||||
- Большой объём однородных данных хранится в typed objects/packed arrays/resources, а не в тысячах служебных Nodes.
|
||||
- Scene не ищет глобально dependency по имени; parent composition передаёт contract/reference явно.
|
||||
|
||||
## Scene organization
|
||||
|
||||
- Каждая scene имеет одну ответственность и может запускаться в isolation с test doubles.
|
||||
- Parent-child означает ownership: удаление parent логично удаляет child. Иначе системы являются siblings/services.
|
||||
- Scene наружу публикует небольшой API/signals, а внутренние NodePaths не становятся публичным контрактом.
|
||||
- Runtime, render sandbox, preview и capture scenes собираются из общих presenters/services.
|
||||
- Generated world preview не сохраняется как authoring source.
|
||||
|
||||
## Dependency injection и signals
|
||||
|
||||
- Dependencies передаются composition root, initializer или exported interface reference.
|
||||
- Signals используются для notification; commands/intents вызываются через явные методы/bus contracts.
|
||||
- Signal connections снимаются симметрично при unload/plugin disable.
|
||||
- Не использовать SceneTree groups как скрытый service locator для core architecture.
|
||||
|
||||
## Autoload policy
|
||||
|
||||
Autoload разрешён только для действительно process-wide lifecycle:
|
||||
|
||||
- composition/bootstrap coordinator;
|
||||
- глобальная immutable build/version information;
|
||||
- crash/diagnostic sink, если ему нужен process lifetime.
|
||||
|
||||
Gameplay state, network session, renderer world, editor session, caches конкретного проекта и audio emitters не должны становиться неявными глобальными managers. Они создаются/освобождаются владельцем режима.
|
||||
|
||||
## Resources and data
|
||||
|
||||
- Resource подходит для immutable/imported descriptors и editor-inspectable configuration.
|
||||
- Runtime mutable authoritative state не хранится в shared Resource, который может быть случайно разделён сценами.
|
||||
- Resource schema имеет version и migration, если сохраняется пользователем.
|
||||
- Original extracted corpus и bulk caches не следует заставлять Godot массово импортировать без необходимости; применять `.gdignore`, custom import staging или native repository.
|
||||
- Importer проверяет malformed input и записывает dependency/profile metadata.
|
||||
|
||||
## Background work
|
||||
|
||||
- Blocking `load()` не используется в frame-critical streaming path; применяются threaded load/jobs.
|
||||
- Worker выполняет file I/O, parsing, decompression, grouping и pure data generation.
|
||||
- SceneTree mutation, Node attach и большинство GPU/RID finalization выполняются main thread budget.
|
||||
- Один Resource не модифицируется несколькими workers.
|
||||
- GDScript container resize/mutation между threads защищается mutex или заменяется message passing.
|
||||
- Все worker tasks ожидаются/очищаются; session cancellation не оставляет orphan work.
|
||||
|
||||
## Rendering performance
|
||||
|
||||
- Сначала измерять SceneTree/CPU/GPU bottleneck, затем переходить к RenderingServer.
|
||||
- Repeated static M2 группировать spatial MultiMesh cells; один MultiMesh на весь мир ухудшает culling.
|
||||
- Large transform buffers готовить линейно/в native code и отправлять батчем.
|
||||
- Visibility ranges/HLOD, occlusion и automatic mesh LOD использовать совместно по профилю.
|
||||
- Unique materials/textures минимизировать; descriptor/resource counts являются budget metric.
|
||||
- RenderingServer RIDs имеют явного владельца и освобождаются в deterministic shutdown test.
|
||||
- Shader/material profiles разделяют Blizzlike и Enhanced; runtime не компилирует тяжёлые варианты при пересечении ADT boundary.
|
||||
|
||||
## EditorPlugin lifecycle
|
||||
|
||||
- Любая регистрация в `_enter_tree()` имеет соответствующее удаление в `_exit_tree()`.
|
||||
- Docks, inspectors, importers, gizmos, debugger plugins, signals и jobs снимаются при disable/reload.
|
||||
- Editor UI не пишет файлы/SQL напрямую — только создаёт application commands.
|
||||
- Mutation интегрирована с EditorUndoRedoManager или общей command history.
|
||||
- Preview nodes считаются disposable; stable content IDs не равны instance IDs.
|
||||
- Долгая операция имеет progress, cancellation и staging output.
|
||||
|
||||
## Editor imports and inspectors
|
||||
|
||||
- Custom import plugin применяется для формата, которому действительно нужен first-class Godot Resource lifecycle.
|
||||
- Один extension/type имеет стабильный importer name и version.
|
||||
- Platform/profile variants фиксируются явно.
|
||||
- Custom inspectors редактируют canonical data через commands и показывают field-level validation.
|
||||
- Gizmos не владеют сущностью и создают одну undoable logical operation на drag/stroke.
|
||||
|
||||
## UI
|
||||
|
||||
- Control tree не читает network packets и не ищет world nodes.
|
||||
- View models immutable на frame/update boundary.
|
||||
- Layout использует anchors/containers/theme tokens, а не hardcoded pixel positions вне fidelity backend.
|
||||
- FrameXML compatibility layout изолирован от modern Godot-native screens.
|
||||
- Input consumption и focus/modal context тестируются сценами.
|
||||
|
||||
## Profiling and diagnostics
|
||||
|
||||
- Использовать Godot profiler для script/physics и Visual Profiler для render CPU/GPU.
|
||||
- Headless/CLI runs сохраняют JSON metrics; GPU runs фиксируют backend/device/driver/resolution.
|
||||
- Performance changes сравниваются cold/warm и p50/p95/p99, а не одним FPS.
|
||||
- GPU validation, memory tracking и RenderDoc workflow документируются для shader/backend defects.
|
||||
- Custom monitors публикуют streaming queues, cache hits, entities, materials, RIDs и job latency.
|
||||
|
||||
## Tests
|
||||
|
||||
- Pure logic тестируется без SceneTree.
|
||||
- Scene runner используется только для lifecycle/input/signals/layout/presentation.
|
||||
- EditorPlugin имеет enable/disable/reload и undo/save/reload tests.
|
||||
- Headless является обязательным CI path; visual tests запускаются отдельно на GPU runner.
|
||||
- GdUnit4 рассматривается как кандидат для GDScript/scene tests; native tests остаются отдельным target.
|
||||
|
||||
## Official references
|
||||
|
||||
- [Godot Best practices](https://docs.godotengine.org/en/stable/tutorials/best_practices/index.html)
|
||||
- [Scene organization](https://docs.godotengine.org/en/stable/tutorials/best_practices/scene_organization.html)
|
||||
- [Autoloads versus regular nodes](https://docs.godotengine.org/en/stable/tutorials/best_practices/autoloads_versus_regular_nodes.html)
|
||||
- [Background loading](https://docs.godotengine.org/en/stable/tutorials/io/background_loading.html)
|
||||
- [Thread-safe APIs](https://docs.godotengine.org/en/stable/tutorials/performance/thread_safe_apis.html)
|
||||
- [Optimization using MultiMeshes](https://docs.godotengine.org/en/stable/tutorials/performance/using_multimesh.html)
|
||||
- [RenderingServer](https://docs.godotengine.org/en/stable/classes/class_renderingserver.html)
|
||||
- [Import plugins](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/import_plugins.html)
|
||||
- [Inspector plugins](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/inspector_plugins.html)
|
||||
- [3D gizmo plugins](https://docs.godotengine.org/en/stable/tutorials/plugins/editor/3d_gizmos.html)
|
||||
- [Command line tutorial](https://docs.godotengine.org/en/stable/tutorials/editor/command_line_tutorial.html)
|
||||
- [Debugging tools](https://docs.godotengine.org/en/stable/tutorials/scripting/debug/overview_of_debugging_tools.html)
|
||||
@@ -27,6 +27,7 @@ Editor UI вызывает services, а не SQL, MPQ parsers или файло
|
||||
- Paths: waypoint, patrol, spline, taxi и transport preview.
|
||||
- Volumes: area, subzone, encounter, graveyard, phase и weather.
|
||||
- Visualization modes: collision, normals, chunk/tile bounds, nav/pathing, spawn density, quest dependencies и streaming budgets.
|
||||
- Navigation overlay: walkable areas, isolated regions, off-mesh connections, path cost и unreachable selection.
|
||||
|
||||
Каждый stroke или placement создаёт одну логическую undoable command. Длительный stroke агрегируется, чтобы history не содержала команду на каждую вершину.
|
||||
|
||||
@@ -66,6 +67,20 @@ Import, bake, validation, packaging и DB snapshot выполняются как
|
||||
- staging output и атомарная публикация результата;
|
||||
- повторяемая команда для headless CI.
|
||||
|
||||
Navmesh bake через `recast-rs` оформляется таким же cancellable job: Editor экспортирует каноническую triangle geometry во staging, запускает pinned CLI/backend, импортирует versioned navigation artifact и показывает diagnostics. Preview-ноды navmesh не сохраняются как источник истины.
|
||||
|
||||
## Navigation validation
|
||||
|
||||
Первый navigation backend — optional `recast-rs`. Он используется для:
|
||||
|
||||
- проверки пути от quest giver к objectives и turn-in;
|
||||
- поиска NPC/gameobject вне walkable polygon;
|
||||
- проверки patrol/escort paths;
|
||||
- проверки связности входов, дверей и encounter rooms;
|
||||
- визуализации area costs, holes и off-mesh connections.
|
||||
|
||||
Профиль bake хранит версию backend и параметры agent radius/height/climb, cell size/height, slope, region и tile size. Изменение профиля инвалидирует navigation cache. Отсутствие backend не должно мешать редактированию, но navigation-dependent validation помечается как `Not run`.
|
||||
|
||||
## Database dock
|
||||
|
||||
- Показывает тип ядра, revision, schema version и capabilities.
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
Исполняемая последовательность текущих целей находится в [`../targets/README.md`](../targets/README.md). Все агенты обязаны следовать корневому [`../AGENTS.md`](../AGENTS.md).
|
||||
|
||||
Полная инженерная карта проекта находится в [`../targets/DEVELOPMENT_ROADMAP.md`](../targets/DEVELOPMENT_ROADMAP.md); она раскрывает renderer, protocol, gameplay, UI/Lua/audio, Editor tooling, server emulators и quality/release workstreams.
|
||||
|
||||
## Статус документов
|
||||
|
||||
Документы описывают целевую архитектуру. Реализованное состояние рендера зафиксировано отдельно в [`../RENDER.md`](../RENDER.md). Если описание целевого решения расходится с кодом, это считается известным архитектурным долгом, а не разрешением обходить границы слоёв.
|
||||
@@ -22,6 +24,12 @@
|
||||
| [TESTING.md](TESTING.md) | Стратегия тестирования runtime, протокола, рендера и authoring pipeline |
|
||||
| [ROADMAP.md](ROADMAP.md) | Последовательность вертикальных срезов и критерии готовности |
|
||||
| [REFERENCES.md](REFERENCES.md) | Использованные локальные reference-проекты и правила работы с ними |
|
||||
| [TOOLING_CATALOG.md](TOOLING_CATALOG.md) | Реестр готовых библиотек, редакторов, test oracles и кандидатов на интеграцию |
|
||||
| [GODOT_BEST_PRACTICES.md](GODOT_BEST_PRACTICES.md) | Нормативные правила scenes, data, jobs, rendering, EditorPlugin, profiling и tests |
|
||||
| [TEAM_WORKFLOW.md](TEAM_WORKFLOW.md) | Ветки/worktree, task claims, ownership, handoff, integration и conflict protocol |
|
||||
| [DOCUMENTATION_STANDARD.md](DOCUMENTATION_STANDARD.md) | Обязательные API docs, module specs, data-flow/state/sequence diagrams и documentation gate |
|
||||
| [modules/README.md](modules/README.md) | Реестр фактических модулей и шаблон их спецификаций |
|
||||
| [CODING_STANDARD.md](CODING_STANDARD.md) | Явные длинные имена, KISS, простота функций/data flow и правила оправданной сложности |
|
||||
|
||||
## Обязательный workflow изменения функции
|
||||
|
||||
|
||||
@@ -41,6 +41,48 @@ Reference-код используется для исследования фор
|
||||
|
||||
Используем для сверки conversion pipeline, материалов, моделей и authoring/export edge cases. Runtime web/Blender architecture не переносится в OpenWC.
|
||||
|
||||
### Wowser
|
||||
|
||||
- [`wowserhq/wowser`](https://github.com/wowserhq/wowser) — MIT proof-of-concept браузерного клиента WoW 3.3.5a.
|
||||
- [`wowserhq/client`](https://github.com/wowserhq/client) — выделенный web client.
|
||||
- [`wowserhq/pipeline`](https://github.com/wowserhq/pipeline) — asset delivery pipeline.
|
||||
|
||||
Используем как независимый reference для auth → realm → character → world lifecycle, SRP/cryptography, binary packet handling, asset delivery и частичного terrain/model renderer. Особенно полезен для поиска уже разобранных 3.3.5a packet/data edge cases и идей test fixtures.
|
||||
|
||||
Не переносим напрямую старый browser stack, WebSocket proxy, React/Three.js abstractions или pipeline server. Заявленное поведение проверяем по TrinityCore/AzerothCore и оригинальному клиенту; proof-of-concept не является спецификацией полноты.
|
||||
|
||||
### recast-rs
|
||||
|
||||
- [`wowemulation-dev/recast-rs`](https://github.com/wowemulation-dev/recast-rs) — Rust-порт Recast/Detour: navmesh generation, tiled pathfinding, spatial queries, crowd simulation и dynamic obstacles.
|
||||
|
||||
Планируем использовать как optional offline navigation backend для Godot Editor: визуализация walkability, проверка достижимости quest/encounter targets, patrol paths и dungeon topology. Начальная интеграция выполняется через CLI/build job, а не через renderer или gameplay runtime.
|
||||
|
||||
Важно: принадлежность к WoW emulation ecosystem не доказывает совместимость с форматами TrinityCore/AzerothCore `.mmap/.mmtile`. Генерация server mmaps разрешается только после отдельного compatibility spike с exact parameter, coordinate, tile и binary/behavior comparison. До этого OpenWC navigation cache и server mmaps считаются разными артефактами.
|
||||
|
||||
### rilua
|
||||
|
||||
- [`wowemulation-dev/rilua`](https://github.com/wowemulation-dev/rilua) — написанная на Rust реализация Lua 5.1.1 с embedding API, bytecode, официальным Lua test corpus, fuzzing и oracle comparison с PUC-Rio.
|
||||
|
||||
Рассматриваем одновременно как кандидата на addon Lua runtime и как headless test oracle. TOC loader, FrameXML, WoW API, events, SavedVariables и secure execution остаются слоями OpenWC. Критический gap — отсутствие готовой WoW taint system; C ABI также намеренно не совместим с PUC-Rio. Production-решение принимается только после сравнения `rilua`, PUC-Rio 5.1.1 и оригинального клиента на WoW-specific corpus.
|
||||
|
||||
Подробные статусы и evaluation cards ведутся в [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md).
|
||||
|
||||
### Godot, GdUnit4 и server tooling
|
||||
|
||||
- [Godot best practices](https://docs.godotengine.org/en/stable/tutorials/best_practices/index.html) — основной источник engine-specific правил; принятые решения собраны в [`GODOT_BEST_PRACTICES.md`](GODOT_BEST_PRACTICES.md).
|
||||
- [`godot-gdunit-labs/gdUnit4`](https://github.com/godot-gdunit-labs/gdUnit4) — кандидат для GDScript/scene/Editor/CI тестов.
|
||||
- [`azerothcore/Keira3`](https://github.com/azerothcore/Keira3) — reference существующего редактора world DB и SQL generation.
|
||||
- [AzerothCore database and module documentation](https://www.azerothcore.org/wiki/documentation-index) — primary reference для AzerothCore adapter/deployment workflow.
|
||||
- [WowBench](https://sourceforge.net/projects/wowbench/) — исторический offline WoW XML/Lua API emulator и источник addon test ideas.
|
||||
|
||||
### Warcraft format ecosystem
|
||||
|
||||
- [`wowemulation-dev/warcraft-rs`](https://github.com/wowemulation-dev/warcraft-rs) — современный Rust CLI/library set для MPQ/DBC/BLP/ADT/WDT/WDL/M2/WMO и независимый кандидат на format oracle.
|
||||
- [`wowdev/WoWDBDefs`](https://github.com/wowdev/WoWDBDefs) — versioned definitions client database tables; полезны для schema generation и coverage.
|
||||
- [`gtker/wow_dbc`](https://github.com/gtker/wow_dbc) — независимый 1.12/2.4.3/3.3.5 DBC reader/writer и converter reference.
|
||||
|
||||
Используем для differential testing текущих native parsers и поиска coverage gaps. Новый Rust dependency не принимается только из-за более широкого списка поддерживаемых форматов: сначала требуются build-specific fixtures и измеренное преимущество.
|
||||
|
||||
## Правила исследования
|
||||
|
||||
1. Для protocol semantics приоритет имеют wire fixtures и соответствующая версия server core.
|
||||
@@ -49,6 +91,7 @@ Reference-код используется для исследования фор
|
||||
4. Любое заимствование проверяется по лицензии и документируется.
|
||||
5. Proprietary data не переносится в fixtures или репозиторий.
|
||||
6. Вывод, влияющий на публичный контракт, оформляется ADR или обновлением соответствующей спецификации.
|
||||
7. Перед собственной реализацией сложной подсистемы проверить [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md) и добавить найденные решения, даже если они останутся только reference.
|
||||
|
||||
## Замеченные полезные уроки
|
||||
|
||||
@@ -57,3 +100,5 @@ Reference-код используется для исследования фор
|
||||
- Coordinate conversion и ID allocation должны быть централизованы раньше world editing.
|
||||
- DB export без canonical model быстро связывает UI с одной версией ядра.
|
||||
- Screenshot smoke полезен, но должен дополняться deterministic state, performance и protocol tests.
|
||||
- Navmesh-библиотека полезна для authoring validation, но не должна становиться источником authoritative NPC movement на клиенте.
|
||||
- Готовая Lua VM сокращает объём работы только на уровне языка; WoW API, FrameXML, addon lifecycle, secure actions и taint всё равно требуют отдельной fidelity-программы.
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
# Renderer baseline M00
|
||||
|
||||
Этот документ описывает воспроизводимый baseline renderer перед архитектурной декомпозицией. Он не является заявлением о визуальной совместимости `1:1` с WoW 3.3.5a.
|
||||
|
||||
## Единый запуск
|
||||
|
||||
Из корня репозитория:
|
||||
|
||||
```powershell
|
||||
.\tools\run_render_baseline.ps1
|
||||
```
|
||||
|
||||
Скрипт последовательно проверяет headless-загрузку проекта, renderer materials, M2 unique-id dedupe, regression manifest и затем снимает checkpoint-ы. Для CI или быстрой проверки контракта без PNG:
|
||||
|
||||
```powershell
|
||||
.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1
|
||||
```
|
||||
|
||||
Если `godot` отсутствует в `PATH`, скрипт использует локальный executable, указанный в `RENDER.md`. Другой executable задаётся через `-GodotPath`.
|
||||
|
||||
Результат по умолчанию находится в `user://render_baseline`:
|
||||
|
||||
- `report.json` — Godot, OS, CPU, GPU/backend, viewport, revision, cache inventory, cache versions, параметры checkpoint-ов и результаты;
|
||||
- `<checkpoint>__cold_process.png` — первый визит в текущем процессе;
|
||||
- `<checkpoint>__warm_revisit.png` — повторный визит в том же процессе после обхода контрольных точек.
|
||||
|
||||
## Парное визуальное сравнение
|
||||
|
||||
Одобренные снимки оригинального клиента хранятся вне Git. Если каталог с ними доступен локально, единый запуск может сразу сравнить PNG с одинаковыми именами:
|
||||
|
||||
```powershell
|
||||
.\tools\run_render_baseline.ps1 `
|
||||
-ReferenceCheckpointDirectory 'D:\private-fixtures\wow-3.3.5a-checkpoints'
|
||||
```
|
||||
|
||||
Результат записывается в `user://render_baseline/visual_comparison.json`. Другой путь задаётся через `-VisualComparisonReport`. `-DryRun` несовместим с визуальным сравнением, потому что не создаёт PNG. Runner сохраняет обычный performance capture в нормативном `1280×900`, затем отдельно снимает visual candidates в `2560×1440`; разрешение можно переопределить через `-VisualComparisonWidth` и `-VisualComparisonHeight`, не меняя performance baseline.
|
||||
|
||||
Отдельный запуск сравнения:
|
||||
|
||||
```powershell
|
||||
godot --headless --path . `
|
||||
--script res://src/tools/compare_render_checkpoints.gd -- `
|
||||
--reference 'D:\private-fixtures\wow-3.3.5a-checkpoints' `
|
||||
--candidate "$env:APPDATA\Godot\app_userdata\OpenWC\render_baseline" `
|
||||
--output user://render_baseline/visual_comparison.json
|
||||
```
|
||||
|
||||
Reference-каталог принимает JPG/JPEG/PNG оригинального клиента с именем checkpoint, например `goldshire_dense_m2.jpg`. Диагностические кадры с префиксом `diagnostic_` игнорируются. Один reference автоматически сопоставляется с `<checkpoint>__cold_process.png` и `<checkpoint>__warm_revisit.png` OpenWC.
|
||||
|
||||
Сравнение переводит sRGB в linear color space, вычисляет взвешенную ошибку яркости/цвета для каждого пикселя, среднюю ошибку кадра и долю пикселей выше локального порога. Нормативные defaults находятся в `comparison_budgets` manifest: mean error `0.015`, changed-pixel ratio `0.01`, pixel error threshold `0.04`. Любое превышение, несовпадение размеров, отсутствие пары или пустой reference-каталог возвращает ненулевой exit code.
|
||||
|
||||
Автоматический pass не является доказательством parity: animation phase, weather, camera alignment и intentional gaps требуют human approval. Для проверки алгоритма без proprietary данных используется:
|
||||
|
||||
```powershell
|
||||
godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd -- --self-test
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
R[Private approved reference PNGs] --> C[Checkpoint comparator]
|
||||
N[New baseline PNGs] --> C
|
||||
M[Manifest tolerance defaults] --> C
|
||||
C --> J[JSON metrics and pass/fail]
|
||||
J --> H[Human fidelity approval]
|
||||
```
|
||||
|
||||
`cold_process` не означает очищенный Windows filesystem cache. Поле `cache_state` и полный inventory обязаны интерпретироваться вместе с результатом. Удаление или принудительная пересборка cache не входит в baseline-команду. Для отдельного запуска после контролируемой очистки безопасного локального cache следует передать осмысленную метку, например `-CacheState rebuilt-clean`; proprietary source assets команда не изменяет.
|
||||
|
||||
## Контрольные точки и детерминизм
|
||||
|
||||
Нормативный manifest: `res://src/tools/render_baseline_manifest.json`. Он не содержит proprietary assets и фиксирует семь обязательных классов:
|
||||
|
||||
- Elwynn terrain overview;
|
||||
- граница ADT около world center;
|
||||
- dense M2 в Goldshire;
|
||||
- Goldshire Inn как large WMO;
|
||||
- Elwynn waterfall как liquid;
|
||||
- синтетический native-animation probe `GryphonRoost01.m2`;
|
||||
- тот же Elwynn overview в 19:00 для sky transition.
|
||||
|
||||
Зафиксированы camera/target/player position, viewport, FOV, quality preset и время мира. Для temporal shader/animation кадров SHA-256 служит идентификатором артефакта, но не самостоятельным pass-критерием: автоматическая визуальная проверка использует perceptual diff и согласованный tolerance. Синтетический animation probe существует только внутри capture tool и не меняет runtime streaming scene.
|
||||
|
||||
## Метрики и budget
|
||||
|
||||
Каждая точка записывает load/wait time, frame time p50/p95/p99, максимальный frame hitch, static/video memory, draw calls, rendered objects и снимок streaming queues. При сравнении следующего renderer с этим baseline p95, p99, max hitch, load time и memory не должны ухудшаться более чем на 10% без отдельного принятого объяснения и обновления baseline.
|
||||
|
||||
Измерение считается неполным, если snapshot показывает незавершённые очереди: это состояние остаётся в `report.json` и не должно скрываться. Значения сравниваются только на одинаковых Godot, backend, hardware/profile, cache state, viewport и manifest revision.
|
||||
|
||||
## Cache contract
|
||||
|
||||
Manifest фиксирует проверяемые версии и причины invalidation:
|
||||
|
||||
| Cache | Version | Invalidate when |
|
||||
|---|---:|---|
|
||||
| baked terrain | 5 | изменились baked geometry или placement payload |
|
||||
| streaming terrain | 2 | изменились streaming geometry или M2 `unique_id` payload |
|
||||
| terrain splat | 1 | изменился splat resource payload |
|
||||
| terrain control splat | 3 | изменились control atlas, layer map или texture array |
|
||||
| WMO streaming | 2 | изменилась WMO render geometry |
|
||||
| WMO builder | 2 | изменились scene transform или builder payload |
|
||||
| M2 material | 2 | изменились material или custom vertex payload |
|
||||
|
||||
`verify_render_baseline_manifest.gd` сравнивает таблицу с runtime constants. Shader-only refresh versions, не изменяющие serialized payload, обновляются по правилам `RENDER.md`; изменение payload требует version bump и rebake.
|
||||
|
||||
## Fidelity gaps относительно 3.3.5a
|
||||
|
||||
Baseline пока не имеет approved парных кадров оригинального клиента build 12340. До их появления нельзя утверждать parity. Известны следующие классы расхождений:
|
||||
|
||||
- неполные M2 animation/skinning, particles и ribbons;
|
||||
- приближённые M2/WMO material combiners и pass ordering;
|
||||
- отсутствие настоящего WMO portal/room culling;
|
||||
- упрощённые liquid depth/shore rules и texture selection;
|
||||
- неполная проверка skybox/lighting transition по зонам;
|
||||
- возможная смена D3D12 на Vulkan при ошибке descriptor heap — фактический backend всегда берётся из `report.json`.
|
||||
|
||||
Парное сравнение с клиентом 3.3.5a должно использовать те же map position, local time и weather. Каждый gap регистрируется как terrain/material, placement, animation, lighting, liquid или culling; baseline сам по себе gap не закрывает.
|
||||
|
||||
Локальная сессия build 12340 от 2026-07-11 откалибровала три непригодные исходные позиции: dense-M2 camera была полностью закрыта кронами, large-WMO camera находилась у дымохода кузни, liquid camera попадала под terrain оригинального клиента. Manifest теперь содержит проверенные replacement camera positions. Пять approved локальных reference JPG хранятся вне Git; synthetic animated probe и dusk checkpoint всё ещё требуют отдельной процедуры.
|
||||
|
||||
Первый paired run после калибровки создал десять сравнений (пять reference × cold/warm), без missing pairs. Все пары ожидаемо превысили строгий tolerance: mean perceptual error `0.0707..0.1746`, changed-pixel ratio `0.5504..0.8187`. Human inspection показал, что это пока не чистая material/lighting ошибка: при тех же WoW-derived координатах OpenWC terrain-overview camera находится под terrain/placements, WMO camera — внутри таверны, liquid camera — внутри скал; ADT и dense-M2 композиции также существенно смещены. До исправления coordinate/placement mismatch эти значения являются gap evidence, а не основанием расширять tolerance.
|
||||
|
||||
Capture tool строит camera basis явно из target и world-up. Это исключает неоднозначный roll `look_at` при автоматической съёмке. `ViewportTexture.get_image()` сохраняется без дополнительного vertical flip для Godot 4.6.1.
|
||||
|
||||
## Coordinate calibration
|
||||
|
||||
Пять принятых build 12340 viewpoints записаны в manifest как `reference_wow_camera`. Они содержат только числовые world coordinates и не включают proprietary данные. Headless probe:
|
||||
|
||||
```powershell
|
||||
godot --headless --path . --script res://src/tools/verify_render_coordinate_calibration.gd
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
O[Observed build 12340 WoW XYZ] --> W[WoW to Godot formula]
|
||||
W --> G[Manifest Godot camera XYZ]
|
||||
G --> R[Godot to WoW round-trip]
|
||||
R --> E[Maximum numeric error]
|
||||
```
|
||||
|
||||
На пяти точках maximum mapping/round-trip error равен `0.000015`. Это исключает текущую формулу `gx = center - wy`, `gy = wz`, `gz = center - wx` как источник крупного paired mismatch. Результат не доказывает renderer parity: следующая диагностика должна отдельно проверить terrain height, placement transforms и фактическое camera direction/FOV. Production `CoordinateMapper` остаётся задачей M01; M00 probe не создаёт второй публичный coordinate contract.
|
||||
|
||||
## Terrain height diagnostic
|
||||
|
||||
Rendered terrain проверяется без нового runtime API: offline probe использует уже загруженный tile mesh, строит `TriangleMesh` и выполняет вертикальный ray в tile-local space.
|
||||
|
||||
```powershell
|
||||
godot --headless --path . --script res://src/tools/probe_render_terrain_height.gd -- --wait 2
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C[Calibrated camera XZ] --> S[Streaming tile state]
|
||||
S --> M[Active terrain mesh]
|
||||
M --> T[Tile-local TriangleMesh ray]
|
||||
T --> H[Terrain height and camera clearance]
|
||||
```
|
||||
|
||||
Измеренный clearance: terrain overview `89.044`, ADT boundary `44.788`, dense M2 `90.178`, large WMO `12.034`, waterfall примерно `76.128` Godot units. Следовательно, все пять камер находятся над rendered terrain; visual obstruction принадлежит placements/WMO/composition, а не terrain height.
|
||||
|
||||
Waterfall XZ сначала давал `no_intersection`, хотя tile `30_49` был available, полностью загружен, имел `control_splat_cache` quality mesh и LOD0 mesh, а probe находился внутри mesh AABB. Ray со смещением `2.0` units пересёк тот же mesh на высоте `113.872`; точная XZ попала на triangle seam/edge numerical miss. Probe теперь сообщает `sampled_nearby`, distance и source tile вместо ложного streaming ownership gap. `--require-all` остаётся строгим режимом для действительно неснятых точек.
|
||||
|
||||
## Camera occluder diagnostic
|
||||
|
||||
Scene-tree placement composition проверяется transformed AABB без изменения renderer:
|
||||
|
||||
```powershell
|
||||
godot --headless --path . --script res://src/tools/probe_render_camera_occluders.gd -- --wait 3
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
C[Calibrated camera] --> A[Published Mesh/MultiMesh AABBs]
|
||||
T[Manifest target] --> S[Camera-to-target segment]
|
||||
A --> P[Camera containment test]
|
||||
A --> I[Segment intersection test]
|
||||
S --> I
|
||||
P --> J[JSON occluder report]
|
||||
I --> J
|
||||
```
|
||||
|
||||
Ни одна из пяти камер не находится внутри опубликованной scene-tree geometry. Terrain-overview segment пересекает четыре Stormwind WMO groups, large-WMO segment — три Goldshire Inn groups, waterfall segment — liquid surface; ADT boundary и dense-M2 segments не пересекают placement AABB. Поэтому прежнее визуальное впечатление «камера внутри WMO/placements» не подтверждается. Основной paired gap сейчас — неточно воспроизведённые manual look direction/target/FOV reference-кадров: например, автоматический Goldshire target направляет луч через фасад внутрь WMO, тогда как reference был вручную скадрирован на весь фасад. Probe охватывает только scene-tree MeshInstance/MultiMesh; RID-only instances не имеют доступного semantic path и явно исключены из coverage.
|
||||
|
||||
## Empirical FOV sweep
|
||||
|
||||
В build 12340 `GetCVar("cameraFoV")` возвращает `nil`, а `/console cameraFoV` и `ConsoleExec("cameraFoV")` не выдают значения. Поэтому capture tool поддерживает additive `--camera-fov`, а comparator — `--only`, позволяющий ограничить sweep одним reference checkpoint.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
F[Candidate vertical FOV] --> C[Dedicated checkpoint camera]
|
||||
C --> P[Filtered checkpoint PNGs]
|
||||
R[One original-client reference] --> D[Perceptual comparator --only]
|
||||
P --> D
|
||||
D --> M[Ranked metrics]
|
||||
```
|
||||
|
||||
Первый sweep обнаружил capture defect: разные FOV иногда создавали одинаковые hashes, потому что scene camera могла перехватить viewport после входа в tree. Capture теперь вызывает `camera.make_current()` после добавления world и перед каждым checkpoint. После исправления ADT-boundary ranking стал: `26° → 0.079588`, `38° → 0.079633`, `50° → 0.084353`, `62° → 0.088360`, `86° → 0.097993` mean error. Plateau `26–38°` и несовпавший changed-pixel optimum показывают доминирование manual look direction/framing; эти данные не обосновывают изменение нормативного manifest FOV `62°`. Для настоящей калибровки reference capture должен сохранять воспроизводимые yaw/pitch/zoom или независимый projection fixture.
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> Этот документ объясняет стратегию на высоком уровне. Исполняемый порядок, текущий статус, checklist и доказательства завершения ведутся только в [`../targets/README.md`](../targets/README.md). При расхождении `targets/` имеет приоритет.
|
||||
|
||||
Полный roadmap с точки зрения разработки, contracts, subsystem dependencies и gates находится в [`../targets/DEVELOPMENT_ROADMAP.md`](../targets/DEVELOPMENT_ROADMAP.md).
|
||||
|
||||
## Стратегия
|
||||
|
||||
Работа идёт вертикальными срезами. Каждый срез включает data, domain, adapter, presentation, editor support, diagnostics и tests. Горизонтальная реализация всех opcodes или всех editor-панелей без пользовательского результата не считается milestone.
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
# Командная разработка OpenWC
|
||||
|
||||
## Цель
|
||||
|
||||
Этот workflow позволяет нескольким людям и Codex-агентам работать параллельно, не перезаписывая изменения друг друга и не разрушая архитектурные границы. Единица параллельной работы — не milestone целиком, а ограниченный `work package`.
|
||||
|
||||
## Основные правила
|
||||
|
||||
1. Каждый агент работает в отдельном clone или Git worktree и собственной ветке.
|
||||
2. Никто не разрабатывает напрямую в `master`.
|
||||
3. До изменений задача получает ID, владельца, claim и список затрагиваемых путей.
|
||||
4. Один work package имеет только одного активного владельца.
|
||||
5. Общие contracts согласуются до параллельной реализации producers/consumers.
|
||||
6. Статус milestone и `OPENWC_TARGET_DONE` изменяет только интегратор.
|
||||
7. Merge выполняется только после handoff, локальных проверок и синхронизации с актуальным `master`.
|
||||
|
||||
## Идентификаторы
|
||||
|
||||
### Agent ID
|
||||
|
||||
Каждый человек/агент выбирает стабильный ID на весь work package:
|
||||
|
||||
```text
|
||||
<human>-<machine>-codex
|
||||
```
|
||||
|
||||
Примеры:
|
||||
|
||||
```text
|
||||
sindo-main-codex
|
||||
alex-laptop-codex
|
||||
```
|
||||
|
||||
ID записывается в claim, ветке, PR/MR и handoff. Нельзя использовать просто `codex`, потому что таких агентов несколько.
|
||||
|
||||
### Work Package ID
|
||||
|
||||
```text
|
||||
<TARGET>-<PROGRAM>-<SHORT-NAME>-<NNN>
|
||||
```
|
||||
|
||||
Примеры:
|
||||
|
||||
```text
|
||||
M01-FND-COORDS-001
|
||||
M03-RND-SCHEDULER-001
|
||||
M08-NET-SRP-001
|
||||
M12-UIA-LUA-SPIKE-001
|
||||
```
|
||||
|
||||
Program codes определены в [`../targets/DEVELOPMENT_ROADMAP.md`](../targets/DEVELOPMENT_ROADMAP.md).
|
||||
|
||||
## Claim protocol
|
||||
|
||||
Предпочтительный канал координации — issue + ранний draft PR/MR на общем Git forge. Если forge недоступен, используется отдельный файл в `coordination/claims/`, который сначала публикуется в общей ветке.
|
||||
|
||||
Claim marker:
|
||||
|
||||
```text
|
||||
<!-- OPENWC_CLAIM:M01-FND-COORDS-001:sindo-main-codex:2026-07-13 -->
|
||||
```
|
||||
|
||||
Последнее поле — дата истечения claim в UTC. Рекомендуемый срок — 48 часов; владелец продлевает его коротким status update. Истёкший claim нельзя забирать молча: сначала оставить сообщение владельцу и интегратору.
|
||||
|
||||
Claim содержит:
|
||||
|
||||
- target и work package;
|
||||
- owner/agent ID;
|
||||
- outcome и non-goals;
|
||||
- exclusive paths;
|
||||
- shared/hotspot paths;
|
||||
- contracts/schema/format changes;
|
||||
- dependencies и blockers;
|
||||
- verification commands;
|
||||
- expected fidelity evidence.
|
||||
- documentation deliverables: inline API, module spec, diagrams и source map.
|
||||
|
||||
## Ветки и worktrees
|
||||
|
||||
Имя ветки:
|
||||
|
||||
```text
|
||||
work/<agent-id>/<target>-<short-name>
|
||||
```
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
work/sindo-main-codex/m01-coordinate-mapper
|
||||
```
|
||||
|
||||
Начало работы:
|
||||
|
||||
```powershell
|
||||
git fetch origin
|
||||
git worktree add ..\open-wc-m01 -b work/sindo-main-codex/m01-coordinate-mapper origin/master
|
||||
```
|
||||
|
||||
Правила:
|
||||
|
||||
- один worktree принадлежит одному агенту;
|
||||
- не запускать двух агентов в одном dirty worktree;
|
||||
- не менять и не удалять чужой worktree;
|
||||
- generated caches и proprietary data не переносятся commit/cherry-pick;
|
||||
- перед handoff ветка синхронизируется с актуальным `origin/master`.
|
||||
|
||||
## Разделение задач
|
||||
|
||||
Хороший work package:
|
||||
|
||||
- даёт один наблюдаемый результат;
|
||||
- принадлежит преимущественно одной подсистеме;
|
||||
- имеет явные входы/выходы;
|
||||
- меняет не более одного публичного contract одновременно;
|
||||
- имеет собственные tests/diagnostics;
|
||||
- может быть review и merged независимо либо после одной явно указанной dependency.
|
||||
|
||||
Плохое разделение:
|
||||
|
||||
- «сделать сеть»;
|
||||
- один агент меняет protocol и UI напрямую;
|
||||
- два агента одновременно рефакторят один monolithic file;
|
||||
- задача зависит от незаписанного устного соглашения;
|
||||
- work package оставляет временный второй источник истины.
|
||||
|
||||
## Parallelization patterns
|
||||
|
||||
### Contract first
|
||||
|
||||
Для cross-layer feature сначала отдельным маленьким work package фиксируются:
|
||||
|
||||
- domain types;
|
||||
- commands/events/read models;
|
||||
- version/capabilities;
|
||||
- failure states;
|
||||
- contract tests.
|
||||
|
||||
После merge независимо работают producer и consumers.
|
||||
|
||||
Пример:
|
||||
|
||||
```text
|
||||
WP-A: MovementSnapshot contract
|
||||
├─ WP-B: network movement decoder
|
||||
├─ WP-C: gameplay reconciliation
|
||||
└─ WP-D: character/camera presentation
|
||||
```
|
||||
|
||||
### Fixtures in parallel
|
||||
|
||||
Пока один агент реализует pure logic, второй может готовить:
|
||||
|
||||
- synthetic fixtures;
|
||||
- original-client comparison procedure;
|
||||
- server-core matrix;
|
||||
- visual checkpoints;
|
||||
- documentation/reference analysis.
|
||||
|
||||
Fixture agent не изменяет production contract без согласования.
|
||||
|
||||
### Adapter split
|
||||
|
||||
После merge canonical interface допустимо параллельно реализовать TrinityCore и AzerothCore adapters, если у каждого разные файлы и общая contract suite.
|
||||
|
||||
### Hotspot extraction first
|
||||
|
||||
Если работа требует одного большого файла, сначала один владелец извлекает стабильный seam. Параллельная работа начинается только после merge seam, а не внутри разных участков монолита.
|
||||
|
||||
## Hotspot-файлы
|
||||
|
||||
Следующие пути требуют явного указания в claim и согласования с интегратором:
|
||||
|
||||
- `project.godot`;
|
||||
- `AGENTS.md`, `docs/README.md`, `targets/README.md`;
|
||||
- текущий `targets/[0-9][0-9]-*.md`;
|
||||
- `RENDER.md`;
|
||||
- `src/scenes/streaming/streaming_world_loader.gd` до декомпозиции;
|
||||
- native registration/CMake/GDExtension manifests;
|
||||
- editor `plugin.cfg` и root plugin lifecycle;
|
||||
- shared schema/version/format constants;
|
||||
- dependency lockfiles и submodules.
|
||||
|
||||
Два активных work packages не должны редактировать один hotspot. При необходимости интегратор выделяет serial integration window.
|
||||
|
||||
## Изменение contracts и форматов
|
||||
|
||||
Contract/schema change обязан иметь:
|
||||
|
||||
- owner и потребителей;
|
||||
- backward compatibility или migration;
|
||||
- old/new fixtures;
|
||||
- version bump policy;
|
||||
- rollout order;
|
||||
- ADR для существенного решения.
|
||||
|
||||
Merge order:
|
||||
|
||||
1. contract + compatibility layer;
|
||||
2. producers;
|
||||
3. consumers;
|
||||
4. removal deprecated path отдельным work package.
|
||||
|
||||
Нельзя одновременно удалить старый API и ожидать, что незамердженная ветка другого агента уже использует новый.
|
||||
|
||||
## Commits
|
||||
|
||||
Формат заголовка:
|
||||
|
||||
```text
|
||||
<program>(<target>): <imperative summary>
|
||||
```
|
||||
|
||||
Примеры:
|
||||
|
||||
```text
|
||||
fnd(M01): add canonical coordinate mapper
|
||||
net(M08): decode auth challenge safely
|
||||
rnd(M03): extract streaming target planner
|
||||
test(M00): add paired checkpoint manifest
|
||||
```
|
||||
|
||||
Описание commit/PR содержит:
|
||||
|
||||
```text
|
||||
Work-Package: M01-FND-COORDS-001
|
||||
Agent: sindo-main-codex
|
||||
Tests: <commands/results>
|
||||
Fidelity: <evidence/gap>
|
||||
```
|
||||
|
||||
Не смешивать formatting unrelated files, generated artifacts и функциональное изменение в одном commit.
|
||||
|
||||
## Handoff protocol
|
||||
|
||||
Готовый work package получает marker:
|
||||
|
||||
```text
|
||||
<!-- OPENWC_HANDOFF:READY:M01-FND-COORDS-001:<commit> -->
|
||||
```
|
||||
|
||||
Handoff обязан содержать:
|
||||
|
||||
- точный commit/branch;
|
||||
- outcome и изменённые paths;
|
||||
- публичные contracts/schema changes;
|
||||
- verification commands и результаты;
|
||||
- fidelity evidence/known gaps;
|
||||
- migration/cache/rebuild requirements;
|
||||
- unresolved risks;
|
||||
- recommended merge order;
|
||||
- updated API/module documentation и перечень diagrams.
|
||||
|
||||
Если работа частичная:
|
||||
|
||||
```text
|
||||
<!-- OPENWC_HANDOFF:PARTIAL:M01-FND-COORDS-001:<commit> -->
|
||||
```
|
||||
|
||||
`PARTIAL` не считается завершением и не разрешает ставить target `DONE`.
|
||||
|
||||
## Integration protocol
|
||||
|
||||
Интегратор:
|
||||
|
||||
1. Проверяет claim и отсутствие overlapping ownership.
|
||||
2. Проверяет diff на unrelated/user changes.
|
||||
3. Синхронизирует branch с `master` без уничтожения чужой истории.
|
||||
4. Запускает package verification и затронутые общие gates.
|
||||
5. Проверяет docs, migrations, cache versions и fidelity statement.
|
||||
6. Проверяет documentation gate из `docs/DOCUMENTATION_STANDARD.md`: API docs, inputs/outputs, diagrams, ownership и recovery.
|
||||
7. Проверяет coding gate из `docs/CODING_STANDARD.md`: ясные имена, KISS, отсутствие premature abstractions и скрытого data flow.
|
||||
8. Merge/squash выполняет в соответствии с историей work package.
|
||||
9. После merge запускает post-merge smoke.
|
||||
10. Закрывает claim marker-ом:
|
||||
|
||||
```text
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M01-FND-COORDS-001:<merge-commit> -->
|
||||
```
|
||||
|
||||
Только после интеграции всех обязательных work packages интегратор обновляет target Evidence/status.
|
||||
|
||||
## Конфликты
|
||||
|
||||
При обнаружении overlapping edits агент должен остановить изменение затронутого файла и связаться с владельцем. Запрещено:
|
||||
|
||||
- разрешать конфликт через слепое `ours/theirs`;
|
||||
- копировать чужой файл поверх своего;
|
||||
- делать `reset --hard` или checkout чужих изменений;
|
||||
- «временно» дублировать contract без migration plan.
|
||||
|
||||
Порядок разрешения:
|
||||
|
||||
1. определить semantic owner;
|
||||
2. выбрать merge order;
|
||||
3. первый владелец публикует минимальный contract/seam;
|
||||
4. второй rebase и адаптирует свою ветку;
|
||||
5. integration tests подтверждают совместимость.
|
||||
|
||||
## Status updates и stale work
|
||||
|
||||
Владелец обновляет claim после значимого результата или не реже срока lease. Status краткий:
|
||||
|
||||
```text
|
||||
State: active | waiting | ready | partial
|
||||
Done: ...
|
||||
Next: ...
|
||||
Blocked by: ...
|
||||
Touched paths: ...
|
||||
```
|
||||
|
||||
Если работа прекращена, claim явно освобождается. Незавершённую ветку не удаляют до handoff с описанием пригодных commits и известных проблем.
|
||||
|
||||
## Минимальная схема для двух разработчиков
|
||||
|
||||
- Один человек назначается интегратором текущего milestone.
|
||||
- Второй берёт work package без пересечения файлов либо готовит fixtures/reference/adapter.
|
||||
- Роль интегратора можно менять после каждого milestone.
|
||||
- Перед началом дня оба выполняют `git fetch` и сверяют claims/draft PRs.
|
||||
- Перед merge второй агент не начинает работу поверх непроверенного commit, если dependency не опубликована как contract branch.
|
||||
|
||||
Пример для M01:
|
||||
|
||||
```text
|
||||
Developer A / integrator:
|
||||
M01-FND-COORDS-001 — coordinate types and mapper
|
||||
|
||||
Developer B:
|
||||
M01-QAR-COORD-FIXTURES-001 — known ADT/server points and tests
|
||||
|
||||
После merge contract:
|
||||
A — StreamingFocus migration
|
||||
B — placement/SQL conversion audit
|
||||
```
|
||||
|
||||
## Автоматическая проверка
|
||||
|
||||
Перед handoff и merge запускать:
|
||||
|
||||
```powershell
|
||||
.\tools\verify_coordination.ps1
|
||||
.\tools\verify_documentation.ps1
|
||||
```
|
||||
|
||||
Первая проверка требует ровно один `ACTIVE` numbered target, соответствие `Current target`, валидные и уникальные fallback claim markers. Вторая проверяет обязательные documentation files, структуру module specs, наличие data-flow diagram и относительные ссылки. Истёкший claim выдаёт warning и требует человеческого решения, но не удаляется автоматически.
|
||||
@@ -23,6 +23,9 @@
|
||||
- adapter round-trip `canonical → SQL → snapshot → canonical`;
|
||||
- Content Project compatibility и migration fixtures;
|
||||
- renderer/editor public API contracts.
|
||||
- `recast-rs` navigation fixtures: geometry/profile → region/poly/detail counts и path queries.
|
||||
- Lua VM contract: общий WoW-specific corpus выполняется на `rilua`, PUC-Rio 5.1.1 и, где возможно, оригинальном клиенте 3.3.5a.
|
||||
- Wowser используется как дополнительный независимый источник auth/world packet cases; fixtures принимаются только после проверки build 12340 semantics и удаления credentials/proprietary payload data.
|
||||
|
||||
### Integration
|
||||
|
||||
@@ -49,6 +52,25 @@
|
||||
- frame time, hitch percentiles, draw calls, memory, descriptor count и streaming queues;
|
||||
- long traversal через ADT boundaries;
|
||||
- dense WMO/M2, water, character equipment и UI scale matrices.
|
||||
- navmesh overlay checkpoints и bake/query budgets для больших tiles/dungeons.
|
||||
|
||||
### Navigation compatibility
|
||||
|
||||
- Golden synthetic meshes проверяют slope, climb, radius erosion, holes, tiled seams и off-mesh connections.
|
||||
- WoW fixtures проверяют canonical coordinates и ADT/WMO collision composition.
|
||||
- `recast-rs` version и bake profile закрепляются в manifest; обновление требует baseline diff.
|
||||
- OpenWC navigation cache сравнивается с C++ Recast reference на выбранных fixtures.
|
||||
- Заявление о server mmap support требует отдельного сравнения с TrinityCore/AzerothCore generator: параметры, tile bounds, polygon queries и фактическое движение существ на disposable core.
|
||||
- Editor reachability diagnostics должны различать `Reachable`, `Unreachable`, `Unknown` и `Not run`.
|
||||
|
||||
### Lua и addon compatibility
|
||||
|
||||
- Official Lua 5.1.1 suite запускается на pinned VM revision.
|
||||
- Oracle tests сравнивают results, errors, byte strings, number formatting, coroutines, metatables, GC-visible behavior и bytecode expectations.
|
||||
- WoW corpus проверяет restricted stdlib, global aliases, `bit`, BOM, GC defaults и запрещённые filesystem/native-module операции.
|
||||
- TOC/load order, FrameXML inheritance, events, SavedVariables и addon errors проверяются отдельно от VM.
|
||||
- Secure actions, combat lockdown и taint propagation имеют обязательные negative tests; без них addon compatibility не получает статус `Verified`.
|
||||
- Performance suite измеряет `OnUpdate`, event fan-out, table churn, allocation и incremental GC frame budget.
|
||||
|
||||
### Exploratory и game design
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# Каталог готовых инструментов и reference-решений
|
||||
|
||||
## Назначение
|
||||
|
||||
OpenWC следует принципу `research before build`: перед реализацией сложной подсистемы сначала ищется существующая библиотека, эмулятор, редактор, specification или test corpus. Цель — быстрее находить проверенные решения, сравнивать независимые реализации и не повторять уже проделанную работу.
|
||||
|
||||
Каталог не является списком автоматически одобренных dependencies. Любой кандидат проходит проверку лицензии, зрелости, fidelity, безопасности, производительности, поддержки платформ и стоимости интеграции.
|
||||
|
||||
## Статусы
|
||||
|
||||
- `REFERENCE` — источник идей, поведения, форматов или тестов; код не подключён.
|
||||
- `CANDIDATE` — возможная dependency; требуется compatibility spike.
|
||||
- `EVALUATING` — выполняется spike с зафиксированными критериями.
|
||||
- `ADOPTED` — закреплены версия, boundary, лицензия, tests и update policy.
|
||||
- `REJECTED` — не подходит; причина сохраняется, чтобы не повторять исследование.
|
||||
- `REPLACE` — используется временно и имеет согласованный план замены.
|
||||
|
||||
## Обязательная карточка кандидата
|
||||
|
||||
```text
|
||||
Name / URL:
|
||||
Status:
|
||||
Problem solved:
|
||||
Planned boundary:
|
||||
License:
|
||||
Platforms/toolchain:
|
||||
Fidelity evidence:
|
||||
Known gaps:
|
||||
Security/data risks:
|
||||
Performance evidence:
|
||||
Spike and acceptance criteria:
|
||||
Pinned version/update policy:
|
||||
Decision/ADR:
|
||||
```
|
||||
|
||||
## Реестр
|
||||
|
||||
| Инструмент | Статус | Область | Планируемое использование | Главный риск |
|
||||
|---|---|---|---|---|
|
||||
| OpenWC native loaders | ADOPTED | MPQ/BLP/ADT/WDT/M2/WMO | Текущий import/render pipeline | Неполная fidelity форматов |
|
||||
| StormLib | ADOPTED | MPQ | Чтение архивов через native extension | Version/license/update audit |
|
||||
| WowUnreal | REFERENCE | Полный клиент | Coverage, acceptance criteria, networking/UI research | Unreal-specific design |
|
||||
| WoWee | REFERENCE | Клиент/editor/formats | Architecture, editor workflows, tests, open formats | Заявления требуют независимой проверки |
|
||||
| Noggit Red | REFERENCE | World editor | Terrain/placement UX, UID workflows | Не Godot architecture |
|
||||
| open-realm | REFERENCE | Formats/runtime | Независимая проверка parsers/render behavior | Другая архитектура и coverage |
|
||||
| whoa | REFERENCE | Client behavior | 3.3.5a runtime semantics и fixtures | Лицензия и переносимость отдельных решений |
|
||||
| wow.export | REFERENCE | Asset conversion | M2/WMO/material/export edge cases | Web-specific pipeline |
|
||||
| Blender WoW Studio | REFERENCE | Authoring/conversion | WMO/M2/ADT authoring knowledge | Blender-specific UI/data model |
|
||||
| [Wowser](https://github.com/wowserhq/wowser) | REFERENCE | 3.3.5a web client | Auth/realm/character/world protocol, binary parsing, asset pipeline и render research | Старый JS/WebGL proof-of-concept, неполный клиент |
|
||||
| [warcraft-rs](https://github.com/wowemulation-dev/warcraft-rs) | CANDIDATE | WoW formats/CLI | Independent MPQ/DBC/BLP/ADT/WDT/WDL/M2/WMO validation и conversion oracle | Rust/tool duplication; claims require fixtures |
|
||||
| [WoWDBDefs](https://github.com/wowdev/WoWDBDefs) | REFERENCE | Client DB schemas | Versioned DBC definitions и typed-code generation input | Definitions still require build-specific validation |
|
||||
| [wow_dbc](https://github.com/gtker/wow_dbc) | REFERENCE | DBC | 1.12/2.4.3/3.3.5 read/write и SQLite conversion ideas | Older release, Rust integration unnecessary by default |
|
||||
| [recast-rs](https://github.com/wowemulation-dev/recast-rs) | CANDIDATE | Navigation | Offline navmesh, reachability и dungeon validation | Server mmap compatibility не доказана |
|
||||
| [rilua](https://github.com/wowemulation-dev/rilua) | CANDIDATE | Lua/addons | Lua 5.1.1 runtime candidate и headless compatibility oracle | Taint/WoW API/FrameXML отсутствуют; Rust bridge |
|
||||
| PUC-Rio Lua 5.1.1 | CANDIDATE | Lua reference/runtime | Authoritative Lua oracle и альтернативный embedded VM | Нужны WoW sandbox, taint и безопасный C++ boundary |
|
||||
| [GdUnit4](https://github.com/godot-gdunit-labs/gdUnit4) | CANDIDATE | Godot tests | GDScript, scene, Editor и CI test runner для Godot 4.6 | Plugin/toolchain pin и adoption spike |
|
||||
| [Keira3](https://github.com/azerothcore/Keira3) | REFERENCE | AzerothCore DB editor | Field semantics, SQL generation и DB editor UX | AGPL; schema-specific web architecture |
|
||||
| [WowBench](https://sourceforge.net/projects/wowbench/) | REFERENCE | WoW UI/API | Offline XML/Lua API emulation и addon test ideas | Старый и неполный implementation |
|
||||
|
||||
## rilua — карточка кандидата
|
||||
|
||||
- **Problem solved:** Lua 5.1.1 VM, bytecode, embedding и официальный compatibility corpus для addon runtime.
|
||||
- **Planned boundary:** реализация внутреннего `LuaRuntime` interface; FrameXML, TOC, WoW API, events и SavedVariables остаются независимыми слоями OpenWC.
|
||||
- **License:** MIT OR Apache-2.0.
|
||||
- **Fidelity evidence:** официальный Lua 5.1.1 test suite и oracle comparison с PUC-Rio по заявлениям проекта; OpenWC обязан повторить проверки на pinned revision.
|
||||
- **Known gaps:** WoW taint/secure execution заявлен как post-1.0; TOC, WoW API stubs и UI находятся вне scope; C ABI не совместим с `lua_State*`; требуется измерение GC/OnUpdate performance.
|
||||
- **Spike:** сравнить `rilua`, PUC-Rio 5.1.1 и оригинальный клиент на WoW-specific corpus; проверить restricted stdlib, globals, `bit`, errors, coroutines, byte strings, GC defaults, addon load и SavedVariables.
|
||||
- **Decision policy:** не связывать UI с Rust types и не объявлять addon parity до secure/taint и FrameXML/API tests.
|
||||
|
||||
## Wowser — карточка референса
|
||||
|
||||
- **Problem studied:** полный путь клиента WoW 3.3.5a в браузере: получение assets, auth/realm/world connections, cryptography, binary packets, workers, audio и WebGL rendering.
|
||||
- **Useful repositories:** [`wowserhq/wowser`](https://github.com/wowserhq/wowser), выделенные [`wowserhq/client`](https://github.com/wowserhq/client) и [`wowserhq/pipeline`](https://github.com/wowserhq/pipeline).
|
||||
- **License:** MIT для основного репозитория; лицензии выделенных репозиториев проверяются отдельно перед переносом кода.
|
||||
- **Fidelity evidence:** заявлена поддержка только WotLK 3.3.5a; реализованы username/password auth, realm list/connection, character list, world join и packet logging. Любая packet semantics всё равно сверяется с server core и fixtures.
|
||||
- **Useful boundaries:** разделение client и asset pipeline, async/background resource work, typed binary handling, protocol lifecycle и независимый WebGL renderer.
|
||||
- **Known gaps:** proof-of-concept, неполный gameplay/UI, browser WebSocket proxy вместо native TCP, устаревший JS ecosystem (React 0.14/Three.js 0.77 era), архитектура не переносится напрямую в Godot.
|
||||
- **Decision policy:** использовать для cross-check и test ideas; не добавлять Node/Web dependencies и не копировать browser-specific abstractions в OpenWC.
|
||||
|
||||
## recast-rs — карточка кандидата
|
||||
|
||||
- **Problem solved:** Recast navmesh generation, Detour queries, tiled navigation и dynamic obstacles.
|
||||
- **Planned boundary:** offline CLI/build job и versioned OpenWC navigation artifact.
|
||||
- **License:** MIT OR Apache-2.0.
|
||||
- **Known gaps:** совместимость `.mmap/.mmtile` и параметров TrinityCore/AzerothCore не доказана.
|
||||
- **Spike:** synthetic + one WoW tile comparison с C++ Recast; затем отдельный server mmap compatibility experiment.
|
||||
|
||||
## warcraft-rs — карточка кандидата
|
||||
|
||||
- **Problem solved:** единый CLI/library set для MPQ, DBC, BLP, ADT, WDT, WDL, M2 и WMO версий 1.12.1–5.4.8, включая заявленную full support 3.3.5a.
|
||||
- **Planned boundary:** сначала внешний pinned CLI как independent parser/validator oracle; не заменять текущий native pipeline автоматически.
|
||||
- **License:** MIT OR Apache-2.0.
|
||||
- **Spike:** сравнить OpenWC и warcraft-rs на synthetic/corrupt/real local fixtures: parsed counts, bounds, flags, alpha, materials, animations и diagnostics.
|
||||
- **Decision policy:** отдельные crates рассматриваются только если дают доказанное преимущество; Rust runtime/toolchain не вводится ради дублирования работающего C++ parser.
|
||||
|
||||
## GdUnit4 — карточка кандидата
|
||||
|
||||
- **Problem solved:** Godot 4 GDScript/scene tests, assertions, mocks, input simulation, command-line and JUnit reports.
|
||||
- **Planned boundary:** development/test plugin only; production export не зависит от test framework.
|
||||
- **License:** MIT.
|
||||
- **Spike:** закрепить совместимую с Godot 4.6.1 версию; проверить pure GDScript, scene lifecycle, input, EditorPlugin и headless CI sample; измерить startup overhead.
|
||||
- **Decision policy:** ADOPTED только после воспроизводимого Windows/headless run и отсутствия конфликта с project plugins.
|
||||
|
||||
## Процесс добавления инструмента
|
||||
|
||||
1. Добавить карточку со статусом `REFERENCE` или `CANDIDATE`.
|
||||
2. Указать конкретную проблему и boundary; «полезная библиотека» недостаточно.
|
||||
3. Проверить лицензию и возможность распространения.
|
||||
4. Создать bounded spike в соответствующем target, не меняя текущую цель без указания пользователя.
|
||||
5. Собрать fixtures и сравнить с исходным поведением WoW 3.3.5a или authoritative implementation.
|
||||
6. После решения обновить статус, pinned version, ADR и regression tests.
|
||||
|
||||
Вендорить или добавлять submodule до статуса `ADOPTED` не следует. Для быстро меняющихся проектов pinned commit обязателен.
|
||||
@@ -0,0 +1,18 @@
|
||||
# OpenWC Module Documentation
|
||||
|
||||
Модульные спецификации создаются по [`TEMPLATE.md`](TEMPLATE.md) и правилам [`../DOCUMENTATION_STANDARD.md`](../DOCUMENTATION_STANDARD.md).
|
||||
|
||||
## Registry
|
||||
|
||||
| Module | Current status | Primary specification |
|
||||
|---|---|---|
|
||||
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
||||
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
||||
| UI/Lua/audio | Prototype/Planned | [`../../targets/roadmap/05-ui-lua-audio.md`](../../targets/roadmap/05-ui-lua-audio.md) |
|
||||
| Editor/content tools | Prototype/Planned | [`../../targets/roadmap/06-editor-and-content-tools.md`](../../targets/roadmap/06-editor-and-content-tools.md) |
|
||||
| Server adapters | Planned | [`../../targets/roadmap/07-server-emulator-integration.md`](../../targets/roadmap/07-server-emulator-integration.md) |
|
||||
| Quality/release | Partial | [`../../targets/roadmap/08-quality-and-release.md`](../../targets/roadmap/08-quality-and-release.md) |
|
||||
|
||||
Roadmap-файл описывает полную целевую программу. При начале реализации самостоятельной подсистемы создаётся отдельный module spec, показывающий фактический статус и текущий data flow.
|
||||
@@ -0,0 +1,127 @@
|
||||
# <Module Name>
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Planned / Prototype / Partial / Implemented / Verified / Deprecated |
|
||||
| Target/work package | |
|
||||
| Owners | |
|
||||
| Last verified | `<commit>, YYYY-MM-DD` |
|
||||
| Profiles/capabilities | |
|
||||
|
||||
## Purpose
|
||||
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[Input contract] --> Module[Module]
|
||||
Module --> Output[Output contract]
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
|
||||
-
|
||||
|
||||
Forbidden dependencies:
|
||||
|
||||
-
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
Side effects:
|
||||
|
||||
-
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Source --> Transform --> Result
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Created
|
||||
Created --> Ready
|
||||
Ready --> Stopped
|
||||
Stopped --> [*]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as Producer
|
||||
participant M as Module
|
||||
participant B as Consumer
|
||||
A->>M: Input
|
||||
M-->>B: Output
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs:
|
||||
- Metrics:
|
||||
- Debug views:
|
||||
- Correlation IDs:
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit/contract tests:
|
||||
- Integration/E2E:
|
||||
- Fidelity evidence:
|
||||
- Performance budgets:
|
||||
- Manual diagnostics:
|
||||
|
||||
## Extension points
|
||||
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- ADR:
|
||||
- Upstream/reference:
|
||||
@@ -0,0 +1,257 @@
|
||||
# World Renderer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 active; декомпозиция M01–M03 |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | M00 visual-diff worktree, 2026-07-11 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
|
||||
Загрузить и визуализировать мир WoW 3.3.5a вокруг streaming focus: terrain, ADT placements, M2, WMO, liquids, sky/light и character/world presentation experiments. Система должна скрывать I/O/parsing/finalization latency за prewarm и per-frame budgets.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Не владеет gameplay/world authoritative state.
|
||||
- Не читает network packets или server DB.
|
||||
- Не определяет player input/movement rules.
|
||||
- Не является Content Project или editor source of truth.
|
||||
- Текущий module не заявляет визуальное соответствие `1:1`; gaps ведутся в [`../../RENDER.md`](../../RENDER.md).
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Focus[Camera/player/editor focus] --> Loader[StreamingWorldLoader]
|
||||
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
|
||||
Cache[Baked terrain/M2/WMO caches] --> Loader
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
Loader --> RS[RenderingServer RIDs/MultiMesh]
|
||||
Sky[DBC sky/light data] --> SkyController[WowSkyController]
|
||||
SkyController --> Scene
|
||||
```
|
||||
|
||||
Allowed dependencies:
|
||||
|
||||
- native format loaders;
|
||||
- render builders/material helpers;
|
||||
- immutable/cached render resources;
|
||||
- renderer configuration and focus;
|
||||
- Godot rendering/scene APIs.
|
||||
|
||||
Forbidden dependencies:
|
||||
|
||||
- packet codecs/world session;
|
||||
- gameplay reducers/quest/combat state;
|
||||
- runtime UI Controls;
|
||||
- direct TrinityCore/AzerothCore database access.
|
||||
|
||||
## Public API
|
||||
|
||||
Текущая система ещё не имеет стабильного facade. Фактический integration surface — `StreamingWorldLoader` Node3D и exported properties. M03 должен заменить этот surface на `WorldRenderFacade`.
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `map_name` | Exported property | WDT/map directory name | Set before world load | Missing WDT produces diagnostic/empty world |
|
||||
| `extracted_dir` | Exported property | Root of legally extracted client data | Session lifetime | Missing files reported per loader path |
|
||||
| `camera_path` | Exported property | Current streaming focus compatibility path | Main thread, scene lifetime | Missing camera prevents normal focus update |
|
||||
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
|
||||
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
|
||||
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
|
||||
|
||||
Публичным contract не считаются внутренние dictionaries, queues, job records и generated node names.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Map/focus/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
|
||||
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
|
||||
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
|
||||
| Output | Desired tile/detail operations | Streaming planner inside loader | Finalize queues | Loader-owned | Cross-frame |
|
||||
| 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 |
|
||||
|
||||
Side effects:
|
||||
|
||||
- reads extracted files and generated caches;
|
||||
- creates/frees scene nodes, meshes, materials, MultiMeshes and RIDs;
|
||||
- submits worker tasks;
|
||||
- mutates shader globals/environment through render/sky paths;
|
||||
- writes logs and baseline captures through tools.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
F[Streaming focus] --> T[Compute desired ADT tiles]
|
||||
WDT[WDT tile catalog] --> T
|
||||
T --> Q[Tile load queue]
|
||||
Q --> P[Worker parse/cache load]
|
||||
P --> R[Result queues]
|
||||
R --> B[Per-frame budget scheduler]
|
||||
B --> Terrain[Terrain attach/upgrade]
|
||||
B --> M2[M2 group/MultiMesh attach]
|
||||
B --> WMO[WMO instance/group attach]
|
||||
B --> Liquid[Liquid attach]
|
||||
Terrain --> World[Godot world]
|
||||
M2 --> World
|
||||
WMO --> World
|
||||
Liquid --> World
|
||||
F --> E[Eviction/retention decisions]
|
||||
E --> World
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Initializing
|
||||
Initializing --> Ready: WDT/catalog/config ready
|
||||
Initializing --> Degraded: missing loaders/data/cache
|
||||
Ready --> Streaming: focus update
|
||||
Streaming --> Streaming: load/finalize/evict ticks
|
||||
Streaming --> SwitchingMap: map/focus reset
|
||||
SwitchingMap --> Initializing
|
||||
Ready --> ShuttingDown
|
||||
Degraded --> ShuttingDown
|
||||
Streaming --> ShuttingDown
|
||||
ShuttingDown --> [*]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Focus as Camera/Player/Editor
|
||||
participant Stream as StreamingWorldLoader
|
||||
participant Worker as Worker task
|
||||
participant Budget as Main-thread budget
|
||||
participant Render as SceneTree/RenderingServer
|
||||
Focus->>Stream: focus position changed
|
||||
Stream->>Stream: desired tiles and priorities
|
||||
Stream->>Worker: parse/load tile and detail data
|
||||
Worker-->>Stream: immutable result
|
||||
Stream->>Budget: enqueue finalize operations
|
||||
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
|
||||
Stream->>Render: evict outside retention range
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
|
||||
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
||||
- Parsed/grouped results передаются обратно через guarded result queues.
|
||||
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
|
||||
- Shutdown/map switch обязан отменить/дождаться jobs и освободить RIDs; M00 всё ещё фиксирует leaked-resource risk.
|
||||
- Cache resources считаются immutable после публикации.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Missing WDT/ADT | File/catalog check | Tile/map unavailable | Loader warning/error | Fix extraction/profile, reload |
|
||||
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
|
||||
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
|
||||
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
|
||||
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
|
||||
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
|
||||
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Shutdown report | Ownership cleanup before DONE |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Quality preset | Scene-specific | All | Limited | Applies radii and finalize budgets |
|
||||
| Terrain control splat | Enabled in current High paths | Render | Limited | Near terrain texture quality |
|
||||
| M2/WMO/liquids | Scene/profile-specific | Render | Debug/config | Enables asset categories |
|
||||
| Runtime stats/hitch profiler | Usually debug | Dev/test | Yes | Observability cost |
|
||||
| Shadow flags | Mostly disabled/limited | Blizzlike/Enhanced | Profile | CPU/GPU cost and visuals |
|
||||
|
||||
Exact exported settings and cache versions remain documented in [`../../RENDER.md`](../../RENDER.md) until extracted services receive individual specs.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
- Renderer reads caches under `data/cache`; caches are not committed.
|
||||
- Format changes require `FORMAT_VERSION`/required-version bump and rebuild instructions.
|
||||
- Atomic cache publication and unified manifest are target-state work from FND/M03.
|
||||
- Material-only changes may refresh without geometry rebake only when documented safe.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs: `HITCH`, `PERF`, `TERRAIN_QUALITY`, `SKY_LIGHT`, `SKYBOX_MODEL`.
|
||||
- Metrics: queue depths, active tiles/assets, named finalize times, frame percentiles and max hitch.
|
||||
- Debug views: render checkpoint scenes/captures; future streaming/LOD/portal overlays.
|
||||
- Correlation IDs: currently mostly tile/path keys; target architecture adds session/job IDs.
|
||||
|
||||
## Verification
|
||||
|
||||
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff.
|
||||
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
|
||||
- Fidelity evidence: пять локальных build 12340 reference JPG откалибровали terrain/ADT/M2/WMO/liquid viewpoints; automated paired-image metrics exist, но synthetic animation/dusk и полный human approval ещё не закрыты.
|
||||
- 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).
|
||||
|
||||
## Extension points
|
||||
|
||||
- Target `WorldRenderFacade` for runtime/editor/capture callers.
|
||||
- Target `StreamingFocus` independent of Camera3D.
|
||||
- Extracted planner, budget scheduler, terrain, M2, WMO, liquid, sky and character services.
|
||||
- Separate Blizzlike/Enhanced material and quality profiles.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
|
||||
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
|
||||
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
||||
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
|
||||
| Stable renderer facade | Planned | M03 | Current caller uses monolithic Node API |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Monolithic streamer mixes planning, jobs, caches and presentation.
|
||||
- Direct camera path remains until M01/M03.
|
||||
- Original-client paired fidelity evidence incomplete.
|
||||
- Первый paired run выявил coordinate/placement mismatch: несколько совпадающих server-derived camera positions оказываются под terrain или внутри WMO/rocks OpenWC.
|
||||
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
|
||||
- Camera-occluder probe не нашёл camera containment в пяти точках; paired mismatch локализован прежде всего в manual look direction/target/FOV calibration, с явным ограничением по RID-only geometry.
|
||||
- Empirical FOV sweep выявил, что checkpoint camera должна явно вызывать `make_current()`; после исправления projection ranking остаётся inconclusive из-за неизвестного manual yaw/pitch/framing reference.
|
||||
- D3D12 descriptor and shutdown RID/resource issues remain.
|
||||
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
||||
- Public API is mostly exported configuration rather than stable contracts.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
|
||||
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
|
||||
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
|
||||
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
|
||||
| `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO groups/doodads/liquids construction |
|
||||
| `src/scenes/sky/wow_sky_controller.gd` | DBC-driven sky/light/environment |
|
||||
| `src/native/src/*_loader.cpp` | Native binary parsing |
|
||||
| `src/tools/build_*cache.gd`, `src/tools/bake_*cache.gd` | Offline cache generation |
|
||||
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
|
||||
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
|
||||
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
|
||||
| `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_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
||||
@@ -943,6 +943,36 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
|
||||
])
|
||||
|
||||
|
||||
## Read-only machine-readable state used by renderer baseline tooling.
|
||||
## Keep this free of queue mutation so checkpoint capture cannot alter runtime behavior.
|
||||
func render_baseline_snapshot() -> Dictionary:
|
||||
return {
|
||||
"tiles": _tile_states.size(),
|
||||
"loading": _tile_loading_tasks.size(),
|
||||
"queues": {
|
||||
"tile": _tile_load_queue.size(),
|
||||
"terrain_upgrade": _terrain_upgrade_tasks.size(),
|
||||
"terrain_control": _terrain_control_splat_cache_tasks.size(),
|
||||
"terrain_splat": _terrain_splat_queue_size(),
|
||||
"water": _water_load_queue.size() + _water_load_tasks.size(),
|
||||
"detail": _detail_asset_queue.size(),
|
||||
"m2_task": _m2_group_tasks.size(),
|
||||
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
"m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
|
||||
"m2_build": _m2_build_queue.size(),
|
||||
"wmo_build": _wmo_build_queue.size() + _wmo_scene_load_requests.size() + _wmo_render_load_requests.size(),
|
||||
"wmo_groups": _wmo_render_build_queue.size(),
|
||||
"lod_create": _tile_lod_create_queue.size(),
|
||||
"lod_remove": _tile_lod_remove_queue.size(),
|
||||
"chunk_create": _chunk_create_queue.size(),
|
||||
"chunk_remove": _chunk_remove_queue.size(),
|
||||
},
|
||||
"cache_entries": _tile_mesh_cache.size(),
|
||||
"m2_active_unique_ids": _m2_unique_registry.size(),
|
||||
"wmo_instances": _wmo_registry.size(),
|
||||
}
|
||||
|
||||
|
||||
func _tick_runtime_stats(delta: float) -> void:
|
||||
if not runtime_stats_enabled or Engine.is_editor_hint():
|
||||
return
|
||||
|
||||
@@ -1,33 +1,7 @@
|
||||
extends SceneTree
|
||||
|
||||
const STREAMING_SCENE := "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn"
|
||||
|
||||
const CHECKPOINTS := [
|
||||
{
|
||||
"name": "elwynn_waterfall_front",
|
||||
"camera": Vector3(16445.0, 125.0, 26295.0),
|
||||
"target": Vector3(16518.84, 68.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "elwynn_waterfall_side",
|
||||
"camera": Vector3(16670.0, 115.0, 26320.0),
|
||||
"target": Vector3(16518.84, 72.0, 26427.27),
|
||||
"player": Vector3(16518.84, 55.0, 26427.27),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_inn_windows",
|
||||
"camera": Vector3(16942.0, 82.0, 26503.0),
|
||||
"target": Vector3(17042.27, 66.0, 26530.91),
|
||||
"player": Vector3(17042.27, 58.0, 26530.91),
|
||||
},
|
||||
{
|
||||
"name": "goldshire_blacksmith_windows",
|
||||
"camera": Vector3(16892.0, 79.0, 26562.0),
|
||||
"target": Vector3(16972.46, 64.0, 26526.7),
|
||||
"player": Vector3(16972.46, 58.0, 26526.7),
|
||||
},
|
||||
]
|
||||
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
@@ -36,45 +10,63 @@ func _initialize() -> void:
|
||||
|
||||
func _capture_async() -> void:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
var output_dir := _arg(args, "--output", "user://render_checkpoints")
|
||||
var manifest_path := _arg(args, "--manifest", DEFAULT_MANIFEST_PATH)
|
||||
var manifest := _load_json(manifest_path)
|
||||
if manifest.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var output_dir := _arg(args, "--output", "user://render_baseline")
|
||||
var only := _arg(args, "--only", "").to_lower()
|
||||
var wait_seconds := float(_arg(args, "--wait", "8.0"))
|
||||
var width := int(_arg(args, "--width", "1280"))
|
||||
var height := int(_arg(args, "--height", "900"))
|
||||
var wait_seconds := float(_arg(args, "--wait", str(manifest.get("default_wait_seconds", 8.0))))
|
||||
var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
|
||||
var revision := _arg(args, "--revision", "worktree")
|
||||
var cache_state := _arg(args, "--cache-state", "existing")
|
||||
var camera_fov_override := float(_arg(args, "--camera-fov", str(manifest.get("camera_fov", 62.0))))
|
||||
var headless := DisplayServer.get_name().to_lower() == "headless"
|
||||
var dry_run := args.has("--dry-run") or headless
|
||||
var viewport: Array = manifest.get("viewport", [1280, 900])
|
||||
var viewport_width := int(_arg(args, "--viewport-width", str(viewport[0])))
|
||||
var viewport_height := int(_arg(args, "--viewport-height", str(viewport[1])))
|
||||
get_root().size = Vector2i(maxi(16, viewport_width), maxi(16, viewport_height))
|
||||
|
||||
get_root().size = Vector2i(maxi(16, width), maxi(16, height))
|
||||
var abs_output_dir := ProjectSettings.globalize_path(output_dir)
|
||||
if not dry_run:
|
||||
DirAccess.make_dir_recursive_absolute(abs_output_dir)
|
||||
elif headless and not args.has("--dry-run"):
|
||||
if headless and not args.has("--dry-run"):
|
||||
print("RENDER_CHECKPOINT dry-run: headless display cannot capture viewport PNGs.")
|
||||
|
||||
var packed: PackedScene = load(STREAMING_SCENE)
|
||||
var scene_path := String(manifest.get("scene", ""))
|
||||
var packed: PackedScene = load(scene_path)
|
||||
if packed == null:
|
||||
push_error("Cannot load streaming scene: %s" % STREAMING_SCENE)
|
||||
push_error("Cannot load streaming scene: %s" % scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var world = packed.instantiate()
|
||||
if not (world is Node3D):
|
||||
push_error("Streaming scene root is not Node3D: %s" % STREAMING_SCENE)
|
||||
push_error("Streaming scene root is not Node3D: %s" % scene_path)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var checkpoints: Array = manifest.get("checkpoints", [])
|
||||
if checkpoints.is_empty():
|
||||
push_error("Render baseline manifest has no checkpoints")
|
||||
quit(1)
|
||||
return
|
||||
var first: Dictionary = checkpoints[0]
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "CheckpointCamera"
|
||||
camera.current = true
|
||||
camera.fov = 62.0
|
||||
camera.fov = camera_fov_override
|
||||
camera.far = 50000.0
|
||||
camera.position = CHECKPOINTS[0].get("camera", Vector3.ZERO)
|
||||
camera.position = _vector3(first.get("camera", [0.0, 0.0, 0.0]))
|
||||
(world as Node3D).add_child(camera)
|
||||
world.set("camera_path", NodePath("CheckpointCamera"))
|
||||
world.set("debug_streaming", true)
|
||||
world.set("runtime_stats_enabled", true)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
camera.make_current()
|
||||
|
||||
var player := world.get_node_or_null("ThirdPersonPlayer") as Node3D
|
||||
if player != null:
|
||||
@@ -84,51 +76,255 @@ func _capture_async() -> void:
|
||||
if visual != null:
|
||||
visual.visible = false
|
||||
|
||||
var report := {
|
||||
"schema_version": 1,
|
||||
"manifest": manifest_path,
|
||||
"profile": manifest.get("profile", "Blizzlike335"),
|
||||
"revision": revision,
|
||||
"created_utc": Time.get_datetime_string_from_system(true, true),
|
||||
"dry_run": dry_run,
|
||||
"cache_state": cache_state,
|
||||
"camera_fov": camera_fov_override,
|
||||
"environment": _environment_metadata(),
|
||||
"comparison_budgets": manifest.get("comparison_budgets", {}),
|
||||
"cache_contract": manifest.get("cache_contract", {}),
|
||||
"cache_inventory": _cache_inventory(),
|
||||
"results": [],
|
||||
}
|
||||
var captured := 0
|
||||
for checkpoint_variant in CHECKPOINTS:
|
||||
var passes := ["cold_process", "warm_revisit"]
|
||||
if dry_run:
|
||||
passes = ["dry_run"]
|
||||
|
||||
for pass_name in passes:
|
||||
for checkpoint_variant in checkpoints:
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
|
||||
continue
|
||||
|
||||
camera.global_position = checkpoint.get("camera", Vector3.ZERO)
|
||||
camera.look_at(checkpoint.get("target", Vector3.ZERO), Vector3.UP)
|
||||
var probe := _create_probe(checkpoint, dry_run)
|
||||
if probe != null:
|
||||
(world as Node3D).add_child(probe)
|
||||
probe.global_position = _vector3(checkpoint.get("target", [0.0, 0.0, 0.0]))
|
||||
probe.scale = Vector3.ONE * float(checkpoint.get("probe_scale", 1.0))
|
||||
|
||||
camera.global_position = _vector3(checkpoint.get("camera", [0.0, 0.0, 0.0]))
|
||||
_orient_camera_without_roll(camera, _vector3(checkpoint.get("target", [0.0, 0.0, 0.0])))
|
||||
camera.make_current()
|
||||
if player != null:
|
||||
player.global_position = checkpoint.get("player", checkpoint.get("target", Vector3.ZERO))
|
||||
player.global_position = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
|
||||
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera.global_position, true)
|
||||
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
if dry_run:
|
||||
print("RENDER_CHECKPOINT dry_run name=%s camera=%s target=%s" % [
|
||||
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s time=%.2f" % [
|
||||
checkpoint_name,
|
||||
str(checkpoint.get("coverage", [])),
|
||||
camera.global_position,
|
||||
checkpoint.get("target", Vector3.ZERO),
|
||||
_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])),
|
||||
float(checkpoint.get("time_hours", 13.0)),
|
||||
])
|
||||
(report["results"] as Array).append(_result_record(checkpoint, pass_name, 0.0, {}, ""))
|
||||
captured += 1
|
||||
if probe != null:
|
||||
probe.queue_free()
|
||||
continue
|
||||
|
||||
var load_started := Time.get_ticks_usec()
|
||||
await create_timer(maxf(wait_seconds, 0.0)).timeout
|
||||
var load_time_ms := float(Time.get_ticks_usec() - load_started) / 1000.0
|
||||
var metrics := await _measure_frames(maxf(measure_seconds, 0.1), world)
|
||||
await RenderingServer.frame_post_draw
|
||||
|
||||
var image := get_root().get_texture().get_image()
|
||||
var file_name := "%s.png" % checkpoint_name
|
||||
var file_name := "%s__%s.png" % [checkpoint_name, pass_name]
|
||||
var abs_path := abs_output_dir.path_join(file_name)
|
||||
var err := image.save_png(abs_path)
|
||||
if err != OK:
|
||||
push_error("Failed to save checkpoint %s to %s (err=%d)" % [checkpoint_name, abs_path, err])
|
||||
quit(1)
|
||||
return
|
||||
print("RENDER_CHECKPOINT saved %s" % abs_path)
|
||||
var sha256 := FileAccess.get_sha256(abs_path)
|
||||
(report["results"] as Array).append(_result_record(checkpoint, pass_name, load_time_ms, metrics, sha256))
|
||||
print("RENDER_CHECKPOINT saved %s sha256=%s" % [abs_path, sha256])
|
||||
captured += 1
|
||||
if probe != null:
|
||||
probe.queue_free()
|
||||
await process_frame
|
||||
|
||||
var report_path := abs_output_dir.path_join("report.json")
|
||||
var report_file := FileAccess.open(report_path, FileAccess.WRITE)
|
||||
if report_file == null:
|
||||
push_error("Cannot write render baseline report: %s" % report_path)
|
||||
quit(1)
|
||||
return
|
||||
report_file.store_string(JSON.stringify(report, " "))
|
||||
report_file.close()
|
||||
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
||||
|
||||
world.queue_free()
|
||||
if captured <= 0:
|
||||
push_error("No checkpoints captured. --only filter was: %s" % only)
|
||||
push_error("No checkpoints selected. --only filter was: %s" % only)
|
||||
quit(1)
|
||||
return
|
||||
quit(0)
|
||||
|
||||
|
||||
func _measure_frames(seconds: float, world: Node) -> Dictionary:
|
||||
var frame_times: Array[float] = []
|
||||
var deadline := Time.get_ticks_usec() + int(seconds * 1000000.0)
|
||||
var previous := Time.get_ticks_usec()
|
||||
while Time.get_ticks_usec() < deadline:
|
||||
await process_frame
|
||||
var now := Time.get_ticks_usec()
|
||||
frame_times.append(float(now - previous) / 1000.0)
|
||||
previous = now
|
||||
frame_times.sort()
|
||||
var snapshot := {}
|
||||
if world.has_method("render_baseline_snapshot"):
|
||||
snapshot = world.call("render_baseline_snapshot")
|
||||
return {
|
||||
"frames": frame_times.size(),
|
||||
"frame_ms_p50": _percentile(frame_times, 0.50),
|
||||
"frame_ms_p95": _percentile(frame_times, 0.95),
|
||||
"frame_ms_p99": _percentile(frame_times, 0.99),
|
||||
"max_hitch_ms": frame_times[-1] if not frame_times.is_empty() else 0.0,
|
||||
"memory_static_bytes": int(Performance.get_monitor(Performance.MEMORY_STATIC)),
|
||||
"video_memory_bytes": int(Performance.get_monitor(Performance.RENDER_VIDEO_MEM_USED)),
|
||||
"draw_calls": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
|
||||
"objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
|
||||
"streaming": snapshot,
|
||||
}
|
||||
|
||||
|
||||
func _orient_camera_without_roll(camera: Camera3D, target_position: Vector3) -> void:
|
||||
var forward := (target_position - camera.global_position).normalized()
|
||||
var right := forward.cross(Vector3.UP).normalized()
|
||||
if right.is_zero_approx():
|
||||
right = Vector3.RIGHT
|
||||
var corrected_up := right.cross(forward).normalized()
|
||||
camera.global_basis = Basis(right, corrected_up, -forward)
|
||||
|
||||
|
||||
func _result_record(checkpoint: Dictionary, pass_name: String, load_time_ms: float, metrics: Dictionary, sha256: String) -> Dictionary:
|
||||
return {
|
||||
"name": checkpoint.get("name", "checkpoint"),
|
||||
"coverage": checkpoint.get("coverage", []),
|
||||
"pass": pass_name,
|
||||
"camera": checkpoint.get("camera", []),
|
||||
"target": checkpoint.get("target", []),
|
||||
"player": checkpoint.get("player", []),
|
||||
"time_hours": checkpoint.get("time_hours", 13.0),
|
||||
"load_time_ms": load_time_ms,
|
||||
"metrics": metrics,
|
||||
"png_sha256": sha256,
|
||||
}
|
||||
|
||||
|
||||
func _create_probe(checkpoint: Dictionary, dry_run: bool) -> Node3D:
|
||||
var rel_path := String(checkpoint.get("probe_model", ""))
|
||||
if rel_path.is_empty() or dry_run:
|
||||
return null
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
push_warning("M2Loader unavailable; skipping probe model %s" % rel_path)
|
||||
return null
|
||||
var extracted_dir := "res://data/extracted"
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(rel_path))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
push_warning("Probe model missing: %s" % abs_path)
|
||||
return null
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
var data: Dictionary = loader.call("load_m2_animated", abs_path)
|
||||
if data.is_empty():
|
||||
push_warning("Probe model could not be parsed: %s" % rel_path)
|
||||
return null
|
||||
return M2_NATIVE_ANIMATED_BUILDER.build(data, extracted_dir)
|
||||
|
||||
|
||||
func _set_sky_time(world: Node, time_hours: float) -> void:
|
||||
var sky := world.get_node_or_null("WowSkyController")
|
||||
if sky != null:
|
||||
sky.set("use_system_time", false)
|
||||
sky.set("time_speed", 0.0)
|
||||
sky.set("fixed_time_hours", time_hours)
|
||||
|
||||
|
||||
func _environment_metadata() -> Dictionary:
|
||||
return {
|
||||
"godot_version": Engine.get_version_info(),
|
||||
"os": OS.get_name(),
|
||||
"os_version": OS.get_version(),
|
||||
"cpu": OS.get_processor_name(),
|
||||
"logical_cpu_count": OS.get_processor_count(),
|
||||
"rendering_method": RenderingServer.get_current_rendering_method(),
|
||||
"rendering_driver": RenderingServer.get_current_rendering_driver_name(),
|
||||
"video_adapter": RenderingServer.get_video_adapter_name(),
|
||||
"video_vendor": RenderingServer.get_video_adapter_vendor(),
|
||||
"video_api": RenderingServer.get_video_adapter_api_version(),
|
||||
"memory": OS.get_memory_info(),
|
||||
"viewport": [get_root().size.x, get_root().size.y],
|
||||
}
|
||||
|
||||
|
||||
func _cache_inventory() -> Dictionary:
|
||||
var paths := [
|
||||
"res://data/cache/baked_terrain_v2",
|
||||
"res://data/cache/baked_terrain_stream_v1",
|
||||
"res://data/cache/terrain_splat_v1",
|
||||
"res://data/cache/terrain_control_splat_v3",
|
||||
"res://data/cache/m2_glb",
|
||||
"res://data/cache/wmo_tscn",
|
||||
"res://data/cache/wmo_render_v1",
|
||||
]
|
||||
var inventory := {}
|
||||
for path in paths:
|
||||
var dir := DirAccess.open(path)
|
||||
inventory[path] = {
|
||||
"present": dir != null,
|
||||
"file_count": _count_files(path) if dir != null else 0,
|
||||
}
|
||||
return inventory
|
||||
|
||||
|
||||
func _count_files(path: String) -> int:
|
||||
var dir := DirAccess.open(path)
|
||||
if dir == null:
|
||||
return 0
|
||||
var count := dir.get_files().size()
|
||||
for child in dir.get_directories():
|
||||
count += _count_files(path.path_join(child))
|
||||
return count
|
||||
|
||||
|
||||
func _percentile(sorted_values: Array[float], fraction: float) -> float:
|
||||
if sorted_values.is_empty():
|
||||
return 0.0
|
||||
var index := clampi(int(ceil(fraction * sorted_values.size())) - 1, 0, sorted_values.size() - 1)
|
||||
return sorted_values[index]
|
||||
|
||||
|
||||
func _load_json(path: String) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Cannot open JSON file: %s" % path)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
push_error("JSON root must be an object: %s" % path)
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
func _vector3(value: Variant) -> Vector3:
|
||||
if value is Array and value.size() >= 3:
|
||||
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var idx := args.find(name)
|
||||
if idx >= 0 and idx + 1 < args.size():
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
extends SceneTree
|
||||
|
||||
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
|
||||
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
|
||||
## --reference <directory> --candidate <directory> [--output <report.json>]
|
||||
## [--pixel-threshold 0.04] [--mean-threshold 0.015] [--changed-ratio-threshold 0.01]
|
||||
## Use --self-test for a synthetic regression check that writes only under user://.
|
||||
|
||||
const REPORT_SCHEMA_VERSION := 1
|
||||
const DEFAULT_PIXEL_THRESHOLD := 0.04
|
||||
const DEFAULT_MEAN_THRESHOLD := 0.015
|
||||
const DEFAULT_CHANGED_RATIO_THRESHOLD := 0.01
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var arguments := _parse_arguments(OS.get_cmdline_user_args())
|
||||
if arguments.has("error"):
|
||||
push_error("RENDER_CHECKPOINT_DIFF: %s" % arguments.error)
|
||||
quit(2)
|
||||
return
|
||||
if arguments.get("self_test", false):
|
||||
quit(_run_self_test())
|
||||
return
|
||||
|
||||
var reference_directory := String(arguments.get("reference", ""))
|
||||
var candidate_directory := String(arguments.get("candidate", ""))
|
||||
if reference_directory.is_empty() or candidate_directory.is_empty():
|
||||
push_error("RENDER_CHECKPOINT_DIFF: --reference and --candidate are required")
|
||||
quit(2)
|
||||
return
|
||||
|
||||
var report := _compare_directories(reference_directory, candidate_directory, arguments)
|
||||
var output_path := String(arguments.get("output", ""))
|
||||
if not output_path.is_empty() and not _write_report(output_path, report):
|
||||
quit(2)
|
||||
return
|
||||
print("RENDER_CHECKPOINT_DIFF %s compared=%d failed=%d missing=%d" % [
|
||||
"PASS" if report.passed else "FAIL",
|
||||
report.compared_count,
|
||||
report.failed_count,
|
||||
report.missing_count,
|
||||
])
|
||||
quit(0 if report.passed else 1)
|
||||
|
||||
|
||||
func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
||||
var parsed := {
|
||||
"pixel_threshold": DEFAULT_PIXEL_THRESHOLD,
|
||||
"mean_threshold": DEFAULT_MEAN_THRESHOLD,
|
||||
"changed_ratio_threshold": DEFAULT_CHANGED_RATIO_THRESHOLD,
|
||||
}
|
||||
var index := 0
|
||||
while index < raw_arguments.size():
|
||||
var argument := raw_arguments[index]
|
||||
if argument == "--self-test":
|
||||
parsed.self_test = true
|
||||
index += 1
|
||||
continue
|
||||
if argument not in ["--reference", "--candidate", "--output", "--only", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
|
||||
return {"error": "unknown argument: %s" % argument}
|
||||
if index + 1 >= raw_arguments.size():
|
||||
return {"error": "missing value for %s" % argument}
|
||||
var key := argument.trim_prefix("--").replace("-", "_")
|
||||
var value := raw_arguments[index + 1]
|
||||
if key.ends_with("threshold"):
|
||||
if not value.is_valid_float():
|
||||
return {"error": "%s requires a number" % argument}
|
||||
parsed[key] = float(value)
|
||||
else:
|
||||
parsed[key] = value
|
||||
index += 2
|
||||
return parsed
|
||||
|
||||
|
||||
func _compare_directories(reference_directory: String, candidate_directory: String, options: Dictionary) -> Dictionary:
|
||||
reference_directory = ProjectSettings.globalize_path(reference_directory)
|
||||
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
||||
var reference_files := _reference_image_file_names(reference_directory)
|
||||
var only_filter := String(options.get("only", "")).to_lower()
|
||||
var results: Array[Dictionary] = []
|
||||
var failed_count := 0
|
||||
var missing_count := 0
|
||||
for file_name in reference_files:
|
||||
if not only_filter.is_empty() and not file_name.get_basename().to_lower().contains(only_filter):
|
||||
continue
|
||||
var reference_path := reference_directory.path_join(file_name)
|
||||
var checkpoint_name := file_name.get_basename()
|
||||
var candidate_file_names := _candidate_file_names(candidate_directory, checkpoint_name)
|
||||
if candidate_file_names.is_empty():
|
||||
results.append({"checkpoint": checkpoint_name, "reference": file_name, "status": "missing_candidate"})
|
||||
missing_count += 1
|
||||
continue
|
||||
for candidate_file_name in candidate_file_names:
|
||||
var candidate_path := candidate_directory.path_join(candidate_file_name)
|
||||
var comparison := _compare_images(reference_path, candidate_path, options)
|
||||
comparison.checkpoint = checkpoint_name
|
||||
comparison.reference = file_name
|
||||
comparison.candidate = candidate_file_name
|
||||
results.append(comparison)
|
||||
if comparison.status != "passed":
|
||||
failed_count += 1
|
||||
var no_reference_images := reference_files.is_empty()
|
||||
return {
|
||||
"schema_version": REPORT_SCHEMA_VERSION,
|
||||
"passed": not no_reference_images and failed_count == 0 and missing_count == 0,
|
||||
"reference_directory": reference_directory,
|
||||
"candidate_directory": candidate_directory,
|
||||
"pixel_threshold": options.pixel_threshold,
|
||||
"mean_threshold": options.mean_threshold,
|
||||
"changed_ratio_threshold": options.changed_ratio_threshold,
|
||||
"compared_count": results.size() - missing_count,
|
||||
"failed_count": failed_count,
|
||||
"missing_count": missing_count,
|
||||
"no_reference_images": no_reference_images,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
|
||||
func _compare_images(reference_path: String, candidate_path: String, options: Dictionary) -> Dictionary:
|
||||
var reference_image := Image.load_from_file(reference_path)
|
||||
var candidate_image := Image.load_from_file(candidate_path)
|
||||
if reference_image == null or candidate_image == null or reference_image.is_empty() or candidate_image.is_empty():
|
||||
return {"status": "load_error"}
|
||||
if reference_image.get_size() != candidate_image.get_size():
|
||||
return {
|
||||
"status": "size_mismatch",
|
||||
"reference_size": [reference_image.get_width(), reference_image.get_height()],
|
||||
"candidate_size": [candidate_image.get_width(), candidate_image.get_height()],
|
||||
}
|
||||
|
||||
var error_sum := 0.0
|
||||
var changed_pixel_count := 0
|
||||
var pixel_count := reference_image.get_width() * reference_image.get_height()
|
||||
for y in reference_image.get_height():
|
||||
for x in reference_image.get_width():
|
||||
var perceptual_error := _perceptual_color_error(reference_image.get_pixel(x, y), candidate_image.get_pixel(x, y))
|
||||
error_sum += perceptual_error
|
||||
if perceptual_error > float(options.pixel_threshold):
|
||||
changed_pixel_count += 1
|
||||
var mean_perceptual_error := error_sum / float(pixel_count)
|
||||
var changed_pixel_ratio := float(changed_pixel_count) / float(pixel_count)
|
||||
var passed := mean_perceptual_error <= float(options.mean_threshold) and changed_pixel_ratio <= float(options.changed_ratio_threshold)
|
||||
return {
|
||||
"status": "passed" if passed else "threshold_exceeded",
|
||||
"width": reference_image.get_width(),
|
||||
"height": reference_image.get_height(),
|
||||
"mean_perceptual_error": mean_perceptual_error,
|
||||
"changed_pixel_ratio": changed_pixel_ratio,
|
||||
}
|
||||
|
||||
|
||||
func _perceptual_color_error(reference_color: Color, candidate_color: Color) -> float:
|
||||
var reference_linear := reference_color.srgb_to_linear()
|
||||
var candidate_linear := candidate_color.srgb_to_linear()
|
||||
var red_delta := absf(reference_linear.r - candidate_linear.r)
|
||||
var green_delta := absf(reference_linear.g - candidate_linear.g)
|
||||
var blue_delta := absf(reference_linear.b - candidate_linear.b)
|
||||
var alpha_delta := absf(reference_linear.a - candidate_linear.a)
|
||||
return 0.2126 * red_delta + 0.7152 * green_delta + 0.0722 * blue_delta + 0.25 * alpha_delta
|
||||
|
||||
|
||||
func _reference_image_file_names(directory_path: String) -> Array[String]:
|
||||
var directory := DirAccess.open(directory_path)
|
||||
if directory == null:
|
||||
return []
|
||||
var file_names: Array[String] = []
|
||||
for file_name in directory.get_files():
|
||||
var lower_name := file_name.to_lower()
|
||||
if not lower_name.begins_with("diagnostic_") and (lower_name.ends_with(".png") or lower_name.ends_with(".jpg") or lower_name.ends_with(".jpeg")):
|
||||
file_names.append(file_name)
|
||||
file_names.sort()
|
||||
return file_names
|
||||
|
||||
|
||||
func _candidate_file_names(candidate_directory: String, checkpoint_name: String) -> Array[String]:
|
||||
var candidates: Array[String] = []
|
||||
for pass_name in ["cold_process", "warm_revisit"]:
|
||||
var file_name := "%s__%s.png" % [checkpoint_name, pass_name]
|
||||
if FileAccess.file_exists(candidate_directory.path_join(file_name)):
|
||||
candidates.append(file_name)
|
||||
if candidates.is_empty():
|
||||
var exact_file_name := "%s.png" % checkpoint_name
|
||||
if FileAccess.file_exists(candidate_directory.path_join(exact_file_name)):
|
||||
candidates.append(exact_file_name)
|
||||
return candidates
|
||||
|
||||
|
||||
func _write_report(output_path: String, report: Dictionary) -> bool:
|
||||
var absolute_path := ProjectSettings.globalize_path(output_path)
|
||||
var error := DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir())
|
||||
if error != OK:
|
||||
push_error("RENDER_CHECKPOINT_DIFF: cannot create report directory: %s" % error_string(error))
|
||||
return false
|
||||
var report_file := FileAccess.open(absolute_path, FileAccess.WRITE)
|
||||
if report_file == null:
|
||||
push_error("RENDER_CHECKPOINT_DIFF: cannot write report: %s" % output_path)
|
||||
return false
|
||||
report_file.store_string(JSON.stringify(report, " "))
|
||||
return true
|
||||
|
||||
|
||||
func _run_self_test() -> int:
|
||||
var root := "user://render_checkpoint_diff_self_test"
|
||||
var reference_directory := root.path_join("reference")
|
||||
var identical_directory := root.path_join("identical")
|
||||
var changed_directory := root.path_join("changed")
|
||||
for directory_path in [reference_directory, identical_directory, changed_directory]:
|
||||
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(directory_path))
|
||||
_clear_directory_files(directory_path)
|
||||
var reference_image := Image.create(2, 2, false, Image.FORMAT_RGBA8)
|
||||
reference_image.fill(Color(0.25, 0.5, 0.75, 1.0))
|
||||
var changed_image := reference_image.duplicate()
|
||||
changed_image.set_pixel(0, 0, Color.WHITE)
|
||||
if reference_image.save_jpg(reference_directory.path_join("synthetic.jpg"), 1.0) != OK:
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: cannot save JPG reference")
|
||||
return 1
|
||||
for pass_name in ["cold_process", "warm_revisit"]:
|
||||
if reference_image.save_png(identical_directory.path_join("synthetic__%s.png" % pass_name)) != OK:
|
||||
return 1
|
||||
if changed_image.save_png(changed_directory.path_join("synthetic__%s.png" % pass_name)) != OK:
|
||||
return 1
|
||||
var options := {
|
||||
"pixel_threshold": DEFAULT_PIXEL_THRESHOLD,
|
||||
"mean_threshold": DEFAULT_MEAN_THRESHOLD,
|
||||
"changed_ratio_threshold": DEFAULT_CHANGED_RATIO_THRESHOLD,
|
||||
}
|
||||
var identical_report := _compare_directories(reference_directory, identical_directory, options)
|
||||
var changed_report := _compare_directories(reference_directory, changed_directory, options)
|
||||
if not identical_report.passed or changed_report.passed:
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
||||
return 1
|
||||
if reference_image.save_jpg(reference_directory.path_join("ignored.jpg"), 1.0) != OK:
|
||||
return 1
|
||||
var filtered_options := options.duplicate()
|
||||
filtered_options["only"] = "synthetic"
|
||||
var filtered_report := _compare_directories(reference_directory, identical_directory, filtered_options)
|
||||
if not filtered_report.passed or filtered_report.compared_count != 2:
|
||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: --only must exclude unrelated references")
|
||||
return 1
|
||||
print("RENDER_CHECKPOINT_DIFF SELF_TEST PASS")
|
||||
return 0
|
||||
|
||||
|
||||
func _clear_directory_files(directory_path: String) -> void:
|
||||
var directory := DirAccess.open(directory_path)
|
||||
if directory == null:
|
||||
return
|
||||
for file_name in directory.get_files():
|
||||
directory.remove(file_name)
|
||||
@@ -0,0 +1,166 @@
|
||||
extends SceneTree
|
||||
|
||||
## Reports published scene-tree geometry around calibrated renderer cameras.
|
||||
## Usage: godot --headless --path . --script res://src/tools/probe_render_camera_occluders.gd --
|
||||
## [--wait 3.0] [--output user://render_camera_occluders/report.json]
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const MAX_REPORTED_INTERSECTIONS := 20
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run_async.call_deferred()
|
||||
|
||||
|
||||
func _run_async() -> void:
|
||||
var arguments := OS.get_cmdline_user_args()
|
||||
var wait_seconds := float(_argument(arguments, "--wait", "3.0"))
|
||||
var output_path := _argument(arguments, "--output", "user://render_camera_occluders/report.json")
|
||||
var only_filter := _argument(arguments, "--only", "").to_lower()
|
||||
var manifest := _load_json(MANIFEST_PATH)
|
||||
var packed_scene: PackedScene = load(String(manifest.get("scene", "")))
|
||||
if packed_scene == null:
|
||||
push_error("CAMERA_OCCLUDER_PROBE: cannot load streaming scene")
|
||||
quit(1)
|
||||
return
|
||||
var world := packed_scene.instantiate() as Node3D
|
||||
var camera := Camera3D.new()
|
||||
camera.name = "OccluderProbeCamera"
|
||||
camera.current = true
|
||||
world.add_child(camera)
|
||||
world.set("camera_path", NodePath(camera.name))
|
||||
world.set("debug_streaming", false)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var results: Array[Dictionary] = []
|
||||
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
if not checkpoint.has("reference_wow_camera"):
|
||||
continue
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if not only_filter.is_empty() and not checkpoint_name.to_lower().contains(only_filter):
|
||||
continue
|
||||
var camera_position := _vector3(checkpoint.get("camera", []))
|
||||
var target_position := _vector3(checkpoint.get("target", []))
|
||||
camera.global_position = camera_position
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera_position, true)
|
||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||
var geometry_nodes: Array[Node3D] = []
|
||||
_collect_geometry_nodes(world, geometry_nodes)
|
||||
var containing: Array[Dictionary] = []
|
||||
var intersecting: Array[Dictionary] = []
|
||||
for geometry_node in geometry_nodes:
|
||||
var world_aabb := _geometry_world_aabb(geometry_node)
|
||||
if world_aabb.size.is_zero_approx():
|
||||
continue
|
||||
var record := _geometry_record(geometry_node, world_aabb, camera_position)
|
||||
if world_aabb.has_point(camera_position):
|
||||
containing.append(record)
|
||||
var intersection = world_aabb.intersects_segment(camera_position, target_position)
|
||||
if intersection != null:
|
||||
record["intersection_distance"] = camera_position.distance_to(intersection as Vector3)
|
||||
intersecting.append(record)
|
||||
intersecting.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||
return float(a.intersection_distance) < float(b.intersection_distance))
|
||||
if intersecting.size() > MAX_REPORTED_INTERSECTIONS:
|
||||
intersecting.resize(MAX_REPORTED_INTERSECTIONS)
|
||||
var result := {
|
||||
"name": checkpoint_name,
|
||||
"geometry_node_count": geometry_nodes.size(),
|
||||
"camera_containing_geometry": containing,
|
||||
"segment_intersecting_geometry": intersecting,
|
||||
}
|
||||
results.append(result)
|
||||
print("CAMERA_OCCLUDERS name=%s geometry=%d containing=%d segment=%d" % [
|
||||
checkpoint_name, geometry_nodes.size(), containing.size(), intersecting.size()])
|
||||
|
||||
var report := {
|
||||
"schema_version": 1,
|
||||
"created_utc": Time.get_datetime_string_from_system(true, true),
|
||||
"wait_seconds": wait_seconds,
|
||||
"coverage": "scene_tree_only; RenderingServer RID-only instances are excluded",
|
||||
"results": results,
|
||||
}
|
||||
if not _write_json(output_path, report):
|
||||
quit(1)
|
||||
return
|
||||
world.queue_free()
|
||||
quit(0 if not results.is_empty() else 1)
|
||||
|
||||
|
||||
func _collect_geometry_nodes(node: Node, output: Array[Node3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
var mesh_instance := node as MeshInstance3D
|
||||
if mesh_instance.mesh != null and not mesh_instance.name.begins_with("TileLOD"):
|
||||
output.append(mesh_instance)
|
||||
elif node is MultiMeshInstance3D:
|
||||
var multimesh_instance := node as MultiMeshInstance3D
|
||||
if multimesh_instance.multimesh != null:
|
||||
output.append(multimesh_instance)
|
||||
for child in node.get_children():
|
||||
_collect_geometry_nodes(child, output)
|
||||
|
||||
|
||||
func _geometry_world_aabb(node: Node3D) -> AABB:
|
||||
if node is MeshInstance3D:
|
||||
return node.global_transform * (node as MeshInstance3D).mesh.get_aabb()
|
||||
if node is MultiMeshInstance3D:
|
||||
return node.global_transform * (node as MultiMeshInstance3D).multimesh.get_aabb()
|
||||
return AABB()
|
||||
|
||||
|
||||
func _geometry_record(node: Node3D, world_aabb: AABB, camera_position: Vector3) -> Dictionary:
|
||||
var node_path := String(node.get_path())
|
||||
var category := "geometry"
|
||||
if node_path.contains("/M2s/") or node is MultiMeshInstance3D:
|
||||
category = "m2"
|
||||
elif node_path.contains("/WMOs/"):
|
||||
category = "wmo"
|
||||
return {
|
||||
"category": category,
|
||||
"node_path": node_path,
|
||||
"distance_to_center": camera_position.distance_to(world_aabb.get_center()),
|
||||
"aabb_position": _vector3_array(world_aabb.position),
|
||||
"aabb_size": _vector3_array(world_aabb.size),
|
||||
}
|
||||
|
||||
|
||||
func _vector3(value_variant) -> Vector3:
|
||||
if not (value_variant is Array) or value_variant.size() != 3:
|
||||
return Vector3.ZERO
|
||||
return Vector3(float(value_variant[0]), float(value_variant[1]), float(value_variant[2]))
|
||||
|
||||
|
||||
func _vector3_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
func _argument(arguments: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var index := arguments.find(name)
|
||||
if index >= 0 and index + 1 < arguments.size():
|
||||
return arguments[index + 1]
|
||||
return default_value
|
||||
|
||||
|
||||
func _load_json(path: String) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
return parsed if parsed is Dictionary else {}
|
||||
|
||||
|
||||
func _write_json(path: String, value: Dictionary) -> bool:
|
||||
var absolute_path := ProjectSettings.globalize_path(path)
|
||||
if DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir()) != OK:
|
||||
return false
|
||||
var file := FileAccess.open(absolute_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_string(JSON.stringify(value, " "))
|
||||
return true
|
||||
@@ -0,0 +1,250 @@
|
||||
extends SceneTree
|
||||
|
||||
## Measures camera clearance against the active rendered terrain mesh.
|
||||
## Usage: godot --path . --script res://src/tools/probe_render_terrain_height.gd --
|
||||
## [--wait 3.0] [--output user://render_terrain_height/report.json]
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const TILE_SIZE := 533.33333
|
||||
const RAY_HEIGHT := 5000.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_run_async.call_deferred()
|
||||
|
||||
|
||||
func _run_async() -> void:
|
||||
var arguments := OS.get_cmdline_user_args()
|
||||
var wait_seconds := float(_argument(arguments, "--wait", "3.0"))
|
||||
var output_path := _argument(arguments, "--output", "user://render_terrain_height/report.json")
|
||||
var only_filter := _argument(arguments, "--only", "").to_lower()
|
||||
var require_all := arguments.has("--require-all")
|
||||
var manifest := _load_json(MANIFEST_PATH)
|
||||
if manifest.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
var packed_scene: PackedScene = load(String(manifest.get("scene", "")))
|
||||
if packed_scene == null:
|
||||
push_error("TERRAIN_HEIGHT_PROBE: cannot load streaming scene")
|
||||
quit(1)
|
||||
return
|
||||
var world := packed_scene.instantiate() as Node3D
|
||||
if world == null:
|
||||
push_error("TERRAIN_HEIGHT_PROBE: streaming scene root is not Node3D")
|
||||
quit(1)
|
||||
return
|
||||
var camera := Camera3D.new()
|
||||
camera.current = true
|
||||
world.add_child(camera)
|
||||
world.set("camera_path", NodePath(camera.name))
|
||||
world.set("debug_streaming", false)
|
||||
get_root().add_child(world)
|
||||
await process_frame
|
||||
await process_frame
|
||||
|
||||
var results: Array[Dictionary] = []
|
||||
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
if not checkpoint.has("reference_wow_camera"):
|
||||
continue
|
||||
if not only_filter.is_empty() and not String(checkpoint.get("name", "")).to_lower().contains(only_filter):
|
||||
continue
|
||||
var camera_position := _vector3(checkpoint.get("camera", []))
|
||||
camera.global_position = camera_position
|
||||
if world.has_method("_refresh_streaming_targets_at"):
|
||||
world.call("_refresh_streaming_targets_at", camera_position, true)
|
||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||
var terrain_sample := _sample_terrain(world, camera_position)
|
||||
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
|
||||
terrain_sample["camera_y"] = camera_position.y
|
||||
if terrain_sample.has("terrain_height"):
|
||||
terrain_sample["camera_clearance"] = camera_position.y - float(terrain_sample.terrain_height)
|
||||
results.append(terrain_sample)
|
||||
|
||||
var report := {
|
||||
"schema_version": 1,
|
||||
"created_utc": Time.get_datetime_string_from_system(true, true),
|
||||
"wait_seconds": wait_seconds,
|
||||
"results": results,
|
||||
}
|
||||
if not _write_json(output_path, report):
|
||||
quit(1)
|
||||
return
|
||||
var sampled_count := 0
|
||||
for result in results:
|
||||
if result.has("terrain_height"):
|
||||
sampled_count += 1
|
||||
print("TERRAIN_HEIGHT name=%s status=%s camera_y=%.3f terrain=%s clearance=%s" % [
|
||||
result.get("name", "checkpoint"),
|
||||
result.get("status", "unknown"),
|
||||
float(result.get("camera_y", 0.0)),
|
||||
str(result.get("terrain_height", "n/a")),
|
||||
str(result.get("camera_clearance", "n/a")),
|
||||
])
|
||||
print("TERRAIN_HEIGHT_PROBE sampled=%d total=%d report=%s" % [sampled_count, results.size(), output_path])
|
||||
world.queue_free()
|
||||
var failed := sampled_count == 0 or (require_all and sampled_count != results.size())
|
||||
quit(1 if failed else 0)
|
||||
|
||||
|
||||
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
|
||||
var tile_coordinate := Vector2i(
|
||||
int(floor(world_position.x / TILE_SIZE)),
|
||||
int(floor(world_position.z / TILE_SIZE)))
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
var highest_terrain_height := -INF
|
||||
var intersected_tile_key := ""
|
||||
var ready_mesh_count := 0
|
||||
for tile_y in range(tile_coordinate.y - 1, tile_coordinate.y + 2):
|
||||
for tile_x in range(tile_coordinate.x - 1, tile_coordinate.x + 2):
|
||||
var tile_key := "%d_%d" % [tile_x, tile_y]
|
||||
if not tile_states.has(tile_key):
|
||||
continue
|
||||
var state: Dictionary = tile_states[tile_key]
|
||||
var terrain_height_variant = _intersect_terrain_state(state, world_position)
|
||||
if terrain_height_variant == null:
|
||||
continue
|
||||
ready_mesh_count += 1
|
||||
var terrain_height := float(terrain_height_variant)
|
||||
if terrain_height > highest_terrain_height:
|
||||
highest_terrain_height = terrain_height
|
||||
intersected_tile_key = tile_key
|
||||
if not is_finite(highest_terrain_height):
|
||||
var missing_result := {
|
||||
"status": "no_intersection" if ready_mesh_count > 0 else "mesh_not_ready",
|
||||
"tile": "%d_%d" % [tile_coordinate.x, tile_coordinate.y],
|
||||
}
|
||||
missing_result.merge(_tile_runtime_diagnostic(world, String(missing_result.tile), world_position), true)
|
||||
if bool(missing_result.get("quality_mesh_present", false)) or bool(missing_result.get("tile_lod_mesh_present", false)):
|
||||
missing_result["status"] = "no_intersection"
|
||||
missing_result.merge(_nearest_terrain_sample(world, world_position), true)
|
||||
if missing_result.get("nearest_sample_distance", null) != null:
|
||||
missing_result["status"] = "sampled_nearby"
|
||||
missing_result["terrain_height"] = float(missing_result.nearest_sample_height)
|
||||
return missing_result
|
||||
return {
|
||||
"status": "sampled",
|
||||
"tile": intersected_tile_key,
|
||||
"terrain_height": highest_terrain_height,
|
||||
}
|
||||
|
||||
|
||||
func _intersect_terrain_state(state: Dictionary, world_position: Vector3):
|
||||
var terrain_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
||||
if terrain_mesh == null:
|
||||
terrain_mesh = state.get("tile_lod_mesh", null)
|
||||
if terrain_mesh == null:
|
||||
return null
|
||||
var triangle_mesh := terrain_mesh.generate_triangle_mesh()
|
||||
if triangle_mesh == null:
|
||||
return null
|
||||
var tile_root := state.get("root", null) as Node3D
|
||||
if tile_root == null:
|
||||
return null
|
||||
var inverse_transform := tile_root.global_transform.affine_inverse()
|
||||
var local_ray_origin := inverse_transform * Vector3(world_position.x, RAY_HEIGHT, world_position.z)
|
||||
var local_ray_direction := inverse_transform.basis * Vector3.DOWN
|
||||
var intersection: Dictionary = triangle_mesh.intersect_ray(local_ray_origin, local_ray_direction)
|
||||
if intersection.is_empty():
|
||||
return null
|
||||
var local_hit: Vector3 = intersection.get("position", Vector3.ZERO)
|
||||
var world_hit := tile_root.global_transform * local_hit
|
||||
return world_hit.y
|
||||
|
||||
|
||||
func _tile_runtime_diagnostic(world: Node3D, tile_key: String, world_position: Vector3) -> Dictionary:
|
||||
var available_tiles: Dictionary = world.get("_available_tiles")
|
||||
var loading_tasks: Dictionary = world.get("_tile_loading_tasks")
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
var load_queue: Array = world.get("_tile_load_queue")
|
||||
var queued_index := -1
|
||||
for index in load_queue.size():
|
||||
var request: Dictionary = load_queue[index]
|
||||
if String(request.get("key", "")) == tile_key:
|
||||
queued_index = index
|
||||
break
|
||||
var diagnostic := {
|
||||
"available": available_tiles.has(tile_key),
|
||||
"queued_index": queued_index,
|
||||
"loading": loading_tasks.has(tile_key),
|
||||
"state_present": tile_states.has(tile_key),
|
||||
"load_queue_size": load_queue.size(),
|
||||
}
|
||||
if tile_states.has(tile_key):
|
||||
var state: Dictionary = tile_states[tile_key]
|
||||
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
||||
var tile_lod_mesh: Mesh = state.get("tile_lod_mesh", null)
|
||||
diagnostic["quality_mesh_present"] = quality_mesh != null
|
||||
diagnostic["tile_lod_mesh_present"] = tile_lod_mesh != null
|
||||
diagnostic["quality_source"] = String(state.get("quality_terrain_source", ""))
|
||||
diagnostic["tile_lod"] = int(state.get("tile_lod", -1))
|
||||
var tile_root := state.get("root", null) as Node3D
|
||||
if tile_root != null:
|
||||
var local_position := tile_root.global_transform.affine_inverse() * world_position
|
||||
diagnostic["tile_root_position"] = _vector3_array(tile_root.global_position)
|
||||
diagnostic["probe_local_position"] = _vector3_array(local_position)
|
||||
var diagnostic_mesh := quality_mesh if quality_mesh != null else tile_lod_mesh
|
||||
if diagnostic_mesh != null:
|
||||
var mesh_aabb := diagnostic_mesh.get_aabb()
|
||||
diagnostic["mesh_aabb_position"] = _vector3_array(mesh_aabb.position)
|
||||
diagnostic["mesh_aabb_size"] = _vector3_array(mesh_aabb.size)
|
||||
return diagnostic
|
||||
|
||||
|
||||
func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictionary:
|
||||
var tile_states: Dictionary = world.get("_tile_states")
|
||||
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
|
||||
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
|
||||
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
|
||||
var sample_tile := Vector2i(
|
||||
int(floor(sample_position.x / TILE_SIZE)),
|
||||
int(floor(sample_position.z / TILE_SIZE)))
|
||||
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
|
||||
if not tile_states.has(sample_key):
|
||||
continue
|
||||
var height_variant = _intersect_terrain_state(tile_states[sample_key], sample_position)
|
||||
if height_variant != null:
|
||||
return {
|
||||
"nearest_sample_distance": radius,
|
||||
"nearest_sample_height": float(height_variant),
|
||||
"nearest_sample_tile": sample_key,
|
||||
}
|
||||
return {"nearest_sample_distance": null}
|
||||
|
||||
|
||||
func _vector3_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
func _vector3(value_variant) -> Vector3:
|
||||
if not (value_variant is Array) or value_variant.size() != 3:
|
||||
return Vector3.ZERO
|
||||
return Vector3(float(value_variant[0]), float(value_variant[1]), float(value_variant[2]))
|
||||
|
||||
|
||||
func _argument(arguments: PackedStringArray, name: String, default_value: String) -> String:
|
||||
var index := arguments.find(name)
|
||||
if index >= 0 and index + 1 < arguments.size():
|
||||
return arguments[index + 1]
|
||||
return default_value
|
||||
|
||||
|
||||
func _load_json(path: String) -> Dictionary:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
return parsed if parsed is Dictionary else {}
|
||||
|
||||
|
||||
func _write_json(path: String, value: Dictionary) -> bool:
|
||||
var absolute_path := ProjectSettings.globalize_path(path)
|
||||
if DirAccess.make_dir_recursive_absolute(absolute_path.get_base_dir()) != OK:
|
||||
return false
|
||||
var file := FileAccess.open(absolute_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_string(JSON.stringify(value, " "))
|
||||
return true
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"profile": "Blizzlike335",
|
||||
"scene": "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||
"map": "Azeroth",
|
||||
"quality_preset": "High",
|
||||
"viewport": [1280, 900],
|
||||
"fidelity_comparison_viewport": [2560, 1440],
|
||||
"camera_fov": 62.0,
|
||||
"default_wait_seconds": 8.0,
|
||||
"default_measure_seconds": 3.0,
|
||||
"comparison_budgets": {
|
||||
"frame_ms_p95_max_regression_percent": 10.0,
|
||||
"frame_ms_p99_max_regression_percent": 10.0,
|
||||
"max_hitch_ms_max_regression_percent": 10.0,
|
||||
"load_time_ms_max_regression_percent": 10.0,
|
||||
"memory_bytes_max_regression_percent": 10.0,
|
||||
"visual_mean_perceptual_error_max": 0.015,
|
||||
"visual_changed_pixel_ratio_max": 0.01,
|
||||
"visual_pixel_error_threshold": 0.04,
|
||||
"visual_diff": "automated thresholds require human approval; exact PNG hashes are evidence, not an animated-scene pass criterion"
|
||||
},
|
||||
"required_coverage": [
|
||||
"terrain",
|
||||
"adt_boundary",
|
||||
"dense_m2",
|
||||
"large_wmo",
|
||||
"liquid",
|
||||
"animated_m2",
|
||||
"sky_transition"
|
||||
],
|
||||
"cache_contract": {
|
||||
"baked_terrain": {"version": 5, "invalidate": "baked geometry or placement payload changes"},
|
||||
"streaming_terrain": {"version": 2, "invalidate": "streaming geometry or M2 unique_id payload changes"},
|
||||
"terrain_splat": {"version": 1, "invalidate": "splat resource payload changes"},
|
||||
"terrain_control_splat": {"version": 3, "invalidate": "control atlas, layer map, or texture array payload changes"},
|
||||
"wmo_streaming": {"version": 2, "invalidate": "WMO render geometry payload changes"},
|
||||
"wmo_builder": {"version": 2, "invalidate": "WMO scene transform or builder payload changes"},
|
||||
"m2_material": {"version": 2, "invalidate": "M2 material or custom vertex payload changes"}
|
||||
},
|
||||
"checkpoints": [
|
||||
{
|
||||
"name": "elwynn_terrain_overview",
|
||||
"coverage": ["terrain"],
|
||||
"camera": [16680.0, 180.0, 26220.0],
|
||||
"reference_wow_camera": [-9153.334, 386.666, 180.0],
|
||||
"target": [16800.0, 62.0, 26400.0],
|
||||
"player": [16800.0, 58.0, 26400.0],
|
||||
"time_hours": 13.0
|
||||
},
|
||||
{
|
||||
"name": "elwynn_adt_boundary",
|
||||
"coverage": ["adt_boundary"],
|
||||
"camera": [17020.0, 105.0, 26590.0],
|
||||
"reference_wow_camera": [-9523.334, 46.666, 105.0],
|
||||
"target": [17066.666, 62.0, 26666.666],
|
||||
"player": [17060.0, 58.0, 26660.0],
|
||||
"time_hours": 13.0
|
||||
},
|
||||
{
|
||||
"name": "goldshire_dense_m2",
|
||||
"coverage": ["dense_m2"],
|
||||
"camera": [16956.666, 150.0, 26466.666],
|
||||
"reference_wow_camera": [-9400.0, 110.0, 150.0],
|
||||
"target": [17015.0, 62.0, 26525.0],
|
||||
"player": [17015.0, 58.0, 26525.0],
|
||||
"time_hours": 13.0
|
||||
},
|
||||
{
|
||||
"name": "goldshire_inn_large_wmo",
|
||||
"coverage": ["large_wmo"],
|
||||
"camera": [17013.666, 72.0, 26451.666],
|
||||
"reference_wow_camera": [-9385.0, 53.0, 72.0],
|
||||
"target": [17042.27, 66.0, 26530.91],
|
||||
"player": [17042.27, 58.0, 26530.91],
|
||||
"time_hours": 13.0
|
||||
},
|
||||
{
|
||||
"name": "elwynn_waterfall_liquid",
|
||||
"coverage": ["liquid"],
|
||||
"camera": [16481.666, 190.0, 26366.666],
|
||||
"reference_wow_camera": [-9300.0, 585.0, 190.0],
|
||||
"target": [16518.84, 68.0, 26427.27],
|
||||
"player": [16518.84, 55.0, 26427.27],
|
||||
"time_hours": 13.0
|
||||
},
|
||||
{
|
||||
"name": "gryphon_roost_native_animation",
|
||||
"coverage": ["animated_m2"],
|
||||
"camera": [16980.0, 72.0, 26505.0],
|
||||
"target": [17000.0, 65.0, 26525.0],
|
||||
"player": [17000.0, 58.0, 26525.0],
|
||||
"time_hours": 13.0,
|
||||
"probe_model": "world/GENERIC/HUMAN/PASSIVE DOODADS/GryphonRoost/GryphonRoost01.m2",
|
||||
"probe_scale": 1.0
|
||||
},
|
||||
{
|
||||
"name": "elwynn_sky_dusk",
|
||||
"coverage": ["sky_transition"],
|
||||
"camera": [16680.0, 180.0, 26220.0],
|
||||
"target": [16800.0, 62.0, 26400.0],
|
||||
"player": [16800.0, 58.0, 26400.0],
|
||||
"time_hours": 19.0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
extends SceneTree
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const STREAMING_LOADER := preload("res://src/scenes/streaming/streaming_world_loader.gd")
|
||||
const STREAMING_TILE := preload("res://src/resources/streaming_adt_tile.gd")
|
||||
const SPLAT_TILE := preload("res://src/resources/splat_adt_tile.gd")
|
||||
const CONTROL_SPLAT_TILE := preload("res://src/resources/control_splat_adt_tile.gd")
|
||||
const WMO_STREAMING := preload("res://src/resources/wmo_streaming_resource.gd")
|
||||
const WMO_BUILDER := preload("res://addons/mpq_extractor/loaders/wmo_builder.gd")
|
||||
const M2_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var manifest := _load_manifest()
|
||||
if manifest.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var failures: Array[String] = []
|
||||
_expect(int(manifest.get("schema_version", 0)) == 1, "manifest schema must be version 1", failures)
|
||||
_expect(String(manifest.get("profile", "")) == "Blizzlike335", "manifest profile must be Blizzlike335", failures)
|
||||
_expect(ResourceLoader.exists(String(manifest.get("scene", ""))), "streaming scene must exist", failures)
|
||||
var fidelity_viewport: Array = manifest.get("fidelity_comparison_viewport", [])
|
||||
_expect(fidelity_viewport.size() == 2 and int(fidelity_viewport[0]) > 0 and int(fidelity_viewport[1]) > 0, "fidelity comparison viewport must contain positive width and height", failures)
|
||||
|
||||
var covered := {}
|
||||
var names := {}
|
||||
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
failures.append("checkpoint entry must be a dictionary")
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", ""))
|
||||
_expect(not checkpoint_name.is_empty(), "checkpoint name must not be empty", failures)
|
||||
_expect(not names.has(checkpoint_name), "duplicate checkpoint name: %s" % checkpoint_name, failures)
|
||||
names[checkpoint_name] = true
|
||||
for key in ["camera", "target", "player"]:
|
||||
var value = checkpoint.get(key, [])
|
||||
_expect(value is Array and value.size() == 3, "%s.%s must contain three coordinates" % [checkpoint_name, key], failures)
|
||||
for coverage_variant in checkpoint.get("coverage", []):
|
||||
covered[String(coverage_variant)] = true
|
||||
|
||||
for required_variant in manifest.get("required_coverage", []):
|
||||
var required := String(required_variant)
|
||||
_expect(covered.has(required), "missing checkpoint coverage: %s" % required, failures)
|
||||
|
||||
var comparison_budgets: Dictionary = manifest.get("comparison_budgets", {})
|
||||
for threshold_name in ["visual_mean_perceptual_error_max", "visual_changed_pixel_ratio_max", "visual_pixel_error_threshold"]:
|
||||
var threshold := float(comparison_budgets.get(threshold_name, -1.0))
|
||||
_expect(threshold >= 0.0 and threshold <= 1.0, "%s must be between 0 and 1" % threshold_name, failures)
|
||||
|
||||
var contract: Dictionary = manifest.get("cache_contract", {})
|
||||
var actual_versions := {
|
||||
"baked_terrain": STREAMING_LOADER.REQUIRED_BAKED_TILE_FORMAT_VERSION,
|
||||
"streaming_terrain": STREAMING_TILE.FORMAT_VERSION,
|
||||
"terrain_splat": SPLAT_TILE.FORMAT_VERSION,
|
||||
"terrain_control_splat": CONTROL_SPLAT_TILE.FORMAT_VERSION,
|
||||
"wmo_streaming": WMO_STREAMING.FORMAT_VERSION,
|
||||
"wmo_builder": WMO_BUILDER.WMO_BUILDER_FORMAT_VERSION,
|
||||
"m2_material": M2_BUILDER.MATERIAL_FORMAT_VERSION,
|
||||
}
|
||||
for cache_name in actual_versions:
|
||||
var entry: Dictionary = contract.get(cache_name, {})
|
||||
_expect(int(entry.get("version", -1)) == int(actual_versions[cache_name]), "cache version mismatch for %s" % cache_name, failures)
|
||||
_expect(not String(entry.get("invalidate", "")).is_empty(), "cache invalidation rule missing for %s" % cache_name, failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("RENDER_BASELINE_MANIFEST: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("RENDER_BASELINE_MANIFEST PASS checkpoints=%d coverage=%d caches=%d" % [names.size(), covered.size(), actual_versions.size()])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _load_manifest() -> Dictionary:
|
||||
var file := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
push_error("Cannot open render baseline manifest: %s" % MANIFEST_PATH)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
push_error("Render baseline manifest is not a JSON object")
|
||||
return {}
|
||||
return parsed
|
||||
|
||||
|
||||
func _expect(condition: bool, message: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(message)
|
||||
@@ -0,0 +1,88 @@
|
||||
extends SceneTree
|
||||
|
||||
## Verifies observed build 12340 camera coordinates against the current renderer
|
||||
## WoW X/Y/Z to Godot X/Y/Z convention. This diagnostic does not define the
|
||||
## future M01 production CoordinateMapper contract.
|
||||
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const WOW_WORLD_CENTER := 17066.666
|
||||
const MAXIMUM_POSITION_ERROR := 0.002
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var manifest := _load_manifest()
|
||||
if manifest.is_empty():
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var failures: Array[String] = []
|
||||
var calibrated_count := 0
|
||||
var maximum_round_trip_error := 0.0
|
||||
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||
if not (checkpoint_variant is Dictionary):
|
||||
continue
|
||||
var checkpoint: Dictionary = checkpoint_variant
|
||||
var reference_wow_camera_variant = checkpoint.get("reference_wow_camera", null)
|
||||
if not (reference_wow_camera_variant is Array):
|
||||
continue
|
||||
var reference_wow_camera: Array = reference_wow_camera_variant
|
||||
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||
if reference_wow_camera.size() != 3:
|
||||
failures.append("%s reference_wow_camera must contain three coordinates" % checkpoint_name)
|
||||
continue
|
||||
var expected_godot_camera := _array_to_vector3(checkpoint.get("camera", []))
|
||||
var reference_wow_position := _array_to_vector3(reference_wow_camera)
|
||||
var mapped_godot_position := _wow_to_godot(reference_wow_position)
|
||||
var round_trip_wow_position := _godot_to_wow(mapped_godot_position)
|
||||
var mapping_error := mapped_godot_position.distance_to(expected_godot_camera)
|
||||
var round_trip_error := round_trip_wow_position.distance_to(reference_wow_position)
|
||||
maximum_round_trip_error = maxf(maximum_round_trip_error, maxf(mapping_error, round_trip_error))
|
||||
if mapping_error > MAXIMUM_POSITION_ERROR:
|
||||
failures.append("%s maps %.6f units away from manifest camera" % [checkpoint_name, mapping_error])
|
||||
if round_trip_error > MAXIMUM_POSITION_ERROR:
|
||||
failures.append("%s round-trip error is %.6f" % [checkpoint_name, round_trip_error])
|
||||
calibrated_count += 1
|
||||
|
||||
if calibrated_count < 5:
|
||||
failures.append("expected at least five build 12340 camera calibrations, found %d" % calibrated_count)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("RENDER_COORDINATE_CALIBRATION: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print("RENDER_COORDINATE_CALIBRATION PASS points=%d maximum_error=%.6f" % [calibrated_count, maximum_round_trip_error])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _wow_to_godot(wow_position: Vector3) -> Vector3:
|
||||
return Vector3(
|
||||
WOW_WORLD_CENTER - wow_position.y,
|
||||
wow_position.z,
|
||||
WOW_WORLD_CENTER - wow_position.x
|
||||
)
|
||||
|
||||
|
||||
func _godot_to_wow(godot_position: Vector3) -> Vector3:
|
||||
return Vector3(
|
||||
WOW_WORLD_CENTER - godot_position.z,
|
||||
WOW_WORLD_CENTER - godot_position.x,
|
||||
godot_position.y
|
||||
)
|
||||
|
||||
|
||||
func _array_to_vector3(value_variant) -> Vector3:
|
||||
if not (value_variant is Array) or value_variant.size() != 3:
|
||||
return Vector3.ZERO
|
||||
return Vector3(float(value_variant[0]), float(value_variant[1]), float(value_variant[2]))
|
||||
|
||||
|
||||
func _load_manifest() -> Dictionary:
|
||||
var manifest_file := FileAccess.open(MANIFEST_PATH, FileAccess.READ)
|
||||
if manifest_file == null:
|
||||
push_error("Cannot open renderer baseline manifest: %s" % MANIFEST_PATH)
|
||||
return {}
|
||||
var parsed = JSON.parse_string(manifest_file.get_as_text())
|
||||
if not (parsed is Dictionary):
|
||||
push_error("Renderer baseline manifest is not a JSON object")
|
||||
return {}
|
||||
return parsed
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
## Steps
|
||||
|
||||
- [ ] Зафиксировать версию Godot, backend, hardware/profile и cache state.
|
||||
- [ ] Выбрать контрольные позиции: terrain, ADT boundary, dense M2, large WMO, liquid, animated M2, sky transition.
|
||||
- [ ] Объединить существующие renderer smoke scripts в документированный запуск без изменения visual behavior.
|
||||
- [ ] Снять deterministic screenshots и metadata для каждой позиции.
|
||||
- [ ] Записать cold/warm показатели frame time p50/p95/p99, max hitch, memory, queues и load time.
|
||||
- [ ] Зафиксировать cache format versions и правила invalidation.
|
||||
- [ ] Добавить regression manifest, не содержащий proprietary assets.
|
||||
- [ ] Обновить `RENDER.md` ссылкой на baseline и известные расхождения с клиентом 3.3.5a.
|
||||
- [x] Зафиксировать версию Godot, backend, hardware/profile и cache state.
|
||||
- [x] Выбрать контрольные позиции: terrain, ADT boundary, dense M2, large WMO, liquid, animated M2, sky transition.
|
||||
- [x] Объединить существующие renderer smoke scripts в документированный запуск без изменения visual behavior.
|
||||
- [x] Снять deterministic screenshots и metadata для каждой позиции.
|
||||
- [x] Записать cold/warm показатели frame time p50/p95/p99, max hitch, memory, queues и load time.
|
||||
- [x] Зафиксировать cache format versions и правила invalidation.
|
||||
- [x] Добавить regression manifest, не содержащий proprietary assets.
|
||||
- [x] Обновить `RENDER.md` ссылкой на baseline и известные расхождения с клиентом 3.3.5a.
|
||||
|
||||
## Fidelity evidence
|
||||
|
||||
@@ -38,10 +38,10 @@ godot --headless --path . --script res://src/tools/capture_render_checkpoints.gd
|
||||
|
||||
## Evidence
|
||||
|
||||
- Date:
|
||||
- Revision/worktree:
|
||||
- Commands:
|
||||
- Results:
|
||||
- Fidelity comparison:
|
||||
- Changed files:
|
||||
- Remaining risks:
|
||||
- Date: 2026-07-10
|
||||
- Revision/worktree: `93bfe11` + worktree baseline changes.
|
||||
- Commands: `.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1 -Output user://render_baseline_runner_test`; `.\tools\run_render_baseline.ps1 -Output user://render_baseline_m00_2026-07-10 -CacheState existing`.
|
||||
- Results: headless load, renderer material smoke, M2 unique-id dedupe and manifest contract passed. Full run produced 14 PNGs and 14 result records for 7 cold/warm checkpoints. Actual renderer was Godot 4.6.1, Vulkan Forward+, NVIDIA RTX 5070, 1280x900, High profile, with all seven cache families present. Cold p95 range was 31.585..86.537 ms with max hitch 258.456 ms; warm p95 range was 18.824..38.085 ms with max hitch 163.363 ms. The final streaming snapshots had an empty tile queue. Report: `user://render_baseline_m00_2026-07-10/report.json`.
|
||||
- Fidelity comparison: fixed positions/time/profile and current visual gaps are recorded, but no approved paired screenshots from the original WoW 3.3.5a build 12340 were available. This baseline does not claim parity.
|
||||
- Changed files: `RENDER.md`, `docs/RENDER_BASELINE.md`, `src/scenes/streaming/streaming_world_loader.gd`, `src/tools/capture_render_checkpoints.gd`, `src/tools/render_baseline_manifest.json`, `src/tools/verify_render_baseline_manifest.gd`, `tools/run_render_baseline.ps1`, `targets/00-render-baseline.md`.
|
||||
- Remaining risks: original-client paired comparison is still required before DONE; D3D12 descriptor heap creation failed and the measured run fell back to Vulkan; shutdown reported leaked renderer RIDs/resources; current captures intentionally preserve visible terrain/placement/material/animation gaps, including the synthetic native animated M2 probe; perceptual image diff/tolerance is documented but not automated yet.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
- [ ] Добавить schema migration `previous → current`, backup и fixtures.
|
||||
- [ ] Реализовать ordered overlays без изменения extracted upstream assets.
|
||||
- [ ] Обеспечить headless validate/build parity с Editor.
|
||||
- [ ] Зарезервировать versioned navigation profile/artifact manifest: source geometry hash, backend/version, parameters и tile checksums без хранения generated cache как исходника.
|
||||
|
||||
## Exit criteria
|
||||
|
||||
|
||||
@@ -13,13 +13,16 @@
|
||||
- [ ] Реализовать transform gizmo, snapping и coordinate display.
|
||||
- [ ] Создать/переместить/удалить creature spawn через undoable commands.
|
||||
- [ ] Добавить waypoint/patrol path minimum.
|
||||
- [ ] Добавить optional `recast-rs` CLI spike: canonical triangle export, pinned bake profile и versioned OpenWC navigation cache.
|
||||
- [ ] Показать walkability overlay и diagnostics для spawn вне navmesh и недостижимого patrol segment.
|
||||
- [ ] Сравнить synthetic/one-tile результат `recast-rs` с C++ Recast reference; не заявлять server mmap compatibility.
|
||||
- [ ] Показать validation и semantic/SQL diff.
|
||||
- [ ] Применить change set к dev DB и выполнить post-apply verification.
|
||||
- [ ] Повторно импортировать snapshot и подтвердить отсутствие неожиданного diff.
|
||||
|
||||
## Exit criteria
|
||||
|
||||
NPC, поставленный на известную точку карты, появляется на dev-core с правильными position/orientation/template и переживает повторный import.
|
||||
NPC, поставленный на известную walkable точку карты, появляется на dev-core с правильными position/orientation/template и переживает повторный import. Navigation overlay воспроизводится из cache, а отсутствие optional backend отображается как `Not run`, не как успешная проверка.
|
||||
|
||||
## Evidence
|
||||
|
||||
|
||||
@@ -17,11 +17,14 @@ Headless protocol sandbox подключается к build 12340 server и ст
|
||||
- [ ] Movement packet family и server time.
|
||||
- [ ] Redacted packet capture/replay и malformed corpus.
|
||||
- [ ] Disconnect/reconnect reasons и structured diagnostics.
|
||||
- [ ] Исследовать Wowser auth/realm/character/world lifecycle и packet handlers как независимый 3.3.5a reference; выписать совпадения/расхождения с TrinityCore/AzerothCore и не переносить WebSocket proxy architecture.
|
||||
|
||||
## Fidelity evidence
|
||||
|
||||
Exact-byte fixtures сверяются с оригинальным клиентом или соответствующей server-core реализацией. Network handler не публикует raw packets за пределы adapter.
|
||||
|
||||
Wowser может подтвердить найденный edge case или дать тестовую идею, но не является authoritative protocol specification.
|
||||
|
||||
## Exit criteria
|
||||
|
||||
Replay детерминирован; live sandbox проходит auth → realm → character → world и создаёт entity registry без renderer/UI.
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
Последовательно закрыть всю матрицу `docs/CLIENT_FEATURES.md` до подтверждённой совместимости.
|
||||
|
||||
Работа выполняется completeness waves C1–C8 из [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md) с обязательным чтением gameplay, UI/audio, server и quality subsystem plans. Этот target не разрешает массово реализовывать все пункты одновременно.
|
||||
|
||||
## Required order
|
||||
|
||||
- [ ] Combat, spells, casting, cooldowns, auras, death/resurrection.
|
||||
@@ -17,7 +19,11 @@
|
||||
- [ ] PvP, duel, battlegrounds, arenas и LFG.
|
||||
- [ ] Vehicles, cinematics, weather, audio и loading transitions.
|
||||
- [ ] Character/M2/WMO/liquid/material/animation fidelity gaps.
|
||||
- [ ] Lua 5.1, FrameXML, default UI и addon compatibility tiers.
|
||||
- [ ] Провести Lua VM compatibility spike: pinned `rilua` против PUC-Rio 5.1.1 и оригинального клиента на official + WoW-specific corpus.
|
||||
- [ ] Выбрать backend за внутренним `LuaRuntime` interface; зафиксировать ADR, license, version/update policy и performance budget.
|
||||
- [ ] Реализовать независимо от VM: TOC, FrameXML, WoW API, events, SavedVariables, secure actions, combat lockdown и taint.
|
||||
- [ ] Проверить default UI и addon compatibility tiers; без taint/security tests статус `Verified` запрещён.
|
||||
- [ ] Использовать Wowser и его выделенные client/pipeline репозитории как coverage reference для пропущенных client lifecycle, asset-loading, UI/XML/Lua и browser-independent data-flow решений; каждое заимствованное предположение подтвердить fidelity test.
|
||||
- [ ] Localization, settings, accessibility и recovery.
|
||||
|
||||
Каждая строка превращается в отдельный vertical-slice spec с acceptance criteria, fixtures и fidelity evidence. Массовая реализация opcodes или Lua stubs без работающего сценария запрещена.
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
- [ ] Loot, quests, localization и instance messages.
|
||||
- [ ] SmartAI generation и/или versioned core script skeleton.
|
||||
- [ ] Validation: reachability, references, door-state graph, wipe recovery и budgets.
|
||||
- [ ] Интегрировать `recast-rs` navigation build для connected regions, patrol/escort routes, off-mesh links и dynamic door obstacles.
|
||||
- [ ] Добавить navigation profiles для player/NPC размеров и regression fixtures для закрытых/открытых encounter states.
|
||||
- [ ] Провести отдельный compatibility spike перед любым экспортом TrinityCore/AzerothCore mmaps; до доказательства генерировать только OpenWC editor cache.
|
||||
- [ ] Offline preview, server deploy и group playtest launcher.
|
||||
- [ ] Deterministic encounter simulation cases и multiplayer soak.
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
# OpenWC Full Development Roadmap
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ — полная инженерная карта разработки OpenWC. Статусы и последовательность исполняемых milestones остаются в [`README.md`](README.md); детальные subsystem plans находятся в [`roadmap/`](roadmap/). При расхождении текущий target определяет, что делать сейчас, а этот roadmap — как решение должно развиваться целиком.
|
||||
|
||||
Godot-specific implementation rules обязательны из [`../docs/GODOT_BEST_PRACTICES.md`](../docs/GODOT_BEST_PRACTICES.md).
|
||||
|
||||
## Итоговый продукт
|
||||
|
||||
OpenWC должен предоставить четыре согласованных режима:
|
||||
|
||||
1. `Blizzlike335` — клиент WoW 3.3.5a build 12340 с максимально точным наблюдаемым поведением.
|
||||
2. `Enhanced` — opt-in графические, UX и accessibility улучшения, не меняющие wire protocol и server authority без отдельного profile.
|
||||
3. `AuthoringStudio` — инструменты мира, контента, БД и playtest внутри Godot Editor.
|
||||
4. `HeadlessTools` — import, validation, bake, packet replay, content build и CI без GUI.
|
||||
|
||||
Один кодовый путь должен обслуживать runtime и editor preview через публичные contracts. Generated preview nodes, DB records и caches не являются источником истины.
|
||||
|
||||
## Текущее положение
|
||||
|
||||
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
|
||||
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
|
||||
- Renderer baseline M00 создан; до `DONE` не хватает paired comparison с оригинальным клиентом и закрытия известных diagnostic gaps.
|
||||
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
|
||||
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
|
||||
|
||||
## Инженерные программы
|
||||
|
||||
| Код | Программа | Детальный план |
|
||||
|---|---|---|
|
||||
| FND | Foundation, application и data | [01-foundation-and-data.md](roadmap/01-foundation-and-data.md) |
|
||||
| RND | Renderer и graphics | [02-rendering-and-graphics.md](roadmap/02-rendering-and-graphics.md) |
|
||||
| NET | Protocol, session и synchronization | [03-network-and-session.md](roadmap/03-network-and-session.md) |
|
||||
| GMP | Gameplay systems | [04-gameplay-systems.md](roadmap/04-gameplay-systems.md) |
|
||||
| UIA | UI, Lua, addons, audio | [05-ui-lua-audio.md](roadmap/05-ui-lua-audio.md) |
|
||||
| EDT | Godot Editor и content authoring | [06-editor-and-content-tools.md](roadmap/06-editor-and-content-tools.md) |
|
||||
| SRV | TrinityCore/AzerothCore integration | [07-server-emulator-integration.md](roadmap/07-server-emulator-integration.md) |
|
||||
| QAR | Fidelity, tests, CI и releases | [08-quality-and-release.md](roadmap/08-quality-and-release.md) |
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```text
|
||||
M00 renderer baseline
|
||||
└─ M01 coordinates/contracts
|
||||
└─ M02 input/movement/camera/presentation split
|
||||
└─ M03 renderer facade
|
||||
├─ M04 editor shell → M05 content → M06 server adapters → M07 world editor
|
||||
└─ M08 network → M09 gameplay domain → M10 playable client
|
||||
M07 + M10 → M11 quest end-to-end
|
||||
└─ M12 completeness waves
|
||||
└─ M13 dungeon authoring
|
||||
```
|
||||
|
||||
M12 не является одним большим этапом. Он выполняется последовательными completeness waves:
|
||||
|
||||
1. `C1 Core loop` — combat, spells, auras, death, loot, inventory, quests.
|
||||
2. `C2 Character` — creation/customization, equipment, animation, pets, mounts.
|
||||
3. `C3 World` — map/minimap, exploration, taxi, transports, weather, audio.
|
||||
4. `C4 Services` — gossip, vendors, trainers, bank, mail, auction, professions.
|
||||
5. `C5 Social` — chat, friends, ignore, party, raid, guild, calendar.
|
||||
6. `C6 Instances/PvP` — lockouts, LFG, vehicles, battlegrounds, arenas, outdoor PvP.
|
||||
7. `C7 UI/addons` — FrameXML, Lua 5.1.1, secure execution, default UI, compatibility tiers.
|
||||
8. `C8 Fidelity/polish` — complete render/material/audio coverage, recovery, accessibility, localization и performance.
|
||||
|
||||
Каждая wave должна завершаться работающим gameplay scenario, а не количеством реализованных opcodes или таблиц.
|
||||
|
||||
## Milestone delivery matrix
|
||||
|
||||
| Milestone | Основные work packages | Обязательный результат |
|
||||
|---|---|---|
|
||||
| M00 | RND baseline, QAR metrics/fidelity lab | Воспроизводимые checkpoints и gaps |
|
||||
| M01 | FND typed coordinates/IDs/contracts | Server/ADT/Godot round-trip и StreamingFocus |
|
||||
| M02 | FND composition, GMP movement, RND presenters | Player split без visual/control regression |
|
||||
| M03 | RND facade/planner/scheduler, FND jobs | Renderer доступен через API и сохраняет budgets |
|
||||
| M04 | EDT plugin shell, QAR editor tests | Reload-safe workspace, commands, undo/recovery |
|
||||
| M05 | FND content schemas/cache, EDT project | Deterministic Content Project build/migration |
|
||||
| M06 | SRV environments/snapshots/adapters | Two-core read/diff/dry-run round-trip |
|
||||
| M07 | EDT world editing/navigation/deploy | NPC placement на dev-core без ручного SQL |
|
||||
| M08 | NET transport/auth/protocol/entities | Headless auth → world и deterministic replay |
|
||||
| M09 | GMP world state/movement/targeting | Packet replay → deterministic gameplay snapshot |
|
||||
| M10 | FND app lifecycle, RND/UIA presentation | Игрок входит, видит entities и перемещается |
|
||||
| M11 | GMP quests, EDT quest tools, SRV deploy, UIA quest UI | Author → deploy → complete quest своим клиентом |
|
||||
| M12 C1–C8 | NET/GMP/RND/UIA completeness + QAR | Feature matrix `Compatible/Verified` по waves |
|
||||
| M13 | EDT dungeon/encounter, SRV modules, GMP instances | Custom dungeon group playtest end-to-end |
|
||||
|
||||
## Recommended implementation rhythm
|
||||
|
||||
Каждая feature внутри milestone проходит один и тот же цикл:
|
||||
|
||||
1. `Research` — original behavior, core expectations, готовые tools/references.
|
||||
2. `Contract` — owner, types, commands/events, capabilities и error states.
|
||||
3. `Fixture` — golden input/output или reproducible live scenario до production code.
|
||||
4. `Pure implementation` — parser/reducer/validator без Node where possible.
|
||||
5. `Adapter` — network, renderer, UI, Editor или server boundary.
|
||||
6. `Vertical integration` — минимальный observable player/author outcome.
|
||||
7. `Failure/recovery` — malformed, cancel, disconnect, rollback, missing asset.
|
||||
8. `Fidelity/performance` — comparison evidence и budgets.
|
||||
9. `Documentation` — coverage status, known gaps, tooling decision и target evidence.
|
||||
|
||||
## Cross-cutting contracts
|
||||
|
||||
- `BuildProfile` — `Blizzlike335`, `Enhanced`, authoring и test capabilities.
|
||||
- `CoordinateMapper` — единственное преобразование WoW/Godot/ADT/server coordinates.
|
||||
- `AssetRepository` — original assets, overlays, caches и provenance.
|
||||
- `ProtocolProfile` — build, opcodes, update fields, codecs и server capabilities.
|
||||
- `WorldState` — authoritative client-side projection без Node dependencies.
|
||||
- `WorldRenderFacade` — presentation boundary.
|
||||
- `ViewModel/Intent` — единственная граница UI с gameplay.
|
||||
- `LuaRuntime` — заменяемый Lua backend.
|
||||
- `ContentProject` — stable IDs, schemas, dependencies и user-authored source.
|
||||
- `ServerSchemaAdapter` — canonical content ↔ core schema/change set.
|
||||
- `JobScheduler` — bounded background work, cancellation, progress и main-thread finalize.
|
||||
- `Diagnostic` — code, severity, location, context, suggested fix и correlation ID.
|
||||
|
||||
## Архитектурные gates
|
||||
|
||||
Перед переходом между крупными фазами обязательны:
|
||||
|
||||
- `Architecture gate`: запрещённые зависимости отсутствуют.
|
||||
- `Fidelity gate`: есть comparison evidence, а gaps зарегистрированы.
|
||||
- `Data gate`: schema/version/migration и deterministic build проверены.
|
||||
- `Performance gate`: budgets измерены на cold/warm и stress scenarios.
|
||||
- `Recovery gate`: failure/reconnect/cancel/rollback проверены.
|
||||
- `Authoring gate`: изменения можно создать, проверить, diff, undo и воспроизвести.
|
||||
- `Server gate`: обе схемы не предполагаются одинаковыми, capabilities обнаружены явно.
|
||||
|
||||
## Правило улучшений
|
||||
|
||||
Современное улучшение допускается, если:
|
||||
|
||||
1. исходное поведение `Blizzlike335` уже измерено или gap отмечен;
|
||||
2. улучшение включается отдельным setting/profile;
|
||||
3. оно не меняет protocol/server authority скрыто;
|
||||
4. есть before/after visual/performance/accessibility evidence;
|
||||
5. Content Project указывает required capability и fallback.
|
||||
|
||||
Примеры допустимых opt-in улучшений: modern shadows, higher terrain texture resolution, anisotropy, enhanced water, longer draw distance, FSR/upscaling, color-blind palettes, scalable UI, richer editor diagnostics. Новые gameplay rules требуют server module/profile, а не только client toggle.
|
||||
|
||||
## Definition of complete client
|
||||
|
||||
Клиент считается полным не тогда, когда перечислены все packet handlers, а когда:
|
||||
|
||||
- все строки [`../docs/CLIENT_FEATURES.md`](../docs/CLIENT_FEATURES.md) имеют evidence-backed статус;
|
||||
- основные сценарии оригинального клиента воспроизводятся end-to-end;
|
||||
- default UI и выбранные addons проходят compatibility tiers;
|
||||
- supported TrinityCore/AzerothCore profiles проходят contract matrix;
|
||||
- renderer, audio, input, localization и recovery имеют утверждённые gaps/budgets;
|
||||
- AuthoringStudio может создать, проверить, развернуть и протестировать новый quest/location/dungeon package;
|
||||
- release не содержит proprietary assets и требует легально полученную установку клиента.
|
||||
@@ -4,12 +4,18 @@
|
||||
|
||||
Этот каталог задаёт обязательную последовательность реализации. Архитектура описана в `docs/`, фактическое состояние renderer — в `RENDER.md`, а здесь находятся исполняемые milestones.
|
||||
|
||||
Полная инженерная декомпозиция всех подсистем находится в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md). Перед работой над milestone агент читает соответствующий subsystem plan из `targets/roadmap/`, но статус меняет только в исполняемом target-файле M00–M13.
|
||||
|
||||
Параллельная командная работа выполняется по [`../docs/TEAM_WORKFLOW.md`](../docs/TEAM_WORKFLOW.md): отдельные worktrees, claims, exclusive paths, contract-first и интегратор milestone.
|
||||
|
||||
## Current target
|
||||
|
||||
`M00` — [00-render-baseline.md](00-render-baseline.md)
|
||||
|
||||
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
||||
|
||||
Статусные метки применяются только к исполняемым файлам `targets/[0-9][0-9]-*.md`. `DEVELOPMENT_ROADMAP.md` и `targets/roadmap/` являются нормативной декомпозицией, но не создают параллельную систему статусов.
|
||||
|
||||
## Definition of fidelity 1:1
|
||||
|
||||
`1:1` означает совпадение наблюдаемого поведения с оригинальным клиентом WoW 3.3.5a build 12340 для выбранного сценария:
|
||||
@@ -42,8 +48,16 @@
|
||||
| M12 | [WoW 3.3.5a completeness](12-client-completeness.md) | M11 | OPEN |
|
||||
| M13 | [Dungeon authoring](13-dungeon-authoring.md) | M12 foundation | OPEN |
|
||||
|
||||
M12 выполняется восемью completeness waves, определёнными в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md), а не одной массовой реализацией.
|
||||
|
||||
M07 и M08 могут выполняться параллельными командами только после M06/M03 соответственно и только если пользователь явно разрешил параллельную работу. Их результаты сходятся в M11.
|
||||
|
||||
`recast-rs` запланирован как optional navigation backend в M05, исследовательский CLI spike и world validation в M07, затем полноценная dungeon navigation validation в M13. Он не блокирует M00–M04 и не считается автоматически совместимым с server `mmaps`.
|
||||
|
||||
`rilua` зарегистрирован как Lua 5.1.1 runtime/test-oracle candidate для M12. До compatibility spike он не добавляется как dependency; UI строится через заменяемый `LuaRuntime`, поскольку WoW API, FrameXML, secure actions и taint не входят в готовый VM.
|
||||
|
||||
Wowser зарегистрирован как независимый MIT reference для M08 и M12: auth → world protocol lifecycle, binary/data handling, asset pipeline и общий client coverage. Его старый browser stack не является кандидатом на интеграцию; полезные решения принимаются только после проверки против build 12340 и server cores.
|
||||
|
||||
## Общий quality gate
|
||||
|
||||
Каждая цель должна сохранить:
|
||||
@@ -53,6 +67,9 @@ M07 и M08 могут выполняться параллельными кома
|
||||
- renderer checkpoints при изменении рендера;
|
||||
- отсутствие новых нарушений dependency rules;
|
||||
- обновлённые документы и diagnostics;
|
||||
- выполнен documentation gate: inline public API, module spec, inputs/outputs и обязательные diagrams;
|
||||
- выполнен coding gate: явные имена, KISS, отсутствие premature abstraction и необоснованной optimization;
|
||||
- проходят `tools/verify_coordination.ps1` и `tools/verify_documentation.ps1`;
|
||||
- воспроизводимость без proprietary данных в Git.
|
||||
|
||||
## Evidence format
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
# FND — Foundation, Application and Data
|
||||
|
||||
## Цель
|
||||
|
||||
Создать стабильную платформу, на которой renderer, client runtime и Godot Editor могут развиваться независимо, используя одинаковые типы и build artifacts.
|
||||
|
||||
## FND-1. Project profiles и configuration
|
||||
|
||||
- Ввести `BuildProfile` и `CapabilitySet`.
|
||||
- Отделить user settings, developer settings, server connection profiles и content project settings.
|
||||
- Secrets хранить вне Git/Resource; поддержать environment/OS credential provider.
|
||||
- Настройки иметь schema version, defaults, validation, migration и reset-by-section.
|
||||
- `Blizzlike335` сделать неизменяемым базовым профилем; enhanced settings наследуют его явно.
|
||||
|
||||
## FND-2. Application lifecycle
|
||||
|
||||
- Реализовать state machine `Boot → DataCheck → Login → Realm → Character → Loading → InWorld → Disconnect/Recovery`.
|
||||
- Editor, sandbox и headless modes запускать разными composition roots.
|
||||
- Dependency injection выполнять composition layer; не использовать scene lookup как service locator.
|
||||
- Autoload ограничить действительно process-wide lifecycle services. Gameplay, renderer и editor sessions должны инстанцироваться и освобождаться явно.
|
||||
- Каждое состояние иметь cancellation, timeout, diagnostics и deterministic cleanup.
|
||||
|
||||
## FND-3. Domain primitives
|
||||
|
||||
- Typed IDs: content, server entry, entity, GUID, map, area, spell, item, quest, display и sound.
|
||||
- Typed coordinates и orientation; запретить `Vector3` как межслойный wire/domain contract.
|
||||
- Value objects для locale, money, duration, percentage, level, power type и flags.
|
||||
- Commands/events/read models не наследуют Node/Resource.
|
||||
- Serialization version и unknown-field policy определить до сохранения snapshots/replays.
|
||||
|
||||
## FND-4. Asset repository
|
||||
|
||||
- Описать archive chain/locale/patch precedence для 3.3.5a.
|
||||
- Отделить `original source → parsed canonical data → imported Godot resource → runtime cache`.
|
||||
- Ввести provenance: source archive/path/hash, parser version, import profile и dependency hashes.
|
||||
- Не изменять extracted upstream files; custom content подключать ordered overlays.
|
||||
- Dedup textures/materials/meshes по canonical key, а не по случайной Node lifetime.
|
||||
- Missing/corrupt asset policy: placeholder только в dev profile, structured error в fidelity runs.
|
||||
|
||||
## FND-5. Binary formats и typed tables
|
||||
|
||||
- MPQ: archive ordering, locale, listfile absence, encrypted/compressed entries.
|
||||
- BLP: paletted/DXT, alpha depths, mip chains, color space и wrap/filter metadata.
|
||||
- DBC: schema descriptors, localized strings, relationships, hot reload только в authoring mode.
|
||||
- WDT/WDL/ADT: tile existence, terrain, holes, layers, placements, liquids, distant data.
|
||||
- M2/SKIN/WMO: versioned structs, bounds, batches, materials, animation, attachments, portals.
|
||||
- Audio/interface assets: MP3/WAV, fonts, XML, Lua, TOC и SavedVariables formats.
|
||||
- Parser API возвращает typed result + diagnostics; partial parse не маскируется как success.
|
||||
- Differential tests сравнивают текущие parsers с независимыми implementations (`warcraft-rs`, `wow_dbc`, WowDBDefs-driven schemas) на одинаковых fixtures; reference disagreement регистрируется, а не выбирается по большинству.
|
||||
|
||||
## FND-6. Cache and build pipeline
|
||||
|
||||
- Единый content-addressed cache manifest для terrain/M2/WMO/characters/audio/navigation/UI.
|
||||
- Atomic staging → verify → publish; crash не оставляет валидно выглядящий partial artifact.
|
||||
- Cache version включает schema, parser, shader/material contract и profile.
|
||||
- Headless commands используют те же services, что Editor actions.
|
||||
- Добавить dependency graph и incremental rebuild по hash.
|
||||
- Папки generated/cache исключить из Git и при необходимости из Godot import через `.gdignore`.
|
||||
|
||||
## FND-7. Native boundary
|
||||
|
||||
- GDExtension использовать для binary parsing, compression/crypto, large buffer transforms и измеренных hot paths.
|
||||
- GDScript использовать для orchestration, Editor UI и presentation composition.
|
||||
- Перед добавлением Rust/C/C++ dependency фиксировать ABI boundary и ownership.
|
||||
- Через native boundary передавать packed arrays/immutable result objects, избегать вызова на каждый vertex/instance.
|
||||
- Версию Godot/GDExtension API закрепить; обновление проходит compatibility matrix.
|
||||
|
||||
## FND-8. Jobs and concurrency
|
||||
|
||||
- Общий `JobScheduler`: priority, bounded capacity, cancellation, progress, correlation ID.
|
||||
- Pure CPU work выполняется workers; scene tree и GPU resources финализируются main thread budget.
|
||||
- Один resource не модифицируется конкурентно; shared mutable collections защищаются.
|
||||
- Каждый submitted worker task дожидается completion/cleanup.
|
||||
- Session/world unload отменяет jobs и освобождает RIDs/resources детерминированно.
|
||||
|
||||
## Deliverables
|
||||
|
||||
- Foundation package без renderer/network dependencies.
|
||||
- Headless data probe и cache inspector.
|
||||
- Coordinate/parser/cache fixtures.
|
||||
- Architecture dependency checker.
|
||||
- Документированные memory/thread ownership rules.
|
||||
|
||||
## Gate
|
||||
|
||||
Одинаковые входы дают одинаковые manifests; application modes собираются разными roots; domain тестируется без SceneTree; cancellation и shutdown не оставляют jobs/RIDs.
|
||||
@@ -0,0 +1,102 @@
|
||||
# RND — Rendering and Graphics
|
||||
|
||||
## Цель
|
||||
|
||||
Воспроизвести визуальные правила WoW 3.3.5a и дать opt-in улучшения, сохранив frame pacing на больших мирах Godot.
|
||||
|
||||
## RND-1. Renderer architecture
|
||||
|
||||
- `WorldRenderFacade` принимает focus, environment snapshot и entity presentation commands.
|
||||
- `StreamingTargetPlanner` вычисляет desired state без Node creation.
|
||||
- `RenderBudgetScheduler` ограничивает main-thread finalize по типам операций.
|
||||
- Terrain, M2, WMO, liquid, sky, character, effects и debug overlays — отдельные services.
|
||||
- RenderingServer/RIDs применяются только там, где profiler доказал overhead scene tree.
|
||||
- Ownership каждого RID и cache entry документируется; world unload проверяет leaks.
|
||||
|
||||
## RND-2. World streaming
|
||||
|
||||
- WDT-driven tile catalog, priority по position/direction/velocity/camera frustum.
|
||||
- Prewarm, retain и hysteresis исключают churn на ADT boundaries.
|
||||
- Separate budgets для I/O, parse, mesh/material load, main-thread attach и eviction.
|
||||
- Cold/warm cache scenarios, teleport, rapid flight и camera rotation stress.
|
||||
- Cancellation stale jobs при teleport/map change.
|
||||
- Streaming focus не обязан быть Camera3D: player, spectator, editor viewport, capture tool.
|
||||
|
||||
## RND-3. Terrain
|
||||
|
||||
- Точная 145-vertex MCNK topology, holes, normals и coordinate seams.
|
||||
- Texture layer order, MCAL alpha variants, animation flags, repeat и mip behavior.
|
||||
- Control-splat/array implementation как Godot optimization с fidelity tests.
|
||||
- WDL/distant terrain и hierarchical LOD.
|
||||
- Terrain collision/query representation отдельно от visual mesh.
|
||||
- Editing path: sculpt, smooth, flatten, holes, texture paint, seam repair и rebake.
|
||||
|
||||
## RND-4. M2
|
||||
|
||||
- Static geometry, SKIN batches, blend modes, flags, UV transforms и texture lookup.
|
||||
- Native pivot-based skeletal animation, bone palettes, global sequences и animation lookup.
|
||||
- Billboards, attachments, key bones, events, cameras и bounding volumes.
|
||||
- Particles, ribbons, texture animation и material combiners.
|
||||
- Static doodads группировать spatial MultiMesh cells; animated/interactive entities не смешивать со static batching.
|
||||
- MultiMesh buffer updates выполнять батчем; cell size выбирать по culling, а не только draw-call minimum.
|
||||
|
||||
## RND-5. WMO
|
||||
|
||||
- Root/group materials, batches, vertex colors, multiple UV sets и doodad sets.
|
||||
- Portals/rooms, indoor/outdoor, BSP/collision, fog и group visibility.
|
||||
- MLIQ, skybox, lights и ambient color semantics.
|
||||
- Lightweight render cache для больших WMO и incremental group attach.
|
||||
- Editor tools для group/portal/doodad validation.
|
||||
|
||||
## RND-6. Characters and creatures
|
||||
|
||||
- Race/gender/customization from DBC.
|
||||
- Geosets, skin regions, hair/facial features и composited textures.
|
||||
- Equipment component models/textures, attachment points, sheath, enchant visuals.
|
||||
- Animation state resolver: locomotion, combat, casting, emotes, death, swim, mount, vehicle.
|
||||
- CreatureDisplayInfo/Extra/ModelData, NPC equipment, pets и shapeshift.
|
||||
- Character preview uses same presenter as world runtime.
|
||||
|
||||
## RND-7. Liquids
|
||||
|
||||
- MH2O/MCLQ layers, existence masks, heights, depth и liquid type.
|
||||
- Отдельные profiles water/ocean/magma/slime.
|
||||
- Blizzlike material: texture animation, alpha, lighting и fog behavior.
|
||||
- Enhanced material: normals, reflections, depth/shore blending — opt-in.
|
||||
- Liquid collision/swim state принадлежит gameplay/physics query, не shader.
|
||||
|
||||
## RND-8. Environment
|
||||
|
||||
- DBC-driven Light/LightParams/LightIntBand/LightFloatBand interpolation.
|
||||
- Server time, day/night, zone/subzone transitions.
|
||||
- Skyboxes, fog, ambient/directional light, weather visual state.
|
||||
- Blizzlike cheap shadow policy; enhanced selective character/object shadows отдельно.
|
||||
- Camera far plane, fog и streaming distances согласованы профилем.
|
||||
|
||||
## RND-9. Effects and presentation
|
||||
|
||||
- SpellVisual/SpellVisualKit-driven cast/impact/aura effects.
|
||||
- Projectiles, decals, dynamic objects, combat text и selection circles.
|
||||
- Weather particles и world emitters.
|
||||
- Cinematic/model cameras и loading transitions.
|
||||
- Effects have pooling, lifetime, visibility и per-frame budgets.
|
||||
|
||||
## RND-10. Graphical extensions
|
||||
|
||||
- Higher-resolution content overlays без модификации originals.
|
||||
- Anisotropy, extended draw distance, improved water/shadows, post-processing и upscaling.
|
||||
- Optional PBR approximation only when it does not replace Blizzlike material profile.
|
||||
- Graphics settings expose measurable cost and fallback.
|
||||
- Editor comparison viewport: Blizzlike/Enhanced side-by-side, screenshot и metric capture.
|
||||
|
||||
## Verification
|
||||
|
||||
- Paired checkpoints с original 3.3.5a по position/time/weather/camera.
|
||||
- Material/animation fixtures from independent references.
|
||||
- Perceptual diff плюс human approval для intentional deviations.
|
||||
- p50/p95/p99, max hitch, draw calls, visible objects, GPU/CPU memory и RID leak checks.
|
||||
- D3D12/Vulkan and quality profile matrix.
|
||||
|
||||
## Gate
|
||||
|
||||
Видимый мир не деградирует при декомпозиции; under-foot/near-field quality готова до появления в кадре; long traversal не создаёт recurring hitches/leaks; Enhanced всегда отключаем.
|
||||
@@ -0,0 +1,94 @@
|
||||
# NET — Network, Protocol and Session
|
||||
|
||||
## Цель
|
||||
|
||||
Реализовать точный клиентский протокол WoW 3.3.5a build 12340, изолировав транспорт и packet layout от gameplay/UI.
|
||||
|
||||
## NET-1. Transport
|
||||
|
||||
- Native TCP auth/world connections, DNS/IPv4/IPv6 policy и configurable endpoints.
|
||||
- Non-blocking read/write, bounded buffers, packet fragmentation/coalescing.
|
||||
- Cancellation, connect/read/write timeouts, keepalive и orderly shutdown.
|
||||
- Network thread работает с bytes; domain events применяются game thread.
|
||||
- Proxy mode только как explicit development adapter, не основной transport.
|
||||
|
||||
## NET-2. Authentication
|
||||
|
||||
- Logon challenge/proof SRP6 values и exact endian/normalization rules.
|
||||
- Realm list parsing, build/locale/platform fields и error mapping.
|
||||
- World auth digest, session key и header encryption state.
|
||||
- Credentials не логируются; session material очищается при disconnect.
|
||||
- Golden vectors и сравнение с TrinityCore/AzerothCore/Wowser/Whoa implementations.
|
||||
|
||||
## NET-3. Packet framework
|
||||
|
||||
- `PacketReader/Writer` с checked boundaries и explicit endianness.
|
||||
- Typed opcode registry generated/validated для build 12340.
|
||||
- Header framing, ARC4/drop policy, zlib compressed packets.
|
||||
- Packed GUID, bit masks, CString/fixed strings и localized text.
|
||||
- Unknown/unsupported opcode telemetry rate-limited; policy зависит от criticality.
|
||||
- Packet handlers возвращают domain events, не меняют scenes/UI.
|
||||
|
||||
## NET-4. Update fields and entity lifecycle
|
||||
|
||||
- Generated object/item/container/unit/player/gameobject/dynamicobject/corpse field descriptors.
|
||||
- `SMSG_UPDATE_OBJECT` blocks: values, movement, create, create2, out-of-range, near objects.
|
||||
- Partial mask application сохраняет неизвестные/необновлённые поля корректно.
|
||||
- Entity type/entry/display/faction/health/power/flags/ownership relationships.
|
||||
- Despawn/out-of-range/map transfer не оставляют presentation entities.
|
||||
|
||||
## NET-5. Movement synchronization
|
||||
|
||||
- Полный movement flags set, timestamps, position/orientation, pitch, fall/jump, spline elevation.
|
||||
- Start/stop/strafe/turn/pitch/jump/fall/land/swim/fly/heartbeat packets.
|
||||
- Transport GUID/seat/relative coordinates и server spline movement.
|
||||
- Client prediction history, acknowledgements и reconciliation metrics.
|
||||
- Teleport/worldport handshake и map transfer.
|
||||
- Anti-cheat-sensitive fields формируются строго по profile; client не симулирует authoritative NPC path.
|
||||
|
||||
## NET-6. Session domains
|
||||
|
||||
- Character enum/create/delete/rename/login/logout.
|
||||
- Time/account data/tutorial/action bars/initial spells.
|
||||
- Chat/channel/social/group/guild.
|
||||
- Combat/spells/auras/threat.
|
||||
- Items/inventory/loot/trade/vendor/mail/auction.
|
||||
- Quests/gossip/POI/taxi.
|
||||
- Instances/LFG/PvP/vehicles/calendar.
|
||||
- Каждый domain имеет отдельный handler module, fixtures и unsupported coverage report.
|
||||
|
||||
## NET-7. Reliability and diagnostics
|
||||
|
||||
- State-aware disconnect reason и recovery path.
|
||||
- Reconnect только когда protocol/server допускает; иначе безопасный возврат к login.
|
||||
- Packet trace modes: metadata-only default, redacted payload для tests, encrypted secrets never.
|
||||
- Correlation IDs session/request/entity.
|
||||
- Replay transport воспроизводит timing или deterministic step mode.
|
||||
- Network impairment harness: latency, jitter, loss, duplication, fragmentation и abrupt close.
|
||||
|
||||
## NET-8. Core compatibility
|
||||
|
||||
- Baseline protocol один, но server capabilities/quirks задаются adapter profile.
|
||||
- Не ветвить handlers по строке `if azerothcore` без documented capability.
|
||||
- Test matrix фиксирует core commit, DB revision, modules и config.
|
||||
- Custom opcodes располагаются в namespaced extension profile и не включены в Blizzlike335.
|
||||
|
||||
## NET-9. Warden compatibility
|
||||
|
||||
- Зафиксировать фактические требования Warden у supported TrinityCore/AzerothCore profiles.
|
||||
- Реализовывать только compatibility с тестовыми/private server environments и документированным build 12340 flow; OpenWC не предназначен для official/retail servers.
|
||||
- Module challenge/response, crypto/state и platform behavior изолировать в optional security adapter.
|
||||
- Любое исполнение загружаемого module code sandboxed, size/hash checked и отключаемо в development profile.
|
||||
- Добавить fixtures и failure UX для unsupported/invalid challenge; не использовать subsystem для обхода чужих защитных механизмов.
|
||||
|
||||
## Tools and references
|
||||
|
||||
- TrinityCore/AzerothCore source — authoritative server-side packet expectations.
|
||||
- Whoa — reverse-engineered client semantics.
|
||||
- Wowser — независимый auth/world proof-of-concept и test ideas.
|
||||
- WoWee/WowUnreal — coverage и lifecycle comparison.
|
||||
- Packet captures используются только легально, минимально и с redaction.
|
||||
|
||||
## Gate
|
||||
|
||||
Headless client проходит auth → realm → character → world; replay создаёт одинаковые domain events; malformed packets не читаются за границы; secrets отсутствуют в logs; обе core configurations проходят contract suite.
|
||||
@@ -0,0 +1,112 @@
|
||||
# GMP — Gameplay Systems
|
||||
|
||||
## Цель
|
||||
|
||||
Создать детерминированную клиентскую проекцию серверного gameplay и полный набор player intents WoW 3.3.5a без переноса authoritative правил на клиент.
|
||||
|
||||
## GMP-1. World model
|
||||
|
||||
- Typed entity registry и object hierarchy.
|
||||
- Components/read models: identity, transform, unit, player, item, gameobject, corpse, dynamic object.
|
||||
- Ownership, charm, pet, target, creator, transport и group relationships.
|
||||
- Reducers применяют domain events; presentation подписывается на changesets.
|
||||
- Snapshot/replay позволяют восстановить состояние без live socket.
|
||||
|
||||
## GMP-2. Input and movement
|
||||
|
||||
- Remappable action layer, keyboard/mouse и optional gamepad/accessibility mappings.
|
||||
- Walk/run/backpedal/strafe/turn/jump/fall/swim/fly/autorun.
|
||||
- Camera-relative или character-relative policy строго по original behavior.
|
||||
- Ground/collision query abstraction, slopes, water transition и falling.
|
||||
- Prediction/reconciliation только локального movement; corrections имеют smoothing/snap thresholds.
|
||||
- Debug flight/speed отделены от production profile.
|
||||
|
||||
## GMP-3. Selection and interaction
|
||||
|
||||
- Mouse/world picking, tab target, target/focus и target-of-target.
|
||||
- Hostility/faction/dead/visibility/selectability filters.
|
||||
- Facing, range, line-of-sight и interaction intents.
|
||||
- NPC flags определяют available services; client не выдумывает доступную функцию.
|
||||
- Gameobject use, gossip, trainer, vendor, mailbox, auctioneer, binder и flight master.
|
||||
|
||||
## GMP-4. Combat
|
||||
|
||||
- Auto attack/auto shot, swing state и attack stop.
|
||||
- Damage/heal/miss/absorb/resist/block/crit feedback.
|
||||
- Threat presentation, combat state, evade и unit flags.
|
||||
- Resources, regeneration presentation и combo points/runes where applicable.
|
||||
- Combat log canonical events отделены от floating text/UI formatting.
|
||||
- Server validates hit, damage и death; client показывает pending/cast state.
|
||||
|
||||
## GMP-5. Spells and auras
|
||||
|
||||
- Spellbook, ranks, passive/active, known/disabled spells.
|
||||
- Target requirements, range, facing, resource и client-predictable validation messages.
|
||||
- Cast/channel/interrupt/pushback, GCD и cooldowns.
|
||||
- Aura add/update/remove, stacks, duration, caster, dispel type и visibility.
|
||||
- Totems, shapeshift, pets, tracking, tradeskill casts.
|
||||
- Spell visual/audio descriptors separate from spell mechanics state.
|
||||
|
||||
## GMP-6. Death and recovery
|
||||
|
||||
- Death/release spirit, ghost, corpse, graveyard, reclaim delay.
|
||||
- Resurrection request/accept, spirit healer penalties.
|
||||
- Durability loss/repair и instance wipe/reset presentation.
|
||||
- Failure during map transfer/reconnect does not duplicate corpse/player state.
|
||||
|
||||
## GMP-7. Items and economy
|
||||
|
||||
- Inventory slots, bags, bank, keyring, equipment, ammo и currencies.
|
||||
- Stack/split/swap/equip/use/destroy with pending/server-confirmed states.
|
||||
- Loot windows, group rolls, master loot, disenchant и loot release.
|
||||
- Item fields, random properties, enchants, gems, durability, sets и cooldowns.
|
||||
- Vendor/buyback/repair, trade, mail, auction и guild bank.
|
||||
- Money formatting/display separated from authoritative transaction.
|
||||
|
||||
## GMP-8. Quests and progression
|
||||
|
||||
- Quest availability, accept, abandon, track, objectives, complete и reward choice.
|
||||
- Kill, item, gameobject, explore, spell, escort, timed, reputation objectives.
|
||||
- Chains, prerequisites, exclusive groups, repeatable/daily/weekly и conditions.
|
||||
- XP/rested/level, reputation, honor, achievements.
|
||||
- Skills, talents, glyphs, professions, recipes и trainers.
|
||||
- Quest state drives view models; authoring graph compiles to server-supported representation.
|
||||
|
||||
## GMP-9. World activities
|
||||
|
||||
- Area/subzone/exploration state и rest areas.
|
||||
- Taxi discovery/routes/flight, transports, elevators и boats.
|
||||
- Fishing, gathering, tracking, world events и scripted gameobjects.
|
||||
- Weather/time events feed render/audio but are owned by world state.
|
||||
- Minimap/world map POI and quest markers are projections, not quest logic.
|
||||
|
||||
## GMP-10. Social
|
||||
|
||||
- Chat types, channels, whispers, system messages и links.
|
||||
- Friends, ignore, who, AFK/DND.
|
||||
- Party/raid invites, leader, roles, loot rules, ready check, raid targets.
|
||||
- Guild roster, ranks, notes, MOTD, event/calendar.
|
||||
- Presence/reconnect updates and privacy/error handling.
|
||||
|
||||
## GMP-11. Instances, PvP and vehicles
|
||||
|
||||
- Difficulty, instance binds, lockouts, encounters, reset и transfer aborted.
|
||||
- LFG queue/proposal/roles/teleport/vote kick/rewards.
|
||||
- Duel, PvP flags, battleground queue/status/score/resurrection и arena teams.
|
||||
- Outdoor PvP objectives.
|
||||
- Vehicle seats, enter/exit/switch, possession action bar и transport-relative movement.
|
||||
|
||||
## GMP-12. Cinematics and scripted presentation
|
||||
|
||||
- Cinematic/movie start/stop, camera control и skippability.
|
||||
- Server-driven scripted sequences reflected as safe presentation commands.
|
||||
- Loading screens and map transitions preserve session state.
|
||||
- Gameplay input contexts prevent actions during restricted states.
|
||||
|
||||
## Testing order
|
||||
|
||||
Для каждой системы: reducer unit tests → packet/domain contract → offline scenario → live core integration → original-client comparison → latency/reconnect scenario.
|
||||
|
||||
## Gate
|
||||
|
||||
Gameplay state воспроизводим replay-ом; UI/render не владеют authoritative values; все intents имеют pending/confirmed/rejected lifecycle; core gameplay loop проходит end-to-end без оригинального клиента.
|
||||
@@ -0,0 +1,97 @@
|
||||
# UIA — UI, Lua, Addons and Audio
|
||||
|
||||
## Цель
|
||||
|
||||
Воспроизвести UI/addon semantics клиента 3.3.5a, сохранив возможность modern UI overlays и полноценную audio presentation.
|
||||
|
||||
## UIA-1. UI architecture
|
||||
|
||||
- Screen flow отделён от in-world HUD.
|
||||
- UI читает immutable view models и отправляет intents.
|
||||
- Godot Control tree является presentation backend, а не gameplay model.
|
||||
- Input contexts: login, character, world, chat, modal, targeting, vehicle, cinematic.
|
||||
- Scale, safe area, resolution, font и localization учитываются на уровне layout system.
|
||||
|
||||
## UIA-2. Frontend screens
|
||||
|
||||
- Data/license check, login, realm list и connection errors.
|
||||
- Character selection/create/delete/rename.
|
||||
- Race/class/customization previews тем же character renderer.
|
||||
- Loading screens по map/instance, progress и cancellation policy.
|
||||
- Disconnect/reconnect dialogs и account/server notices.
|
||||
|
||||
## UIA-3. FrameXML
|
||||
|
||||
- TOC parser: metadata, dependencies, optional deps, SavedVariables и load order.
|
||||
- XML includes, templates, `virtual`, inheritance и named/anonymous objects.
|
||||
- Frames, Regions, Textures, FontStrings, Layers и Scripts.
|
||||
- Anchors, dimensions, strata, frame level, draw layers, clipping и scale.
|
||||
- Widget types: Button, CheckButton, EditBox, Slider, StatusBar, ScrollFrame, MessageFrame, Tooltip, Cooldown, Model и Minimap.
|
||||
- Handler scripts: OnLoad, OnEvent, OnUpdate, mouse/keyboard/drag/show/hide/value events.
|
||||
|
||||
## UIA-4. Lua runtime
|
||||
|
||||
- Внутренний `LuaRuntime` позволяет сравнить/заменить PUC-Rio 5.1.1 и `rilua`.
|
||||
- Restricted libraries, byte strings, number semantics, globals, `bit` и error formatting.
|
||||
- VM lifetime: one UI environment per client session/profile with controlled reset.
|
||||
- Incremental GC имеет frame budget и metrics.
|
||||
- Native functions возвращают stable Lua values, а не Godot/Rust/C++ objects напрямую.
|
||||
- VM backend выбирается ADR после official + WoW-specific corpus.
|
||||
|
||||
## UIA-5. WoW API and event system
|
||||
|
||||
- API inventory строится из 3.3.5a FrameXML/Lua usage и независимых references.
|
||||
- Реализация группируется: unit, spell, aura, item, quest, group, chat, map, CVar, bindings, frames.
|
||||
- Event dispatcher имеет deterministic ordering, argument contracts и registration lifecycle.
|
||||
- `OnUpdate` throttling не меняет original semantics в Blizzlike335.
|
||||
- API, требующий server state, читает gameplay read model или создаёт intent; raw packets недоступны.
|
||||
- Missing API выдаёт диагностируемую ошибку/coverage report, а не silent nil where incompatible.
|
||||
|
||||
## UIA-6. Security, taint and addons
|
||||
|
||||
- Secure/insecure execution origins.
|
||||
- Protected functions/actions, secure templates и combat lockdown.
|
||||
- Taint propagation through variables/tables/functions в пределах подтверждённой 3.3.5a модели.
|
||||
- Addon memory/CPU/error isolation и configurable script limits без скрытого изменения default behavior.
|
||||
- SavedVariables account/character scopes, serialization, corruption recovery и atomic writes.
|
||||
- Addon compatibility tiers: loads, renders, functional, secure-combat verified.
|
||||
- Headless addon runner и UI inspector входят в AuthoringStudio/Test Center.
|
||||
|
||||
## UIA-7. Default UI coverage
|
||||
|
||||
- Unit/player/target/focus/party/raid frames.
|
||||
- Action bars, stance/pet/vehicle bars, cooldowns и keybindings.
|
||||
- Buffs/debuffs, cast bars, combat text и errors.
|
||||
- Bags, character, spellbook, talents, glyphs, skills, professions.
|
||||
- Quest log/tracker/gossip, map/minimap/taxi.
|
||||
- Chat, social, guild, calendar, LFG, PvP score.
|
||||
- Vendor, trainer, bank, mail, auction, trade, loot и roll.
|
||||
- Game menu, settings, macros, addons и help.
|
||||
|
||||
## UIA-8. Audio
|
||||
|
||||
- Typed DBC wrappers for SoundEntries, ZoneMusic, IntroMusic, Ambience and related tables.
|
||||
- Decoder/streaming/cache for MP3/WAV with category budgets.
|
||||
- Music day/night/zone transitions and crossfade.
|
||||
- Ambient loops, random emitters, indoor/outdoor и weather.
|
||||
- Positional world sounds, footsteps, water, spells, combat, NPC voice и UI feedback.
|
||||
- Audio events come from presentation descriptors; UI/gameplay do not manipulate players directly.
|
||||
- Device change, focus/mute policy, category volumes и missing sound diagnostics.
|
||||
|
||||
## UIA-9. Localization and accessibility
|
||||
|
||||
- Locale-aware fonts, fallback, text encoding и layout expansion.
|
||||
- Keybindings fully remappable; mouse sensitivity/inversion and optional gamepad.
|
||||
- UI scale, contrast/color-blind profiles, subtitle/text alternatives where possible.
|
||||
- Critical state does not rely only on color/audio.
|
||||
- Enhanced accessibility remains compatible with server and can coexist with Blizzlike visuals.
|
||||
|
||||
## References
|
||||
|
||||
- Original 3.3.5a FrameXML/Lua extracted from legally owned data is primary behavior corpus.
|
||||
- `rilua`/PUC-Rio are VM candidates/oracles, not complete WoW UI.
|
||||
- WowBench/Wowser/WowUnreal/WoWee provide API/event/loader test ideas.
|
||||
|
||||
## Gate
|
||||
|
||||
Default UI boots deterministically; selected real addons pass declared tiers; secure actions fail correctly during combat; Lua GC/OnUpdate remain within budgets; UI is replaceable without changing gameplay/network.
|
||||
@@ -0,0 +1,109 @@
|
||||
# EDT — Godot Editor and Content Authoring
|
||||
|
||||
## Цель
|
||||
|
||||
Превратить Godot Editor в единую среду расширения мира, gameplay, UI, audio и graphics с безопасным build/deploy/playtest workflow.
|
||||
|
||||
## EDT-1. Plugin platform
|
||||
|
||||
- `addons/openwc_editor` регистрирует workspace, docks, inspector/import/gizmo/debugger plugins.
|
||||
- `_enter_tree/_exit_tree` симметричны: все controls/plugins/signals/jobs снимаются без leaks.
|
||||
- UI вызывает authoring application services; не пишет SQL/files/scene tree напрямую.
|
||||
- Editor selection отделена от runtime preview nodes.
|
||||
- Shared services получают context через dependency injection, не global editor singleton.
|
||||
|
||||
## EDT-2. Workspace and UX
|
||||
|
||||
- World viewport, Content Browser, Outliner, Inspector, Validation, History, Build, Database, Test Center.
|
||||
- Search/filter/favorites/recent items и stable links между сущностями.
|
||||
- Multi-selection и bulk edits показывают scope/diff.
|
||||
- Diagnostics имеют severity/code/location/fix; double-click открывает field/node/world position.
|
||||
- Layout/workspace preferences сохраняются отдельно от Content Project.
|
||||
|
||||
## EDT-3. Commands, undo and recovery
|
||||
|
||||
- Любая mutation — command с execute/undo/description/affected IDs.
|
||||
- Terrain strokes агрегируются в bounded deltas; большие операции используют snapshots/chunks.
|
||||
- Autosave recovery journal атомарный и имеет schema version.
|
||||
- External changes обнаруживаются через hashes; merge/overwrite требует решения пользователя.
|
||||
- Quick Fix всегда previewable и undoable.
|
||||
|
||||
## EDT-4. Import and asset browser
|
||||
|
||||
- MPQ/archive browser, locale/patch source provenance и extraction jobs.
|
||||
- Preview BLP, DBC rows, M2 animations/materials, WMO groups/portals, ADT layers и audio.
|
||||
- Godot import plugins применяются только к authoring source formats, где lifecycle `.import` полезен.
|
||||
- Raw extracted corpus не должен вызывать неконтролируемый массовый Godot reimport.
|
||||
- Batch conversion использует staging, cancellation, cache hash и report.
|
||||
|
||||
## EDT-5. World editing
|
||||
|
||||
- Map/tile/chunk navigation и coordinate overlays.
|
||||
- Terrain sculpt/smooth/flatten/noise/stamps/holes.
|
||||
- Texture layers, alpha painting, shading и liquids.
|
||||
- M2/WMO placement, snapping, transforms, doodad sets и unique ID allocation.
|
||||
- Creature/gameobject spawns, groups, formations, patrol/escort paths.
|
||||
- Areas, triggers, graveyards, teleports, taxi/transport points, sound/weather volumes.
|
||||
- Collision/navmesh/streaming/render-cost visualization.
|
||||
|
||||
## EDT-6. Data editors
|
||||
|
||||
- Creature/gameobject templates, models, equipment, movement and text.
|
||||
- Item/loot/vendor/trainer/profession editors.
|
||||
- Quest form + dependency/progression graph + localization.
|
||||
- Gossip/conditions/SmartAI graph with raw representation inspector.
|
||||
- Spawn/pool/game event/phase editor.
|
||||
- Dungeon package, encounter state graph, doors/triggers/lockouts/loot.
|
||||
- UI/addon manifest, CVar/keybinding и audio zone configuration.
|
||||
|
||||
## EDT-7. Navigation
|
||||
|
||||
- `recast-rs` optional offline backend behind navigation build interface.
|
||||
- Bake profiles: agent size/climb/slope/cell/region/tile parameters.
|
||||
- Walkability, disconnected islands, path cost, off-mesh links и dynamic door states.
|
||||
- Quest giver/objective/turn-in reachability; patrol/escort validation.
|
||||
- OpenWC cache отделён от server mmap until compatibility proven.
|
||||
|
||||
## EDT-8. Graphics extension tools
|
||||
|
||||
- Material/shader profile editor с Blizzlike/Enhanced comparison.
|
||||
- Texture replacement/upscale overlays и mip/alpha/color-space validation.
|
||||
- Water/sky/weather/light preview with fixed time and reference capture.
|
||||
- Character/equipment/animation/attachment preview.
|
||||
- Spell visual/particle/ribbon authoring where formats permit.
|
||||
- Quality preset cost preview и automatic render checkpoint generation.
|
||||
|
||||
## EDT-9. Content Project
|
||||
|
||||
- Git-friendly files, schema version, symbolic IDs, dependencies и capability requirements.
|
||||
- Original data referenced, never copied silently.
|
||||
- Overlay priority/conflict report и semantic merges.
|
||||
- Deterministic numeric ID allocation per server profile.
|
||||
- Localization bundles, scripts, client assets, tests и package manifests.
|
||||
- Schema migrations with backup, fixture and loss policy.
|
||||
|
||||
## EDT-10. Build, deploy and playtest
|
||||
|
||||
```text
|
||||
Validate → Compile → Bake → Diff → Package → Dry-run
|
||||
→ Apply dev DB/module/assets → Start/reload core → Launch client at context
|
||||
→ Collect logs/results → Roll back disposable environment
|
||||
```
|
||||
|
||||
- Jobs cancellable, progress-aware and identical to headless CI services.
|
||||
- Production target physically/logically separated and disabled by default.
|
||||
- Server reload used only for tables known to support it; otherwise restart.
|
||||
- Playtest bundle records content/core/client revisions and profile.
|
||||
- One-click convenience never hides SQL/module/client artifact diff.
|
||||
|
||||
## EDT-11. Extensibility
|
||||
|
||||
- `ContentTypeDescriptor`: schema, icon, inspector, validator, compiler, presenter.
|
||||
- `EditorTool`: selection contract, command factory, gizmo/overlay.
|
||||
- `BuildStep`: inputs/outputs/cache key/cancellation/diagnostics.
|
||||
- Plugin declares API version, dependencies, permissions and migrations.
|
||||
- User plugins cannot mutate production DB or original assets outside approved services.
|
||||
|
||||
## Best-practice gate
|
||||
|
||||
Plugin reload clean; undo/save/reload deterministic; long jobs do not block Editor; preview nodes disposable; every deployment reproducible headless; common scenarios require no manual SQL.
|
||||
@@ -0,0 +1,84 @@
|
||||
# SRV — TrinityCore and AzerothCore Integration
|
||||
|
||||
## Цель
|
||||
|
||||
Поддержать server emulators как versioned backends: protocol-compatible runtime, безопасное редактирование данных и расширение mechanics через modules/scripts.
|
||||
|
||||
## SRV-1. Environment model
|
||||
|
||||
- `ServerEnvironment`: family, core commit, DB revision, modules, config profile, endpoints и data artifact versions.
|
||||
- Отдельные disposable, local-dev, shared-test, staging и production classes.
|
||||
- Credentials/secrets вне Content Project.
|
||||
- Capability discovery проверяет schema fingerprint и optional modules/tables/hooks.
|
||||
- Unknown environment всегда read-only.
|
||||
|
||||
## SRV-2. Database snapshots
|
||||
|
||||
- Read-only import world schema subsets с provenance/checksum.
|
||||
- Auth/characters DB открываются только для нужных test/admin сценариев и никогда не становятся content source.
|
||||
- Snapshot filters reproducible; personal/account data excluded.
|
||||
- Canonical entities preserve supported fields and explicitly record unsupported/raw extras.
|
||||
- Incremental snapshot возможен, но full consistency snapshot остаётся verification path.
|
||||
|
||||
## SRV-3. Canonical mappings
|
||||
|
||||
- Creature/gameobject templates, models, spawns, addons, formations, movement, texts.
|
||||
- Quests, relations, objectives, POI, conditions, gossip.
|
||||
- Loot/reference loot, vendors, trainers, items, reputation.
|
||||
- SmartAI, script names, waypoint/script tables.
|
||||
- Instances, encounters, LFG, graveyards, teleports, game events, weather, transports.
|
||||
- Каждая mapping имеет per-core capability table и round-trip tests.
|
||||
|
||||
## SRV-4. Change sets and SQL
|
||||
|
||||
- Immutable change set: target fingerprint, preconditions, ordered operations, checksum.
|
||||
- SQL generation соответствует conventions целевого core/profile.
|
||||
- Custom SQL хранится в version control и compatible custom/module directory, не только применяется напрямую.
|
||||
- Dry-run на disposable DB, targeted post-apply queries, core startup smoke.
|
||||
- Idempotent migration или безопасное precondition failure.
|
||||
- Rollback где возможен; destructive/irreversible operations требуют backup и явного подтверждения.
|
||||
|
||||
## SRV-5. Scripts and modules
|
||||
|
||||
- DB/SmartAI для выразимых data-driven mechanics.
|
||||
- C++ module/script skeleton для mechanics, требующих server hooks.
|
||||
- AzerothCore modules namespaced config/SQL/hooks; core patch — только если hook принципиально отсутствует.
|
||||
- TrinityCore adapter следует его текущим script/customization conventions отдельно.
|
||||
- Build artifact включает source, config.dist, SQL, tests и compatibility manifest.
|
||||
- Client extension capability договаривается явно; hidden custom opcode запрещён.
|
||||
|
||||
## SRV-6. Server data artifacts
|
||||
|
||||
- DBC/maps/vmaps/mmaps versions связываются с ContentBundleManifest.
|
||||
- Custom map требует согласованного client terrain, server maps/collision/pathfinding и DB Map/Area data.
|
||||
- OpenWC navmesh не объявляется mmap до binary/query/live behavior compatibility.
|
||||
- Asset/map IDs распределяются стабильно и проверяются на collision.
|
||||
- Client/server package mismatch блокирует playtest с actionable diagnostic.
|
||||
|
||||
## SRV-7. Protocol compatibility
|
||||
|
||||
- Baseline 3.3.5a build 12340 остаётся общим.
|
||||
- Core-specific quirks описываются named capabilities и fixtures.
|
||||
- Custom extensions используют versioned handshake/profile и fallback/rejection.
|
||||
- Server authority одинаково соблюдается для обеих families.
|
||||
- Compatibility matrix закрепляет tested commits, а не расплывчатое «поддерживает AzerothCore».
|
||||
|
||||
## SRV-8. Test environments
|
||||
|
||||
- Docker/managed local environment для repeatable DB/core setup, где поддерживается.
|
||||
- Seed accounts/characters synthetic и не содержат реальные credentials.
|
||||
- Reset DB через assembler/base + project migrations.
|
||||
- Logs world/auth/database/client собираются с correlation metadata.
|
||||
- DB-only change test, module build test, startup smoke, scripted client scenario и cleanup.
|
||||
|
||||
## SRV-9. Existing tools
|
||||
|
||||
- Keira3 — reference для field semantics, SQL generation и AzerothCore DB UX.
|
||||
- AzerothCore wiki/schema/module template — primary backend documentation.
|
||||
- TrinityCore source/schema/tools — primary для Trinity profile.
|
||||
- Noggit/WoW modding tools — client-world authoring reference, но не server schema oracle.
|
||||
- OpenWC Editor объединяет workflow, но сохраняет raw SQL preview и ссылки на upstream docs.
|
||||
|
||||
## Gate
|
||||
|
||||
Одна canonical сущность проходит import → edit → diff → dry-run → apply → reimport на каждом supported profile без silent loss; module/content packages воспроизводимы; production write disabled by default; test environment очищается.
|
||||
@@ -0,0 +1,121 @@
|
||||
# QAR — Fidelity, Quality, CI and Release
|
||||
|
||||
## Цель
|
||||
|
||||
Сделать точность, производительность и расширяемость проверяемыми свойствами, а не обещаниями.
|
||||
|
||||
## QAR-1. Fidelity lab
|
||||
|
||||
- Каталог сценариев original client 3.3.5a: setup, input, server state, expected output.
|
||||
- Paired screenshots/video/log/packet/state evidence с provenance.
|
||||
- Поведение классифицируется: `Unknown`, `Observed`, `Matched`, `Intentional deviation`, `Blocked`.
|
||||
- Independent references могут подсказать hypothesis, но не повышают статус без evidence.
|
||||
- Blizzlike bugs/exploits обрабатываются policy decision: protocol compatibility отдельно от harmful behavior.
|
||||
|
||||
## QAR-2. Test layers
|
||||
|
||||
- Native unit tests: parsers, crypto, compression, coordinates, math, caches.
|
||||
- GDScript/domain unit tests: reducers, validators, commands, view models.
|
||||
- Contract tests: packets, DB adapters, Lua API, content schemas, renderer facade.
|
||||
- Scene tests: lifecycle, input, UI, renderer attachment, EditorPlugin reload.
|
||||
- Integration: disposable core/DB, import/build/deploy, login/world/gameplay.
|
||||
- E2E: leveling loop, dungeon, PvP/service flows и authoring vertical slices.
|
||||
- Visual/performance and exploratory design tests complement automation.
|
||||
|
||||
## QAR-3. Godot testing stack
|
||||
|
||||
- Оценить GdUnit4 как candidate для Godot 4.6 GDScript/scene/CI tests.
|
||||
- Native GDExtension tests используют подходящий C++ test runner отдельно от engine-internal doctest unless Godot itself is rebuilt.
|
||||
- Headless commands возвращают non-zero и machine-readable JUnit/JSON reports.
|
||||
- Scene tests не зависят от proprietary assets: synthetic fixtures or locally resolved optional corpus.
|
||||
- Flaky retries не превращают failure в success; instability измеряется и исправляется.
|
||||
|
||||
## QAR-4. Golden and property testing
|
||||
|
||||
- Golden binary structures and exact-byte packet vectors.
|
||||
- Parser round-trip where format permits; malformed/truncated/fuzz corpus.
|
||||
- Coordinate transform property tests and known world points.
|
||||
- Deterministic content manifests, SQL diff and migration before/after fixtures.
|
||||
- Lua oracle corpus, navmesh reference counts/queries, material descriptor mappings.
|
||||
|
||||
## QAR-5. Performance budgets
|
||||
|
||||
- Startup/data check/login/world entry.
|
||||
- Streaming cold/warm traversal and teleport.
|
||||
- CPU/GPU frame p50/p95/p99/max hitch, draw calls, memory и descriptors.
|
||||
- Network decode/apply queues and reconciliation error.
|
||||
- UI `OnUpdate`, event fan-out, Lua allocations/GC.
|
||||
- Editor import/bake/validation latency and cancellation responsiveness.
|
||||
- Server deployment/playtest turnaround.
|
||||
- Budgets hardware/profile-specific and trend tracked.
|
||||
|
||||
## QAR-6. Observability
|
||||
|
||||
- Structured logging categories and stable diagnostic codes.
|
||||
- Correlation IDs session/job/entity/content change set.
|
||||
- Runtime metrics: queues, entities, cache hit, resource counts, RIDs, packet rate.
|
||||
- Editor metrics: jobs, diagnostics, artifact hashes, DB operations.
|
||||
- Debug overlays and Godot profiler/visual profiler capture workflow.
|
||||
- Payload, credentials, chat/personal content redacted by default.
|
||||
|
||||
## QAR-7. CI pipeline
|
||||
|
||||
Pull request gates:
|
||||
|
||||
1. formatting/static analysis/document links/target marker validation;
|
||||
2. native build and unit tests;
|
||||
3. headless Godot load and GDScript tests;
|
||||
4. affected parser/protocol/content/adapter contracts;
|
||||
5. deterministic artifact check;
|
||||
6. renderer smoke if visual code changed;
|
||||
7. license/dependency audit.
|
||||
|
||||
Nightly/weekly:
|
||||
|
||||
- disposable TrinityCore/AzerothCore integration;
|
||||
- full packet/Lua/parser replay corpus;
|
||||
- visual checkpoints on available GPU runners;
|
||||
- long traversal, reconnect, cache migration and soak;
|
||||
- previous schema/Godot/core compatibility matrix.
|
||||
|
||||
## QAR-8. Security and robustness
|
||||
|
||||
- Bounds checked binary parsing and fuzzing.
|
||||
- Lua sandbox/taint/protected actions.
|
||||
- DB least privilege, read-only default, production protection.
|
||||
- Untrusted addon/content/plugin permission model.
|
||||
- Dependency license/advisory/source pin audit.
|
||||
- Atomic files, backup, recovery and rollback.
|
||||
- No proprietary assets, secrets or production dumps in repository/artifacts.
|
||||
|
||||
## QAR-9. Documentation and decisions
|
||||
|
||||
- Architecture/API/schema changes update docs and ADR в том же work package.
|
||||
- Каждый самостоятельный module имеет specification по `docs/DOCUMENTATION_STANDARD.md`: purpose, boundaries, public API, inputs/outputs, data flow, ownership, errors, tests и source map.
|
||||
- Data-flow diagram обязательна всегда; state/sequence/dependency diagrams обязательны по сложности/lifecycle rules стандарта.
|
||||
- Inline docs являются источником точной сигнатуры, module spec — источником концепции и фактического статуса.
|
||||
- Tooling candidates tracked in `docs/TOOLING_CATALOG.md` with status and pinned version.
|
||||
- Feature coverage links implementation, tests, fidelity evidence and known gaps.
|
||||
- Generated API docs for documented GDScript/GDExtension public surface включаются в quality gate, когда pipeline подготовлен.
|
||||
|
||||
## QAR-9A. Code clarity and KISS
|
||||
|
||||
- Naming и simplicity проверяются по `docs/CODING_STANDARD.md`.
|
||||
- Длинное точное имя предпочтительнее неочевидного сокращения; length не является целью сама по себе.
|
||||
- Complexity metrics используются как сигнал для review, а не автоматическая причина переписывания domain-heavy code.
|
||||
- Premature generic frameworks, service locators, duplicate registries и unmeasured native optimizations блокируют integration.
|
||||
- Неизбежно сложный parser/protocol/streaming/UI security code обязан быть изолирован, иметь reference fixtures и диаграмму.
|
||||
- Contributor workflow describes smallest reproducible diagnostic commands.
|
||||
|
||||
## QAR-10. Packaging and release
|
||||
|
||||
- Client ships without Blizzard data and guides user to select legally owned install.
|
||||
- Import/cache first-run is resumable and reports disk/time requirements.
|
||||
- Platform packages include matching GDExtension libraries and dependency notices.
|
||||
- Content package and server module releases version independently with compatibility manifest.
|
||||
- Save/settings/content schema migrations tested from supported previous versions.
|
||||
- Release channels: developer, preview, stable; fidelity gaps visible in notes.
|
||||
|
||||
## Exit gate
|
||||
|
||||
Release candidate passes required core/platform/profile matrix; no critical unowned diagnostic; performance regression within approved bounds; artifacts reproducible and license-clean; known fidelity deviations documented.
|
||||
@@ -0,0 +1,95 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$GodotPath,
|
||||
[string]$Output = "user://render_baseline",
|
||||
[string]$CacheState = "existing",
|
||||
[double]$WaitSeconds = 8.0,
|
||||
[double]$MeasureSeconds = 3.0,
|
||||
[string]$ReferenceCheckpointDirectory,
|
||||
[string]$VisualComparisonReport = "user://render_baseline/visual_comparison.json",
|
||||
[int]$VisualComparisonWidth = 2560,
|
||||
[int]$VisualComparisonHeight = 1440,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
if (-not $GodotPath) {
|
||||
$command = Get-Command godot -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
$GodotPath = $command.Source
|
||||
} else {
|
||||
$GodotPath = Join-Path $env:TEMP "godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe"
|
||||
}
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $GodotPath)) {
|
||||
throw "Godot executable not found: $GodotPath"
|
||||
}
|
||||
|
||||
$revision = (& git -C $repoRoot rev-parse --short HEAD).Trim()
|
||||
if (-not $revision) {
|
||||
$revision = "worktree"
|
||||
}
|
||||
|
||||
function Invoke-GodotStep {
|
||||
param([string]$Name, [string[]]$Arguments)
|
||||
Write-Host "[$Name] $GodotPath $($Arguments -join ' ')"
|
||||
$process = Start-Process -FilePath $GodotPath -ArgumentList $Arguments -NoNewWindow -Wait -PassThru
|
||||
if ($process.ExitCode -ne 0) {
|
||||
throw "$Name failed with exit code $($process.ExitCode)"
|
||||
}
|
||||
}
|
||||
|
||||
Push-Location $repoRoot
|
||||
try {
|
||||
Invoke-GodotStep "project-load" @("--headless", "--path", ".", "--quit")
|
||||
Invoke-GodotStep "render-materials" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_materials.gd")
|
||||
Invoke-GodotStep "m2-unique-dedupe" @("--headless", "--path", ".", "--script", "res://src/tools/verify_m2_unique_dedupe.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")
|
||||
|
||||
$captureArgs = @(
|
||||
"--path", ".",
|
||||
"--script", "res://src/tools/capture_render_checkpoints.gd",
|
||||
"--",
|
||||
"--output", $Output,
|
||||
"--cache-state", $CacheState,
|
||||
"--revision", $revision,
|
||||
"--wait", $WaitSeconds.ToString([Globalization.CultureInfo]::InvariantCulture),
|
||||
"--measure", $MeasureSeconds.ToString([Globalization.CultureInfo]::InvariantCulture)
|
||||
)
|
||||
if ($DryRun) {
|
||||
$captureArgs = @("--headless") + $captureArgs + @("--dry-run")
|
||||
}
|
||||
Invoke-GodotStep "capture" $captureArgs
|
||||
if ($ReferenceCheckpointDirectory) {
|
||||
if ($DryRun) {
|
||||
throw "Reference checkpoint comparison requires PNG capture; remove -DryRun"
|
||||
}
|
||||
$visualCandidateOutput = "$Output/visual_comparison_candidates"
|
||||
$visualCaptureArgs = @(
|
||||
"--path", ".",
|
||||
"--script", "res://src/tools/capture_render_checkpoints.gd",
|
||||
"--",
|
||||
"--output", $visualCandidateOutput,
|
||||
"--cache-state", $CacheState,
|
||||
"--revision", $revision,
|
||||
"--wait", $WaitSeconds.ToString([Globalization.CultureInfo]::InvariantCulture),
|
||||
"--measure", $MeasureSeconds.ToString([Globalization.CultureInfo]::InvariantCulture),
|
||||
"--viewport-width", $VisualComparisonWidth.ToString(),
|
||||
"--viewport-height", $VisualComparisonHeight.ToString()
|
||||
)
|
||||
Invoke-GodotStep "visual-capture" $visualCaptureArgs
|
||||
Invoke-GodotStep "visual-comparison" @(
|
||||
"--headless", "--path", ".",
|
||||
"--script", "res://src/tools/compare_render_checkpoints.gd",
|
||||
"--",
|
||||
"--reference", $ReferenceCheckpointDirectory,
|
||||
"--candidate", $visualCandidateOutput,
|
||||
"--output", $VisualComparisonReport
|
||||
)
|
||||
}
|
||||
Write-Host "Renderer baseline completed. Report: $Output/report.json"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
param(
|
||||
[string]$Root = (Split-Path -Parent $PSScriptRoot)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$failures = [System.Collections.Generic.List[string]]::new()
|
||||
$warnings = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
$targetsDir = Join-Path $Root 'targets'
|
||||
$targetIndex = Join-Path $targetsDir 'README.md'
|
||||
$targetFiles = Get-ChildItem -LiteralPath $targetsDir -File |
|
||||
Where-Object { $_.Name -match '^\d\d-.*\.md$' } |
|
||||
Sort-Object Name
|
||||
|
||||
$activeTargets = [System.Collections.Generic.List[object]]::new()
|
||||
$statusPattern = '<!-- OPENWC_TARGET:(OPEN|ACTIVE|BLOCKED|DONE) -->'
|
||||
|
||||
foreach ($file in $targetFiles) {
|
||||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName
|
||||
$matches = [regex]::Matches($content, $statusPattern)
|
||||
if ($matches.Count -ne 1) {
|
||||
$failures.Add("$($file.Name): expected exactly one target status marker, found $($matches.Count)")
|
||||
continue
|
||||
}
|
||||
if ($matches[0].Groups[1].Value -eq 'ACTIVE') {
|
||||
$activeTargets.Add($file)
|
||||
}
|
||||
}
|
||||
|
||||
if ($activeTargets.Count -ne 1) {
|
||||
$failures.Add("Expected exactly one ACTIVE target, found $($activeTargets.Count)")
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $targetIndex)) {
|
||||
$failures.Add('targets/README.md is missing')
|
||||
} else {
|
||||
$indexContent = Get-Content -Raw -Encoding UTF8 -LiteralPath $targetIndex
|
||||
$currentMatch = [regex]::Match($indexContent, '(?s)## Current target\s+`(M\d\d)`[^\r\n]*\[[^\]]+\]\(([^)]+)\)')
|
||||
if (-not $currentMatch.Success) {
|
||||
$failures.Add('Could not parse Current target from targets/README.md')
|
||||
} elseif ($activeTargets.Count -eq 1) {
|
||||
$expectedId = $currentMatch.Groups[1].Value
|
||||
$expectedFile = $currentMatch.Groups[2].Value
|
||||
$activeFile = $activeTargets[0].Name
|
||||
$activeId = 'M' + $activeFile.Substring(0, 2)
|
||||
if ($expectedId -ne $activeId -or $expectedFile -ne $activeFile) {
|
||||
$failures.Add("Current target points to $expectedId/$expectedFile but ACTIVE is $activeId/$activeFile")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$claimsDir = Join-Path $Root 'coordination\claims'
|
||||
$claimIds = @{}
|
||||
$claimPattern = '<!-- OPENWC_CLAIM:([A-Z0-9-]+):([a-z0-9-]+):(\d{4}-\d{2}-\d{2}) -->'
|
||||
|
||||
if (Test-Path -LiteralPath $claimsDir) {
|
||||
$claimFiles = Get-ChildItem -LiteralPath $claimsDir -Filter '*.md' -File |
|
||||
Where-Object { $_.Name -ne 'README.md' }
|
||||
foreach ($file in $claimFiles) {
|
||||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName
|
||||
$matches = [regex]::Matches($content, $claimPattern)
|
||||
if ($matches.Count -ne 1) {
|
||||
$failures.Add("coordination/claims/$($file.Name): expected exactly one valid OPENWC_CLAIM marker")
|
||||
continue
|
||||
}
|
||||
$claimId = $matches[0].Groups[1].Value
|
||||
if ($claimIds.ContainsKey($claimId)) {
|
||||
$failures.Add("Duplicate claim ID $claimId in $($claimIds[$claimId]) and $($file.Name)")
|
||||
} else {
|
||||
$claimIds[$claimId] = $file.Name
|
||||
}
|
||||
$expiryText = $matches[0].Groups[3].Value
|
||||
$expiry = [datetime]::ParseExact($expiryText, 'yyyy-MM-dd', [Globalization.CultureInfo]::InvariantCulture)
|
||||
if ($expiry.Date -lt (Get-Date).ToUniversalTime().Date) {
|
||||
$warnings.Add("Claim $claimId expired on $expiryText")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($warning in $warnings) {
|
||||
Write-Warning $warning
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
foreach ($failure in $failures) {
|
||||
Write-Error $failure
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output "Coordination check passed: targets=$($targetFiles.Count) active=$($activeTargets.Count) fallback_claims=$($claimIds.Count) warnings=$($warnings.Count)"
|
||||
exit 0
|
||||
@@ -0,0 +1,105 @@
|
||||
param(
|
||||
[string]$Root = (Split-Path -Parent $PSScriptRoot)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$failures = [System.Collections.Generic.List[string]]::new()
|
||||
|
||||
$requiredFiles = @(
|
||||
'AGENTS.md',
|
||||
'docs\README.md',
|
||||
'docs\CODING_STANDARD.md',
|
||||
'docs\DOCUMENTATION_STANDARD.md',
|
||||
'docs\modules\README.md',
|
||||
'docs\modules\TEMPLATE.md',
|
||||
'targets\DEVELOPMENT_ROADMAP.md'
|
||||
)
|
||||
|
||||
foreach ($relative in $requiredFiles) {
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $Root $relative))) {
|
||||
$failures.Add("Missing required documentation file: $relative")
|
||||
}
|
||||
}
|
||||
|
||||
$agentGuide = Join-Path $Root 'AGENTS.md'
|
||||
if (Test-Path -LiteralPath $agentGuide) {
|
||||
$agentContent = Get-Content -Raw -Encoding UTF8 -LiteralPath $agentGuide
|
||||
if ($agentContent -notmatch 'DOCUMENTATION_STANDARD\.md') {
|
||||
$failures.Add('AGENTS.md does not reference docs/DOCUMENTATION_STANDARD.md')
|
||||
}
|
||||
if ($agentContent -notmatch 'CODING_STANDARD\.md') {
|
||||
$failures.Add('AGENTS.md does not reference docs/CODING_STANDARD.md')
|
||||
}
|
||||
}
|
||||
|
||||
$requiredHeadings = @(
|
||||
'## Metadata',
|
||||
'## Purpose',
|
||||
'## Non-goals',
|
||||
'## Context and boundaries',
|
||||
'## Public API',
|
||||
'## Inputs and outputs',
|
||||
'## Data flow',
|
||||
'## Ownership, threading and resources',
|
||||
'## Errors, cancellation and recovery',
|
||||
'## Verification',
|
||||
'## Known gaps and risks',
|
||||
'## Source map'
|
||||
)
|
||||
|
||||
$modulesDir = Join-Path $Root 'docs\modules'
|
||||
$moduleCount = 0
|
||||
if (Test-Path -LiteralPath $modulesDir) {
|
||||
$moduleFiles = Get-ChildItem -LiteralPath $modulesDir -Filter '*.md' -File |
|
||||
Where-Object { $_.Name -notin @('README.md', 'TEMPLATE.md') }
|
||||
foreach ($file in $moduleFiles) {
|
||||
++$moduleCount
|
||||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName
|
||||
foreach ($heading in $requiredHeadings) {
|
||||
if (-not $content.Contains($heading)) {
|
||||
$failures.Add("docs/modules/$($file.Name): missing heading '$heading'")
|
||||
}
|
||||
}
|
||||
if ($content -notmatch '(?s)```mermaid\s+flowchart') {
|
||||
$failures.Add("docs/modules/$($file.Name): missing Mermaid data-flow diagram")
|
||||
}
|
||||
if ($content -notmatch '\| Status \|') {
|
||||
$failures.Add("docs/modules/$($file.Name): missing Status metadata")
|
||||
}
|
||||
if ($content -notmatch '\| Last verified \|') {
|
||||
$failures.Add("docs/modules/$($file.Name): missing Last verified metadata")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$documentationRoots = @('docs', 'targets', 'coordination')
|
||||
foreach ($relativeRoot in $documentationRoots) {
|
||||
$path = Join-Path $Root $relativeRoot
|
||||
if (-not (Test-Path -LiteralPath $path)) {
|
||||
continue
|
||||
}
|
||||
foreach ($file in Get-ChildItem -LiteralPath $path -Recurse -Filter '*.md' -File) {
|
||||
$content = Get-Content -Raw -Encoding UTF8 -LiteralPath $file.FullName
|
||||
foreach ($match in [regex]::Matches($content, '\[[^\]]+\]\(([^)#]+)(?:#[^)]+)?\)')) {
|
||||
$target = $match.Groups[1].Value
|
||||
if ($target -match '^(https?:|mailto:)') {
|
||||
continue
|
||||
}
|
||||
$resolved = Join-Path $file.DirectoryName $target
|
||||
if (-not (Test-Path -LiteralPath $resolved)) {
|
||||
$relativeFile = Resolve-Path -LiteralPath $file.FullName -Relative
|
||||
$failures.Add("$relativeFile`: broken relative link '$target'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
foreach ($failure in $failures) {
|
||||
Write-Error $failure
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Output "Documentation check passed: module_specs=$moduleCount required_files=$($requiredFiles.Count)"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user