aboutsummaryrefslogtreecommitdiff
path: root/docs/tomes/04-world.md
blob: 82d9e47cb7671925652189e8b2f61f6bdb277510 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
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.