diff options
| author | Valentin Popov <valentin@popov.link> | 2026-06-22 00:58:51 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-06-22 00:58:51 +0300 |
| commit | 78fc5f1debf1395d5df0bab7cc0dde54351205cb (patch) | |
| tree | ef8f7c72a183723fcbea0b2d1fefd7c28ca7bc18 /docs/tomes/04-world.md | |
| parent | 50c2cf4686b53ebd2b76318223096660e92305a4 (diff) | |
| download | fparkan-78fc5f1debf1395d5df0bab7cc0dde54351205cb.tar.xz fparkan-78fc5f1debf1395d5df0bab7cc0dde54351205cb.zip | |
docs: rewrite MkDocs documentation
Diffstat (limited to 'docs/tomes/04-world.md')
| -rw-r--r-- | docs/tomes/04-world.md | 648 |
1 files changed, 648 insertions, 0 deletions
diff --git a/docs/tomes/04-world.md b/docs/tomes/04-world.md new file mode 100644 index 0000000..82d9e47 --- /dev/null +++ b/docs/tomes/04-world.md @@ -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. |
