docs: rewrite MkDocs documentation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 34s
Test / Lint (push) Failing after 1m7s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped

This commit is contained in:
2026-06-22 01:58:51 +04:00
parent 50c2cf4686
commit 78fc5f1deb
44 changed files with 6336 additions and 2586 deletions
+200
View File
@@ -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.
+120
View File
@@ -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
View File
@@ -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
сохранение.
+69
View File
@@ -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`.
+82
View File
@@ -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.
+61
View File
@@ -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.
+56
View File
@@ -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.
+69
View File
@@ -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`.
+67
View File
@@ -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.
+64
View File
@@ -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.
-35
View File
@@ -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» для побайтного/пошагового сравнения с оригиналом.
-31
View File
@@ -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-тестов поведения навигационных запросов на одинаковых входах.
-28
View File
@@ -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-сценариях.
-28
View File
@@ -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-тесты на записанных последовательностях ввода.
-45
View File
@@ -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-тесты».
-202
View File
@@ -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 новый рендер) на фиксированных сценах.
-144
View File
@@ -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 на реальных моделях.
-18
View File
@@ -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`.
-46
View File
@@ -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)
-126
View File
@@ -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» на длинных анимациях.
-192
View File
@@ -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-паритетом.
-118
View File
@@ -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-метаданных (материал/эффект/анимация).
-39
View File
@@ -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-контрактов.
-28
View File
@@ -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-тестов на контролируемой потере/задержке.
-200
View File
@@ -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)
-145
View File
@@ -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-паритета.
-90
View File
@@ -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.
-182
View File
@@ -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)
-227
View File
@@ -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-диапазон).
-18
View File
@@ -1,18 +0,0 @@
# Runtime pipeline
Актуальный документ по полному кадру находится здесь:
- [Render pipeline](render.md)
Эта страница оставлена как совместимый указатель для старых ссылок.
## Статус покрытия и что осталось до 100%
Закрыто:
1. Актуальный runtime-пайплайн централизован в `render.md`.
Осталось:
1. Поддерживать обратную совместимость ссылок при дальнейшей декомпозиции render-документа.
-32
View File
@@ -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-тестов (тайминг/громкость/панорама).
-291
View File
@@ -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 закрыты, но не все биты имеют документированные геймплейные имена).
-153
View File
@@ -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 веток.
-33
View File
@@ -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-тесты (скриншотные и событийные).
-96
View File
@@ -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».
+371
View File
@@ -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. Что должна сделать реализация, если правило нарушено?
Если на один из вопросов нет ответа, утверждение остаётся согласованным выводом
или открытым вопросом, а не закрытым контрактом.
+472
View File
@@ -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.
+561
View File
@@ -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 без
потери.
+648
View File
@@ -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.
+863
View File
@@ -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).
+769
View File
@@ -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.
+674
View File
@@ -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
View File
@@ -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 &copy; 2023 &mdash; 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: