31 KiB
IV. Мир, миссии и игровой runtime
Миссия в Iron3D не является готовым снимком мира. Она задаёт исходные данные:
маршруты, кланы, размещённые объекты, свойства, ссылку на ландшафт и
дополнительные записи. Runtime строит из этого карту, пространственные
структуры, очередь World3D, визуальные представления, controllers и связи с
ресурсной системой.
Для совместимой реализации важно не смешивать три слоя:
- Disk data --
data.tma,Land.msh,Land.map,BuildDat.lstи связанные resource archives. - Prepared data -- разобранные paths, clans, terrain streams, areal graph, prototype graph, material и texture handles.
- Runtime objects -- World3D instances, domain controllers, spatial registration, AI/scripts, timers и расчётный tick.
Граница между этими слоями нужна для диагностики и отката. Ошибка в достижимой цепочке размещённого объекта должна остановить создание миссии до публикации объекта в очереди событий. Недостижимая запись общего архива может быть inventory warning и не обязана блокировать текущую карту.
data.tma: данные миссии
data.tma -- основное описание расстановки и логической конфигурации миссии.
Он не содержит всю геометрию, материалы или AI-код. Файл перечисляет paths,
clans, objects, свойства и ссылки на внешние прототипы. Подробный справочный
контракт формата вынесен в TMA, но глава использует его
как часть сквозного runtime pipeline.
TMA читается строго последовательно bounded cursor-ом. Записи имеют переменную длину, поэтому offsets следующих секций получаются только после разбора предыдущих. Секции нельзя искать по сигнатурам: порядок управляется счётчиками, длинами и mode-dependent ветками.
Главный критерий корректности -- cursor.offset == file_size после последней
записи. Неописанный хвост, переполнение при вычислении размеров, отрицательный
или чрезмерный count и выход за bounds являются ошибками parser-а, а не
материалом для эвристического восстановления.
Верхний уровень
Все переменные строки в проверенных TMA используют length-prefixed primitive:
struct LpString {
uint32_t byte_length;
uint8_t bytes[byte_length];
};
Завершающий NUL не является обязательной частью framing. Reader продвигается
ровно на 4 + byte_length. Текст можно декодировать как legacy ANSI/CP1251 для
человекочитаемого представления, но исходные bytes сохраняются для lossless
режима.
Подтверждённый верхний уровень:
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
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-часть:
LpString name
i32 raw_id
f32 anchor_x
f32 anchor_y
u32 mode
mode-dependent body
relation table
Для обычных modes 1..3 тело содержит две пары:
LpString resource_path
i32 resource_tag
LpString resource_path
i32 resource_tag
После них идёт relation table:
u32 relation_count
repeat relation_count:
LpString other_clan_name
i32 relation_value
Первая ресурсная строка обычно указывает на script/formula base, вторая -- на TRF или пустой ресурс. Tags различаются между кланами и должны сохраняться как raw-поля, пока их потребительская семантика не закрыта.
Mode 0 имеет отдельный count-driven layout:
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. Оно имеет два рабочих
варианта:
- прямой логический ключ прототипа, который ищется в
objects.rlb; - путь к unit DAT, из которого получается список компонентных ключей.
Доказанное framing объектной записи:
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:
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 является вспомогательным, а не каноническим представлением.
struct ExtraRecord28 {
float position[3];
uint32_t raw[4];
};
Последние четыре слова ExtraRecord28 пока не нормализуются. Reader хранит их
как raw data и не позволяет extra record поглотить начало следующей секции или
файловый хвост.
Покрытие полных каталогов:
Часть 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.
Сквозная цепочка:
TMA object
-> direct prototype key или unit DAT
-> component key
-> objects.rlb entry
-> MSH и WEAR
-> material slots
-> MAT0 phases
-> Texm и lightmap
-> prepared World3D instance
Контейнеры и графические форматы описаны отдельно в NRes, MSH, WEAR и MAT0 и Texm. В этой главе они рассматриваются как ребра создания мира.
Фазы loader-а
- Mission context. Выбрать каталог миссии, прочитать конфигурацию и определить карту.
- World foundation. Загрузить
Land.msh,Land.map,BuildDat.lstи создать spatial managers. - Mission description. Разобрать TMA, paths и clans, но пока не публиковать объекты.
- Prototype resolution. Для каждой размещённой сущности раскрыть прямой ключ или unit DAT и построить component list.
- Resource preparation. Открыть требуемые RLB/LIB, проверить MSH, WEAR, MAT0, textures, lightmaps и effects.
- Instance construction. Создать World3D objects и domain controllers, заполнить transform, ownership и properties.
- Registration. Только после успешной настройки добавить instances в queue и spatial structures.
- 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:
Часть 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-архивом, но его содержимое
отличается от обычной объектной MSH. Он хранит геометрию поверхности, таблицы
участков и ускорители пространственных запросов. Видимые buffers являются лишь
частью данных: CPU-подсистемам остаются нужны adjacency, surface classes и
cell accelerator streams.
Во всех проверенных картах порядок типов одинаков:
1, 2, 3, 4, 5, 18, 14, 11, 21
Типы 1, 3, 4 и 5 совместимы по базовому представлению с узлами,
позициями, нормалями и UV обычной модели. Типы 11 и 21 специфичны для
terrain; 14 и 18 являются дополнительными потоками.
Streams и размеры элементов
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:
+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(). Неизвестные биты полной маски сохраняются отдельно, иначе
обратное преобразование потеряет информацию.
Основное соответствие:
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.
Проверенное покрытие
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
Расширенное покрытие:
Часть 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 ареала
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
За вершинами хранится массив:
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 обязан быть меньше общего числа ареалов.
Покрытие:
Ранние шесть карт: 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; здесь важна подготовка 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-битными масками:
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-ов:
- открыть
Land.mshи построить geometry/spatial данные terrain; - открыть
Land.mapи создать areals, links и cell grid; - загрузить категории
BuildDat.lst; - создать world managers для поверхности, областей, света и атмосферы;
- разобрать TMA, paths и clans;
- раскрыть object resources через unit DAT и
objects.rlb; - подготовить MSH, WEAR, MAT0, Texm, lightmap и FXID dependencies;
- создать World3D objects и domain controllers в pending state;
- проверить cross references между components, controllers и spatial data;
- зарегистрировать visual, physical и behavior components;
- подключить AI/scripts и разрешить первый calculation tick.
Минимальный псевдокод объектной части:
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.