# 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 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.