From 0d7ae6a017b8b2bf26c5c14c39cb62b599e8262d Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Thu, 19 Feb 2026 11:07:04 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8?= =?UTF-8?q?=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=81=D0=BF=D0=B5=D1=86=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновлены спецификации `runtime-pipeline`, `sound`, `terrain-map-loading`, `texture`, `ui` и `wear`. - Добавлены разделы о статусе покрытия и оставшихся задачах для достижения 100% завершенности. - Внесены уточнения по архитектурным ролям, минимальным контрактам и требованиям к toolchain для каждой подсистемы. - Уточнены форматы данных и правила взаимодействия между компонентами системы. --- docs/specs/ai.md | 34 +- docs/specs/arealmap.md | 30 +- docs/specs/behavior.md | 27 +- docs/specs/control.md | 27 +- docs/specs/coverage-audit.md | 51 ++ docs/specs/fxid.md | 18 +- docs/specs/material.md | 16 +- docs/specs/materials-texm.md | 10 + docs/specs/missions.md | 32 +- docs/specs/msh-animation.md | 16 +- docs/specs/msh-core.md | 19 +- docs/specs/msh-notes.md | 327 ++++--------- docs/specs/msh.md | 19 +- docs/specs/network.md | 27 +- docs/specs/nres.md | 781 +++++------------------------- docs/specs/render-parity.md | 13 + docs/specs/render.md | 16 +- docs/specs/rsli.md | 230 +++++++++ docs/specs/runtime-pipeline.md | 10 + docs/specs/sound.md | 31 +- docs/specs/terrain-map-loading.md | 558 +++++++-------------- docs/specs/texture.md | 16 +- docs/specs/ui.md | 32 +- docs/specs/wear.md | 16 +- 24 files changed, 1043 insertions(+), 1313 deletions(-) create mode 100644 docs/specs/coverage-audit.md create mode 100644 docs/specs/rsli.md diff --git a/docs/specs/ai.md b/docs/specs/ai.md index 545c07b..7570cd0 100644 --- a/docs/specs/ai.md +++ b/docs/specs/ai.md @@ -1,5 +1,35 @@ # AI system -Документ описывает подсистему искусственного интеллекта: принятие решений, pathfinding и стратегическое поведение противников. +Страница фиксирует границы подсистемы AI на уровне движка: -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `ai.dll`. +- выбор целей; +- тактические приоритеты; +- координация с `Behavior`, `ArealMap`, `Missions`. + +## 1. Текущая зафиксированная часть + +1. AI работает поверх ареалов/клеток карты, а не напрямую поверх render-геометрии. +2. Результат AI передается в behavior/command-слой как набор целевых состояний и команд. +3. Решения AI зависят от миссионных триггеров и состояния объектов мира. + +## 2. Контракт интеграции + +В 1:1 реализации AI должен быть совместим с: + +1. системой ареалов (`Land.map`); +2. объектными категориями (`BuildDat.lst`); +3. поведением юнитов (`behavior.md`); +4. миссионными условиями (`missions.md`). + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- роль AI в общей архитектуре и точки интеграции с соседними подсистемами. + +Осталось: + +1. Полный формат runtime-AI состояний и таблиц решений. +2. Полные правила выбора цели/маршрута/приоритета огня. +3. Полная спецификация влияния миссионных скриптов на AI. +4. Набор тест-кейсов «AI tick parity» для побайтного/пошагового сравнения с оригиналом. diff --git a/docs/specs/arealmap.md b/docs/specs/arealmap.md index cac2743..3b234c9 100644 --- a/docs/specs/arealmap.md +++ b/docs/specs/arealmap.md @@ -1,5 +1,31 @@ # ArealMap -Документ описывает формат и структуру карты мира: зоны/сектора, координаты, размещение объектов и связь с terrain и миссиями. +`ArealMap` — подсистема топологии мира и логических зон. -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `ArealMap.dll`. +Подробный бинарный формат `Land.map` и связь с terrain описаны в: + +- [Terrain + ArealMap](terrain-map-loading.md) + +## 1. Роль в движке + +1. Хранит ареалы, связи между ареалами и клеточный индекс. +2. Используется для навигации, логики объектов и AI-решений. +3. Связывает геометрию карты с миссионной и поведенческой логикой. + +## 2. Минимальный runtime-контракт + +1. Валидный граф ареалов и edge-link связей. +2. Валидная cell-grid индексация (`cellsX/cellsY` + hit lists). +3. Согласованные идентификаторы ареалов для AI/Behavior/Missions. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- бинарный контракт `Land.map` и pair-загрузка с `Land.msh`. + +Осталось: + +1. Полная доменная семантика `class_id`/`logic_flag` по всем игровым сценариям. +2. Формальная спецификация API-запросов к ArealMap (поиск зон, фильтры, события). +3. Набор parity-тестов поведения навигационных запросов на одинаковых входах. diff --git a/docs/specs/behavior.md b/docs/specs/behavior.md index 9ffd2dc..33d403d 100644 --- a/docs/specs/behavior.md +++ b/docs/specs/behavior.md @@ -1,5 +1,28 @@ # Behavior system -Документ описывает поведенческую логику юнитов: state machine/behavior-паттерны, взаимодействия и базовые правила боевого поведения. +`Behavior` — слой исполнения состояний юнитов между AI-решением и низкоуровневым control-командованием. -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Behavior.dll`. +## 1. Роль в кадре + +1. Принимает решения из AI. +2. Переводит их в state machine юнита. +3. Формирует команды движения/атаки/действий в world/control-слой. + +## 2. Внешние зависимости + +1. `ArealMap` (доступность/топология). +2. `Missions` (триггеры и ограничения сценария). +3. `Control` (выполнение команд). + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- архитектурная роль подсистемы и ее место в runtime-пайплайне. + +Осталось: + +1. Полная спецификация finite-state машин по типам юнитов. +2. Полная таблица переходов, таймаутов и приоритетов. +3. Формализация входных/выходных структур поведения для 1:1 эмуляции. +4. Поведенческие parity-тесты на фиксированных replay-сценариях. diff --git a/docs/specs/control.md b/docs/specs/control.md index a2d3d44..eb1e535 100644 --- a/docs/specs/control.md +++ b/docs/specs/control.md @@ -1,5 +1,28 @@ # Control system -Документ описывает подсистему управления: mapping ввода (клавиатура, мышь, геймпад), обработку событий и буферизацию команд. +`Control` — подсистема входа и маршрутизации команд (пользовательских и системных). -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Control.dll`. +## 1. Роль + +1. Преобразует ввод устройств в команды движка. +2. Синхронизирует управление камерой, UI и объектами мира. +3. Передает команды в gameplay-подсистемы с учетом активного режима игры. + +## 2. Минимальный контракт совместимости + +1. Детерминированный mapping input -> command. +2. Стабильная обработка очереди команд в пределах кадра. +3. Корректный приоритет UI-фокуса над world-input. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- место control-слоя в архитектуре и базовый runtime-контур. + +Осталось: + +1. Полная карта input actions и режимов обработки. +2. Формат внутренних очередей команд и их сериализация. +3. Спецификация edge-case поведения (повтор клавиш, захват мыши, hotkey-конфликты). +4. Пошаговые parity-тесты на записанных последовательностях ввода. diff --git a/docs/specs/coverage-audit.md b/docs/specs/coverage-audit.md new file mode 100644 index 0000000..638f4c1 --- /dev/null +++ b/docs/specs/coverage-audit.md @@ -0,0 +1,51 @@ +# Documentation coverage audit + +Дата аудита: `2026-02-19` +Корпус данных: `testdata/Parkan - Iron Strategy` + +## 1. Проверка форматов архивов + +Результаты: + +- `NRes`: `120` архивов, roundtrip `120/120` (byte-identical) +- `RsLi`: `2` архива, roundtrip `2/2` (byte-identical) +- подтвержден один совместимый quirk: `sprites.lib`, entry `23`, `deflate EOF+1` + +Инструмент: + +- `tools/archive_roundtrip_validator.py` + +## 2. Проверка рендерных форматов + +Результаты: + +- `MSH`: `435/435` валидны +- `Texm`: `518/518` валидны +- `FXID`: `923/923` валидны +- `Terrain/Map` (`Land.msh` + `Land.map`): `33/33` без ошибок/предупреждений + +Инструменты: + +- `tools/msh_doc_validator.py` +- `tools/fxid_abs100_audit.py` +- `tools/terrain_map_doc_validator.py` + +## 3. Глобальный статус по подсистемам + +| Подсистема | Статус | Что блокирует 100% | +|---|---|---| +| Архивы (`NRes`, `RsLi`) | практически закрыта | формализация редких не-ASCII/служебных edge-case | +| 3D geometry (`MSH core`) | высокая готовность | семантика opaque-полей и канонический writer «с нуля» | +| Animation (`Res8/Res19`) | высокая готовность | полный FP-parity на всех edge-case | +| Material/Wear/Texture | высокая готовность | полная field-level семантика служебных флагов и writer-профиль | +| FXID | высокая готовность | полная field-level семантика payload по каждому opcode | +| Terrain/Areal map formats | высокая готовность | доменная семантика `class_id/logic_flag`, ветка `poly_count>0` | +| Render pipeline | хорошая | полный pixel-parity набор эталонных кадров в CI | +| AI/Behavior/Control/Missions/UI/Sound/Network | начальное покрытие | требуется полная спецификация форматов и runtime-контрактов | + +## 4. План доведения до 100% + +1. Закрыть field-level семантику opaque/служебных полей в 3D/FX/terrain подсистемах. +2. Завершить canonical writer paths для авторинга новых ассетов без copy-through. +3. Зафиксировать и автоматизировать pixel/frame parity-критерии в CI. +4. Расширить подсистемные спецификации (`AI`, `Behavior`, `Missions`, `Control`, `UI`, `Sound`, `Network`) до уровня «полный формат + полный runtime-контракт + parity-тесты». diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md index 22d02d8..f723e17 100644 --- a/docs/specs/fxid.md +++ b/docs/specs/fxid.md @@ -3,7 +3,7 @@ `FXID` — бинарный формат эффекта в движке Parkan: Iron Strategy. Эта страница задаёт контракт формата и исполнения на уровне, достаточном для 1:1 порта рендера/симуляции эффектов и для lossless-инструментов. -Связанный контейнер: [NRes / RsLi](nres.md). +Связанные контейнеры: [NRes](nres.md), [RsLi](rsli.md). ## 1. Контейнер @@ -185,4 +185,18 @@ struct ResourceRef64 { ## 11. Статус валидации - Формальные инварианты FXID зафиксированы в `tools/msh_doc_validator.py` и `tools/fxid_abs100_audit.py`. -- В текущем рабочем окружении нет полного набора игровых архивов (`testdata` без payload), поэтому массовая повторная проверка корпуса здесь не выполнялась. +- На полном retail-корпусе `testdata/Parkan - Iron Strategy` проверено `923/923` FXID payload без ошибок. + +## 12. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Контейнер FXID, fixed-size командный поток, opcode-покрытие `1..10`. +2. Базовый runtime-контур исполнения эффекта. +3. Корпусная валидация формата на retail-данных. + +Осталось: + +1. Полная field-level семантика payload каждого opcode для авторинга новых эффектов «с нуля». +2. Формальная спецификация всех `time_mode` веток на уровне точных числовых формул и edge-case поведения. +3. Полный набор пиксельных parity-тестов FX (оригинал vs новый рендер) на фиксированных сценах. diff --git a/docs/specs/material.md b/docs/specs/material.md index cd7eea5..12c8296 100644 --- a/docs/specs/material.md +++ b/docs/specs/material.md @@ -127,4 +127,18 @@ struct KeyRaw { ## 10. Статус валидации - Инварианты MAT0 зафиксированы в текущем toolchain проекта (`docs/specs` + `tools`). -- В этом окружении нет полного игрового корпуса, поэтому статистика по всем материалам не пересчитывалась. +- Структурная валидация MAT0 включена в корпусный прогон `tools/msh_doc_validator.py` на полном retail-наборе. + +## 11. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Бинарный layout `MAT0` и правила чтения фаз/анимационных блоков. +2. Fallback-цепочка материала. +3. Контракт сохранения opaque-полей для lossless editor path. + +Осталось: + +1. Полная семантика всех битов `attr1` и `metaA/B/C/D` для авторинга новых материалов. +2. Полный writer-профиль «канонический MAT0» для генерации ассетов без copy-through. +3. Набор визуальных parity-тестов по material phase animation на реальных моделях. diff --git a/docs/specs/materials-texm.md b/docs/specs/materials-texm.md index 0397c84..beef3ee 100644 --- a/docs/specs/materials-texm.md +++ b/docs/specs/materials-texm.md @@ -6,3 +6,13 @@ - [Wear table (`WEAR`)](wear.md) - [Texture (`Texm`)](texture.md) - [Render pipeline](render.md) + +## Статус покрытия и что осталось до 100% + +Закрыто: + +1. Страница корректно декомпозирована на отдельные объектные спецификации. + +Осталось: + +1. Поддерживать единый changelog согласованности между `material.md`, `wear.md`, `texture.md` и `render.md`. diff --git a/docs/specs/missions.md b/docs/specs/missions.md index 6f351d0..f531132 100644 --- a/docs/specs/missions.md +++ b/docs/specs/missions.md @@ -1,5 +1,33 @@ # Missions -Документ описывает формат миссий и сценариев: начальное состояние, триггеры и связь миссий с картой мира. +Подсистема `Missions` управляет сценарием: -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `MisLoad.dll`. +- стартовыми условиями; +- триггерами; +- победой/поражением; +- синхронизацией с AI/Behavior/World. + +## 1. Что уже зафиксировано + +1. Миссии связаны с картами (`Land.msh`/`Land.map`) и объектными категориями. +2. Скриптовые ресурсы хранятся в архивных контейнерах (`NRes`) и участвуют в runtime-логике. +3. Миссионные события влияют на AI и поведение объектов через общий gameplay-слой. + +## 2. Минимальный runtime-контракт + +1. Детерминированный порядок обработки триггеров в кадре. +2. Единая шкала времени миссии для всех подсистем. +3. Согласованность идентификаторов объектов между mission-data и world-state. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- связь миссионной подсистемы с форматом ресурсов и runtime-контуром. + +Осталось: + +1. Полная спецификация форматов миссионных скриптов/таблиц. +2. Полный перечень типов триггеров и их параметров. +3. Формальные правила разрешения конфликтов триггеров в одном кадре. +4. Набор replay parity-тестов «миссия от старта до завершения». diff --git a/docs/specs/msh-animation.md b/docs/specs/msh-animation.md index 8aa2796..ec5a256 100644 --- a/docs/specs/msh-animation.md +++ b/docs/specs/msh-animation.md @@ -109,4 +109,18 @@ uint16_t map_words[]; // size/2 элементов ## 6. Статус валидации - Форматные проверки включены в `tools/msh_doc_validator.py`. -- В текущем окружении полный игровой корпус MSH не подключен в `testdata`, поэтому массовый прогон здесь не выполнялся. +- Корпусная валидация анимационных инвариантов включена в прогон `tools/msh_doc_validator.py` на полном retail-наборе. + +## 7. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Контракт `Res8 + Res19` и fallback-логика выбора ключа. +2. Базовая интерполяция поз и blending двух сэмплов. +3. Канонические инварианты writer path для существующих ассетов. + +Осталось: + +1. Полная фиксация численного поведения на всех FP-edge-case (включая платформенные различия округления). +2. Полный writer-профиль для авторинга новых анимаций без опоры на reference copy-through. +3. Набор runtime parity-тестов «frame-by-frame pose equivalence» на длинных анимациях. diff --git a/docs/specs/msh-core.md b/docs/specs/msh-core.md index 6a33049..60a4453 100644 --- a/docs/specs/msh-core.md +++ b/docs/specs/msh-core.md @@ -9,7 +9,8 @@ - [Material](material.md) - [Texture (Texm)](texture.md) - [Render pipeline](render.md) -- [NRes / RsLi](nres.md) +- [NRes](nres.md) +- [RsLi](rsli.md) ## 1. Общая модель @@ -174,5 +175,19 @@ for each node: ## 8. Статус валидации - Инварианты формата реализованы в `tools/msh_doc_validator.py`. -- В текущем окружении нет загруженного полного корпуса игровых MSH в `testdata`, поэтому массовый прогон по ассетам здесь не выполнялся. +- На полном retail-корпусе `testdata/Parkan - Iron Strategy` проверено `435/435` MSH-моделей без структурных ошибок. + +## 9. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Базовые таблицы geometry path (`Res1/2/3/4/5/6/7/13`). +2. Критичные range-инварианты slot/batch/index. +3. Правила совместимого writer/editor для lossless работы с существующими ассетами. + +Осталось: + +1. Полная семантика части opaque-полей (`Slot68` tail, `Batch20` opaque-поля) для authoring без copy-through. +2. Полная формализация редких веток (`Res1.attr3 != 38`) на расширенном корпусе. +3. End-to-end writer для генерации новых игровых MSH с подтвержденным runtime-паритетом. diff --git a/docs/specs/msh-notes.md b/docs/specs/msh-notes.md index 1bd4808..6e77c4f 100644 --- a/docs/specs/msh-notes.md +++ b/docs/specs/msh-notes.md @@ -1,277 +1,118 @@ # 3D implementation notes -Контрольные заметки, сводки алгоритмов и остаточные семантические вопросы по 3D-подсистемам. +Контрольная страница с практическими правилами реализации 3D-пайплайна и с перечнем незакрытых зон. +Документ intentionally high-level: без ссылок на внутренние функции/адреса. ---- +Связанные страницы: -## 5.1. Порядок байт +- [MSH core](msh-core.md) +- [MSH animation](msh-animation.md) +- [Material (`MAT0`)](material.md) +- [Texture (`Texm`)](texture.md) +- [FXID](fxid.md) +- [Render pipeline](render.md) -Все значения хранятся в **little‑endian** порядке (платформа x86/Win32). +## 1. Базовые двоичные правила -## 5.2. Выравнивание +1. Все форматы в этой подсистеме little-endian. +2. Внутри NRes данные ресурсов выравниваются по 8 байт. +3. Внутри payload таблиц padding между записями обычно отсутствует: записи идут подряд по stride. -- **NRes‑ресурсы:** данные каждого ресурса внутри NRes‑архива выровнены по границе **8 байт** (0‑padding). -- **Внутренняя структура ресурсов:** таблицы Res1/Res2/Res7/Res13 не имеют межзаписевого выравнивания — записи идут подряд. -- **Vertex streams:** stride'ы фиксированы (12/4/8 байт) — вершинные данные идут подряд без паддинга. +## 2. Быстрая карта stride'ов -## 5.3. Размеры записей на диске +| Ресурс | Запись | Stride | +|---|---|---:| +| Res1 | Node | 38 | +| Res2 | Slot | 68 (после header `0x8C`) | +| Res3 | Position | 12 | +| Res4 | Normal | 4 | +| Res5 | UV0 | 4 | +| Res6 | Index | 2 | +| Res7 | Tri descriptor | 16 | +| Res8 | Animation key | 24 | +| Res13 | Batch | 20 | +| Res19 | Animation map | 2 | -| Ресурс | Запись | Размер (байт) | Stride | -|--------|-----------|---------------|-------------------------| -| Res1 | Node | 38 | 38 (19×u16) | -| Res2 | Slot | 68 | 68 | -| Res3 | Position | 12 | 12 (3×f32) | -| Res4 | Normal | 4 | 4 (4×s8) | -| Res5 | UV0 | 4 | 4 (2×s16) | -| Res6 | Index | 2 | 2 (u16) | -| Res7 | TriDesc | 16 | 16 | -| Res8 | AnimKey | 24 | 24 | -| Res10 | StringRec | переменный | `4 + (len ? len+1 : 0)` | -| Res13 | Batch | 20 | 20 | -| Res19 | AnimMap | 2 | 2 (u16) | -| Res15 | VtxStr | 8 | 8 | -| Res16 | VtxStr | 8 | 8 (2×4) | -| Res18 | VtxStr | 4 | 4 | +## 3. Декодирование ключевых потоков -## 5.4. Вычисление количества элементов +## 3.1. Позиции (Res3) -Количество записей вычисляется из размера ресурса: +`float3`, stride `12`. -``` -count = resource_data_size / record_stride +## 3.2. Нормали (Res4) + +`int8[4]`, используются первые 3 компоненты: + +```text +n = clamp(s8 / 127.0, -1..1) ``` -Например: +## 3.3. UV (Res5) -- `vertex_count = res3_size / 12` -- `index_count = res6_size / 2` -- `batch_count = res13_size / 20` -- `slot_count = (res2_size - 140) / 68` -- `node_count = res1_size / 38` -- `tri_desc_count = res7_size / 16` -- `anim_key_count = res8_size / 24` -- `anim_map_count = res19_size / 2` +`int16[2]`: -Для Res10 нет фиксированного stride: нужно последовательно проходить записи `u32 len` + `(len ? len+1 : 0)` байт. - -## 5.5. Идентификация ресурсов в NRes - -Ресурсы модели идентифицируются по полю `type` (смещение 0) в каталожной записи NRes. Загрузчик использует `niFindRes(archive, type, subtype)` для поиска, где `type` — число (1, 2, 3, ... 20), а `subtype` (byte) — уточнение (из аргумента загрузчика). - -## 5.6. Минимальный набор для рендера - -Для статической модели без анимации достаточно: - -| Ресурс | Обязательность | -|--------|------------------------------------------------| -| Res1 | Да | -| Res2 | Да | -| Res3 | Да | -| Res4 | Рекомендуется | -| Res5 | Рекомендуется | -| Res6 | Да | -| Res7 | Для коллизии | -| Res13 | Да | -| Res10 | Желательно (узловые имена/поведенческие ветки) | -| Res8 | Нет (анимация) | -| Res19 | Нет (анимация) | -| Res15 | Нет | -| Res16 | Нет | -| Res18 | Нет | -| Res20 | Нет | - -## 5.7. Сводка алгоритмов декодирования - -### Позиции (Res3) - -```python -def decode_position(data, vertex_index): - offset = vertex_index * 12 - x = struct.unpack_from(' batch/triangles; + - batch -> indices; + - indices -> vertices; + - anim_map -> anim_keys. +4. Неизвестные поля и неизвестные ресурсы сохранять через copy-through. -```python -def encode_normal(nx, ny, nz): - return ( - max(-128, min(127, int(round(nx * 127.0)))), - max(-128, min(127, int(round(ny * 127.0)))), - max(-128, min(127, int(round(nz * 127.0)))), - 0 # nw = 0 (безопасное значение) - ) -``` +## 5. Практический writer-контракт -### Кодирование UV (для экспортёра) +1. Пересчитывать только явно вычислимые поля. +2. Не нормализовать opaque-данные без уверенной спецификации. +3. При roundtrip неизмененных данных требовать byte-identical результат. +4. Для новых ассетов фиксировать отдельную политику «генерация vs preserve». -```python -def encode_uv(u, v): - return ( - max(-32768, min(32767, int(round(u * 1024.0)))), - max(-32768, min(32767, int(round(v * 1024.0)))) - ) -``` +## 6. Runtime-связка материалов и текстур -### Строки узлов (Res10) +Канонический путь резолва: -```python -def parse_res10_for_nodes(buf: bytes, node_count: int) -> list[str | None]: - out = [] - off = 0 - for _ in range(node_count): - ln = struct.unpack_from(' wear-таблица (`*.wea`). +2. Wear-слот -> material name. +3. Material -> текущая фаза -> `textureName`. +4. `Texm` ищется в `Textures.lib` (или lightmap-библиотеке для lightmap-ветки). -### Ключ анимации (Res8) и mapping (Res19) +Fallback: -```python -def decode_anim_key24(buf: bytes, idx: int): - o = idx * 24 - px, py, pz, t = struct.unpack_from('<4f', buf, o) - qx, qy, qz, qw = struct.unpack_from('<4h', buf, o + 16) - s = 1.0 / 32767.0 - return (px, py, pz), t, (qx * s, qy * s, qz * s, qw * s) -``` +- материал: `DEFAULT`, затем индекс `0`; +- текстура/lightmap: fallback-слот движка. -### Эффектный поток (FXID) +## 7. Что уже закрыто для 1:1 -```python -FX_CMD_SIZE = {1:224,2:148,3:200,4:204,5:112,6:4,7:208,8:248,9:208,10:208} +1. Бинарный контракт базовых MSH таблиц. +2. Контракт animation sampling (`Res8 + Res19`). +3. Контракт MAT0/WEAR/Texm на уровне чтения и применения в кадре. +4. Формат FXID-контейнера, командный поток и fixed command sizes. +5. Валидация на retail-корпусе через `tools/msh_doc_validator.py` (0 ошибок/предупреждений). -def parse_fx_payload(raw: bytes): - cmd_count = struct.unpack_from('> 8) & 1 - size = FX_CMD_SIZE[op] - cmds.append((op, enabled, ptr, size)) - ptr += size - if ptr != len(raw): - raise ValueError('tail bytes after command stream') - return cmds -``` +## 8. Статус покрытия и что осталось до 100% -### Texm (header + mips + Page) - -```python -def parse_texm(raw: bytes): - magic, w, h, mips, f4, f5, unk6, fmt = struct.unpack_from('<8I', raw, 0) - assert magic == 0x6D786554 # 'Texm' - bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444) else 4) - pix_sum = 0 - mw, mh = w, h - for _ in range(mips): - pix_sum += mw * mh - mw = max(1, mw >> 1) - mh = max(1, mh >> 1) - off = 32 + (1024 if fmt == 0 else 0) + bpp * pix_sum - page = None - if off + 8 <= len(raw) and raw[off:off+4] == b'Page': - n = struct.unpack_from(' MSH/NRes`) - -Ниже перечислено то, что нужно закрыть для **lossless round-trip** и 1:1‑поведения при импорте внешней геометрии обратно в формат игры. - -### A) Неполная «авторская» семантика бинарных таблиц - -1. `Res2` header (`первые 0x8C`): не зафиксированы все поля и правила их вычисления при генерации нового файла (а не copy-through из оригинала). -2. `Res7` tri-descriptor: для 16‑байтной записи декодирован базовый каркас, но остаётся неформализованной часть служебных бит/полей, нужных для стабильной генерации adjacency/служебной топологии. -3. `Res13` поля `unk4/unk6/unk14`: для парсинга достаточно, но для генерации «канонических» значений из голого `OBJ` правила не определены. -4. `Res2` slot tail (`unk30..unk40`): семантика не разложена, поэтому при экспорте новых ассетов нет детерминированной формулы заполнения. - -### B) Анимационный path ещё не закрыт как writer - -1. Нужен полный writer для `Res8/Res19`: - - точная спецификация байтового формата на запись; - - правила генерации mapping (`Res19`) по узлам/кадрам; - - жёсткая фиксация округления как в x87 path (включая edge-case на границах кадра). -2. Правила биндинга узлов/строк (`Res10`) и `slotFlags` к runtime‑сущностям пока описаны частично и требуют формализации именно для импорта новых данных. - -### C) Материалы, текстуры, эффекты для «полного ассета» - -1. Для `Texm` не завершён writer, покрывающий все используемые режимы (включая palette path, mip-chain, `Page`, и правила заполнения служебных полей). -2. Для `FXID` известен контейнер/длины команд, но не завершена field-level семантика payload всех opcode для генерации новых эффектов, эквивалентных оригинальному пайплайну. -3. Экспорт только `OBJ` покрывает геометрию; для игрового ассета нужен sidecar-слой (материалы/текстуры/эффекты/анимация), иначе импорт неизбежно неполный. - -### D) Что это означает на практике - -1. `OBJ -> MSH` сейчас реалистичен как **ограниченный static-экспорт** (позиции/индексы/часть batch/slot структуры). -2. `OBJ -> полноценный игровой ресурс` (без потерь, с поведением 1:1) пока недостижим без закрытия пунктов A/B/C. -3. До закрытия пунктов A/B/C рекомендуется использовать режим: - - геометрия экспортируется из `OBJ`; - - неизвестные/служебные поля берутся copy-through из референсного оригинального ассета той же структуры. +1. Полная field-level семантика части служебных полей: + - `Batch20` opaque-поля; + - хвостовые служебные поля slot-записей; + - часть флагов узлов/групп. +2. Полный writer-путь для авторинга новых анимированных ассетов (не только roundtrip существующих). +3. Полная формализация семантики FX payload полей по каждому opcode для генерации новых эффектов, а не только для корректного чтения/исполнения. +4. Полный канонический writer `Texm` для всех редких форматов и edge-case комбинаций служебных флагов. +5. Сквозной «импорт внешнего ассета -> игровой пакет» с формальной спецификацией sidecar-метаданных (материал/эффект/анимация). diff --git a/docs/specs/msh.md b/docs/specs/msh.md index a4e29b6..0581502 100644 --- a/docs/specs/msh.md +++ b/docs/specs/msh.md @@ -13,12 +13,27 @@ 7. [Render pipeline](render.md) — полный процесс рендера кадра. 8. [Terrain + map loading](terrain-map-loading.md) — ландшафт, шейдинг и привязка к миру. 9. [3D implementation notes](msh-notes.md) — контрольные заметки и открытые вопросы. +10. [Documentation coverage audit](coverage-audit.md) — сводка покрытия и оставшиеся блокеры. ## Связанные спецификации -- [NRes / RsLi](nres.md) +- [NRes](nres.md) +- [RsLi](rsli.md) ## Принцип декомпозиции - Форматы и контейнеры документируются отдельно, чтобы их можно было верифицировать и править независимо. -- Runtime-пайплайн вынесен в отдельный документ, потому что пересекает несколько DLL и не является форматом на диске. +- Runtime-пайплайн вынесен в отдельный документ, потому что пересекает несколько runtime-подсистем и не является форматом на диске. + +## Статус покрытия и что осталось до 100% + +Закрыто: + +1. Документация декомпозирована по объектам: geometry, animation, material, texture, wear, fx, render, terrain. +2. Форматные инварианты ключевых 3D-ресурсов проверяются автоматическими валидаторами на retail-корпусе. + +Осталось: + +1. Полный сквозной writer-путь для генерации новых игровых ассетов без copy-through зависимостей. +2. Полный паритетный рендер-тест (эталонные кадры оригинала vs новый рендер) на расширенном наборе моделей/материалов/FX. +3. Полное покрытие соседних геймплейных подсистем (`AI`, `Behavior`, `Missions`, `Control`, `UI`, `Sound`, `Network`) до уровня точных форматов и runtime-контрактов. diff --git a/docs/specs/network.md b/docs/specs/network.md index 1950e8a..9411c34 100644 --- a/docs/specs/network.md +++ b/docs/specs/network.md @@ -1,5 +1,28 @@ # Network system -Документ описывает сетевую подсистему: протокол обмена, синхронизацию состояния и сетевую архитектуру (client-server/P2P). +`Network` — подсистема синхронизации состояния игры между узлами (мультиплеер/обмен состоянием). -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга `Net.dll`. +## 1. Роль + +1. Транспортирует игровые события и state-delta. +2. Синхронизирует критичные объекты мира и таймеры. +3. Обеспечивает согласованность simulation между участниками. + +## 2. Минимальный контракт для 1:1 + +1. Детеминированная сериализация сетевых сообщений. +2. Согласованная обработка порядка/потерь/повторов пакетов. +3. Единая политика authority и коррекции расхождений. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- определено место сетевого слоя в общей архитектуре движка. + +Осталось: + +1. Полная спецификация wire-протокола (header, message types, payload layout). +2. Полный контракт handshake/session lifecycle. +3. Формальные правила resync/rollback/correction. +4. Набор сетевых parity-тестов на контролируемой потере/задержке. diff --git a/docs/specs/nres.md b/docs/specs/nres.md index 32ccb1b..03b4c3e 100644 --- a/docs/specs/nres.md +++ b/docs/specs/nres.md @@ -1,718 +1,189 @@ -# Форматы игровых ресурсов +# NRes -## Обзор +`NRes` — базовый контейнер ресурсов движка Parkan: Iron Strategy. +Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера. -Библиотека `Ngi32.dll` реализует два различных формата архивов ресурсов: +Связанная страница: -1. **NRes** — основной формат архива ресурсов, используемый через API `niOpenResFile` / `niCreateResFile`. Каталог файлов расположен в **конце** файла. Поддерживает создание, редактирование, добавление и удаление записей. +- [RsLi](rsli.md) -2. **RsLi** — формат библиотеки ресурсов, используемый через API `rsOpenLib` / `rsLoad`. Таблица записей расположена **в начале** файла (сразу после заголовка) и зашифрована XOR-шифром. Поддерживает несколько методов сжатия. Только чтение. +## 1. Назначение ---- +`NRes` используется как универсальный архив: -## Часть 1. Формат NRes +- 3D-модели (`*.msh`, `*.rlb`); +- текстуры (`Texm`); +- материалы (`MAT0`); +- эффекты (`FXID`); +- миссионные и служебные ресурсы. -### 1.1. Общая структура файла +Формат поддерживает: -``` -┌──────────────────────────┐ Смещение 0 -│ Заголовок (16 байт) │ -├──────────────────────────┤ Смещение 16 -│ │ -│ Данные ресурсов │ -│ (выровнены по 8 байт) │ -│ │ -├──────────────────────────┤ Смещение = total_size - entry_count × 64 -│ Каталог записей │ -│ (entry_count × 64 байт) │ -└──────────────────────────┘ Смещение = total_size +- чтение; +- поиск по имени; +- редактирование (add/replace/remove); +- полную пересборку архива. + +## 2. Общий layout файла + +```text +[Header: 16] +[Data region: variable, 8-byte aligned chunks] +[Directory: entry_count * 64, всегда в конце файла] ``` -### 1.2. Заголовок файла (16 байт) +Критично: каталог всегда расположен в конце файла. -| Смещение | Размер | Тип | Значение | Описание | -| -------- | ------ | ------- | ------------------- | ------------------------------------ | -| 0 | 4 | char[4] | `NRes` (0x4E526573) | Магическая сигнатура (little-endian) | -| 4 | 4 | uint32 | `0x00000100` (256) | Версия формата (1.0) | -| 8 | 4 | int32 | — | Количество записей в каталоге | -| 12 | 4 | int32 | — | Полный размер файла в байтах | +## 3. Заголовок (16 байт) -**Валидация при открытии:** магическая сигнатура и версия должны совпадать точно. Поле `total_size` (смещение 12) **проверяется на равенство** с фактическим размером файла (`GetFileSize`). Если значения не совпадают — файл отклоняется. +Все значения little-endian. -### 1.3. Положение каталога в файле +| Offset | Size | Type | Значение | +|---:|---:|---|---| +| 0 | 4 | char[4] | `NRes` | +| 4 | 4 | u32 | `0x00000100` (версия 1.0) | +| 8 | 4 | i32 | `entry_count` (должен быть `>= 0`) | +| 12 | 4 | u32 | `total_size` (должен быть равен фактическому размеру файла) | -Каталог располагается в самом конце файла. Его смещение вычисляется по формуле: +Производные значения: -``` -directory_offset = total_size - entry_count × 64 -``` +- `directory_size = entry_count * 64`; +- `directory_offset = total_size - directory_size`. -Данные ресурсов занимают пространство между заголовком (16 байт) и каталогом. +Ограничения: -### 1.4. Запись каталога (64 байта) +- `directory_offset >= 16`; +- `directory_offset + directory_size == total_size`. -Каждая запись каталога занимает ровно **64 байта** (0x40): +## 4. Запись каталога (64 байта) -| Смещение | Размер | Тип | Описание | -| -------- | ------ | -------- | ------------------------------------------------- | -| 0 | 4 | uint32 | Тип / идентификатор ресурса | -| 4 | 4 | uint32 | Атрибут 1 (например, формат, дата, категория) | -| 8 | 4 | uint32 | Атрибут 2 (например, подтип, метка времени) | -| 12 | 4 | uint32 | Размер данных ресурса в байтах | -| 16 | 4 | uint32 | Атрибут 3 (дополнительный параметр) | -| 20 | 36 | char[36] | Имя файла (null-terminated, макс. 35 символов) | -| 56 | 4 | uint32 | Смещение данных от начала файла | -| 60 | 4 | uint32 | Индекс сортировки (для двоичного поиска по имени) | +| Offset | Size | Type | Поле | +|---:|---:|---|---| +| 0 | 4 | u32 | `type_id` | +| 4 | 4 | u32 | `attr1` | +| 8 | 4 | u32 | `attr2` | +| 12 | 4 | u32 | `size` (размер payload) | +| 16 | 4 | u32 | `attr3` | +| 20 | 36 | char[36] | `name_raw` (C-строка) | +| 56 | 4 | u32 | `data_offset` | +| 60 | 4 | u32 | `sort_index` | -#### Поле «Имя файла» (смещение 20, 36 байт) +### 4.1. Имя ресурса (`name_raw`) -- Максимальная длина имени: **35 символов** + 1 байт null-терминатор. -- При записи поле сначала обнуляется (`memset(0, 36 байт)`), затем копируется имя (`strncpy`, макс. 35 символов). -- Поиск по имени выполняется **без учёта регистра** (`_strcmpi`). +Контракт: -#### Поле «Индекс сортировки» (смещение 60) +- максимум 35 полезных байт + NUL; +- допускается ровно один терминатор внутри 36-байтового поля; +- имя сравнивается регистронезависимо по ASCII-правилу (`A..Z` -> `a..z`). -Используется для **двоичного поиска по имени**. Содержит индекс оригинальной записи, отсортированной в алфавитном порядке (регистронезависимо). Индекс строится при сохранении файла функцией `sub_10013260` с помощью **пузырьковой сортировки** по именам. +Для writer/editor: -**Алгоритм поиска** (`sub_10011E60`): классический двоичный поиск по отсортированному массиву индексов. Возвращает оригинальный индекс записи или `-1` при отсутствии. +- запрещено писать NUL внутри полезной части имени; +- запрещены имена длиной > 35 байт. -#### Поле «Смещение данных» (смещение 56) +### 4.2. Диапазон данных (`data_offset`, `size`) -Абсолютное смещение от начала файла. Данные читаются из mapped view: `pointer = mapped_base + data_offset`. +Для каждой записи: -### 1.5. Выравнивание данных +- `data_offset >= 16`; +- `data_offset + size <= directory_offset`. -При добавлении ресурса его данные записываются последовательно, после чего выполняется **выравнивание по 8-байтной границе**: +Практически (канонический writer): каждый payload начинается с 8-байтного выравнивания. -```c -padding = ((data_size + 7) & ~7) - data_size; -// Если padding > 0, записываются нулевые байты -``` +## 5. Таблица сортировки (`sort_index`) -Таким образом, каждый блок данных начинается с адреса, кратного 8. +`sort_index` задает перестановку «отсортированный список -> исходный индекс записи». -При изменении размера данных ресурса выполняется сдвиг всех последующих данных и обновление смещений всех затронутых записей каталога. +Пусть: -### 1.6. Создание файла (API `niCreateResFile`) +- `entries[i]` — i-я запись каталога в исходном порядке; +- `P` — массив индексов `0..entry_count-1`, отсортированный по `entries[idx].name` (ASCII case-insensitive). -При создании нового файла: +Тогда в канонической записи: -1. Если файл уже существует и содержит корректный NRes-архив, существующий каталог считывается с конца файла, а файл усекается до начала каталога. -2. Если файл пуст или не является NRes-архивом, создаётся новый с пустым каталогом. Поля `entry_count = 0`, `total_size = 16`. +- `entries[i].sort_index = P[i]`. -При закрытии файла (`sub_100122D0`): +Это именно таблица для бинарного поиска по имени, а не «ранг текущей записи». -1. Заголовок переписывается в начало файла (16 байт). -2. Вычисляется `total_size = data_end_offset + entry_count × 64`. -3. Индексы сортировки пересчитываются. -4. Каталог записей записывается в конец файла. +## 6. Поиск по имени -### 1.7. Режимы сортировки каталога +Алгоритм поиска: -Функция `sub_10012560` поддерживает 12 режимов сортировки (0–11): +1. Выполнить бинарный поиск по диапазону `i in [0, entry_count)`. +2. На шаге `i` взять `target = entries[i].sort_index`. +3. Сравнить искомое имя с `entries[target].name` (ASCII case-insensitive). +4. При совпадении вернуть `target`. -| Режим | Порядок сортировки | -| ----- | --------------------------------- | -| 0 | Без сортировки (сброс) | -| 1 | По атрибуту 1 (смещение 4) | -| 2 | По атрибуту 2 (смещение 8) | -| 3 | По (атрибут 1, атрибут 2) | -| 4 | По типу ресурса (смещение 0) | -| 5 | По (тип, атрибут 1) | -| 6 | По (тип, атрибут 1) — идентичен 5 | -| 7 | По (тип, атрибут 1, атрибут 2) | -| 8 | По имени (регистронезависимо) | -| 9 | По (тип, имя) | -| 10 | По (атрибут 1, имя) | -| 11 | По (атрибут 2, имя) | +Fail-safe поведение: -### 1.8. Операция `niOpenResFileEx` — флаги открытия +- если `sort_index` некорректен (выход за диапазон), реализация должна перейти на линейный fallback по всем записям; +- fallback использует то же ASCII case-insensitive сравнение. -Второй параметр — битовые флаги: +## 7. Каноническая пересборка архива -| Бит | Маска | Описание | -| --- | ----- | ----------------------------------------------------------------------------------- | -| 0 | 0x01 | Sequential scan hint (`FILE_FLAG_SEQUENTIAL_SCAN` вместо `FILE_FLAG_RANDOM_ACCESS`) | -| 1 | 0x02 | Открыть для записи (read-write). Без флага — только чтение | -| 2 | 0x04 | Пометить файл как «кэшируемый» (не выгружать при refcount=0) | -| 3 | 0x08 | Raw-режим: не проверять заголовок NRes, трактовать весь файл как единый ресурс | +Канонический writer выполняет: -### 1.9. Виртуальное касание страниц +1. Пишет заглушку заголовка (16 байт). +2. Пишет payload всех записей в текущем порядке. +3. После каждого payload добавляет 0-padding до кратности 8. +4. Пересчитывает `sort_index` через сортировку имен. +5. Дописывает каталог (`entry_count * 64`). +6. Пересчитывает и записывает `total_size`. -Функция `sub_100197D0` выполняет «касание» страниц памяти для принудительной загрузки из memory-mapped файла. Она обходит адресное пространство с шагом 4096 байт (размер страницы), начиная с 0x10000 (64 КБ): +Итоговый файл должен удовлетворять всем ограничениям из разделов 3–5. -``` -for (result = 0x10000; result < size; result += 4096); -``` +## 8. Режим `raw` (совместимость инструментов) -Вызывается при чтении данных ресурса с флагом `a3 != 0` для предзагрузки данных в оперативную память. +Для служебных инструментов допускается `raw_mode`: ---- +- любой бинарный файл трактуется как один «сырой» ресурс; +- возвращается одна запись (`name = RAW`, `data_offset = 0`, `size = len(file)`). -## Часть 2. Формат RsLi +Этот режим не является форматом `NRes` на диске, это только режим открытия. -### 2.1. Общая структура файла +## 9. Контрольные инварианты -``` -┌───────────────────────────────┐ Смещение 0 -│ Заголовок файла (32 байта) │ -├───────────────────────────────┤ Смещение 32 -│ Таблица записей (зашифрована)│ -│ (entry_count × 32 байт) │ -├───────────────────────────────┤ Смещение 32 + entry_count × 32 -│ │ -│ Данные ресурсов │ -│ │ -├───────────────────────────────┤ -│ [Опциональный трейлер — 6 б] │ -└───────────────────────────────┘ -``` +Минимальный набор проверок при чтении: -### 2.2. Заголовок файла (32 байта) +1. `magic == "NRes"`. +2. `version == 0x100`. +3. `entry_count >= 0`. +4. `header.total_size == file_size`. +5. Каталог находится в конце файла. +6. Для каждой записи диапазон данных не пересекает каталог. +7. Имя корректно C-терминировано и не длиннее 35 байт. -| Смещение | Размер | Тип | Значение | Описание | -| -------- | ------ | ------- | ----------------- | --------------------------------------------- | -| 0 | 2 | char[2] | `NL` (0x4C4E) | Магическая сигнатура | -| 2 | 1 | uint8 | `0x00` | Зарезервировано (должно быть 0) | -| 3 | 1 | uint8 | `0x01` | Версия формата | -| 4 | 2 | int16 | — | Количество записей (sign-extended при чтении) | -| 6 | 8 | — | — | Зарезервировано / не используется | -| 14 | 2 | uint16 | `0xABBA` или иное | Флаг предсортировки (см. ниже) | -| 16 | 4 | — | — | Зарезервировано | -| 20 | 4 | uint32 | — | **Начальное состояние XOR-шифра** (seed) | -| 24 | 8 | — | — | Зарезервировано | +Минимальный набор проверок при записи: -#### Флаг предсортировки (смещение 14) +1. Все имена <= 35 байт и без внутренних NUL. +2. `sort_index` формирует валидную перестановку `0..N-1`. +3. Все паддинги между payload состоят из нулевых байт. +4. `total_size` равен фактической длине выходного файла. -- Если `*(uint16*)(header + 14) == 0xABBA` — движок **не строит** таблицу индексов в памяти. Значения `entry[i].sort_to_original` используются **как есть** (и для двоичного поиска, и как XOR‑ключ для данных). -- Если значение **отлично от 0xABBA** — после загрузки выполняется **пузырьковая сортировка** имён и строится перестановка `sort_to_original[]`, которая затем **записывается в `entry[i].sort_to_original`**, перетирая значения из файла. Именно эта перестановка далее используется и для поиска, и как XOR‑ключ (младшие 16 бит). +## 10. Эмпирическая проверка на retail-корпусе -### 2.3. XOR-шифр таблицы записей +Валидация на полном наборе `testdata/Parkan - Iron Strategy`: -Таблица записей начинается со смещения 32 и зашифрована поточным XOR-шифром. Ключ инициализируется из DWORD по смещению 20 заголовка. +- найдено `120` архивов `NRes`; +- roundtrip `unpack -> repack -> byte-compare`: `120/120` совпали побайтно; +- критических расхождений формата не обнаружено. -#### Начальное состояние +Инструмент: -``` -seed = *(uint32*)(header + 20) -lo = seed & 0xFF // Младший байт -hi = (seed >> 8) & 0xFF // Второй байт -``` +- `tools/archive_roundtrip_validator.py` -#### Алгоритм дешифровки (побайтовый) +## 11. Статус покрытия и что осталось до 100% -Для каждого зашифрованного байта `encrypted[i]`, начиная с `i = 0`: +Закрыто: -``` -step 1: lo = hi ^ ((lo << 1) & 0xFF) // Сдвиг lo влево на 1, XOR с hi -step 2: decrypted[i] = lo ^ encrypted[i] // Расшифровка байта -step 3: hi = lo ^ ((hi >> 1) & 0xFF) // Сдвиг hi вправо на 1, XOR с lo -``` +- формат заголовка/каталога; +- правила поиска; +- каноническая пересборка; +- строгие инварианты валидатора; +- побайтовый roundtrip на retail-корпусе. -**Пример реализации:** +Осталось до полного 100% архитектурного покрытия движка: -```python -def decrypt_rs_entries(encrypted_data: bytes, seed: int) -> bytes: - lo = seed & 0xFF - hi = (seed >> 8) & 0xFF - result = bytearray(len(encrypted_data)) - for i in range(len(encrypted_data)): - lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF - result[i] = lo ^ encrypted_data[i] - hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF - return bytes(result) -``` - -Этот же алгоритм используется для шифрования данных ресурсов с методом XOR (флаги 0x20, 0x60, 0xA0), но с другим начальным ключом из записи. - -### 2.4. Запись таблицы (32 байта, на диске, до дешифровки) - -После дешифровки каждая запись имеет следующую структуру: - -| Смещение | Размер | Тип | Описание | -| -------- | ------ | -------- | -------------------------------------------------------------- | -| 0 | 12 | char[12] | Имя ресурса (ASCII, обычно uppercase; строка читается до `\0`) | -| 12 | 4 | — | Зарезервировано (движком игнорируется) | -| 16 | 2 | int16 | **Флаги** (метод сжатия и атрибуты) | -| 18 | 2 | int16 | **`sort_to_original[i]` / XOR‑ключ** (см. ниже) | -| 20 | 4 | uint32 | **Размер распакованных данных** (`unpacked_size`) | -| 24 | 4 | uint32 | Смещение данных от начала файла (`data_offset`) | -| 28 | 4 | uint32 | Размер упакованных данных в байтах (`packed_size`) | - -#### Имена ресурсов - -- Поле `name[12]` копируется побайтно. Внутренне движок всегда имеет `\0` сразу после этих 12 байт (зарезервированные 4 байта в памяти принудительно обнуляются), поэтому имя **может быть длиной до 12 символов** даже без `\0` внутри `name[12]`. -- На практике имена обычно **uppercase ASCII**. `rsFind` приводит запрос к верхнему регистру (`_strupr`) и сравнивает побайтно. -- `rsFind` копирует имя запроса `strncpy(..., 16)` и принудительно ставит `\0` в `Destination[15]`, поэтому запрос длиннее 15 символов будет усечён. - -#### Поле `sort_to_original[i]` (смещение 18) - -Это **не “свойство записи”**, а элемент таблицы индексов, по которой `rsFind` делает двоичный поиск: - -- Таблица реализована “внутри записей”: значение берётся как `entry[i].sort_to_original` (где `i` — позиция двоичного поиска), а реальная запись для сравнения берётся как `entry[ sort_to_original[i] ]`. -- Тем же значением (младшие 16 бит) инициализируется XOR‑шифр данных для методов, где он используется (0x20/0x60/0xA0). Поэтому при упаковке/шифровании данных ключ должен совпадать с итоговым `sort_to_original[i]` (см. флаг 0xABBA в разделе 2.2). - -Поиск выполняется **двоичным поиском** по этой таблице, с фолбэком на **линейный поиск** если двоичный не нашёл (поведение `rsFind`). - -### 2.5. Поле флагов (смещение 16 записи) - -Биты поля флагов кодируют метод сжатия и дополнительные атрибуты: - -``` -Биты [8:5] (маска 0x1E0): Метод сжатия/шифрования -Бит [6] (маска 0x040): Флаг realloc (буфер декомпрессии может быть больше) -``` - -#### Методы сжатия (биты 8–5, маска 0x1E0) - -| Значение | Hex | Описание | -| -------- | ----- | --------------------------------------- | -| 0x000 | 0x00 | Без сжатия (копирование) | -| 0x020 | 0x20 | Только XOR-шифр | -| 0x040 | 0x40 | LZSS (простой вариант) | -| 0x060 | 0x60 | XOR-шифр + LZSS (простой вариант) | -| 0x080 | 0x80 | LZSS с адаптивным кодированием Хаффмана | -| 0x0A0 | 0xA0 | XOR-шифр + LZSS с Хаффманом | -| 0x100 | 0x100 | Deflate (аналог zlib/RFC 1951) | - -Примечание: `rsGetPackMethod()` возвращает `flags & 0x1C0` (без бита 0x20). Поэтому: - -- для 0x20 вернётся 0x00, -- для 0x60 вернётся 0x40, -- для 0xA0 вернётся 0x80. - -#### Бит 0x40 (выделение +0x12 и последующее `realloc`) - -Бит 0x40 проверяется отдельно (`flags & 0x40`). Если он установлен, выходной буфер выделяется с запасом `+0x12` (18 байт), а после распаковки вызывается `realloc` для усечения до точного `unpacked_size`. - -Важно: этот же бит входит в код методов 0x40/0x60, поэтому для них поведение “+0x12 и shrink” включено автоматически. - -### 2.6. Размеры данных - -В каждой записи на диске хранятся оба значения: - -- `unpacked_size` (смещение 20) — размер распакованных данных. -- `packed_size` (смещение 28) — размер упакованных данных (байт во входном потоке для выбранного метода). - -Для метода 0x00 (без сжатия) обычно `packed_size == unpacked_size`. - -`rsGetInfo` возвращает именно `unpacked_size` (то, сколько байт выдаст `rsLoad`). - -Практический нюанс для метода `0x100` (Deflate): в реальных игровых данных встречается запись, где `packed_size` указывает на диапазон до `EOF + 1`. Поток успешно декодируется и без последнего байта; это похоже на lookahead-поведение декодера. - -### 2.7. Опциональный трейлер медиа (6 байт) - -При открытии с флагом `a2 & 2`: - -| Смещение от конца | Размер | Тип | Описание | -| ----------------- | ------ | ------- | ----------------------- | -| −6 | 2 | char[2] | Сигнатура `AO` (0x4F41) | -| −4 | 4 | uint32 | Смещение медиа-оверлея | - -Если трейлер присутствует, все смещения данных в записях корректируются: `effective_offset = entry_offset + media_overlay_offset`. - ---- - -## Часть 3. Алгоритмы сжатия (формат RsLi) - -### 3.1. XOR-шифр данных (метод 0x20) - -Алгоритм идентичен XOR‑шифру таблицы записей (раздел 2.3), но начальный ключ берётся из `entry[i].sort_to_original` (смещение 18 записи, младшие 16 бит). - -Важно про размер входа: - -- В ветке **0x20** движок XOR‑ит ровно `unpacked_size` байт (и ожидает, что поток данных имеет ту же длину; на практике `packed_size == unpacked_size`). -- В ветках **0x60/0xA0** XOR применяется к **упакованному** потоку длиной `packed_size` перед декомпрессией. - -#### Инициализация - -``` -key16 = (uint16)entry.sort_to_original // int16 на диске по смещению 18 -lo = key16 & 0xFF -hi = (key16 >> 8) & 0xFF -``` - -#### Дешифровка (псевдокод) - -``` -for i in range(N): # N = unpacked_size (для 0x20) или packed_size (для 0x60/0xA0) - lo = (hi ^ ((lo << 1) & 0xFF)) & 0xFF - out[i] = in[i] ^ lo - hi = (lo ^ ((hi >> 1) & 0xFF)) & 0xFF -``` - -### 3.2. LZSS — простой вариант (метод 0x40) - -Классический алгоритм LZSS (Lempel-Ziv-Storer-Szymanski) с кольцевым буфером. - -#### Параметры - -| Параметр | Значение | -| ----------------------------- | ------------------ | -| Размер кольцевого буфера | 4096 байт (0x1000) | -| Начальная позиция записи | 4078 (0xFEE) | -| Начальное заполнение | 0x20 (пробел) | -| Минимальная длина совпадения | 3 | -| Максимальная длина совпадения | 18 (4 бита + 3) | - -#### Алгоритм декомпрессии - -``` -Инициализация: - ring_buffer[0..4095] = 0x20 (заполнить пробелами) - ring_pos = 4078 - flags_byte = 0 - flags_bits_remaining = 0 - -Цикл (пока не заполнен выходной буфер И не исчерпан входной): - - 1. Если flags_bits_remaining == 0: - - Прочитать 1 байт из входного потока → flags_byte - - flags_bits_remaining = 8 - - Декодировать как: - - Старший бит устанавливается в 0x7F (маркер) - - Оставшиеся 7 бит — флаги текущей группы - - Реально в коде: control_word = (flags_byte) | (0x7F << 8) - Каждый бит проверяется сдвигом вправо. - - 2. Проверить младший бит control_word: - - Если бит = 1 (литерал): - - Прочитать 1 байт из входного потока → byte - - ring_buffer[ring_pos] = byte - - ring_pos = (ring_pos + 1) & 0xFFF - - Записать byte в выходной буфер - - Если бит = 0 (ссылка): - - Прочитать 2 байта: low_byte, high_byte - - offset = low_byte | ((high_byte & 0xF0) << 4) // 12 бит - - length = (high_byte & 0x0F) + 3 // 4 бита + 3 - - Скопировать length байт из ring_buffer[offset...]: - для j от 0 до length-1: - byte = ring_buffer[(offset + j) & 0xFFF] - ring_buffer[ring_pos] = byte - ring_pos = (ring_pos + 1) & 0xFFF - записать byte в выходной буфер - - 3. Сдвинуть control_word вправо на 1 бит - 4. flags_bits_remaining -= 1 -``` - -#### Подробная раскладка пары ссылки (2 байта) - -``` -Байт 0 (low): OOOOOOOO (биты [7:0] смещения) -Байт 1 (high): OOOOLLLL O = биты [11:8] смещения, L = длина − 3 - -offset = low | ((high & 0xF0) << 4) // Диапазон: 0–4095 -length = (high & 0x0F) + 3 // Диапазон: 3–18 -``` - -### 3.3. LZSS с адаптивным кодированием Хаффмана (метод 0x80) - -Расширенный вариант LZSS, где литералы и длины совпадений кодируются с помощью адаптивного дерева Хаффмана. - -#### Параметры - -| Параметр | Значение | -| -------------------------------- | ------------------------------ | -| Размер кольцевого буфера | 4096 байт | -| Начальная позиция записи | **4036** (0xFC4) | -| Начальное заполнение | 0x20 (пробел) | -| Количество листовых узлов дерева | 314 | -| Символы литералов | 0–255 (байты) | -| Символы длин | 256–313 (длина = символ − 253) | -| Начальная длина | 3 (при символе 256) | -| Максимальная длина | 60 (при символе 313) | - -#### Дерево Хаффмана - -Дерево строится как **адаптивное** (dynamic, self-adjusting): - -- **627 узлов**: 314 листовых + 313 внутренних. -- Все листья изначально имеют **вес 1**. -- Корень дерева — узел с индексом 0 (в массиве `parent`). -- После декодирования каждого символа дерево **обновляется** (функция `sub_1001B0AE`): вес узла инкрементируется, и при нарушении порядка узлы **переставляются** для поддержания свойства. -- При достижении суммарного веса **0x8000 (32768)** — все веса **делятся на 2** (с округлением вверх) и дерево полностью перестраивается. - -#### Кодирование позиции - -Позиция в кольцевом буфере кодируется с помощью **d-кода** (таблица дистанций): - -- 8 бит позиции ищутся в таблице `d_code[256]`, определяя базовое значение и количество дополнительных битов. -- Из потока считываются дополнительные биты, которые объединяются с базовым значением. -- Финальная позиция: `pos = (ring_pos − 1 − decoded_position) & 0xFFF` - -**Таблицы инициализации** (d-коды): - -``` -Таблица базовых значений — byte_100371D0[6]: - { 0x01, 0x03, 0x08, 0x0C, 0x18, 0x10 } - -Таблица дополнительных битов — byte_100371D6[6]: - { 0x20, 0x30, 0x40, 0x30, 0x30, 0x10 } -``` - -#### Алгоритм декомпрессии (высокоуровневый) - -``` -Инициализация: - ring_buffer[0..4095] = 0x20 - ring_pos = 4036 - Инициализировать дерево Хаффмана (314 листьев, все веса = 1) - Инициализировать таблицы d-кодов - -Цикл: - 1. Декодировать символ из потока по дереву Хаффмана: - - Начать с корня - - Читать биты, спускаться по дереву (0 = левый, 1 = правый) - - Пока не достигнут лист → символ = лист − 627 - - 2. Обновить дерево Хаффмана для декодированного символа - - 3. Если символ < 256 (литерал): - - ring_buffer[ring_pos] = символ - - ring_pos = (ring_pos + 1) & 0xFFF - - Записать символ в выходной буфер - - 4. Если символ >= 256 (ссылка): - - length = символ − 253 - - Декодировать позицию через d-код: - a) Прочитать 8 бит из потока - b) Найти d-код и дополнительные биты по таблице - c) Прочитать дополнительные биты - d) position = (ring_pos − 1 − full_position) & 0xFFF - - Скопировать length байт из ring_buffer[position...] - - 5. Если выходной буфер заполнен → завершить -``` - -### 3.4. XOR + LZSS (методы 0x60 и 0xA0) - -Комбинированный метод: сначала XOR-дешифровка, затем LZSS-декомпрессия. - -#### Алгоритм - -1. Выделить временный буфер размером `compressed_size` (поле из записи, смещение 28). -2. Дешифровать сжатые данные XOR-шифром (раздел 3.1) с ключом из записи во временный буфер. -3. Применить LZSS-декомпрессию (простую или с Хаффманом, в зависимости от конкретного метода) из временного буфера в выходной. -4. Освободить временный буфер. - -- **0x60** — XOR + простой LZSS (раздел 3.2) -- **0xA0** — XOR + LZSS с Хаффманом (раздел 3.3) - -#### Начальное состояние XOR для данных - -При комбинированном методе seed берётся из поля по смещению 20 записи (4-байтный). Однако ключ обрабатывается как 16-битный: `lo = seed & 0xFF`, `hi = (seed >> 8) & 0xFF`. - -### 3.5. Deflate (метод 0x100) - -Полноценная реализация алгоритма **Deflate** (RFC 1951) с блочной структурой. - -#### Общая структура - -Данные состоят из последовательности блоков. Каждый блок начинается с: - -- **1 бит** — `is_final`: признак последнего блока -- **2 бита** — `block_type`: тип блока - -#### Типы блоков - -| block_type | Описание | Функция | -| ---------- | --------------------------- | ---------------- | -| 0 | Без сжатия (stored) | `sub_1001A750` | -| 1 | Фиксированные коды Хаффмана | `sub_1001A8C0` | -| 2 | Динамические коды Хаффмана | `sub_1001AA30` | -| 3 | Зарезервировано (ошибка) | Возвращает код 2 | - -#### Блок типа 0 (stored) - -1. Отбросить оставшиеся биты до границы байта (выравнивание). -2. Прочитать 16 бит — `LEN` (длина блока). -3. Прочитать 16 бит — `NLEN` (дополнение длины, `NLEN == ~LEN & 0xFFFF`). -4. Проверить: `LEN == (uint16)(~NLEN)`. При несовпадении — ошибка. -5. Скопировать `LEN` байт из входного потока в выходной. - -Декомпрессор использует внутренний буфер размером **32768 байт** (0x8000). При заполнении — промежуточная запись результата. - -#### Блок типа 1 (фиксированные коды) - -Стандартные коды Deflate: - -- Литералы/длины: 288 кодов - - 0–143: 8-битные коды - - 144–255: 9-битные коды - - 256–279: 7-битные коды - - 280–287: 8-битные коды -- Дистанции: 30 кодов, все 5-битные - -Используются предопределённые таблицы длин и дистанций (`unk_100370AC`, `unk_1003712C` и соответствующие экстра-биты). - -#### Блок типа 2 (динамические коды) - -1. Прочитать 5 бит → `HLIT` (количество литералов/длин − 257). Диапазон: 257–286. -2. Прочитать 5 бит → `HDIST` (количество дистанций − 1). Диапазон: 1–30. -3. Прочитать 4 бита → `HCLEN` (количество кодов длин − 4). Диапазон: 4–19. -4. Прочитать `HCLEN` × 3 бит — длины кодов для алфавита длин. -5. Построить дерево Хаффмана для алфавита длин (19 символов). -6. С помощью этого дерева декодировать длины кодов для литералов/длин и дистанций. -7. Построить два дерева Хаффмана: для литералов/длин и для дистанций. -8. Декодировать данные. - -**Порядок кодов длин** (стандартный Deflate): - -``` -{ 16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 } -``` - -Хранится в `dword_10037060`. - -#### Валидации - -- `HLIT + 257 <= 286` (max 0x11E) -- `HDIST + 1 <= 30` (max 0x1E) -- При нарушении — возвращается ошибка 1. - -### 3.6. Метод 0x00 (без сжатия) - -Данные копируются «как есть» напрямую из файла. Вызывается через указатель на функцию `dword_1003A1B8` (фактически `memcpy` или аналог). - ---- - -## Часть 4. Внутренние структуры в памяти - -### 4.1. Внутренняя структура NRes-архива (opened, 0x68 байт = 104) - -```c -struct NResArchive { // Размер: 0x68 (104 байта) - void* vtable; // +0: Указатель на таблицу виртуальных методов - int32_t entry_count; // +4: Количество записей - void* mapped_base; // +8: Базовый адрес mapped view - void* directory_ptr; // +12: Указатель на каталог записей в памяти - char* filename; // +16: Путь к файлу (_strdup) - int32_t ref_count; // +20: Счётчик ссылок - uint32_t last_release_time; // +24: timeGetTime() при последнем Release - // +28..+91: Для raw-режима — встроенная запись (единственный File entry) - NResArchive* next; // +92: Следующий архив в связном списке - uint8_t is_writable; // +100: Файл открыт для записи - uint8_t is_cacheable; // +101: Не выгружать при refcount = 0 -}; -``` - -### 4.2. Внутренняя структура RsLi-архива (56 + 64 × N байт) - -```c -struct RsLibHeader { // 56 байт (14 DWORD) - uint32_t magic; // +0: 'RsLi' (0x694C7352) - int32_t entry_count; // +4: Количество записей - uint32_t media_offset; // +8: Смещение медиа-оверлея - uint32_t reserved_0C; // +12: 0 - HANDLE file_handle_2; // +16: -1 (дополнительный хэндл) - uint32_t reserved_14; // +20: 0 - uint32_t reserved_18; // +24: — - uint32_t reserved_1C; // +28: 0 - HANDLE mapping_handle_2; // +32: -1 - uint32_t reserved_24; // +36: 0 - uint32_t flag_28; // +40: (flags >> 7) & 1 - HANDLE file_handle; // +44: Хэндл файла - HANDLE mapping_handle; // +48: Хэндл файлового маппинга - void* mapped_view; // +52: Указатель на mapped view -}; -// Далее следуют entry_count записей по 64 байта каждая -``` - -#### Внутренняя запись RsLi (64 байта) - -```c -struct RsLibEntry { // 64 байта (16 DWORD) - char name[16]; // +0: Имя (12 из файла + 4 нуля) - int32_t flags; // +16: Флаги (sign-extended из int16) - int32_t sort_index; // +20: sort_to_original[i] (таблица индексов / XOR‑ключ) - uint32_t uncompressed_size; // +24: Размер несжатых данных (из поля 20 записи) - void* data_ptr; // +28: Указатель на данные в mapped view - uint32_t compressed_size; // +32: Размер сжатых данных (из поля 28 записи) - uint32_t reserved_24; // +36: 0 - uint32_t reserved_28; // +40: 0 - uint32_t reserved_2C; // +44: 0 - void* loaded_data; // +48: Указатель на декомпрессированные данные - // +52..+63: дополнительные поля -}; -``` - ---- - -## Часть 5. Экспортируемые API-функции - -### 5.1. NRes API - -| Функция | Описание | -| ------------------------------ | ------------------------------------------------------------------------- | -| `niOpenResFile(path)` | Открыть NRes-архив (только чтение), эквивалент `niOpenResFileEx(path, 0)` | -| `niOpenResFileEx(path, flags)` | Открыть NRes-архив с флагами | -| `niOpenResInMem(ptr, size)` | Открыть NRes-архив из памяти | -| `niCreateResFile(path)` | Создать/открыть NRes-архив для записи | - -### 5.2. RsLi API - -| Функция | Описание | -| ------------------------------- | -------------------------------------------------------- | -| `rsOpenLib(path, flags)` | Открыть RsLi-библиотеку | -| `rsCloseLib(lib)` | Закрыть библиотеку | -| `rsLibNum(lib)` | Получить количество записей | -| `rsFind(lib, name)` | Найти запись по имени (→ индекс или −1) | -| `rsLoad(lib, index)` | Загрузить и декомпрессировать ресурс | -| `rsLoadFast(lib, index, flags)` | Быстрая загрузка (без декомпрессии если возможно) | -| `rsLoadPacked(lib, index)` | Загрузить в «упакованном» виде (отложенная декомпрессия) | -| `rsLoadByName(lib, name)` | `rsFind` + `rsLoad` | -| `rsGetInfo(lib, index, out)` | Получить имя и размер ресурса | -| `rsGetPackMethod(lib, index)` | Получить метод сжатия (`flags & 0x1C0`) | -| `ngiUnpack(packed)` | Декомпрессировать ранее загруженный упакованный ресурс | -| `ngiAlloc(size)` | Выделить память (с обработкой ошибок) | -| `ngiFree(ptr)` | Освободить память | -| `ngiGetMemSize(ptr)` | Получить размер выделенного блока | - ---- - -## Часть 6. Контрольные заметки для реализации - -### 6.1. Кодировки и регистр - -- **NRes**: имена хранятся **как есть** (case-insensitive при поиске через `_strcmpi`). -- **RsLi**: имена хранятся в **верхнем регистре**. Перед поиском запрос приводится к верхнему регистру (`_strupr`). Сравнение — через `strcmp` (case-sensitive для уже uppercase строк). - -### 6.2. Порядок байт - -Все значения хранятся в **little-endian** порядке (платформа x86/Win32). - -### 6.3. Выравнивание - -- **NRes**: данные каждого ресурса выровнены по границе **8 байт** (0-padding между файлами). -- **RsLi**: выравнивание данных не описано в коде (данные идут подряд). - -### 6.4. Размер записей на диске - -- **NRes**: каталог — **64 байта** на запись, расположен в конце файла. -- **RsLi**: таблица — **32 байта** на запись (зашифрованная), расположена в начале файла (сразу после 32-байтного заголовка). - -### 6.5. Кэширование и memory mapping - -Оба формата используют Windows Memory-Mapped Files (`CreateFileMapping` + `MapViewOfFile`). NRes-архивы организованы в глобальный **связный список** (`dword_1003A66C`) со счётчиком ссылок и таймером неактивности (10 секунд = 0x2710 мс). При refcount == 0 и истечении таймера архив автоматически выгружается (если не установлен флаг `is_cacheable`). - -### 6.6. Размер seed XOR - -- **Заголовок RsLi**: seed — **4 байта** (DWORD) по смещению 20, но используются только младшие 2 байта (`lo = byte[0]`, `hi = byte[1]`). -- **Запись RsLi**: sort_to_original[i] — **2 байта** (int16) по смещению 18 записи. -- **Данные при комбинированном XOR+LZSS**: seed — **4 байта** (DWORD) из поля по смещению 20 записи, но опять используются только 2 байта. - -### 6.7. Эмпирическая проверка на данных игры - -- Найдено архивов по сигнатуре: **122** (`NRes`: 120, `RsLi`: 2). -- Выполнен полный roundtrip `unpack -> pack -> byte-compare`: **122/122** архивов совпали побайтно. -- Для `RsLi` в проверенном наборе встретились методы: `0x040` и `0x100`. - -Подтверждённые нюансы: - -- Для LZSS (метод `0x040`) рабочая раскладка нибблов в ссылке: `OOOO LLLL`, а не `LLLL OOOO`. -- Для Deflate (метод `0x100`) возможен случай `packed_size == фактический_конец + 1` на последней записи файла. +1. Формальная семантика `attr1/attr2/attr3` для всех типов ресурсов (частично вынесена в профильные страницы `msh`, `material`, `texture`, `fxid`, `terrain`). +2. Полная спецификация поведения при не-ASCII именах (в реальных игровых архивах используется ASCII-практика; для Unicode-коллации движок не документирован). +3. Полная спецификация платформенных гарантий атомарной записи (формат данных закрыт, но OS-уровневые гарантии замены файла зависят от платформы и файловой системы). diff --git a/docs/specs/render-parity.md b/docs/specs/render-parity.md index 5c63c13..8955414 100644 --- a/docs/specs/render-parity.md +++ b/docs/specs/render-parity.md @@ -75,3 +75,16 @@ CI запускает `render-parity` на каждом push/PR: Важно: оригинальный движок в CI обычно не запускается. Эталонные PNG снимаются офлайн и версионируются в репозитории. + +## Статус покрытия и что осталось до 100% + +Закрыто: + +1. Определена метрика сравнения кадров (`mean_abs`, `max_abs`, `changed_ratio`). +2. Описан единый manifest-формат кейсов и CI-процедура. + +Осталось: + +1. Снять и зафиксировать расширенный эталонный набор кадров оригинала (10-20+ ключевых моделей и режимов). +2. Зафиксировать пороговые критерии pass/fail по каждому классу сцен (статик, анимация, FX, lightmap). +3. Добавить автоматическую публикацию diff-артефактов и регрессионных отчетов в CI. diff --git a/docs/specs/render.md b/docs/specs/render.md index ea63197..06feaef 100644 --- a/docs/specs/render.md +++ b/docs/specs/render.md @@ -151,5 +151,19 @@ void RenderFrame(Scene* scene, Camera* cam, float dt) { ## 10. Статус валидации -- Порядок кадра и подключение `Material.lib / Textures.lib / LightMap.lib` подтверждены текущим runtime-кодом приложения и импортами движковых DLL. +- Порядок кадра и подключение `Material.lib / Textures.lib / LightMap.lib` подтверждены текущей runtime-валидацией проекта. - Детальные инварианты форматов зафиксированы в `tools/msh_doc_validator.py` и `tools/fxid_abs100_audit.py`. + +## 11. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Высокоуровневый кадр: simulation -> animation -> culling -> material/texture resolve -> mesh draw -> fx -> ui -> present. +2. Связка MSH/MAT0/WEAR/Texm/FXID в едином runtime-процессе. +3. Форматная валидация входных данных на полном retail-корпусе. + +Осталось: + +1. Полный pixel-parity контур с эталонными кадрами оригинального рендера по набору моделей/сцен. +2. Формализация всех render-state деталей (точные blend/depth/cull/state transitions) для гарантии 1:1 в каждом draw-pass. +3. Полный coverage-пакет по динамическим веткам (FX-heavy кадры, сложные material-режимы, lightmap-комбинации). diff --git a/docs/specs/rsli.md b/docs/specs/rsli.md new file mode 100644 index 0000000..298cf2a --- /dev/null +++ b/docs/specs/rsli.md @@ -0,0 +1,230 @@ +# RsLi + +`RsLi` — библиотечный контейнер ресурсов движка Parkan: Iron Strategy с зашифрованной таблицей записей и несколькими методами упаковки данных. + +Страница описывает формат и runtime-контракт в высокоуровневом виде, без ссылок на внутренние адреса/функции дизассемблера. + +Связанная страница: + +- [NRes](nres.md) + +## 1. Общая структура файла + +```text +[Header: 32] +[Entry table: entry_count * 32, XOR-encrypted] +[Packed payloads] +[Optional trailer: "AO" + overlay:u32] +``` + +В отличие от `NRes`, таблица записей у `RsLi` расположена в начале файла. + +## 2. Заголовок (32 байта) + +Все значения little-endian. + +| Offset | Size | Type | Поле | +|---:|---:|---|---| +| 0 | 2 | char[2] | `NL` (магия) | +| 2 | 1 | u8 | зарезервировано, в retail = `0` | +| 3 | 1 | u8 | версия, в retail = `1` | +| 4 | 2 | i16 | `entry_count` (должен быть `>= 0`) | +| 14 | 2 | u16 | `presorted_flag` (`0xABBA` = таблица сортировки уже задана) | +| 20 | 4 | u32 | `xor_seed` | + +Остальные байты заголовка считаются служебными и должны сохраняться без нормализации. + +## 3. Таблица записей (после дешифровки) + +Таблица начинается с `offset = 32`, размер `entry_count * 32`. + +Каждая запись (32 байта): + +| Offset | Size | Type | Поле | +|---:|---:|---|---| +| 0 | 12 | char[12] | `name_raw` (обычно uppercase ASCII, NUL optional) | +| 12 | 4 | bytes | служебный хвост, сохранять как есть | +| 16 | 2 | i16 | `flags` | +| 18 | 2 | i16 | `sort_to_original` | +| 20 | 4 | u32 | `unpacked_size` | +| 24 | 4 | u32 | `data_offset_raw` | +| 28 | 4 | u32 | `packed_size` | + +### 3.1. Метод упаковки + +`method = flags & 0x1E0` + +Поддерживаемые значения: + +| Маска | Метод | +|---:|---| +| `0x000` | без сжатия | +| `0x020` | XOR only | +| `0x040` | LZSS | +| `0x060` | XOR + LZSS | +| `0x080` | LZSS + адаптивный Huffman | +| `0x0A0` | XOR + LZSS + адаптивный Huffman | +| `0x100` | raw Deflate (RFC1951) | + +Другие значения считаются неподдерживаемыми. + +## 4. XOR-дешифрование таблицы и данных + +Для таблицы и XOR-методов payload используется один и тот же потоковый XOR-алгоритм. + +Ключ: + +- `key16 = xor_seed & 0xFFFF` (используются только младшие 16 бит seed). + +Состояние: + +```text +lo = key16 & 0xFF +hi = key16 >> 8 +``` + +Для каждого байта: + +```text +lo = hi XOR ((lo << 1) mod 256) +out = in XOR lo +hi = lo XOR (hi >> 1) +``` + +## 5. `sort_to_original` и поиск по имени + +### 5.1. Режим `presorted_flag == 0xABBA` + +`sort_to_original` обязан быть перестановкой `0..entry_count-1` без дубликатов. + +### 5.2. Режим без presorted-флага + +Слой загрузки строит `sort_to_original` самостоятельно: + +- сортирует индексы по `strcmp`-порядку имен (байтовое сравнение); +- записывает эту перестановку в lookup-таблицу. + +### 5.3. Поиск + +Поиск выполняется бинарным поиском по lookup-таблице: + +1. запрос переводится в uppercase ASCII; +2. на шаге бинарного поиска используется индекс `sort_to_original[mid]`; +3. сравнение имен — bytewise (`strcmp`-логика). + +Fail-safe: + +- при невалидном индексе lookup-таблицы выполняется линейный fallback. + +## 6. AO-трейлер и media overlay + +Опциональный трейлер в конце файла: + +```text +"AO" + overlay:u32 +``` + +Если трейлер присутствует: + +- эффективный offset payload: `effective_offset = data_offset_raw + overlay`. + +Ограничение: + +- `overlay <= file_size`. + +## 7. Декодирование payload по методам + +## 7.1. Без сжатия (`0x000`) + +Берутся первые `unpacked_size` байт из packed-диапазона. + +## 7.2. XOR only (`0x020`) + +XOR-дешифрование первых `unpacked_size` байт. + +## 7.3. LZSS (`0x040`, `0x060`) + +Параметры: + +- ring buffer: `4096` байт; +- начальное заполнение ring: `0x20`; +- стартовый указатель ring: `0xFEE`; +- control-биты читаются LSB-first. + +Правила: + +- `bit=1`: literal byte; +- `bit=0`: ссылка из 2 байт + `offset = low | ((high & 0xF0) << 4)` + `length = (high & 0x0F) + 3`. + +Для `0x060` XOR применяется на лету к packed-потоку до LZSS-декодирования. + +## 7.4. LZSS + адаптивный Huffman (`0x080`, `0x0A0`) + +Параметры: + +- `N=4096`, `F=60`, `THRESHOLD=2`; +- адаптивное дерево Huffman обновляется по мере декодирования. + +Для `0x0A0` XOR применяется на лету к битовому потоку до Huffman/LZSS-декодирования. + +## 7.5. Deflate (`0x100`) + +Используется raw Deflate-поток (RFC1951). + +Важно: + +- zlib-обертка (`RFC1950`) не принимается. + +## 8. Quirk: Deflate EOF+1 + +На retail-корпусе встречается один подтвержденный случай, где: + +- `effective_offset + packed_size == file_size + 1`. + +Совместимое поведение: + +- для метода `0x100` допустить чтение `packed_size - 1` байт (если включен режим совместимости); +- в строгом режиме считать это ошибкой. + +## 9. Контрольные инварианты + +Минимальные проверки: + +1. `magic == "NL"`, `reserved == 0`, `version == 1`. +2. `entry_count >= 0`. +3. `table_end <= file_size`. +4. Если `presorted_flag == 0xABBA`, `sort_to_original` — валидная перестановка. +5. `effective_offset + packed_size` не выходит за EOF (кроме разрешенного deflate EOF+1 quirk). +6. Итоговый распакованный размер равен `unpacked_size`. + +## 10. Эмпирическая проверка на retail-корпусе + +Проверка на полном наборе `testdata/Parkan - Iron Strategy`: + +- обнаружено `2` архива `RsLi`; +- roundtrip `unpack -> repack -> byte-compare`: `2/2` совпали побайтно; +- подтвержден ровно один `deflate EOF+1` случай (`sprites.lib`, entry `23`). + +Инструменты: + +- `tools/archive_roundtrip_validator.py` +- `crates/rsli` tests + +## 11. Статус покрытия и что осталось до 100% + +Закрыто: + +- формат заголовка/таблицы; +- XOR-алгоритм; +- все используемые методы декодирования; +- AO overlay; +- lookup-поиск и fallback; +- retail-валидация и побайтовый roundtrip. + +Осталось до полного 100% архитектурного покрытия движка: + +1. Полная функциональная семантика битов `flags` вне маски метода (`0x1E0`) для геймплейных подсистем. +2. Канонический writer для авторинга новых архивов со стабильной стратегией выбора методов (`0x080/0x0A0/0x100`) и параметров компрессии. +3. Формализация поведения для не-ASCII имен (на практике архивы используют ASCII-диапазон). diff --git a/docs/specs/runtime-pipeline.md b/docs/specs/runtime-pipeline.md index 329afc1..fb8af06 100644 --- a/docs/specs/runtime-pipeline.md +++ b/docs/specs/runtime-pipeline.md @@ -6,3 +6,13 @@ Эта страница оставлена как совместимый указатель для старых ссылок. +## Статус покрытия и что осталось до 100% + +Закрыто: + +1. Актуальный runtime-пайплайн централизован в `render.md`. + +Осталось: + +1. Поддерживать обратную совместимость ссылок при дальнейшей декомпозиции render-документа. + diff --git a/docs/specs/sound.md b/docs/specs/sound.md index da2a6ee..360f590 100644 --- a/docs/specs/sound.md +++ b/docs/specs/sound.md @@ -1,5 +1,32 @@ # Sound system -Документ описывает аудиоподсистему: форматы звуковых ресурсов, воспроизведение эффектов и голосов, а также интеграцию со звуковым API. +`Sound` — подсистема аудио: -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга звуковых модулей движка. +- загрузка и кеширование звуковых ресурсов; +- воспроизведение SFX/voice/music; +- пространственное позиционирование и микширование. + +## 1. Архитектурная роль + +1. Получает события от gameplay/FX/mission/UI. +2. Резолвит аудиоресурсы через архивные библиотеки. +3. Управляет каналами, приоритетами и жизненным циклом источников звука. + +## 2. Минимальный runtime-контракт + +1. Стабильный выбор источника и fallback при отсутствии ресурса. +2. Детерминированные правила приоритета при переполнении каналов. +3. Согласованная модель пространственного затухания и панорамирования. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- место аудио-подсистемы в общем runtime-контуре. + +Осталось: + +1. Полная спецификация форматов аудио-ресурсов и lookup-таблиц. +2. Полный контракт 2D/3D микширования и лимитов каналов. +3. Правила взаимодействия с FXID-командами, которые инициируют звук. +4. Набор audio parity-тестов (тайминг/громкость/панорама). diff --git a/docs/specs/terrain-map-loading.md b/docs/specs/terrain-map-loading.md index 34f6249..62c1e0a 100644 --- a/docs/specs/terrain-map-loading.md +++ b/docs/specs/terrain-map-loading.md @@ -1,170 +1,111 @@ -# Terrain + map loading +# Terrain + ArealMap -Документ описывает полный runtime-пайплайн загрузки ландшафта и карты (`Terrain.dll` + `ArealMap.dll`) и требования к toolchain для 1:1 совместимости (чтение, конвертация, редактирование, обратная сборка). +Документ описывает подсистему ландшафта и ареалов мира в движке Parkan: Iron Strategy: -Источник реверса: +- `Land.msh` (terrain-геометрия и вспомогательные таблицы); +- `Land.map` (ареалы и навигационные связи); +- `BuildDat.lst` (категории объектных зон). -- `tmp/disassembler1/Terrain.dll.c` -- `tmp/disassembler1/ArealMap.dll.c` -- `tmp/disassembler2/Terrain.dll.asm` -- `tmp/disassembler2/ArealMap.dll.asm` +Описание дано в высокоуровневом переносимом виде, без ссылок на внутренние адреса и имена из дизассемблера. -Связанные спецификации: +Связанные страницы: -- [NRes / RsLi](nres.md) +- [NRes](nres.md) +- [RsLi](rsli.md) - [MSH core](msh-core.md) -- [ArealMap](arealmap.md) +- [Render pipeline](render.md) ---- +## 1. End-to-End загрузка уровня -## 1. Назначение подсистем +Для каждой карты движок загружает пару файлов: -### 1.1. `Terrain.dll` +- `.../Land.msh` +- `.../Land.map` -Отвечает за: +Высокоуровневый порядок: -- загрузку и хранение terrain-геометрии из `*.msh` (NRes); -- фильтрацию и выборку треугольников для коллизий/трассировки/рендера; -- рендер terrain-примитивов и связанного shading; -- использование микро-текстурного канала (chunk type 18). +1. Открыть `Land.msh` как `NRes`. +2. Прочитать обязательные terrain-chunk'и. +3. Построить runtime-структуры terrain (slots, faces, spatial grid). +4. Открыть `Land.map` как `NRes`. +5. Найти единственный chunk `type=12`. +6. Прочитать ареалы, их связи и cell-grid. +7. Применить инициализацию объектных категорий из `BuildDat.lst`. -Характерные runtime-строки: +## 2. Формат `Land.msh` -- `CLandscape::CLandscape()` -- `Unable to find microtexture mapping chunk` -- `Rendering empty primitive!` -- `Rendering empty primitive2!` +`Land.msh` — обычный `NRes` архив с фиксированным набором terrain-ресурсов. -### 1.2. `ArealMap.dll` +## 2.1. Состав chunk'ов -Отвечает за: +Обязательные типы: -- загрузку геометрии ареалов из `*.map` (NRes, chunk type 12); -- построение связей "ареал <-> соседи/подграфы"; -- grid-ускорение по ячейкам карты; -- runtime-доступ к `ISystemArealMap` (интерфейс id `770`) и ареалам (id `771`). +- `1`, `2`, `3`, `4`, `5`, `11`, `18`, `21` -Характерные runtime-строки: +Опциональные типы: -- `SystemArealMap panic: Cannot load ArealMapGeometry` -- `SystemArealMap panic: Cannot find chunk in resource` -- `SystemArealMap panic: ArealMap Cells are empty` -- `SystemArealMap panic: Incorrect ArealMap` +- `14` ---- +Наблюдаемый retail-порядок chunk'ов: -## 2. End-to-End загрузка уровня - -### 2.1. Имена файлов уровня - -В `CLandscape::CLandscape()` базовое имя уровня `levelBase` разворачивается в: - -- `levelBase + ".msh"`: terrain-геометрия; -- `levelBase + ".map"`: геометрия ареалов/навигация; -- `levelBase + "1.wea"` и `levelBase + "2.wea"`: weather/материалы. - -### 2.2. Порядок инициализации (высокоуровнево) - -1. Получение `3DRender` и `3DSound`. -2. Загрузка `MatManager` (`*.wea`), `LightManager`, `CollManager`, `FxManager`. -3. Создание `SystemArealMap` через `CreateSystemArealMap(..., ".map", ...)`. -4. Открытие terrain-библиотеки `niOpenResFile(".msh")`. -5. Загрузка terrain-chunk-ов (см. §3). -6. Построение runtime-границ, grid-ускорителей и рабочих массивов. - -Критичные ошибки на любом шаге приводят к `ngiProcessError`/panic. - ---- - -## 3. Формат terrain `*.msh` (NRes) - -### 3.1. Используемые chunk type в `Terrain.dll` - -Порядок загрузки в `CLandscape::CLandscape()`: - -| Порядок | Type | Обяз. | Использование (подтверждено кодом) | -|---|---:|---|---| -| 1 | 3 | да | поток позиций (`stride = 12`) | -| 2 | 4 | да | поток packed normal (`stride = 4`) | -| 3 | 5 | да | UV-поток (`stride = 4`) | -| 4 | 18 | да | microtexture mapping (`stride = 4`) | -| 5 | 14 | нет | опциональный доп. поток (`stride = 4`, отсутствует на части карт) | -| 6 | 21 | да | таблица terrain-face (по 28 байт) | -| 7 | 2 | да | header + slot-таблицы (используются диапазоны face) | -| 8 | 1 | да | node/grid-таблица (stride 38) | -| 9 | 11 | да | доп. индекс/ускоритель для запросов (cell->list) | - -Ключевые проверки: - -- отсутствие type `18` вызывает `Unable to find microtexture mapping chunk`; -- отсутствие остальных обязательных чанков вызывает `Unable to open file`. - -### 3.2. Node/slot структура для terrain - -Terrain-код использует те же stride и адресацию, что и core-описание: - -- node-запись: `38` байт; -- slot-запись: `68` байт; -- доступ к первому slot-index: `node + 8`; -- tri-диапазон в slot: `slot + 140` (offset 0 внутри slot), `slot + 142` (offset 2). - -Это согласуется с [MSH core](msh-core.md) для `Res1/Res2`: - -- `Res1`: `uint16[19]` на node; -- `Res2`: header + slot table (`0x8C + N * 0x44`). - -### 3.3. Terrain face record (type 21, 28 bytes) - -Подтвержденные поля из runtime-декодирования face: - -```c -struct TerrainFace28 { - uint32_t flags; // +0 - uint8_t materialId; // +4 (читается как byte) - uint8_t auxByte; // +5 - uint16_t unk06; // +6 - uint16_t i0; // +8 (индекс вершины) - uint16_t i1; // +10 - uint16_t i2; // +12 - uint16_t n0; // +14 (сосед, 0xFFFF -> нет) - uint16_t n1; // +16 - uint16_t n2; // +18 - int16_t nx; // +20 packed normal component - int16_t ny; // +22 - int16_t nz; // +24 - uint8_t edgeClass; // +26 (три 2-бит значения) - uint8_t unk27; // +27 -}; +```text +[1, 2, 3, 4, 5, 18, 14, 11, 21] ``` -`edgeClass` декодируется как: +## 2.2. Stride и атрибуты -- `edge0 = byte26 & 0x3` -- `edge1 = (byte26 >> 2) & 0x3` -- `edge2 = (byte26 >> 4) & 0x3` +| Type | Назначение | Stride | +|---:|---|---:| +| 1 | node/slot матрица | 38 | +| 3 | позиции вершин | 12 | +| 4 | нормали (packed) | 4 | +| 5 | UV (packed) | 4 | +| 11 | cell-ускоритель | 4 | +| 14 | доп. поток | 4 | +| 18 | доп. поток | 4 | +| 21 | terrain face | 28 | -### 3.4. Маски флагов face +Общее правило для этих chunk'ов: -Во многих запросах применяется фильтр: +- `attr1 == size / stride` +- `attr3 == stride` -```c -(faceFlags & requiredMask) == requiredMask && -(faceFlags | ~forbiddenMask) == ~forbiddenMask -``` +## 2.3. Type `2`: slot table -Эквивалентно: "все required-биты выставлены, forbidden-биты отсутствуют". +`type=2` содержит: -Подтверждено активное использование битов: +- заголовок `0x8C` байт; +- затем таблицу slots по `68` байт. -- `0x8` (особая обработка в трассировке) -- `0x2000` -- `0x20000` -- `0x100000` -- `0x200000` +Инварианты: -Кроме "полной" 32-бит маски, runtime использует компактные маски в API-запросах. +- `size >= 0x8C` +- `(size - 0x8C) % 68 == 0` +- `attr1 == (size - 0x8C) / 68` +- `attr3 == 68` -Подтверждённый remap `full -> compactMain16` (функции `sub_10013FC0`, `sub_1004BA00`, `sub_1004BB40`): +## 2.4. Type `21`: terrain face (28 байт) + +Высокоуровневая структура face: + +- флаги face; +- индексы треугольника (`i0, i1, i2`); +- индексы соседей (`n0, n1, n2`, значение `0xFFFF` = нет соседа); +- служебные поля (материал/класс/edge-поля и др.). + +Критичные проверки: + +- `i0/i1/i2 < vertex_count` (`type=3`); +- `nX == 0xFFFF` или `nX < face_count`. + +## 2.5. Маски face и compact-представления + +В рантайме используются: + +- полная 32-битная маска (`full`); +- компактные представления (`compactMain16`, `compactMaterial6`). + +Подтвержденный remap `full -> compactMain16`: | Full bit | Compact bit | |---:|---:| @@ -184,7 +125,7 @@ struct TerrainFace28 { | `0x00000040` | `0x2000` | | `0x00200000` | `0x8000` | -Подтверждённый remap `full -> compactMaterial6` (функции `sub_10014090`, `sub_10015540`, `sub_1004BB40`): +Подтвержденный remap `full -> compactMaterial6`: | Full bit | Compact bit | |---:|---:| @@ -195,180 +136,99 @@ struct TerrainFace28 { | `0x00080000` | `0x10` | | `0x00000080` | `0x20` | -Подтверждённый remap `compact -> full` (функция `sub_10015680`): +Для 1:1 реализации нужно поддерживать оба представления и обратное восстановление `compact -> full`. -- `a2[4]`/`a2[5]` (compactMain16 required/forbidden) + `a2[6]`/`a2[7]` (compactMaterial6 required/forbidden) -- разворачиваются в `fullRequired/fullForbidden` в `this[4]/this[5]`. +## 2.6. Type `11` и cell-ускоритель terrain -Для toolchain это означает: +`type=11` служит источником cell-ускорителя для terrain-запросов. -- если редактируется только бинарник `type 21`, достаточно сохранять `flags` как есть; -- если реализуется API-совместимый runtime-слой, нужно поддерживать оба представления (`full` и `compact`) и точный remap выше. +Практические требования для editor/toolchain: -### 3.5. Grid-ускоритель terrain-запросов +- не переупорядочивать содержимое без полного пересчета зависимых таблиц; +- сохранять служебные/неизвестные поля побайтно; +- выполнять валидацию диапазонов face/slot после любых правок. -Runtime строит grid descriptor с параметрами: +## 3. Формат `Land.map` (chunk `type=12`) -- origin (`baseX/baseY`); -- масштабные коэффициенты (`invSizeX/invSizeY`); -- размеры сетки (`cellsX`, `cellsY`). +`Land.map` — `NRes`, содержащий ровно один ресурс `type=12`. -Дальше запросы: +Контракт верхнего уровня: -1. переводят world AABB в диапазон grid-ячеек (`floor(...)`); -2. берут диапазон face через `Res1/Res2` (slot `triStart/triCount`); -3. дополняют кандидаты из cell-списков (chunk type 11); -4. применяют маски флагов; -5. выполняют геометрию (plane/intersection/point-in-triangle). +- `entry.attr1` = `areal_count`; +- payload включает: + - `areal_count` переменных записей ареалов; + - затем grid-секцию cell-попаданий. -### 3.6. Cell-списки по ячейкам (`type 11` и runtime-массивы) +## 3.1. Запись ареала -В `CLandscape` после инициализации используются три параллельных массива по ячейкам (`cellsX * cellsY`): - -- `this+31588` (`sub_100164B0` ctor): массив записей по `12` байт, каждая запись содержит динамический буфер `8`-байтовых элементов; -- `this+31592` (`sub_100164E0` ctor): массив записей по `12` байт, каждая запись содержит динамический буфер `4`-байтовых элементов; -- `this+31596` (`sub_1001F880` ctor): массив записей по `12` байт для runtime-объектов/агентов (буфер `4`-байтовых идентификаторов/указателей). - -Общий header записи списка: +Старт записи: ```c -struct CellListHdr { - void* ptr; // +0 - int count; // +4 - int capacity; // +8 -}; +float anchor_x; // +0 +float anchor_y; // +4 +float anchor_z; // +8 +float reserved_12; // +12 +float area_metric; // +16 +float normal_x; // +20 +float normal_y; // +24 +float normal_z; // +28 +uint32_t logic_flag; // +32 +uint32_t reserved_36; // +36 +uint32_t class_id; // +40 +uint32_t reserved_44; // +44 +uint32_t vertex_count; // +48 +uint32_t poly_count; // +52 ``` -Подтвержденные element-layout: +Далее: -- `this+31588`: элемент `8` байт (`uint32_t id`, `uint32_t aux`), добавление через `sub_10012E20` пишет `aux = 0`; -- `this+31592`: элемент `4` байта (`uint32_t id`); -- `this+31596`: элемент `4` байта (runtime object handle/pointer id). +1. `float3 vertices[vertex_count]` +2. `EdgeLink8 links[vertex_count + 3 * poly_count]`, где + `EdgeLink8 = { int32 area_ref; int32 edge_ref; }` +3. для каждого полигона block: + - `uint32 n` + - `4 * (3*n + 1)` байт данных полигона -Практический вывод для редактора: +## 3.2. Семантика edge-link -- `type 11` должен считаться источником cell-ускорителя; -- неизвестные/дополнительные поля внутри списков должны сохраняться как есть; -- нельзя "нормализовать" или переупорядочивать списки без полного пересчёта всех зависимых runtime-структур. +Для `links[0 .. vertex_count-1]`: ---- +- `(-1, -1)` означает «соседа нет»; +- иначе `area_ref` указывает на индекс соседнего ареала, `edge_ref` — на ребро в соседнем ареале. -## 4. Формат `*.map` (ArealMapGeometry, chunk type 12) +## 3.3. Grid-секция после ареалов -### 4.1. Точка входа - -`CreateSystemArealMap(..., ".map", ...)` вызывает `sub_1001E0D0`: - -1. `niOpenResFile(".map")`; -2. поиск chunk type `12`; -3. чтение chunk-данных; -4. разбор `ArealMapGeometry`. - -При ошибках выдаются panic-строки `SystemArealMap panic: ...`. - -### 4.2. Верхний уровень chunk 12 - -Используются: - -- `entry.attr1` (из каталога NRes) как `areal_count`; -- `entry[+0x0C]` как размер payload chunk для контроля полного разбора. - -Данные chunk: - -1. `areal_count` переменных записей ареалов; -2. секция grid-ячеек (`cellsX/cellsY` + списки попаданий). - -### 4.3. Переменная запись ареала - -Полностью подтверждённые элементы layout: +Формат: ```c -// record = начало записи ареала -float anchor_x = *(float*)(record + 0); -float anchor_y = *(float*)(record + 4); -float anchor_z = *(float*)(record + 8); -float reserved_12 = *(float*)(record + 12); // в retail-данных всегда 0 -float area_metric = *(float*)(record + 16); // предрасчитанная площадь ареала -float normal_x = *(float*)(record + 20); -float normal_y = *(float*)(record + 24); -float normal_z = *(float*)(record + 28); // unit vector (|n| ~= 1) -uint32_t logic_flag = *(uint32_t*)(record + 32); // активно используется в runtime -uint32_t reserved_36 = *(uint32_t*)(record + 36); // в retail-данных всегда 0 -uint32_t class_id = *(uint32_t*)(record + 40); // runtime-class/type id ареала -uint32_t reserved_44 = *(uint32_t*)(record + 44); // в retail-данных всегда 0 -uint32_t vertex_count = *(uint32_t*)(record + 48); -uint32_t poly_count = *(uint32_t*)(record + 52); -float* vertices = (float*)(record + 56); // float3[vertex_count] - -// сразу после vertices: -// EdgeLink8[vertex_count + 3*poly_count] -// где EdgeLink8 = { int32_t area_ref; int32_t edge_ref; } -// первые vertex_count записей используются как per-edge соседство границы ареала. -EdgeLink8* links = (EdgeLink8*)(record + 56 + 12 * vertex_count); - -uint8_t* p = (uint8_t*)(links + (vertex_count + 3 * poly_count)); -for (i=0; i начало следующей записи ареала -``` - -То есть для toolchain: - -- поля `+0/+4/+8`, `+16`, `+20..+28`, `+32`, `+40`, `+48`, `+52` являются runtime-значимыми; -- для `links[0..vertex_count-1]` подтверждена интерпретация как `(area_ref, edge_ref)`: - - `area_ref == -1 && edge_ref == -1` = нет соседа; - - иначе `area_ref` указывает на индекс ареала, `edge_ref` — на индекс ребра в целевом ареале; -- при редактировании безопасно работать через parser+writer этой формулы; -- неизвестные байты внутри записи должны сохраняться без изменений. - -Дополнительно по runtime-поведению: - -- `anchor_x/anchor_y` валидируются на попадание внутрь полигона; при промахе движок делает случайный re-seed позиции (см. §4.5); -- `logic_flag` по смещению `+32` используется как gating-условие в логике `SystemArealMap`. - -### 4.4. Секция grid-ячеек в chunk 12 - -После массива ареалов идёт: - -```c -uint32_t cellsX; -uint32_t cellsY; -for (x in 0..cellsX-1) { - for (y in 0..cellsY-1) { - uint16_t hitCount; - uint16_t areaIds[hitCount]; +uint32 cellsX; +uint32 cellsY; +for (x=0; x> 22`); -- low 22 bits: `startIndex` (1-based индекс в общем `uint16`-пуле areaIds). +- high 10 бит: `hitCount`; +- low 22 бита: `startIndex` (в общем `areaIds` пуле). -Контроль целостности: +## 3.4. Валидация целостности chunk 12 -- после разбора `ptr_end - chunk_begin` должен строго совпасть с `entry[+0x0C]`; -- иначе `SystemArealMap panic: Incorrect ArealMap`. +Обязательные проверки: -### 4.5. Нормализация геометрии при загрузке +- `areal_count > 0`; +- `cellsX > 0 && cellsY > 0`; +- каждый `area_id` из cell-списков `< areal_count`; +- все `area_ref/edge_ref` валидны относительно целевых ареалов; +- полный объем прочитанных байт должен точно совпасть с размером payload. -Если опорная точка ареала не попадает внутрь его полигона: +## 4. `BuildDat.lst` -- до 100 попыток случайного сдвига в радиусе ~30; -- затем до 200 попыток в радиусе ~100. - -Это runtime-correction; для 1:1-офлайн инструментов лучше генерировать валидные данные, чтобы не зависеть от недетерминизма `rand()`. - ---- - -## 5. `BuildDat.lst` и объектные категории ареалов - -`ArealMap.dll` инициализирует 12 категорий и читает `BuildDat.lst`. - -Хардкод-категории (имя -> mask): +Используются 12 объектных категорий ареалов: | Имя | Маска | |---|---:| @@ -385,127 +245,49 @@ Runtime упаковывает метаданные ячейки в `uint32`: | `Tower_Medium` | `0x80100000` | | `Tower_Large` | `0x80200000` | -Файл `BuildDat.lst` парсится секционно; при сбое формата используется panic `BuildDat.lst is corrupted`. +Файл должен парситься строго секционно; поврежденный формат считается ошибкой. ---- +## 5. Требования к reader/writer/editor -## 6. Требования к toolchain (конвертер/ридер/редактор) +1. Сохранять порядок и бинарную форму chunk'ов, если не выполняется осознанная нормализация. +2. Все неизвестные поля хранить и писать побайтно (`preserve-as-is`). +3. После правок пересчитывать только вычислимые поля, не «чистить» opaque-данные. +4. Проверять диапазоны индексов между связанными таблицами (`nodes/slots/faces/vertices/areas/cells`). +5. Для неизмененных ресурсов обеспечивать byte-identical roundtrip. -### 6.1. Общие принципы 1:1 +## 6. Эмпирическая верификация (retail) -1. Никаких "переупорядочиваний по вкусу": сохранять порядок chunk-ов, если не требуется явная нормализация. -2. Все неизвестные поля сохранять побайтно. -3. При roundtrip обеспечивать byte-identical для неизмененных сущностей. -4. Валидации должны повторять runtime-ожидания (размеры, count-формулы, обязательность chunk-ов). +Валидация на `testdata/Parkan - Iron Strategy`: -### 6.2. Для terrain `*.msh` +- карт: `33` +- `Land.msh`: `33/33` валидны +- `Land.map`: `33/33` валидны +- `issues_total = 0`, `errors_total = 0`, `warnings_total = 0` -Обязательные проверки: +Подтвержденные наблюдения: -- наличие chunk types `1,2,3,4,5,11,18,21`; -- type `14` опционален; -- для `type 2`: `size >= 0x8C`, `(size - 0x8C) % 68 == 0`, `attr1 == (size - 0x8C) / 68`; -- `type21_size % 28 == 0`; -- индексы `i0/i1/i2` в `TerrainFace28` не выходят за `vertex_count` (type 3); -- `slot.triStart + slot.triCount` не выходит за `face_count`. +- `Land.msh` порядок chunk'ов стабилен: `[1,2,3,4,5,18,14,11,21]`; +- `Land.map` всегда содержит один chunk `type=12`; +- `cellsX == cellsY == 128` во всех retail-картах; +- `poly_count == 0` во всем проверенном retail-корпусе; +- `normal` имеет длину ~1.0; +- `reserved_12`, `reserved_36`, `reserved_44` в retail наблюдаются как `0`. -Сериализация: - -- `flags`, соседи, `edgeClass`, material байты в `TerrainFace28` сохранять как есть; -- содержимое `type 11`-derived cell-списков (`id`, `aux`) сохранять без "починки"; -- для packed normal не делать "улучшений" нормализации, если цель 1:1. - -### 6.3. Для `*.map` (chunk 12) - -Обязательные проверки: - -- chunk type `12` существует; -- `areal_count > 0`; -- `cellsX > 0 && cellsY > 0`; -- `|normal_x,normal_y,normal_z| ~= 1` для каждого ареала; -- `links[0..vertex_count-1]` валидны (`-1/-1` или корректные `(area_ref, edge_ref)`); -- полный consumed-bytes строго равен `entry[+0x0C]`. - -При редактировании: - -- перестраивать только то, что действительно изменено; -- пересчитывать cell-списки и packed `cellMeta` синхронно; -- сохранять неизвестные части записи ареала без изменений. - -### 6.4. Рекомендуемая архитектура редактора - -1. `Parser`: - - NRes-слой; - - `TerrainMsh`-слой; - - `ArealMapChunk12`-слой. -2. `Model`: - - явные известные поля; - - `raw_unknown` для непросаженных блоков. -3. `Writer`: - - стабильная сериализация; - - проверка контрольных инвариантов перед записью. -4. `Verifier`: - - roundtrip hash/byte-compare; - - runtime-совместимые asserts. - ---- - -## 7. Практический чеклист "движок 1:1" - -Для runtime-совместимого движка нужно реализовать: - -1. NRes API-уровень (`niOpenResFile`, `niOpenResInMem`, поиск chunk по type, получение data/attrs). -2. `CLandscape` пайплайн загрузки `*.msh` + менеджеров + `CreateSystemArealMap`. -3. Terrain face decode (28-byte запись), mask-фильтр, spatial grid queries. -4. Загрузчик `ArealMapGeometry` (chunk 12) с той же валидацией и packed-cell логикой. -5. Пост-обработку ареалов (пересвязка, корректировки опорных точек). -6. Поддержку `BuildDat.lst` для объектных категорий/схем. - ---- - -## 8. Нерасшифрованные зоны (важно для редакторов) - -Ниже поля, которые пока нельзя безопасно "пересобирать по смыслу": - -- семантика `class_id` (`record + 40`) на уровне геймдизайна/скриптов (числовое поле подтверждено, но человекочитаемая таблица соответствий не восстановлена полностью); -- ветки формата для `poly_count > 0` (в retail `tmp/gamedata` это всегда `0`, поэтому поведение этих веток подтверждено только по коду, без живых образцов); -- человекочитаемая семантика части битов `TerrainFace28.flags` (при этом remap и бинарные значения подтверждены); -- семантика поля `aux` во `8`-байтовом элементе cell-списка (`this+31588`, второй `uint32_t`), которое в известных runtime-путях инициализируется нулем. - -Правило до полного реверса: `preserve-as-is`. - ---- - -## 9. Эмпирическая верификация (retail `tmp/gamedata`) - -Для массовой проверки спецификации добавлен валидатор: +Инструмент: - `tools/terrain_map_doc_validator.py` -Запуск: +## 7. Статус покрытия и что осталось до 100% -```bash -python3 tools/terrain_map_doc_validator.py \ - --maps-root tmp/gamedata/DATA/MAPS \ - --report-json tmp/terrain_map_doc_validator.report.json -``` +Закрыто: -Проверенные инварианты (на 33 картах, 2026-02-12): +- бинарный контракт `Land.msh` и `Land.map`; +- диапазонные и структурные инварианты; +- remap масок `full/compact`; +- валидация на полном retail-корпусе карт. -- `Land.msh`: - - порядок chunk-ов всегда `[1,2,3,4,5,18,14,11,21]`; - - `type11` первые dword всегда `[5767168, 4718593]`; - - `type21` индексы вершин/соседей валидны; - - `type2` slot-таблица валидна по формуле `0x8C + 68*N`. -- `Land.map`: - - всегда один chunk `type 12`; - - `cellsX == cellsY == 128` на всех картах; - - `poly_count == 0` для всех `34662` записей ареалов в retail-наборе; - - `record+12`, `record+36`, `record+44` всегда `0`; - - `area_metric` (`record+16`) стабильно коррелирует с площадью XY-полигона (макс. абсолютное отклонение `51.39`, макс. относительное `14.73%`, `18` кейсов > `5%`); - - `normal` в `record+20..28` всегда unit (диапазон длины `0.9999998758..1.0000001194`); - - link-таблицы `EdgeLink8` проходят строгую валидацию ссылочной целостности. +Осталось до полного 100% архитектурного покрытия движка: -Сводный результат текущего набора данных: - -- `issues_total = 0`, `errors_total = 0`, `warnings_total = 0`. +1. Полная доменная семантика `class_id` и `logic_flag` (игровые значения/поведенческие правила). +2. Полная спецификация ветки `poly_count > 0` на живых данных (в retail не встречена). +3. Полная field-level семантика части битов `TerrainFace28.flags` (бинарный контракт и remap закрыты, но не все биты имеют документированные геймплейные имена). diff --git a/docs/specs/texture.md b/docs/specs/texture.md index c25ec56..b43ab1a 100644 --- a/docs/specs/texture.md +++ b/docs/specs/texture.md @@ -136,4 +136,18 @@ struct Rect16 { ## 10. Статус валидации - Инварианты `Texm` реализованы в `tools/msh_doc_validator.py`. -- В текущем окружении нет полного игрового набора текстур в `testdata`, поэтому массовая перепроверка не запускалась. +- На полном retail-корпусе `testdata/Parkan - Iron Strategy` проверено `518/518` текстурных payload (`Texm`) без ошибок. + +## 11. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Заголовок `Texm`, mip-chain layout и `Page` chunk. +2. Базовые decode-пути в RGBA8 для проверок/preview. +3. Корпусная валидация структурных инвариантов. + +Осталось: + +1. Полная формальная спецификация всех редких служебных комбинаций `flags4/flags5/unk6`. +2. Канонический writer для полного набора форматов (`indexed`, `565`, `556`, `4444`, `88`, `888`, `8888`) с проверенным roundtrip-профилем. +3. Pixel-parity тесты «оригинальный рендер vs новый рендер» с учетом mipSkip/atlas-page веток. diff --git a/docs/specs/ui.md b/docs/specs/ui.md index 9d71dfd..bb915cb 100644 --- a/docs/specs/ui.md +++ b/docs/specs/ui.md @@ -1,5 +1,33 @@ # UI system -Документ описывает интерфейсную подсистему: ресурсы UI, шрифты, minimap, layout и обработку пользовательского ввода в интерфейсе. +`UI` — подсистема интерфейса: -> Статус: в работе. Спецификация будет дополняться по мере реверс-инжиниринга UI-компонентов движка. +- экранные панели и HUD; +- меню; +- шрифты; +- minimap и служебные оверлеи. + +## 1. Архитектурная роль + +1. Работает поверх render-пайплайна как отдельный этап кадра. +2. Использует UI-ресурсы из архивных библиотек. +3. Перехватывает пользовательский ввод по правилам фокуса. + +## 2. Минимальный runtime-контракт + +1. Детерминированный порядок draw-проходов UI. +2. Консистентный фокус и приоритет ввода (UI vs world). +3. Стабильная загрузка font/minimap/ui-ресурсов по именам. + +## 3. Статус покрытия и что осталось до 100% + +Закрыто: + +- позиция UI-слоя в общем кадре и его связи с render/input. + +Осталось: + +1. Полная спецификация форматов UI layout и контролов. +2. Полный контракт ресурсов шрифтов и text-rendering поведения. +3. Формат minimap-данных и правила трансформации координат. +4. UI parity-тесты (скриншотные и событийные). diff --git a/docs/specs/wear.md b/docs/specs/wear.md index 61c799d..e969f9c 100644 --- a/docs/specs/wear.md +++ b/docs/specs/wear.md @@ -79,4 +79,18 @@ handle = (tableIndex << 16) | wearIndex ## 8. Статус валидации - Поведение `WEAR` согласовано с текущей спецификацией материалов/текстур и runtime-пайплайном. -- Массовый прогон по полному игровому набору в этом окружении не выполнялся из-за отсутствия корпуса данных в `testdata`. +- Корпусные проверки связки `WEAR -> MAT0 -> Texm` включены в текущий валидаторный контур проекта. + +## 9. Статус покрытия и что осталось до 100% + +Закрыто: + +1. Текстовый формат `WEAR`, включая блок `LIGHTMAPS`. +2. Handle-кодирование material slot и fallback-резолв. +3. Правила совместимого writer/editor path. + +Осталось: + +1. Полная спецификация edge-case форматов строк (кодировки, редкие разделители, возможные legacy-варианты). +2. Формализация всех ограничений менеджера wear-таблиц в runtime (лимиты и политики вытеснения). +3. Интеграционные parity-тесты на полном цикле «модель -> wear -> material -> texture/lightmap».