aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes/04-world.md
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 00:58:51 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 00:58:51 +0300
commit78fc5f1debf1395d5df0bab7cc0dde54351205cb (patch)
treeef8f7c72a183723fcbea0b2d1fefd7c28ca7bc18 /docs/tomes/04-world.md
parent50c2cf4686b53ebd2b76318223096660e92305a4 (diff)
downloadfparkan-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.md648
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.