649 lines
31 KiB
Markdown
649 lines
31 KiB
Markdown
|
|
# 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.
|