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) и связанные с задачей нормативные документы. 3. Прочитать [`docs/README.md`](docs/README.md) и связанные с задачей нормативные документы.
4. Для renderer-задач прочитать [`RENDER.md`](RENDER.md). 4. Для renderer-задач прочитать [`RENDER.md`](RENDER.md).
5. Проверить рабочее дерево и не перезаписывать чужие изменения. 5. Проверить рабочее дерево и не перезаписывать чужие изменения.
6. Перед созданием сложной подсистемы или добавлением dependency проверить [`docs/TOOLING_CATALOG.md`](docs/TOOLING_CATALOG.md), зарегистрировать найденные готовые решения и провести bounded evaluation вместо немедленного вендоринга.
Если задача пользователя противоречит target-порядку, прямое указание пользователя имеет приоритет, но агент обязан сохранить архитектурные границы и отметить влияние на roadmap. Если задача пользователя противоречит target-порядку, прямое указание пользователя имеет приоритет, но агент обязан сохранить архитектурные границы и отметить влияние на roadmap.
@@ -28,6 +29,7 @@
- Интерактивный authoring UI реализуется только через Godot `EditorPlugin`. - Интерактивный authoring UI реализуется только через Godot `EditorPlugin`.
- Любой новый формат, публичный контракт или несовместимое решение требует обновления документации и при необходимости ADR. - Любой новый формат, публичный контракт или несовместимое решение требует обновления документации и при необходимости ADR.
- Исправление дефекта должно получать regression test, когда это технически возможно. - Исправление дефекта должно получать regression test, когда это технически возможно.
- Не писать крупную подсистему с нуля до поиска готовых libraries, reference implementations, fixtures и tooling; результат исследования фиксируется в `docs/TOOLING_CATALOG.md`.
## Метки состояния ## Метки состояния
+2
View File
@@ -1,5 +1,7 @@
# Renderer Notes # 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 и без постоянного отката видимых участков к низкому качеству. Цель 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 запрещён. Владеет 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 ### Audio
Владеет music, ambience, positional SFX, UI sounds и voice playback. Получает zone/combat/weather events и DBC-backed descriptors. Gameplay не управляет AudioStreamPlayer напрямую. Владеет 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 является отдельной явно подтверждаемой операцией. 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 ```text
@@ -129,6 +137,7 @@ src/
editor/ editor/
server_data/ server_data/
build_pipeline/ build_pipeline/
navigation/
shared/ shared/
tests/ tests/
addons/ addons/
+2
View File
@@ -92,6 +92,7 @@
- HUD: unit frames, target/focus, action bars, buffs, cast bars, bags, quest tracking, chat и combat text. - HUD: unit frames, target/focus, action bars, buffs, cast bars, bags, quest tracking, chat и combat text.
- FrameXML: templates, inheritance, anchors, strata, layers, scripts и events. - FrameXML: templates, inheritance, anchors, strata, layers, scripts и events.
- Lua 5.1-compatible API surface, secure boundaries, SavedVariables и addon lifecycle. - 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. - Widgets: Frame, Button, CheckButton, EditBox, Slider, StatusBar, ScrollFrame, Tooltip, Cooldown, Model, Texture, FontString, Minimap.
- Macro conditionals, keybindings, slash commands, localization и UI scaling. - Macro conditionals, keybindings, slash commands, localization и UI scaling.
@@ -110,6 +111,7 @@
- Quests, gossip, conditions, loot, vendors, trainers и localization. - Quests, gossip, conditions, loot, vendors, trainers и localization.
- Zones, areas, graveyards, teleports, taxi, triggers и phase rules. - Zones, areas, graveyards, teleports, taxi, triggers и phase rules.
- Dungeon packages, encounters, doors, bosses, scripts и validation. - 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. - Import, diff, package, SQL/change-set generation, dev deployment и playtest.
## Требование к расширяемости ## Требование к расширяемости
+9
View File
@@ -91,6 +91,15 @@ Custom locations и mechanics могут требовать согласован
`ContentBundleManifest` связывает client и server artifacts общей версией. Клиент не входит в мир при несовместимой обязательной версии пакета без понятной диагностики. `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 и модификации ## Overlay и модификации
Оригинальные извлечённые файлы не изменяются in-place. Пользовательские пакеты образуют ordered overlay: Оригинальные извлечённые файлы не изменяются in-place. Пользовательские пакеты образуют ordered overlay:
+4
View File
@@ -36,6 +36,8 @@
Graph validator обнаруживает циклы без намеренного repeat policy, недостижимые узлы, отсутствующие targets и dead ends. 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 и приблизительную стоимость рендера. Редактор должен показывать 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 ## Подземелья и encounters
`DungeonPackage` объединяет входы, difficulty, encounters, doors, triggers, trash groups, bosses, loot, quests, graveyards, scripts и localization. `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. - Paths: waypoint, patrol, spline, taxi и transport preview.
- Volumes: area, subzone, encounter, graveyard, phase и weather. - Volumes: area, subzone, encounter, graveyard, phase и weather.
- Visualization modes: collision, normals, chunk/tile bounds, nav/pathing, spawn density, quest dependencies и streaming budgets. - 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 не содержала команду на каждую вершину. Каждый stroke или placement создаёт одну логическую undoable command. Длительный stroke агрегируется, чтобы history не содержала команду на каждую вершину.
@@ -66,6 +67,20 @@ Import, bake, validation, packaging и DB snapshot выполняются как
- staging output и атомарная публикация результата; - staging output и атомарная публикация результата;
- повторяемая команда для headless CI. - повторяемая команда для 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 ## Database dock
- Показывает тип ядра, revision, schema version и capabilities. - Показывает тип ядра, revision, schema version и capabilities.
+1
View File
@@ -22,6 +22,7 @@
| [TESTING.md](TESTING.md) | Стратегия тестирования runtime, протокола, рендера и authoring pipeline | | [TESTING.md](TESTING.md) | Стратегия тестирования runtime, протокола, рендера и authoring pipeline |
| [ROADMAP.md](ROADMAP.md) | Последовательность вертикальных срезов и критерии готовности | | [ROADMAP.md](ROADMAP.md) | Последовательность вертикальных срезов и критерии готовности |
| [REFERENCES.md](REFERENCES.md) | Использованные локальные reference-проекты и правила работы с ними | | [REFERENCES.md](REFERENCES.md) | Использованные локальные reference-проекты и правила работы с ними |
| [TOOLING_CATALOG.md](TOOLING_CATALOG.md) | Реестр готовых библиотек, редакторов, test oracles и кандидатов на интеграцию |
## Обязательный workflow изменения функции ## Обязательный workflow изменения функции
+29
View File
@@ -41,6 +41,32 @@ Reference-код используется для исследования фор
Используем для сверки conversion pipeline, материалов, моделей и authoring/export edge cases. Runtime web/Blender architecture не переносится в OpenWC. Используем для сверки 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. 1. Для protocol semantics приоритет имеют wire fixtures и соответствующая версия server core.
@@ -49,6 +75,7 @@ Reference-код используется для исследования фор
4. Любое заимствование проверяется по лицензии и документируется. 4. Любое заимствование проверяется по лицензии и документируется.
5. Proprietary data не переносится в fixtures или репозиторий. 5. Proprietary data не переносится в fixtures или репозиторий.
6. Вывод, влияющий на публичный контракт, оформляется ADR или обновлением соответствующей спецификации. 6. Вывод, влияющий на публичный контракт, оформляется ADR или обновлением соответствующей спецификации.
7. Перед собственной реализацией сложной подсистемы проверить [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md) и добавить найденные решения, даже если они останутся только reference.
## Замеченные полезные уроки ## Замеченные полезные уроки
@@ -57,3 +84,5 @@ Reference-код используется для исследования фор
- Coordinate conversion и ID allocation должны быть централизованы раньше world editing. - Coordinate conversion и ID allocation должны быть централизованы раньше world editing.
- DB export без canonical model быстро связывает UI с одной версией ядра. - DB export без canonical model быстро связывает UI с одной версией ядра.
- Screenshot smoke полезен, но должен дополняться deterministic state, performance и protocol tests. - 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`; - adapter round-trip `canonical → SQL → snapshot → canonical`;
- Content Project compatibility и migration fixtures; - Content Project compatibility и migration fixtures;
- renderer/editor public API contracts. - 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 ### Integration
@@ -49,6 +52,25 @@
- frame time, hitch percentiles, draw calls, memory, descriptor count и streaming queues; - frame time, hitch percentiles, draw calls, memory, descriptor count и streaming queues;
- long traversal через ADT boundaries; - long traversal через ADT boundaries;
- dense WMO/M2, water, character equipment и UI scale matrices. - 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 ### 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: func _tick_runtime_stats(delta: float) -> void:
if not runtime_stats_enabled or Engine.is_editor_hint(): if not runtime_stats_enabled or Engine.is_editor_hint():
return return
+256 -74
View File
@@ -1,33 +1,7 @@
extends SceneTree extends SceneTree
const STREAMING_SCENE := "res://src/scenes/streaming/eastern_kingdoms_streaming.tscn" 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")
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),
},
]
func _initialize() -> void: func _initialize() -> void:
@@ -36,42 +10,56 @@ func _initialize() -> void:
func _capture_async() -> void: func _capture_async() -> void:
var args := OS.get_cmdline_user_args() 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 only := _arg(args, "--only", "").to_lower()
var wait_seconds := float(_arg(args, "--wait", "8.0")) var wait_seconds := float(_arg(args, "--wait", str(manifest.get("default_wait_seconds", 8.0))))
var width := int(_arg(args, "--width", "1280")) var measure_seconds := float(_arg(args, "--measure", str(manifest.get("default_measure_seconds", 3.0))))
var height := int(_arg(args, "--height", "900")) var revision := _arg(args, "--revision", "worktree")
var cache_state := _arg(args, "--cache-state", "existing")
var headless := DisplayServer.get_name().to_lower() == "headless" var headless := DisplayServer.get_name().to_lower() == "headless"
var dry_run := args.has("--dry-run") or 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) var abs_output_dir := ProjectSettings.globalize_path(output_dir)
if not dry_run: DirAccess.make_dir_recursive_absolute(abs_output_dir)
DirAccess.make_dir_recursive_absolute(abs_output_dir) if headless and not args.has("--dry-run"):
elif headless and not args.has("--dry-run"):
print("RENDER_CHECKPOINT dry-run: headless display cannot capture viewport PNGs.") 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: if packed == null:
push_error("Cannot load streaming scene: %s" % STREAMING_SCENE) push_error("Cannot load streaming scene: %s" % scene_path)
quit(1) quit(1)
return return
var world = packed.instantiate() var world = packed.instantiate()
if not (world is Node3D): 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) quit(1)
return 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() var camera := Camera3D.new()
camera.name = "CheckpointCamera" camera.name = "CheckpointCamera"
camera.current = true camera.current = true
camera.fov = 62.0 camera.fov = float(manifest.get("camera_fov", 62.0))
camera.far = 50000.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 as Node3D).add_child(camera)
world.set("camera_path", NodePath("CheckpointCamera")) world.set("camera_path", NodePath("CheckpointCamera"))
world.set("debug_streaming", true) world.set("debug_streaming", true)
world.set("runtime_stats_enabled", true)
get_root().add_child(world) get_root().add_child(world)
await process_frame await process_frame
await process_frame await process_frame
@@ -84,51 +72,245 @@ func _capture_async() -> void:
if visual != null: if visual != null:
visual.visible = false 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 var captured := 0
for checkpoint_variant in CHECKPOINTS: var passes := ["cold_process", "warm_revisit"]
var checkpoint: Dictionary = checkpoint_variant if dry_run:
var checkpoint_name := String(checkpoint.get("name", "checkpoint")) passes = ["dry_run"]
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
continue
camera.global_position = checkpoint.get("camera", Vector3.ZERO) for pass_name in passes:
camera.look_at(checkpoint.get("target", Vector3.ZERO), Vector3.UP) for checkpoint_variant in checkpoints:
if player != null: if not (checkpoint_variant is Dictionary):
player.global_position = checkpoint.get("player", checkpoint.get("target", Vector3.ZERO)) continue
if world.has_method("_refresh_streaming_targets_at"): var checkpoint: Dictionary = checkpoint_variant
world.call("_refresh_streaming_targets_at", camera.global_position, true) var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
if not only.is_empty() and not checkpoint_name.to_lower().contains(only):
continue
await create_timer(maxf(wait_seconds, 0.0)).timeout var probe := _create_probe(checkpoint, dry_run)
if dry_run: if probe != null:
print("RENDER_CHECKPOINT dry_run name=%s camera=%s target=%s" % [ (world as Node3D).add_child(probe)
checkpoint_name, probe.global_position = _vector3(checkpoint.get("target", [0.0, 0.0, 0.0]))
camera.global_position, probe.scale = Vector3.ONE * float(checkpoint.get("probe_scale", 1.0))
checkpoint.get("target", Vector3.ZERO),
]) 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 = _vector3(checkpoint.get("player", checkpoint.get("target", [0.0, 0.0, 0.0])))
_set_sky_time(world, float(checkpoint.get("time_hours", 13.0)))
if world.has_method("_refresh_streaming_targets_at"):
world.call("_refresh_streaming_targets_at", camera.global_position, true)
if dry_run:
print("RENDER_CHECKPOINT dry_run name=%s coverage=%s camera=%s target=%s time=%.2f" % [
checkpoint_name,
str(checkpoint.get("coverage", [])),
camera.global_position,
_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()
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
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 captured += 1
continue if probe != null:
probe.queue_free()
await process_frame
await RenderingServer.frame_post_draw var report_path := abs_output_dir.path_join("report.json")
var report_file := FileAccess.open(report_path, FileAccess.WRITE)
var image := get_root().get_texture().get_image() if report_file == null:
var file_name := "%s.png" % checkpoint_name push_error("Cannot write render baseline report: %s" % report_path)
var abs_path := abs_output_dir.path_join(file_name) quit(1)
var err := image.save_png(abs_path) return
if err != OK: report_file.store_string(JSON.stringify(report, " "))
push_error("Failed to save checkpoint %s to %s (err=%d)" % [checkpoint_name, abs_path, err]) report_file.close()
quit(1) print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
return
print("RENDER_CHECKPOINT saved %s" % abs_path)
captured += 1
world.queue_free() world.queue_free()
if captured <= 0: 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) quit(1)
return return
quit(0) 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: func _arg(args: PackedStringArray, name: String, default_value: String) -> String:
var idx := args.find(name) var idx := args.find(name)
if idx >= 0 and idx + 1 < args.size(): 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 ## Steps
- [ ] Зафиксировать версию Godot, backend, hardware/profile и cache state. - [x] Зафиксировать версию Godot, backend, hardware/profile и cache state.
- [ ] Выбрать контрольные позиции: terrain, ADT boundary, dense M2, large WMO, liquid, animated M2, sky transition. - [x] Выбрать контрольные позиции: terrain, ADT boundary, dense M2, large WMO, liquid, animated M2, sky transition.
- [ ] Объединить существующие renderer smoke scripts в документированный запуск без изменения visual behavior. - [x] Объединить существующие renderer smoke scripts в документированный запуск без изменения visual behavior.
- [ ] Снять deterministic screenshots и metadata для каждой позиции. - [x] Снять deterministic screenshots и metadata для каждой позиции.
- [ ] Записать cold/warm показатели frame time p50/p95/p99, max hitch, memory, queues и load time. - [x] Записать cold/warm показатели frame time p50/p95/p99, max hitch, memory, queues и load time.
- [ ] Зафиксировать cache format versions и правила invalidation. - [x] Зафиксировать cache format versions и правила invalidation.
- [ ] Добавить regression manifest, не содержащий proprietary assets. - [x] Добавить regression manifest, не содержащий proprietary assets.
- [ ] Обновить `RENDER.md` ссылкой на baseline и известные расхождения с клиентом 3.3.5a. - [x] Обновить `RENDER.md` ссылкой на baseline и известные расхождения с клиентом 3.3.5a.
## Fidelity evidence ## Fidelity evidence
@@ -38,10 +38,10 @@ godot --headless --path . --script res://src/tools/capture_render_checkpoints.gd
## Evidence ## Evidence
- Date: - Date: 2026-07-10
- Revision/worktree: - Revision/worktree: `93bfe11` + worktree baseline changes.
- Commands: - 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: - 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: - 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: - 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: - 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. - [ ] Добавить schema migration `previous → current`, backup и fixtures.
- [ ] Реализовать ordered overlays без изменения extracted upstream assets. - [ ] Реализовать ordered overlays без изменения extracted upstream assets.
- [ ] Обеспечить headless validate/build parity с Editor. - [ ] Обеспечить headless validate/build parity с Editor.
- [ ] Зарезервировать versioned navigation profile/artifact manifest: source geometry hash, backend/version, parameters и tile checksums без хранения generated cache как исходника.
## Exit criteria ## Exit criteria
+4 -1
View File
@@ -13,13 +13,16 @@
- [ ] Реализовать transform gizmo, snapping и coordinate display. - [ ] Реализовать transform gizmo, snapping и coordinate display.
- [ ] Создать/переместить/удалить creature spawn через undoable commands. - [ ] Создать/переместить/удалить creature spawn через undoable commands.
- [ ] Добавить waypoint/patrol path minimum. - [ ] Добавить 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. - [ ] Показать validation и semantic/SQL diff.
- [ ] Применить change set к dev DB и выполнить post-apply verification. - [ ] Применить change set к dev DB и выполнить post-apply verification.
- [ ] Повторно импортировать snapshot и подтвердить отсутствие неожиданного diff. - [ ] Повторно импортировать snapshot и подтвердить отсутствие неожиданного diff.
## Exit criteria ## 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 ## Evidence
+3
View File
@@ -17,11 +17,14 @@ Headless protocol sandbox подключается к build 12340 server и ст
- [ ] Movement packet family и server time. - [ ] Movement packet family и server time.
- [ ] Redacted packet capture/replay и malformed corpus. - [ ] Redacted packet capture/replay и malformed corpus.
- [ ] Disconnect/reconnect reasons и structured diagnostics. - [ ] Disconnect/reconnect reasons и structured diagnostics.
- [ ] Исследовать Wowser auth/realm/character/world lifecycle и packet handlers как независимый 3.3.5a reference; выписать совпадения/расхождения с TrinityCore/AzerothCore и не переносить WebSocket proxy architecture.
## Fidelity evidence ## Fidelity evidence
Exact-byte fixtures сверяются с оригинальным клиентом или соответствующей server-core реализацией. Network handler не публикует raw packets за пределы adapter. Exact-byte fixtures сверяются с оригинальным клиентом или соответствующей server-core реализацией. Network handler не публикует raw packets за пределы adapter.
Wowser может подтвердить найденный edge case или дать тестовую идею, но не является authoritative protocol specification.
## Exit criteria ## Exit criteria
Replay детерминирован; live sandbox проходит auth → realm → character → world и создаёт entity registry без renderer/UI. Replay детерминирован; live sandbox проходит auth → realm → character → world и создаёт entity registry без renderer/UI.
+5 -1
View File
@@ -17,7 +17,11 @@
- [ ] PvP, duel, battlegrounds, arenas и LFG. - [ ] PvP, duel, battlegrounds, arenas и LFG.
- [ ] Vehicles, cinematics, weather, audio и loading transitions. - [ ] Vehicles, cinematics, weather, audio и loading transitions.
- [ ] Character/M2/WMO/liquid/material/animation fidelity gaps. - [ ] 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. - [ ] Localization, settings, accessibility и recovery.
Каждая строка превращается в отдельный vertical-slice spec с acceptance criteria, fixtures и fidelity evidence. Массовая реализация opcodes или Lua stubs без работающего сценария запрещена. Каждая строка превращается в отдельный vertical-slice spec с acceptance criteria, fixtures и fidelity evidence. Массовая реализация opcodes или Lua stubs без работающего сценария запрещена.
+3
View File
@@ -14,6 +14,9 @@
- [ ] Loot, quests, localization и instance messages. - [ ] Loot, quests, localization и instance messages.
- [ ] SmartAI generation и/или versioned core script skeleton. - [ ] SmartAI generation и/или versioned core script skeleton.
- [ ] Validation: reachability, references, door-state graph, wipe recovery и budgets. - [ ] 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. - [ ] Offline preview, server deploy и group playtest launcher.
- [ ] Deterministic encounter simulation cases и multiplayer soak. - [ ] Deterministic encounter simulation cases и multiplayer soak.
+6
View File
@@ -44,6 +44,12 @@
M07 и M08 могут выполняться параллельными командами только после M06/M03 соответственно и только если пользователь явно разрешил параллельную работу. Их результаты сходятся в M11. 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 ## Общий 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
}