docs: rewrite MkDocs documentation
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# Глоссарий
|
||||
|
||||
Глоссарий объясняет термины в том смысле, в котором они используются в этой
|
||||
книге. Короткое определение не заменяет профильную главу: практический контракт
|
||||
понятия раскрывается в соответствующем томе или справочной странице.
|
||||
|
||||
## Бинарные файлы и ABI
|
||||
|
||||
**PE (Portable Executable)** -- формат исполняемых файлов Windows: EXE и DLL.
|
||||
Он содержит заголовки, секции, таблицы импортов и экспортов, relocations и
|
||||
адрес точки входа.
|
||||
|
||||
**Image base** -- предпочтительный адрес начала загруженного PE-образа.
|
||||
**VA** -- виртуальный адрес в процессе. **RVA** -- адрес относительно image
|
||||
base.
|
||||
|
||||
**Import** -- внешняя функция или переменная, которую модуль получает из другой
|
||||
DLL. **Export** -- символ, предоставляемый другим модулям. Имя, ordinal и
|
||||
calling convention вместе образуют часть binary contract.
|
||||
|
||||
**ABI** -- соглашение о двоичном взаимодействии: размещение аргументов, возврат
|
||||
значений, очистка stack, layout структур, порядок virtual methods и правила
|
||||
владения.
|
||||
|
||||
**Calling convention** -- часть ABI, определяющая передачу аргументов и очистку
|
||||
stack. Для исследованного 32-bit code важны `__cdecl`, `__stdcall` и
|
||||
`__thiscall`.
|
||||
|
||||
**Vtable** -- массив указателей на virtual methods C++-объекта. Запись
|
||||
`vtable +0x34` означает вызов указателя по байтовому смещению `0x34` от начала
|
||||
таблицы.
|
||||
|
||||
**Static analysis** исследует файл без исполнения: disassembly, strings,
|
||||
imports, call graph и data flow. **Dynamic analysis** наблюдает работающую
|
||||
программу: breakpoints, traces, API hooks, memory state и packet/frame captures.
|
||||
|
||||
**Evidence** -- повторяемое наблюдение. **Inference** -- вывод, объединяющий
|
||||
несколько наблюдений. **Hypothesis** -- рабочее предположение, ещё не
|
||||
подтверждённое достаточным экспериментом.
|
||||
|
||||
## Форматы данных
|
||||
|
||||
**Archive** -- контейнер, объединяющий множество ресурсов. **Entry** -- запись
|
||||
его каталога. **Payload** -- полезные bytes конкретной записи.
|
||||
|
||||
**Magic** -- короткая сигнатура формата, например `NRes` или `Texm`.
|
||||
**Version** -- номер варианта layout. Проверка одной magic без проверки version
|
||||
и размеров недостаточна.
|
||||
|
||||
**Offset** -- положение данных относительно начала файла или структуры.
|
||||
**Size** -- число bytes. **Stride** -- размер одного элемента массива.
|
||||
**Alignment** -- требование начинать данные на offset, кратном заданному числу.
|
||||
|
||||
**Little-endian** -- порядок, в котором младший byte многобайтного числа
|
||||
расположен первым. Основные числовые поля форматов Iron3D используют этот
|
||||
порядок.
|
||||
|
||||
**Fixed-size string** -- поле заранее известной длины. Полезная строка
|
||||
заканчивается первым NUL, но оставшиеся bytes могут содержать служебный хвост и
|
||||
должны сохраняться.
|
||||
|
||||
**Opaque field** -- поле с доказанными offset и size, но не установленным
|
||||
предметным смыслом. Его безопасно читать и копировать, но нельзя очищать или
|
||||
переосмысливать без эксперимента.
|
||||
|
||||
**Invariant** -- условие, которое обязано выполняться: range лежит внутри
|
||||
payload, индекс указывает на существующий элемент, count соответствует размеру
|
||||
секции.
|
||||
|
||||
**Strict reader** отклоняет любое нарушение контракта. **Compatibility reader**
|
||||
дополнительно воспроизводит только известные особенности оригинала.
|
||||
|
||||
**Fallback** -- явно предписанный запасной путь, например material `DEFAULT`,
|
||||
затем entry 0. **Heuristic** -- догадка по похожим данным; она не должна
|
||||
незаметно заменять доказанный fallback.
|
||||
|
||||
**Roundtrip** -- последовательность decode -> encode. **Byte-identical
|
||||
roundtrip** создаёт файл, полностью совпадающий с исходным. **Lossless editor**
|
||||
может изменить известное поле, сохранив все остальные bytes и порядок записей.
|
||||
|
||||
## Ресурсы
|
||||
|
||||
**NRes** -- основной контейнер ресурсов с каталогом в конце файла.
|
||||
|
||||
**RsLi** -- библиотечный архив с каталогом в начале файла и несколькими методами
|
||||
упаковки payload.
|
||||
|
||||
**TMA** -- mission data: paths, clans, placed objects, properties, land path и
|
||||
extras.
|
||||
|
||||
**MSH** -- модель Iron3D, представленная как NRes с entries для geometry,
|
||||
nodes, slots, batches, animation и auxiliary streams.
|
||||
|
||||
**WEAR** -- таблица внешнего вида модели, переводящая material index в MAT0
|
||||
name и lightmap slots.
|
||||
|
||||
**MAT0** -- материал: phases, parameters, animation blocks и texture references.
|
||||
|
||||
**Texm** -- texture payload с header, palette, mip chain и optional Page atlas.
|
||||
|
||||
**FXID** -- ресурс эффектов: команды, references, lifetime, random/time modes и
|
||||
runtime instances.
|
||||
|
||||
## Игровой runtime
|
||||
|
||||
**Engine** -- программная среда, которая загружает данные, ведёт время,
|
||||
исполняет мир и формирует изображение/звук. **Game** -- правила, миссии и
|
||||
content поверх engine services.
|
||||
|
||||
**World** -- долгоживущее состояние миссии: objects, terrain, время, кланы и
|
||||
managers. **Scene** -- представление части мира для конкретной обработки,
|
||||
обычно текущей камеры.
|
||||
|
||||
**Game object** -- сущность с идентичностью, transform, properties и lifecycle.
|
||||
**Component/controller** -- специализированная часть поведения: animation,
|
||||
physics, AI или rendering representation.
|
||||
|
||||
**Simulation** отвечает за изменение мира. **Tick** -- один расчётный шаг.
|
||||
**Frame** -- одно подготовленное изображение. Число ticks и frames за единицу
|
||||
времени не обязано совпадать.
|
||||
|
||||
**Event/message** -- типизированное сообщение между objects или subsystems.
|
||||
**Queue traversal** -- стабильный обход зарегистрированных объектов.
|
||||
**Deferred deletion** -- перенос фактического удаления до безопасной границы.
|
||||
|
||||
**Snapshot** -- согласованное состояние, которое renderer читает без изменения
|
||||
simulation. **Determinism** -- одинаковый результат при одинаковом initial
|
||||
state, input, времени и порядке событий.
|
||||
|
||||
**Authority** -- subsystem или network peer, которому разрешено окончательно
|
||||
менять состояние объекта. **Mirror object** -- локальное представление объекта,
|
||||
authority которого находится у другого player.
|
||||
|
||||
## Геометрия и рендеринг
|
||||
|
||||
**Vertex** -- вершина geometry. **Index** -- номер вершины. **Triangle** --
|
||||
примитив из трёх индексов.
|
||||
|
||||
**Node** -- элемент hierarchy модели со своим local transform. **Slot** в MSH
|
||||
-- выбранная геометрическая группа для комбинации node, LOD и group. **Batch**
|
||||
-- непрерывный индексный диапазон с material slot и render state.
|
||||
|
||||
**Transform** переводит данные между coordinate spaces. **Matrix** задаёт
|
||||
линейное преобразование и translation. Порядок умножения matrices является
|
||||
частью контракта.
|
||||
|
||||
**Bounds** -- упрощённый объём для быстрых тестов. **AABB** -- min/max по осям.
|
||||
**Bounding sphere** -- center и radius.
|
||||
|
||||
**Renderer** преобразует подготовленную сцену в изображение. **Backend** --
|
||||
реализация поверх конкретного API или устройства.
|
||||
|
||||
**Draw call** -- команда нарисовать диапазон primitives. **Indexed draw**
|
||||
использует index buffer и base vertex.
|
||||
|
||||
**Material phase** -- одно временное состояние анимированного материала.
|
||||
**Texture** -- двумерный массив texels. **Mip chain** -- последовательность
|
||||
уменьшенных уровней texture. **Atlas** -- texture с несколькими под-
|
||||
изображениями.
|
||||
|
||||
**Fixed-function pipeline** -- старый graphics pipeline, где приложение
|
||||
выбирает predefined transform, lighting, texture-stage и blend states вместо
|
||||
пользовательских shaders.
|
||||
|
||||
**Depth test**, **culling**, **alpha test** и **blending** -- render states,
|
||||
которые влияют на порядок и видимость fragments.
|
||||
|
||||
**Pixel parity** -- совпадение конечного изображения при фиксированных camera,
|
||||
time, seed, resolution и device profile.
|
||||
|
||||
## Навигация, звук и сеть
|
||||
|
||||
**Areal** -- логическая область карты с границей, class/flags и связями с
|
||||
соседями. **Areal graph** -- граф областей и переходов. **Cell grid** --
|
||||
пространственный индекс для быстрых candidate queries.
|
||||
|
||||
**Pathfinding** -- поиск маршрута по graph. **Corridor** -- локальная полоса,
|
||||
построенная из последовательности areals. **Local steering** корректирует
|
||||
ближайший шаг внутри corridor.
|
||||
|
||||
**Collision proxy** -- упрощённое представление объекта для столкновений.
|
||||
**Broad phase** быстро находит потенциальные пары. **Narrow phase** выполняет
|
||||
точную проверку и вычисляет contact.
|
||||
|
||||
**Sample** -- декодированные звуковые данные. **Source** -- конкретный
|
||||
экземпляр воспроизведения с position, gain, loop state и временем. **Listener**
|
||||
-- положение и ориентация слушателя для 3D spatialization.
|
||||
|
||||
**Transport** -- механизм доставки bytes между peers. **Protocol** -- framing,
|
||||
message types, порядок и правила подтверждения. **Wire compatibility** --
|
||||
способность обмениваться данными с оригинальным клиентом.
|
||||
|
||||
**Serialization** -- преобразование typed state в byte sequence. **Framing** --
|
||||
способ отделить одно сообщение от следующего. **Reliable delivery** гарантирует
|
||||
доставку/порядок в пределах выбранной модели; **unreliable delivery** допускает
|
||||
потери ради задержки.
|
||||
|
||||
**Player ID** транспорта и **game player number** -- разные идентичности.
|
||||
**Ownership transfer** меняет authority объекта. **Replication** передаёт
|
||||
состояние или события remote mirrors.
|
||||
@@ -0,0 +1,120 @@
|
||||
# Границы знания
|
||||
|
||||
Этот раздел перечисляет области, где контракт ещё не закрыт полностью. Они не
|
||||
мешают безопасному чтению и lossless сохранению, но не должны превращаться в
|
||||
authoring API без динамического подтверждения.
|
||||
|
||||
## Render state
|
||||
|
||||
Доказаны frame boundaries, world traversal, material resolve и крупные проходы.
|
||||
Не доказаны символами точные имена renderer vtable slots, полный набор CShade
|
||||
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
|
||||
|
||||
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
|
||||
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
|
||||
log на минимальных сценах с одним типом материала.
|
||||
|
||||
## FXID field-level semantics
|
||||
|
||||
Размеры команд, resource references, lifecycle, flags families и используемые
|
||||
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
|
||||
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
|
||||
time modes.
|
||||
|
||||
Закрывающий эксперимент: изменять по одному полю копии эффекта, воспроизводить
|
||||
его в контролируемой сцене и логировать runtime command object, emitted
|
||||
primitives, sound events и reads в `Effect.dll`.
|
||||
|
||||
## Script VM
|
||||
|
||||
Доступны packages, symbols, event sections, variable declarations и version
|
||||
checks. Полная instruction grammar `.scr`, semantics opcodes и serialization
|
||||
state ещё не восстановлены.
|
||||
|
||||
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
|
||||
table с instruction sizes, построить disassembler и сравнить выполнение
|
||||
коротких scripts с оригиналом.
|
||||
|
||||
## Saves and campaign state
|
||||
|
||||
Найдены `saveslots.cfg` и `missions/dispatcher.ini`, но binary savegame payload,
|
||||
serialization World3D/AI/script/RNG и migration rules не закрыты.
|
||||
|
||||
Нужны сохранения оригинала в контролируемых состояниях: старт миссии, изменение
|
||||
позиции, здоровья, order/path, FX/timer, script variable, research/economy,
|
||||
mission completion, pause и non-default game time.
|
||||
|
||||
## Physical/control formats
|
||||
|
||||
CTLD и связанные resources структурно читаются, count patterns и variants
|
||||
известны. Не названы все секции, shape types, coefficients и точный contact
|
||||
solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags.
|
||||
|
||||
Закрывающий эксперимент: трассировать `LoadControlSystem`,
|
||||
`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать
|
||||
каждый изменяемый field с созданным shape, contact или реакцией на движение.
|
||||
|
||||
## DirectPlay wire
|
||||
|
||||
DirectPlay lifecycle и имена игровых messages известны. Wire framing, payload
|
||||
schema, reliability flags и `netZipData` требуют записи обмена двух
|
||||
оригинальных клиентов.
|
||||
|
||||
Native interoperability подтверждается только успешным обменом original client
|
||||
<-> compatibility implementation в обе стороны.
|
||||
|
||||
## Shell, HUD, шрифты и локализация
|
||||
|
||||
Граница shell подтверждена exports `createShell/getIShell`, `IGUIServer`,
|
||||
верхнеуровневым UI-pass и файлами `ui/*.cfg`, `DATA/TextRes.cfg`,
|
||||
`gamefont.rlb` и `sprites.lib`. RsLi framing библиотек закрыт, но widget tree,
|
||||
layout rules, glyph metrics, sprite command semantics, focus/navigation и HUD
|
||||
state machine пока не восстановлены до field-level спецификации.
|
||||
|
||||
Закрывающий эксперимент: трассировать загрузку `shell_ctrls.cfg`,
|
||||
`menu_resources.cfg`, `cursor.cfg`, `game_resources.cfg` и `hq.cfg`, сопоставить
|
||||
GUI object factories и снять command/event captures для меню, HUD, briefing и
|
||||
диалогов.
|
||||
|
||||
## Research, economy and properties
|
||||
|
||||
Экспорты `LoadResearch`, `CalcFullResearchCost`, TRF/preload resources и TMA
|
||||
properties доказывают отдельный слой исследований, стоимости, добычи и
|
||||
производственных параметров. Формулы стоимости, dependency graph технологий,
|
||||
inventory/economy transitions и точная типизация всех 16-byte property values
|
||||
не закрыты.
|
||||
|
||||
Закрывающий эксперимент: сопоставить research functions с ресурсами и UI,
|
||||
снять изменения state на контролируемых покупках/исследованиях и построить
|
||||
typed schema свойств по consumers, а не по одному имени.
|
||||
|
||||
## Rare branches
|
||||
|
||||
- `Land.map poly_count > 0`;
|
||||
- RsLi adaptive methods `0x080` и `0x0A0`;
|
||||
- Texm formats 556 и 88;
|
||||
- FX opcode 6;
|
||||
- редкие material flags и MSH auxiliary streams.
|
||||
|
||||
Такие ветки реализуются по бинарному коду и synthetic tests, а статус
|
||||
corpus-verified получают только после реального файла или runtime trace.
|
||||
|
||||
## Dynamic-stage requirements
|
||||
|
||||
Оставшиеся вопросы нельзя закрыть только статическими архивами. Нужна
|
||||
изолированная 32-bit Windows-среда, неизменённые игровые каталоги, manifest
|
||||
SHA-256, debugger, API/vtable hooks, controlled clocks/input и автоматический
|
||||
launcher, который восстанавливает snapshot, запускает один test case, собирает
|
||||
логи и завершает процесс без ручного вмешательства.
|
||||
|
||||
Для каждого capture сохраняются build profile, module hashes, mission/resource
|
||||
key, configuration, device profile, initial state, input/time script и версии
|
||||
инструментов.
|
||||
|
||||
## Closure criteria
|
||||
|
||||
Вопрос считается закрытым только при наличии build fingerprint, raw trace,
|
||||
parser trace-а, минимального воспроизводимого input/resource/save/message,
|
||||
формального контракта или явно ограниченной гипотезы, differential test для
|
||||
изменённых DLL, обновления тематической главы и regression case, запускаемого
|
||||
без ручного анализа.
|
||||
+46
-12
@@ -1,17 +1,51 @@
|
||||
# Welcome to MkDocs
|
||||
# FParkan
|
||||
|
||||
For full documentation visit [mkdocs.org](https://www.mkdocs.org).
|
||||
FParkan -- самостоятельная техническая книга о восстановлении игрового движка
|
||||
Iron3D из *Parkan: Iron Strategy*. Она ведёт от запуска оригинальной программы
|
||||
и карты DLL к форматам ресурсов, загрузке миссии, геометрии, материалам,
|
||||
рендеру, поведению, звуку, сети и плану чистой совместимой реализации.
|
||||
|
||||
## Commands
|
||||
Сайт оформлен как онлайн-книга: тома читаются последовательно, а справочник
|
||||
используется как быстрый доступ к форматам, проверочным правилам и границам
|
||||
доказанного знания.
|
||||
|
||||
* `mkdocs new [dir-name]` - Create a new project.
|
||||
* `mkdocs serve` - Start the live-reloading docs server.
|
||||
* `mkdocs build` - Build the documentation site.
|
||||
* `mkdocs -h` - Print help message and exit.
|
||||
## Как читать
|
||||
|
||||
## Project layout
|
||||
Если вы впервые разбираете игровой движок, начните с тома I и II. Там вводится
|
||||
лексика, доказательная политика, модульная архитектура и жизненный цикл кадра.
|
||||
|
||||
mkdocs.yml # The configuration file.
|
||||
docs/
|
||||
index.md # The documentation homepage.
|
||||
... # Other markdown pages, images and other files.
|
||||
Если нужна реализация совместимого движка, читайте тома III--VII линейно:
|
||||
ресурсы, миссии, мир, рендер, интерактивные подсистемы и порядок работ.
|
||||
|
||||
Если вы проверяете выводы, переходите к тому VIII и приложениям. Там собраны
|
||||
уровни уверенности, corpus gates, открытые вопросы и критерии закрытия.
|
||||
|
||||
## Восемь томов
|
||||
|
||||
1. **Путеводитель и методика** -- назначение книги, маршруты чтения, язык
|
||||
предметной области и правила проверки.
|
||||
2. **Запуск, архитектура и игровой цикл** -- `iron_3d.exe`, пятнадцать DLL,
|
||||
сервисы, World3D, очередь объектов и границы кадра.
|
||||
3. **Ресурсная система и форматы** -- NRes, RsLi, кэши, имена, `objects.rlb`,
|
||||
unit DAT и сквозное разрешение ресурсов.
|
||||
4. **Мир, миссии и runtime** -- TMA, ландшафт, ареалы, маршруты, создание мира
|
||||
и свойства размещённых объектов.
|
||||
5. **Геометрия, материалы и рендер** -- MSH, анимация, WEAR, MAT0, Texm, FXID,
|
||||
свет, атмосфера и полный render frame.
|
||||
6. **Поведение, управление, звук и сеть** -- AI, Behavior, Wizard, Control,
|
||||
ввод, камера, звук и DirectPlay-слой.
|
||||
7. **Руководство по полной реализации** -- целевая архитектура, этапы работ,
|
||||
тестовый контур, точность, скорость и критерий совместимости.
|
||||
8. **Справочник и доказательная база** -- ABI, конфигурация, статистика
|
||||
корпусов, границы знания и глоссарий.
|
||||
|
||||
## Политика доказательств
|
||||
|
||||
Специфические утверждения об Iron3D принимаются только после локальной проверки
|
||||
на исполняемых файлах, DLL, демоверсии, полных каталогах Частей 1 и 2 или на
|
||||
взаимных инвариантах реальных ресурсов. Внешние описания и текущий код FParkan
|
||||
могут подсказывать вопросы, но не заменяют проверку.
|
||||
|
||||
Неизвестные поля не получают правдоподобных имён. Пока смысл не закрыт,
|
||||
документация фиксирует raw layout, границы, безопасное чтение и lossless
|
||||
сохранение.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# WEAR и MAT0
|
||||
|
||||
MSH batch хранит только `material_index`. WEAR переводит этот индекс в имя
|
||||
материала, а MAT0 по этому имени описывает phases, parameters и texture
|
||||
references.
|
||||
|
||||
```text
|
||||
Batch20.material_index
|
||||
-> WEAR row
|
||||
-> MAT0 entry
|
||||
-> active phase
|
||||
-> textureName
|
||||
```
|
||||
|
||||
## WEAR
|
||||
|
||||
WEAR -- текстовый ресурс type ID `0x52414557`, обычно `*.wea` рядом с моделью.
|
||||
|
||||
```text
|
||||
<wearCount>
|
||||
<legacyId> <materialName>
|
||||
...
|
||||
|
||||
[empty line]
|
||||
[LIGHTMAPS
|
||||
<lightmapCount>
|
||||
<legacyId> <lightmapName>
|
||||
...]
|
||||
```
|
||||
|
||||
`legacyId` сохраняется, но выбор выполняется по позиции строки и имени. Между
|
||||
основной таблицей и `LIGHTMAPS` нужен пустой разделитель.
|
||||
|
||||
## MAT0
|
||||
|
||||
MAT0 имеет type ID `0x3054414D`, обычно расположен в `Material.lib`. `attr1`
|
||||
содержит runtime flags, `attr2` -- версию payload.
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct Mat0PrefixV4Plus {
|
||||
uint16_t phase_count;
|
||||
uint16_t animation_block_count;
|
||||
uint8_t metadata_a;
|
||||
uint8_t metadata_b;
|
||||
uint32_t metadata_c_raw;
|
||||
uint32_t metadata_d_raw;
|
||||
};
|
||||
|
||||
struct Phase34 {
|
||||
uint8_t parameters[18];
|
||||
char texture_name[16];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Versioned fields читаются только если версия их содержит. Для старых версий
|
||||
используются runtime defaults, а raw values сохраняются.
|
||||
|
||||
## Fallback
|
||||
|
||||
Material resolve:
|
||||
|
||||
1. имя из WEAR;
|
||||
2. `DEFAULT`;
|
||||
3. entry с индексом 0.
|
||||
|
||||
Пустое texture name означает намеренно нетекстурированную поверхность. Lightmap
|
||||
fallback отдельный: отсутствующий lightmap даёт slot `-1`.
|
||||
@@ -0,0 +1,82 @@
|
||||
# MSH
|
||||
|
||||
Файл `*.msh` является NRes-контейнером. Geometry, узлы, slots, batches,
|
||||
animation и служебные streams лежат в entries с разными `type_id`.
|
||||
|
||||
## Entry map
|
||||
|
||||
```text
|
||||
type 1 nodes and slot selection
|
||||
type 2 header 0x8C + Slot68 records
|
||||
type 3 positions float3
|
||||
type 4 packed normals
|
||||
type 5 packed UV0
|
||||
type 6 index buffer u16
|
||||
type 7 triangle descriptors
|
||||
type 8 animation keys
|
||||
type 9 service stream
|
||||
type 10 strings and node names
|
||||
type 13 Batch20 records
|
||||
type 15 auxiliary stream
|
||||
type 17 auxiliary data
|
||||
type 18 rare stream
|
||||
type 19 animation frame map
|
||||
type 20 rare auxiliary table
|
||||
```
|
||||
|
||||
Reader ищет entries по type, но сохраняет исходный порядок для roundtrip.
|
||||
|
||||
## Node and slot selection
|
||||
|
||||
Type 1 обычно состоит из records по 38 bytes:
|
||||
|
||||
```c
|
||||
struct Node38 {
|
||||
uint16_t hdr0;
|
||||
uint16_t parent_or_link;
|
||||
uint16_t anim_map_start;
|
||||
uint16_t fallback_key;
|
||||
uint16_t slot_index[15];
|
||||
};
|
||||
```
|
||||
|
||||
`slot_index[lod * 5 + group]` выбирает geometry slot. `0xFFFF` означает
|
||||
отсутствие геометрии для комбинации LOD/group.
|
||||
|
||||
## Slot and batch
|
||||
|
||||
Type 2 содержит header `0x8C`, затем `Slot68`:
|
||||
|
||||
```c
|
||||
struct Slot68 {
|
||||
uint16_t tri_start;
|
||||
uint16_t tri_count;
|
||||
uint16_t batch_start;
|
||||
uint16_t batch_count;
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
float sphere_center[3];
|
||||
float sphere_radius;
|
||||
uint32_t opaque[5];
|
||||
};
|
||||
```
|
||||
|
||||
Type 13 задаёт draw ranges:
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct Batch20 {
|
||||
uint16_t batch_flags;
|
||||
uint16_t material_index;
|
||||
uint16_t opaque4;
|
||||
uint16_t opaque6;
|
||||
uint16_t index_count;
|
||||
uint32_t index_start;
|
||||
uint16_t opaque14;
|
||||
uint32_t base_vertex;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Index check выполняется как `base_vertex + index < vertex_count` для всего
|
||||
используемого slice.
|
||||
@@ -0,0 +1,61 @@
|
||||
# NRes
|
||||
|
||||
`NRes` -- основной контейнер ресурсов Iron3D. Он используется как внешний
|
||||
архив и как внутренний контейнер модели `*.msh`.
|
||||
|
||||
```text
|
||||
[Header: 16 bytes]
|
||||
[Data region: payload with alignment]
|
||||
[Directory: entry_count * 64 bytes]
|
||||
```
|
||||
|
||||
## Header
|
||||
|
||||
```c
|
||||
struct NResHeader16 {
|
||||
char magic[4]; // "NRes"
|
||||
uint32_t version; // 0x00000100
|
||||
int32_t entry_count; // >= 0
|
||||
uint32_t total_size; // equals file size
|
||||
};
|
||||
```
|
||||
|
||||
`directory_offset = total_size - entry_count * 64`. Reader проверяет отсутствие
|
||||
переполнений, `directory_offset >= 16` и точное окончание каталога на
|
||||
`total_size`.
|
||||
|
||||
## Entry
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct NResEntry64 {
|
||||
uint32_t type_id;
|
||||
uint32_t attr1;
|
||||
uint32_t attr2;
|
||||
uint32_t size;
|
||||
uint32_t attr3;
|
||||
char name[36];
|
||||
uint32_t data_offset;
|
||||
uint32_t sort_index;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Имя содержит bounded C-string до 35 полезных bytes. `sort_index` задаёт
|
||||
отображение из sorted position в original entry index. В строгом режиме все
|
||||
`sort_index` образуют перестановку `0..N-1`.
|
||||
|
||||
## Data region
|
||||
|
||||
Payload каждой записи лежит после header и до начала каталога. Игровые архивы
|
||||
выравнивают следующий payload до 8 bytes нулями, но reader не должен требовать
|
||||
плотного покрытия data region.
|
||||
|
||||
Различаются:
|
||||
|
||||
- active payload -- диапазон, на который указывает entry;
|
||||
- gap/padding -- bytes между активными диапазонами;
|
||||
- unindexed preserved region -- произвольные bytes, не принадлежащие entry.
|
||||
|
||||
Lossless editor сохраняет все три категории. Compact writer может исключить
|
||||
unindexed regions только при явной операции repack.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Render frame
|
||||
|
||||
Кадр является последней стадией цикла, а не самостоятельной функцией renderer-а.
|
||||
До draw calls уже накоплен input, рассчитан tick, применены отложенные операции,
|
||||
выбрана камера и обновлён 3D sound listener.
|
||||
|
||||
## Frame skeleton
|
||||
|
||||
```text
|
||||
system messages and input
|
||||
-> simulation calculation
|
||||
-> deferred object operations
|
||||
-> animation and transforms
|
||||
-> camera and sound listener
|
||||
-> visibility and render queues
|
||||
-> materials and draw passes
|
||||
-> renderer completion
|
||||
-> end-of-render callbacks and UI
|
||||
```
|
||||
|
||||
В `World3D::stdRenderGame` доказан крупный порядок: camera передаётся Terrain,
|
||||
настраиваются viewport/matrices, вызываются renderer boundary slots,
|
||||
устанавливается `in_render`, выполняется traversal мира, закрывается world/shade
|
||||
pass, вызывается renderer completion, снимается `in_render`, рассылается
|
||||
end-of-render.
|
||||
|
||||
## Draw item
|
||||
|
||||
Подготовленный draw item содержит:
|
||||
|
||||
- node world matrix;
|
||||
- batch flags and index range;
|
||||
- WEAR material handle;
|
||||
- MAT0 active phase and coefficients;
|
||||
- texture handle;
|
||||
- optional lightmap handle;
|
||||
- render phase and sorting key;
|
||||
- legacy pipeline state.
|
||||
|
||||
Подготовленный item должен ссылаться на immutable данные кадра. Изменение phase
|
||||
или texture cache посреди прохода не должно менять уже собранную очередь.
|
||||
|
||||
## Parity risks
|
||||
|
||||
- x87 precision and rounding;
|
||||
- scalar/SIMD `g_FastProc` differences;
|
||||
- object, batch and transparent primitive order;
|
||||
- depth, cull, alpha test and blend transitions;
|
||||
- mip-skip, palette and Page coordinates;
|
||||
- material fallback and phase selection;
|
||||
- RNG sequence for FX and atmosphere;
|
||||
- device capability fallback;
|
||||
- simulation time quantization.
|
||||
|
||||
Для отладки нужен deterministic frame capture: camera state, visible object IDs,
|
||||
draw-item list, pipeline keys, matrices и hashes промежуточных buffers.
|
||||
@@ -0,0 +1,69 @@
|
||||
# RsLi
|
||||
|
||||
`RsLi` -- библиотечный архив Iron3D с каталогом в начале файла и payloads после
|
||||
него.
|
||||
|
||||
```text
|
||||
[Header: 32 bytes]
|
||||
[Entry table: entry_count * 32 bytes]
|
||||
[Payloads]
|
||||
[optional trailer]
|
||||
```
|
||||
|
||||
## Header fields
|
||||
|
||||
```text
|
||||
+0x00 char[2] "NL"
|
||||
+0x02 u8 reserved
|
||||
+0x03 u8 version = 1
|
||||
+0x04 i16 entry_count
|
||||
+0x0E u16 presorted_flag = 0xABBA
|
||||
+0x14 u32 xor_seed
|
||||
```
|
||||
|
||||
Остальные bytes сохраняются без нормализации.
|
||||
|
||||
## Entry
|
||||
|
||||
```c
|
||||
struct RsLiEntry32 {
|
||||
char name[12];
|
||||
uint8_t service[4];
|
||||
int16_t flags;
|
||||
int16_t sort_to_original;
|
||||
uint32_t unpacked_size;
|
||||
uint32_t data_offset_raw;
|
||||
uint32_t packed_size;
|
||||
};
|
||||
```
|
||||
|
||||
Имя обычно хранится в uppercase ASCII. `sort_to_original` связывает sorted
|
||||
position с исходной записью.
|
||||
|
||||
## Table transform
|
||||
|
||||
Entry table проходит обратимое потоковое XOR-преобразование. Начальное
|
||||
состояние берётся из младших 16 bits `xor_seed` и продолжается через всю
|
||||
таблицу, не сбрасываясь на границе записи.
|
||||
|
||||
## Storage methods
|
||||
|
||||
```text
|
||||
0x000 raw block
|
||||
0x020 byte transform only
|
||||
0x040 LZSS
|
||||
0x060 transform + LZSS
|
||||
0x080 adaptive Huffman + LZSS
|
||||
0x0A0 transform + adaptive Huffman + LZSS
|
||||
0x100 raw Deflate
|
||||
```
|
||||
|
||||
После любого пути должно получиться ровно `unpacked_size` bytes. Методы
|
||||
`0x080` и `0x0A0` подтверждены decoder-кодом, но не живыми payload демоверсии
|
||||
или обеих частей.
|
||||
|
||||
## Compatibility quirk
|
||||
|
||||
`sprites.lib::INTERF8.TEX` объявляет Deflate range на один byte дальше EOF.
|
||||
Совместимый reader допускает `packed_size - 1` только для этого именованного
|
||||
случая. Строгий режим сообщает `deflate_eof_plus_one`.
|
||||
@@ -0,0 +1,67 @@
|
||||
# Texm
|
||||
|
||||
`Texm` -- основной формат изображений Iron3D. Payload содержит header,
|
||||
необязательную палитру, mip chain и иногда `Page` chunk.
|
||||
|
||||
```c
|
||||
struct TexmHeader32 {
|
||||
uint32_t magic; // 'Texm'
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t mip_count;
|
||||
uint32_t flags4;
|
||||
uint32_t flags5;
|
||||
uint32_t unknown6;
|
||||
uint32_t format;
|
||||
};
|
||||
```
|
||||
|
||||
## Pixel formats
|
||||
|
||||
```text
|
||||
0 Indexed8 + palette 256 * 4 bytes
|
||||
565 R5 G6 B5
|
||||
556 R5 G5 B6
|
||||
4444 A4 R4 G4 B4
|
||||
88 L8 A8
|
||||
888 RGB8 in four-byte element
|
||||
8888 A8 R8 G8 B8
|
||||
```
|
||||
|
||||
Короткие каналы расширяются до 8 bits повторением значимых bits. Для 888
|
||||
служебный четвёртый byte сохраняется при roundtrip.
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
TexmHeader32
|
||||
[palette 1024 bytes, only for format 0]
|
||||
level 0 pixels
|
||||
level 1 pixels
|
||||
...
|
||||
level mip_count-1 pixels
|
||||
[optional Page chunk]
|
||||
```
|
||||
|
||||
Размер mip level вычисляется через `max(1, width >> i)` и
|
||||
`max(1, height >> i)`. Parser суммирует размеры с проверкой переполнения до
|
||||
чтения данных.
|
||||
|
||||
## Page chunk
|
||||
|
||||
```c
|
||||
struct PageHeader8 {
|
||||
uint32_t magic; // 'Page'
|
||||
uint32_t rect_count;
|
||||
};
|
||||
|
||||
struct PageRect8 {
|
||||
int16_t x;
|
||||
int16_t width;
|
||||
int16_t y;
|
||||
int16_t height;
|
||||
};
|
||||
```
|
||||
|
||||
Chunk обязан иметь размер `8 + rect_count * 8`. Rectangles находятся в pixel
|
||||
space базового mip и масштабируются после mip-skip.
|
||||
@@ -0,0 +1,64 @@
|
||||
# TMA
|
||||
|
||||
`data.tma` -- основное описание расстановки и логической конфигурации миссии.
|
||||
Файл перечисляет paths, clans, objects, свойства, ссылку на ландшафт и extras.
|
||||
|
||||
## String primitive
|
||||
|
||||
```c
|
||||
struct LpString {
|
||||
uint32_t byte_length;
|
||||
uint8_t bytes[byte_length];
|
||||
};
|
||||
```
|
||||
|
||||
Reader продвигается ровно на `4 + byte_length`. Завершающий NUL не является
|
||||
обязательной частью framing. Для человекочитаемого вида используется legacy
|
||||
ANSI/CP1251 view, но исходные bytes сохраняются.
|
||||
|
||||
## Top level
|
||||
|
||||
```text
|
||||
u32 format_version
|
||||
u32 path_count
|
||||
PathRecord paths[path_count]
|
||||
u32 clan_section_version
|
||||
u32 clan_count
|
||||
ClanRecord clans[clan_count]
|
||||
u32 object_section_version
|
||||
u32 object_count
|
||||
PlacedObject objects[object_count]
|
||||
LpString land_path
|
||||
u32 mission_flag
|
||||
LpString description_raw
|
||||
u32 extra_section_version
|
||||
u32 extra_count
|
||||
ExtraRecord28 extras[extra_count]
|
||||
```
|
||||
|
||||
Все 60 TMA Частей 1 и 2 проходят parser до точного EOF. Версии стабильны:
|
||||
верхний уровень `1`, clan section `6`, object section `10`, property schema
|
||||
`1`, trailing section `1`.
|
||||
|
||||
## PlacedObject
|
||||
|
||||
```text
|
||||
u32 raw_kind
|
||||
u32 class_or_flags
|
||||
LpString resource_name
|
||||
u32 raw_after_resource
|
||||
u32 identity_or_clan_raw
|
||||
f32 position[3]
|
||||
f32 orientation[3]
|
||||
f32 scale[3]
|
||||
LpString instance_name
|
||||
u32 raw_after_name
|
||||
i32 link0
|
||||
i32 link1
|
||||
u32 property_schema_version
|
||||
u32 property_count
|
||||
Property properties[property_count]
|
||||
```
|
||||
|
||||
`Property` состоит из четырёх raw `u32` и имени. Typed views разрешены только
|
||||
для доказанных property names и consumers.
|
||||
@@ -1,35 +0,0 @@
|
||||
# AI system
|
||||
|
||||
Страница фиксирует границы подсистемы AI на уровне движка:
|
||||
|
||||
- выбор целей;
|
||||
- тактические приоритеты;
|
||||
- координация с `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» для побайтного/пошагового сравнения с оригиналом.
|
||||
@@ -1,31 +0,0 @@
|
||||
# ArealMap
|
||||
|
||||
`ArealMap` — подсистема топологии мира и логических зон.
|
||||
|
||||
Подробный бинарный формат `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-тестов поведения навигационных запросов на одинаковых входах.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Behavior system
|
||||
|
||||
`Behavior` — слой исполнения состояний юнитов между AI-решением и низкоуровневым control-командованием.
|
||||
|
||||
## 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-сценариях.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Control system
|
||||
|
||||
`Control` — подсистема входа и маршрутизации команд (пользовательских и системных).
|
||||
|
||||
## 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-тесты на записанных последовательностях ввода.
|
||||
@@ -1,45 +0,0 @@
|
||||
# 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`
|
||||
|
||||
Проверено legacy-валидатором архивов.
|
||||
|
||||
## 2. Проверка рендерных форматов
|
||||
|
||||
Результаты:
|
||||
|
||||
- `MSH`: `435/435` валидны
|
||||
- `Texm`: `518/518` валидны
|
||||
- `FXID`: `923/923` валидны
|
||||
- `Terrain/Map` (`Land.msh` + `Land.map`): `33/33` без ошибок/предупреждений
|
||||
|
||||
Проверено legacy-валидаторами рендерных форматов.
|
||||
|
||||
## 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-тесты».
|
||||
@@ -1,202 +0,0 @@
|
||||
# FXID
|
||||
|
||||
`FXID` — бинарный формат эффекта в движке Parkan: Iron Strategy.
|
||||
Эта страница задаёт контракт формата и исполнения на уровне, достаточном для 1:1 порта рендера/симуляции эффектов и для lossless-инструментов.
|
||||
|
||||
Связанные контейнеры: [NRes](nres.md), [RsLi](rsli.md).
|
||||
|
||||
## 1. Контейнер
|
||||
|
||||
- Тип ресурса в `NRes`: `0x44495846` (`FXID`).
|
||||
- Значения `attr1/attr2/attr3` в типовых игровых данных стабильны, но при редактуре их нужно сохранять как есть.
|
||||
|
||||
## 2. Бинарный формат
|
||||
|
||||
Все значения little-endian.
|
||||
|
||||
### 2.1. Заголовок (60 байт)
|
||||
|
||||
```c
|
||||
struct FxHeader60 {
|
||||
uint32_t cmd_count; // 0x00
|
||||
uint32_t time_mode; // 0x04
|
||||
float duration_sec; // 0x08
|
||||
float phase_jitter; // 0x0C
|
||||
uint32_t flags; // 0x10
|
||||
uint32_t settings_id; // 0x14
|
||||
float rand_shift_x; // 0x18
|
||||
float rand_shift_y; // 0x1C
|
||||
float rand_shift_z; // 0x20
|
||||
float pivot_x; // 0x24
|
||||
float pivot_y; // 0x28
|
||||
float pivot_z; // 0x2C
|
||||
float scale_x; // 0x30
|
||||
float scale_y; // 0x34
|
||||
float scale_z; // 0x38
|
||||
};
|
||||
```
|
||||
|
||||
Поток команд начинается строго с `offset = 0x3C`.
|
||||
|
||||
### 2.2. Команда
|
||||
|
||||
Каждая команда:
|
||||
|
||||
1. `uint32 cmd_word`
|
||||
2. body фиксированного размера, зависящего от `opcode`
|
||||
|
||||
Поля `cmd_word`:
|
||||
|
||||
- `opcode = cmd_word & 0xFF`
|
||||
- `enabled = (cmd_word >> 8) & 1`
|
||||
- `bits 9..31` нужно сохранять 1:1
|
||||
|
||||
Выравнивания между командами нет.
|
||||
|
||||
### 2.3. Размеры команд
|
||||
|
||||
| Opcode | Размер |
|
||||
|---:|---:|
|
||||
| 1 | 224 |
|
||||
| 2 | 148 |
|
||||
| 3 | 200 |
|
||||
| 4 | 204 |
|
||||
| 5 | 112 |
|
||||
| 6 | 4 |
|
||||
| 7 | 208 |
|
||||
| 8 | 248 |
|
||||
| 9 | 208 |
|
||||
| 10 | 208 |
|
||||
|
||||
## 3. Смысл заголовка
|
||||
|
||||
- `cmd_count`: число команд в потоке.
|
||||
- `time_mode`: способ вычисления текущего коэффициента эффекта.
|
||||
- `duration_sec`: длительность (в рантайме переводится в миллисекунды).
|
||||
- `phase_jitter`: амплитуда случайного фазового сдвига.
|
||||
- `flags`: флаги поведения (видимость, альфа-модификаторы, режимы гейтинга).
|
||||
- `settings_id`: индекс профиля/настроек эффекта.
|
||||
- `rand_shift_*`: случайный пространственный сдвиг.
|
||||
- `pivot_*`: локальная опора.
|
||||
- `scale_*`: базовый масштаб инстанса эффекта.
|
||||
|
||||
## 4. Флаги заголовка
|
||||
|
||||
Практически важные биты:
|
||||
|
||||
- `0x0001`: случайный сдвиг фазы
|
||||
- `0x0008`: случайный пространственный сдвиг (`rand_shift_*`)
|
||||
- `0x0010`: ветки видимости/окклюзии
|
||||
- `0x0020`: треугольный ремап альфы
|
||||
- `0x0040`: инверсия исходного active-state
|
||||
- `0x0080`, `0x0100`: фильтрация по времени суток
|
||||
- `0x0200`: умножение альфы на нормализованное время жизни
|
||||
- `0x0400`, `0x1000`: дополнительные биты состояния менеджера эффекта
|
||||
- `0x0800`: дополнительный гейтинг
|
||||
|
||||
Неизвестные биты должны сохраняться без изменений.
|
||||
|
||||
## 5. `time_mode` (0..17)
|
||||
|
||||
База:
|
||||
|
||||
- `tn = (now - start) / (end - start)`
|
||||
- `prev = предыдущая вычисленная альфа`
|
||||
|
||||
Поддерживаемые семейства режимов:
|
||||
|
||||
- константный режим;
|
||||
- линейный (`tn`), обратный (`1-tn`), циклический (`fract(tn)`);
|
||||
- режимы от внешних параметров мира/очереди;
|
||||
- режимы на основе норм векторов состояния;
|
||||
- режимы с ограничением вниз/вверх относительно `prev`.
|
||||
|
||||
После вычисления:
|
||||
|
||||
- при `flags & 0x0200` применяется `alpha *= tn`;
|
||||
- при `flags & 0x0020` применяется triangular remap.
|
||||
|
||||
## 6. Resource-ссылки внутри команд
|
||||
|
||||
Для opcode `2/3/4/5/7/8/9/10` используется ссылка:
|
||||
|
||||
```c
|
||||
struct ResourceRef64 {
|
||||
char archive[32];
|
||||
char name[32];
|
||||
};
|
||||
```
|
||||
|
||||
Контракт:
|
||||
|
||||
- строки ASCII, нуль-терминированные;
|
||||
- сравнение имён регистронезависимое;
|
||||
- обычно:
|
||||
- `opcode 2`: `sounds.lib` + `*.wav`
|
||||
- остальные: `material.lib` + имя материала/эффекта.
|
||||
|
||||
## 7. Runtime-контракт исполнения
|
||||
|
||||
На создании инстанса:
|
||||
|
||||
1. Заголовок копируется в runtime-состояние.
|
||||
2. Вычисляется `end_time`.
|
||||
3. Для каждой команды создаётся runtime-объект по `opcode`.
|
||||
4. В объект копируется `enabled`.
|
||||
5. Объект инициализируется контекстом эффекта.
|
||||
|
||||
На каждом кадре:
|
||||
|
||||
1. Вычисляется текущий коэффициент/альфа по `time_mode` и `flags`.
|
||||
2. Выполняется update каждой команды.
|
||||
3. Выполняется emit/render часть активных команд.
|
||||
4. Применяются события Start/Stop/Restart.
|
||||
|
||||
## 8. Строгий парсер (рекомендуемый)
|
||||
|
||||
1. Проверить `len(payload) >= 60`.
|
||||
2. Прочитать `cmd_count`.
|
||||
3. Идти от `ptr = 0x3C`.
|
||||
4. Для каждой команды:
|
||||
- проверить `ptr + 4 <= len`;
|
||||
- прочитать `opcode`;
|
||||
- проверить, что `opcode` поддержан;
|
||||
- проверить `ptr + size(opcode) <= len`;
|
||||
- сдвинуть `ptr += size(opcode)`.
|
||||
5. Проверить `ptr == len(payload)`.
|
||||
|
||||
## 9. Writer и редактор
|
||||
|
||||
Для lossless-совместимости:
|
||||
|
||||
- сохранять все неизвестные поля/биты;
|
||||
- не менять фиксированные размеры команд;
|
||||
- не добавлять padding;
|
||||
- пересчитывать только `cmd_count` и размеры контейнера;
|
||||
- сохранять порядок команд.
|
||||
|
||||
## 10. Что требуется для 1:1 переноса
|
||||
|
||||
1. Полная поддержка opcode `1..10`.
|
||||
2. Точный контракт вычисления `time_mode` и `flags`.
|
||||
3. Точное поведение `ResourceRef64`.
|
||||
4. Повторяемый RNG и одинаковая политика плавающей точки.
|
||||
|
||||
## 11. Статус валидации
|
||||
|
||||
- Формальные инварианты FXID зафиксированы в спецификациях проекта и проверены legacy-валидаторами.
|
||||
- На полном 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 новый рендер) на фиксированных сценах.
|
||||
@@ -1,144 +0,0 @@
|
||||
# Material (`MAT0`)
|
||||
|
||||
`MAT0` описывает материал и его фазовую анимацию.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [Wear table (`WEAR`)](wear.md)
|
||||
- [Texture (`Texm`)](texture.md)
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
## 1. Контейнер
|
||||
|
||||
- Тип ресурса: `0x3054414D` (`MAT0`).
|
||||
- Обычно хранится в `Material.lib`.
|
||||
- `attr1` используется как битовое поле runtime-флагов материала.
|
||||
- `attr2` задаёт версию заголовка payload.
|
||||
|
||||
## 2. Бинарный layout
|
||||
|
||||
```c
|
||||
struct Mat0Payload {
|
||||
uint16_t phaseCount;
|
||||
uint16_t animBlockCount; // должно быть < 20
|
||||
|
||||
// если attr2 >= 2
|
||||
uint8_t metaA8;
|
||||
uint8_t metaB8;
|
||||
// если attr2 >= 3
|
||||
uint32_t metaC32;
|
||||
// если attr2 >= 4
|
||||
uint32_t metaD32;
|
||||
|
||||
PhaseRecord34 phases[phaseCount];
|
||||
AnimBlockRaw anim[animBlockCount];
|
||||
};
|
||||
```
|
||||
|
||||
Если `attr2 < 2`, используются runtime-значения по умолчанию:
|
||||
|
||||
- `metaA = 255`
|
||||
- `metaB = 255`
|
||||
- `metaC = 1.0f`
|
||||
- `metaD = 0`
|
||||
|
||||
## 3. Фазы материала
|
||||
|
||||
```c
|
||||
struct PhaseRecord34 {
|
||||
uint8_t params[18];
|
||||
char textureName[16];
|
||||
};
|
||||
```
|
||||
|
||||
В рантайме запись разворачивается в структуру ~76 байт:
|
||||
|
||||
- набор коэффициентов цвета/освещения/прозрачности;
|
||||
- индекс слота текстуры;
|
||||
- дополнительные целочисленные поля.
|
||||
|
||||
`textureName`:
|
||||
|
||||
- пустая строка -> фаза без текстуры (`texSlot = -1`);
|
||||
- непустая строка -> загрузка текстуры по имени.
|
||||
|
||||
## 4. Анимационные блоки
|
||||
|
||||
```c
|
||||
struct AnimBlockRaw {
|
||||
uint32_t headerRaw; // mode = low 3 bits, interpMask = остальные
|
||||
uint16_t keyCount;
|
||||
KeyRaw keys[keyCount];
|
||||
};
|
||||
|
||||
struct KeyRaw {
|
||||
uint16_t k0;
|
||||
uint16_t k1;
|
||||
uint16_t k2; // opaque, сохранять 1:1
|
||||
};
|
||||
```
|
||||
|
||||
`k2` нельзя удалять или нормализовать: это часть бинарного контракта.
|
||||
|
||||
## 5. Выбор текущей фазы
|
||||
|
||||
Материал выбирает фазу по времени и по режиму анимации блока:
|
||||
|
||||
- loop;
|
||||
- ping-pong;
|
||||
- one-shot с clamp;
|
||||
- random-offset.
|
||||
|
||||
При смешивании интерполируется только часть полей, остальные копируются из активной фазы.
|
||||
Для 1:1 совместимости важно сохранить эту выборочную интерполяцию.
|
||||
|
||||
## 6. Загрузка и fallback
|
||||
|
||||
При запросе материала по имени:
|
||||
|
||||
1. Точный поиск по имени.
|
||||
2. Если не найдено — fallback на `DEFAULT`.
|
||||
3. Если `DEFAULT` отсутствует — используется запись с индексом `0`.
|
||||
|
||||
## 7. Атрибуты и флаги
|
||||
|
||||
Практически важные биты `attr1`:
|
||||
|
||||
- бит загрузки текстурной фазы с расширенными флагами;
|
||||
- флаги аппаратного профиля;
|
||||
- 4-битный режим (`nibbleMode`);
|
||||
- дополнительный флаг material-поведения.
|
||||
|
||||
Неизвестные биты должны сохраняться без изменений.
|
||||
|
||||
## 8. Ограничения
|
||||
|
||||
- `animBlockCount < 20`
|
||||
- `phaseCount` и фактический размер секции фаз должны совпадать
|
||||
- `textureName` должен быть NUL-terminated и укладываться в 16 байт
|
||||
|
||||
## 9. Правила writer/editor
|
||||
|
||||
1. Сохранять `attr1/attr2/attr3`.
|
||||
2. Не менять `metaA/B/C/D` без явного запроса.
|
||||
3. Сохранять opaque-поля анимации (включая `k2`) 1:1.
|
||||
4. Проверять выход за границы payload при парсинге.
|
||||
|
||||
## 10. Статус валидации
|
||||
|
||||
- Инварианты MAT0 зафиксированы в спецификациях проекта.
|
||||
- Структурная валидация MAT0 проверена legacy-валидатором на полном 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 на реальных моделях.
|
||||
@@ -1,18 +0,0 @@
|
||||
# Materials, WEAR, Texm
|
||||
|
||||
Старая объединённая страница разбита по объектам.
|
||||
|
||||
- [Material (`MAT0`)](material.md)
|
||||
- [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`.
|
||||
@@ -1,46 +0,0 @@
|
||||
# Missions
|
||||
|
||||
Подсистема `Missions` управляет сценарием:
|
||||
|
||||
- стартовыми условиями;
|
||||
- триггерами;
|
||||
- победой/поражением;
|
||||
- синхронизацией с 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-тестов «миссия от старта до завершения».
|
||||
## 4. Mission -> Prototype -> Mesh bridge
|
||||
|
||||
Для 3D-объектов миссии обязательна промежуточная стадия `objects.rlb`:
|
||||
|
||||
1. `data.tma` задаёт либо прямой ключ объекта, либо путь к `*.dat`.
|
||||
2. `*.dat` даёт `model_key` (в retail-наборе через `objects.rlb`).
|
||||
3. Ключ резолвится в запись прототипа внутри `objects.rlb`.
|
||||
4. Из прототипа выбирается фактический `*.msh` и архив (например `bases.rlb`, `static.rlb`, `fortif.rlb`).
|
||||
5. Только после этого запускается стандартная цепочка материалов и текстур.
|
||||
|
||||
Детальный формат и алгоритм вынесены в отдельную страницу:
|
||||
|
||||
- [Object registry (`objects.rlb`)](object-registry.md)
|
||||
@@ -1,126 +0,0 @@
|
||||
# MSH animation
|
||||
|
||||
`MSH animation` описывает связку `Res8 + Res19` и runtime-правила сэмплирования/смешивания поз.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [MSH core](msh-core.md)
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
## 1. Ресурсы анимации
|
||||
|
||||
### 1.1. `Res8` (пул ключей)
|
||||
|
||||
```c
|
||||
struct AnimKey24 {
|
||||
float pos_x;
|
||||
float pos_y;
|
||||
float pos_z;
|
||||
float time;
|
||||
int16_t qx;
|
||||
int16_t qy;
|
||||
int16_t qz;
|
||||
int16_t qw;
|
||||
};
|
||||
```
|
||||
|
||||
Декодирование quaternion-компонент: `q = s16 / 32767.0`.
|
||||
|
||||
### 1.2. `Res19` (карта кадров)
|
||||
|
||||
```c
|
||||
uint16_t map_words[]; // size/2 элементов
|
||||
```
|
||||
|
||||
`Res19.attr2` хранит глобальную длину таймлайна (число кадров).
|
||||
|
||||
### 1.3. Связь с `Res1`
|
||||
|
||||
Для каждого узла:
|
||||
|
||||
- `anim_map_start` (`hdr2`) — начало блока в `Res19` или `0xFFFF`.
|
||||
- `fallback_key` (`hdr3`) — индекс fallback-ключа в `Res8`.
|
||||
|
||||
## 2. Сэмплирование узла
|
||||
|
||||
Вход: время `t`, текущий узел.
|
||||
Выход: `quat(w,x,y,z)` и `pos(x,y,z)`.
|
||||
|
||||
### 2.1. Индекс кадра
|
||||
|
||||
Движок использует x87-совместимое округление для выражения `t - 0.5`.
|
||||
Для 1:1 повторения нужно сохранить ту же политику плавающей точки.
|
||||
|
||||
### 2.2. Выбор key index
|
||||
|
||||
1. Если кадр вне диапазона `frame_count` -> `fallback_key`.
|
||||
2. Если `anim_map_start == 0xFFFF` -> `fallback_key`.
|
||||
3. Иначе берётся `map_words[anim_map_start + frame]`:
|
||||
- если значение `>= fallback_key`, тоже используется `fallback_key`;
|
||||
- иначе используется значение из map.
|
||||
|
||||
### 2.3. Интерполяция
|
||||
|
||||
Если выбран fallback, возвращается ровно этот ключ без интерполяции.
|
||||
|
||||
Иначе:
|
||||
|
||||
1. Берутся соседние ключи `k0` и `k1`.
|
||||
2. Если `t` точно равен `k0.time` или `k1.time`, возвращается соответствующий ключ.
|
||||
3. Иначе:
|
||||
- `alpha = (t - k0.time) / (k1.time - k0.time)`
|
||||
- `pos = lerp(k0.pos, k1.pos, alpha)`
|
||||
- `quat = slerp_like(k0.quat, k1.quat, alpha)`
|
||||
|
||||
Кватернион в runtime хранится в порядке `[w, x, y, z]`.
|
||||
|
||||
## 3. Смешивание двух сэмплов
|
||||
|
||||
При blending между позами A и B:
|
||||
|
||||
1. Выбираются валидные стороны по `blend` и валидности времени.
|
||||
2. Если активна одна сторона, берётся она.
|
||||
3. Если активны обе:
|
||||
- применяется shortest-path flip для `qB`;
|
||||
- выполняется quaternion blend;
|
||||
- позиция смешивается линейно.
|
||||
|
||||
Матрица строится из quaternion, а translation подставляется отдельным шагом.
|
||||
|
||||
## 4. Каноника writer
|
||||
|
||||
Рекомендуемые правила:
|
||||
|
||||
1. Ключи узлов писать подряд в `Res8` в порядке узлов.
|
||||
2. `fallback_key` узла указывает на последний ключ его трека.
|
||||
3. Для узлов с map выделять блок длины `frame_count` в `Res19`.
|
||||
4. Для статических узлов: `anim_map_start = 0xFFFF`, один ключ с `time=0`.
|
||||
5. `Res8.attr1 = key_count`, `Res8.attr3 = 4`.
|
||||
6. `Res19.attr1 = map_word_count`, `Res19.attr2 = frame_count`, `Res19.attr3 = 2`.
|
||||
|
||||
## 5. Валидация перед сохранением
|
||||
|
||||
- `Res8.size % 24 == 0`
|
||||
- `Res19.size % 2 == 0`
|
||||
- каждый `fallback_key < key_count`
|
||||
- для узла с map: `anim_map_start + frame_count <= map_word_count`
|
||||
- внутри трека времена ключей строго возрастают
|
||||
|
||||
## 6. Статус валидации
|
||||
|
||||
- Форматные проверки были покрыты legacy-валидатором.
|
||||
- Корпусная валидация анимационных инвариантов выполнена на полном 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» на длинных анимациях.
|
||||
@@ -1,192 +0,0 @@
|
||||
# MSH core
|
||||
|
||||
`MSH core` описывает геометрию, слоты, батчи и базовые таблицы модели.
|
||||
Документ покрывает контракт, необходимый для 1:1 воспроизведения рендера и коллизии.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [MSH animation](msh-animation.md)
|
||||
- [Material](material.md)
|
||||
- [Texture (Texm)](texture.md)
|
||||
- [Render pipeline](render.md)
|
||||
- [NRes](nres.md)
|
||||
- [RsLi](rsli.md)
|
||||
|
||||
## 1. Общая модель
|
||||
|
||||
MSH-модель хранится как `NRes`-контейнер.
|
||||
Связь таблиц строится по `type`, а не по порядку записей.
|
||||
|
||||
Базовый путь геометрии:
|
||||
|
||||
1. `Res1` выбирает slot по `(node, lod, group)`.
|
||||
2. `Res2.slot` задаёт диапазоны треугольников и батчей.
|
||||
3. `Res13` задаёт диапазон индексов и `baseVertex`.
|
||||
4. `Res6` даёт `uint16` индексы.
|
||||
5. `Res3/Res4/Res5` дают вершины, нормали и UV.
|
||||
|
||||
## 2. Карта core-ресурсов
|
||||
|
||||
| Type | Ресурс | Обязательность | Stride / layout |
|
||||
|---:|---|---|---|
|
||||
| 1 | Node table | обязательный | обычно 38 байт |
|
||||
| 2 | Header + slots | обязательный | `0x8C + n*68` |
|
||||
| 3 | Positions | обязательный | 12 |
|
||||
| 4 | Packed normals | обычно обязательный | 4 |
|
||||
| 5 | Packed UV0 | обычно обязательный | 4 |
|
||||
| 6 | Index buffer | обязательный | 2 |
|
||||
| 7 | Tri descriptors | для коллизии/пикинга | 16 |
|
||||
| 8 | Anim key pool | для анимированных | 24 |
|
||||
| 10 | Node strings | опциональный | variable |
|
||||
| 13 | Batch table | обязательный | 20 |
|
||||
| 15 | Доп. stream | опциональный | 8 |
|
||||
| 16 | Доп. stream | опциональный | 8 |
|
||||
| 18 | Доп. stream | опциональный | 4 |
|
||||
| 19 | Anim map | для анимированных | 2 |
|
||||
| 20 | Доп. таблица | опциональный | variable |
|
||||
|
||||
## 3. Основные структуры
|
||||
|
||||
### 3.1. `Res1` (узлы)
|
||||
|
||||
```c
|
||||
struct Node38 {
|
||||
uint16_t hdr0;
|
||||
uint16_t parent_or_link;
|
||||
uint16_t anim_map_start;
|
||||
uint16_t fallback_key;
|
||||
uint16_t slotIndex[15]; // lod0:g0..g4, lod1:g0..g4, lod2:g0..g4
|
||||
};
|
||||
```
|
||||
|
||||
Формула slot-выбора:
|
||||
|
||||
```c
|
||||
slot = node.slotIndex[lod * 5 + group]
|
||||
```
|
||||
|
||||
`0xFFFF` означает отсутствие слота.
|
||||
|
||||
### 3.2. `Res2` (header + slot records)
|
||||
|
||||
```c
|
||||
struct Slot68 {
|
||||
uint16_t triStart;
|
||||
uint16_t triCount;
|
||||
uint16_t batchStart;
|
||||
uint16_t batchCount;
|
||||
float aabbMin[3];
|
||||
float aabbMax[3];
|
||||
float sphereCenter[3];
|
||||
float sphereRadius;
|
||||
uint32_t opaque[5];
|
||||
};
|
||||
```
|
||||
|
||||
`opaque[5]` должны сохраняться 1:1.
|
||||
|
||||
### 3.3. `Res3`, `Res4`, `Res5`, `Res6`
|
||||
|
||||
- `Res3`: `float3` позиции (`stride=12`)
|
||||
- `Res4`: `int8[4]` packed normal (`stride=4`)
|
||||
- `Res5`: `int16[2]` UV (`stride=4`)
|
||||
- `Res6`: `uint16` индексы (`stride=2`)
|
||||
|
||||
Декодирование:
|
||||
|
||||
- normal = `clamp(n / 127.0, -1..1)`
|
||||
- uv = `packed / 1024.0`
|
||||
|
||||
### 3.4. `Res7` и `Res13`
|
||||
|
||||
```c
|
||||
struct TriDesc16 {
|
||||
uint16_t triFlags;
|
||||
uint16_t link0;
|
||||
uint16_t link1;
|
||||
uint16_t link2;
|
||||
int16_t nx;
|
||||
int16_t ny;
|
||||
int16_t nz;
|
||||
uint16_t selPacked;
|
||||
};
|
||||
|
||||
struct Batch20 {
|
||||
uint16_t batchFlags;
|
||||
uint16_t materialIndex;
|
||||
uint16_t opaque4;
|
||||
uint16_t opaque6;
|
||||
uint16_t indexCount;
|
||||
uint32_t indexStart;
|
||||
uint16_t opaque14;
|
||||
uint32_t baseVertex;
|
||||
};
|
||||
```
|
||||
|
||||
`selPacked` хранит 3 селектора по 2 бита; значение `3` трактуется как `0xFFFF`.
|
||||
|
||||
## 4. Runtime-обход модели
|
||||
|
||||
Псевдокод рендера:
|
||||
|
||||
```c
|
||||
for each node:
|
||||
slot = resolve_slot(node, lod, group)
|
||||
if slot == none: continue
|
||||
|
||||
if culled(slot.bounds, node_transform): continue
|
||||
|
||||
for b in slot.batchRange:
|
||||
batch = batches[b]
|
||||
bind_material(batch.materialIndex)
|
||||
|
||||
draw_indexed(
|
||||
baseVertex = batch.baseVertex,
|
||||
indexStart = batch.indexStart,
|
||||
indexCount = batch.indexCount
|
||||
)
|
||||
```
|
||||
|
||||
## 5. Критические инварианты
|
||||
|
||||
Обязательно проверять:
|
||||
|
||||
- `Res2.size >= 0x8C`
|
||||
- `(Res2.size - 0x8C) % 68 == 0`
|
||||
- `batchStart + batchCount` не выходит за `Res13`
|
||||
- `triStart + triCount` не выходит за `Res7`
|
||||
- `indexStart + indexCount` не выходит за `Res6`
|
||||
- `baseVertex + max(indexSlice) < vertexCount`
|
||||
- `slotIndex == 0xFFFF` или `< slotCount`
|
||||
|
||||
## 6. Важные edge-cases
|
||||
|
||||
- Встречается редкий вариант `Res1.attr3 = 24`; для существующих ассетов нужен copy-through.
|
||||
- Для строгого writer лучше генерировать `Res1` в основном формате `38` байт/узел.
|
||||
- Неизвестные поля таблиц нельзя нормализовать или обнулять.
|
||||
|
||||
## 7. Правила для writer/editor
|
||||
|
||||
1. Сохранять неизвестные поля и неизвестные `type`-ресурсы.
|
||||
2. Пересчитывать только явно вычислимые атрибуты (`attr1/attr3` и size-зависимые поля).
|
||||
3. Не менять порядок/контент opaque-данных без явной цели.
|
||||
4. Сериализовать little-endian, без внутреннего padding.
|
||||
|
||||
## 8. Статус валидации
|
||||
|
||||
- Инварианты формата проверены legacy-валидатором.
|
||||
- На полном 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-паритетом.
|
||||
@@ -1,118 +0,0 @@
|
||||
# 3D implementation notes
|
||||
|
||||
Контрольная страница с практическими правилами реализации 3D-пайплайна и с перечнем незакрытых зон.
|
||||
Документ intentionally high-level: без ссылок на внутренние функции/адреса.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [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)
|
||||
|
||||
## 1. Базовые двоичные правила
|
||||
|
||||
1. Все форматы в этой подсистеме little-endian.
|
||||
2. Внутри NRes данные ресурсов выравниваются по 8 байт.
|
||||
3. Внутри payload таблиц padding между записями обычно отсутствует: записи идут подряд по stride.
|
||||
|
||||
## 2. Быстрая карта stride'ов
|
||||
|
||||
| Ресурс | Запись | 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 |
|
||||
|
||||
## 3. Декодирование ключевых потоков
|
||||
|
||||
## 3.1. Позиции (Res3)
|
||||
|
||||
`float3`, stride `12`.
|
||||
|
||||
## 3.2. Нормали (Res4)
|
||||
|
||||
`int8[4]`, используются первые 3 компоненты:
|
||||
|
||||
```text
|
||||
n = clamp(s8 / 127.0, -1..1)
|
||||
```
|
||||
|
||||
## 3.3. UV (Res5)
|
||||
|
||||
`int16[2]`:
|
||||
|
||||
```text
|
||||
u = s16 / 1024.0
|
||||
v = s16 / 1024.0
|
||||
```
|
||||
|
||||
## 3.4. Animation key (Res8)
|
||||
|
||||
`pos(float3) + time(float) + quat(int16x4)`:
|
||||
|
||||
```text
|
||||
q = s16 / 32767.0
|
||||
```
|
||||
|
||||
## 4. Практический reader-контракт
|
||||
|
||||
Для runtime-совместимого чтения модели:
|
||||
|
||||
1. Найти нужные ресурсы по `type_id` в NRes.
|
||||
2. Проверить `size/stride`-инварианты.
|
||||
3. Проверить диапазоны ссылок:
|
||||
- slot -> batch/triangles;
|
||||
- batch -> indices;
|
||||
- indices -> vertices;
|
||||
- anim_map -> anim_keys.
|
||||
4. Неизвестные поля и неизвестные ресурсы сохранять через copy-through.
|
||||
|
||||
## 5. Практический writer-контракт
|
||||
|
||||
1. Пересчитывать только явно вычислимые поля.
|
||||
2. Не нормализовать opaque-данные без уверенной спецификации.
|
||||
3. При roundtrip неизмененных данных требовать byte-identical результат.
|
||||
4. Для новых ассетов фиксировать отдельную политику «генерация vs preserve».
|
||||
|
||||
## 6. Runtime-связка материалов и текстур
|
||||
|
||||
Канонический путь резолва:
|
||||
|
||||
1. Модель -> wear-таблица (`*.wea`).
|
||||
2. Wear-слот -> material name.
|
||||
3. Material -> текущая фаза -> `textureName`.
|
||||
4. `Texm` ищется в `Textures.lib` (или lightmap-библиотеке для lightmap-ветки).
|
||||
|
||||
Fallback:
|
||||
|
||||
- материал: `DEFAULT`, затем индекс `0`;
|
||||
- текстура/lightmap: fallback-слот движка.
|
||||
|
||||
## 7. Что уже закрыто для 1:1
|
||||
|
||||
1. Бинарный контракт базовых MSH таблиц.
|
||||
2. Контракт animation sampling (`Res8 + Res19`).
|
||||
3. Контракт MAT0/WEAR/Texm на уровне чтения и применения в кадре.
|
||||
4. Формат FXID-контейнера, командный поток и fixed command sizes.
|
||||
5. Валидация на retail-корпусе legacy-валидатором (0 ошибок/предупреждений).
|
||||
|
||||
## 8. Статус покрытия и что осталось до 100%
|
||||
|
||||
1. Полная field-level семантика части служебных полей:
|
||||
- `Batch20` opaque-поля;
|
||||
- хвостовые служебные поля slot-записей;
|
||||
- часть флагов узлов/групп.
|
||||
2. Полный writer-путь для авторинга новых анимированных ассетов (не только roundtrip существующих).
|
||||
3. Полная формализация семантики FX payload полей по каждому opcode для генерации новых эффектов, а не только для корректного чтения/исполнения.
|
||||
4. Полный канонический writer `Texm` для всех редких форматов и edge-case комбинаций служебных флагов.
|
||||
5. Сквозной «импорт внешнего ассета -> игровой пакет» с формальной спецификацией sidecar-метаданных (материал/эффект/анимация).
|
||||
@@ -1,39 +0,0 @@
|
||||
# Форматы 3D-ресурсов движка NGI
|
||||
|
||||
Этот документ теперь является обзором и точкой входа в набор отдельных спецификаций.
|
||||
|
||||
## Структура спецификаций
|
||||
|
||||
1. [MSH core](msh-core.md) — геометрия, узлы, батчи, LOD, slot-матрица.
|
||||
2. [MSH animation](msh-animation.md) — `Res8`, `Res19`, выбор ключей и интерполяция.
|
||||
3. [Material (`MAT0`)](material.md) — формат материала и фазовая анимация.
|
||||
4. [Wear (`WEAR`)](wear.md) — текстовая таблица привязки материалов/lightmap.
|
||||
5. [Texture (`Texm`)](texture.md) — форматы текстур, mip-chain и `Page`.
|
||||
6. [FXID](fxid.md) — контейнер эффекта и поток команд.
|
||||
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](nres.md)
|
||||
- [RsLi](rsli.md)
|
||||
|
||||
## Принцип декомпозиции
|
||||
|
||||
- Форматы и контейнеры документируются отдельно, чтобы их можно было верифицировать и править независимо.
|
||||
- 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-контрактов.
|
||||
@@ -1,28 +0,0 @@
|
||||
# Network system
|
||||
|
||||
`Network` — подсистема синхронизации состояния игры между узлами (мультиплеер/обмен состоянием).
|
||||
|
||||
## 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-тестов на контролируемой потере/задержке.
|
||||
@@ -1,200 +0,0 @@
|
||||
# NRes
|
||||
|
||||
`NRes` — базовый контейнер ресурсов движка Parkan: Iron Strategy.
|
||||
Страница фиксирует формат на диске и runtime-контракт чтения/поиска/сохранения в высокоуровневом виде, без привязки к внутренним адресам и именам из дизассемблера.
|
||||
|
||||
Связанная страница:
|
||||
|
||||
- [RsLi](rsli.md)
|
||||
|
||||
## 1. Назначение
|
||||
|
||||
`NRes` используется как универсальный архив:
|
||||
|
||||
- 3D-модели (`*.msh`, `*.rlb`);
|
||||
- текстуры (`Texm`);
|
||||
- материалы (`MAT0`);
|
||||
- эффекты (`FXID`);
|
||||
- миссионные и служебные ресурсы.
|
||||
|
||||
Формат поддерживает:
|
||||
|
||||
- чтение;
|
||||
- поиск по имени;
|
||||
- редактирование (add/replace/remove);
|
||||
- полную пересборку архива.
|
||||
|
||||
## 2. Общий layout файла
|
||||
|
||||
```text
|
||||
[Header: 16]
|
||||
[Data region: variable, 8-byte aligned chunks]
|
||||
[Directory: entry_count * 64, всегда в конце файла]
|
||||
```
|
||||
|
||||
Критично: каталог всегда расположен в конце файла.
|
||||
|
||||
## 3. Заголовок (16 байт)
|
||||
|
||||
Все значения little-endian.
|
||||
|
||||
| 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_size = entry_count * 64`;
|
||||
- `directory_offset = total_size - directory_size`.
|
||||
|
||||
Ограничения:
|
||||
|
||||
- `directory_offset >= 16`;
|
||||
- `directory_offset + directory_size == total_size`.
|
||||
|
||||
## 4. Запись каталога (64 байта)
|
||||
|
||||
| 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` |
|
||||
|
||||
### 4.1. Имя ресурса (`name_raw`)
|
||||
|
||||
Контракт:
|
||||
|
||||
- максимум 35 полезных байт + NUL;
|
||||
- допускается ровно один терминатор внутри 36-байтового поля;
|
||||
- имя сравнивается регистронезависимо по ASCII-правилу (`A..Z` -> `a..z`).
|
||||
|
||||
Для writer/editor:
|
||||
|
||||
- запрещено писать NUL внутри полезной части имени;
|
||||
- запрещены имена длиной > 35 байт.
|
||||
|
||||
### 4.2. Диапазон данных (`data_offset`, `size`)
|
||||
|
||||
Для каждой записи:
|
||||
|
||||
- `data_offset >= 16`;
|
||||
- `data_offset + size <= directory_offset`.
|
||||
|
||||
Практически (канонический writer): каждый payload начинается с 8-байтного выравнивания.
|
||||
|
||||
## 5. Таблица сортировки (`sort_index`)
|
||||
|
||||
`sort_index` задает перестановку «отсортированный список -> исходный индекс записи».
|
||||
|
||||
Пусть:
|
||||
|
||||
- `entries[i]` — i-я запись каталога в исходном порядке;
|
||||
- `P` — массив индексов `0..entry_count-1`, отсортированный по `entries[idx].name` (ASCII case-insensitive).
|
||||
|
||||
Тогда в канонической записи:
|
||||
|
||||
- `entries[i].sort_index = P[i]`.
|
||||
|
||||
Это именно таблица для бинарного поиска по имени, а не «ранг текущей записи».
|
||||
|
||||
## 6. Поиск по имени
|
||||
|
||||
Алгоритм поиска:
|
||||
|
||||
1. Выполнить бинарный поиск по диапазону `i in [0, entry_count)`.
|
||||
2. На шаге `i` взять `target = entries[i].sort_index`.
|
||||
3. Сравнить искомое имя с `entries[target].name` (ASCII case-insensitive).
|
||||
4. При совпадении вернуть `target`.
|
||||
|
||||
Fail-safe поведение:
|
||||
|
||||
- если `sort_index` некорректен (выход за диапазон), реализация должна перейти на линейный fallback по всем записям;
|
||||
- fallback использует то же ASCII case-insensitive сравнение.
|
||||
|
||||
## 7. Каноническая пересборка архива
|
||||
|
||||
Канонический writer выполняет:
|
||||
|
||||
1. Пишет заглушку заголовка (16 байт).
|
||||
2. Пишет payload всех записей в текущем порядке.
|
||||
3. После каждого payload добавляет 0-padding до кратности 8.
|
||||
4. Пересчитывает `sort_index` через сортировку имен.
|
||||
5. Дописывает каталог (`entry_count * 64`).
|
||||
6. Пересчитывает и записывает `total_size`.
|
||||
|
||||
Итоговый файл должен удовлетворять всем ограничениям из разделов 3–5.
|
||||
|
||||
## 8. Режим `raw` (совместимость инструментов)
|
||||
|
||||
Для служебных инструментов допускается `raw_mode`:
|
||||
|
||||
- любой бинарный файл трактуется как один «сырой» ресурс;
|
||||
- возвращается одна запись (`name = RAW`, `data_offset = 0`, `size = len(file)`).
|
||||
|
||||
Этот режим не является форматом `NRes` на диске, это только режим открытия.
|
||||
|
||||
## 9. Контрольные инварианты
|
||||
|
||||
Минимальный набор проверок при чтении:
|
||||
|
||||
1. `magic == "NRes"`.
|
||||
2. `version == 0x100`.
|
||||
3. `entry_count >= 0`.
|
||||
4. `header.total_size == file_size`.
|
||||
5. Каталог находится в конце файла.
|
||||
6. Для каждой записи диапазон данных не пересекает каталог.
|
||||
7. Имя корректно C-терминировано и не длиннее 35 байт.
|
||||
|
||||
Минимальный набор проверок при записи:
|
||||
|
||||
1. Все имена <= 35 байт и без внутренних NUL.
|
||||
2. `sort_index` формирует валидную перестановку `0..N-1`.
|
||||
3. Все паддинги между payload состоят из нулевых байт.
|
||||
4. `total_size` равен фактической длине выходного файла.
|
||||
|
||||
## 10. Эмпирическая проверка на retail-корпусе
|
||||
|
||||
Валидация на полном наборе `testdata/Parkan - Iron Strategy`:
|
||||
|
||||
- найдено `120` архивов `NRes`;
|
||||
- roundtrip `unpack -> repack -> byte-compare`: `120/120` совпали побайтно;
|
||||
- критических расхождений формата не обнаружено.
|
||||
|
||||
Проверено legacy-валидатором архивов.
|
||||
|
||||
## 11. Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
- формат заголовка/каталога;
|
||||
- правила поиска;
|
||||
- каноническая пересборка;
|
||||
- строгие инварианты валидатора;
|
||||
- побайтовый roundtrip на retail-корпусе.
|
||||
|
||||
Осталось до полного 100% архитектурного покрытия движка:
|
||||
|
||||
1. Формальная семантика `attr1/attr2/attr3` для всех типов ресурсов (частично вынесена в профильные страницы `msh`, `material`, `texture`, `fxid`, `terrain`).
|
||||
2. Полная спецификация поведения при не-ASCII именах (в реальных игровых архивах используется ASCII-практика; для Unicode-коллации движок не документирован).
|
||||
3. Полная спецификация платформенных гарантий атомарной записи (формат данных закрыт, но OS-уровневые гарантии замены файла зависят от платформы и файловой системы).
|
||||
## 12. Специализация `objects.rlb`
|
||||
|
||||
Хотя `objects.rlb` формально является обычным `NRes`, его payload имеет отдельный семантический контракт:
|
||||
|
||||
- запись каталога соответствует одному объектному прототипу;
|
||||
- payload записи - массив фиксированных ссылок `ObjectRef64` (`archive_name[32] + resource_name[32]`);
|
||||
- runtime-резолв меша выполняется через эти ссылки, а не через имя entry `*.msh` внутри `objects.rlb`.
|
||||
|
||||
Это означает, что `objects.rlb` должен рассматриваться не как архив мешей, а как реестр привязок между mission/unit-ключами и фактическими ресурсами.
|
||||
|
||||
См. детальную страницу:
|
||||
|
||||
- [Object registry (`objects.rlb`)](object-registry.md)
|
||||
@@ -1,145 +0,0 @@
|
||||
# Object Registry (`objects.rlb`)
|
||||
|
||||
`objects.rlb` - это не архив с готовыми мешами.
|
||||
Это реестр игровых прототипов, который связывает логический идентификатор объекта (`r_h_01`, `s_tree_04`, `fr_m_brige`, ...) с набором реальных ресурсов в других архивах.
|
||||
|
||||
Документ описывает формат и runtime-контракт на высоком уровне, без привязки к внутренним именам/адресам из дизассемблера.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [Missions](missions.md)
|
||||
- [NRes](nres.md)
|
||||
- [MSH core](msh-core.md)
|
||||
- [Wear (`WEAR`)](wear.md)
|
||||
- [Material (`MAT0`)](material.md)
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
## 1. Роль в пайплайне
|
||||
|
||||
При загрузке миссии движок работает так:
|
||||
|
||||
1. Из `data.tma` получает `resource_name` объекта:
|
||||
- либо прямой ключ (`s_tree_04`);
|
||||
- либо путь к `*.dat` (например `UNITS\\UNITS\\HERO\\tut1_p.dat`).
|
||||
2. Для `*.dat` читает заголовок и получает:
|
||||
- `archive_name` (в retail-корпусе всегда `objects.rlb`);
|
||||
- `model_key` (например `R_H_02`).
|
||||
3. В `objects.rlb` по ключу (`model_key`/`resource_name`) ищет запись прототипа.
|
||||
4. Из записи прототипа резолвит фактический `*.msh` и архив, где лежит геометрия.
|
||||
5. Дальше запускается стандартная цепочка:
|
||||
`MSH -> WEAR -> MAT0 -> Texm`.
|
||||
|
||||
## 2. Контейнер
|
||||
|
||||
`objects.rlb` сам является обычным `NRes`-архивом.
|
||||
|
||||
Практические наблюдения на retail-корпусе:
|
||||
|
||||
- формат заголовка/каталога полностью совпадает с `NRes`;
|
||||
- payload каждой записи прототипа кратен `64` байтам;
|
||||
- имя entry в каталоге - это логический ключ объекта (например `r_h_01`, `s_tree_04`).
|
||||
|
||||
## 3. Формат payload записи прототипа
|
||||
|
||||
Payload состоит из массива фиксированных записей:
|
||||
|
||||
```c
|
||||
struct ObjectRef64 {
|
||||
char archive_name[32]; // C-строка (CP1251/ASCII)
|
||||
char resource_name[32]; // C-строка (CP1251/ASCII)
|
||||
}
|
||||
```
|
||||
|
||||
Интерпретация:
|
||||
|
||||
- `archive_name`: архив-источник (`bases.rlb`, `static.rlb`, `fortif.rlb`, `effects.rlb`, ...).
|
||||
- `resource_name`: имя ресурса в этом архиве (`*.msh`, `*.wea`, `*.cpt`, `*.ctl`, `*.bas`, ...).
|
||||
|
||||
Важно:
|
||||
|
||||
- после первого `NUL` в 32-байтовом поле могут встречаться служебные байты; для runtime-резолва используется только C-строка до первого `NUL`;
|
||||
- неизвестные хвостовые байты должны сохраняться 1:1 при writer/roundtrip-редактировании.
|
||||
|
||||
## 4. Runtime-резолв геометрии
|
||||
|
||||
Канонический порядок выбора меша:
|
||||
|
||||
1. Найти запись прототипа по ключу в `objects.rlb`.
|
||||
2. Прочитать список `ObjectRef64`.
|
||||
3. Если есть ссылка на `*.msh`:
|
||||
- взять первую валидную ссылку;
|
||||
- открыть указанный архив;
|
||||
- загрузить этот `*.msh`.
|
||||
4. Если `*.msh` нет, но есть `*.bas`:
|
||||
- взять stem от `*.bas` (`fr_m_brige.bas` -> `fr_m_brige`);
|
||||
- искать `<stem>.msh` в том же архиве (`fortif.rlb`).
|
||||
5. Если нет ни `*.msh`, ни `*.bas`, объект трактуется как не-геометрический (пример: солнечный/системный объект) и в 3D-проход не попадает.
|
||||
|
||||
## 5. Типовые примеры
|
||||
|
||||
`r_h_01`:
|
||||
|
||||
- `bases.rlb :: r_h_01.msh`
|
||||
- `bases.rlb :: r_h_01.wea`
|
||||
- `bases.rlb :: r_h_01.cpt`
|
||||
- ...
|
||||
|
||||
`s_tree_04`:
|
||||
|
||||
- `static.rlb :: s_tree_0_04.msh`
|
||||
- `static.rlb :: s_tree_0_04.wea`
|
||||
- ...
|
||||
|
||||
`fr_m_brige`:
|
||||
|
||||
- прямого `*.msh` в записи нет;
|
||||
- есть `fortif.rlb :: fr_m_brige.bas`;
|
||||
- меш резолвится как `fortif.rlb :: fr_m_brige.msh`.
|
||||
|
||||
`sun_01`:
|
||||
|
||||
- ссылки на `*.sun`/effect-ресурсы;
|
||||
- 3D-меш отсутствует.
|
||||
|
||||
## 6. Инварианты для reader/writer
|
||||
|
||||
Reader:
|
||||
|
||||
- payload записи прототипа должен быть кратен `64`;
|
||||
- каждая запись читается как две независимые C-строки фиксированной длины;
|
||||
- поиск в архивах должен быть case-insensitive по ASCII.
|
||||
|
||||
Writer/editor:
|
||||
|
||||
- сохранять порядок `ObjectRef64` без перестановок;
|
||||
- сохранять неизвестные служебные байты полей 1:1;
|
||||
- не нормализовать имена, если это не требуется задачей.
|
||||
|
||||
## 7. Валидация
|
||||
|
||||
Проверено на retail-корпусе `testdata/Parkan - Iron Strategy`:
|
||||
|
||||
- все `590` записей `objects.rlb` имеют payload, кратный `64`;
|
||||
- `554` записей имеют прямую ссылку на `*.msh`;
|
||||
- `34` записи используют ветку через `*.bas`;
|
||||
- `2` записи не содержат геометрии (системные/sun).
|
||||
|
||||
Интеграционные тесты в Rust подтверждают резолв:
|
||||
|
||||
- `r_h_01 -> bases.rlb :: r_h_01.msh`
|
||||
- `s_tree_04 -> static.rlb :: s_tree_0_04.msh`
|
||||
- `fr_m_brige -> fortif.rlb :: fr_m_brige.msh`
|
||||
|
||||
## 8. Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
1. Формат payload записи прототипа (`ObjectRef64`) и правила чтения.
|
||||
2. Runtime-алгоритм выбора меша (`*.msh` напрямую и fallback через `*.bas`).
|
||||
3. Корпусная проверка структуры и интеграционные тесты резолва.
|
||||
|
||||
Осталось:
|
||||
|
||||
1. Полная field-level семантика служебных байтов после `NUL` в `resource_name[32]`.
|
||||
2. Формальная семантика всех категорий ссылок (`*.ctl`, `*.cpt`, `*.ndp`, `*.sun`) в терминах систем движка (не только render-пути).
|
||||
3. Writer-спецификация уровня "authoring new prototype from scratch" с гарантией runtime-паритета.
|
||||
@@ -1,90 +0,0 @@
|
||||
# Рендер-паритет (кадровый diff)
|
||||
|
||||
Документ описывает процесс проверки соответствия рендера:
|
||||
`оригинальный движок -> эталонный кадр -> render-demo -> diff-метрики`.
|
||||
|
||||
## Цель
|
||||
|
||||
- Зафиксировать объективный критерий "паритет достигнут / не достигнут".
|
||||
- Убрать субъективную визуальную оценку "похоже/не похоже".
|
||||
- Дать CI-проверку, которая ловит регрессии сразу после коммита.
|
||||
|
||||
## Единица проверки
|
||||
|
||||
Один тест-кейс = один объект (одна модель) + фиксированная конфигурация:
|
||||
|
||||
- архив ресурса;
|
||||
- имя модели;
|
||||
- `lod`;
|
||||
- `group`;
|
||||
- размер кадра (`width`, `height`);
|
||||
- угол камеры (`angle`);
|
||||
- PNG-эталон из оригинального рендера.
|
||||
|
||||
## Инварианты детерминизма
|
||||
|
||||
Для корректного сравнения кадры должны быть сняты в одинаковых условиях:
|
||||
|
||||
- одинаковый FOV и расстояние камеры до объекта;
|
||||
- одинаковый clear-color/фон;
|
||||
- одинаковые `lod/group`;
|
||||
- фиксированный угол (`angle`), без анимации;
|
||||
- фиксированное разрешение.
|
||||
|
||||
## Метрики сравнения
|
||||
|
||||
Сравнение выполняется по RGB-каналам:
|
||||
|
||||
- `mean_abs`: средняя абсолютная разница канала (0..255);
|
||||
- `max_abs`: максимальная разница канала;
|
||||
- `changed_ratio`: доля пикселей, где хотя бы один канал превышает `diff_threshold`.
|
||||
|
||||
Кейс считается пройденным, если:
|
||||
|
||||
- `mean_abs <= max_mean_abs`;
|
||||
- `changed_ratio <= max_changed_ratio`.
|
||||
|
||||
## Конфигурация кейсов
|
||||
|
||||
Файл: `parity/cases.toml`.
|
||||
|
||||
- секция `[meta]`: глобальные дефолты;
|
||||
- `[[case]]`: параметры конкретной модели и путь к эталонному PNG.
|
||||
|
||||
Эталонные кадры хранятся в `parity/reference/`.
|
||||
|
||||
## Локальный запуск
|
||||
|
||||
```bash
|
||||
cargo run -p render-parity -- \
|
||||
--manifest parity/cases.toml \
|
||||
--output-dir target/render-parity/current
|
||||
```
|
||||
|
||||
При расхождении утилита пишет diff-изображение в:
|
||||
|
||||
- `target/render-parity/current/diff/<case>.png`
|
||||
|
||||
## CI-модель
|
||||
|
||||
CI запускает `render-parity` на каждом push/PR:
|
||||
|
||||
1. собирает `parkan-render-demo`;
|
||||
2. прогоняет кейсы из `cases.toml`;
|
||||
3. при падении публикует текущие кадры и diff как артефакт.
|
||||
|
||||
Важно: оригинальный движок в CI обычно не запускается.
|
||||
Эталонные PNG снимаются офлайн и версионируются в репозитории.
|
||||
|
||||
## Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
1. Определена метрика сравнения кадров (`mean_abs`, `max_abs`, `changed_ratio`).
|
||||
2. Описан единый manifest-формат кейсов и CI-процедура.
|
||||
|
||||
Осталось:
|
||||
|
||||
1. Снять и зафиксировать расширенный эталонный набор кадров оригинала (10-20+ ключевых моделей и режимов).
|
||||
2. Зафиксировать пороговые критерии pass/fail по каждому классу сцен (статик, анимация, FX, lightmap).
|
||||
3. Добавить автоматическую публикацию diff-артефактов и регрессионных отчетов в CI.
|
||||
@@ -1,182 +0,0 @@
|
||||
# Render pipeline
|
||||
|
||||
Документ описывает полный процесс рендера кадра в движке Parkan: Iron Strategy, без привязки к внутренним адресам/именам дизассемблера.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [MSH core](msh-core.md)
|
||||
- [MSH animation](msh-animation.md)
|
||||
- [Material (`MAT0`)](material.md)
|
||||
- [Wear table (`WEAR`)](wear.md)
|
||||
- [Texture (`Texm`)](texture.md)
|
||||
- [FXID](fxid.md)
|
||||
|
||||
## 1. Инициализация рендера
|
||||
|
||||
На старте движок:
|
||||
|
||||
1. Выбирает видеодрайвер (software или аппаратный).
|
||||
2. Создаёт render backend.
|
||||
3. Подключает библиотеки ресурсов:
|
||||
- `Material.lib`
|
||||
- `Textures.lib`
|
||||
- `LightMap.lib`
|
||||
- `palettes.lib`
|
||||
4. Инициализирует менеджеры:
|
||||
- material manager
|
||||
- texture/lightmap cache
|
||||
- effect manager
|
||||
5. Загружает базовые world-ресурсы (включая наборы объектов сцены).
|
||||
|
||||
## 2. Структура кадра
|
||||
|
||||
Кадр выполняется как последовательность:
|
||||
|
||||
1. `Simulation update`
|
||||
2. `Animation sampling`
|
||||
3. `Visibility / culling`
|
||||
4. `Material + texture resolve`
|
||||
5. `Mesh draw`
|
||||
6. `FX update + draw`
|
||||
7. `UI/overlay draw`
|
||||
8. `Present`
|
||||
|
||||
## 3. Geometry path
|
||||
|
||||
### 3.1. Подготовка инстансов
|
||||
|
||||
Для каждого видимого объекта:
|
||||
|
||||
1. Вычисляется `world transform`.
|
||||
2. Выбирается `LOD`.
|
||||
3. Для каждого узла выбирается slot через `Res1`.
|
||||
|
||||
### 3.2. Culling
|
||||
|
||||
Сначала отсекаются узлы/слоты по bounds (`AABB/sphere`) из `Res2`.
|
||||
|
||||
### 3.3. Батчи
|
||||
|
||||
Для каждого прошедшего slot:
|
||||
|
||||
1. Берутся батчи из диапазона `Res13`.
|
||||
2. По `materialIndex` выбирается активный материал.
|
||||
3. По фазе материала выбирается текстура/lightmap.
|
||||
4. Выполняется `DrawIndexedPrimitive`:
|
||||
- индексный диапазон: `indexStart/indexCount`
|
||||
- базовая вершина: `baseVertex`
|
||||
- индексы читаются из `Res6`
|
||||
- вершины/атрибуты читаются из `Res3/Res4/Res5` (+ optional streams)
|
||||
|
||||
## 4. Animation path
|
||||
|
||||
Для анимированных моделей:
|
||||
|
||||
1. Для узла выбирается ключ через `Res19` и fallback-логику.
|
||||
2. Декодируются `pos + quat` из `Res8`.
|
||||
3. При необходимости выполняется blending двух сэмплов.
|
||||
4. Узловая матрица передаётся в geometry path.
|
||||
|
||||
## 5. Material path
|
||||
|
||||
Material pipeline на кадре:
|
||||
|
||||
1. По material handle выбирается запись `MAT0`.
|
||||
2. По игровому времени выбирается текущая фаза.
|
||||
3. Применяются коэффициенты фазы (цвет/альфа/параметры).
|
||||
4. Резолвятся ссылки на texture/lightmap.
|
||||
5. Невалидные ссылки обрабатываются fallback-стратегией.
|
||||
|
||||
Практическая цепочка привязки для большинства `*.msh` ассетов из `*.rlb`:
|
||||
|
||||
1. Для модели выбирается одноимённый `WEAR` (`<model_stem>.wea`).
|
||||
2. Из `WEAR` берётся material-слот (по имени, `legacyId` не участвует в выборе).
|
||||
3. В `Material.lib` ищется `MAT0` по имени (`DEFAULT`, затем индекс `0` как fallback).
|
||||
4. Из выбранной material-фазы берётся `textureName`.
|
||||
5. `Texm` ищется в `Textures.lib` (и/или lightmap-архиве для lightmap-ветки).
|
||||
|
||||
## 6. Texture path
|
||||
|
||||
При резолве текстуры:
|
||||
|
||||
1. Ищется `Texm` entry по имени.
|
||||
2. Проверяется и декодируется заголовок.
|
||||
3. При необходимости применяется `mipSkip`.
|
||||
4. Для indexed-формата подключается палитра.
|
||||
5. Optional `Page` chunk интерпретируется как atlas-таблица.
|
||||
6. Объект текстуры кладётся/берётся из cache.
|
||||
|
||||
## 7. FX path
|
||||
|
||||
Эффекты выполняются параллельно mesh-рендеру:
|
||||
|
||||
1. Для активных инстансов FX вычисляется runtime-коэффициент (`time_mode + flags`).
|
||||
2. Команды FX обновляют внутреннее состояние.
|
||||
3. Команды emit-этапа формируют примитивы/батчи эффектов.
|
||||
4. Эффекты рисуются в 3D-кадре с собственным счётчиком батчей.
|
||||
|
||||
## 8. Псевдокод кадра
|
||||
|
||||
```c
|
||||
void RenderFrame(Scene* scene, Camera* cam, float dt) {
|
||||
UpdateGame(scene, dt);
|
||||
|
||||
for (Object* obj : scene->objects) {
|
||||
if (!obj->visible) continue;
|
||||
|
||||
UpdateObjectAnimation(obj, scene->time);
|
||||
BuildObjectNodeTransforms(obj);
|
||||
}
|
||||
|
||||
BeginFrame(cam);
|
||||
|
||||
for (Object* obj : scene->objects) {
|
||||
if (!obj->visible) continue;
|
||||
RenderObjectMeshes(obj, cam);
|
||||
}
|
||||
|
||||
UpdateAndRenderFx(scene, dt, cam);
|
||||
RenderUI(scene);
|
||||
Present();
|
||||
}
|
||||
```
|
||||
|
||||
## 9. Критичные условия для 1:1
|
||||
|
||||
1. Та же политика округления/FP для анимации и FX.
|
||||
2. Та же логика fallback по материалам и текстурам.
|
||||
3. Та же очередность стадий кадра.
|
||||
4. Тот же контракт интерпретации `Res1/Res2/Res13/Res6`.
|
||||
5. Тот же контракт `FXID` командного потока.
|
||||
|
||||
## 10. Статус валидации
|
||||
|
||||
- Порядок кадра и подключение `Material.lib / Textures.lib / LightMap.lib` подтверждены текущей runtime-валидацией проекта.
|
||||
- Детальные инварианты форматов зафиксированы в спецификациях проекта и проверены legacy-валидаторами.
|
||||
|
||||
## 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-комбинации).
|
||||
|
||||
## 12. Object registry bridge (`objects.rlb`)
|
||||
|
||||
Для миссионного/юнитного рендера критично учитывать промежуточный слой прототипов:
|
||||
|
||||
1. `TMA`/`*.dat` обычно дают не прямой `*.msh`, а ключ прототипа.
|
||||
2. Ключ резолвится через `objects.rlb` (реестр ссылок на реальные архивы ресурсов).
|
||||
3. Только после этого выполняется стандартный путь:
|
||||
`MSH -> WEAR -> MAT0 -> Texm`.
|
||||
|
||||
Детальная спецификация этого шага вынесена в отдельную страницу:
|
||||
|
||||
- [Object registry (`objects.rlb`)](object-registry.md)
|
||||
@@ -1,227 +0,0 @@
|
||||
# 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`).
|
||||
|
||||
Проверено legacy-валидатором архивов и тестами `crates/rsli`.
|
||||
|
||||
## 11. Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
- формат заголовка/таблицы;
|
||||
- XOR-алгоритм;
|
||||
- все используемые методы декодирования;
|
||||
- AO overlay;
|
||||
- lookup-поиск и fallback;
|
||||
- retail-валидация и побайтовый roundtrip.
|
||||
|
||||
Осталось до полного 100% архитектурного покрытия движка:
|
||||
|
||||
1. Полная функциональная семантика битов `flags` вне маски метода (`0x1E0`) для геймплейных подсистем.
|
||||
2. Канонический writer для авторинга новых архивов со стабильной стратегией выбора методов (`0x080/0x0A0/0x100`) и параметров компрессии.
|
||||
3. Формализация поведения для не-ASCII имен (на практике архивы используют ASCII-диапазон).
|
||||
@@ -1,18 +0,0 @@
|
||||
# Runtime pipeline
|
||||
|
||||
Актуальный документ по полному кадру находится здесь:
|
||||
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
Эта страница оставлена как совместимый указатель для старых ссылок.
|
||||
|
||||
## Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
1. Актуальный runtime-пайплайн централизован в `render.md`.
|
||||
|
||||
Осталось:
|
||||
|
||||
1. Поддерживать обратную совместимость ссылок при дальнейшей декомпозиции render-документа.
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
# Sound system
|
||||
|
||||
`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-тестов (тайминг/громкость/панорама).
|
||||
@@ -1,291 +0,0 @@
|
||||
# Terrain + ArealMap
|
||||
|
||||
Документ описывает подсистему ландшафта и ареалов мира в движке Parkan: Iron Strategy:
|
||||
|
||||
- `Land.msh` (terrain-геометрия и вспомогательные таблицы);
|
||||
- `Land.map` (ареалы и навигационные связи);
|
||||
- `BuildDat.lst` (категории объектных зон).
|
||||
|
||||
Описание дано в высокоуровневом переносимом виде, без ссылок на внутренние адреса и имена из дизассемблера.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [NRes](nres.md)
|
||||
- [RsLi](rsli.md)
|
||||
- [MSH core](msh-core.md)
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
## 1. End-to-End загрузка уровня
|
||||
|
||||
Для каждой карты движок загружает пару файлов:
|
||||
|
||||
- `.../Land.msh`
|
||||
- `.../Land.map`
|
||||
|
||||
Высокоуровневый порядок:
|
||||
|
||||
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`.
|
||||
|
||||
## 2. Формат `Land.msh`
|
||||
|
||||
`Land.msh` — обычный `NRes` архив с фиксированным набором terrain-ресурсов.
|
||||
|
||||
## 2.1. Состав chunk'ов
|
||||
|
||||
Обязательные типы:
|
||||
|
||||
- `1`, `2`, `3`, `4`, `5`, `11`, `18`, `21`
|
||||
|
||||
Опциональные типы:
|
||||
|
||||
- `14`
|
||||
|
||||
Наблюдаемый retail-порядок chunk'ов:
|
||||
|
||||
```text
|
||||
[1, 2, 3, 4, 5, 18, 14, 11, 21]
|
||||
```
|
||||
|
||||
## 2.2. Stride и атрибуты
|
||||
|
||||
| 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 |
|
||||
|
||||
Общее правило для этих chunk'ов:
|
||||
|
||||
- `attr1 == size / stride`
|
||||
- `attr3 == stride`
|
||||
|
||||
## 2.3. Type `2`: slot table
|
||||
|
||||
`type=2` содержит:
|
||||
|
||||
- заголовок `0x8C` байт;
|
||||
- затем таблицу slots по `68` байт.
|
||||
|
||||
Инварианты:
|
||||
|
||||
- `size >= 0x8C`
|
||||
- `(size - 0x8C) % 68 == 0`
|
||||
- `attr1 == (size - 0x8C) / 68`
|
||||
- `attr3 == 68`
|
||||
|
||||
## 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 |
|
||||
|---:|---:|
|
||||
| `0x00000001` | `0x0001` |
|
||||
| `0x00000008` | `0x0002` |
|
||||
| `0x00000010` | `0x0004` |
|
||||
| `0x00000020` | `0x0008` |
|
||||
| `0x00001000` | `0x0010` |
|
||||
| `0x00004000` | `0x0020` |
|
||||
| `0x00000002` | `0x0040` |
|
||||
| `0x00000400` | `0x0080` |
|
||||
| `0x00000800` | `0x0100` |
|
||||
| `0x00020000` | `0x0200` |
|
||||
| `0x00002000` | `0x0400` |
|
||||
| `0x00000200` | `0x0800` |
|
||||
| `0x00000004` | `0x1000` |
|
||||
| `0x00000040` | `0x2000` |
|
||||
| `0x00200000` | `0x8000` |
|
||||
|
||||
Подтвержденный remap `full -> compactMaterial6`:
|
||||
|
||||
| Full bit | Compact bit |
|
||||
|---:|---:|
|
||||
| `0x00000100` | `0x01` |
|
||||
| `0x00008000` | `0x02` |
|
||||
| `0x00010000` | `0x04` |
|
||||
| `0x00040000` | `0x08` |
|
||||
| `0x00080000` | `0x10` |
|
||||
| `0x00000080` | `0x20` |
|
||||
|
||||
Для 1:1 реализации нужно поддерживать оба представления и обратное восстановление `compact -> full`.
|
||||
|
||||
## 2.6. Type `11` и cell-ускоритель terrain
|
||||
|
||||
`type=11` служит источником cell-ускорителя для terrain-запросов.
|
||||
|
||||
Практические требования для editor/toolchain:
|
||||
|
||||
- не переупорядочивать содержимое без полного пересчета зависимых таблиц;
|
||||
- сохранять служебные/неизвестные поля побайтно;
|
||||
- выполнять валидацию диапазонов face/slot после любых правок.
|
||||
|
||||
## 3. Формат `Land.map` (chunk `type=12`)
|
||||
|
||||
`Land.map` — `NRes`, содержащий ровно один ресурс `type=12`.
|
||||
|
||||
Контракт верхнего уровня:
|
||||
|
||||
- `entry.attr1` = `areal_count`;
|
||||
- payload включает:
|
||||
- `areal_count` переменных записей ареалов;
|
||||
- затем grid-секцию cell-попаданий.
|
||||
|
||||
## 3.1. Запись ареала
|
||||
|
||||
Старт записи:
|
||||
|
||||
```c
|
||||
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
|
||||
```
|
||||
|
||||
Далее:
|
||||
|
||||
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
|
||||
|
||||
Для `links[0 .. vertex_count-1]`:
|
||||
|
||||
- `(-1, -1)` означает «соседа нет»;
|
||||
- иначе `area_ref` указывает на индекс соседнего ареала, `edge_ref` — на ребро в соседнем ареале.
|
||||
|
||||
## 3.3. Grid-секция после ареалов
|
||||
|
||||
Формат:
|
||||
|
||||
```c
|
||||
uint32 cellsX;
|
||||
uint32 cellsY;
|
||||
for (x=0; x<cellsX; x++) {
|
||||
for (y=0; y<cellsY; y++) {
|
||||
uint16 hitCount;
|
||||
uint16 areaIds[hitCount];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
В runtime существует упакованное cell-meta представление:
|
||||
|
||||
- high 10 бит: `hitCount`;
|
||||
- low 22 бита: `startIndex` (в общем `areaIds` пуле).
|
||||
|
||||
## 3.4. Валидация целостности chunk 12
|
||||
|
||||
Обязательные проверки:
|
||||
|
||||
- `areal_count > 0`;
|
||||
- `cellsX > 0 && cellsY > 0`;
|
||||
- каждый `area_id` из cell-списков `< areal_count`;
|
||||
- все `area_ref/edge_ref` валидны относительно целевых ареалов;
|
||||
- полный объем прочитанных байт должен точно совпасть с размером payload.
|
||||
|
||||
## 4. `BuildDat.lst`
|
||||
|
||||
Используются 12 объектных категорий ареалов:
|
||||
|
||||
| Имя | Маска |
|
||||
|---|---:|
|
||||
| `Bunker_Small` | `0x80010000` |
|
||||
| `Bunker_Medium` | `0x80020000` |
|
||||
| `Bunker_Large` | `0x80040000` |
|
||||
| `Generator` | `0x80000002` |
|
||||
| `Mine` | `0x80000004` |
|
||||
| `Storage` | `0x80000008` |
|
||||
| `Plant` | `0x80000010` |
|
||||
| `Hangar` | `0x80000040` |
|
||||
| `MainTeleport` | `0x80000200` |
|
||||
| `Institute` | `0x80000400` |
|
||||
| `Tower_Medium` | `0x80100000` |
|
||||
| `Tower_Large` | `0x80200000` |
|
||||
|
||||
Файл должен парситься строго секционно; поврежденный формат считается ошибкой.
|
||||
|
||||
## 5. Требования к reader/writer/editor
|
||||
|
||||
1. Сохранять порядок и бинарную форму chunk'ов, если не выполняется осознанная нормализация.
|
||||
2. Все неизвестные поля хранить и писать побайтно (`preserve-as-is`).
|
||||
3. После правок пересчитывать только вычислимые поля, не «чистить» opaque-данные.
|
||||
4. Проверять диапазоны индексов между связанными таблицами (`nodes/slots/faces/vertices/areas/cells`).
|
||||
5. Для неизмененных ресурсов обеспечивать byte-identical roundtrip.
|
||||
|
||||
## 6. Эмпирическая верификация (retail)
|
||||
|
||||
Валидация на `testdata/Parkan - Iron Strategy`:
|
||||
|
||||
- карт: `33`
|
||||
- `Land.msh`: `33/33` валидны
|
||||
- `Land.map`: `33/33` валидны
|
||||
- `issues_total = 0`, `errors_total = 0`, `warnings_total = 0`
|
||||
|
||||
Подтвержденные наблюдения:
|
||||
|
||||
- `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`.
|
||||
|
||||
Проверено legacy-валидатором terrain/map форматов.
|
||||
|
||||
## 7. Статус покрытия и что осталось до 100%
|
||||
|
||||
Закрыто:
|
||||
|
||||
- бинарный контракт `Land.msh` и `Land.map`;
|
||||
- диапазонные и структурные инварианты;
|
||||
- remap масок `full/compact`;
|
||||
- валидация на полном retail-корпусе карт.
|
||||
|
||||
Осталось до полного 100% архитектурного покрытия движка:
|
||||
|
||||
1. Полная доменная семантика `class_id` и `logic_flag` (игровые значения/поведенческие правила).
|
||||
2. Полная спецификация ветки `poly_count > 0` на живых данных (в retail не встречена).
|
||||
3. Полная field-level семантика части битов `TerrainFace28.flags` (бинарный контракт и remap закрыты, но не все биты имеют документированные геймплейные имена).
|
||||
@@ -1,153 +0,0 @@
|
||||
# Texture (`Texm`)
|
||||
|
||||
`Texm` — основной формат текстур движка.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [Material (`MAT0`)](material.md)
|
||||
- [Wear table (`WEAR`)](wear.md)
|
||||
- [Render pipeline](render.md)
|
||||
|
||||
## 1. Контейнер
|
||||
|
||||
- Тип ресурса: `0x6D786554` (`Texm`).
|
||||
- Используется в `Textures.lib`, `LightMap.lib` и других `NRes` архивах.
|
||||
|
||||
## 2. Заголовок
|
||||
|
||||
```c
|
||||
struct TexmHeader32 {
|
||||
uint32_t magic; // 'Texm'
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t mipCount;
|
||||
uint32_t flags4;
|
||||
uint32_t flags5;
|
||||
uint32_t unk6;
|
||||
uint32_t format;
|
||||
};
|
||||
```
|
||||
|
||||
## 3. Поддерживаемые форматы
|
||||
|
||||
Базовые форматы:
|
||||
|
||||
- `0` (8-bit indexed + palette)
|
||||
- `565`
|
||||
- `4444`
|
||||
- `888`
|
||||
- `8888`
|
||||
|
||||
Дополнительные ветки загрузки поддерживают также `556` и `88`.
|
||||
|
||||
## 4. Layout payload
|
||||
|
||||
1. `TexmHeader32` (32 байта)
|
||||
2. palette `1024` байта, если `format == 0`
|
||||
3. mip-chain пикселей
|
||||
4. optional `Page` chunk
|
||||
|
||||
Расчёт ядра:
|
||||
|
||||
```c
|
||||
bytesPerPixel =
|
||||
(format == 0) ? 1 :
|
||||
(format == 565 || format == 556 || format == 4444 || format == 88) ? 2 :
|
||||
4;
|
||||
|
||||
pixelCount = sum(max(1, width>>i) * max(1, height>>i), i=0..mipCount-1);
|
||||
sizeCore = 32 + (format==0 ? 1024 : 0) + bytesPerPixel * pixelCount;
|
||||
```
|
||||
|
||||
## 4.1. Декодирование в RGBA8 (runtime/инструменты)
|
||||
|
||||
Для CPU-пути (preview, валидация, оффлайн-конвертация) используется декодирование:
|
||||
|
||||
- `0` (`Indexed8`): `index -> palette[index]` (`RGBA` из палитры 256×4).
|
||||
- `565`: `R5 G6 B5`, `A=255`.
|
||||
- `556`: `R5 G5 B6`, `A=255`.
|
||||
- `4444`: `A4 R4 G4 B4` (с расширением 4-битных каналов в 8-битные).
|
||||
- `88`: `L8 A8` (`R=G=B=L`).
|
||||
- `888`: `R8 G8 B8` + padding/служебный байт, `A=255`.
|
||||
- `8888`: `A8 R8 G8 B8`.
|
||||
|
||||
Это декодирование соответствует текущему test/demo pipeline проекта.
|
||||
|
||||
## 5. `Page` chunk
|
||||
|
||||
```c
|
||||
struct PageChunk {
|
||||
uint32_t magic; // 'Page'
|
||||
uint32_t rectCount;
|
||||
Rect16 rects[rectCount];
|
||||
};
|
||||
|
||||
struct Rect16 {
|
||||
int16_t x;
|
||||
int16_t w;
|
||||
int16_t y;
|
||||
int16_t h;
|
||||
};
|
||||
```
|
||||
|
||||
`Page` задаёт atlas-прямоугольники для выборки под-областей текстуры.
|
||||
|
||||
## 6. Mip-skip политика
|
||||
|
||||
Загрузчик может пропускать первые mip-уровни в зависимости от:
|
||||
|
||||
- `flags5`,
|
||||
- размеров текстуры,
|
||||
- количества mip.
|
||||
|
||||
После `mipSkip`:
|
||||
|
||||
- уменьшаются `width/height/mipCount`;
|
||||
- сдвигается начало пиксельных данных;
|
||||
- `Page`-координаты пересчитываются в соответствии с новым базовым уровнем.
|
||||
|
||||
## 7. Палитры
|
||||
|
||||
Для части текстур движок связывает палитру по суффиксу имени.
|
||||
|
||||
Практический формат:
|
||||
|
||||
- буква `A..Z` + вариант `""` или `0..9`
|
||||
- всего `26 * 11 = 286` возможных слотов палитр.
|
||||
|
||||
Невалидные суффиксы нужно считать ошибкой входных данных в инструментах.
|
||||
|
||||
## 8. Кэширование
|
||||
|
||||
Движок ведёт отдельные кэши:
|
||||
|
||||
- общий texture cache;
|
||||
- lightmap cache.
|
||||
|
||||
Для обычных текстур используется отложенный сбор неиспользуемых слотов (по времени нулевого refcount).
|
||||
|
||||
## 9. Правила writer/editor
|
||||
|
||||
1. Не нормализовать `flags4/flags5/unk6`.
|
||||
2. Сохранять payload без лишних хвостовых байт.
|
||||
3. Если есть `Page`, его размер должен быть ровно `8 + rectCount * 8`.
|
||||
4. Проверять `width > 0`, `height > 0`, `mipCount > 0`.
|
||||
|
||||
## 10. Статус валидации
|
||||
|
||||
- Инварианты `Texm` проверены legacy-валидатором.
|
||||
- На полном 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 веток.
|
||||
@@ -1,33 +0,0 @@
|
||||
# UI system
|
||||
|
||||
`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-тесты (скриншотные и событийные).
|
||||
@@ -1,96 +0,0 @@
|
||||
# Wear table (`WEAR`)
|
||||
|
||||
`WEAR` — текстовый ресурс, который связывает слоты wear с именами материалов и lightmap.
|
||||
|
||||
Связанные страницы:
|
||||
|
||||
- [Material (`MAT0`)](material.md)
|
||||
- [Texture (`Texm`)](texture.md)
|
||||
|
||||
## 1. Контейнер
|
||||
|
||||
- Тип ресурса: `0x52414557` (`WEAR`).
|
||||
- Обычно хранится как `*.wea` внутри world/mission архивов.
|
||||
|
||||
## 2. Формат текста
|
||||
|
||||
```text
|
||||
<wearCount:int>
|
||||
<legacyId:int> <materialName>
|
||||
... (wearCount строк)
|
||||
|
||||
[пустая строка]
|
||||
[LIGHTMAPS
|
||||
<lightmapCount:int>
|
||||
<legacyId:int> <lightmapName>
|
||||
... (lightmapCount строк)]
|
||||
```
|
||||
|
||||
`legacyId` читается, но логика выбора работает по имени.
|
||||
|
||||
## 3. Совместимость парсинга
|
||||
|
||||
В движке используются два режима чтения (`из файла` и `из буфера`), у которых различается обработка блока `LIGHTMAPS`.
|
||||
|
||||
Практическое правило для полного совпадения:
|
||||
|
||||
- если присутствует блок `LIGHTMAPS`, перед строкой `LIGHTMAPS` должна быть пустая строка-разделитель.
|
||||
|
||||
## 4. Runtime-ограничения
|
||||
|
||||
- Число wear-таблиц в менеджере ограничено: максимум `70`.
|
||||
- Для `wearCount <= 0` ресурс считается некорректным.
|
||||
- Для `LIGHTMAPS` блока `lightmapCount <= 0` — также ошибка формата.
|
||||
|
||||
## 5. Поведение резолва
|
||||
|
||||
### 5.1. Материал
|
||||
|
||||
Для каждого wear-слота:
|
||||
|
||||
1. Ищется материал по имени.
|
||||
2. Если не найден — используется fallback (`DEFAULT`, затем индекс 0).
|
||||
|
||||
### 5.2. Lightmap
|
||||
|
||||
Для каждого lightmap-слота:
|
||||
|
||||
1. Ищется текстура lightmap по имени.
|
||||
2. Если не найдено — слот получает `-1`.
|
||||
|
||||
## 6. Handle-кодирование
|
||||
|
||||
Движок кодирует ссылку на material-slot как:
|
||||
|
||||
```c
|
||||
handle = (tableIndex << 16) | wearIndex
|
||||
```
|
||||
|
||||
- `tableIndex` — номер wear-таблицы.
|
||||
- `wearIndex` — индекс строки внутри таблицы.
|
||||
|
||||
## 7. Правила writer/editor
|
||||
|
||||
1. Сохранять порядок строк.
|
||||
2. Не переставлять и не нормализовать `legacyId`.
|
||||
3. Для совместимости buffer-парсинга сохранять пустую строку перед `LIGHTMAPS`.
|
||||
4. Проверять, что число строк соответствует `wearCount`/`lightmapCount`.
|
||||
|
||||
## 8. Статус валидации
|
||||
|
||||
- Поведение `WEAR` согласовано с текущей спецификацией материалов/текстур и runtime-пайплайном.
|
||||
- Корпусные проверки связки `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».
|
||||
@@ -0,0 +1,371 @@
|
||||
# I. Путеводитель и методика
|
||||
|
||||
Первый том задаёт язык и правила всей документации. Он объясняет, как читать
|
||||
технические главы, какие термины используются для игрового runtime, как
|
||||
разделяются уровни уверенности и какие требования предъявляются к реализации,
|
||||
которая должна работать с оригинальными данными без потери информации.
|
||||
|
||||
Документация рассчитана на разработчика, который уже умеет читать C/C++,
|
||||
байтовые форматы, PE-модули и графические pipeline, но не обязательно знаком с
|
||||
Iron3D. Поэтому этот том не описывает один конкретный crate, package или
|
||||
физическое деление будущего кода. Он фиксирует контракты: что должно быть
|
||||
прочитано, сохранено, рассчитано и показано.
|
||||
|
||||
## Назначение книги
|
||||
|
||||
Книга ведёт от общей архитектуры Iron3D к точным форматам данных и алгоритмам
|
||||
исполнения. Практическая цель -- реализация, способная открыть оригинальный
|
||||
каталог *Parkan: Iron Strategy*, загрузить миссию, создать мир, провести
|
||||
игровой шаг и сформировать кадр.
|
||||
|
||||
Форматы в главах описываются как байтовые контракты. Если указано поле
|
||||
`+0x10`, это означает расположение в потоке или структуре данных, а не
|
||||
разрешение читать файл прямым `reinterpret_cast`. Для постоянных layouts
|
||||
используются offsets, проверки размеров, bounded cursor и явное сохранение
|
||||
неизвестных байтов. Для versioned и variable-length записей приоритет имеет
|
||||
последовательный parser с контролем границ.
|
||||
|
||||
Игровое поведение описывается не только размером структур. Совместимая
|
||||
реализация должна учитывать порядок событий, время, fallback-правила,
|
||||
идентификаторы объектов, численные ограничения, состояние материалов,
|
||||
границы кадра и правила завершения операций.
|
||||
|
||||
## Маршруты чтения
|
||||
|
||||
**Читатель, новый для игровой разработки**, начинает с базовых понятий этого
|
||||
тома, затем переходит к архитектуре, игровому циклу и вводу в рендер. После
|
||||
этого имеет смысл читать главы о миссиях, мире и ресурсных форматах.
|
||||
|
||||
**Разработчик совместимого движка** читает тома II-VII линейно. Технические
|
||||
главы имеют одинаковую логику: назначение подсистемы, данные на диске,
|
||||
представление в памяти, алгоритм работы, проверки и требования к новой
|
||||
реализации.
|
||||
|
||||
**Аналитик оригинальной программы** использует этот том вместе с разделами о
|
||||
доказательной базе, ABI, результатах корпусных проверок и границах знания.
|
||||
Факты, согласованные выводы и открытые вопросы должны оставаться разделёнными:
|
||||
это позволяет расширять реализацию без подмены проверенных контрактов
|
||||
удобными догадками.
|
||||
|
||||
## Состав документации
|
||||
|
||||
1. **Путеводитель и методика** -- язык предметной области, правила чтения и
|
||||
процедура проверки.
|
||||
2. [**Запуск, архитектура и игровой цикл**](02-architecture.md) -- от
|
||||
`iron_3d.exe` до расчёта и вывода кадра.
|
||||
3. [**Ресурсная система и форматы**](03-resources.md) -- архивы, кэши, реестры
|
||||
и служебные данные.
|
||||
4. [**Мир, миссии и игровой runtime**](04-world.md) -- TMA, ландшафт, ареалы и
|
||||
создание объектов.
|
||||
5. [**Геометрия, материалы и рендер**](05-render.md) -- от вершины модели до
|
||||
изображения на экране.
|
||||
6. [**Поведение, управление, звук и сеть**](06-behavior.md) -- интерактивные
|
||||
подсистемы.
|
||||
7. [**Руководство по полной реализации**](07-implementation.md) -- предлагаемая
|
||||
архитектура и порядок работ.
|
||||
8. [**Справочник и доказательная база**](08-evidence.md) -- ABI,
|
||||
конфигурация, статистика и открытые вопросы.
|
||||
|
||||
Дополнительные краткие определения собраны в
|
||||
[глоссарии](../appendices/glossary.md). Технические области, где контракт ещё
|
||||
не закрыт полностью, перечислены в
|
||||
[границах знания](../appendices/knowledge-boundaries.md).
|
||||
|
||||
## Условные обозначения
|
||||
|
||||
`+0x10` означает смещение поля относительно начала структуры или записи.
|
||||
`RVA 0x13B60` -- адрес относительно базы PE-модуля. `u16`, `u32`, `i16` и
|
||||
`float32` обозначают типы фиксированной ширины. `LE` означает little-endian.
|
||||
`payload` -- полезные данные записи после метаданных контейнера. `EOF` -- точное
|
||||
завершение файла или ограниченного блока.
|
||||
|
||||
Если в тексте указан hash, RVA или ordinal, значение относится к явно
|
||||
обозначенному binary profile. Адреса разных сборок не объединяются по имени
|
||||
функции. При публикации функции нужны минимум модуль, SHA-256 сборки и RVA.
|
||||
|
||||
Размеры структур выражаются в байтах. Счётчики и offsets считаются частью
|
||||
формата, даже когда их можно восстановить из длины файла. Padding, reserved
|
||||
поля, неизвестные хвосты и gaps не нормализуются без доказанного правила.
|
||||
|
||||
## Совместимость
|
||||
|
||||
Слово "совместимость" в этой книге имеет несколько уровней.
|
||||
|
||||
**Reader** умеет открыть файл, проверить границы, извлечь известные поля и
|
||||
сохранить неизвестные bytes так, чтобы данные можно было записать обратно.
|
||||
|
||||
**Viewer** умеет показать ресурс: модель, texture, material, эффект или карту.
|
||||
Viewer может быть полезен для анализа, но он не доказывает поведение runtime.
|
||||
|
||||
**Runtime** умеет создать мир, зарегистрировать объекты, исполнять события,
|
||||
обновлять время, применять контроллеры, выбирать видимое состояние и передавать
|
||||
его рендеру.
|
||||
|
||||
**Полноценный движок** дополнительно воспроизводит порядок операций, численные
|
||||
правила, fallback-поведение, resource lifetime, reference ownership, pause,
|
||||
manual input, сетевые идентификаторы, boundaries кадра и состояние
|
||||
интерактивных подсистем.
|
||||
|
||||
Поэтому файл может быть "прочитан правильно", но всё ещё не быть реализованным
|
||||
на уровне движка. Например, reader MSH может восстановить вершины и индексы,
|
||||
viewer может нарисовать mesh, а runtime обязан ещё сохранить material slots,
|
||||
animation state, bounds, LOD, visibility, collision и связи с объектом мира.
|
||||
|
||||
## Движок как программа длительного действия
|
||||
|
||||
Обычная прикладная программа получает запрос, вычисляет результат и заканчивает
|
||||
работу. Игра живёт в цикле: прочитать ввод, обновить состояние мира,
|
||||
сформировать звук и изображение, показать кадр и повторить. Движок -- набор
|
||||
подсистем и соглашений, которые делают этот цикл устойчивым.
|
||||
|
||||
**Simulation** отвечает на вопрос "что произошло в мире": куда переместился
|
||||
объект, кого он видит, сколько у него здоровья, сработал ли эффект, изменился
|
||||
ли маршрут или приказ. **Rendering** отвечает на другой вопрос: "как текущее
|
||||
состояние показать". В корректной архитектуре рендер не решает игровые правила,
|
||||
а читает подготовленное состояние.
|
||||
|
||||
**Tick** -- один шаг расчёта. **Frame** -- одно изображение. Они могут
|
||||
выполняться с разной частотой: игра способна рассчитать несколько шагов между
|
||||
двумя показами или временно не рисовать, не останавливая логику. Поэтому время,
|
||||
накопление input, порядок callbacks и момент удаления объектов считаются частью
|
||||
контракта.
|
||||
|
||||
## Мир, сцена и объект
|
||||
|
||||
**Мир** -- долгоживущее состояние миссии: ландшафт, объекты, время, погода,
|
||||
принадлежность к кланам и глобальные сервисы. **Сцена** -- представление той
|
||||
части мира, которую можно обработать для текущей камеры. **Игровой объект** --
|
||||
сущность с идентификатором, положением, набором свойств и поведением.
|
||||
|
||||
В Iron3D объектами управляет World3D. Объекты регистрируются в общей очереди,
|
||||
получают события, участвуют в расчёте и могут быть удалены отложенно, чтобы не
|
||||
разрушить обход коллекции посреди шага. Это важнее, чем конкретный контейнер в
|
||||
новой реализации: совместимость определяется моментом наблюдаемого добавления,
|
||||
обновления и удаления.
|
||||
|
||||
Мир не равен renderer scene graph. Один объект может иметь runtime state,
|
||||
controller, сетевой mirror, визуальную модель, collision bounds и script state.
|
||||
Часть этих данных нужна для gameplay, часть -- для вывода, часть -- для
|
||||
сохранения и воспроизведения.
|
||||
|
||||
## Ресурс, модель и материал
|
||||
|
||||
**Ресурс** -- именованный блок данных, который можно найти и загрузить. Архивы
|
||||
`NRes` и `RsLi` содержат таблицы таких блоков. Имя, индекс, размер, offset,
|
||||
compression method и fallback-правило являются частью контракта загрузки.
|
||||
|
||||
**Модель** описывает форму объекта. Она состоит из вершин, индексов, узлов,
|
||||
групп треугольников, слотов материалов и auxiliary streams. **Vertex** хранит
|
||||
положение и обычно дополнительные атрибуты: нормаль для освещения и
|
||||
UV-координату для выборки texture. **Triangle** -- три вершины, образующие
|
||||
примитив. **Index buffer** хранит номера вершин и позволяет переиспользовать их
|
||||
между треугольниками. **Batch** -- непрерывный диапазон индексов, который
|
||||
рисуется одним материалом и одним набором состояний.
|
||||
|
||||
**Материал** описывает способ отображения поверхности: texture references,
|
||||
цвет, прозрачность, режимы смешивания и анимацию параметров. **Texture** --
|
||||
изображение в памяти графической системы. **Mip-уровни** -- уменьшенные копии
|
||||
изображения для дальних объектов. **Lightmap** -- дополнительная texture с
|
||||
заранее рассчитанным освещением.
|
||||
|
||||
Runtime должен связывать эти уровни по цепочке: миссия выбирает объект, объект
|
||||
ссылается на prototype, prototype приводит к модели, модель -- к WEAR,
|
||||
материалам, textures и lightmaps. Ошибка на любом участке этой цепочки может
|
||||
не проявиться в parser-е, но проявится в игровом кадре.
|
||||
|
||||
## Пространственные понятия
|
||||
|
||||
**Transform** переводит точку из локальных координат модели в координаты мира,
|
||||
камеры и экрана. **Иерархия узлов** позволяет одному элементу наследовать
|
||||
движение другого. **LOD** выбирает менее подробную геометрию вдали. **Culling**
|
||||
отбрасывает то, что не видно. **Bounds** -- упрощённая оболочка объекта,
|
||||
обычно сфера или AABB, используемая для быстрых тестов.
|
||||
|
||||
**Collision** отвечает на геометрические пересечения. **Navigation** ищет
|
||||
допустимый маршрут. В Iron3D эти задачи разделены: Control обслуживает
|
||||
физическую модель и столкновения, а ArealMap хранит пространственные области и
|
||||
связи между ними.
|
||||
|
||||
Важно не смешивать визуальные и игровые упрощения. Render bounds могут быть
|
||||
достаточны для отсечения, но не обязаны совпадать с collision shape. Навигация
|
||||
может использовать areal graph, который не является ни mesh-ем модели, ни
|
||||
геометрией ландшафта в renderer-е.
|
||||
|
||||
## Графический конвейер
|
||||
|
||||
Процессор выбирает видимые объекты, готовит матрицы, материалы и списки
|
||||
примитивов. Графический backend передаёт вершины, индексы, textures и state
|
||||
драйверу. Видеокарта преобразует вершины в координаты экрана, разбивает
|
||||
треугольники на фрагменты, проверяет глубину, смешивает цвет и записывает
|
||||
результат в буфер кадра. После завершения буфер становится видимым
|
||||
пользователю.
|
||||
|
||||
Для совместимости важны не только данные draw call. Контракт включает frame
|
||||
boundaries, viewport, camera state, порядок world traversal, material resolve,
|
||||
shadow/transparent/FX subpasses, завершение renderer-а, восстановление state и
|
||||
callbacks после рендера. Если часть имён vtable slots ещё не доказана, новая
|
||||
реализация должна фиксировать крупный порядок операций и оставлять
|
||||
детализацию проверяемой.
|
||||
|
||||
## Практический словарь реализации
|
||||
|
||||
**Handle** -- компактная ссылка на управляемый объект. **Cache** -- сохранённый
|
||||
результат загрузки или декодирования. **Reference count** -- число владельцев
|
||||
ресурса. **Fallback** -- предписанный запасной вариант при отсутствии данных.
|
||||
**Invariant** -- условие, которое всегда должно быть истинным для корректного
|
||||
файла или runtime-состояния. **Determinism** -- повторяемость результата при
|
||||
одинаковых входных данных и порядке событий.
|
||||
|
||||
**Strict mode** -- режим parser-а, который принимает только корректный файл:
|
||||
верные magic, версии, размеры, ranges, индексы и точный EOF. **Lossless mode**
|
||||
-- режим чтения/записи, который сохраняет неизвестные поля, padding, gaps и raw
|
||||
payload без нормализации. **Quirk** -- именованное отклонение, разрешённое
|
||||
только после проверки на реальных данных или исполняемом коде.
|
||||
|
||||
Эти слова используются как технические термины. Если глава называет значение
|
||||
fallback-ом, invariant-ом или quirk-ом, это должно иметь проверяемое
|
||||
последствие в reader-е, writer-е или runtime.
|
||||
|
||||
## Как читать C/C++-схемы структур
|
||||
|
||||
Структуры в главах описывают байтовый layout, а не переносимый C++ object
|
||||
model. Если поля на диске идут без padding, reader должен читать их по offsets
|
||||
либо использовать явно проверенный packed layout. Прямое отображение native
|
||||
struct допустимо только при доказанном размере, выравнивании и endian-правиле.
|
||||
|
||||
`sizeof` обязательно проверяется `static_assert` или эквивалентным compile-time
|
||||
test. Это особенно важно для records, где 32-битное поле начинается после
|
||||
нечётного числа 16-битных или 8-битных полей: стандартное выравнивание
|
||||
современного compiler-а может вставить скрытые bytes и изменить offsets.
|
||||
|
||||
Для variable-length форматов предпочтителен bounded cursor:
|
||||
|
||||
1. Прочитать header и проверить минимальный размер.
|
||||
2. Проверить, что offsets и sizes лежат внутри текущего блока.
|
||||
3. Прочитать таблицы до объявленного count, не до "пока получается".
|
||||
4. Проверить ссылки между таблицами.
|
||||
5. Дойти до точного EOF или сохранить явно разрешённый trailing payload.
|
||||
|
||||
Writer пересчитывает только производные значения: размеры, offsets, число
|
||||
записей, сортировочные таблицы и padding, если правило доказано. Unknown fields
|
||||
и reserved ranges сохраняются побайтно.
|
||||
|
||||
## Иерархия доказательств
|
||||
|
||||
Документация использует четыре уровня уверенности.
|
||||
|
||||
**Прямое наблюдение** -- поле, значение или последовательность видны в
|
||||
инструкции программы, таблице PE, экспорте, строке, обработчике файла или в
|
||||
самом ресурсе. Это самый сильный уровень.
|
||||
|
||||
**Корпусное подтверждение** -- правило проверено на всех подходящих файлах
|
||||
одного или нескольких явно названных наборов: демоверсии, Части 1 и Части 2.
|
||||
Например, базовый корпус содержит 435 моделей MSH, 518 textures Texm и 923
|
||||
эффекта FXID, прошедших структурные проверки без ошибок; полные части расширяют
|
||||
эту матрицу вариантов.
|
||||
|
||||
**Согласованный вывод** -- назначение восстановлено по нескольким независимым
|
||||
признакам: вызывающим функциям, vtable slots, строкам ошибок, диапазонам
|
||||
значений и связям между форматами. Такой вывод пригоден для реализации, но его
|
||||
численные детали следует проверять тестами.
|
||||
|
||||
**Открытый вопрос** -- данные можно читать и сохранять, однако предметный смысл
|
||||
поля или редкой ветки не доказан. Такие bytes нельзя обнулять,
|
||||
переупорядочивать или превращать в authoring API.
|
||||
|
||||
Уровень уверенности должен быть виден из формулировки. "Поле равно" означает
|
||||
проверенный layout или значение. "Вероятно отвечает за" означает согласованный
|
||||
вывод. "Неизвестно" означает сохранять без изменения и не строить вокруг этого
|
||||
публичный контракт.
|
||||
|
||||
## Проверенные материалы
|
||||
|
||||
Локальный набор проверки включает демоверсию, полные каталоги Частей 1 и 2,
|
||||
исполняемые файлы, 15 DLL каждой сборки и игровые ресурсы. DLL из
|
||||
первоначального архива и DLL демоверсии совпали по SHA-256: `15/15`, поэтому
|
||||
выводы по этому коду и demo-ресурсам образуют один доказательный профиль.
|
||||
|
||||
Исполняемый файл демоверсии `iron_3d.exe` имеет размер 36 864 байта, PE32/x86,
|
||||
entry RVA `0x141E`, image base `0x400000` и SHA-256
|
||||
`b0a8b0db1c3a8698c4d4604d89c655496bd91ac1f8859a455e8a45838aebfbd6`.
|
||||
|
||||
Исполняемые файлы Частей 1 и 2 также имеют размер 36 864 байта и побайтно
|
||||
совпадают между собой, но относятся к другому binary profile: entry RVA
|
||||
`0x147E`, SHA-256
|
||||
`f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7`.
|
||||
|
||||
Полные каталоги Частей 1 и 2 суммарно включают 60 TMA, 1 101 unit DAT, 254
|
||||
NRes-файла и 14 975 NRes entries. Все контейнеры и TMA прошли bounded parser до
|
||||
точного EOF; полный достижимый граф обеих частей разрешился без ошибок.
|
||||
|
||||
## Процедура проверки
|
||||
|
||||
Проверка строится как воспроизводимая цепочка:
|
||||
|
||||
1. Снять PE-метаданные, хэши, импорты, экспорты, ordinals, RTTI и строки.
|
||||
2. Построить граф вызовов между модулями и отметить фабрики подсистем.
|
||||
3. Разобрать функции запуска, загрузчики файлов, главный цикл и критические
|
||||
vtable-вызовы.
|
||||
4. Проверить форматы независимыми reader-скриптами с контролем границ и точного
|
||||
завершения файла.
|
||||
5. Построить цепочку миссия -> объект -> прототип -> модель -> материал ->
|
||||
texture.
|
||||
6. Сравнить счётчики, диапазоны, ссылки и размеры на всём доступном корпусе.
|
||||
|
||||
Ключевой результат сквозной проверки демо-миссий: все 201 объектов шести
|
||||
миссий разрешились в 501 запрос прототипов, затем в 501 модель, 501 таблицу
|
||||
WEAR, 3 879 слотов материалов и 5 085 ссылок на textures или lightmaps. Ошибок
|
||||
в фактически исполняемом пути нет.
|
||||
|
||||
## Что не считается доказательством
|
||||
|
||||
Удобное имя поля не доказывает его назначение. Совпадение layout с текущей
|
||||
реализацией не доказывает поведение оригинального runtime. Успешный viewer не
|
||||
доказывает writer. Успешный reader одного файла не доказывает формат всего
|
||||
корпуса. Совпадение ABI не доказывает побайтную идентичность всех сборок.
|
||||
|
||||
Если локальные данные и предположение расходятся, приоритет имеют исполняемый
|
||||
код, реальные ресурсы и взаимные invariants между форматами. Неизвестное поле
|
||||
лучше оставить без имени, чем дать ему ложное предметное значение.
|
||||
|
||||
## Требования к воспроизводимости
|
||||
|
||||
Каждая новая реализация должна иметь strict parser mode, lossless roundtrip
|
||||
mode и набор corpus tests. Неизвестные поля сохраняются побайтно. Любое
|
||||
присвоенное полю имя должно сопровождаться наблюдаемым поведением или тестом.
|
||||
Численные правила -- округление, порядок умножения, RNG и время -- считаются
|
||||
частью формата исполнения, даже если файл читается правильно.
|
||||
|
||||
Минимальный отчёт проверки должен фиксировать:
|
||||
|
||||
1. build profile и hashes модулей;
|
||||
2. путь или ключ ресурса;
|
||||
3. размер входного файла и hash входных bytes;
|
||||
4. версию parser-а или commit реализации;
|
||||
5. список включённых quirks;
|
||||
6. число прочитанных записей и точку EOF;
|
||||
7. ошибки, предупреждения и unknown ranges;
|
||||
8. результат roundtrip, если writer участвует в проверке.
|
||||
|
||||
Для runtime-проверок дополнительно нужны mission key, configuration, device
|
||||
profile, начальное состояние, input/time script и trace значимых callbacks.
|
||||
|
||||
## Разделение профилей
|
||||
|
||||
Binary profile описывает исполняемый код: PE-метаданные, exports/imports,
|
||||
ordinals, hashes, RVA и layout функций. Corpus profile описывает набор файлов:
|
||||
каталог, миссии, ресурсы, размеры, counts, variants и статистику parser-а.
|
||||
|
||||
Эти профили нельзя смешивать без явной пометки. Один и тот же формат может
|
||||
иметь общий смысл в разных сборках, но отличаться редкими ветками, адресами
|
||||
функций или набором встреченных вариантов. Один и тот же address может иметь
|
||||
смысл только внутри конкретного module hash.
|
||||
|
||||
При расширении документации новое утверждение должно отвечать на три вопроса:
|
||||
|
||||
1. Где это видно напрямую?
|
||||
2. На каком корпусе это проверено?
|
||||
3. Что должна сделать реализация, если правило нарушено?
|
||||
|
||||
Если на один из вопросов нет ответа, утверждение остаётся согласованным выводом
|
||||
или открытым вопросом, а не закрытым контрактом.
|
||||
@@ -0,0 +1,472 @@
|
||||
# II. Запуск, архитектура и игровой цикл
|
||||
|
||||
Этот том описывает путь от запуска `iron_3d.exe` до устойчивого кадра:
|
||||
загрузку `iron3d.dll`, создание shell/game objects, поднятие платформенных
|
||||
сервисов, запуск World3D, расчёт simulation step, безопасное удаление объектов,
|
||||
рендер и завершение программы.
|
||||
|
||||
Главная особенность Iron3D -- это не один монолитный engine object, а связка
|
||||
небольшого Win32 bootstrap и набора DLL, которые обмениваются фабриками,
|
||||
singleton-интерфейсами и C++ vtable. Совместимая реализация может изменить
|
||||
физическое деление на библиотеки, но не может произвольно менять порядок
|
||||
инициализации, object identity, правила владения, fallback ресурсов и порядок
|
||||
событий.
|
||||
|
||||
```text
|
||||
iron_3d.exe
|
||||
-> iron3d.dll
|
||||
-> services.dll
|
||||
-> World3D.dll
|
||||
-> Terrain.dll
|
||||
-> Ngi32.dll
|
||||
-> AniMesh.dll / ArealMap.dll / Effect.dll
|
||||
-> ai.dll / Behavior.dll / Wizard.dll
|
||||
-> Control.dll / MisLoad.dll / Net.dll / Joystick.dll
|
||||
```
|
||||
|
||||
## Карта модулей
|
||||
|
||||
Во внешней архитектуре обнаружено пятнадцать DLL. Экспортов сравнительно мало:
|
||||
они обычно создают объект, возвращают singleton или дают доступ к уже поднятой
|
||||
подсистеме. Основная работа выполняется через C++-интерфейсы, поэтому порядок
|
||||
виртуальных слотов является частью ABI, особенно для compatibility shim эпохи
|
||||
MSVC6.
|
||||
|
||||
```text
|
||||
iron_3d.exe
|
||||
|
|
||||
v
|
||||
iron3d.dll -- композиция игры, shell и главный цикл
|
||||
|
|
||||
+-- services.dll -- доступ к display, GUI, ресурсам, звуку, таймеру и сети
|
||||
+-- World3D.dll -- объекты, очередь, время, камера и кадр
|
||||
+-- Terrain.dll -- ландшафт, свет, атмосфера и визуальный слой мира
|
||||
+-- ai.dll / Behavior.dll / Wizard.dll
|
||||
+-- Control.dll / Effect.dll / MisLoad.dll
|
||||
+-- Net.dll / Joystick.dll
|
||||
+-- Ngi32.dll -- ресурсы, графика, звук, математика и CPU dispatch
|
||||
```
|
||||
|
||||
Циклы импортов между DLL ожидаемы. Terrain создаёт визуальные объекты и
|
||||
обращается к World3D, а World3D получает world-interface из Terrain. Это не
|
||||
значит, что обе библиотеки совместно владеют всем состоянием. Реальные границы
|
||||
задаются интерфейсами, refcount, очередью объектов и порядком shutdown.
|
||||
|
||||
Практичная новая структура может быть внутренним набором модулей `platform`,
|
||||
`resources`, `world`, `mission`, `terrain`, `render`, `animation`, `effects`,
|
||||
`behavior`, `physics`, `audio` и `network`. Важно сохранить не DLL-границы, а
|
||||
контракты: имена ресурсов, порядок поиска, fallback-ветки, object ID, момент
|
||||
создания mirror objects, численное поведение и последовательность событий.
|
||||
|
||||
## Роли модулей
|
||||
|
||||
`iron3d.dll` создаёт shell и game objects, читает `iron_3d.ini`, поднимает
|
||||
display, sound, CD-audio, network и настройки World3D, загружает миссионные и
|
||||
UI-конфигурации, содержит message pump и вызывает расчёт/рендер игры.
|
||||
|
||||
`services.dll` работает как service locator. Через него запрашиваются display,
|
||||
GUI, network manager, resource manager, sound server и timer. Этот слой отделяет
|
||||
высокоуровневую игру от деталей создания устройств.
|
||||
|
||||
`World3D.dll` -- центральный runtime: очередь объектов, идентификаторы,
|
||||
события, отложенное удаление, game time, pause, manual input, камера,
|
||||
material/texture/lightmap managers, сетевые mirrors, расчёт и 3D-проход.
|
||||
|
||||
`Terrain.dll` отвечает не только за землю. В его область входят ландшафт,
|
||||
здания, визуальный слой мира, камера, shade/state layer, primitive buffers,
|
||||
сортировочные слои, источники света, тени, microtextures, атмосфера, дождь,
|
||||
молнии, солнце и flares.
|
||||
|
||||
`Ngi32.dll` содержит низкоуровневые сервисы: DirectDraw/Direct3D-era renderer,
|
||||
DirectSound, readers `NRes`/`RsLi`, память, часы, математику, пересечения,
|
||||
определение CPU и таблицу быстрых процедур `g_FastProc`.
|
||||
|
||||
Предметные DLL закрывают отдельные области. `AniMesh.dll` загружает модели и
|
||||
агентов. `ArealMap.dll` строит spatial graph и маршруты. `Behavior.dll`
|
||||
реализует поведение юнитов. `ai.dll` содержит стратегический AI и миссионные
|
||||
сценарии. `Wizard.dll` корректирует локальное движение. `Control.dll`
|
||||
обслуживает физическую модель и столкновения. `Effect.dll` создаёт runtime-FX.
|
||||
`MisLoad.dll` читает миссионные данные. `Net.dll` инкапсулирует DirectPlay.
|
||||
`Joystick.dll` работает через DirectInput.
|
||||
|
||||
## Поток данных
|
||||
|
||||
Миссия не создаёт готовый кадр напрямую. Данные проходят через несколько
|
||||
уровней: описание объекта, прототипы, ресурсы, runtime-object, контроллеры,
|
||||
simulation state, render items и только затем платформенный renderer.
|
||||
|
||||
```text
|
||||
mission data
|
||||
-> object identity and properties
|
||||
-> prototype registry
|
||||
-> model/material/texture/effect resources
|
||||
-> World3D object + domain controllers
|
||||
-> simulation state
|
||||
-> visible render items
|
||||
-> Ngi32 render interface
|
||||
-> DirectX-era device
|
||||
```
|
||||
|
||||
Этот поток объясняет, почему нельзя объединять физический архив, metadata entry,
|
||||
декодированный payload и готовый runtime-кэш. У каждого уровня свой срок жизни,
|
||||
собственный refcount и собственные ошибки. Детали ресурсного конвейера описаны
|
||||
в [Томе III](03-resources.md), а сборка мира из миссии -- в [Томе IV](04-world.md).
|
||||
|
||||
## Bootstrap
|
||||
|
||||
`iron_3d.exe` -- небольшой PE32/x86 bootstrap размером 36 864 байта. Основная
|
||||
игровая логика находится в `iron3d.dll`. Исполняемый файл создаёт Win32-процесс,
|
||||
подготавливает окружение, загружает библиотеку и получает восемь публичных
|
||||
точек входа:
|
||||
|
||||
```text
|
||||
createShell deleteShell
|
||||
createGame deleteGame
|
||||
createSubsystems deleteSubsystems
|
||||
getIGame getIShell
|
||||
```
|
||||
|
||||
Эти функции образуют внешнюю границу игры. `createShell` создаёт оболочку
|
||||
интерфейса и меню, `createGame` -- объект игровой логики, `createSubsystems` --
|
||||
аппаратные и runtime-сервисы. Getter-функции возвращают уже созданные объекты.
|
||||
|
||||
Запуск удобно читать как конечный автомат:
|
||||
|
||||
```text
|
||||
PROCESS_CREATED
|
||||
-> LIBRARY_READY
|
||||
-> ENTRYPOINTS_READY
|
||||
-> SHELL_CREATED
|
||||
-> GAME_CREATED
|
||||
-> SUBSYSTEMS_READY
|
||||
-> MAIN_LOOP
|
||||
-> SUBSYSTEMS_CLOSED
|
||||
-> GAME_DELETED
|
||||
-> SHELL_DELETED
|
||||
```
|
||||
|
||||
Каждый переход имеет обратное действие. Если display, sound или другой
|
||||
обязательный сервис не создан, главный цикл не начинается, но уже созданные
|
||||
объекты освобождаются в обратном порядке. Новая оболочка запуска должна
|
||||
работать из каталога оригинальной установки, сохранять смысл относительных
|
||||
путей, создавать окно до графической подсистемы и закрывать частично поднятые
|
||||
сервисы без предположения, что init дошёл до конца.
|
||||
|
||||
Bootstrap обеих полных частей побайтно одинаков, хотя файл второй части может
|
||||
иметь другое имя:
|
||||
|
||||
```text
|
||||
size 36 864
|
||||
entry RVA 0x147E
|
||||
SHA-256 f476af85c034a4b4f34f49d0806e4dff397b5da0ee26d382a7674231144979f7
|
||||
```
|
||||
|
||||
Следовательно, различия полных частей начинаются после передачи управления DLL
|
||||
и игровым данным. Адреса executable демоверсии относятся к другой binary
|
||||
profile и не должны переноситься на полные версии без проверки hash.
|
||||
|
||||
## Инициализация подсистем
|
||||
|
||||
Iron3D разделяет создание высокоуровневых объектов и создание подсистем.
|
||||
`createShell` конструирует оболочку пользовательского интерфейса, `createGame`
|
||||
создаёт объект игры, а `createSubsystems` связывает их с display, sound,
|
||||
network и World3D.
|
||||
|
||||
Высокоуровневая последовательность выглядит так:
|
||||
|
||||
```text
|
||||
прочитать iron_3d.ini
|
||||
-> получить display service
|
||||
-> создать окно и графическое устройство
|
||||
-> проверить доступность 3D-драйвера
|
||||
-> выбрать CURRENT_D3DCARD
|
||||
-> получить sound service и настроить громкость
|
||||
-> создать network instance и передать application GUID
|
||||
-> создать World3D game settings
|
||||
```
|
||||
|
||||
Ошибка отсутствующего 3D-устройства обрабатывается отдельно от ошибок ресурсов:
|
||||
это разные стадии запуска. Конфигурация влияет не только на разрешение. В
|
||||
runtime попадают графическая карта, громкость эффектов, CD-audio, режим
|
||||
CD-sound, сетевое приложение и World3D settings. Application GUID сетевой
|
||||
подсистемы:
|
||||
|
||||
```text
|
||||
{3C1D1F01-A870-11D1-8400-000021B14415}
|
||||
```
|
||||
|
||||
Один и тот же GUID передаётся сетевому объекту и service layer. Если он
|
||||
разойдётся, экземпляры игры станут логически разными приложениями, даже при
|
||||
исправном транспорте.
|
||||
|
||||
## `stdInitGame`
|
||||
|
||||
После платформенных сервисов World3D создаёт внутренний runtime:
|
||||
|
||||
1. Создаёт глобальную очередь объектов.
|
||||
2. Сохраняет window handle и режим игры.
|
||||
3. При нужном режиме ограничивает курсор областью окна.
|
||||
4. Получает или создаёт 3D sound object.
|
||||
5. Загружает реестр адресов компонентов из `Comp.ini`.
|
||||
6. Получает или создаёт 3D renderer.
|
||||
7. Читает профиль возможностей renderer.
|
||||
8. Загружает component type 6.
|
||||
9. Для multiplayer создаёт NetWatcher.
|
||||
10. Получает world-interface из Terrain.
|
||||
11. Устанавливает исходные параметры света и тумана.
|
||||
|
||||
Порядок важен. World objects не должны появляться до queue, ресурсы рендера --
|
||||
до renderer, сетевые mirror objects -- до NetWatcher. В новой реализации у
|
||||
каждого этапа должен быть явный признак успешного создания, чтобы shutdown мог
|
||||
безопасно разобрать неполный init.
|
||||
|
||||
## Завершение
|
||||
|
||||
Shutdown идёт в обратном направлении: прекращаются игровые расчёты и сетевые
|
||||
наблюдатели, разбираются отложенные операции, освобождаются world objects и
|
||||
менеджеры, затем renderer и sound, затем game settings и platform services.
|
||||
Ограничение курсора снимается, глобальные ссылки очищаются.
|
||||
|
||||
Полезный протокол завершения:
|
||||
|
||||
1. Запретить новые события и новые объекты.
|
||||
2. Дождаться выхода из calculation/render traversal.
|
||||
3. Разобрать очередь deferred operations.
|
||||
4. Отсоединить объекты от очереди, контроллеров и менеджеров.
|
||||
5. Освободить managers и singletons после их consumers.
|
||||
6. Закрыть устройства и платформенные сервисы.
|
||||
|
||||
Такой порядок защищает от dangling-ссылок между World3D, Terrain, renderer,
|
||||
sound и сетевым слоем.
|
||||
|
||||
## Главный цикл
|
||||
|
||||
Главный цикл -- не одна функция `update_and_render`, а расписание, связывающее
|
||||
Win32 messages, input, игровые события, таймеры, сеть и renderer. Системная
|
||||
очередь сообщает об активации окна, вводе, изменении состояния процесса и
|
||||
выходе. Очередь World3D рассчитывает игровые объекты. У этих очередей разные
|
||||
правила времени и владения, поэтому их нельзя смешивать в один контейнер.
|
||||
|
||||
Подтверждённые точки вызова в одном из профилей:
|
||||
|
||||
```text
|
||||
stdCalculateGame RVA 0x5FA94, 0x604C1, 0x6086B
|
||||
ClearManualEventsList RVA 0x6052F
|
||||
stdRenderGame RVA 0x60B2F
|
||||
UpdateManualEventsList в обработчике сообщений около RVA 0xA3759
|
||||
```
|
||||
|
||||
Смысловой skeleton:
|
||||
|
||||
```c
|
||||
while (running) {
|
||||
stdCalculateGame();
|
||||
clear_keyboard_snapshot();
|
||||
update_shell_and_mode();
|
||||
ClearManualEventsList();
|
||||
process_window_messages();
|
||||
update_timers_ui_gameplay_network();
|
||||
if (mode_requires_extra_step) stdCalculateGame();
|
||||
if (render_enabled) {
|
||||
stdSetCurrentCamera(camera);
|
||||
stdRenderGame(camera);
|
||||
} else {
|
||||
sleep_briefly();
|
||||
}
|
||||
update_post_render_state();
|
||||
}
|
||||
```
|
||||
|
||||
Ввод из window messages накапливается между расчётными шагами. Если читать
|
||||
клавиатуру только внутри рендера, события будут теряться при пропущенных кадрах
|
||||
или отключённом выводе.
|
||||
|
||||
## `stdCalculateGame`
|
||||
|
||||
Calculation pass сначала очищает или подготавливает список manual events,
|
||||
увеличивает внутренний depth/counter и опрашивает input device. Если устройство
|
||||
временно потеряно, выполняется повторное получение доступа и чтение повторяется.
|
||||
Затем при незамороженной игре выставляется признак `in_calculation` и вызывается
|
||||
основной traversal очереди объектов.
|
||||
|
||||
```text
|
||||
prepare input/events
|
||||
-> enter calculation
|
||||
-> dispatch queue events
|
||||
-> objects update behavior and transforms
|
||||
-> leave calculation
|
||||
-> apply deferred operations
|
||||
-> occasional cache maintenance
|
||||
```
|
||||
|
||||
После traversal разбирается deferred-delete list. Объект может запросить
|
||||
собственное удаление во время события, но память освобождается только после
|
||||
завершения обхода. Периодически также очищаются давно неиспользуемые ресурсы и
|
||||
объекты по порогам часов порядка 20 и 60 секунд.
|
||||
|
||||
Совместимый runtime должен иметь явный traversal depth или флаг
|
||||
`in_calculation`. Нельзя полагаться на то, что контейнер выдержит удаление
|
||||
текущего элемента из обработчика события.
|
||||
|
||||
## Жизненный цикл кадра
|
||||
|
||||
Рендер читает состояние, подготовленное расчётом. Кадр начинается до renderer-а:
|
||||
message pump уже накопил ввод, World3D уже обновил объекты, отложенные операции
|
||||
и анимации, после чего выбирается камера и обновляется listener звука.
|
||||
|
||||
```text
|
||||
system messages and input
|
||||
-> simulation calculation
|
||||
-> deferred object operations
|
||||
-> animation and transforms
|
||||
-> camera and sound listener
|
||||
-> visibility and render queues
|
||||
-> materials and draw passes
|
||||
-> renderer completion
|
||||
-> end-of-render callbacks and UI
|
||||
```
|
||||
|
||||
В `World3D::stdRenderGame` виден крупный каркас: установка camera/viewport,
|
||||
renderer frame boundaries, traversal мира, завершение world/shade path,
|
||||
renderer completion, снятие `in_render`, восстановление viewport и рассылка
|
||||
end-of-render callbacks. Эти callbacks позволяют объектам безопасно обновить
|
||||
временные ресурсы после того, как draw-команды больше их не используют.
|
||||
|
||||
Один calculation step не обязан соответствовать одному изображению. Главный
|
||||
цикл допускает дополнительный вызов `stdCalculateGame` и режим, в котором
|
||||
расчёт продолжается без вывода кадра. Поэтому нужно хранить отдельно:
|
||||
|
||||
1. монотонные платформенные часы;
|
||||
2. игровое время с pause и масштабированием;
|
||||
3. длительность текущего calculation step;
|
||||
4. локальное время анимации и FX;
|
||||
5. реальные часы обслуживания кэшей.
|
||||
|
||||
Игровую логику нельзя выводить из render delta: изменение частоты кадров тогда
|
||||
изменит движение, камеру и сценарные таймеры. Подробности render item и рисков
|
||||
кадровой совместимости вынесены в справочник [Render frame](../reference/render-frame.md).
|
||||
|
||||
## World3D
|
||||
|
||||
World3D связывает игровые объекты, события, время, ввод, камеру, сетевые
|
||||
отражения и визуальное представление. Он не содержит всю предметную логику:
|
||||
движение делегируется Behavior/Wizard, физика -- Control, мир -- Terrain. Его
|
||||
задача -- общая идентичность, порядок вызовов и безопасный жизненный цикл.
|
||||
|
||||
`CreateQueue` создаёт singleton-объект размером 20 байт, а `GetQueue`
|
||||
возвращает его. Очередь служит центральным маршрутизатором событий и операций
|
||||
над объектами.
|
||||
|
||||
Публичный слой предоставляет отдельные функции для локальных и сетевых
|
||||
объектов:
|
||||
|
||||
```text
|
||||
CreateObject
|
||||
AddObjectToGame
|
||||
AddNewObjectToGame
|
||||
CreateMirrorObject
|
||||
AddMirrorObjectToGame
|
||||
AddNewMirrorToGame
|
||||
```
|
||||
|
||||
Разделение "создать" и "добавить в игру" означает два этапа: сначала выделить и
|
||||
настроить instance, затем зарегистрировать его в общей системе. Это позволяет
|
||||
loader-у заполнить свойства до появления объекта в расчётной очереди.
|
||||
|
||||
## Идентичность объектов
|
||||
|
||||
Object ID кодирует не только порядковый номер. Проверки диапазонов показывают
|
||||
разбиение на номер игрока, класс и индекс. Mirror object представляет объект,
|
||||
владельцем которого является другой участник. Локальный runtime хранит его
|
||||
видимое состояние, но источник авторитетных изменений находится удалённо.
|
||||
|
||||
```c
|
||||
struct ObjectId {
|
||||
uint32_t raw;
|
||||
uint16_t owner_player;
|
||||
uint16_t class_and_index;
|
||||
};
|
||||
```
|
||||
|
||||
Точное битовое разбиение нужно брать из сетевых функций. На уровне API уже
|
||||
сейчас полезно разделить логические свойства: `is_local`, `is_mirror`, `owner`,
|
||||
`class` и `index`.
|
||||
|
||||
Минимальный runtime-object должен хранить identifier, type, owner, transform,
|
||||
active state, ordered property bag, ссылки на controllers, участие в расчёте и
|
||||
рендере, сетевой статус и флаг отложенного удаления. Специализированные DLL
|
||||
могут быть представлены компонентами, но порядок их вызовов задаёт World3D.
|
||||
|
||||
## Отложенное удаление
|
||||
|
||||
`DeleteGameObject` проверяет, идёт ли calculation pass. Если обход активен и
|
||||
удаление не принудительное, объект помещается в deferred list. `KillGameObject`
|
||||
отправляет запрос через очередь, а не освобождает память напрямую.
|
||||
|
||||
```c
|
||||
void request_delete(Object* o) {
|
||||
if (world.in_calculation) {
|
||||
world.deferred_delete.push_back(o);
|
||||
o->pending_delete = true;
|
||||
} else {
|
||||
world.detach_and_release(o);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Это защищает итераторы, связи и текущий стек вызовов. Любая новая подсистема,
|
||||
способная удалить объект из обработчика события, обязана пользоваться тем же
|
||||
механизмом.
|
||||
|
||||
Регистрация в очереди и владение памятью -- разные понятия. Удаление из мира не
|
||||
всегда означает немедленное освобождение instance: часть объектов и managers
|
||||
использует intrusive reference count, а renderer, sound и resource managers
|
||||
могут возвращать уже существующий singleton с увеличенным счётчиком. Поэтому
|
||||
global manager закрывается после всех объектов, которые на него ссылаются.
|
||||
|
||||
## Детерминизм
|
||||
|
||||
Даже при одинаковых формулах результат зависит от порядка. Стабильный runtime
|
||||
сохраняет последовательность queue traversal, момент формирования input
|
||||
snapshot, порядок сетевых сообщений, обработку deferred operations и порядок
|
||||
обращений к RNG. Оптимизация и многопоточность допустимы только при
|
||||
детерминированном объединении результатов.
|
||||
|
||||
Для переносимой реализации полезно разделить scheduler phases и immutable
|
||||
render snapshot. Это архитектурная рекомендация для новой реализации, а не
|
||||
утверждение о точном layout исходных C++ classes.
|
||||
|
||||
## Стабильность между сборками
|
||||
|
||||
Внешняя архитектура полных Частей 1 и 2 сохраняет те же пятнадцать DLL, 313
|
||||
exports, имена, ordinals и import sets. Побайтно идентичны:
|
||||
|
||||
```text
|
||||
ai.dll, Behavior.dll, Joystick.dll, MisLoad.dll, Net.dll,
|
||||
Ngi32.dll, Terrain.dll, Wizard.dll, World3D.dll
|
||||
```
|
||||
|
||||
Пересобраны:
|
||||
|
||||
```text
|
||||
AniMesh.dll, ArealMap.dll, Control.dll, Effect.dll,
|
||||
iron3d.dll, services.dll
|
||||
```
|
||||
|
||||
Это разделяет переносимость выводов. World3D lifecycle, Terrain, NRes/RsLi
|
||||
readers, mission loader, AI/Behavior/Wizard, DirectPlay wrapper и joystick
|
||||
adapter подтверждаются одной машинной реализацией. Model/agent runtime,
|
||||
collision, effects, shell/composition и service layer требуют отдельного
|
||||
сравнения поведения Частей 1 и 2.
|
||||
|
||||
Для `World3D.dll` Частей 1 и 2 применим общий hash:
|
||||
|
||||
```text
|
||||
World3D.dll SHA-256
|
||||
17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e
|
||||
```
|
||||
|
||||
RVA внутри `iron3d.dll` нельзя считать общими без проверки конкретного файла:
|
||||
эта DLL пересобрана между частями, а демоверсия имеет отдельный binary profile.
|
||||
Смысловая последовательность цикла переносится как контракт scheduler-а, но
|
||||
адреса остаются build-specific.
|
||||
@@ -0,0 +1,561 @@
|
||||
# III. Ресурсная система и форматы
|
||||
|
||||
Ресурсная система Iron3D переводит имена из миссий и прототипов в объекты,
|
||||
которыми пользуются подсистемы мира, рендера, анимации, звука, эффектов и
|
||||
управления. В этом пути участвуют несколько разных сущностей: файл на диске,
|
||||
открытый архив, запись каталога, подготовленный payload и готовый runtime-объект.
|
||||
Их нельзя смешивать, потому что у каждого уровня свой срок жизни, свои правила
|
||||
кэширования и свой набор проверок.
|
||||
|
||||
Основной контейнер ресурсов -- [NRes](../reference/nres.md). Он используется как
|
||||
внешний архив (`objects.rlb`, `Material.lib`, `Textures.lib`) и как внутренний
|
||||
контейнер модели `*.msh`. Второй библиотечный формат -- [RsLi](../reference/rsli.md):
|
||||
его каталог находится в начале файла, а payload может храниться raw, через
|
||||
потоковое преобразование, LZSS, адаптивный Huffman + LZSS или raw Deflate.
|
||||
Визуальная часть прототипа дальше проходит через [MSH](../reference/msh.md),
|
||||
[WEAR/MAT0](../reference/materials.md) и [Texm](../reference/texm.md), но этот
|
||||
том описывает именно ресурсный слой: как найти, проверить, раскрыть и сохранить
|
||||
данные до передачи их предметным подсистемам.
|
||||
|
||||
```text
|
||||
TMA или unit DAT
|
||||
-> логический ключ
|
||||
-> objects.rlb
|
||||
-> archive.rlb :: model.msh
|
||||
-> model.wea
|
||||
-> Material.lib :: MAT0
|
||||
-> Textures.lib / LightMap.lib :: Texm
|
||||
```
|
||||
|
||||
На демо-корпусе эта цепочка проверена целиком для всех реально размещённых
|
||||
объектов. При этом полная таблица прототипов может содержать ссылки на контент,
|
||||
которого нет в урезанной поставке. Диагностика должна различать недостижимую
|
||||
ссылку в общем реестре и ресурс, реально требуемый выбранной миссией.
|
||||
|
||||
## Ресурсный конвейер
|
||||
|
||||
Загрузка ресурса состоит из последовательных стадий:
|
||||
|
||||
1. Разрешить относительный путь с учётом глобального resource path и текущего
|
||||
каталога игры.
|
||||
2. Открыть архив или вернуть уже открытый archive object из кэша.
|
||||
3. Найти запись каталога по имени, не меняя исходный порядок каталога.
|
||||
4. Проверить bounds, размер payload и способ хранения.
|
||||
5. Подготовить bytes: распаковать, применить потоковое преобразование или
|
||||
вернуть raw-диапазон.
|
||||
6. Разобрать предметный формат и создать объект подсистемы.
|
||||
7. Сохранить готовый объект в отдельном кэше, если формат допускает повторное
|
||||
использование.
|
||||
|
||||
Эти стадии дают четыре независимых уровня кэша:
|
||||
|
||||
1. Открытые архивы.
|
||||
2. Каталоги имён, offsets и размеров.
|
||||
3. Подготовленные блоки данных.
|
||||
4. Кэши моделей, материалов, текстур, lightmaps, эффектов и служебных объектов.
|
||||
|
||||
Повторное открытие того же нормализованного пути возвращает существующий
|
||||
archive object и увеличивает счётчик владельцев. Готовая texture или model при
|
||||
этом может жить дольше file handle и иметь собственную политику удаления. Кэш
|
||||
предметного объекта не должен напрямую закрывать архив: он зависит от данных,
|
||||
но не владеет файлом как ресурсом операционной системы.
|
||||
|
||||
## Имена и пути
|
||||
|
||||
Большинство игровых имён сравнивается без учёта регистра в ASCII-диапазоне. Это
|
||||
не Unicode case folding. Для совместимости достаточно нормализовать `A..Z` в
|
||||
`a..z`, а для RsLi-поиска -- переводить запрос в uppercase ASCII и укладывать его
|
||||
в фиксированный ключ.
|
||||
|
||||
Фиксированные строки читаются bounded parser-ом: строковая часть заканчивается
|
||||
на первом NUL, но оставшийся хвост поля сохраняется. Нельзя очищать хвосты,
|
||||
пересобирать регистр, заменять смешанные разделители или заранее переводить все
|
||||
пути в абсолютные имена. Старые данные используют исторические имена библиотек,
|
||||
разный регистр исходных путей и фиксированные поля, где после терминатора могут
|
||||
оставаться значимые для roundtrip bytes.
|
||||
|
||||
## Строгий и совместимый режимы
|
||||
|
||||
Строгий reader нужен тестам, редактору и проверке корпуса. Он валидирует
|
||||
структуру до выдачи любого `EntryView`: magic, версию, счётчики, арифметические
|
||||
переполнения, bounds, sort permutation, alignment и точное завершение payload.
|
||||
Если формат требует NUL-терминатор, строгий режим проверяет его именно в пределах
|
||||
фиксированного поля.
|
||||
|
||||
Совместимый reader повторяет только известные особенности оригинала:
|
||||
|
||||
- линейный поиск при повреждённой сортировочной таблице;
|
||||
- RsLi-исключение `deflate_eof_plus_one` для `sprites.lib::INTERF8.TEX`;
|
||||
- material fallbacks, подтверждённые ресурсной цепочкой;
|
||||
- отсутствие геометрии у системных и солнечных объектов, где mesh pass не
|
||||
требуется.
|
||||
|
||||
Режим совместимости не должен скрывать произвольные ошибки. Каждое послабление
|
||||
оформляется как именованное правило и покрывается отдельным тестом. Если quirk
|
||||
применим только к Deflate-записи, он не распространяется на LZSS, Huffman или
|
||||
raw-диапазоны.
|
||||
|
||||
## NRes
|
||||
|
||||
`NRes` хранит произвольные именованные payload и их атрибуты. Каталог расположен
|
||||
в конце файла, поэтому начало каталога вычисляется из полного размера файла и
|
||||
числа записей.
|
||||
|
||||
```text
|
||||
[Header: 16 байт]
|
||||
[Data region: payload с выравниванием]
|
||||
[Directory: entry_count x 64 байта]
|
||||
```
|
||||
|
||||
Все числа little-endian.
|
||||
|
||||
```c
|
||||
struct NResHeader16 {
|
||||
char magic[4]; // "NRes"
|
||||
uint32_t version; // 0x00000100
|
||||
int32_t entry_count; // >= 0
|
||||
uint32_t total_size; // равен фактическому размеру файла
|
||||
};
|
||||
```
|
||||
|
||||
Производные значения:
|
||||
|
||||
```text
|
||||
directory_size = entry_count * 64
|
||||
directory_offset = total_size - directory_size
|
||||
```
|
||||
|
||||
Reader проверяет, что `directory_offset >= 16`, умножение не переполнено, а
|
||||
каталог заканчивается точно на `total_size`.
|
||||
|
||||
### Запись каталога NRes
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct NResEntry64 {
|
||||
uint32_t type_id; // +0x00
|
||||
uint32_t attr1; // +0x04
|
||||
uint32_t attr2; // +0x08
|
||||
uint32_t size; // +0x0C
|
||||
uint32_t attr3; // +0x10
|
||||
char name[36]; // +0x14
|
||||
uint32_t data_offset; // +0x38
|
||||
uint32_t sort_index; // +0x3C
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Имя содержит не более 35 полезных байт и завершающий ноль. Writer запрещает
|
||||
внутренний NUL и слишком длинное имя, но сохраняет неизвестные атрибуты
|
||||
`attr1`, `attr2`, `attr3` без нормализации. Их смысл зависит от конкретного
|
||||
типа ресурса и не может быть выведен из контейнера.
|
||||
|
||||
Поле `sort_index` задаёт отображение из позиции в отсортированном списке в
|
||||
исходный индекс записи. Каталог остаётся в исходном порядке. Поиск идёт по
|
||||
отсортированному отображению, но возвращает исходную запись. При сохранении
|
||||
writer строит массив исходных индексов, сортирует его по ASCII-case-insensitive
|
||||
именам и записывает результат в `sort_index`. Если отображение нельзя использовать
|
||||
или оно не является перестановкой в строгом режиме, совместимый путь переходит к
|
||||
последовательному сравнению имён.
|
||||
|
||||
### Размещение данных NRes
|
||||
|
||||
Каждый active payload должен лежать после 16-байтового заголовка и полностью до
|
||||
начала каталога. Канонические игровые файлы выравнивают начало следующего
|
||||
payload до границы 8 байт нулевым заполнением.
|
||||
|
||||
Порядок canonical save:
|
||||
|
||||
1. Записать временный заголовок.
|
||||
2. Записать payload всех записей в текущем порядке.
|
||||
3. После каждого блока добавить нули до кратности 8.
|
||||
4. Построить таблицу поиска имён.
|
||||
5. Дописать каталог.
|
||||
6. Записать окончательный `total_size`.
|
||||
|
||||
Строгий reader выполняет проверки до выдачи записи:
|
||||
|
||||
- `magic == "NRes"` и `version == 0x100`;
|
||||
- `entry_count >= 0`, а `entry_count * 64` вычисляется без переполнения;
|
||||
- `total_size` равен фактической длине файла;
|
||||
- `directory_offset = total_size - entry_count * 64` не меньше 16;
|
||||
- для каждой записи `data_offset >= 16` и `data_offset + size <= directory_offset`;
|
||||
- поле имени содержит NUL в пределах 36 байт;
|
||||
- каждый `sort_index < entry_count`;
|
||||
- в строгом режиме все `sort_index` образуют перестановку `0..N-1`.
|
||||
|
||||
Нулевое заполнение до границы 8 байт -- подтверждённое поведение игровых
|
||||
архивов и canonical writer-а. Reader не должен считать ненулевой gap частью
|
||||
соседнего payload, но lossless-редактор сохраняет исходные bytes, если файл
|
||||
открыт не в режиме канонической пересборки.
|
||||
|
||||
### Неплотная data region
|
||||
|
||||
Проверка 120 NRes-файлов / 6 804 entries Части 1 и 134 файлов / 8 171 entries
|
||||
Части 2 не выявила нарушений magic, version, total size, bounds, sort
|
||||
permutation, ASCII-order, 8-byte alignment или перекрытий активных payload.
|
||||
Однако `Textures.lib` Части 2 содержит большой ненулевой диапазон в data region,
|
||||
который не адресуется ни одной записью каталога. Первый активный payload
|
||||
начинается значительно позже начала файла, а каталог и все активные entries
|
||||
остаются корректными.
|
||||
|
||||
Следовательно, parser не должен требовать плотного покрытия data region. Нужно
|
||||
различать три вида диапазонов:
|
||||
|
||||
- `active payload` -- bytes, на которые указывает запись каталога;
|
||||
- `gap/padding` -- bytes между активными диапазонами;
|
||||
- `unindexed preserved region` -- произвольные bytes, не принадлежащие ни одной
|
||||
записи.
|
||||
|
||||
Canonical compact writer может исключить unindexed region только при явной
|
||||
операции repack. Lossless editor сохраняет её побайтно вместе с исходным
|
||||
порядком entries и gaps.
|
||||
|
||||
## RsLi
|
||||
|
||||
`RsLi` -- библиотечный архив с каталогом в начале файла. Записи могут храниться
|
||||
в исходном виде или проходить один из поддержанных путей подготовки.
|
||||
|
||||
```text
|
||||
[Header: 32 байта]
|
||||
[Entry table: entry_count x 32 байта]
|
||||
[Payloads]
|
||||
[необязательный trailer]
|
||||
```
|
||||
|
||||
Заголовок начинается с двух байт `NL`. Версия равна `1`, число записей хранится
|
||||
как знаковое 16-битное значение. Поле по смещению `0x0E` может содержать
|
||||
`0xABBA`: это означает, что отображение сортировки уже подготовлено.
|
||||
|
||||
Подтверждённые поля header:
|
||||
|
||||
```text
|
||||
+0x00 char[2] "NL"
|
||||
+0x02 u8 reserved, в корпусе 0
|
||||
+0x03 u8 version, в корпусе 1
|
||||
+0x04 i16 entry_count
|
||||
+0x0E u16 presorted_flag, значение 0xABBA
|
||||
+0x14 u32 xor_seed
|
||||
```
|
||||
|
||||
Остальные bytes заголовка сохраняются без нормализации.
|
||||
|
||||
### Запись каталога RsLi
|
||||
|
||||
После подготовки таблицы каждая запись имеет layout 32 байта:
|
||||
|
||||
```c
|
||||
struct RsLiEntry32 {
|
||||
char name[12];
|
||||
uint8_t service[4];
|
||||
int16_t flags;
|
||||
int16_t sort_to_original;
|
||||
uint32_t unpacked_size;
|
||||
uint32_t data_offset_raw;
|
||||
uint32_t packed_size;
|
||||
};
|
||||
```
|
||||
|
||||
Имя обычно хранится в uppercase ASCII. Четыре служебных байта после имени
|
||||
сохраняются без изменения. `sort_to_original` играет ту же роль, что и
|
||||
`sort_index` в NRes: связывает отсортированную позицию с исходной записью.
|
||||
|
||||
Таблица на диске проходит обратимое побайтовое преобразование. Начальное
|
||||
состояние берётся из младших 16 бит `xor_seed`. Если обозначить два байта
|
||||
состояния как `lo` и `hi`, для каждого входного байта выполняется:
|
||||
|
||||
```text
|
||||
lo = hi XOR ((lo << 1) mod 256)
|
||||
out = in XOR lo
|
||||
hi = lo XOR (hi >> 1)
|
||||
```
|
||||
|
||||
Операция симметрична: один и тот же цикл используется для подготовки и
|
||||
восстановления. Состояние непрерывно проходит по всей таблице; его нельзя
|
||||
перезапускать на каждой записи.
|
||||
|
||||
### Способы хранения RsLi
|
||||
|
||||
Способ определяется выражением `flags & 0x1E0`:
|
||||
|
||||
```text
|
||||
0x000 исходный блок
|
||||
0x020 только потоковое байтовое преобразование
|
||||
0x040 LZSS
|
||||
0x060 преобразование, затем LZSS
|
||||
0x080 адаптивный Huffman, затем LZSS
|
||||
0x0A0 преобразование, адаптивный Huffman и LZSS
|
||||
0x100 raw Deflate без оболочки zlib
|
||||
```
|
||||
|
||||
Reader обязан различать все значения, а неизвестную маску отклонять как
|
||||
неподдерживаемую. После любого пути должно быть получено ровно `unpacked_size`
|
||||
байт. Методы `0x080` и `0x0A0` подтверждены decoder-кодом и синтетическими
|
||||
тестами, но живых payload этих веток в проверенных RsLi-файлах не найдено.
|
||||
|
||||
Параметры LZSS:
|
||||
|
||||
- размер кольцевого окна -- `4096`;
|
||||
- начальное заполнение -- байт `0x20`;
|
||||
- начальная позиция -- `0xFEE`;
|
||||
- управляющие признаки читаются от младшего бита к старшему;
|
||||
- двухбайтовая ссылка кодирует 12-битную позицию и длину `n + 3`;
|
||||
- восстановленные bytes сразу записываются обратно в кольцевое окно.
|
||||
|
||||
В конце файла может находиться шестибайтовый media overlay trailer: два символа
|
||||
`AO` и 32-битное значение `overlay`. В таком режиме фактическая позиция блока
|
||||
равна `data_offset_raw + overlay`. Reader сначала проверяет, что overlay не
|
||||
выходит за размер отображённого файла, затем проверяет весь диапазон записи.
|
||||
|
||||
### Поиск, кэш и проверки RsLi
|
||||
|
||||
Запрос имени переводится в uppercase ASCII и укладывается в фиксированный ключ.
|
||||
При признаке `0xABBA` используется сохранённое отображение сортировки. Если
|
||||
признака нет, loader строит его после чтения каталога. Некорректный индекс
|
||||
приводит к последовательному поиску.
|
||||
|
||||
Файл открывается через memory mapping. Runtime-запись хранит указатель на
|
||||
упакованный диапазон, размеры и необязательный указатель на подготовленные
|
||||
данные. Первый обычный `load` создаёт буфер и сохраняет результат; повторный
|
||||
возвращает его из кэша. Быстрый путь может вернуть указатель непосредственно в
|
||||
mapped file только для исходного блока.
|
||||
|
||||
Reader проверяет:
|
||||
|
||||
- сигнатуру `NL`, служебный байт и версию;
|
||||
- неотрицательное число записей;
|
||||
- размещение всей таблицы в файле;
|
||||
- что сохранённое отображение сортировки является перестановкой;
|
||||
- что эффективный диапазон каждого блока не выходит за конец файла;
|
||||
- что способ хранения известен;
|
||||
- что после подготовки получено ровно `unpacked_size` байт.
|
||||
|
||||
В demo-каталоге и полных каталогах обеих частей наблюдаются два RsLi-файла:
|
||||
|
||||
```text
|
||||
gamefont.rlb 2 entries, все 0x040 LZSS
|
||||
sprites.lib 24 entries, все 0x100 raw Deflate
|
||||
```
|
||||
|
||||
Последняя запись `sprites.lib::INTERF8.TEX` объявляет packed range, который
|
||||
заканчивается на один байт после физического EOF. Совместимый путь читает на
|
||||
один байт меньше; строгий путь регистрирует именованный quirk
|
||||
`deflate_eof_plus_one`. Это исключение не распространяется на другие записи,
|
||||
методы или произвольные выходы за конец файла.
|
||||
|
||||
Writer, который редактирует существующий архив, сохраняет все служебные bytes
|
||||
заголовка и записей. Выбор оптимального способа упаковки для новых файлов
|
||||
является отдельной политикой и не должен менять уже существующие entries без
|
||||
явного запроса.
|
||||
|
||||
## Реестр объектов
|
||||
|
||||
Имя объекта в миссии является логическим ключом. Связь этого ключа с файлами
|
||||
модели, материалов и служебных данных хранится в `objects.rlb`, который сам
|
||||
использует формат NRes. Имя записи каталога -- ключ прототипа. Payload записи
|
||||
состоит из записей по 64 байта:
|
||||
|
||||
```c
|
||||
struct ObjectRef64 {
|
||||
char archive_name[32];
|
||||
char resource_name[32];
|
||||
};
|
||||
```
|
||||
|
||||
Payload каждой записи `objects.rlb` обязан быть кратен 64 байтам. Это
|
||||
проверяется до чтения первой ссылки. Оба поля читаются как строки до первого
|
||||
NUL, но полный 32-байтовый блок сохраняется при редактировании без очистки
|
||||
хвоста.
|
||||
|
||||
Разрешение прототипа:
|
||||
|
||||
1. Найти entry реестра по логическому ключу без учёта ASCII-регистра.
|
||||
2. Прочитать все `ObjectRef64` в исходном порядке.
|
||||
3. Если ссылка указывает обратно в `objects.rlb`, рекурсивно раскрыть указанный
|
||||
родительский prototype.
|
||||
4. Объединить effective references родителя с локальными references дочерней
|
||||
записи, сохранив порядок и происхождение.
|
||||
5. Выбрать первую существующую ссылку с расширением `.msh`, открыть указанный
|
||||
архив и найти модель по имени.
|
||||
6. Загружать `.bas` как отдельный служебный ресурс сооружения, а не как замену
|
||||
MSH.
|
||||
7. Если effective prototype не содержит MSH, считать объект негеометрическим,
|
||||
если это допускает его назначение.
|
||||
|
||||
Resolver обязан детектировать циклы наследования, ограничивать глубину и
|
||||
кэшировать результат раскрытия. В обеих частях fortification-прототипы используют
|
||||
явного родителя из `objects.rlb`: родитель предоставляет MSH/WEAR/CPT/NDP/CTL,
|
||||
а дочерняя запись добавляет собственный BASE. Негеометрический объект не является
|
||||
ошибкой сам по себе: системные и солнечные сущности могут участвовать в логике
|
||||
или эффектах без mesh pass.
|
||||
|
||||
Контракт реализации:
|
||||
|
||||
- сохранять порядок ссылок внутри прототипа;
|
||||
- не выводить имя модели из имени entry, если имеется явная ссылка;
|
||||
- проверять существование указанного архива и ресурса независимо;
|
||||
- отделять статус «негеометрический объект» от статуса «повреждённая ссылка»;
|
||||
- кэшировать результат разрешения ключа, но инвалидировать его при замене архива;
|
||||
- в diagnostic mode строить полный граф зависимостей и отмечать узлы, достижимые
|
||||
из выбранной миссии.
|
||||
|
||||
В demo-варианте `objects.rlb` содержит 590 прототипов. У 554 есть прямая ссылка
|
||||
на MSH; 549 таких ссылок разрешаются в доступных demo-архивах. Ещё 34 прототипа
|
||||
раскрываются через родительскую запись `objects.rlb` и дополняются локальным
|
||||
BASE. Семь записей не дают геометрию, а 41 ссылка всего реестра указывает на
|
||||
контент, которого нет в урезанной поставке. Для 501 запросов прототипов,
|
||||
порождаемых шестью demo-миссиями, найдены прототип, MSH и WEAR.
|
||||
|
||||
## Unit DAT
|
||||
|
||||
Запись миссии может ссылаться не на один ключ, а на unit-файл `*.dat`. Такой файл
|
||||
перечисляет компоненты сложного игрового объекта.
|
||||
|
||||
```text
|
||||
TMA object
|
||||
-> путь к unit DAT
|
||||
-> список component keys
|
||||
-> несколько entries objects.rlb
|
||||
-> модели, WEAR, control points, effects и другие ресурсы
|
||||
```
|
||||
|
||||
Это объясняет, почему один размещённый unit может состоять из корпуса, башен,
|
||||
оружия, эффектов и служебных частей. В демоверсии найдено 425 unit-файлов и
|
||||
5 219 записей; все разобраны без ошибок. Наблюдаемый тип записи равен `1`, а
|
||||
архив назначения -- `objects.rlb`. В 5 205 из 5 219 фиксированных полей имени
|
||||
обнаружены ненулевые bytes после строкового терминатора; reader использует
|
||||
строковую часть, а lossless writer сохраняет весь исходный блок.
|
||||
|
||||
Размер каждого unit DAT удовлетворяет формуле:
|
||||
|
||||
```text
|
||||
file_size = 8 + record_count * 112
|
||||
```
|
||||
|
||||
Первые два байта header равны `F1 F0`. Оставшиеся шесть bytes имеют несколько
|
||||
наблюдаемых вариантов; их семантика пока не названа и они сохраняются как
|
||||
`header_opaque[6]`.
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct UnitDatRecord112 {
|
||||
char archive_name[32]; // +0x00
|
||||
char resource_name[32]; // +0x20
|
||||
uint32_t kind; // +0x40, в корпусе всегда 1
|
||||
int32_t parent_or_link; // +0x44
|
||||
char description[32]; // +0x48
|
||||
uint32_t tail0; // +0x68, opaque
|
||||
uint32_t tail1; // +0x6C, opaque
|
||||
};
|
||||
#pragma pack(pop)
|
||||
```
|
||||
|
||||
Во всех проверенных records `archive_name == "objects.rlb"` и `kind == 1`.
|
||||
Поле `parent_or_link` встречается как `-1`, `0`, `1` и другие небольшие индексы
|
||||
и связывает компоненты составного unit; точная предметная классификация ссылки
|
||||
ещё не закрыта. `description` -- человекочитаемое описание компонента. В Части 2
|
||||
есть поля `description[32]`, полностью заполненные без NUL; это валидная bounded
|
||||
string длиной 32 байта. Требование обязательного terminator применяется только
|
||||
к полям, где оно доказано форматом. `tail0` и `tail1` нельзя нормализовать.
|
||||
|
||||
Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records
|
||||
Части 2. Все соответствуют формуле размера, `kind == 1` и
|
||||
`archive_name == "objects.rlb"`.
|
||||
|
||||
## Вспомогательные форматы
|
||||
|
||||
MSH, материал и текстура отвечают за видимую форму. Полноценный прототип
|
||||
дополнительно хранит точки крепления, зависимости, управляющие параметры,
|
||||
области взаимодействия и ссылки на эффекты. Эти данные распределены между
|
||||
несколькими небольшими форматами.
|
||||
|
||||
Для них действует строгая граница знания: framing, counts и валидность корпуса
|
||||
могут быть подтверждены parser-ом, тогда как предметный смысл части полей
|
||||
остаётся неизвестным. Reader предоставляет typed view для доказанных полей и
|
||||
raw bytes для остальных. Инструмент должен показывать статус поля:
|
||||
`layout-confirmed`, `consumer-inferred` или `opaque`.
|
||||
|
||||
### CTPT
|
||||
|
||||
В demo-корпусе найдено 284 CTPT-ресурса и 3 599 точек; все прочитаны без ошибок.
|
||||
Имена показывают назначение слоя: `TurretCenter`, `TurretDirect`,
|
||||
`CameraCenter`, `TargetDirect`, `Root`, `Sfx_1`, `Sign_Entrance1`, `Width`,
|
||||
`Height`, `Dir`.
|
||||
|
||||
CTPT хранит локальные marker-точки модели. После применения transform такая точка
|
||||
становится позицией или направлением в мире. Оружие может использовать её для
|
||||
дула или оси башни, камера -- для привязки обзора, эффект -- для точки появления.
|
||||
Конкретное назначение определяется именем и consumer-ом, а не одним общим флагом.
|
||||
Первое 32-битное поле чаще равно `0`; встречаются `0x80000000` и редкий
|
||||
вариант. До установления точной семантики оно хранится как `flags_raw`.
|
||||
|
||||
### NDPR
|
||||
|
||||
Проверено 494 NDPR-ресурса и 1 915 записей. Они ссылаются на `animals.rlb`,
|
||||
`system.rlb`, `static.rlb`, `turrets.rlb`, `weapon.rlb` или используют пустое
|
||||
имя архива. В 89 записях присутствует связанный эффект. Пустое имя архива
|
||||
разрешается относительно текущего контекста. Reader хранит ссылку и остальные
|
||||
параметры раздельно; writer сохраняет исходный порядок.
|
||||
|
||||
### EXPL и reference arrays
|
||||
|
||||
Проверено 144 ресурса EXPL: 26 используют версию 1, 54 -- версию 2, 64 --
|
||||
версию 3. Reader выбирает layout по version field и требует точного завершения
|
||||
payload. Полная field-level семантика всех версий пока не доказана, поэтому
|
||||
version-specific opaque sections сохраняются.
|
||||
|
||||
Отдельная проверенная группа из 585 ресурсов содержит 2 956 однотипных
|
||||
ссылочных records. Их границы и counts закрыты, однако единое предметное имя
|
||||
всего семейства не подтверждено всеми consumers. В API безопаснее использовать
|
||||
нейтральное `ReferenceArray` и конкретизировать назначение на уровне типа entry.
|
||||
|
||||
### SUND и CTLD
|
||||
|
||||
Два ресурса SUND содержат суммарно 12 ключей. Их следует загружать как параметры
|
||||
системного объекта, а не как геометрию.
|
||||
|
||||
Для CTLD проверено 531 payload. Размеры и сочетания счётчиков сильно различаются,
|
||||
поэтому parser должен быть версионно- и счётчик-ориентированным, а неизвестные
|
||||
секции -- храниться в исходном виде.
|
||||
|
||||
### TRF, ANI и SKE
|
||||
|
||||
В демоверсии обнаружены 5 файлов TRF, 38 preload-записей, 8 ANI-ресурсов и
|
||||
6 SKE-ресурсов. Все проходят структурный разбор. Эти семейства участвуют в
|
||||
подготовке компонентов и анимационных или управляющих данных до создания
|
||||
runtime-объекта.
|
||||
|
||||
Поскольку живой корпус невелик, редактор не должен синтезировать новые варианты
|
||||
этих форматов по догадке. Безопасный режим -- читать доказанные счётчики и
|
||||
ссылки, предоставлять raw-view неизвестных секций и обеспечивать побайтовое
|
||||
сохранение неизменённых данных.
|
||||
|
||||
### BASE
|
||||
|
||||
Проверено 30 BASE-ресурсов; каждый содержит ровно один polygon record и проходит
|
||||
структурную проверку. BASE payload и ссылка `.bas` в `objects.rlb` выполняют
|
||||
связанные, но разные роли:
|
||||
|
||||
- наличие ссылки `.bas` позволяет registry resolver-у искать одноимённый
|
||||
`<stem>.msh` в том же архиве;
|
||||
- сам BASE payload загружается отдельной подсистемой сооружений и не заменяет
|
||||
MSH geometry.
|
||||
|
||||
Resolver не должен интерпретировать bytes BASE как mesh. Writer сохраняет
|
||||
polygon record и неизвестные поля 1:1, пока полный gameplay-контракт BASE не
|
||||
подтверждён.
|
||||
|
||||
## Правило сохранения
|
||||
|
||||
Lossless editor сохраняет неизвестные поля, хвосты фиксированных строк,
|
||||
служебные bytes, gaps, padding и unindexed regions. Writer пересчитывает только
|
||||
явно производные значения: размеры, offsets, число записей, сортировочную
|
||||
перестановку и padding. Такая дисциплина позволяет редактировать известную
|
||||
часть ресурса, не разрушая данные, смысл которых пока не установлен.
|
||||
|
||||
Canonical repack допустим только как явная операция. Он может исключать
|
||||
неиндексируемые диапазоны, пересортировывать таблицы и пересобирать padding, но
|
||||
не должен быть побочным эффектом обычного редактирования. Если пользователь
|
||||
открыл существующий архив и изменил один известный атрибут, все остальные bytes,
|
||||
не являющиеся производными от этого изменения, должны пройти roundtrip без
|
||||
потери.
|
||||
@@ -0,0 +1,648 @@
|
||||
# IV. Мир, миссии и игровой runtime
|
||||
|
||||
Миссия в Iron3D не является готовым снимком мира. Она задаёт исходные данные:
|
||||
маршруты, кланы, размещённые объекты, свойства, ссылку на ландшафт и
|
||||
дополнительные записи. Runtime строит из этого карту, пространственные
|
||||
структуры, очередь `World3D`, визуальные представления, controllers и связи с
|
||||
ресурсной системой.
|
||||
|
||||
Для совместимой реализации важно не смешивать три слоя:
|
||||
|
||||
1. **Disk data** -- `data.tma`, `Land.msh`, `Land.map`, `BuildDat.lst` и
|
||||
связанные resource archives.
|
||||
2. **Prepared data** -- разобранные paths, clans, terrain streams, areal graph,
|
||||
prototype graph, material и texture handles.
|
||||
3. **Runtime objects** -- World3D instances, domain controllers, spatial
|
||||
registration, AI/scripts, timers и расчётный tick.
|
||||
|
||||
Граница между этими слоями нужна для диагностики и отката. Ошибка в достижимой
|
||||
цепочке размещённого объекта должна остановить создание миссии до публикации
|
||||
объекта в очереди событий. Недостижимая запись общего архива может быть
|
||||
inventory warning и не обязана блокировать текущую карту.
|
||||
|
||||
## `data.tma`: данные миссии
|
||||
|
||||
`data.tma` -- основное описание расстановки и логической конфигурации миссии.
|
||||
Он не содержит всю геометрию, материалы или AI-код. Файл перечисляет paths,
|
||||
clans, objects, свойства и ссылки на внешние прототипы. Подробный справочный
|
||||
контракт формата вынесен в [TMA](../reference/tma.md), но глава использует его
|
||||
как часть сквозного runtime pipeline.
|
||||
|
||||
TMA читается строго последовательно bounded cursor-ом. Записи имеют переменную
|
||||
длину, поэтому offsets следующих секций получаются только после разбора
|
||||
предыдущих. Секции нельзя искать по сигнатурам: порядок управляется счётчиками,
|
||||
длинами и mode-dependent ветками.
|
||||
|
||||
Главный критерий корректности -- `cursor.offset == file_size` после последней
|
||||
записи. Неописанный хвост, переполнение при вычислении размеров, отрицательный
|
||||
или чрезмерный count и выход за bounds являются ошибками parser-а, а не
|
||||
материалом для эвристического восстановления.
|
||||
|
||||
### Верхний уровень
|
||||
|
||||
Все переменные строки в проверенных TMA используют length-prefixed primitive:
|
||||
|
||||
```c
|
||||
struct LpString {
|
||||
uint32_t byte_length;
|
||||
uint8_t bytes[byte_length];
|
||||
};
|
||||
```
|
||||
|
||||
Завершающий NUL не является обязательной частью framing. Reader продвигается
|
||||
ровно на `4 + byte_length`. Текст можно декодировать как legacy ANSI/CP1251 для
|
||||
человекочитаемого представления, но исходные bytes сохраняются для lossless
|
||||
режима.
|
||||
|
||||
Подтверждённый верхний уровень:
|
||||
|
||||
```text
|
||||
u32 format_version // 1
|
||||
u32 path_count
|
||||
PathRecord paths[path_count]
|
||||
u32 clan_section_version // 6
|
||||
u32 clan_count
|
||||
ClanRecord clans[clan_count]
|
||||
u32 object_section_version // 10
|
||||
u32 object_count
|
||||
PlacedObject objects[object_count]
|
||||
LpString land_path
|
||||
u32 mission_flag
|
||||
LpString description_raw
|
||||
u32 extra_section_version // 1
|
||||
u32 extra_count
|
||||
ExtraRecord28 extras[extra_count]
|
||||
```
|
||||
|
||||
Имена `clan_section_version`, `object_section_version` и
|
||||
`extra_section_version` описывают устойчивое положение полей в контракте. Они
|
||||
не доказывают исходные имена C++-структур. Strict mode проверяет известные
|
||||
значения, compatible mode сохраняет raw value и сообщает диагностический
|
||||
контекст.
|
||||
|
||||
### Paths
|
||||
|
||||
```c
|
||||
struct PathRecord {
|
||||
int32_t path_id;
|
||||
uint32_t point_count;
|
||||
float points[point_count][3];
|
||||
};
|
||||
```
|
||||
|
||||
Paths идут сразу после `path_count` без имён и padding. `path_id` не обязан
|
||||
совпадать с физической позицией записи: script/gameplay reference должен
|
||||
использовать сохранённый ID, а не индекс массива.
|
||||
|
||||
Перед выделением массива проверяются `point_count`, умножение `point_count *
|
||||
12` и наличие всего диапазона в файле. Координаты хранятся как little-endian
|
||||
`float32` triples в общей системе координат мира.
|
||||
|
||||
### Clans
|
||||
|
||||
Clan section задаёт участников миссии, их ресурсные связи, позиционные anchors
|
||||
и таблицы отношений. Общая prefix-часть:
|
||||
|
||||
```text
|
||||
LpString name
|
||||
i32 raw_id
|
||||
f32 anchor_x
|
||||
f32 anchor_y
|
||||
u32 mode
|
||||
mode-dependent body
|
||||
relation table
|
||||
```
|
||||
|
||||
Для обычных modes `1..3` тело содержит две пары:
|
||||
|
||||
```text
|
||||
LpString resource_path
|
||||
i32 resource_tag
|
||||
LpString resource_path
|
||||
i32 resource_tag
|
||||
```
|
||||
|
||||
После них идёт relation table:
|
||||
|
||||
```text
|
||||
u32 relation_count
|
||||
repeat relation_count:
|
||||
LpString other_clan_name
|
||||
i32 relation_value
|
||||
```
|
||||
|
||||
Первая ресурсная строка обычно указывает на script/formula base, вторая -- на
|
||||
TRF или пустой ресурс. Tags различаются между кланами и должны сохраняться как
|
||||
raw-поля, пока их потребительская семантика не закрыта.
|
||||
|
||||
Mode `0` имеет отдельный count-driven layout:
|
||||
|
||||
```text
|
||||
LpString first_resource
|
||||
u32 spatial_group_count
|
||||
repeat spatial_group_count:
|
||||
u32 record_count
|
||||
repeat record_count:
|
||||
float raw_spatial[5]
|
||||
LpString second_resource
|
||||
i32 second_tag
|
||||
u32 relation_count
|
||||
relations...
|
||||
```
|
||||
|
||||
Внутренний `record_count` в известных живых образцах равен `1`, но parser читает
|
||||
объявленное значение. Нельзя разбирать mode `0` как обычные две resource
|
||||
references: это сдвигает cursor и ломает последующую relation table.
|
||||
|
||||
### PlacedObject и свойства
|
||||
|
||||
Ключевое поле размещённого объекта -- `resource_name`. Оно имеет два рабочих
|
||||
варианта:
|
||||
|
||||
1. прямой логический ключ прототипа, который ищется в `objects.rlb`;
|
||||
2. путь к unit DAT, из которого получается список компонентных ключей.
|
||||
|
||||
Доказанное framing объектной записи:
|
||||
|
||||
```text
|
||||
u32 raw_kind
|
||||
u32 class_or_flags
|
||||
LpString resource_name
|
||||
u32 raw_after_resource
|
||||
u32 identity_or_clan_raw
|
||||
f32 position[3]
|
||||
f32 orientation[3]
|
||||
f32 scale[3]
|
||||
LpString instance_name
|
||||
u32 raw_after_name
|
||||
i32 link0
|
||||
i32 link1
|
||||
u32 property_schema_version // 1
|
||||
u32 property_count
|
||||
Property properties[property_count]
|
||||
```
|
||||
|
||||
`orientation[3]` названа по наблюдаемому использованию как transform-поле, но
|
||||
точный Euler order должен подтверждаться pose/render parity. `scale` в
|
||||
большинстве записей равен `(1,1,1)`. `instance_name` может быть пустым у
|
||||
unit-ссылки или содержать stem размещённого прототипа.
|
||||
|
||||
Свойства хранятся как ordered property bag:
|
||||
|
||||
```text
|
||||
Property:
|
||||
u32 raw_value[4]
|
||||
LpString name
|
||||
```
|
||||
|
||||
Порядок, повторяемость имени и raw 16-byte value важнее удобного словаря.
|
||||
Разные consumers интерпретируют четыре слова как integer, float, default или
|
||||
range data в зависимости от имени свойства. Typed view допустим только для
|
||||
доказанных property names; базовый parser обязан сохранить исходный порядок.
|
||||
|
||||
В раннем проверенном корпусе на каждом из 201 размещённого объекта встречаются
|
||||
`Invulnerability` и `Life state`. Для 48 unit-ссылок дополнительно наблюдаются
|
||||
`LogicalID`, `ClanID`, `Type`, `MaxSpeedPercent`, `MaximumOre`, `CurrentOre`,
|
||||
`ChargeRadius`, `FreeBotNum`, `FreeTechnoNum`, `FreeConstructionTime` и
|
||||
`FreeResearchTime`. Имя `NOT USED` встречается массово и сохраняется как
|
||||
обычное поле, несмотря на исторический смысл названия.
|
||||
|
||||
### Epilogue и extras
|
||||
|
||||
После объектов идут путь к ландшафту, флаг миссии, raw-описание и trailing
|
||||
section. `description_raw` не всегда является чистым текстом: внутри
|
||||
объявленной длины встречаются служебные bytes и остатки путей. Поэтому decoded
|
||||
view является вспомогательным, а не каноническим представлением.
|
||||
|
||||
```c
|
||||
struct ExtraRecord28 {
|
||||
float position[3];
|
||||
uint32_t raw[4];
|
||||
};
|
||||
```
|
||||
|
||||
Последние четыре слова `ExtraRecord28` пока не нормализуются. Reader хранит их
|
||||
как raw data и не позволяет extra record поглотить начало следующей секции или
|
||||
файловый хвост.
|
||||
|
||||
Покрытие полных каталогов:
|
||||
|
||||
```text
|
||||
Часть 1: 29 TMA, 34 paths, 101 clans, 864 objects, 28 extra records
|
||||
Часть 2: 31 TMA, 61 paths, 91 clans, 885 objects, 41 extra records
|
||||
```
|
||||
|
||||
Версии стабильны: верхний уровень `1`, clan section `6`, object section `10`,
|
||||
property schema `1`, trailing section `1`. У всех размещённых объектов
|
||||
`class_or_flags == 0x80000002`.
|
||||
|
||||
## Сквозная загрузка миссии
|
||||
|
||||
`data.tma` описывает размещение, но видимый runtime-объект появляется только
|
||||
после прохождения dependency graph. Простая загрузка файлов с похожим stem
|
||||
работает на отдельных объектах, но ломается на составных unit DAT, изменённых
|
||||
именах моделей и наследовании прототипов через `objects.rlb`.
|
||||
|
||||
Сквозная цепочка:
|
||||
|
||||
```text
|
||||
TMA object
|
||||
-> direct prototype key или unit DAT
|
||||
-> component key
|
||||
-> objects.rlb entry
|
||||
-> MSH и WEAR
|
||||
-> material slots
|
||||
-> MAT0 phases
|
||||
-> Texm и lightmap
|
||||
-> prepared World3D instance
|
||||
```
|
||||
|
||||
Контейнеры и графические форматы описаны отдельно в [NRes](../reference/nres.md),
|
||||
[MSH](../reference/msh.md), [WEAR и MAT0](../reference/materials.md) и
|
||||
[Texm](../reference/texm.md). В этой главе они рассматриваются как ребра
|
||||
создания мира.
|
||||
|
||||
### Фазы loader-а
|
||||
|
||||
1. **Mission context.** Выбрать каталог миссии, прочитать конфигурацию и
|
||||
определить карту.
|
||||
2. **World foundation.** Загрузить `Land.msh`, `Land.map`, `BuildDat.lst` и
|
||||
создать spatial managers.
|
||||
3. **Mission description.** Разобрать TMA, paths и clans, но пока не публиковать
|
||||
объекты.
|
||||
4. **Prototype resolution.** Для каждой размещённой сущности раскрыть прямой
|
||||
ключ или unit DAT и построить component list.
|
||||
5. **Resource preparation.** Открыть требуемые RLB/LIB, проверить MSH, WEAR,
|
||||
MAT0, textures, lightmaps и effects.
|
||||
6. **Instance construction.** Создать World3D objects и domain controllers,
|
||||
заполнить transform, ownership и properties.
|
||||
7. **Registration.** Только после успешной настройки добавить instances в
|
||||
queue и spatial structures.
|
||||
8. **Scenario start.** Подключить AI/scripts, активировать timers и разрешить
|
||||
первый calculation tick.
|
||||
|
||||
Разделение construction и registration предотвращает появление наполовину
|
||||
созданного объекта в очереди событий. Если ошибка возникает до регистрации,
|
||||
pending objects освобождаются без рассылки gameplay-событий. После регистрации
|
||||
откат выполняется через обычный lifecycle очереди.
|
||||
|
||||
### Статистика dependency graph
|
||||
|
||||
Для ранних шести миссий 201 размещённый объект даёт 48 ссылок на unit-файлы и
|
||||
153 прямых ключа. Unit-файлы раскрываются в 348 компонентов. Всего получается
|
||||
501 запрос прототипа; для каждого достижимого запроса найдены запись реестра,
|
||||
MSH и WEAR.
|
||||
|
||||
Полный dependency graph частей 1 и 2:
|
||||
|
||||
```text
|
||||
Часть 1
|
||||
864 placed objects
|
||||
463 unit references -> 4 300 components
|
||||
4 701 prototype/MSH/WEAR requests
|
||||
36 954 material slots
|
||||
48 806 texture requests + 139 lightmaps
|
||||
failures 0
|
||||
|
||||
Часть 2
|
||||
885 placed objects
|
||||
561 unit references -> 5 521 components
|
||||
5 845 prototype/MSH/WEAR requests
|
||||
50 888 material slots
|
||||
68 603 texture requests + 214 lightmaps
|
||||
failures 0
|
||||
```
|
||||
|
||||
`failures 0` означает, что для каждой достижимой ветви найдены prototype,
|
||||
effective MSH/WEAR, MAT0, Texm и lightmap. Это не означает, что во всём
|
||||
глобальном каталоге нет недостижимых или служебных записей.
|
||||
|
||||
Метрики нужно помечать областью. Чистая object chain шести ранних миссий даёт
|
||||
3 873 material slots и 5 049 texture requests. Mission total включает по одной
|
||||
environment WEAR-таблице на миссию и становится 3 879 material slots и 5 067
|
||||
texture references.
|
||||
|
||||
### Диагностика ошибок
|
||||
|
||||
Ошибка привязывается к конкретному ребру графа:
|
||||
|
||||
- миссия ссылается на отсутствующий unit-файл;
|
||||
- unit DAT раскрывается в component key, которого нет в реестре;
|
||||
- prototype найден, но его MSH отсутствует в ожидаемом archive;
|
||||
- WEAR указывает на неизвестный MAT0;
|
||||
- MAT0 phase ссылается на отсутствующий Texm или lightmap;
|
||||
- prepared object не прошёл валидацию transform/properties.
|
||||
|
||||
Сообщение вида `resource not found` недостаточно для восстановления каталога.
|
||||
Диагностика должна содержать исходный placed object, раскрытый ключ, archive,
|
||||
entry и тип связи.
|
||||
|
||||
## `Land.msh`: ландшафт как специализированная модель
|
||||
|
||||
`Land.msh` является [NRes](../reference/nres.md)-архивом, но его содержимое
|
||||
отличается от обычной объектной MSH. Он хранит геометрию поверхности, таблицы
|
||||
участков и ускорители пространственных запросов. Видимые buffers являются лишь
|
||||
частью данных: CPU-подсистемам остаются нужны adjacency, surface classes и
|
||||
cell accelerator streams.
|
||||
|
||||
Во всех проверенных картах порядок типов одинаков:
|
||||
|
||||
```text
|
||||
1, 2, 3, 4, 5, 18, 14, 11, 21
|
||||
```
|
||||
|
||||
Типы `1`, `3`, `4` и `5` совместимы по базовому представлению с узлами,
|
||||
позициями, нормалями и UV обычной модели. Типы `11` и `21` специфичны для
|
||||
terrain; `14` и `18` являются дополнительными потоками.
|
||||
|
||||
### Streams и размеры элементов
|
||||
|
||||
```text
|
||||
type 1 38 байт node/slot mapping
|
||||
type 3 12 байт float3 positions
|
||||
type 4 4 байта packed normals
|
||||
type 5 4 байта packed UV
|
||||
type 11 4 байта cell accelerator data
|
||||
type 14 4 байта auxiliary stream
|
||||
type 18 4 байта auxiliary stream
|
||||
type 21 28 байт terrain face
|
||||
```
|
||||
|
||||
Для этих streams `attr1` соответствует числу элементов, а `attr3` -- stride.
|
||||
Тип `2` начинается заголовком размером `0x8C`, после которого идут slot records
|
||||
по 68 байт. Число slots вычисляется как `(size - 0x8C) / 68`; reader проверяет
|
||||
делимость, bounds и отсутствие хвоста.
|
||||
|
||||
### `TerrainFace28`
|
||||
|
||||
Запись type `21` связывает triangles, соседей и surface metadata:
|
||||
|
||||
```text
|
||||
+0x00 .. +0x07 flags и служебные поля
|
||||
+0x08 u16 vertex0
|
||||
+0x0A u16 vertex1
|
||||
+0x0C u16 vertex2
|
||||
+0x0E u16 neighbor0
|
||||
+0x10 u16 neighbor1
|
||||
+0x12 u16 neighbor2
|
||||
+0x14 .. +0x1B material/class/edge fields
|
||||
```
|
||||
|
||||
Каждый vertex index обязан быть меньше числа позиций type `3`. Neighbor равен
|
||||
`0xFFFF` либо указывает на другой элемент type `21`. Последние восемь bytes
|
||||
сохраняются без нормализации до полного закрытия предметной семантики.
|
||||
|
||||
### Маски поверхности
|
||||
|
||||
Runtime использует полную 32-битную маску face и два compact-представления.
|
||||
Основное 16-битное поле собирается из отдельных битов полной маски; второе
|
||||
шестибитное поле хранит material classes. Это не усечение младших битов.
|
||||
|
||||
Для совместимого writer-а нужны явные функции `full_to_compact()` и
|
||||
`compact_to_full()`. Неизвестные биты полной маски сохраняются отдельно, иначе
|
||||
обратное преобразование потеряет информацию.
|
||||
|
||||
Основное соответствие:
|
||||
|
||||
```text
|
||||
full 00000001 -> compact 0001
|
||||
full 00000008 -> compact 0002
|
||||
full 00000010 -> compact 0004
|
||||
full 00000020 -> compact 0008
|
||||
full 00001000 -> compact 0010
|
||||
full 00004000 -> compact 0020
|
||||
full 00000002 -> compact 0040
|
||||
full 00000400 -> compact 0080
|
||||
full 00000800 -> compact 0100
|
||||
full 00020000 -> compact 0200
|
||||
full 00002000 -> compact 0400
|
||||
full 00000200 -> compact 0800
|
||||
full 00000004 -> compact 1000
|
||||
full 00000040 -> compact 2000
|
||||
full 00200000 -> compact 8000
|
||||
```
|
||||
|
||||
Для шестибитного material-поля используются full-биты `0x100`, `0x8000`,
|
||||
`0x10000`, `0x40000`, `0x80000` и `0x80`; они переходят соответственно в
|
||||
compact-биты `1`, `2`, `4`, `8`, `0x10`, `0x20`.
|
||||
|
||||
### Проверенное покрытие
|
||||
|
||||
```text
|
||||
AutoMAP 3 051 вершина, 3 174 faces
|
||||
PROL 11 125 вершин, 9 234 faces
|
||||
Tut_1 8 827 вершин, 8 290 faces
|
||||
Tut_2 9 456 вершин, 8 996 faces
|
||||
Tut_3 9 833 вершины, 8 560 faces
|
||||
Tut_4 9 022 вершины, 8 612 faces
|
||||
```
|
||||
|
||||
Расширенное покрытие:
|
||||
|
||||
```text
|
||||
Часть 1: 33 карты, 299 450 vertices, 275 882 faces
|
||||
Часть 2: 32 карты, 188 024 vertices, 184 454 faces
|
||||
```
|
||||
|
||||
Во всех 65 картах порядок типов равен `[1,2,3,4,5,18,14,11,21]`. Strides,
|
||||
count-driven размеры, vertex indices, neighbor indices и payload bounds
|
||||
валидны. Различия карт являются различиями данных, а не новым вариантом
|
||||
loader-а.
|
||||
|
||||
## `Land.map` и ArealMap
|
||||
|
||||
`Land.map` хранит логическое разбиение пространства на связанные области. Это
|
||||
NRes-архив с одной записью type `12`. Payload содержит переменное число
|
||||
ареалов, links и grid быстрого поиска.
|
||||
|
||||
Ареал -- участок мира с геометрической границей и метаданными. Граф соседств
|
||||
позволяет искать маршрут между крупными областями вместо обхода каждой
|
||||
terrain-вершины. Grid отвечает на быстрый вопрос: какие области потенциально
|
||||
находятся рядом с координатой.
|
||||
|
||||
### Prefix ареала
|
||||
|
||||
```c
|
||||
struct ArealPrefix56 {
|
||||
float anchor_x;
|
||||
float anchor_y;
|
||||
float anchor_z;
|
||||
float reserved_12;
|
||||
float area_metric;
|
||||
float normal_x;
|
||||
float normal_y;
|
||||
float normal_z;
|
||||
uint32_t logic_flag;
|
||||
uint32_t reserved_36;
|
||||
uint32_t class_id;
|
||||
uint32_t reserved_44;
|
||||
uint32_t vertex_count;
|
||||
uint32_t poly_count;
|
||||
};
|
||||
```
|
||||
|
||||
После prefix идут `float3 vertices[vertex_count]`. Нормаль в проверенных
|
||||
записях имеет длину, практически равную единице. Поля `reserved_12`,
|
||||
`reserved_36` и `reserved_44` в живом корпусе равны нулю, но writer сохраняет
|
||||
их без нормализации.
|
||||
|
||||
### Links и polygon blocks
|
||||
|
||||
За вершинами хранится массив:
|
||||
|
||||
```c
|
||||
struct EdgeLink8 {
|
||||
int32_t area_ref;
|
||||
int32_t edge_ref;
|
||||
};
|
||||
```
|
||||
|
||||
Пара `(-1, -1)` означает отсутствие соседа. Иначе `area_ref` указывает на
|
||||
другую область, а `edge_ref` -- на соответствующее ребро. Число пар равно
|
||||
`vertex_count + 3 * poly_count`.
|
||||
|
||||
После links для каждого polygon читается `u32 n`, затем block размером
|
||||
`4 * (3*n + 1)` bytes. Во всех 65 проверенных картах `poly_count == 0`.
|
||||
Framing ветки восстановлен по loader path, но предметное поведение polygon
|
||||
blocks не получает статус corpus-verified.
|
||||
|
||||
### Grid быстрого поиска
|
||||
|
||||
После всех ареалов записаны `cellsX` и `cellsY`. Далее для каждой ячейки идут
|
||||
`u16 hitCount` и `hitCount` номеров областей. Runtime уплотняет это в одно
|
||||
32-битное значение: старшие 10 бит содержат число попаданий, младшие 22 --
|
||||
начальный индекс в общем пуле.
|
||||
|
||||
Grid не является точной геометрической проверкой. Он возвращает короткий список
|
||||
candidates, после чего выполняется проверка принадлежности области. При
|
||||
загрузке каждый area ID обязан быть меньше общего числа ареалов.
|
||||
|
||||
Покрытие:
|
||||
|
||||
```text
|
||||
Ранние шесть карт: 3 811 areals, grid 128 x 128
|
||||
Часть 1: 33 карты, 34 662 areals, 197 698 areal vertices
|
||||
Часть 2: 32 карты, 18 984 areals, 114 968 areal vertices
|
||||
```
|
||||
|
||||
Во всех картах grid равен `128 x 128`. Максимальное число candidates в ячейке
|
||||
-- 20 для Части 1 и 14 для Части 2. Все area/edge references находятся в
|
||||
диапазоне, normals имеют единичную длину в пределах float32-погрешности, parser
|
||||
заканчивается точно на конце payload.
|
||||
|
||||
## Пространственные задачи runtime
|
||||
|
||||
Движок решает три похожих, но независимых вопроса:
|
||||
|
||||
- **видимость** -- нужно ли рисовать объект для текущей камеры;
|
||||
- **столкновение** -- пересекается ли движение с поверхностью или другим телом;
|
||||
- **навигация** -- через какие области допустимо провести маршрут.
|
||||
|
||||
Terrain, Control и ArealMap используют общие координаты мира, но разные
|
||||
структуры данных. Нельзя заменять навигационный граф видимыми triangles или
|
||||
вычислять collision только по границе areal. Render frame описан отдельно в
|
||||
[Render frame](../reference/render-frame.md); здесь важна подготовка world data,
|
||||
которую renderer получает уже после загрузки миссии.
|
||||
|
||||
### Поиск области
|
||||
|
||||
Координата переводится в ячейку grid из `Land.map`. Ячейка даёт список
|
||||
candidate areas, затем выполняется точная геометрическая проверка. Такой запрос
|
||||
не перебирает все области карты и не зависит от количества terrain faces.
|
||||
|
||||
Если координата попадает в несколько candidates, выбор должен учитывать
|
||||
геометрию boundary и class/logic flags, а не только первый ID из grid cell.
|
||||
Если область не найдена, caller получает явный miss и решает, допустим ли
|
||||
fallback к ближайшей области.
|
||||
|
||||
### Маршрут
|
||||
|
||||
После определения начальной и целевой областей маршрут строится по графу
|
||||
соседств. Результат высокого уровня -- последовательность areal IDs. Из неё
|
||||
формируется локальный corridor, внутри которого movement controller выбирает
|
||||
конкретное движение по поверхности.
|
||||
|
||||
Такое разделение оставляет навигацию устойчивой к деталям terrain mesh:
|
||||
изменение density triangles не должно менять high-level route, пока areal graph
|
||||
и links остаются теми же.
|
||||
|
||||
### Категории зон объектов
|
||||
|
||||
`BuildDat.lst` связывает 12 имён категорий с 32-битными масками:
|
||||
|
||||
```text
|
||||
Bunker_Small 80010000
|
||||
Bunker_Medium 80020000
|
||||
Bunker_Large 80040000
|
||||
Generator 80000002
|
||||
Mine 80000004
|
||||
Storage 80000008
|
||||
Plant 80000010
|
||||
Hangar 80000040
|
||||
MainTeleport 80000200
|
||||
Institute 80000400
|
||||
Tower_Medium 80100000
|
||||
Tower_Large 80200000
|
||||
```
|
||||
|
||||
Файл читается секционно. Неизвестное имя, дублирование или нарушенная структура
|
||||
не должны тихо превращаться в нулевую маску. Нулевая маска является
|
||||
диагностируемым состоянием, а не универсальным default.
|
||||
|
||||
## Создание мира
|
||||
|
||||
Инициализация карты должна быть staged pipeline, а не набором независимых
|
||||
autoload-ов:
|
||||
|
||||
1. открыть `Land.msh` и построить geometry/spatial данные terrain;
|
||||
2. открыть `Land.map` и создать areals, links и cell grid;
|
||||
3. загрузить категории `BuildDat.lst`;
|
||||
4. создать world managers для поверхности, областей, света и атмосферы;
|
||||
5. разобрать TMA, paths и clans;
|
||||
6. раскрыть object resources через unit DAT и `objects.rlb`;
|
||||
7. подготовить MSH, WEAR, MAT0, Texm, lightmap и FXID dependencies;
|
||||
8. создать World3D objects и domain controllers в pending state;
|
||||
9. проверить cross references между components, controllers и spatial data;
|
||||
10. зарегистрировать visual, physical и behavior components;
|
||||
11. подключить AI/scripts и разрешить первый calculation tick.
|
||||
|
||||
Минимальный псевдокод объектной части:
|
||||
|
||||
```c
|
||||
for (const PlacedObject& placed : mission.objects) {
|
||||
vector<string> keys = expand_resource_name(placed.resource_name);
|
||||
|
||||
for (const string& key : keys) {
|
||||
Prototype p = registry.resolve(key);
|
||||
PreparedVisual v = prepare_visual(p);
|
||||
Object* o = construct_component(p, v, placed.properties);
|
||||
|
||||
o->set_world_transform(placed.transform);
|
||||
pending_registration.push_back(o);
|
||||
}
|
||||
}
|
||||
|
||||
validate_cross_references(pending_registration);
|
||||
register_all(pending_registration);
|
||||
```
|
||||
|
||||
`prepare_visual` использует явные ссылки прототипа и правила fallback ресурсной
|
||||
системы. Она не должна угадывать модель по имени placed object, если prototype
|
||||
уже задаёт другой effective MSH/WEAR.
|
||||
|
||||
## Инварианты реализации
|
||||
|
||||
- Reader всех count-driven структур проверяет overflow до выделения памяти.
|
||||
- Parser TMA, `Land.msh` и `Land.map` завершает работу точно на конце своего
|
||||
payload.
|
||||
- Неизвестные поля, reserved bytes, raw strings и property values сохраняются
|
||||
lossless.
|
||||
- Object properties остаются ordered property bag; сортировка имён запрещена.
|
||||
- Clan relations и area links проверяются на диапазон, но физический порядок
|
||||
записей сохраняется.
|
||||
- Terrain vertex indices, face neighbors и areal references валидируются до
|
||||
публикации spatial managers.
|
||||
- Достижимый missing resource останавливает mission load до регистрации
|
||||
объектов; недостижимая запись общего каталога остаётся диагностикой.
|
||||
- Calculation tick включается только после успешной сборки terrain, areal graph,
|
||||
managers, object queue и scenario bindings.
|
||||
@@ -0,0 +1,863 @@
|
||||
# V. Геометрия, материалы и рендер
|
||||
|
||||
Этот том описывает путь от загруженного игрового состояния до pixels в back
|
||||
buffer. Renderer не решает игровые правила: он получает transforms, geometry,
|
||||
материалы, свет, эффекты, камеру и список видимых объектов, затем превращает
|
||||
их в упорядоченный набор draw calls и fixed-function states.
|
||||
|
||||
Графический pipeline FParkan держится на нескольких слоях данных:
|
||||
|
||||
```text
|
||||
MSH node/slot/batch
|
||||
-> Batch20.material_index
|
||||
-> строка WEAR
|
||||
-> имя MAT0
|
||||
-> активная phase
|
||||
-> textureName и lightmap slot
|
||||
-> Texm payload
|
||||
-> LegacyRenderState
|
||||
-> draw item кадра
|
||||
```
|
||||
|
||||
Важное практическое правило: форматы ресурсов, runtime-состояние renderer-а и
|
||||
современный backend являются разными уровнями. Файл можно прочитать правильно и
|
||||
всё равно получить неверный кадр из-за другой сортировки, другого mip-skip,
|
||||
другой ветки material fallback или другого округления animation time.
|
||||
|
||||
## Контур рендера
|
||||
|
||||
Изображение является последней стадией длинного цикла. До renderer-а уже
|
||||
накоплен ввод, рассчитан simulation step, применены отложенные операции,
|
||||
обновлены animation states, выбрана camera и выставлен listener для 3D sound.
|
||||
|
||||
```text
|
||||
system messages and input
|
||||
-> simulation calculation
|
||||
-> deferred object operations
|
||||
-> animation and transforms
|
||||
-> camera and sound listener
|
||||
-> visibility and render queues
|
||||
-> materials and draw passes
|
||||
-> renderer completion
|
||||
-> end-of-render callbacks and UI
|
||||
```
|
||||
|
||||
CPU делает отбор объектов, сэмплирует animation, собирает matrices, выбирает
|
||||
LOD/slot, группирует batches и готовит состояния. Графический pipeline
|
||||
преобразует вершины из model space в screen space, rasterizes triangles,
|
||||
проверяет depth, применяет texture stages, lighting, alpha test/blend и пишет
|
||||
pixels.
|
||||
|
||||
Координатный путь вершины:
|
||||
|
||||
```text
|
||||
local/model space
|
||||
-> world space
|
||||
-> view/camera space
|
||||
-> clip space
|
||||
-> normalized device coordinates
|
||||
-> viewport pixels
|
||||
```
|
||||
|
||||
Порядок умножения матриц и соглашение о layout должны быть едины во всём
|
||||
движке. Ошибка транспонирования часто выглядит как сломанная анимация, хотя
|
||||
ключи модели прочитаны верно.
|
||||
|
||||
## Граница Ngi32
|
||||
|
||||
`Ngi32.dll` является платформенной границей Iron3D-era renderer-а. Она создаёт
|
||||
графический и звуковой interfaces, перечисляет устройства, хранит capability
|
||||
profile, предоставляет память, часы и быстрые математические процедуры.
|
||||
Высокоуровневые DLL должны обращаться к interface Ngi32, а не напрямую к
|
||||
конкретному DirectDraw/Direct3D device.
|
||||
|
||||
`iron_3d.ini` задаёт выбранный `CURRENT_D3DCARD`. Display layer перечисляет
|
||||
drivers и video modes, проверяет поддержку 3D, переводит native capabilities во
|
||||
внутренний профиль и создаёт render object. `niCreate3DRender` принимает
|
||||
выбранный driver/mode, window handle и flags владения, динамически получает
|
||||
функции DirectDraw/Direct3D семейства 5-7 и публикует refcounted renderer.
|
||||
`niGet3DRender` возвращает уже созданный объект и увеличивает число владельцев.
|
||||
|
||||
```text
|
||||
enumerate adapters and video modes
|
||||
-> choose CURRENT_D3DCARD
|
||||
-> translate native capabilities
|
||||
-> create DirectDraw surfaces and 3D interface
|
||||
-> construct engine renderer
|
||||
-> publish global refcounted pointer
|
||||
```
|
||||
|
||||
Старый API работает как state machine. Перед draw подсистема terrain/shade
|
||||
выбирает matrices, texture stages, filtering, depth test/write, culling, alpha
|
||||
test, blending и vertex format. Современный backend может собрать это в
|
||||
immutable pipeline key и реализовать через shaders, но compatibility layer
|
||||
должен видеть исходную fixed-function модель.
|
||||
|
||||
```c
|
||||
struct LegacyRenderState {
|
||||
Mat4 world, view, projection;
|
||||
TextureStage stages[2];
|
||||
BlendMode blend;
|
||||
DepthMode depth;
|
||||
CullMode cull;
|
||||
bool alpha_test;
|
||||
uint8_t alpha_ref;
|
||||
VertexFormat vertex_format;
|
||||
};
|
||||
```
|
||||
|
||||
Эта структура является переносимой моделью наблюдаемого контракта, а не
|
||||
утверждением о точном layout оригинального объекта renderer-а.
|
||||
|
||||
Отдельная часть ABI -- таблица `g_FastProc`. При запуске выбираются scalar,
|
||||
MMX, Katmai/SSE, 3DNow или PPro-реализации процедур, а `niGetProcAddress(index)`
|
||||
возвращает pointer из изменяемой таблицы. Номер slot является частью ABI:
|
||||
signature менять нельзя. Различия scalar/SIMD округления способны менять
|
||||
animation sampling, culling, particles и даже gameplay-adjacent decisions.
|
||||
|
||||
## MSH как граф модели
|
||||
|
||||
`*.msh` является nested NRes, а не одной монолитной структурой. Geometry,
|
||||
nodes, slots, batches, animation и служебные streams лежат в отдельных entries
|
||||
и связываются по `type_id`. Физический порядок entries сохраняется для
|
||||
roundtrip, но reader не должен выводить из него смысловую связь.
|
||||
|
||||
Карта основных entries:
|
||||
|
||||
```text
|
||||
type 1 узлы и выбор slot, обычно stride 38
|
||||
type 2 header 0x8C + slots по 68 байт
|
||||
type 3 positions float3, stride 12
|
||||
type 4 packed normals, stride 4
|
||||
type 5 packed UV0, stride 4
|
||||
type 6 index buffer, u16
|
||||
type 7 triangle descriptors, stride 16
|
||||
type 8 animation keys, stride 24
|
||||
type 9 служебный поток модели
|
||||
type 10 строки и имена узлов
|
||||
type 13 draw batches, stride 20
|
||||
type 15 дополнительный поток, stride 8
|
||||
type 17 вспомогательные данные
|
||||
type 18 редкий поток, stride 4
|
||||
type 19 animation frame map, u16
|
||||
type 20 редкая вспомогательная таблица
|
||||
```
|
||||
|
||||
Базовый набор types стабилен для проверенных моделей Частей 1 и 2. Расширенный
|
||||
вариант добавляет types 18 и 20. Редкий вариант `MTCHECK.MSH` имеет
|
||||
альтернативный атрибут type 1; его payload нужно поддерживать copy-through до
|
||||
закрытия layout.
|
||||
|
||||
### Узлы и slots
|
||||
|
||||
Type 1 обычно состоит из записей по 38 байт:
|
||||
|
||||
```c
|
||||
struct Node38 {
|
||||
uint16_t hdr0;
|
||||
uint16_t parent_or_link;
|
||||
uint16_t anim_map_start;
|
||||
uint16_t fallback_key;
|
||||
uint16_t slot_index[15];
|
||||
};
|
||||
```
|
||||
|
||||
`slot_index` образует матрицу `3 LOD x 5 groups`. Выбор выполняется как
|
||||
`slot_index[lod * 5 + group]`; `0xFFFF` означает отсутствие geometry для этой
|
||||
комбинации. Поле `parent_or_link` участвует в иерархии или связи узлов, но
|
||||
название остаётся описательным.
|
||||
|
||||
Type 2 начинается с header `0x8C`, затем содержит slots по 68 байт:
|
||||
|
||||
```c
|
||||
struct Slot68 {
|
||||
uint16_t tri_start;
|
||||
uint16_t tri_count;
|
||||
uint16_t batch_start;
|
||||
uint16_t batch_count;
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
float sphere_center[3];
|
||||
float sphere_radius;
|
||||
uint32_t opaque[5];
|
||||
};
|
||||
```
|
||||
|
||||
Slot связывает диапазон triangle descriptors, диапазон draw batches, AABB и
|
||||
sphere bounds. AABB удобен для более точных осевых тестов, sphere -- для
|
||||
быстрого отбрасывания. Последние пять слов сохраняются без интерпретации.
|
||||
|
||||
Обязательные проверки:
|
||||
|
||||
- `type 2` имеет размер не меньше `0x8C`;
|
||||
- остаток после header кратен 68;
|
||||
- каждый `slot_index` либо `0xFFFF`, либо меньше числа slots;
|
||||
- `tri_start + tri_count` не выходит за type 7;
|
||||
- `batch_start + batch_count` не выходит за type 13.
|
||||
|
||||
### Vertex streams, triangles и batches
|
||||
|
||||
Основные vertex streams:
|
||||
|
||||
```text
|
||||
type 3: position = три float32
|
||||
type 4: normal = четыре int8
|
||||
type 5: UV0 = два int16
|
||||
type 6: index = uint16
|
||||
```
|
||||
|
||||
Normal XYZ декодируется как signed component / `127.0` с clamp в `[-1, 1]`.
|
||||
Четвёртый byte normal stream не отбрасывается при roundtrip. UV декодируется
|
||||
как `packed / 1024.0`. Index buffer адресует вершины относительно `base_vertex`
|
||||
batch-а, поэтому проверка допустимости всегда использует
|
||||
`base_vertex + index < vertex_count`.
|
||||
|
||||
Type 7 хранит descriptors triangles:
|
||||
|
||||
```c
|
||||
struct TriDesc16 {
|
||||
uint16_t tri_flags;
|
||||
uint16_t link0;
|
||||
uint16_t link1;
|
||||
uint16_t link2;
|
||||
int16_t nx;
|
||||
int16_t ny;
|
||||
int16_t nz;
|
||||
uint16_t sel_packed;
|
||||
};
|
||||
```
|
||||
|
||||
Descriptors используются коллизией, выбором и связями triangles. `sel_packed`
|
||||
содержит три двухбитовых selector-а; значение `3` преобразуется в отсутствие
|
||||
ссылки (`0xFFFF`). Полная семантика links и flags не закрывается одним layout.
|
||||
|
||||
Type 13 задаёт draw ranges:
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct Batch20 {
|
||||
uint16_t batch_flags; // +0x00
|
||||
uint16_t material_index; // +0x02
|
||||
uint16_t opaque4; // +0x04
|
||||
uint16_t opaque6; // +0x06
|
||||
uint16_t index_count; // +0x08
|
||||
uint32_t index_start; // +0x0A
|
||||
uint16_t opaque14; // +0x0E
|
||||
uint32_t base_vertex; // +0x10
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(Batch20) == 20);
|
||||
```
|
||||
|
||||
`material_index` выбирает строку WEAR. `index_start`, `index_count` и
|
||||
`base_vertex` описывают один indexed draw. Неизвестные поля могут влиять на
|
||||
редкие проходы или state grouping, поэтому writer сохраняет их 1:1.
|
||||
|
||||
Типовой обход модели:
|
||||
|
||||
```c
|
||||
for (Node& node : model.nodes) {
|
||||
Matrix node_world = parent_world * local_transform(node);
|
||||
uint16_t sid = node.slot_index[lod * 5 + group];
|
||||
if (sid == 0xFFFF) continue;
|
||||
|
||||
Slot& slot = model.slots[sid];
|
||||
if (camera.culls(transform(slot.bounds, node_world))) continue;
|
||||
|
||||
for (uint32_t i = 0; i < slot.batch_count; ++i) {
|
||||
Batch& b = model.batches[slot.batch_start + i];
|
||||
bind_wear_material(b.material_index);
|
||||
draw_indexed(b.base_vertex, b.index_start, b.index_count);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
В реальном кадре между culling и draw добавляются material resolve, lightmap,
|
||||
render queues и сортировка, но связи данных остаются такими.
|
||||
|
||||
## Иерархия и анимация
|
||||
|
||||
Анимация MSH меняет локальный transform узлов. Geometry streams не изменяются:
|
||||
для каждого узла на кадр строится matrix из position и quaternion. Дочерний
|
||||
узел наследует transform родителя, поэтому изменение корпуса переносит башню,
|
||||
точки крепления и все связанные slots.
|
||||
|
||||
Связка состоит из:
|
||||
|
||||
- type 8: пул animation keys;
|
||||
- type 19: карта кадров;
|
||||
- `anim_map_start` и `fallback_key` в `Node38`;
|
||||
- parent links, задающих порядок умножения matrices.
|
||||
|
||||
Ключ type 8 занимает 24 байта:
|
||||
|
||||
```c
|
||||
struct AnimKey24 {
|
||||
float position[3];
|
||||
float time;
|
||||
int16_t qx;
|
||||
int16_t qy;
|
||||
int16_t qz;
|
||||
int16_t qw;
|
||||
};
|
||||
```
|
||||
|
||||
Quaternion components декодируются как signed value / `32767.0`. На диске
|
||||
порядок полей XYZ-W, но runtime math использует логическое `[w, x, y, z]`.
|
||||
Безусловная современная нормализация после чтения не добавляется без parity
|
||||
проверки: она может изменить крайние кадры.
|
||||
|
||||
Type 19 является массивом `uint16_t`; его `attr2` задаёт общее число кадров
|
||||
timeline. Для конкретного узла `anim_map_start` указывает на блок длиной
|
||||
`frame_count` либо равен `0xFFFF`.
|
||||
|
||||
Выбор ключа:
|
||||
|
||||
1. вычислить frame index из времени;
|
||||
2. если frame вне диапазона, взять `fallback_key`;
|
||||
3. если `anim_map_start == 0xFFFF`, взять `fallback_key`;
|
||||
4. иначе прочитать `map_words[anim_map_start + frame]`;
|
||||
5. если значение не меньше `fallback_key`, снова использовать fallback;
|
||||
6. иначе использовать mapped key и следующий key для interpolation.
|
||||
|
||||
Fallback возвращается без interpolation. Это защищает статические узлы и конец
|
||||
track-а.
|
||||
|
||||
Для времени между двумя keys:
|
||||
|
||||
```text
|
||||
alpha = (t - k0.time) / (k1.time - k0.time)
|
||||
position = lerp(k0.position, k1.position, alpha)
|
||||
rotation = shortest-path quaternion blend
|
||||
```
|
||||
|
||||
Перед quaternion blend проверяется dot product. Если стороны находятся в
|
||||
противоположных полусферах, знак второй стороны меняется, чтобы пройти по
|
||||
короткому пути. При точном совпадении времени возвращается соответствующий key
|
||||
без вычисления alpha.
|
||||
|
||||
Объект может переходить между двумя animation states. Тогда для каждого узла
|
||||
сэмплируются позы A и B, затем position смешивается линейно, а quaternion --
|
||||
через shortest-path blend. Если одна сторона невалидна, используется другая.
|
||||
|
||||
```c
|
||||
Pose sample_node(Node n, float t);
|
||||
Pose blend_pose(Pose a, Pose b, float weight);
|
||||
Mat4 local = quaternion_matrix(pose.rotation);
|
||||
local.set_translation(pose.position);
|
||||
world[n] = world[parent(n)] * local;
|
||||
```
|
||||
|
||||
Для parity особенно важны x87-compatible округление при выборе frame index и
|
||||
порядок операций. Одинаковая формула на SSE может выбрать соседний кадр возле
|
||||
границы.
|
||||
|
||||
Проверки animation data:
|
||||
|
||||
- размер type 8 кратен 24;
|
||||
- размер type 19 кратен 2;
|
||||
- каждый `fallback_key` меньше числа keys;
|
||||
- блок карты узла полностью помещается в type 19;
|
||||
- времена keys внутри track возрастают;
|
||||
- parent links не образуют cycle;
|
||||
- quaternion components читаются как signed 16-bit.
|
||||
|
||||
## WEAR и MAT0
|
||||
|
||||
MSH batch хранит только числовой `material_index`. WEAR переводит позиционный
|
||||
slot в имя материала. MAT0 по этому имени описывает phases, parameters,
|
||||
texture names и animation blocks. Такое разделение позволяет одной geometry
|
||||
использовать разные appearances.
|
||||
|
||||
```text
|
||||
Batch20.material_index
|
||||
-> строка WEAR
|
||||
-> имя MAT0
|
||||
-> активная phase
|
||||
-> textureName и render parameters
|
||||
```
|
||||
|
||||
### WEAR
|
||||
|
||||
WEAR имеет type ID `0x52414557` и обычно хранится как `*.wea` рядом с моделью.
|
||||
Формат текстовый:
|
||||
|
||||
```text
|
||||
<wearCount>
|
||||
<legacyId> <materialName>
|
||||
... wearCount строк
|
||||
|
||||
[пустая строка]
|
||||
[LIGHTMAPS
|
||||
<lightmapCount>
|
||||
<legacyId> <lightmapName>
|
||||
... lightmapCount строк]
|
||||
```
|
||||
|
||||
`legacyId` читается и сохраняется, но material выбирается по позиции строки и
|
||||
имени. Пустая строка перед `LIGHTMAPS` является частью совместимого framing:
|
||||
parser paths по-разному обрабатывают переход, и отсутствие разделителя ломает
|
||||
совместимость. Material handle кодируется как `(table_index << 16) |
|
||||
wear_index`; manager поддерживает ограниченное число wear tables.
|
||||
|
||||
Fallback material resolve строго разделён:
|
||||
|
||||
1. имя из WEAR;
|
||||
2. `DEFAULT`;
|
||||
3. entry 0;
|
||||
4. для lightmap отсутствие означает slot `-1`, а не замену обычной texture.
|
||||
|
||||
Пустое имя texture внутри phase означает намеренно untextured surface.
|
||||
Lightmap ищется в отдельном cache и не подменяется diffuse texture.
|
||||
|
||||
### MAT0
|
||||
|
||||
MAT0 имеет type ID `0x3054414D` и обычно находится в `Material.lib`. `attr1`
|
||||
содержит runtime flags, `attr2` -- версию payload. Versioned metadata читается
|
||||
cursor-ом: старые версии получают runtime defaults, но reader не пытается
|
||||
насильно читать поля новой версии.
|
||||
|
||||
```c
|
||||
#pragma pack(push, 1)
|
||||
struct Mat0PrefixV4Plus {
|
||||
uint16_t phase_count; // +0x00
|
||||
uint16_t animation_block_count; // +0x02, меньше 20
|
||||
uint8_t metadata_a; // +0x04, attr2 >= 2
|
||||
uint8_t metadata_b; // +0x05, attr2 >= 2
|
||||
uint32_t metadata_c_raw; // +0x06, attr2 >= 3
|
||||
uint32_t metadata_d_raw; // +0x0A, attr2 >= 4
|
||||
};
|
||||
|
||||
struct Phase34 {
|
||||
uint8_t parameters[18];
|
||||
char texture_name[16];
|
||||
};
|
||||
#pragma pack(pop)
|
||||
static_assert(sizeof(Phase34) == 34);
|
||||
```
|
||||
|
||||
Если `attr2 < 2`, metadata A/B получают default `255`; при `attr2 < 3`
|
||||
значение C соответствует `1.0f`; при `attr2 < 4` D равно 0. C/D сохраняются
|
||||
как raw 32-bit values до полного подтверждения интерпретации. Phase parameters
|
||||
сохраняются как 18 raw bytes даже там, где часть bytes уже имеет понятный
|
||||
смысл.
|
||||
|
||||
Каждая phase разворачивается в runtime-запись примерно 76 байт: коэффициенты
|
||||
цвета, освещения и прозрачности, texture slot и служебные поля. Material time
|
||||
выбирает одну или две phases; только часть полей интерполируется, остальные
|
||||
копируются из активной записи.
|
||||
|
||||
Animation block MAT0 имеет плотный framing без 4-byte tail alignment:
|
||||
|
||||
```text
|
||||
u32 header_raw
|
||||
u16 key_count
|
||||
repeat key_count:
|
||||
u16 k0
|
||||
u16 k1
|
||||
u16 k2
|
||||
```
|
||||
|
||||
Младшие три бита `header_raw` задают числовой mode, остальные образуют mask
|
||||
interpolation. Наблюдаются modes 0, 1, 2 и 3, связанные с семействами loop,
|
||||
ping-pong, one-shot/clamp и random-offset, но точные boundary cases остаются
|
||||
предметом runtime parity. Поле `k2` сохраняется всегда.
|
||||
|
||||
Проверки MAT0:
|
||||
|
||||
- `animation_block_count < 20`;
|
||||
- все versioned metadata помещаются в payload;
|
||||
- секция phases имеет ровно `phase_count * 34` байта;
|
||||
- `texture_name` ограничено 16 байтами;
|
||||
- каждый animation block и его keys помещаются в payload;
|
||||
- parser заканчивает чтение на точном конце записи.
|
||||
|
||||
Material manager кэширует разобранный MAT0 и texture handles. Current phase
|
||||
лучше вычислять на экземпляр материала, если random offset или локальное время
|
||||
различаются между объектами; immutable phase data остаются общими.
|
||||
|
||||
## Texm: текстуры, mip-уровни и атласы
|
||||
|
||||
`Texm` -- основной формат изображений. Он хранится в `Textures.lib`,
|
||||
`LightMap.lib` и других NRes-архивах. Payload содержит header, необязательную
|
||||
palette, mip chain и иногда `Page` chunk для atlas rectangles.
|
||||
|
||||
```c
|
||||
struct TexmHeader32 {
|
||||
uint32_t magic; // 'Texm'
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t mip_count;
|
||||
uint32_t flags4;
|
||||
uint32_t flags5;
|
||||
uint32_t unknown6;
|
||||
uint32_t format;
|
||||
};
|
||||
```
|
||||
|
||||
Подтверждённые formats:
|
||||
|
||||
```text
|
||||
0 Indexed8 + palette 256 x 4 байта
|
||||
565 R5 G6 B5
|
||||
556 R5 G5 B6
|
||||
4444 A4 R4 G4 B4
|
||||
88 L8 A8
|
||||
888 RGB8 в четырёхбайтовом element
|
||||
8888 A8 R8 G8 B8
|
||||
```
|
||||
|
||||
Formats 556 и 88 являются loader-confirmed, но не corpus-verified для
|
||||
доступных игровых payload. CPU decoder расширяет короткие каналы до 8 bit через
|
||||
повторение значимых bit, а не простым shift. Для 888 служебный четвёртый byte
|
||||
сохраняется при roundtrip.
|
||||
|
||||
Layout:
|
||||
|
||||
```text
|
||||
TexmHeader32
|
||||
[palette 1024 байта, только для format 0]
|
||||
level 0 pixels
|
||||
level 1 pixels
|
||||
...
|
||||
level mip_count-1 pixels
|
||||
[optional Page chunk]
|
||||
```
|
||||
|
||||
Размер уровня `i` вычисляется из `max(1, width >> i)` и
|
||||
`max(1, height >> i)`. Bytes per pixel: 1 для indexed; 2 для 565, 556, 4444 и
|
||||
88; 4 для 888 и 8888. Parser суммирует размеры с проверкой overflow до чтения.
|
||||
|
||||
`Page` chunk:
|
||||
|
||||
```c
|
||||
struct PageHeader8 {
|
||||
uint32_t magic; // 'Page'
|
||||
uint32_t rect_count;
|
||||
};
|
||||
|
||||
struct PageRect8 {
|
||||
int16_t x;
|
||||
int16_t width;
|
||||
int16_t y;
|
||||
int16_t height;
|
||||
};
|
||||
```
|
||||
|
||||
Chunk обязан иметь размер `8 + rect_count * 8`; произвольный tail не
|
||||
допускается. Rectangles задаются в pixel space базового mip. Если loader
|
||||
пропускает верхние mip-уровни, rectangles масштабируются вместе с новым base
|
||||
level.
|
||||
|
||||
Mip-skip является поведением loader-а, а не offline-изменением файла. После
|
||||
skip меняются runtime width, height, mip count и pointer на первый загружаемый
|
||||
уровень. Современный renderer должен повторить выбор base level или
|
||||
эквивалентно эмулировать его upload policy; использование полной texture при
|
||||
тех же UV меняет резкость и atlas coordinates.
|
||||
|
||||
Indexed texture требует связанную palette. Часть palettes выбирается по suffix
|
||||
имени: буква `A..Z` и вариант пустой или `0..9`, всего 286 возможных slots.
|
||||
Невалидный suffix диагностируется явно.
|
||||
|
||||
Обычные textures и lightmaps находятся в разных managers. Обычный cache
|
||||
отслеживает refcount и время неиспользования, а eviction выполняется
|
||||
отложенно. Lightmap lifetime связан с world/mission и не должен попадать под
|
||||
ту же политику удаления.
|
||||
|
||||
Строгий Texm parser проверяет положительные dimensions, положительный
|
||||
`mip_count`, известный format, точный размер palette/mip chain, корректный
|
||||
`Page` и отсутствие лишних bytes. `flags4`, `flags5` и `unknown6` сохраняются
|
||||
1:1; участие `flags5` в mip-skip подтверждено, но полная семантика всех bits не
|
||||
закрыта.
|
||||
|
||||
## Свет, тени, атмосфера и сортировка
|
||||
|
||||
Свет является отдельной world-подсистемой. Terrain layer создаёт
|
||||
`LightManager`, `Shader` и primitive managers. Это не один глобальный
|
||||
коэффициент яркости: world управляет point lights, lightmaps, shadows,
|
||||
atmospheric objects и sort phases. Материал сообщает свойства поверхности, а
|
||||
CShade превращает их в states renderer-а.
|
||||
|
||||
Подтверждённые точки: `CreateLightManager`, `CreateShader`,
|
||||
`CreateAtmosphere`, `CreatePrimitives`, `CreatePrimitives2`,
|
||||
`CShade::StartMeshRender`, `CShade::EndMeshRender` и
|
||||
`CShade::ConfigureTextureAndAlphaBlendModes`.
|
||||
|
||||
CShade получает active MAT0 phase, capability profile устройства и pass
|
||||
context. Он выбирает texture mode, alpha blending, depth/cull behavior и способ
|
||||
освещения. Наличие fallback вроде `TEXTUREMODE_MODULATE not supported`
|
||||
означает, что material нельзя напрямую преобразовать в современный PBR.
|
||||
Сначала строится legacy state, затем он сопоставляется shader permutation.
|
||||
|
||||
CLightManager выдаёт numeric IDs источникам и проверяет допустимое количество.
|
||||
Ветка `EmulatePointLights()` позволяет воспроизводить point lights даже при
|
||||
ограничениях hardware lighting. Неизвестный type light должен давать отдельную
|
||||
ошибку.
|
||||
|
||||
Lightmap не является обычной diffuse texture. WEAR содержит отдельный блок
|
||||
`LIGHTMAPS`, manager открывает `LightMap.lib`, а shade path подаёт lightmap
|
||||
отдельным slot или texture stage. Замена lightmap предварительным умножением в
|
||||
diffuse texture ломает LOD, atlas coordinates и динамическую модуляцию.
|
||||
|
||||
Тени проходят отдельным render pass. Terrain содержит пути для теней зданий и
|
||||
роботов, ограничения максимального числа, detail level и smoothing. Доказаны
|
||||
shadow manager/pass, настройки detail/smoothing/count и зависимость от
|
||||
Terrain/CShade; полная формула projection geometry для каждого caster требует
|
||||
dynamic trace. Unknown settings из `shade.cfg` читаются и сохраняются по
|
||||
именам, а не заменяются произвольными modern defaults.
|
||||
|
||||
Atmosphere manager создаёт world objects для фоновых и погодных явлений.
|
||||
Отдельно подтверждены lightning, sun render, flare, `env_lightning`, rain
|
||||
background sound и обязательные ссылки на lightning effect. Эти объекты
|
||||
обновляются по игровому времени, но часть параметров зависит от camera: flare
|
||||
требует screen position и occlusion test, rain -- области рядом с observer,
|
||||
sound -- listener. Их нельзя один раз запечь в terrain.
|
||||
|
||||
RNG для lightning, atmosphere phases и FX должен иметь стабильный порядок.
|
||||
Даже правильный средний интервал не даёт повторяемый кадр, если random values
|
||||
запрашиваются в другой последовательности.
|
||||
|
||||
Согласованная модель sort phases:
|
||||
|
||||
```text
|
||||
opaque terrain and models
|
||||
-> lightmapped/state-grouped passes
|
||||
-> shadows and projected primitives
|
||||
-> alpha-tested surfaces
|
||||
-> transparent objects/effects back-to-front
|
||||
-> atmosphere, flares and overlays
|
||||
```
|
||||
|
||||
Точный взаимный порядок отдельных FX, shadow и atmosphere subpasses требует
|
||||
capture. Новый renderer должен хранить явный `RenderPhase` и стабильный
|
||||
secondary sort key, а не сортировать всё только по material ID.
|
||||
|
||||
## FXID: система эффектов
|
||||
|
||||
FXID -- не готовая картинка, а описание небольшого runtime command stream.
|
||||
Header задаёт lifetime, time mode, random shifts и transform. Затем идут
|
||||
команды разных types. При создании manager превращает disk-команды в runtime
|
||||
objects; во время кадра они обновляются и выпускают sounds, particles,
|
||||
materials или projected primitives.
|
||||
|
||||
Type ID равен `0x44495846`. Header занимает 60 байт:
|
||||
|
||||
```c
|
||||
struct FxHeader60 {
|
||||
uint32_t command_count;
|
||||
uint32_t time_mode;
|
||||
float duration_seconds;
|
||||
float phase_jitter;
|
||||
uint32_t flags;
|
||||
uint32_t settings_id;
|
||||
float random_shift[3];
|
||||
float pivot[3];
|
||||
float scale[3];
|
||||
};
|
||||
```
|
||||
|
||||
Поток команд начинается строго с offset `0x3C`. `duration_seconds`
|
||||
преобразуется runtime-ом во внутреннюю шкалу времени. `phase_jitter` и
|
||||
`random_shift` используются только при соответствующих flags. Pivot задаёт
|
||||
локальную точку опоры, scale -- базовый масштаб экземпляра. Unknown flags и
|
||||
settings ID сохраняются.
|
||||
|
||||
Каждая команда начинается с `uint32_t command_word`:
|
||||
|
||||
```text
|
||||
opcode = command_word & 0xFF
|
||||
enabled = (command_word >> 8) & 1
|
||||
```
|
||||
|
||||
Bits 9-31 являются частью данных и сохраняются. Между командами нет
|
||||
выравнивания. Размер команды, включая word:
|
||||
|
||||
```text
|
||||
opcode 1 224 байта
|
||||
opcode 2 148 байт
|
||||
opcode 3 200 байт
|
||||
opcode 4 204 байта
|
||||
opcode 5 112 байт
|
||||
opcode 6 4 байта
|
||||
opcode 7 208 байт
|
||||
opcode 8 248 байт
|
||||
opcode 9 208 байт
|
||||
opcode 10 208 байт
|
||||
```
|
||||
|
||||
Parser использует opcode только для выбора фиксированного размера. Неизвестный
|
||||
opcode отклоняется: попытка угадать длину потеряет синхронизацию всего stream.
|
||||
|
||||
Opcodes 2, 3, 4, 5, 7, 8, 9 и 10 содержат pair fixed strings:
|
||||
|
||||
```c
|
||||
struct FxResourceRef64 {
|
||||
char archive[32];
|
||||
char name[32];
|
||||
};
|
||||
```
|
||||
|
||||
Имена сравниваются case-insensitive по ASCII, а tail после первого nul byte
|
||||
сохраняется. Resolve выполняется при создании command object или лениво при
|
||||
первом запуске, но ошибка должна включать имя эффекта, номер команды, archive
|
||||
и resource name.
|
||||
|
||||
Базовый normalized age:
|
||||
|
||||
```text
|
||||
tn = (now - start_time) / (end_time - start_time)
|
||||
```
|
||||
|
||||
`time_mode` выбирает источник коэффициента: constant, forward/reverse age,
|
||||
cyclic phase, external world state и варианты с ограничением относительно
|
||||
предыдущего значения. Точные формулы редких modes являются parity-задачей.
|
||||
Flags могут умножать alpha на lifetime, применять triangular remap, случайно
|
||||
сдвигать phase/space, инвертировать active-state, фильтровать по времени суток
|
||||
или включать manager gates.
|
||||
|
||||
Lifecycle:
|
||||
|
||||
```text
|
||||
create instance
|
||||
-> copy header and external transform
|
||||
-> calculate end time and random offsets
|
||||
-> create command objects in disk order
|
||||
-> resolve required resources
|
||||
-> Start
|
||||
|
||||
on each calculation/render frame
|
||||
-> evaluate time coefficient and gates
|
||||
-> update commands in stable order
|
||||
-> emit active primitives or sounds
|
||||
-> collect render batches
|
||||
-> handle Stop / Restart / end-of-life
|
||||
```
|
||||
|
||||
Update и emit разделяются. Simulation может продолжаться в кадре без render, а
|
||||
emit не должен повторно менять игровое состояние. Для authoring безопасно
|
||||
типизировать header и resource references, а body редких commands сохранять raw
|
||||
до подтверждения field-level semantics.
|
||||
|
||||
## Полный кадр
|
||||
|
||||
Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан
|
||||
следующий порядок boundary операций:
|
||||
|
||||
1. передать camera в Terrain через `stdSetCurrentCamera2` и сохранить её как
|
||||
текущую;
|
||||
2. получить camera/view/viewport interfaces через virtual queries;
|
||||
3. обновить положение и ориентацию 3D sound listener;
|
||||
4. настроить renderer viewport и matrices;
|
||||
5. вызвать два renderer boundary slots перед traversal;
|
||||
6. установить глобальный флаг `in_render`;
|
||||
7. вызвать главный virtual метод camera/world traversal;
|
||||
8. выполнить дополнительную post queue при включённом режиме;
|
||||
9. завершить world/shade pass;
|
||||
10. вызвать renderer completion slot;
|
||||
11. снять `in_render`, восстановить viewport и разослать end-of-render.
|
||||
|
||||
Семантические имена нескольких slots перед и после traversal не подтверждены,
|
||||
поэтому в compatibility code их лучше временно называть
|
||||
`frame_boundary_0`, `frame_boundary_1`, `frame_boundary_2`.
|
||||
|
||||
Обход видимого мира:
|
||||
|
||||
```text
|
||||
проверить active/visible state
|
||||
-> выбрать LOD по расстоянию и настройкам
|
||||
-> получить node matrices из animation state
|
||||
-> выбрать slot для каждого node/group
|
||||
-> преобразовать bounds в world space
|
||||
-> выполнить culling
|
||||
-> добавить batches в подходящую render queue
|
||||
```
|
||||
|
||||
Material/texture resolve желательно выполнять после visibility и slot
|
||||
selection, чтобы невидимые объекты не меняли порядок обращений к caches и не
|
||||
создавали лишние side effects. Невидимость объекта и отсутствие slot являются
|
||||
разными причинами пропуска и диагностируются отдельно.
|
||||
|
||||
Подготовленный draw item содержит:
|
||||
|
||||
```text
|
||||
node world matrix
|
||||
batch flags and index range
|
||||
WEAR material handle
|
||||
MAT0 active phase and coefficients
|
||||
texture handle
|
||||
optional lightmap handle
|
||||
render phase and sorting key
|
||||
legacy pipeline state
|
||||
```
|
||||
|
||||
Draw item должен ссылаться на immutable данные кадра. Изменение phase или
|
||||
texture cache посреди прохода не должно менять уже собранную очередь.
|
||||
|
||||
Согласованная декомпозиция внутренних render phases:
|
||||
|
||||
1. подготовка frame state, camera и viewport;
|
||||
2. непрозрачный terrain;
|
||||
3. непрозрачные object batches;
|
||||
4. lightmap и дополнительные material passes;
|
||||
5. projected primitives и тени;
|
||||
6. alpha-tested geometry;
|
||||
7. transparent objects и FX в сортировочных слоях;
|
||||
8. atmosphere, sun, flare и weather;
|
||||
9. renderer completion boundary;
|
||||
10. end-of-render callbacks;
|
||||
11. shell/UI и post-render state.
|
||||
|
||||
Точный взаимный порядок пунктов 4-8 и связь completion slot с физическим
|
||||
DirectDraw flip/present требуют dynamic capture. Сортировка внутри каждой фазы
|
||||
должна быть стабильной: для opaque первичен pipeline/material key, для
|
||||
transparent -- distance layer и depth order, затем stable insertion ID.
|
||||
|
||||
Геометрический draw использует streams type 3/4/5, optional streams, index
|
||||
buffer type 6, `base_vertex`, `index_start` и `index_count`. Матрица узла
|
||||
устанавливается как world transform, затем CShade привязывает texture stages и
|
||||
fixed-function state.
|
||||
|
||||
```c
|
||||
set_world_matrix(item.node_world);
|
||||
bind_vertex_streams(model.streams);
|
||||
bind_index_buffer(model.indices);
|
||||
apply_legacy_state(item.pipeline);
|
||||
bind_texture(0, item.texture);
|
||||
bind_texture(1, item.lightmap);
|
||||
draw_indexed(item.batch.base_vertex,
|
||||
item.batch.index_start,
|
||||
item.batch.index_count);
|
||||
```
|
||||
|
||||
После последнего world pass renderer закрывает сцену и выводит back buffer.
|
||||
World3D снимает `in_render`, восстанавливает временный viewport state и вызывает
|
||||
`on_end_render` у active objects. Только после этого допустимо освобождать
|
||||
temporary vertex buffers или заменять render representation. UI/shell
|
||||
обслуживается верхним уровнем после возврата из world-render path; для
|
||||
диагностики полезно уметь сохранять world-only command list и финальный
|
||||
framebuffer отдельно.
|
||||
|
||||
## Проверки паритета
|
||||
|
||||
Главные риски совпадения кадра:
|
||||
|
||||
- x87 extended precision и правила округления;
|
||||
- различия scalar/SIMD slots `g_FastProc`;
|
||||
- порядок objects, batches и transparent primitives;
|
||||
- depth write/test, cull, alpha test и blend transitions;
|
||||
- mip-skip, palette и `Page` coordinates;
|
||||
- material fallback и выбор phase;
|
||||
- последовательность RNG для FX и atmosphere;
|
||||
- capability fallback конкретного устройства;
|
||||
- quantization времени и дополнительный simulation step;
|
||||
- eager/lazy resource resolve и cache side effects.
|
||||
|
||||
Минимальный deterministic frame capture должен включать camera state, viewport,
|
||||
visible object IDs, выбранные LOD/group/slot, draw-item list, material и texture
|
||||
handles, pipeline keys, matrices, render phase, sort key, причины culling и
|
||||
hashes промежуточных buffers. Без такой трассировки нельзя уверенно отделить
|
||||
ошибку формата MSH от ошибки state machine renderer-а или сортировки.
|
||||
|
||||
Связанные справочные страницы с таблицами форматов: [MSH](../reference/msh.md),
|
||||
[materials](../reference/materials.md), [Texm](../reference/texm.md) и
|
||||
[render frame](../reference/render-frame.md).
|
||||
@@ -0,0 +1,769 @@
|
||||
# VI. Поведение, управление, звук и сеть
|
||||
|
||||
Шестой том описывает подсистемы, которые превращают загруженный мир в
|
||||
реагирующую игру: AI, Behavior, Wizard, Control, ввод, камеру, звук и сеть.
|
||||
Эти области нельзя восстанавливать только по структуре файлов. Для них важны
|
||||
порядок кадра, ownership объектов, timing событий и доказуемые границы между
|
||||
решением, движением, presentation и транспортом.
|
||||
|
||||
Ключевой принцип: reader compatibility не равна gameplay compatibility.
|
||||
Корректно разобранный ресурс ещё не доказывает, что runtime выбирает ту же
|
||||
цель, строит тот же маршрут, применяет ту же collision correction, создаёт тот
|
||||
же sound event или отправляет тот же network payload. Поэтому все утверждения
|
||||
ниже разделяют подтверждённую структуру, восстановленный архитектурный
|
||||
контракт и открытые участки, требующие динамической трассировки.
|
||||
|
||||
```text
|
||||
AI / mission script
|
||||
-> стратегическая цель, условия, команды миссии
|
||||
Behavior
|
||||
-> состояние объекта, target, global/local path
|
||||
Wizard
|
||||
-> локальная коррекция траектории
|
||||
Control
|
||||
-> physical step, collision proxy, итоговый transform
|
||||
World3D
|
||||
-> очередь событий, ownership, deferred deletion
|
||||
Render / Sound / Net
|
||||
-> представление, listener, mirrors и сообщения
|
||||
```
|
||||
|
||||
Связанные главы: [мир и миссии](04-world.md), [геометрия и рендер](05-render.md)
|
||||
и справочный [render frame](../reference/render-frame.md).
|
||||
|
||||
## AI, Behavior и Wizard
|
||||
|
||||
Iron3D разделяет стратегическое принятие решений, поведение конкретного объекта
|
||||
и локальную коррекцию движения. Это разделение должно сохраниться в новой
|
||||
реализации: стратегический AI не меняет transform напрямую, а collision manager
|
||||
не выбирает игровую цель.
|
||||
|
||||
```text
|
||||
ai.dll / SuperAI
|
||||
-> цель клана, миссии и группы
|
||||
Behavior.dll
|
||||
-> состояние юнита, target, global path, local corridor
|
||||
Wizard.dll
|
||||
-> ближайшая допустимая траектория
|
||||
Control.dll
|
||||
-> физическое движение и столкновения
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
`CreateBehaviour` создаёт controller для отдельного игрового объекта.
|
||||
`CreateDistributor` восстановлен по consumers как посредник распределения
|
||||
команд или ресурсов; это высокоуверенный архитектурный вывод, а не доказанное
|
||||
имя внутреннего класса. Behavior получает `IArealMap` через AI/клановый
|
||||
контекст, ведёт radar/target state, строит global path, превращает его в local
|
||||
corridor и передаёт движение Wizard.
|
||||
|
||||
Ошибочные состояния проверяются явно:
|
||||
|
||||
1. отсутствует system map;
|
||||
2. отсутствует terrain interface;
|
||||
3. active behavior не имеет `IArealMap`;
|
||||
4. объект попал в non-reachable area;
|
||||
5. объект пытается выйти из non-walkable area;
|
||||
6. path generator вошёл в infinite cycle.
|
||||
|
||||
Эти случаи являются fatal или diagnostic conditions. Совместимая реализация не
|
||||
должна тихо исправлять их teleport-ом, потому что такое исправление скрывает
|
||||
ошибку areal graph, terrain query или state machine.
|
||||
|
||||
### Параметры Behavior.ini
|
||||
|
||||
Подтверждены настройки:
|
||||
|
||||
```text
|
||||
PathFind_BuildingHitDist
|
||||
PathFind_BuildingNearestDist
|
||||
PathFind_NearBuildSpeedPercent
|
||||
PathFind_CorridorRadius
|
||||
PathFind_NearDoorCoeff
|
||||
PathFind_fStepOffBuilding
|
||||
PathFind_MaxAccel
|
||||
PathFind_MaxRotation
|
||||
PathFind_fStepDist
|
||||
PathFind_MinPointInTrajectory
|
||||
Network_ResourceTransferMaxDelay
|
||||
```
|
||||
|
||||
Они задают геометрию corridor, дистанции реакции на здания, снижение скорости
|
||||
возле препятствий, пределы ускорения и поворота, дискретизацию trajectory и
|
||||
сетевой timeout передачи ресурсов. Значения читаются как runtime-конфигурация,
|
||||
а не компилируются в код. Parser должен поддерживать комментарии `//`, пробелы
|
||||
вокруг `=` и CRLF.
|
||||
|
||||
Файл также содержит logging/debug switches: `Behavior.log`, уровни ошибок,
|
||||
show vectors и z-buffer debug. Эти переключатели полезны не только для
|
||||
совместимости, но и как модель современных trace flags.
|
||||
|
||||
### Wizard
|
||||
|
||||
Wizard получает желаемое направление и corridor, анализирует ближайшие
|
||||
ограничения и выдаёт скорректированную локальную траекторию. Behavior может
|
||||
очищать её через `ClearWizardPath` при смене цели, повреждении global path или
|
||||
переходе объекта в неактивное состояние.
|
||||
|
||||
Нужно различать четыре уровня движения:
|
||||
|
||||
- **global path** -- последовательность areals;
|
||||
- **local path** -- точки или сегменты внутри corridor;
|
||||
- **wizard path** -- краткосрочное движение с учётом ближайших препятствий;
|
||||
- **physical step** -- фактически разрешённое Control перемещение.
|
||||
|
||||
Хранение всего маршрута одним массивом лишает систему возможности локально
|
||||
обойти препятствие без полного повторного поиска. Граница Behavior/Wizard
|
||||
существует именно для того, чтобы краткосрочная геометрическая коррекция не
|
||||
ломала стратегический path state.
|
||||
|
||||
### SuperAI и миссионные сценарии
|
||||
|
||||
`CreateSuperAI` создаёт центральный controller клана; `GetSuperAI` возвращает
|
||||
его. AI загружает файлы из `MISSIONS\SCRIPTS\`, проверяет версию и пишет ошибки
|
||||
в `ai.log`. Несовпадение версии является отдельной ошибкой, а не неизвестной
|
||||
командой.
|
||||
|
||||
Сценарный корпус содержит binary `.scr`, formula exports `.fml`, таблицу
|
||||
переменных `varset.var` и `.trf`-данные. `.scr` хранит именованные секции и
|
||||
события, например `Init`, `Mission`, `Problems0`, `Fort_Task_Complete` и
|
||||
`Hero_Teleported`, вместе с числовыми ссылками на compiled instructions.
|
||||
`.fml` является текстовым экспортом formula set. `varset.var` декларативно
|
||||
описывает типы, defaults, ranges и строки через макросоподобные формы
|
||||
`VAR(...)` и `STRING(...)`.
|
||||
|
||||
Безопасная runtime-модель:
|
||||
|
||||
```text
|
||||
load script bundle
|
||||
-> validate version and symbol tables
|
||||
-> create global/formula variables
|
||||
-> bind named events to instruction offsets
|
||||
-> instantiate SuperAI per clan
|
||||
-> dispatch MISSION_START and object events
|
||||
-> update timers/conditions each simulation tick
|
||||
-> enqueue game commands through World3D/Behavior
|
||||
```
|
||||
|
||||
Сценарий не должен владеть игровым объектом напрямую. Он хранит logical/object
|
||||
IDs и отправляет команды через игровые interfaces, чтобы удаление объекта или
|
||||
сетевой mirror не оставили dangling pointer.
|
||||
|
||||
Полная grammar compiled instructions и точное значение всех opcodes остаются
|
||||
открытым направлением. До появления decompiler-а `.scr` binary body сохраняется
|
||||
lossless, а доказанные symbol/event tables документируются отдельно.
|
||||
|
||||
### TRF и preload-данные
|
||||
|
||||
TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial
|
||||
variants имеют сигнатуру [NRes](../reference/nres.md) и содержат большие
|
||||
таблицы имён игровых прототипов: оружия, башен, сооружений и других объектов.
|
||||
Также найдены preload-записи, ANI и SKE resources.
|
||||
|
||||
По содержимому, порядку загрузки и consumers TRF с высокой вероятностью
|
||||
предоставляет AI/сценарному слою заранее подготовленную таблицу типов и
|
||||
связанных данных. Framing и имена подтверждены corpus-ом, но полная семантика
|
||||
каждой TRF-записи ещё не закрыта. Имена должны разрешаться через тот же
|
||||
resource registry, что и миссионные объекты.
|
||||
|
||||
### Стабильность AI-слоя
|
||||
|
||||
`ai.dll`, `Behavior.dll` и `Wizard.dll` побайтно идентичны в Частях 1 и 2. Это
|
||||
подтверждает, что разделение SuperAI -> Behavior -> Wizard и бинарная
|
||||
реализация этих трёх уровней не менялись.
|
||||
|
||||
Сценарный корпус:
|
||||
|
||||
```text
|
||||
Часть 1: 58 SCR, 58 FML, 29 TRF
|
||||
Часть 2: 59 SCR, 59 FML, 44 TRF
|
||||
```
|
||||
|
||||
Все TRF являются структурно валидными NRes. Неизменность DLL усиливает вывод о
|
||||
стабильной VM, но не закрывает instruction grammar `.scr`: для неё нужен
|
||||
dispatcher/jump-table decompiler. Дополнительные сценарные данные расширяют
|
||||
differential corpus, но не заменяют анализ VM.
|
||||
|
||||
## Control, физика и коллизии
|
||||
|
||||
Control превращает желаемое движение в физически допустимое изменение
|
||||
состояния. World3D владеет жизненным циклом объекта; Terrain предоставляет
|
||||
поверхность и world queries; Behavior/Wizard задают намерение; Control создаёт
|
||||
physical controller и collision representation.
|
||||
|
||||
Публичная поверхность:
|
||||
|
||||
```text
|
||||
InitializeSettings
|
||||
LoadControlSystem
|
||||
LoadPhysicalModel
|
||||
CreateCollManager
|
||||
CreateCollObject
|
||||
```
|
||||
|
||||
Модуль импортирует World3D queue/object functions, `Terrain::GetWorld`, часы,
|
||||
тригонометрию и `g_FastProc`. Это подтверждает его положение между gameplay
|
||||
object и геометрией мира.
|
||||
|
||||
### Control system и physical model
|
||||
|
||||
`LoadControlSystem` загружает настройки controller-а: ограничения скорости,
|
||||
ускорения, поворота и режимы управления. `LoadPhysicalModel` загружает форму и
|
||||
параметры, используемые для столкновений. Visible MSH не обязан совпадать с
|
||||
collision representation: для физики часто нужна более простая и устойчивая
|
||||
форма.
|
||||
|
||||
Практичная runtime-модель:
|
||||
|
||||
```c
|
||||
struct PhysicalState {
|
||||
Transform transform;
|
||||
Vec3 linear_velocity;
|
||||
Vec3 angular_velocity;
|
||||
float requested_speed;
|
||||
float requested_turn;
|
||||
uint32_t flags;
|
||||
};
|
||||
|
||||
struct CollisionProxy {
|
||||
ObjectId owner;
|
||||
ShapeSet shapes;
|
||||
Bounds broad_phase_bounds;
|
||||
uint32_t category_mask;
|
||||
};
|
||||
```
|
||||
|
||||
Названия полей здесь описывают контракт совместимой реализации, а не точный
|
||||
layout исходного C++-объекта.
|
||||
|
||||
### Collision pipeline
|
||||
|
||||
Один расчётный шаг удобно разделить так:
|
||||
|
||||
1. controller получает желаемые `speed`/`turn` от Behavior или manual input;
|
||||
2. вычисляет кандидатный transform на основе `dt`;
|
||||
3. обновляет broad-phase bounds collision object;
|
||||
4. collision manager находит потенциальные пары и terrain candidates;
|
||||
5. narrow phase вычисляет контакт или допустимый остаток перемещения;
|
||||
6. physical state корректируется;
|
||||
7. World3D получает итоговый transform;
|
||||
8. событие `GMSG_COLLISION_DETECTED` отправляется в согласованной фазе.
|
||||
|
||||
Позиция collision event после narrow phase является рекомендуемой фазой
|
||||
реализации и согласуется с назначением сообщения, но точный call-site
|
||||
относительно всех correction steps требует динамической трассировки Control.
|
||||
Удаление объекта из обработчика остаётся отложенным по правилам World3D.
|
||||
Collision manager не должен хранить прямую незащищённую ссылку на объект,
|
||||
который уже pending-delete.
|
||||
|
||||
### CTLD и physical resources
|
||||
|
||||
Реестр прототипов ссылается на `*.ctl`, `*.cpt` и связанные control resources.
|
||||
В Части 1 структурно проверен 531 CTLD payload без ошибок. Размеры и пять
|
||||
внутренних счётчиков образуют множество вариантов: наиболее частый размер
|
||||
392 байта с pattern `(0,0,0,1,0)`, но встречаются блоки от примерно 212 до
|
||||
1868 байт и более сложные комбинации.
|
||||
|
||||
CTLD является составным count-driven форматом, а не фиксированной struct.
|
||||
Parser должен:
|
||||
|
||||
- прочитать prefix и все счётчики с проверкой переполнения;
|
||||
- вычислить границы секций по их counts;
|
||||
- сохранять неизвестные records в typed raw containers;
|
||||
- требовать точного завершения payload;
|
||||
- не использовать размер одного популярного варианта как универсальный layout.
|
||||
|
||||
Полная предметная семантика всех секций ещё не доказана, но существующие файлы
|
||||
можно безопасно читать, индексировать и сохранять.
|
||||
|
||||
### Terrain queries и movement handoff
|
||||
|
||||
Control получает world-interface Terrain и использует поверхность, faces и
|
||||
ускорители для высоты, нормали и пересечений. Навигационный маршрут сообщает,
|
||||
куда двигаться, но итоговый transform определяется по физической поверхности.
|
||||
При переходе через склон controller должен согласовать горизонтальный шаг,
|
||||
высоту и ориентацию с terrain normal.
|
||||
|
||||
Порядок операций должен быть детерминированным: пары collision objects
|
||||
сортируются по стабильному ID, contacts обрабатываются в фиксированной
|
||||
последовательности, а интеграция использует одну политику `dt` и округления.
|
||||
Иначе одинаковая миссия постепенно расходится даже без сети.
|
||||
|
||||
### Различия Control в Части 2
|
||||
|
||||
`Control.dll` пересобрана при неизменных размере, imports и пяти именах/ordinals
|
||||
exports; RVA всех пяти exports изменились. Форматы и cross-module boundary
|
||||
сохранились, но точное physical/collision behavior нельзя считать побайтно тем
|
||||
же.
|
||||
|
||||
CTLD-корпус расширен с 531 до 623 payload. Новых framing errors не найдено;
|
||||
большинство общих CTLD изменено вместе с переработанными моделями. Это
|
||||
подтверждает count-driven parser, но не закрывает предметную семантику shape
|
||||
records и contact solver.
|
||||
|
||||
Differential test обеих частей должен воспроизводить движение без препятствий,
|
||||
slope following, pair collision, timing collision event и удаление объекта в
|
||||
callback. Сравниваются transforms и contact events по tick, а не только факт
|
||||
успешной загрузки.
|
||||
|
||||
## Ввод, камера и управление
|
||||
|
||||
World3D нормализует клавиатуру, мышь и joystick в общие scan codes и manual
|
||||
commands. Win32 message handler вызывает `UpdateManualEventsList`; перед
|
||||
обработкой новой порции сообщений основной цикл вызывает
|
||||
`ClearManualEventsList`. Снимок клавиатуры очищается отдельно через
|
||||
`stdClearKeyboard`.
|
||||
|
||||
Публичная поверхность включает `WinMsg2ScanCode`, converters для
|
||||
keyboard/mouse/joystick/predicate, `ScanCode2Str`, `ManualCommand2Str`,
|
||||
`stdIsKeyPressed`, lock/unlock keyboard и чтение mouse shift. Это позволяет
|
||||
хранить конфигурацию управления независимо от физического устройства.
|
||||
|
||||
### Event, state и axis
|
||||
|
||||
Ввод имеет минимум три семантики:
|
||||
|
||||
- **edge event** -- нажатие или отпускание в текущей порции сообщений;
|
||||
- **held state** -- клавиша остаётся нажатой между кадрами;
|
||||
- **analog value** -- смещение мыши или положение joystick axis.
|
||||
|
||||
Manual command дополняет источник коэффициентом, режимом wrap, dead
|
||||
zone/threshold и временной характеристикой. Строки camera bindings показывают
|
||||
команды `MCMD_STATE`, `MCMD_ANGLE_X`, `MCMD_ANGLE_Y`, режимы `MAN_WRAP` и
|
||||
`MAN_NOTWRAP`, а также параметры ускорения в миллисекундах.
|
||||
|
||||
Simulation читает подготовленный input snapshot. Renderer не должен
|
||||
самостоятельно опрашивать OS, иначе одно и то же нажатие будет зависеть от
|
||||
частоты кадров.
|
||||
|
||||
### Joystick через DirectInput
|
||||
|
||||
`Joystick.dll` экспортирует:
|
||||
|
||||
```text
|
||||
QueryJoy
|
||||
CreateJoy
|
||||
ReleaseJoy
|
||||
SetJoyRange
|
||||
PeekJoyMessage
|
||||
GetJoyCaps
|
||||
```
|
||||
|
||||
`QueryJoy` обнаруживает устройство, `CreateJoy` получает интерфейс DirectInput,
|
||||
`SetJoyRange` нормализует оси в диапазон движка, `PeekJoyMessage` выдаёт
|
||||
очередное унифицированное событие.
|
||||
|
||||
При потере устройства чтение может вернуть ошибку acquired state. Интерфейс
|
||||
следует повторно получить, очистить устаревшее состояние и продолжить.
|
||||
Hot-unplug не должен оставлять последнюю ось навсегда отклонённой.
|
||||
`GetInstalledJoyNames` и `SetActiveJoy` в World3D связывают device list с
|
||||
game-facing выбором.
|
||||
|
||||
### Два camera interface
|
||||
|
||||
World3D предоставляет `stdSetCurrentCamera`/`stdGetCurrentCamera`: это камера
|
||||
как часть игрового состояния. Terrain имеет
|
||||
`stdSetCurrentCamera2`/`stdGetCurrentCamera2`: concrete camera, которую world
|
||||
renderer использует для matrices, viewport и visibility.
|
||||
|
||||
`LoadCamera` экспортирован обоими модулями. По call graph World3D-вариант
|
||||
играет роль component bridge, а Terrain-вариант связан с concrete
|
||||
camera/world implementation. Это архитектурный вывод: точные class names и
|
||||
layout не восстановлены.
|
||||
|
||||
Минимальные данные камеры:
|
||||
|
||||
```text
|
||||
world position and orientation
|
||||
view matrix
|
||||
projection parameters / field of view
|
||||
near and far planes
|
||||
viewport rectangle
|
||||
camera mode and target object
|
||||
manual angles/state
|
||||
```
|
||||
|
||||
Такая граница позволяет game code работать с абстрактной камерой, не зная
|
||||
внутреннего renderer representation.
|
||||
|
||||
### Camera commands и порядок кадра
|
||||
|
||||
Подтверждены команды `CMD_CAMERA_LEFT`, `CMD_CAMERA_RIGHT`, `CMD_CAMERA_UP`,
|
||||
`CMD_CAMERA_DOWN`, `CMD_CAMERA_CENTER`, `CMD_CAMERA_INFRARED`, а также
|
||||
spotlight и внешние/миссионные camera modes. Горизонтальный угол использует
|
||||
wrap, вертикальный -- ограниченный диапазон. Center плавно возвращает обе оси к
|
||||
заданному значению.
|
||||
|
||||
Порядок кадра:
|
||||
|
||||
1. собрать manual events;
|
||||
2. обновить camera controller во время calculation;
|
||||
3. вычислить итоговый transform и ограничения;
|
||||
4. перед render установить current camera;
|
||||
5. передать её Terrain и sound listener;
|
||||
6. после кадра сохранить mode-specific state.
|
||||
|
||||
Camera smoothing должно использовать игровое время или специально
|
||||
подтверждённые часы. Привязка к render delta делает управление разным при 30 и
|
||||
144 FPS.
|
||||
|
||||
## Звуковая подсистема
|
||||
|
||||
Ngi32 создаёт низкоуровневый DirectSound backend. `services.dll` публикует
|
||||
`ISoundServer`. Game, Terrain и FX работают уже через эти интерфейсы:
|
||||
воспроизводят 2D/3D sources, меняют volume и связывают listener с camera.
|
||||
|
||||
Публичные функции Ngi32:
|
||||
|
||||
```text
|
||||
niCreate3DSound
|
||||
niGet3DSound
|
||||
niGet3DSoundCaps
|
||||
niMuteSound
|
||||
```
|
||||
|
||||
Backend динамически вызывает `DirectSoundEnumerateA` и `DirectSoundCreate`;
|
||||
параметр `DisableDSound` может полностью отключить этот путь.
|
||||
|
||||
### Устройство и capabilities
|
||||
|
||||
Конфигурация учитывает `3D Sound`, качество, reverse sound, частоту buffer,
|
||||
режим постоянного воспроизведения и автоматический выбор лучшего устройства.
|
||||
Эти значения преобразуются во внутренний capability/profile object до создания
|
||||
sources.
|
||||
|
||||
Код содержит отдельный no-device state и строку `3D Sound was not initialized`.
|
||||
Отсутствие 3D sound обрабатывается отдельно от ошибок simulation/resources.
|
||||
Новый runtime не должен позволять отсутствию звука разрушать simulation и
|
||||
обязан возвращать звуковым командам явный no-device result.
|
||||
|
||||
Общий sound object разделяется между подсистемами и использует счётчик
|
||||
владельцев. Закрывать DirectSound следует после остановки всех sources и
|
||||
atmosphere/FX managers.
|
||||
|
||||
### Sound resources и SWAV
|
||||
|
||||
Основная библиотека называется `sounds.lib`; `mission.cfg` также создаёт
|
||||
именованные sound resources и variations. Legacy API `rsLoadWave` загружает
|
||||
waveform из archive. Импорт `MSACM32` подтверждает путь преобразования сжатых
|
||||
wave-данных в формат playback buffer.
|
||||
|
||||
Resource identity состоит из library и name. Один sound asset может иметь
|
||||
несколько runtime sources с различными position, volume, pitch/flags и временем
|
||||
запуска. Поэтому кэшировать следует decoded sample/buffer, а source object
|
||||
создавать на событие.
|
||||
|
||||
FX opcode 2 хранит `archive[32] + name[32]` и обычно создаёт sound command.
|
||||
Atmosphere использует отдельные loop/variation sources, например rain
|
||||
background. Миссионный слой содержит voice events для завершения или провала
|
||||
задания.
|
||||
|
||||
Проверенный SWAV-корпус:
|
||||
|
||||
```text
|
||||
Часть 1: 399 — 306 MS ADPCM, 93 PCM
|
||||
Часть 2: 540 — 446 MS ADPCM, 93 PCM, 1 empty entry
|
||||
```
|
||||
|
||||
Все непустые записи имеют RIFF/WAVE framing и частоту 22 050 Hz. В Части 2
|
||||
entry `ALIEN_ME.WAV` имеет размер 0. Это присутствующий archive key без
|
||||
decodable waveform.
|
||||
|
||||
Sound loader должен различать:
|
||||
|
||||
- `entry_missing`;
|
||||
- `entry_empty`;
|
||||
- `wave_invalid`;
|
||||
- `decoded_sample`.
|
||||
|
||||
Нулевой payload не передаётся RIFF parser-у и не должен приводить к чтению
|
||||
header за границей.
|
||||
|
||||
### 3D listener и sources
|
||||
|
||||
Перед world traversal `stdRenderGame` обновляет listener из camera transform.
|
||||
Listener содержит position, orientation и, при наличии, velocity. Source
|
||||
содержит world position и параметры затухания. Spatialization выполняется
|
||||
backend-ом либо совместимой программной моделью.
|
||||
|
||||
```text
|
||||
camera transform
|
||||
-> listener position/front/up
|
||||
object or effect transform
|
||||
-> source position
|
||||
sample + source parameters
|
||||
-> DirectSound 3D buffer
|
||||
```
|
||||
|
||||
Прямо подтверждено обновление listener в начале `stdRenderGame`, до world
|
||||
traversal. Sound events могут создаваться и в calculation/FX path, поэтому
|
||||
нельзя утверждать, что listener предшествует созданию каждого source. Важно,
|
||||
что spatial backend получает camera state текущего отображаемого кадра до
|
||||
завершения его обработки. Перенос listener update после world render создаст
|
||||
как минимум однокадровое рассогласование presentation.
|
||||
|
||||
### Громкость, mute и CD-аудио
|
||||
|
||||
`iron3d.dll` применяет отдельные настройки эффектов и CD sound. Параметр
|
||||
`FORCE_CD_SOUND` меняет политику выбора музыкального источника. `niMuteSound`
|
||||
должен временно остановить вывод без разрушения sample cache и logical playback
|
||||
state.
|
||||
|
||||
В новой реализации полезно разделить buses: master, effects, ambient, voice и
|
||||
music/CD. Это проектное решение совместимого backend-а, а не доказанный layout
|
||||
оригинального mixer-а. Оно позволяет применять старые коэффициенты, не
|
||||
переписывая individual source volume.
|
||||
|
||||
### Граница service layer
|
||||
|
||||
`Ngi32.dll` с DirectSound/backend code не изменилась между Частями 1 и 2, но
|
||||
`services.dll` пересобрана и уменьшилась на 4 096 байт. Поэтому low-level
|
||||
decoder/device path подтверждается одной машинной реализацией, а service
|
||||
lifecycle, GUI/audio wiring и defaults требуют раздельной трассировки обеих
|
||||
частей.
|
||||
|
||||
## Сетевая подсистема
|
||||
|
||||
Net инкапсулирует DirectPlay4A и lobby/service-provider API. World3D строит над
|
||||
транспортом player identity, mirror objects и игровые сообщения. Эти уровни
|
||||
следует разделять: DirectPlay отвечает за доставку bytes между players,
|
||||
World3D -- за смысл сообщения и владение объектом.
|
||||
|
||||
Application GUID:
|
||||
|
||||
```text
|
||||
{3C1D1F01-A870-11D1-8400-000021B14415}
|
||||
```
|
||||
|
||||
Он передаётся network instance и service layer. Экземпляры с другим GUID не
|
||||
принадлежат одному логическому приложению.
|
||||
|
||||
### Lifecycle соединения
|
||||
|
||||
Публичные функции Net покрывают полный цикл:
|
||||
|
||||
```text
|
||||
CreateNetworkInstance
|
||||
-> select/use service provider
|
||||
-> setup connection
|
||||
-> enumerate or create session
|
||||
-> join/create session
|
||||
-> create local player
|
||||
-> send/receive messages and player data
|
||||
-> destroy player
|
||||
-> close session
|
||||
-> close connection
|
||||
```
|
||||
|
||||
Поддерживаются providers эпохи DirectPlay: TCP/IP, IPX и modem/lobby варианты,
|
||||
если они установлены в системе. Функции явно проверяют, что DirectPlay enabled
|
||||
до enumeration, session и player operations. Неверный порядок вызовов должен
|
||||
возвращать понятную ошибку, а не разыменовывать пустой interface.
|
||||
|
||||
### Sessions, players и адреса
|
||||
|
||||
Net предоставляет enumeration service providers и sessions, выбор host/join,
|
||||
player name/password/data, latency, максимальный размер сообщения, размер
|
||||
очереди, server player info и provider address. Lobby launch обрабатывается
|
||||
отдельной веткой.
|
||||
|
||||
Внутренняя модель должна хранить как минимум:
|
||||
|
||||
```c
|
||||
struct NetPlayer {
|
||||
TransportPlayerId transport_id;
|
||||
uint16_t game_player_number;
|
||||
string name;
|
||||
RawBytes player_data;
|
||||
bool is_local;
|
||||
bool is_host;
|
||||
};
|
||||
```
|
||||
|
||||
Transport ID нельзя использовать как постоянный `ObjectId`. NetWatcher связывает
|
||||
временный DirectPlay identifier с номером игрока и World3D entities.
|
||||
|
||||
### Игровые сообщения World3D
|
||||
|
||||
Подтверждённые имена message surface:
|
||||
|
||||
```text
|
||||
GMSG_CREATE_REMOTE_PLAYER
|
||||
GMSG_APPEND_RESOURCE
|
||||
GMSG_CHANGE_OBJECT_OWNER
|
||||
GMSG_SET_PLAYER_DATA
|
||||
GMSG_MISSION_DATA_PATH
|
||||
GMSG_TAKE_OBJECT
|
||||
GMSG_TEXT_FOR_PLAYER
|
||||
GMSG_SYNC_STATE
|
||||
GMSG_CREATE_MIRROR
|
||||
GMSG_PAUSE_REMOTE_PLAYER
|
||||
GMSG_CONFIRM_PLAYER_DATA
|
||||
GMSG_KILL_PLAYER
|
||||
SYSMSG_SET_TIME
|
||||
SYSMSG_SET_PLAYER_NUMBER
|
||||
GMSG_END_MESSAGE_SEQ
|
||||
GMSG_REMOVE_RESOURCE
|
||||
```
|
||||
|
||||
`GMSG_COLLISION_DETECTED` относится к общей очереди, но не обязательно
|
||||
передаётся по сети. Message ID, payload size и delivery policy должны быть
|
||||
частью явной schema. Нельзя сериализовать C++ pointers или native padding.
|
||||
|
||||
### Mirror objects и ownership
|
||||
|
||||
Удалённо принадлежащий объект представлен local mirror instance. Он участвует в
|
||||
рендере и spatial queries, но authority над его созданием, ключевыми properties
|
||||
и удалением находится у owner player. Сообщение смены владельца обновляет эту
|
||||
границу; оно не должно создавать второй объект с тем же ID.
|
||||
|
||||
Типовой путь:
|
||||
|
||||
```text
|
||||
remote create message
|
||||
-> validate player and ObjectId
|
||||
-> resolve prototype/resources
|
||||
-> CreateMirrorObject
|
||||
-> apply initial state
|
||||
-> AddMirrorObjectToGame
|
||||
-> subsequent sync messages update mirror
|
||||
```
|
||||
|
||||
При потере player NetWatcher инициирует предписанное удаление или transfer
|
||||
ownership через World3D queue. Мгновенное освобождение во время receive callback
|
||||
запрещено по тем же причинам, что и в calculation pass.
|
||||
|
||||
### Сжатие и wire compatibility
|
||||
|
||||
`netZipData` и `netUnZipData` образуют встроенный слой упаковки payload. Он
|
||||
находится выше транспорта: переход с DirectPlay на UDP/ENet не отменяет
|
||||
необходимость воспроизводить формат упакованного сообщения, если требуется
|
||||
соединение с оригинальной игрой.
|
||||
|
||||
Полный wire schema, framing и алгоритм сжатия пока не доказаны packet
|
||||
capture-ом. Поэтому нужны два режима:
|
||||
|
||||
- **native compatibility** -- отдельный adapter, реализуемый после трассировки
|
||||
оригинальных packets;
|
||||
- **modern multiplayer** -- новая versioned protocol schema, использующая ту же
|
||||
game-message семантику, но не заявляющая совместимость с DirectPlay client.
|
||||
|
||||
Эти режимы нельзя незаметно смешивать. До доказательства native wire
|
||||
compatibility современный transport должен быть versioned и отделён от слоя,
|
||||
который претендует на совместимость с оригинальным клиентом.
|
||||
|
||||
### Стабильность сетевого слоя
|
||||
|
||||
`Net.dll` и `World3D.dll` побайтно идентичны в обеих частях. Application GUID,
|
||||
DirectPlay wrapper, mirror-object API и World3D message surface относятся к
|
||||
одной машинной реализации.
|
||||
|
||||
Это подтверждает отсутствие отдельной сетевой реализации для Части 2, но не
|
||||
закрывает wire schema: без packet/send-receive capture по-прежнему неизвестны
|
||||
точное framing, reliability flags, payload layouts и алгоритм `netZipData` для
|
||||
native interoperability.
|
||||
|
||||
Для binary regression достаточно одного профиля неизменённых DLL, но message
|
||||
captures должны включать контент обеих частей, потому что prototype/resource IDs
|
||||
и mission data различаются.
|
||||
|
||||
## Контракты реализации
|
||||
|
||||
Совместимая реализация должна фиксировать не только результат, но и момент его
|
||||
появления в кадре. Для Behavior, Control, input, sound и network особенно важны
|
||||
tick boundaries: одна и та же команда, применённая на один tick раньше или
|
||||
позже, меняет дальнейшую симуляцию.
|
||||
|
||||
### Trace-события
|
||||
|
||||
Минимальный trace для этого тома:
|
||||
|
||||
- input snapshot: edge events, held state, analog values;
|
||||
- camera state: mode, target, angles, matrices, viewport;
|
||||
- Behavior: target, areal, global path revision, local corridor;
|
||||
- Wizard: requested vector, constraints, wizard path;
|
||||
- Control: candidate transform, contacts, correction, final transform;
|
||||
- World3D queue: message name, ObjectId, dispatch phase, deferred deletion;
|
||||
- sound: sample key, source owner, position, event tick, listener state;
|
||||
- network: player mapping, message ID, payload length, delivery policy.
|
||||
|
||||
Для рендера это связывается с [render frame](../reference/render-frame.md):
|
||||
camera и listener должны попадать в trace до world traversal, иначе нельзя
|
||||
отделить ошибку presentation от ошибки управления.
|
||||
|
||||
### Проверки Behavior и сценариев
|
||||
|
||||
- script version mismatch даёт отдельную ошибку;
|
||||
- event table читается lossless;
|
||||
- VM body сохраняется без потери неизвестных bytes;
|
||||
- отсутствующий `IArealMap` не замалчивается;
|
||||
- non-walkable/non-reachable states дают diagnostic condition;
|
||||
- одинаковый input log воспроизводит одинаковый sequence Behavior commands;
|
||||
- resource names из TRF разрешаются через общий registry.
|
||||
|
||||
### Проверки Control
|
||||
|
||||
- движение без препятствий;
|
||||
- slope/terrain-following;
|
||||
- симметричные pair-collision tests с переставленными IDs;
|
||||
- contact event отправляется один раз в предписанной фазе;
|
||||
- удаление объекта в collision callback безопасно;
|
||||
- replay одинакового input log даёт одинаковые transforms;
|
||||
- collision proxy перестраивается после смены component/model state.
|
||||
|
||||
### Проверки input и камеры
|
||||
|
||||
- edge event не повторяется как held state;
|
||||
- mouse/joystick axis сбрасывается по правилам snapshot;
|
||||
- hot-unplug joystick не оставляет старое отклонение;
|
||||
- camera horizontal angle wraps, vertical angle clamps;
|
||||
- center command использует подтверждённое время, а не render FPS;
|
||||
- Terrain и sound получают одну и ту же camera frame.
|
||||
|
||||
### Проверки звука
|
||||
|
||||
- backend может отсутствовать без нарушения simulation;
|
||||
- один decoded sample переиспользуется несколькими sources;
|
||||
- `entry_missing`, `entry_empty` и `wave_invalid` различаются;
|
||||
- listener совпадает с camera frame;
|
||||
- loop source корректно переживает pause/resume;
|
||||
- mute не сбрасывает position и time;
|
||||
- missing sound resource содержит полную диагностическую цепочку;
|
||||
- deterministic test сравнивает список sound events, а не waveform устройства.
|
||||
|
||||
### Проверки сети
|
||||
|
||||
- нельзя создавать queue с активной сетью и нулевым player ID;
|
||||
- session/player operations до enable/setup возвращают ошибку;
|
||||
- сообщения проверяют длину до чтения payload;
|
||||
- sequence/end markers обрабатываются в стабильном порядке;
|
||||
- duplicate create mirror не создаёт второй instance;
|
||||
- ownership change атомарно обновляет routing;
|
||||
- pause/time messages применяются в одной simulation boundary;
|
||||
- resource transfer имеет timeout `Network_ResourceTransferMaxDelay`;
|
||||
- disconnect не оставляет objects с несуществующим owner;
|
||||
- replay записанного message log даёт одинаковое World3D state.
|
||||
|
||||
`resnet.log` и `NetWatch.log` следует поддерживать как отдельные каналы: первый
|
||||
относится к transport/resource exchange, второй -- к связи players и game
|
||||
objects.
|
||||
|
||||
## Границы знания
|
||||
|
||||
Подтверждены внешние interfaces, часть runtime order, значимые строки,
|
||||
конфигурационные параметры, corpus-level counts и стабильность ряда DLL между
|
||||
двумя частями. Открытыми остаются:
|
||||
|
||||
- instruction grammar `.scr` и semantics всех VM opcodes;
|
||||
- точная семантика всех TRF-записей;
|
||||
- полный layout CTLD shape records;
|
||||
- contact solver и порядок всех correction steps;
|
||||
- class layout камер, контроллеров, sound service и network watcher;
|
||||
- DirectPlay wire framing, reliability flags и payload schema;
|
||||
- алгоритм `netZipData`/`netUnZipData`;
|
||||
- точные defaults service layer там, где DLL пересобраны.
|
||||
|
||||
Эти границы должны оставаться видимыми в документации и тестах. Если новая
|
||||
реализация вводит удобный современный abstraction layer, он обязан быть
|
||||
отделён от утверждений о native compatibility и покрыт отдельным trace.
|
||||
@@ -0,0 +1,674 @@
|
||||
# VII. Руководство по полной реализации
|
||||
|
||||
Этот том описывает инженерный путь к совместимому движку FParkan. Он опирается
|
||||
на доказанные форматы и runtime-контракты, но не требует повторять физическое
|
||||
деление оригинала на пятнадцать DLL. Повторить нужно наблюдаемое поведение:
|
||||
форматы, имена, fallback, object IDs, порядок событий, численную политику,
|
||||
границы кадра, сохранения и воспроизводимость прохождения.
|
||||
|
||||
Предложенные ниже modules, handles, snapshots, queues и scheduler phases являются
|
||||
целевой архитектурой новой реализации, а не восстановленным внутренним layout
|
||||
оригинального Iron3D. Главная практическая цель: запускаться из неизменённого
|
||||
оригинального каталога игры, проходить corpus gates для демоверсии, Части 1 и
|
||||
Части 2, а затем измеримо двигаться от archive compatibility к полной игровой
|
||||
совместимости.
|
||||
|
||||
## Целевая архитектура
|
||||
|
||||
Практичная форма новой реализации -- модульный монолит с узкими интерфейсами и
|
||||
отдельными platform adapters. Внутренние границы должны соответствовать ролям
|
||||
Iron3D, а не обязательно его DLL. Это упрощает перенос на современные платформы
|
||||
и оставляет возможность поддерживать разные compatibility profiles для разных
|
||||
сборок данных.
|
||||
|
||||
```text
|
||||
application запуск, окно, конфигурация, shutdown
|
||||
platform filesystem, clocks, input, threads, dynamic libraries
|
||||
resources NRes, RsLi, paths, archives, cache and diagnostics
|
||||
assets MSH, WEAR, MAT0, Texm, FXID and auxiliary formats
|
||||
mission TMA, unit DAT, prototype graph, scenario data
|
||||
world ObjectId, queue, lifecycle, time, messages, mirrors
|
||||
terrain Land.msh, Land.map, surface and spatial queries
|
||||
navigation areals, graph search, corridors
|
||||
behavior unit state machines, target and path requests
|
||||
physics control systems, collision proxies and contacts
|
||||
animation pose sampling, hierarchy and blending
|
||||
audio sample cache, sources, listener and buses
|
||||
render legacy-state compatibility and modern backend
|
||||
network game message schema plus transport adapters
|
||||
tools validators, extractors, viewers, captures and editors
|
||||
```
|
||||
|
||||
Каждый модуль зависит от нижележащих интерфейсов, а не от concrete managers.
|
||||
Behavior видит `INavigation` и `IPhysicsCommandSink`, но не включает headers
|
||||
renderer-а. Render получает immutable snapshot, а не mutable world. Network
|
||||
receive не меняет мир напрямую: validated messages попадают в очередь следующей
|
||||
calculation boundary.
|
||||
|
||||
### Центральные идентичности
|
||||
|
||||
Resource identity хранит и исходное написание, и нормализованный ASCII-key для
|
||||
поиска:
|
||||
|
||||
```c
|
||||
struct ResourceKey {
|
||||
NormalizedRelativePath archive;
|
||||
FixedAsciiName name;
|
||||
uint32_t type_id;
|
||||
};
|
||||
```
|
||||
|
||||
Normalization сохраняет исходную строку для diagnostics и roundtrip, а отдельный
|
||||
ASCII-casefold key используется только для lookup. Эта граница важна для
|
||||
архивов [NRes](../reference/nres.md), таблиц [RsLi](../reference/rsli.md),
|
||||
prototype references и fallback-путей материалов.
|
||||
|
||||
Object identity разделяет внутреннюю защиту от dangling references и исходную
|
||||
сетевую/script-семантику:
|
||||
|
||||
```c
|
||||
struct ObjectHandle { uint32_t generation; uint32_t slot; };
|
||||
struct OriginalObjectId { uint32_t raw; };
|
||||
```
|
||||
|
||||
`ObjectHandle` нужен для безопасного внутреннего владения, deferred deletion и
|
||||
weak references. `OriginalObjectId` сохраняет наблюдаемую семантику исходной
|
||||
игры: scripts, mirrors, network messages и savegame references должны видеть
|
||||
логический ID, а не адрес объекта или номер slot в новом allocator-е.
|
||||
|
||||
Frame snapshot отделяет simulation от render. Simulation пишет mutable state;
|
||||
renderer читает опубликованное состояние или строго ограниченную фазу
|
||||
`in_render`. Deferred deletion применяется между фазами, а не во время traversal.
|
||||
Командный контур renderer-а должен сверяться с [описанием кадра](../reference/render-frame.md)
|
||||
до pixel comparison.
|
||||
|
||||
### Владение ресурсами
|
||||
|
||||
Ресурс проходит несколько уровней:
|
||||
|
||||
```text
|
||||
ArchiveHandle -> EntryView -> DecodedBlob -> ParsedAsset -> RuntimeResource
|
||||
```
|
||||
|
||||
`EntryView` ссылается на metadata архива, `DecodedBlob` владеет подготовленными
|
||||
bytes, `ParsedAsset` является CPU-представлением, `RuntimeResource` может
|
||||
дополнительно владеть GPU/audio objects. Eviction верхнего уровня не закрывает
|
||||
архив, если он ещё нужен другому entry. Ссылки идут вниз только через явные
|
||||
handles.
|
||||
|
||||
Для shared objects допустимы reference counting или generation handles.
|
||||
Intrusive refcount нужен только в ABI-shim; внутренний современный код
|
||||
предпочтительно держит понятное владение и weak handles. Архивы, decoded blobs,
|
||||
CPU assets и GPU resources имеют отдельные бюджеты и отдельные diagnostics.
|
||||
|
||||
### Backend adapters
|
||||
|
||||
Render, audio, input и network получают отдельные adapters. Legacy compatibility
|
||||
state живёт выше Vulkan, D3D11 или Metal backend; DirectPlay compatibility живёт
|
||||
отдельно от modern transport. Так можно заменить платформу, не меняя форматы,
|
||||
игровую семантику и regression corpus.
|
||||
|
||||
Backend adapter не должен быть местом, где исправляются данные. Если
|
||||
[MSH](../reference/msh.md), [MAT0](../reference/materials.md) или
|
||||
[Texm](../reference/texm.md) требуют fallback, это фиксируется в asset/runtime
|
||||
слое и попадает в trace. Backend получает уже выбранные resources, states и
|
||||
draw items.
|
||||
|
||||
### Scheduler phases
|
||||
|
||||
```text
|
||||
collect_platform_events
|
||||
build_input_snapshot
|
||||
advance_game_clock
|
||||
calculate_world_queue
|
||||
apply_deferred_operations
|
||||
update_navigation_physics_animation_fx
|
||||
publish_render_snapshot
|
||||
render_world
|
||||
render_ui
|
||||
end_frame_callbacks
|
||||
maintenance_and_eviction
|
||||
```
|
||||
|
||||
Фазы имеют стабильный порядок и запрещённые операции. Registry mutation
|
||||
запрещена во время world traversal, GPU upload не изменяет simulation state, а
|
||||
maintenance не влияет на gameplay. Script timers, material animation и FX
|
||||
lifetime относятся к game time, если обратное не доказано.
|
||||
|
||||
Сначала реализуется однопоточный эталон. Параллелизм добавляется только внутри
|
||||
фаз с детерминированным merge: decoding независимых assets, culling chunks или
|
||||
подготовка immutable draw items. Это снижает риск скрытых race conditions и
|
||||
расхождений replay.
|
||||
|
||||
### Структурированные ошибки
|
||||
|
||||
Каждая ошибка должна содержать фазу, путь, archive entry, object/prototype key,
|
||||
offset и цепочку причины.
|
||||
|
||||
```text
|
||||
MissionLoadError
|
||||
mission: Campaign.00/Mission.02
|
||||
object: 17
|
||||
resource_name: UNITS/.../unit.dat
|
||||
component: e_tur_...
|
||||
prototype: objects.rlb::e_tur_...
|
||||
cause: model archive missing
|
||||
```
|
||||
|
||||
Логическое отсутствие необязательного lightmap, отсутствующий entry в архиве,
|
||||
неизвестное opaque поле, выход ссылки за диапазон и повреждённый offset имеют
|
||||
разный severity и разные способы исправления. Ошибка данных должна быть
|
||||
actionable chain, а не строка вида `failed to load resource`.
|
||||
|
||||
## Порядок работ
|
||||
|
||||
Движок строится от данных к поведению и от детерминированных CPU-компонентов к
|
||||
аппаратным. Каждый этап заканчивается исполняемым инструментом и тестовым
|
||||
критерием. Нельзя начинать полноценный gameplay, пока ресурсный граф и
|
||||
model/material path не дают воспроизводимый результат.
|
||||
|
||||
### Этап 0. Corpus harness
|
||||
|
||||
- индексировать оригинальный каталог и вычислить hashes;
|
||||
- реализовать bounded binary cursor и structured diagnostics;
|
||||
- создать CLI для массового запуска parser-ов;
|
||||
- сохранять JSON-отчёт с counts, variants, warnings и failures;
|
||||
- зафиксировать демоверсию, Часть 1 и Часть 2 как независимые baselines.
|
||||
|
||||
Готовность: повторный запуск на каждом неизменённом каталоге даёт идентичный
|
||||
отчёт. Любой parser умеет завершиться контролируемой ошибкой с offset и
|
||||
контекстом, а не crash или allocation по непроверенному count.
|
||||
|
||||
### Этап 1. Архивы и пути
|
||||
|
||||
- реализовать strict/lossless [NRes](../reference/nres.md) reader/writer;
|
||||
- реализовать [RsLi](../reference/rsli.md) mapping, table transform, lookup,
|
||||
LZSS и Deflate;
|
||||
- добавить адаптивный decoder для методов `0x080` и `0x0A0`;
|
||||
- воспроизвести overlay и известные compatibility quirks;
|
||||
- реализовать archive-handle cache и ASCII name policy.
|
||||
|
||||
Готовность: неизменённые архивы проходят byte-identical roundtrip; поиск всех
|
||||
имён совпадает с каталогом; malformed corpus отклоняется без выхода за память.
|
||||
NRes с ненулевым unindexed region обязательно остаётся regression case.
|
||||
|
||||
### Этап 2. Граф ресурсов
|
||||
|
||||
- разобрать `objects.rlb` и unit DAT;
|
||||
- построить resolver прямой MSH, рекурсивного parent prototype через
|
||||
`objects.rlb` и отдельного BASE payload;
|
||||
- реализовать dependency graph с reachability от миссии;
|
||||
- добавить parsers CTPT, NDPR и остальных служебных форматов в lossless-режиме;
|
||||
- создать инспектор прототипа, показывающий все связанные ресурсы.
|
||||
|
||||
Готовность: 201 demo-объект раскрывается в 501 прототип. Затем все миссии
|
||||
Частей 1 и 2 дают 4 701 и 5 845 prototype requests без failures. Недостижимые
|
||||
отсутствующие ресурсы отмечаются отдельно от критических ошибок в reachable
|
||||
graph.
|
||||
|
||||
### Этап 3. Статический asset viewer
|
||||
|
||||
- реализовать [MSH](../reference/msh.md) core streams, slots и batches;
|
||||
- декодировать Texm во все подтверждённые pixel formats;
|
||||
- разобрать WEAR и [MAT0](../reference/materials.md) с точными fallback;
|
||||
- построить современный renderer compatibility layer;
|
||||
- добавить wireframe, normals, bounds, LOD/group и material debug views.
|
||||
|
||||
Готовность: открываются 435/511 моделей, 518/631 textures и 905/1 127 materials
|
||||
Частей 1/2; batch/index bounds не нарушаются; viewer показывает корректно
|
||||
текстурированную статическую модель из исходного архива. Красивый viewer всё ещё
|
||||
означает только asset compatibility, а не готовую игру.
|
||||
|
||||
### Этап 4. Анимация и эффекты
|
||||
|
||||
- реализовать MSH type 8/type 19 sampling и hierarchy;
|
||||
- добавить x87-compatible reference path для чувствительных формул;
|
||||
- реализовать material phase animation;
|
||||
- разобрать FXID header/commands и runtime instances;
|
||||
- сначала поддержать все opcodes, встречающиеся в корпусе, сохраняя raw body;
|
||||
- добавить deterministic RNG stream и effect capture.
|
||||
|
||||
Готовность: frame-by-frame poses совпадают с golden reference своей части; все
|
||||
923/1 065 FXID создаются без parser errors; перезапуск одинакового effect seed
|
||||
даёт идентичный список emitted primitives.
|
||||
|
||||
### Этап 5. Карта и мир
|
||||
|
||||
- реализовать `Land.msh` и corrected `TerrainFace28` layout;
|
||||
- построить terrain rendering и CPU surface queries;
|
||||
- реализовать `Land.map`, cell grid и graph links;
|
||||
- визуализировать areals и найденные маршруты;
|
||||
- разобрать [TMA](../reference/tma.md) и выполнять staged mission loading;
|
||||
- создать World3D queue, ObjectId и deferred deletion.
|
||||
|
||||
Готовность: 65 карт и 60 TMA Частей 1 и 2 загружаются до EOF; все areal links
|
||||
валидны; objects появляются в правильных transforms; мир выдерживает расчётные
|
||||
шаги без рендера.
|
||||
|
||||
### Этап 6. Gameplay controllers
|
||||
|
||||
- подключить input snapshot и camera controller;
|
||||
- реализовать navigation corridor, Behavior state machine и Wizard boundary;
|
||||
- создать physical controller и collision manager;
|
||||
- загрузить control resources в lossless typed model;
|
||||
- внедрить game time, pause, event queue и end-of-frame callbacks;
|
||||
- подключить AI layer и symbol/event layer сценариев.
|
||||
|
||||
Готовность: юнит получает цель, строит маршрут, движется по terrain, реагирует
|
||||
на collision и исполняет базовые миссионные события в детерминированном replay.
|
||||
На этом этапе вводится differential branch для изменённых `AniMesh`, `Control` и
|
||||
`Effect`; неизменённые DLL используют общий reference path.
|
||||
|
||||
### Этап 7. Полный кадр, звук и UI
|
||||
|
||||
- реализовать render phases, sorting, lighting, shadows и atmosphere;
|
||||
- подключить 3D listener, sample cache, FX sounds и mission audio;
|
||||
- воспроизвести shell/UI loading и post-world pass;
|
||||
- добавить frame capture до UI и после UI;
|
||||
- зафиксировать capability fallback profiles.
|
||||
|
||||
Готовность: миссия визуально и звуково проходима; каждый draw и sound event
|
||||
имеет trace; одинаковый replay создаёт одинаковые command lists. На этом этапе
|
||||
вводится differential branch для `iron3d` и `services`.
|
||||
|
||||
### Этап 8. Сеть, сохранения и динамическая совместимость
|
||||
|
||||
- реализовать modern transport над versioned game-message schema;
|
||||
- отдельно исследовать DirectPlay wire и `netZipData` для native compatibility;
|
||||
- добавить mirrors, ownership transfer и disconnect cleanup;
|
||||
- восстановить save/campaign state и dispatcher;
|
||||
- выполнить динамические captures оригинала для render states, script VM и
|
||||
physics edge cases.
|
||||
|
||||
Готовность: одиночная кампания запускается из оригинального каталога,
|
||||
сохраняется и продолжается; multiplayer replay согласован между peers; full
|
||||
corpus не создаёт новых parser variants без явной регистрации.
|
||||
|
||||
## Тестовый контур
|
||||
|
||||
Совместимость нельзя подтвердить одним screenshot. Нужны тесты на уровне bytes,
|
||||
структур, ссылок, simulation state, команд renderer-а и конечного изображения.
|
||||
Каждый слой локализует свой класс ошибки.
|
||||
|
||||
```text
|
||||
unit tests
|
||||
-> parser/property tests
|
||||
-> corpus validation
|
||||
-> cross-resource integration
|
||||
-> deterministic simulation replay
|
||||
-> render/audio command captures
|
||||
-> pixel and gameplay parity
|
||||
```
|
||||
|
||||
Failure верхнего уровня всегда должен позволять спуститься к меньшему тесту и
|
||||
понять причину.
|
||||
|
||||
### Unit, property и fuzz tests
|
||||
|
||||
Для каждого binary primitive проверяются little-endian чтение, bounded strings,
|
||||
checked arithmetic и cursor boundaries. Для структур -- минимальный размер,
|
||||
максимальные counts, пустые arrays, нулевые варианты и редкие branches.
|
||||
|
||||
Property tests генерируют случайные корректные NRes/RsLi/WEAR records,
|
||||
выполняют encode -> decode и сравнивают семантику. Fuzz tests изменяют длины,
|
||||
offsets, counts и termination bytes и требуют контролируемой ошибки без crash и
|
||||
чрезмерного выделения памяти.
|
||||
|
||||
Критические алгоритмы имеют отдельные vectors: ASCII casefold, NRes permutation
|
||||
search, RsLi byte transform, LZSS backreferences, quaternion shortest path,
|
||||
matrix composition и terrain mask remap.
|
||||
|
||||
### Corpus validation
|
||||
|
||||
Каждый файл оригинального каталога проходит parser своего семейства. Отчёт
|
||||
содержит hash, variant, counts, warnings, errors и точный offset сбоя. Baseline
|
||||
демоверсии:
|
||||
|
||||
```text
|
||||
MSH 435
|
||||
MAT0 905
|
||||
Texm 518
|
||||
FXID 923
|
||||
WEAR 457
|
||||
Land.msh 6
|
||||
Land.map 6
|
||||
TMA 6
|
||||
unit DAT 425
|
||||
errors 0
|
||||
```
|
||||
|
||||
Изменение parser-а принимается только если baseline остаётся стабильной либо
|
||||
новый variant зарегистрирован с образцом и объяснением. Warnings должны быть
|
||||
именованными: «неизвестное opaque поле» не равно «выход ссылки за диапазон».
|
||||
|
||||
### Cross-resource integration
|
||||
|
||||
Интеграционный тест начинается с миссии и проходит весь dependency graph:
|
||||
object -> prototype -> MSH -> WEAR -> MAT0 -> Texm/lightmap/FXID. Он не
|
||||
ограничивается тем, что файлы существуют: material slot должен указывать на
|
||||
допустимый MAT0, phase -- на допустимую texture, model batch -- на существующий
|
||||
WEAR index.
|
||||
|
||||
Demo mission total: 201 objects -> 501 prototypes -> 501 object MSH/WEAR.
|
||||
Чистый object graph даёт 3 873 material slots и 5 049 texture requests; после
|
||||
включения environment WEAR итог равен 3 879 material slots, 5 067 textures и
|
||||
18 lightmaps, failures 0. Такой тест ловит ошибки casefold, suffix, fallback и
|
||||
путей, которые отдельный parser не замечает.
|
||||
|
||||
Для каждого отсутствующего узла отчёт хранит полный parent chain, чтобы
|
||||
различать broken global archive и реально достижимый mission failure.
|
||||
|
||||
### Deterministic simulation replay
|
||||
|
||||
Записывается начальная миссия, seed, input events, network messages и значения
|
||||
внешних часов. На контрольных ticks сохраняется canonical state hash:
|
||||
|
||||
```text
|
||||
sorted ObjectId list
|
||||
transforms and velocities
|
||||
critical properties and owners
|
||||
AI/behavior state IDs
|
||||
active effect state
|
||||
game clock and RNG states
|
||||
```
|
||||
|
||||
Pointer addresses, allocator order и GPU handles в hash не входят. Два запуска с
|
||||
одинаковым log должны давать одинаковый state hash на каждом checkpoint. Первое
|
||||
расхождение гораздо информативнее финального разного результата миссии.
|
||||
|
||||
### Render command parity
|
||||
|
||||
До pixel comparison сравнивается command list:
|
||||
|
||||
```text
|
||||
camera matrices and viewport
|
||||
visible ObjectIds
|
||||
render phase and stable order
|
||||
model/node/slot/batch IDs
|
||||
material phase and texture handles
|
||||
legacy pipeline states
|
||||
index ranges and transforms
|
||||
```
|
||||
|
||||
Если command lists совпадают, но pixels различаются, проблема находится в
|
||||
shader/backend, sampling или численной точности. Если command lists уже
|
||||
различаются, pixel diff лишь скрывает более раннюю ошибку.
|
||||
|
||||
Golden captures следует хранить отдельно для статической модели, анимации,
|
||||
terrain, transparent FX, shadows, lightmap и atmosphere.
|
||||
|
||||
### Pixel, audio и network tests
|
||||
|
||||
Pixel tests используют фиксированное разрешение, camera, device profile, seed и
|
||||
timeline. Сравниваются exact pixels для CPU/reference path и tolerance metrics
|
||||
для GPU path, но tolerance не должна скрывать переставленные прозрачные
|
||||
primitives.
|
||||
|
||||
Audio tests сравнивают список sound events, sample IDs, positions, loop flags и
|
||||
gains; waveform зависит от mixer/device и является вторичным уровнем. Network
|
||||
tests воспроизводят captured message sequences, проверяют mirrors, ownership и
|
||||
disconnect. Для native DirectPlay compatibility дополнительно нужен packet-level
|
||||
corpus.
|
||||
|
||||
## Regression baselines
|
||||
|
||||
Corpus validation формирует три независимых отчёта: демоверсия, Часть 1 и
|
||||
Часть 2. Каждый сохраняет manifest файлов, hashes executable/DLL, variants,
|
||||
warnings, global archive health и mission reachability.
|
||||
|
||||
Ключевые corpus gates:
|
||||
|
||||
```text
|
||||
NRes: 120 файлов / 6 804 entries и 134 / 8 171 для Частей 1/2
|
||||
TMA: 29 миссий / 864 objects / 28 extras и 31 / 885 / 41
|
||||
MSH: 435 и 511 моделей
|
||||
MAT0: 905 и 1 127 материалов
|
||||
Texm: 518 и 631 текстура
|
||||
FXID: 923 и 1 065 эффектов
|
||||
full reachability: 4 701 и 5 845 prototype requests, failures 0
|
||||
```
|
||||
|
||||
Расширенные mission-reachability totals:
|
||||
|
||||
```text
|
||||
Часть 1: 29 TMA, 864 objects, 4 701 prototypes,
|
||||
36 954 materials, 48 806 textures, 139 lightmaps, failures 0
|
||||
Часть 2: 31 TMA, 885 objects, 5 845 prototypes,
|
||||
50 888 materials, 68 603 textures, 214 lightmaps, failures 0
|
||||
```
|
||||
|
||||
Обязательные regression cases:
|
||||
|
||||
- NRes с ненулевым unindexed region;
|
||||
- prototype inheritance через `objects.rlb`;
|
||||
- unit DAT `description[32]` без NUL;
|
||||
- TMA epilogue и `extra_count` 0--4;
|
||||
- empty SWAV entry;
|
||||
- stale save-slot metadata без payload;
|
||||
- build-scoped RVA lookup.
|
||||
|
||||
Byte-identical asset comparison выполняется только внутри одного корпуса. Между
|
||||
Частями 1 и 2 сравниваются semantic invariants и decoded representation,
|
||||
поскольку многие assets пересобраны.
|
||||
|
||||
## Точность, скорость и повторяемость
|
||||
|
||||
Совместимый движок должен быть корректным, повторяемым и достаточно быстрым.
|
||||
Эти свойства нельзя получать одним и тем же приёмом. Сначала создаётся простой
|
||||
эталонный путь, затем он измеряется и оптимизируется без изменения результата.
|
||||
|
||||
Главные источники расхождений: x87 extended precision, преобразование float в
|
||||
integer, порядок операций, старые SIMD implementations, нестабильная сортировка,
|
||||
RNG и использование разных часов.
|
||||
|
||||
### x87 и округление
|
||||
|
||||
Оригинальный x86-код мог хранить промежуточные значения в 80-битных регистрах
|
||||
x87, а в память записывать 32-битный float. Современный compiler чаще использует
|
||||
SSE с округлением после каждой операции. Различие заметно на границах animation
|
||||
frame, culling plane и collision threshold.
|
||||
|
||||
Для критических формул нужен reference mode:
|
||||
|
||||
- фиксированный порядок операций без reassociation;
|
||||
- запрещённый fast-math;
|
||||
- явные преобразования и проверенный режим округления;
|
||||
- тесты возле half-integer и epsilon boundaries;
|
||||
- при необходимости extended intermediate через `long double` на проверенной
|
||||
платформе.
|
||||
|
||||
Не требуется эмулировать x87 во всём движке. Нужно локализовать функции, где
|
||||
малое отличие меняет дискретное решение, и держать для них scalar reference path.
|
||||
|
||||
### RNG как часть состояния
|
||||
|
||||
FX, atmosphere и, вероятно, AI используют случайные значения. Один глобальный
|
||||
RNG легко расходится, если новая реализация запрашивает дополнительное число для
|
||||
визуальной оптимизации. Для трассировки полезны именованные streams:
|
||||
|
||||
```text
|
||||
world/gameplay RNG
|
||||
AI/script RNG
|
||||
FX instance RNG
|
||||
atmosphere RNG
|
||||
non-deterministic cosmetic RNG
|
||||
```
|
||||
|
||||
Для native parity может потребоваться один общий алгоритм и точная sequence. До
|
||||
подтверждения capture каждый stream хранит seed и счётчик вызовов в trace.
|
||||
Cosmetic stream не входит в simulation hash.
|
||||
|
||||
### Стабильный порядок
|
||||
|
||||
Коллекции не должны зависеть от адресов, unordered containers или порядка
|
||||
завершения worker threads. Для объектов, collision pairs, opaque/transparent
|
||||
draws и network messages задаются явные stable keys:
|
||||
|
||||
- objects -- queue insertion sequence или OriginalObjectId;
|
||||
- collision pairs -- упорядоченная пара IDs;
|
||||
- opaque draws -- phase, pipeline key, material, stable insertion ID;
|
||||
- transparent draws -- layer, quantized distance, stable insertion ID;
|
||||
- network messages -- sequence и sender.
|
||||
|
||||
Даже когда математический результат коммутативен, side effects, cache accesses и
|
||||
RNG делают порядок наблюдаемым.
|
||||
|
||||
### Часы и fixed-step
|
||||
|
||||
Monotonic platform clock хранится отдельно от game clock. Pause и time scaling
|
||||
применяются к game clock. Simulation работает с фиксированным или точно
|
||||
воспроизводимым шагом, а render может интерполировать presentation state, не
|
||||
изменяя authoritative world.
|
||||
|
||||
Maintenance timers кэшей используют реальные часы или отдельную подтверждённую
|
||||
шкалу; их срабатывание не должно менять gameplay. При перегрузке лучше выполнить
|
||||
ограниченное число simulation steps и явно зафиксировать dropped presentation
|
||||
frames, чем передать огромный `dt` в AI/physics.
|
||||
|
||||
### Оптимизация без потери эталона
|
||||
|
||||
1. Сохранить scalar reference implementation.
|
||||
2. Добавить profiler counters на decoding, culling, sorting, animation, upload
|
||||
и draw.
|
||||
3. Оптимизировать только измеренный bottleneck.
|
||||
4. Сравнить SIMD/parallel результат с reference на полном corpus.
|
||||
5. Оставить runtime switch для отключения оптимизации при диагностике.
|
||||
|
||||
`g_FastProc` удобно моделировать как таблицу function objects: все slots сначала
|
||||
указывают на scalar path, затем безопасные slots заменяются SIMD-вариантами
|
||||
после self-test на старте.
|
||||
|
||||
### Кэш и память
|
||||
|
||||
Архивы, decoded blobs, CPU assets и GPU resources имеют отдельные budgets.
|
||||
Eviction разрешена только для объектов с нулевым external refcount и после
|
||||
безопасной frame fence. Original delayed cleanup порядка десятков секунд можно
|
||||
воспроизвести policy-параметрами, не сканируя все entries каждый кадр.
|
||||
|
||||
Основные показатели: число открытых архивов, decoded bytes, resident
|
||||
textures/lightmaps, models, active FX, draw items и deferred-delete size. Любой
|
||||
неограниченно растущий счётчик является regression. Производительность считается
|
||||
достаточной только после корректности: стабильные 60 FPS с неверным LOD или
|
||||
пропущенными эффектами не являются успехом.
|
||||
|
||||
## Release gates
|
||||
|
||||
Версия не выпускается, если:
|
||||
|
||||
- появился новый corpus error;
|
||||
- изменился byte roundtrip неизменённых ресурсов;
|
||||
- dependency graph получил failure в достижимом пути;
|
||||
- deterministic replay расходится;
|
||||
- command capture изменился без ожидаемого changelog;
|
||||
- parser допускает allocation по непроверенному count;
|
||||
- новая оптимизация не имеет scalar reference comparison.
|
||||
|
||||
Каждое исправление регистрирует минимальный regression asset или synthetic
|
||||
vector. Если новый behavior намеренно отличается от предыдущего, изменение
|
||||
должно иметь compatibility profile, corpus sample и объяснение, почему старый
|
||||
baseline был неполным или неверным.
|
||||
|
||||
## Уровни совместимости
|
||||
|
||||
Слово «совместимый» используется только с уровнем:
|
||||
|
||||
1. **Archive-compatible** -- открывает и сохраняет контейнеры.
|
||||
2. **Asset-compatible** -- декодирует модели, материалы, текстуры и эффекты.
|
||||
3. **Mission-compatible** -- загружает карту и создаёт все объекты.
|
||||
4. **Runtime-compatible** -- исполняет время, события, поведение и физику.
|
||||
5. **Presentation-compatible** -- воспроизводит рендер и звук.
|
||||
6. **Game-compatible** -- позволяет пройти миссии, сохраняться и продолжать.
|
||||
7. **Native-interoperable** -- взаимодействует с оригинальной сетью и внешним
|
||||
ABI.
|
||||
|
||||
Viewer с красивой моделью находится только на втором уровне.
|
||||
|
||||
### Обязательные критерии запуска и данных
|
||||
|
||||
- приложение запускается из неизменённого оригинального каталога;
|
||||
- относительные пути, регистр и legacy encodings разрешаются по исходным
|
||||
правилам;
|
||||
- все требуемые NRes/RsLi открываются без предварительной конвертации;
|
||||
- parsers проверяют границы и не используют неопределённые bytes как указатели;
|
||||
- неизвестные поля сохраняются lossless;
|
||||
- все mission-reachable prototype, model, material, texture, lightmap и effect
|
||||
references разрешаются;
|
||||
- отсутствие необязательного ресурса следует документированному fallback, а не
|
||||
случайному default.
|
||||
|
||||
### Обязательные критерии мира
|
||||
|
||||
- TMA разбирается до точного EOF;
|
||||
- `Land.msh` и `Land.map` создают корректную поверхность и areal graph;
|
||||
- ObjectId, owner и mirror semantics устойчивы;
|
||||
- queue traversal и deferred deletion безопасны;
|
||||
- pause, game time и simulation steps повторяемы;
|
||||
- AI/Behavior/Wizard/Control взаимодействуют через заданные границы;
|
||||
- collision и navigation не подменяют друг друга;
|
||||
- script events используют logical IDs и переживают удаление объектов;
|
||||
- deterministic replay совпадает на контрольных ticks.
|
||||
|
||||
### Обязательные критерии presentation
|
||||
|
||||
- static и animated MSH используют правильные slots, batches и transforms;
|
||||
- WEAR/MAT0/Texm fallback и phase timing совпадают;
|
||||
- mip-skip, palettes, Page atlases и lightmaps работают;
|
||||
- render phases, depth/cull/blend state и transparent order подтверждены
|
||||
captures;
|
||||
- FXID commands и RNG дают устойчивый результат;
|
||||
- camera и 3D sound listener синхронизированы;
|
||||
- atmosphere, тени, солнце и flares не являются декоративными заглушками;
|
||||
- UI и world rendering имеют правильную границу;
|
||||
- golden command captures стабильны, pixel parity измеряется на фиксированных
|
||||
сценах.
|
||||
|
||||
### Обязательные критерии полной игры
|
||||
|
||||
- все доступные миссии стартуют, завершаются и корректно сообщают
|
||||
success/failure;
|
||||
- campaign dispatcher сохраняет прогресс;
|
||||
- savegame восстанавливает world, script, AI, RNG и clocks, а не только
|
||||
placement;
|
||||
- input remapping, pause, camera modes, sound и настройки работают из UI;
|
||||
- длительный прогон не накапливает objects, resources или audio sources;
|
||||
- ошибки данных показывают actionable chain;
|
||||
- производительность приемлема без отключения подсистем;
|
||||
- демоверсия, Часть 1 и Часть 2 проходят один и тот же тестовый контур с
|
||||
раздельными manifests и эталонами.
|
||||
|
||||
### Native interoperability
|
||||
|
||||
Самый строгий уровень дополнительно требует совпадения x86 ABI экспортов, vtable
|
||||
slots и calling conventions для подключаемых оригинальных модулей, а также
|
||||
DirectPlay wire/framing и compression. Этот уровень независим от возможности
|
||||
играть в новом standalone runtime.
|
||||
|
||||
Проект может честно заявлять game compatibility без native DLL/network
|
||||
interoperability, но это должно быть явно указано. Аналогично pixel-perfect режим
|
||||
может быть отдельным compatibility profile поверх функционально корректного
|
||||
renderer-а.
|
||||
|
||||
### Совместимость нескольких наборов данных
|
||||
|
||||
Критерий полной совместимости применяется отдельно к демоверсии, Части 1 и
|
||||
Части 2. Прохождение одного набора не позволяет заявлять поддержку остальных.
|
||||
|
||||
Обязательное различие:
|
||||
|
||||
- **format compatibility** -- один parser принимает все три набора;
|
||||
- **content compatibility** -- конкретная миссия разрешает весь reachable graph;
|
||||
- **behavior compatibility** -- runtime совпадает с соответствующей сборкой
|
||||
изменённых DLL;
|
||||
- **cross-version support** -- один новый движок выбирает корректные данные и
|
||||
defaults по fingerprint установки.
|
||||
|
||||
Content fingerprint включает hashes executable/DLL и manifest ключевых архивов.
|
||||
Он не используется для запрета модификаций, но выбирает compatibility profile и
|
||||
делает отклонение диагностируемым.
|
||||
|
||||
## Definition of done
|
||||
|
||||
Полное документирование и реализация считаются завершёнными только когда каждый
|
||||
критерий связан с главой спецификации, executable test и хотя бы одним
|
||||
corpus/golden case. Утверждение без проверяемого критерия остаётся
|
||||
исследовательской заметкой, а не контрактом.
|
||||
File diff suppressed because it is too large
Load Diff
+57
-27
@@ -3,7 +3,7 @@ site_name: FParkan
|
||||
site_url: https://fparkan.popov.link/
|
||||
site_author: Valentin Popov
|
||||
site_description: >-
|
||||
Utilities and tools for the game “Parkan: Iron Strategy”.
|
||||
Техническая книга о восстановлении игрового движка Iron3D из Parkan: Iron Strategy.
|
||||
|
||||
# Repository
|
||||
repo_name: valentineus/fparkan
|
||||
@@ -16,36 +16,66 @@ copyright: Copyright © 2023 — 2026 Valentin Popov
|
||||
theme:
|
||||
name: material
|
||||
language: ru
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.sections
|
||||
- navigation.indexes
|
||||
- navigation.top
|
||||
- toc.follow
|
||||
- search.highlight
|
||||
- search.suggest
|
||||
palette:
|
||||
scheme: slate
|
||||
- media: "(prefers-color-scheme: light)"
|
||||
scheme: default
|
||||
primary: indigo
|
||||
accent: deep orange
|
||||
- media: "(prefers-color-scheme: dark)"
|
||||
scheme: slate
|
||||
primary: indigo
|
||||
accent: deep orange
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- attr_list
|
||||
- def_list
|
||||
- md_in_html
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.details
|
||||
- pymdownx.highlight
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tabbed:
|
||||
alternate_style: true
|
||||
|
||||
plugins:
|
||||
- search:
|
||||
lang:
|
||||
- ru
|
||||
- en
|
||||
|
||||
# Navigation
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Specs:
|
||||
- 3D implementation notes: specs/msh-notes.md
|
||||
- AI system: specs/ai.md
|
||||
- ArealMap: specs/arealmap.md
|
||||
- Behavior system: specs/behavior.md
|
||||
- Control system: specs/control.md
|
||||
- FXID: specs/fxid.md
|
||||
- Material (MAT0): specs/material.md
|
||||
- Wear (WEAR): specs/wear.md
|
||||
- Texture (Texm): specs/texture.md
|
||||
- Materials index: specs/materials-texm.md
|
||||
- Missions: specs/missions.md
|
||||
- Object registry (objects.rlb): specs/object-registry.md
|
||||
- MSH animation: specs/msh-animation.md
|
||||
- MSH core: specs/msh-core.md
|
||||
- Network system: specs/network.md
|
||||
- NRes / RsLi: specs/nres.md
|
||||
- Render pipeline: specs/render.md
|
||||
- Render parity: specs/render-parity.md
|
||||
- Runtime pointer: specs/runtime-pipeline.md
|
||||
- Sound system: specs/sound.md
|
||||
- Terrain + map loading: specs/terrain-map-loading.md
|
||||
- UI system: specs/ui.md
|
||||
- Форматы 3D‑ресурсов (обзор): specs/msh.md
|
||||
- Начало: index.md
|
||||
- Книга:
|
||||
- I. Путеводитель и методика: tomes/01-guide.md
|
||||
- II. Запуск, архитектура и игровой цикл: tomes/02-architecture.md
|
||||
- III. Ресурсная система и форматы: tomes/03-resources.md
|
||||
- IV. Мир, миссии и игровой runtime: tomes/04-world.md
|
||||
- V. Геометрия, материалы и рендер: tomes/05-render.md
|
||||
- VI. Поведение, управление, звук и сеть: tomes/06-behavior.md
|
||||
- VII. Руководство по полной реализации: tomes/07-implementation.md
|
||||
- VIII. Справочник и доказательная база: tomes/08-evidence.md
|
||||
- Справочник:
|
||||
- NRes: reference/nres.md
|
||||
- RsLi: reference/rsli.md
|
||||
- TMA: reference/tma.md
|
||||
- MSH: reference/msh.md
|
||||
- WEAR и MAT0: reference/materials.md
|
||||
- Texm: reference/texm.md
|
||||
- Render frame: reference/render-frame.md
|
||||
- Приложения:
|
||||
- Глоссарий: appendices/glossary.md
|
||||
- Границы знания: appendices/knowledge-boundaries.md
|
||||
|
||||
# Additional configuration
|
||||
extra:
|
||||
|
||||
Reference in New Issue
Block a user