2026-02-11 21:12:05 +00:00
# Terrain + map loading
2026-02-12 10:17:41 +00:00
Документ описывает полный runtime-пайплайн загрузки ландшафта и карты (`Terrain.dll` + `ArealMap.dll` ) и требования к toolchain для 1:1 совместимости (чтение, конвертация, редактирование, обратная сборка).
Источник реверса:
- `tmp/disassembler1/Terrain.dll.c`
- `tmp/disassembler1/ArealMap.dll.c`
- `tmp/disassembler2/Terrain.dll.asm`
- `tmp/disassembler2/ArealMap.dll.asm`
Связанные спецификации:
- [NRes / RsLi ](nres.md )
- [MSH core ](msh-core.md )
- [ArealMap ](arealmap.md )
---
## 1. Назначение подсистем
### 1.1. `Terrain.dll`
Отвечает за:
- загрузку и хранение terrain-геометрии из `*.msh` (NRes);
- фильтрацию и выборку треугольников для коллизий/трассировки/рендера;
- рендер terrain-примитивов и связанного shading;
- использование микро-текстурного канала (chunk type 18).
Характерные runtime-строки:
- `CLandscape::CLandscape()`
- `Unable to find microtexture mapping chunk`
- `Rendering empty primitive!`
- `Rendering empty primitive2!`
### 1.2. `ArealMap.dll`
Отвечает за:
- загрузку геометрии ареалов из `*.map` (NRes, chunk type 12);
- построение связей "ареал <-> соседи/подграфы";
- grid-ускорение по ячейкам карты;
- runtime-доступ к `ISystemArealMap` (интерфейс id `770` ) и ареалам (id `771` ).
Характерные runtime-строки:
- `SystemArealMap panic: Cannot load ArealMapGeometry`
- `SystemArealMap panic: Cannot find chunk in resource`
- `SystemArealMap panic: ArealMap Cells are empty`
- `SystemArealMap panic: Incorrect ArealMap`
---
## 2. End-to-End загрузка уровня
### 2.1. Имена файлов уровня
В `CLandscape::CLandscape()` базовое имя уровня `levelBase` разворачивается в:
- `levelBase + ".msh"` : terrain-геометрия;
- `levelBase + ".map"` : геометрия ареалов/навигация;
- `levelBase + "1.wea"` и `levelBase + "2.wea"` : weather/материалы.
### 2.2. Порядок инициализации (высокоуровнево)
1. Получение `3DRender` и `3DSound` .
2. Загрузка `MatManager` (`*.wea` ), `LightManager` , `CollManager` , `FxManager` .
3. Создание `SystemArealMap` через `CreateSystemArealMap(..., "<level>.map", ...)` .
4. Открытие terrain-библиотеки `niOpenResFile("<level>.msh")` .
5. Загрузка terrain-chunk-ов (см. §3).
6. Построение runtime-границ, grid-ускорителей и рабочих массивов.
Критичные ошибки на любом шаге приводят к `ngiProcessError` /panic.
---
## 3. Формат terrain `*.msh` (NRes)
### 3.1. Используемые chunk type в `Terrain.dll`
Порядок загрузки в `CLandscape::CLandscape()` :
| Порядок | Type | Обяз. | Использование (подтверждено кодом) |
|---|---:|---|---|
| 1 | 3 | да | поток позиций (`stride = 12` ) |
| 2 | 4 | да | поток packed normal (`stride = 4` ) |
| 3 | 5 | да | UV-поток (`stride = 4` ) |
| 4 | 18 | да | microtexture mapping (`stride = 4` ) |
| 5 | 14 | нет | опциональный доп. поток (`stride = 4` , отсутствует на части карт) |
| 6 | 21 | да | таблица terrain-face (по 28 байт) |
| 7 | 2 | да | header + slot-таблицы (используются диапазоны face) |
| 8 | 1 | да | node/grid-таблица (stride 38) |
| 9 | 11 | да | доп. индекс/ускоритель для запросов (cell->list) |
Ключевые проверки:
- отсутствие type `18` вызывает `Unable to find microtexture mapping chunk` ;
- отсутствие остальных обязательных чанков вызывает `Unable to open file` .
### 3.2. Node/slot структура для terrain
Terrain-код использует те же stride и адресацию, что и core-описание:
- node-запись: `38` байт;
- slot-запись: `68` байт;
- доступ к первому slot-index: `node + 8` ;
- tri-диапазон в slot: `slot + 140` (offset 0 внутри slot), `slot + 142` (offset 2).
Это согласуется с [MSH core ](msh-core.md ) для `Res1/Res2` :
- `Res1` : `uint16[19]` на node;
- `Res2` : header + slot table (`0x8C + N * 0x44` ).
### 3.3. Terrain face record (type 21, 28 bytes)
Подтвержденные поля из runtime-декодирования face:
```c
struct TerrainFace28 {
uint32_t flags; // +0
uint8_t materialId; // +4 (читается как byte)
uint8_t auxByte; // +5
uint16_t unk06; // +6
uint16_t i0; // +8 (индекс вершины)
uint16_t i1; // +10
uint16_t i2; // +12
uint16_t n0; // +14 (сосед, 0xFFFF -> нет)
uint16_t n1; // +16
uint16_t n2; // +18
int16_t nx; // +20 packed normal component
int16_t ny; // +22
int16_t nz; // +24
uint8_t edgeClass; // +26 (три 2-бит значения)
uint8_t unk27; // +27
};
```
`edgeClass` декодируется как:
- `edge0 = byte26 & 0x3`
- `edge1 = (byte26 >> 2) & 0x3`
- `edge2 = (byte26 >> 4) & 0x3`
### 3.4. Маски флагов face
В о многих запросах применяется фильтр:
```c
(faceFlags & requiredMask) == requiredMask &&
(faceFlags | ~forbiddenMask) == ~forbiddenMask
```
Эквивалентно: "все required-биты выставлены, forbidden-биты отсутствуют".
Подтверждено активное использование битов:
- `0x8` (особая обработка в трассировке)
- `0x2000`
- `0x20000`
- `0x100000`
- `0x200000`
Кроме "полной" 32-бит маски, runtime использует компактные маски в API-запросах.
Подтверждённый remap `full -> compactMain16` (функции `sub_10013FC0` , `sub_1004BA00` , `sub_1004BB40` ):
| Full bit | Compact bit |
|---:|---:|
| `0x00000001` | `0x0001` |
| `0x00000008` | `0x0002` |
| `0x00000010` | `0x0004` |
| `0x00000020` | `0x0008` |
| `0x00001000` | `0x0010` |
| `0x00004000` | `0x0020` |
| `0x00000002` | `0x0040` |
| `0x00000400` | `0x0080` |
| `0x00000800` | `0x0100` |
| `0x00020000` | `0x0200` |
| `0x00002000` | `0x0400` |
| `0x00000200` | `0x0800` |
| `0x00000004` | `0x1000` |
| `0x00000040` | `0x2000` |
| `0x00200000` | `0x8000` |
Подтверждённый remap `full -> compactMaterial6` (функции `sub_10014090` , `sub_10015540` , `sub_1004BB40` ):
| Full bit | Compact bit |
|---:|---:|
| `0x00000100` | `0x01` |
| `0x00008000` | `0x02` |
| `0x00010000` | `0x04` |
| `0x00040000` | `0x08` |
| `0x00080000` | `0x10` |
| `0x00000080` | `0x20` |
Подтверждённый remap `compact -> full` (функция `sub_10015680` ):
- `a2[4]` /`a2[5]` (compactMain16 required/forbidden) + `a2[6]` /`a2[7]` (compactMaterial6 required/forbidden)
- разворачиваются в `fullRequired/fullForbidden` в `this[4]/this[5]` .
Для toolchain это означает:
- если редактируется только бинарник `type 21` , достаточно сохранять `flags` как есть;
- если реализуется API-совместимый runtime-слой, нужно поддерживать о б а представления (`full` и `compact` ) и точный remap выше.
### 3.5. Grid-ускоритель terrain-запросов
Runtime строит grid descriptor с параметрами:
- origin (`baseX/baseY` );
- масштабные коэффициенты (`invSizeX/invSizeY` );
- размеры сетки (`cellsX` , `cellsY` ).
Дальше запросы:
1. переводят world AABB в диапазон grid-ячеек (`floor(...)` );
2. берут диапазон face через `Res1/Res2` (slot `triStart/triCount` );
3. дополняют кандидаты из cell-списков (chunk type 11);
4. применяют маски флагов;
5. выполняют геометрию (plane/intersection/point-in-triangle).
### 3.6. Cell-списки по ячейкам (`type 11` и runtime-массивы)
В `CLandscape` после инициализации используются три параллельных массива по ячейкам (`cellsX * cellsY` ):
- `this+31588` (`sub_100164B0` ctor): массив записей по `12` байт, каждая запись содержит динамический буфер `8` -байтовых элементов;
- `this+31592` (`sub_100164E0` ctor): массив записей по `12` байт, каждая запись содержит динамический буфер `4` -байтовых элементов;
- `this+31596` (`sub_1001F880` ctor): массив записей по `12` байт для runtime-объектов/агентов (буфер `4` -байтовых идентификаторов/указателей).
Общий header записи списка:
```c
struct CellListHdr {
void* ptr; // +0
int count; // +4
int capacity; // +8
};
```
Подтвержденные element-layout:
- `this+31588` : элемент `8` байт (`uint32_t id` , `uint32_t aux` ), добавление через `sub_10012E20` пишет `aux = 0` ;
- `this+31592` : элемент `4` байта (`uint32_t id` );
- `this+31596` : элемент `4` байта (runtime object handle/pointer id).
Практический вывод для редактора:
- `type 11` должен считаться источником cell-ускорителя;
- неизвестные/дополнительные поля внутри списков должны сохраняться как есть;
- нельзя "нормализовать" или переупорядочивать списки без полного пересчёта всех зависимых runtime-структур.
2026-02-11 21:12:05 +00:00
---
2026-02-12 10:17:41 +00:00
## 4. Формат `*.map` (ArealMapGeometry, chunk type 12)
### 4.1. Точка входа
`CreateSystemArealMap(..., "<level>.map", ...)` вызывает `sub_1001E0D0` :
1. `niOpenResFile("<level>.map")` ;
2. поиск chunk type `12` ;
3. чтение chunk-данных;
4. разбор `ArealMapGeometry` .
При ошибках выдаются panic-строки `SystemArealMap panic: ...` .
### 4.2. Верхний уровень chunk 12
Используются:
- `entry.attr1` (из каталога NRes) как `areal_count` ;
- `entry[+0x0C]` как размер payload chunk для контроля полного разбора.
Данные chunk:
1. `areal_count` переменных записей ареалов;
2. секция grid-ячеек (`cellsX/cellsY` + списки попаданий).
### 4.3. Переменная запись ареала
Полностью подтверждённые элементы layout:
```c
// record = начало записи ареала
float anchor_x = * (float * )(record + 0);
float anchor_y = * (float * )(record + 4);
float anchor_z = * (float * )(record + 8);
float reserved_12 = * (float * )(record + 12); // в retail-данных всегда 0
float area_metric = * (float * )(record + 16); // предрасчитанная площадь ареала
float normal_x = * (float * )(record + 20);
float normal_y = * (float * )(record + 24);
float normal_z = * (float * )(record + 28); // unit vector (|n| ~= 1)
uint32_t logic_flag = * (uint32_t * )(record + 32); // активно используется в runtime
uint32_t reserved_36 = * (uint32_t * )(record + 36); // в retail-данных всегда 0
uint32_t class_id = * (uint32_t * )(record + 40); // runtime-class/type id ареала
uint32_t reserved_44 = * (uint32_t * )(record + 44); // в retail-данных всегда 0
uint32_t vertex_count = * (uint32_t * )(record + 48);
uint32_t poly_count = * (uint32_t * )(record + 52);
float* vertices = (float*)(record + 56); // float3[vertex_count]
// сразу после vertices:
// EdgeLink8[vertex_count + 3*poly_count]
// где EdgeLink8 = { int32_t area_ref; int32_t edge_ref; }
// первые vertex_count записей используются как per-edge соседство границы ареала.
EdgeLink8* links = (EdgeLink8*)(record + 56 + 12 * vertex_count);
uint8_t* p = (uint8_t*)(links + (vertex_count + 3 * poly_count));
for (i=0; i<poly_count; i++) {
uint32_t n = * (uint32_t * )p;
p += 4 * (3*n + 1);
}
// p -> начало следующей записи ареала
```
Т о есть для toolchain:
- поля `+0/+4/+8` , `+16` , `+20..+28` , `+32` , `+40` , `+48` , `+52` являются runtime-значимыми;
- для `links[0..vertex_count-1]` подтверждена интерпретация как `(area_ref, edge_ref)` :
- `area_ref == -1 && edge_ref == -1` = нет соседа;
- иначе `area_ref` указывает на индекс ареала, `edge_ref` — на индекс р е б р а в целевом ареале;
- при редактировании безопасно работать через parser+writer этой формулы;
- неизвестные байты внутри записи должны сохраняться без изменений.
Дополнительно по runtime-поведению:
- `anchor_x/anchor_y` валидируются на попадание внутрь полигона; при промахе движок делает случайный re-seed позиции (см. §4.5);
- `logic_flag` по смещению `+32` используется как gating-условие в логике `SystemArealMap` .
### 4.4. Секция grid-ячеек в chunk 12
После массива ареалов идёт:
```c
uint32_t cellsX;
uint32_t cellsY;
for (x in 0..cellsX-1) {
for (y in 0..cellsY-1) {
uint16_t hitCount;
uint16_t areaIds[hitCount];
}
}
```
Runtime упаковывает метаданные ячейки в `uint32` :
- high 10 bits: `hitCount` (`value >> 22` );
- low 22 bits: `startIndex` (1-based индекс в общем `uint16` -пуле areaIds).
Контроль целостности:
- после разбора `ptr_end - chunk_begin` должен строго совпасть с `entry[+0x0C]` ;
- иначе `SystemArealMap panic: Incorrect ArealMap` .
### 4.5. Нормализация геометрии при загрузке
Если опорная точка ареала не попадает внутрь е г о полигона:
- до 100 попыток случайного сдвига в радиусе ~30;
- затем до 200 попыток в радиусе ~100.
Это runtime-correction; для 1:1-офлайн инструментов лучше генерировать валидные данные, чтобы не зависеть от недетерминизма `rand()` .
---
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
## 5. `BuildDat.lst` и объектные категории ареалов
`ArealMap.dll` инициализирует 12 категорий и читает `BuildDat.lst` .
Хардкод-категории (имя -> mask):
| Имя | Маска |
|---|---:|
| `Bunker_Small` | `0x80010000` |
| `Bunker_Medium` | `0x80020000` |
| `Bunker_Large` | `0x80040000` |
| `Generator` | `0x80000002` |
| `Mine` | `0x80000004` |
| `Storage` | `0x80000008` |
| `Plant` | `0x80000010` |
| `Hangar` | `0x80000040` |
| `MainTeleport` | `0x80000200` |
| `Institute` | `0x80000400` |
| `Tower_Medium` | `0x80100000` |
| `Tower_Large` | `0x80200000` |
Файл `BuildDat.lst` парсится секционно; при с б о е формата используется panic `BuildDat.lst is corrupted` .
---
## 6. Требования к toolchain (конвертер/ридер/редактор)
### 6.1. Общие принципы 1:1
1. Никаких "переупорядочиваний по вкусу": сохранять порядок chunk-ов, если не требуется явная нормализация.
2. В с е неизвестные поля сохранять побайтно.
3. При roundtrip обеспечивать byte-identical для неизмененных сущностей.
4. Валидации должны повторять runtime-ожидания (размеры, count-формулы, обязательность chunk-ов).
### 6.2. Для terrain `*.msh`
Обязательные проверки:
- наличие chunk types `1,2,3,4,5,11,18,21` ;
- type `14` опционален;
- для `type 2` : `size >= 0x8C` , `(size - 0x8C) % 68 == 0` , `attr1 == (size - 0x8C) / 68` ;
- `type21_size % 28 == 0` ;
- индексы `i0/i1/i2` в `TerrainFace28` не выходят за `vertex_count` (type 3);
- `slot.triStart + slot.triCount` не выходит за `face_count` .
Сериализация:
- `flags` , соседи, `edgeClass` , material байты в `TerrainFace28` сохранять как есть;
- содержимое `type 11` -derived cell-списков (`id` , `aux` ) сохранять без "починки";
- для packed normal не делать "улучшений" нормализации, если цель 1:1.
### 6.3. Для `*.map` (chunk 12)
Обязательные проверки:
- chunk type `12` существует;
- `areal_count > 0` ;
- `cellsX > 0 && cellsY > 0` ;
- `|normal_x,normal_y,normal_z| ~= 1` для каждого ареала;
- `links[0..vertex_count-1]` валидны (`-1/-1` или корректные `(area_ref, edge_ref)` );
- полный consumed-bytes строго равен `entry[+0x0C]` .
При редактировании:
- перестраивать только то, что действительно изменено;
- пересчитывать cell-списки и packed `cellMeta` синхронно;
- сохранять неизвестные части записи ареала без изменений.
### 6.4. Рекомендуемая архитектура редактора
1. `Parser` :
- NRes-слой;
- `TerrainMsh` -слой;
- `ArealMapChunk12` -слой.
2. `Model` :
- явные известные поля;
- `raw_unknown` для непросаженных блоков.
3. `Writer` :
- стабильная сериализация;
- проверка контрольных инвариантов перед записью.
4. `Verifier` :
- roundtrip hash/byte-compare;
- runtime-совместимые asserts.
---
## 7. Практический чеклист "движок 1:1"
Для runtime-совместимого движка нужно реализовать:
1. NRes API-уровень (`niOpenResFile` , `niOpenResInMem` , поиск chunk по type, получение data/attrs).
2. `CLandscape` пайплайн загрузки `*.msh` + менеджеров + `CreateSystemArealMap` .
3. Terrain face decode (28-byte запись), mask-фильтр, spatial grid queries.
4. Загрузчик `ArealMapGeometry` (chunk 12) с той же валидацией и packed-cell логикой.
5. Пост-обработку ареалов (пересвязка, корректировки опорных точек).
6. Поддержку `BuildDat.lst` для объектных категорий/схем.
---
## 8. Нерасшифрованные зоны (важно для редакторов)
Ниже поля, которые пока нельзя безопасно "пересобирать по смыслу":
- семантика `class_id` (`record + 40` ) на уровне геймдизайна/скриптов (числовое поле подтверждено, но человекочитаемая таблица соответствий не восстановлена полностью);
- ветки формата для `poly_count > 0` (в retail `tmp/gamedata` это всегда `0` , поэтому поведение этих веток подтверждено только по коду, без живых образцов);
- человекочитаемая семантика части битов `TerrainFace28.flags` (при этом remap и бинарные значения подтверждены);
- семантика поля `aux` во `8` -байтовом элементе cell-списка (`this+31588` , второй `uint32_t` ), которое в известных runtime-путях инициализируется нулем.
Правило до полного реверса: `preserve-as-is` .
---
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
## 9. Эмпирическая верификация (retail `tmp/gamedata`)
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
Для массовой проверки спецификации добавлен валидатор:
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
- `tools/terrain_map_doc_validator.py`
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
Запуск:
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
```bash
python3 tools/terrain_map_doc_validator.py \
--maps-root tmp/gamedata/DATA/MAPS \
--report-json tmp/terrain_map_doc_validator.report.json
```
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
Проверенные инварианты (на 33 картах, 2026-02-12):
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
- `Land.msh` :
- порядок chunk-ов всегда `[1,2,3,4,5,18,14,11,21]` ;
- `type11` первые dword всегда `[5767168, 4718593]` ;
- `type21` индексы вершин/соседей валидны;
- `type2` slot-таблица валидна по формуле `0x8C + 68*N` .
- `Land.map` :
- всегда один chunk `type 12` ;
- `cellsX == cellsY == 128` на всех картах;
- `poly_count == 0` для всех `34662` записей ареалов в retail-наборе;
- `record+12` , `record+36` , `record+44` всегда `0` ;
- `area_metric` (`record+16` ) стабильно коррелирует с площадью XY-полигона (макс. абсолютное отклонение `51.39` , макс. относительное `14.73%` , `18` кейсов > `5%` );
- `normal` в `record+20..28` всегда unit (диапазон длины `0.9999998758..1.0000001194` );
- link-таблицы `EdgeLink8` проходят строгую валидацию ссылочной целостности.
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
Сводный результат текущего набора данных:
2026-02-11 21:12:05 +00:00
2026-02-12 10:17:41 +00:00
- `issues_total = 0` , `errors_total = 0` , `warnings_total = 0` .