diff options
Diffstat (limited to 'docs/specs/terrain-map-loading.md')
| -rw-r--r-- | docs/specs/terrain-map-loading.md | 546 |
1 files changed, 164 insertions, 382 deletions
diff --git a/docs/specs/terrain-map-loading.md b/docs/specs/terrain-map-loading.md index 34f6249..62c1e0a 100644 --- a/docs/specs/terrain-map-loading.md +++ b/docs/specs/terrain-map-loading.md @@ -1,170 +1,111 @@ -# Terrain + map loading +# Terrain + ArealMap -Документ описывает полный runtime-пайплайн загрузки ландшафта и карты (`Terrain.dll` + `ArealMap.dll`) и требования к toolchain для 1:1 совместимости (чтение, конвертация, редактирование, обратная сборка). +Документ описывает подсистему ландшафта и ареалов мира в движке Parkan: Iron Strategy: -Источник реверса: +- `Land.msh` (terrain-геометрия и вспомогательные таблицы); +- `Land.map` (ареалы и навигационные связи); +- `BuildDat.lst` (категории объектных зон). -- `tmp/disassembler1/Terrain.dll.c` -- `tmp/disassembler1/ArealMap.dll.c` -- `tmp/disassembler2/Terrain.dll.asm` -- `tmp/disassembler2/ArealMap.dll.asm` +Описание дано в высокоуровневом переносимом виде, без ссылок на внутренние адреса и имена из дизассемблера. -Связанные спецификации: +Связанные страницы: -- [NRes / RsLi](nres.md) +- [NRes](nres.md) +- [RsLi](rsli.md) - [MSH core](msh-core.md) -- [ArealMap](arealmap.md) +- [Render pipeline](render.md) ---- +## 1. End-to-End загрузка уровня -## 1. Назначение подсистем +Для каждой карты движок загружает пару файлов: -### 1.1. `Terrain.dll` +- `.../Land.msh` +- `.../Land.map` -Отвечает за: +Высокоуровневый порядок: -- загрузку и хранение terrain-геометрии из `*.msh` (NRes); -- фильтрацию и выборку треугольников для коллизий/трассировки/рендера; -- рендер terrain-примитивов и связанного shading; -- использование микро-текстурного канала (chunk type 18). +1. Открыть `Land.msh` как `NRes`. +2. Прочитать обязательные terrain-chunk'и. +3. Построить runtime-структуры terrain (slots, faces, spatial grid). +4. Открыть `Land.map` как `NRes`. +5. Найти единственный chunk `type=12`. +6. Прочитать ареалы, их связи и cell-grid. +7. Применить инициализацию объектных категорий из `BuildDat.lst`. -Характерные runtime-строки: +## 2. Формат `Land.msh` -- `CLandscape::CLandscape()` -- `Unable to find microtexture mapping chunk` -- `Rendering empty primitive!` -- `Rendering empty primitive2!` +`Land.msh` — обычный `NRes` архив с фиксированным набором terrain-ресурсов. -### 1.2. `ArealMap.dll` +## 2.1. Состав chunk'ов -Отвечает за: +Обязательные типы: -- загрузку геометрии ареалов из `*.map` (NRes, chunk type 12); -- построение связей "ареал <-> соседи/подграфы"; -- grid-ускорение по ячейкам карты; -- runtime-доступ к `ISystemArealMap` (интерфейс id `770`) и ареалам (id `771`). +- `1`, `2`, `3`, `4`, `5`, `11`, `18`, `21` -Характерные runtime-строки: +Опциональные типы: -- `SystemArealMap panic: Cannot load ArealMapGeometry` -- `SystemArealMap panic: Cannot find chunk in resource` -- `SystemArealMap panic: ArealMap Cells are empty` -- `SystemArealMap panic: Incorrect ArealMap` +- `14` ---- +Наблюдаемый retail-порядок chunk'ов: -## 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()`: +```text +[1, 2, 3, 4, 5, 18, 14, 11, 21] +``` -| Порядок | 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) | +## 2.2. Stride и атрибуты -Ключевые проверки: +| Type | Назначение | Stride | +|---:|---|---:| +| 1 | node/slot матрица | 38 | +| 3 | позиции вершин | 12 | +| 4 | нормали (packed) | 4 | +| 5 | UV (packed) | 4 | +| 11 | cell-ускоритель | 4 | +| 14 | доп. поток | 4 | +| 18 | доп. поток | 4 | +| 21 | terrain face | 28 | -- отсутствие type `18` вызывает `Unable to find microtexture mapping chunk`; -- отсутствие остальных обязательных чанков вызывает `Unable to open file`. +Общее правило для этих chunk'ов: -### 3.2. Node/slot структура для terrain +- `attr1 == size / stride` +- `attr3 == stride` -Terrain-код использует те же stride и адресацию, что и core-описание: +## 2.3. Type `2`: slot table -- node-запись: `38` байт; -- slot-запись: `68` байт; -- доступ к первому slot-index: `node + 8`; -- tri-диапазон в slot: `slot + 140` (offset 0 внутри slot), `slot + 142` (offset 2). +`type=2` содержит: -Это согласуется с [MSH core](msh-core.md) для `Res1/Res2`: +- заголовок `0x8C` байт; +- затем таблицу slots по `68` байт. -- `Res1`: `uint16[19]` на node; -- `Res2`: header + slot table (`0x8C + N * 0x44`). +Инварианты: -### 3.3. Terrain face record (type 21, 28 bytes) +- `size >= 0x8C` +- `(size - 0x8C) % 68 == 0` +- `attr1 == (size - 0x8C) / 68` +- `attr3 == 68` -Подтвержденные поля из runtime-декодирования face: +## 2.4. Type `21`: terrain face (28 байт) -```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 -}; -``` +Высокоуровневая структура face: -`edgeClass` декодируется как: +- флаги face; +- индексы треугольника (`i0, i1, i2`); +- индексы соседей (`n0, n1, n2`, значение `0xFFFF` = нет соседа); +- служебные поля (материал/класс/edge-поля и др.). -- `edge0 = byte26 & 0x3` -- `edge1 = (byte26 >> 2) & 0x3` -- `edge2 = (byte26 >> 4) & 0x3` +Критичные проверки: -### 3.4. Маски флагов face +- `i0/i1/i2 < vertex_count` (`type=3`); +- `nX == 0xFFFF` или `nX < face_count`. -Во многих запросах применяется фильтр: +## 2.5. Маски face и compact-представления -```c -(faceFlags & requiredMask) == requiredMask && -(faceFlags | ~forbiddenMask) == ~forbiddenMask -``` +В рантайме используются: -Эквивалентно: "все required-биты выставлены, forbidden-биты отсутствуют". +- полная 32-битная маска (`full`); +- компактные представления (`compactMain16`, `compactMaterial6`). -Подтверждено активное использование битов: - -- `0x8` (особая обработка в трассировке) -- `0x2000` -- `0x20000` -- `0x100000` -- `0x200000` - -Кроме "полной" 32-бит маски, runtime использует компактные маски в API-запросах. - -Подтверждённый remap `full -> compactMain16` (функции `sub_10013FC0`, `sub_1004BA00`, `sub_1004BB40`): +Подтвержденный remap `full -> compactMain16`: | Full bit | Compact bit | |---:|---:| @@ -184,7 +125,7 @@ struct TerrainFace28 { | `0x00000040` | `0x2000` | | `0x00200000` | `0x8000` | -Подтверждённый remap `full -> compactMaterial6` (функции `sub_10014090`, `sub_10015540`, `sub_1004BB40`): +Подтвержденный remap `full -> compactMaterial6`: | Full bit | Compact bit | |---:|---:| @@ -195,180 +136,99 @@ struct TerrainFace28 { | `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]`. +Для 1:1 реализации нужно поддерживать оба представления и обратное восстановление `compact -> full`. -Для toolchain это означает: +## 2.6. Type `11` и cell-ускоритель terrain -- если редактируется только бинарник `type 21`, достаточно сохранять `flags` как есть; -- если реализуется API-совместимый runtime-слой, нужно поддерживать оба представления (`full` и `compact`) и точный remap выше. +`type=11` служит источником cell-ускорителя для terrain-запросов. -### 3.5. Grid-ускоритель terrain-запросов +Практические требования для editor/toolchain: -Runtime строит grid descriptor с параметрами: +- не переупорядочивать содержимое без полного пересчета зависимых таблиц; +- сохранять служебные/неизвестные поля побайтно; +- выполнять валидацию диапазонов face/slot после любых правок. -- origin (`baseX/baseY`); -- масштабные коэффициенты (`invSizeX/invSizeY`); -- размеры сетки (`cellsX`, `cellsY`). +## 3. Формат `Land.map` (chunk `type=12`) -Дальше запросы: +`Land.map` — `NRes`, содержащий ровно один ресурс `type=12`. -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-массивы) +- `entry.attr1` = `areal_count`; +- payload включает: + - `areal_count` переменных записей ареалов; + - затем grid-секцию cell-попаданий. -В `CLandscape` после инициализации используются три параллельных массива по ячейкам (`cellsX * cellsY`): +## 3.1. Запись ареала -- `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 -}; +float anchor_x; // +0 +float anchor_y; // +4 +float anchor_z; // +8 +float reserved_12; // +12 +float area_metric; // +16 +float normal_x; // +20 +float normal_y; // +24 +float normal_z; // +28 +uint32_t logic_flag; // +32 +uint32_t reserved_36; // +36 +uint32_t class_id; // +40 +uint32_t reserved_44; // +44 +uint32_t vertex_count; // +48 +uint32_t poly_count; // +52 ``` -Подтвержденные 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-структур. - ---- - -## 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: ...`. +1. `float3 vertices[vertex_count]` +2. `EdgeLink8 links[vertex_count + 3 * poly_count]`, где + `EdgeLink8 = { int32 area_ref; int32 edge_ref; }` +3. для каждого полигона block: + - `uint32 n` + - `4 * (3*n + 1)` байт данных полигона -### 4.2. Верхний уровень chunk 12 +## 3.2. Семантика edge-link -Используются: +Для `links[0 .. vertex_count-1]`: -- `entry.attr1` (из каталога NRes) как `areal_count`; -- `entry[+0x0C]` как размер payload chunk для контроля полного разбора. +- `(-1, -1)` означает «соседа нет»; +- иначе `area_ref` указывает на индекс соседнего ареала, `edge_ref` — на ребро в соседнем ареале. -Данные chunk: +## 3.3. Grid-секция после ареалов -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]; +uint32 cellsX; +uint32 cellsY; +for (x=0; x<cellsX; x++) { + for (y=0; y<cellsY; y++) { + uint16 hitCount; + uint16 areaIds[hitCount]; } } ``` -Runtime упаковывает метаданные ячейки в `uint32`: - -- high 10 bits: `hitCount` (`value >> 22`); -- low 22 bits: `startIndex` (1-based индекс в общем `uint16`-пуле areaIds). - -Контроль целостности: +В runtime существует упакованное cell-meta представление: -- после разбора `ptr_end - chunk_begin` должен строго совпасть с `entry[+0x0C]`; -- иначе `SystemArealMap panic: Incorrect ArealMap`. +- high 10 бит: `hitCount`; +- low 22 бита: `startIndex` (в общем `areaIds` пуле). -### 4.5. Нормализация геометрии при загрузке +## 3.4. Валидация целостности chunk 12 -Если опорная точка ареала не попадает внутрь его полигона: - -- до 100 попыток случайного сдвига в радиусе ~30; -- затем до 200 попыток в радиусе ~100. - -Это runtime-correction; для 1:1-офлайн инструментов лучше генерировать валидные данные, чтобы не зависеть от недетерминизма `rand()`. - ---- +Обязательные проверки: -## 5. `BuildDat.lst` и объектные категории ареалов +- `areal_count > 0`; +- `cellsX > 0 && cellsY > 0`; +- каждый `area_id` из cell-списков `< areal_count`; +- все `area_ref/edge_ref` валидны относительно целевых ареалов; +- полный объем прочитанных байт должен точно совпасть с размером payload. -`ArealMap.dll` инициализирует 12 категорий и читает `BuildDat.lst`. +## 4. `BuildDat.lst` -Хардкод-категории (имя -> mask): +Используются 12 объектных категорий ареалов: | Имя | Маска | |---|---:| @@ -385,127 +245,49 @@ Runtime упаковывает метаданные ячейки в `uint32`: | `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" +## 5. Требования к reader/writer/editor -Для runtime-совместимого движка нужно реализовать: +1. Сохранять порядок и бинарную форму chunk'ов, если не выполняется осознанная нормализация. +2. Все неизвестные поля хранить и писать побайтно (`preserve-as-is`). +3. После правок пересчитывать только вычислимые поля, не «чистить» opaque-данные. +4. Проверять диапазоны индексов между связанными таблицами (`nodes/slots/faces/vertices/areas/cells`). +5. Для неизмененных ресурсов обеспечивать byte-identical roundtrip. -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` для объектных категорий/схем. +## 6. Эмпирическая верификация (retail) ---- +Валидация на `testdata/Parkan - Iron Strategy`: -## 8. Нерасшифрованные зоны (важно для редакторов) +- карт: `33` +- `Land.msh`: `33/33` валидны +- `Land.map`: `33/33` валидны +- `issues_total = 0`, `errors_total = 0`, `warnings_total = 0` -Ниже поля, которые пока нельзя безопасно "пересобирать по смыслу": +Подтвержденные наблюдения: -- семантика `class_id` (`record + 40`) на уровне геймдизайна/скриптов (числовое поле подтверждено, но человекочитаемая таблица соответствий не восстановлена полностью); -- ветки формата для `poly_count > 0` (в retail `tmp/gamedata` это всегда `0`, поэтому поведение этих веток подтверждено только по коду, без живых образцов); -- человекочитаемая семантика части битов `TerrainFace28.flags` (при этом remap и бинарные значения подтверждены); -- семантика поля `aux` во `8`-байтовом элементе cell-списка (`this+31588`, второй `uint32_t`), которое в известных runtime-путях инициализируется нулем. +- `Land.msh` порядок chunk'ов стабилен: `[1,2,3,4,5,18,14,11,21]`; +- `Land.map` всегда содержит один chunk `type=12`; +- `cellsX == cellsY == 128` во всех retail-картах; +- `poly_count == 0` во всем проверенном retail-корпусе; +- `normal` имеет длину ~1.0; +- `reserved_12`, `reserved_36`, `reserved_44` в retail наблюдаются как `0`. -Правило до полного реверса: `preserve-as-is`. +Инструмент: ---- - -## 9. Эмпирическая верификация (retail `tmp/gamedata`) +- `tools/terrain_map_doc_validator.py` -Для массовой проверки спецификации добавлен валидатор: +## 7. Статус покрытия и что осталось до 100% -- `tools/terrain_map_doc_validator.py` +Закрыто: -Запуск: +- бинарный контракт `Land.msh` и `Land.map`; +- диапазонные и структурные инварианты; +- remap масок `full/compact`; +- валидация на полном retail-корпусе карт. -```bash -python3 tools/terrain_map_doc_validator.py \ - --maps-root tmp/gamedata/DATA/MAPS \ - --report-json tmp/terrain_map_doc_validator.report.json -``` +Осталось до полного 100% архитектурного покрытия движка: -Проверенные инварианты (на 33 картах, 2026-02-12): - -- `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` проходят строгую валидацию ссылочной целостности. - -Сводный результат текущего набора данных: - -- `issues_total = 0`, `errors_total = 0`, `warnings_total = 0`. +1. Полная доменная семантика `class_id` и `logic_flag` (игровые значения/поведенческие правила). +2. Полная спецификация ветки `poly_count > 0` на живых данных (в retail не встречена). +3. Полная field-level семантика части битов `TerrainFace28.flags` (бинарный контракт и remap закрыты, но не все биты имеют документированные геймплейные имена). |
