Compare commits

..

2 Commits

Author SHA1 Message Date
sindoring 01953cc25e обновление md файлов 2026-07-10 15:45:57 +04:00
sindoring b199876ce6 базовый рендер 2026-07-10 11:21:33 +04:00
24 changed files with 828 additions and 91 deletions
+2
View File
@@ -15,6 +15,7 @@
3. Прочитать [`docs/README.md`](docs/README.md) и связанные с задачей нормативные документы.
4. Для renderer-задач прочитать [`RENDER.md`](RENDER.md).
5. Проверить рабочее дерево и не перезаписывать чужие изменения.
6. Перед созданием сложной подсистемы или добавлением dependency проверить [`docs/TOOLING_CATALOG.md`](docs/TOOLING_CATALOG.md), зарегистрировать найденные готовые решения и провести bounded evaluation вместо немедленного вендоринга.
Если задача пользователя противоречит target-порядку, прямое указание пользователя имеет приоритет, но агент обязан сохранить архитектурные границы и отметить влияние на roadmap.
@@ -28,6 +29,7 @@
- Интерактивный authoring UI реализуется только через Godot `EditorPlugin`.
- Любой новый формат, публичный контракт или несовместимое решение требует обновления документации и при необходимости ADR.
- Исправление дефекта должно получать regression test, когда это технически возможно.
- Не писать крупную подсистему с нуля до поиска готовых libraries, reference implementations, fixtures и tooling; результат исследования фиксируется в `docs/TOOLING_CATALOG.md`.
## Метки состояния
+2
View File
@@ -1,5 +1,7 @@
# Renderer Notes
Воспроизводимый renderer baseline для текущей цели M00, regression manifest, метрики и правила сравнения описаны в [`docs/RENDER_BASELINE.md`](docs/RENDER_BASELINE.md). Baseline фиксирует текущее поведение и известные расхождения, но не заявляет parity с WoW 3.3.5a.
Цель renderer-работы в этом проекте: добиться ощущения производительности оригинального клиента WoW 3.3.5a в Godot, без видимых фризов при переходе ADT -> ADT и без постоянного отката видимых участков к низкому качеству.
Этот документ фиксирует текущее состояние рендера, сделанные оптимизации и практические правила дальнейшей работы.
+9
View File
@@ -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/
+2
View File
@@ -92,6 +92,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 +111,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.
## Требование к расширяемости
+9
View File
@@ -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:
+4
View File
@@ -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.
+15
View File
@@ -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.
+1
View File
@@ -22,6 +22,7 @@
| [TESTING.md](TESTING.md) | Стратегия тестирования runtime, протокола, рендера и authoring pipeline |
| [ROADMAP.md](ROADMAP.md) | Последовательность вертикальных срезов и критерии готовности |
| [REFERENCES.md](REFERENCES.md) | Использованные локальные reference-проекты и правила работы с ними |
| [TOOLING_CATALOG.md](TOOLING_CATALOG.md) | Реестр готовых библиотек, редакторов, test oracles и кандидатов на интеграцию |
## Обязательный workflow изменения функции
+29
View File
@@ -41,6 +41,32 @@ 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).
## Правила исследования
1. Для protocol semantics приоритет имеют wire fixtures и соответствующая версия server core.
@@ -49,6 +75,7 @@ Reference-код используется для исследования фор
4. Любое заимствование проверяется по лицензии и документируется.
5. Proprietary data не переносится в fixtures или репозиторий.
6. Вывод, влияющий на публичный контракт, оформляется ADR или обновлением соответствующей спецификации.
7. Перед собственной реализацией сложной подсистемы проверить [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md) и добавить найденные решения, даже если они останутся только reference.
## Замеченные полезные уроки
@@ -57,3 +84,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-программы.
+76
View File
@@ -0,0 +1,76 @@
# 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` — повторный визит в том же процессе после обхода контрольных точек.
`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 не закрывает.
+22
View File
@@ -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
+91
View File
@@ -0,0 +1,91 @@
# Каталог готовых инструментов и 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, неполный клиент |
| [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 |
## 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.
## Процесс добавления инструмента
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 обязателен.
@@ -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
+233 -51
View File
@@ -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,42 +10,56 @@ 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 headless := DisplayServer.get_name().to_lower() == "headless"
var dry_run := args.has("--dry-run") or headless
var viewport: Array = manifest.get("viewport", [1280, 900])
get_root().size = Vector2i(maxi(16, int(viewport[0])), maxi(16, int(viewport[1])))
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 = float(manifest.get("camera_fov", 62.0))
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
@@ -84,51 +72,245 @@ 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,
"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]))
camera.look_at(_vector3(checkpoint.get("target", [0.0, 0.0, 0.0])), Vector3.UP)
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
image.flip_y()
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 _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():
+97
View File
@@ -0,0 +1,97 @@
{
"schema_version": 1,
"profile": "Blizzlike335",
"scene": "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
"map": "Azeroth",
"quality_preset": "High",
"viewport": [1280, 900],
"camera_fov": 62.0,
"default_wait_seconds": 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_diff": "perceptual comparison required; 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],
"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],
"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": [16835.0, 135.0, 26435.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": [16942.0, 82.0, 26503.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": [16445.0, 125.0, 26295.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,83 @@
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 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 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)
+15 -15
View File
@@ -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.
+1
View File
@@ -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
+4 -1
View File
@@ -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
+3
View File
@@ -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.
+5 -1
View File
@@ -17,7 +17,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 без работающего сценария запрещена.
+3
View File
@@ -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.
+6
View File
@@ -44,6 +44,12 @@
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
Каждая цель должна сохранить:
+63
View File
@@ -0,0 +1,63 @@
[CmdletBinding()]
param(
[string]$GodotPath,
[string]$Output = "user://render_baseline",
[string]$CacheState = "existing",
[double]$WaitSeconds = 8.0,
[double]$MeasureSeconds = 3.0,
[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")
$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
Write-Host "Renderer baseline completed. Report: $Output/report.json"
} finally {
Pop-Location
}