aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/materials-texm.md
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-02-12 00:50:33 +0300
committerValentin Popov <valentin@popov.link>2026-02-12 00:50:33 +0300
commit70ed6480c2b2b2ecab4956216c1e8e85b0938b4c (patch)
tree701eba2b9d08c50f6e8392f69925bbe9cb7b8644 /docs/specs/materials-texm.md
parent662b292b5b47d0f7df3b19808db746bbc2ecc48c (diff)
downloadfparkan-70ed6480c2b2b2ecab4956216c1e8e85b0938b4c.tar.xz
fparkan-70ed6480c2b2b2ecab4956216c1e8e85b0938b4c.zip
Refactor materials and Texm documentation for clarity and completeness
- Updated the structure and content of the materials and Texm documentation to provide a comprehensive overview of the material subsystem in the engine. - Enhanced sections on identifiers, architecture, material layout, and runtime storage. - Improved explanations of material attributes, animation modes, and parsing behavior. - Added detailed specifications for toolchain interactions, including lossless write rules and validation recommendations. - Included pseudocode examples for parsing MAT0 and Texm formats to aid in understanding.
Diffstat (limited to 'docs/specs/materials-texm.md')
-rw-r--r--docs/specs/materials-texm.md784
1 files changed, 601 insertions, 183 deletions
diff --git a/docs/specs/materials-texm.md b/docs/specs/materials-texm.md
index 4c8c8f4..f386265 100644
--- a/docs/specs/materials-texm.md
+++ b/docs/specs/materials-texm.md
@@ -1,299 +1,717 @@
-# Materials + Texm
+# Materials, WEAR, MAT0 и Texm
-Документ описывает материалы, текстуры, палитры, блоки `WEAR` / `LIGHTMAPS` и формат `Texm`.
+Документ описывает материальную подсистему движка (World3D/Ngi32) на уровне, достаточном для:
+
+- реализации runtime 1:1;
+- создания инструментов чтения/валидации;
+- создания инструментов конвертации и редактирования с lossless round-trip.
+
+Источник: дизассемблированные `tmp/disassembler1/*.c` и `tmp/disassembler2/*.asm`, плюс проверка на `tmp/gamedata`.
+
+---
+
+## 1. Идентификаторы и сущности
+
+| Сущность | ID (LE uint32) | ASCII | Где используется |
+|---|---:|---|---|
+| Material resource | `0x3054414D` | `MAT0` | `Material.lib` |
+| Wear resource | `0x52414557` | `WEAR` | `.wea` записи в world/mission `.rlb` |
+| Texture resource | `0x6D786554` | `Texm` | `Textures.lib`, `lightmap.lib`, другие `.lib/.rlb` |
+| Atlas tail chunk | `0x65676150` | `Page` | хвост payload `Texm` |
+
+Дополнительно: палитры загружаются отдельным путём (через `SetPalettesLib` + `sub_10002B40`) и не являются `Texm`.
---
-## 2.1. Архитектура материальной системы
+## 2. Архитектура подсистемы
-Материальная подсистема реализована в `World3D.dll` и включает:
+### 2.1 Экспортируемые точки входа (World3D)
-- **Менеджер материалов** (`LoadMatManager`) — объект размером 0x470 байт (1136), хранящий до 140 таблиц материалов (поле `+572`, `this[143]`).
-- **Библиотека палитр** (`SetPalettesLib`) — NRes‑архив с палитрами.
-- **Библиотека текстур** (`SetTexturesLib`) — путь к файлу/каталогу текстур.
-- **Библиотека материалов** (`SetMaterialLib`) — NRes‑архив с данными материалов.
-- **Библиотека lightmap'ов** (`SetLightMapLib`) — опциональная.
+- `LoadMatManager`
+- `SetPalettesLib`
+- `SetTexturesLib`
+- `SetMaterialLib`
+- `SetLightMapLib`
+- `SetGameTime`
+- `UnloadAllTextures`
+
+`Set*Lib` просто копируют строки путей в глобальные буферы; валидации пути нет.
+
+### 2.2 Дефолтные библиотеки (из `iron3d.dll`)
+
+- `Textures.lib`
+- `Material.lib`
+- `LightMap.lib`
+- `palettes.lib` (строка собирается как `'p' + "alettes.lib"`)
+
+### 2.3 Ключевые runtime-хранилища
+
+1. Менеджер материалов (`LoadMatManager`) — объект `0x470` байт.
+2. Кэш текстурных объектов.
+3. Кэш lightmap-объектов.
+4. Банк загруженных палитр.
+5. Глобальный пул определений материалов (`MAT0`).
+
+---
-### Загрузка палитр (sub_10002B40)
+## 3. Layout `MatManager` (0x470)
-Палитры загружаются из NRes‑архива по именам. Система перебирает буквы `'A'`..'Z'` (26 категорий) × 11 суффиксов, формируя имена вида `"A<suffix>.pal"`. Каждая палитра загружается через `niOpenResFile` → `niReadData` и регистрируется как текстурный объект в графическом движке.
+Объект содержит 70 таблиц wear/lightmaps (не 140).
-Максимальное количество палитр: 26 × 11 = **286**.
+```c
+// int-индексы относительно this (DWORD*), размер 284 DWORD = 0x470
+// [0] vtable
+// [1] callback iface
+// [2] callback data
+// [3..72] wearTablePtrs[70] // ptr на массив по 8 байт
+// [73..142] wearCounts[70]
+// [143] tableCount
+// [144..213] lightmapTablePtrs[70] // ptr на массив по 4 байта
+// [214..283] lightmapCounts[70]
+```
-## 2.2. Запись материала (76 байт)
+### 3.1 Vtable методов (`off_100209E4`)
-Материал представлен записью размером **76 байт** (19 DWORD). Поля восстановлены из функции интерполяции `sub_10003030` и функций `sub_100031F0` / `sub_10003680`.
+| Индекс | Функция | Назначение |
+|---:|---|---|
+| 0 | `loc_10002CE0` | служебный/RTTI-заглушка |
+| 1 | `sub_10002D10` | деструктор + освобождение таблиц |
+| 2 | `PreLoadAllTextures` | экспорт, но фактически `retn 4` (заглушка) |
+| 3 | `sub_100031F0` | получить материал-фазу по `gameTime` |
+| 4 | `sub_10003AE0` | сбросить startTime записи wear к `SetGameTime()` |
+| 5 | `sub_10003680` | получить материал-фазу по нормализованному `t` |
+| 6 | `sub_10003B10` | загрузить wear/lightmaps (файл/ресурс) |
+| 7 | `sub_10003F80` | загрузить wear/lightmaps из буфера |
+| 8 | `sub_100031A0` | получить указатель на lightmap texture object |
+| 9 | `sub_10003AB0` | получить runtime-метаданные материала |
+| 10 | `sub_100031D0` | получить `wearCount` для таблицы |
-| Смещение | Размер | Тип | Интерполяция | Описание |
-|----------|--------|--------|--------------|--------------------------------------|
-| 0 | 4 | uint32 | Нет | `flags` — тип/режим материала |
-| 4 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — R |
-| 8 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — G |
-| 12 | 4 | float | Бит 1 (0x02) | Цветовой компонент A — B |
-| 16 | 4 | — | Нет | Зарезервировано / паддинг |
-| 20 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — R |
-| 24 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — G |
-| 28 | 4 | float | Бит 0 (0x01) | Цветовой компонент B — B |
-| 32 | 4 | float | Бит 4 (0x10) | Скалярный параметр (power / opacity) |
-| 36 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — R |
-| 40 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — G |
-| 44 | 4 | float | Бит 2 (0x04) | Цветовой компонент C — B |
-| 48 | 4 | — | Нет | Зарезервировано / паддинг |
-| 52 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — R |
-| 56 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — G |
-| 60 | 4 | float | Бит 3 (0x08) | Цветовой компонент D — B |
-| 64 | 4 | — | Нет | Зарезервировано / паддинг |
-| 68 | 4 | int32 | Нет | `textureIndex` — индекс текстуры |
-| 72 | 4 | int32 | Нет | Дополнительный параметр |
+### 3.2 Кодирование material-handle
-### Маппинг компонентов на D3D Material (предположительный)
+`uint32 handle = (tableIndex << 16) | wearIndex`.
-По аналогии со стандартной структурой `D3DMATERIAL7`:
+- `HIWORD(handle)` -> индекс таблицы `0..69`
+- `LOWORD(handle)` -> индекс материала в wear-таблице
-| Компонент | Вероятное назначение | Биты интерполяции |
-|--------------|----------------------|-------------------|
-| A (+4..+12) | Diffuse (RGB) | 0x02 |
-| B (+20..+28) | Ambient (RGB) | 0x01 |
-| C (+36..+44) | Specular (RGB) | 0x04 |
-| D (+52..+60) | Emissive (RGB) | 0x08 |
-| (+32) | Specular power | 0x10 |
+---
-### Поле textureIndex (+68)
+## 4. Глобальные кэши и их ёмкость
-- Значение `< 0` означает «нет текстуры» → `texture_ptr = NULL`.
-- Значение `≥ 0` используется как индекс в глобальном массиве текстурных объектов: `texture = texture_array[5 * textureIndex]`.
+Ёмкости подтверждены границами циклов/адресов в дизассемблере.
-## 2.3. Алгоритм интерполяции материалов
+### 4.1 Кэш текстур (`dword_1014E910`...)
-Движок поддерживает **анимацию материалов** между ключевыми кадрами. Функция `sub_10003030`:
+- Размер слота: `5 DWORD` (20 байт)
+- Ёмкость: `777`
+```c
+struct TextureSlot {
+ int32_t resIndex; // +0 индекс записи в NRes (не hash), -1 = свободно
+ void* textureObject; // +4
+ int32_t refCount; // +8
+ uint32_t lastZeroRefTime;// +12 время, когда refCount стал 0
+ uint32_t loadFlags; // +16 флаги загрузки
+};
```
-Вход: mat_a (исходный), mat_b (целевой), t (фактор 0..1), mask (битовая маска)
-Выход: mat_result
+### 4.2 Кэш lightmaps (`dword_10029C98`...)
+
+- Тот же layout `5 DWORD`
+- Ёмкость: `100`
+
+### 4.3 Пул материалов (`dword_100669F0`...)
+
+- Шаг: `92 DWORD` (`368` байт)
+- Ёмкость: `700`
+
+Фиксированные поля на шаг `i*92`:
+
+| DWORD offset | Byte offset | Поле |
+|---:|---:|---|
+| 0 | 0 | `nameResIndex` (`MAT0` entry index), `-1` = free |
+| 1 | 4 | `refCount` |
+| 2 | 8 | `phaseCount` |
+| 3 | 12 | `phaseArrayPtr` (`phaseCount * 76`) |
+| 4 | 16 | `animBlockCount` (`< 20`) |
+| 5..84 | 20..339 | `animBlocks[20]` по 16 байт |
+| 85 | 340 | metaA (`dword_10066B44`) |
+| 86 | 344 | metaB (`dword_10066B48`) |
+| 87 | 348 | metaC (`dword_10066B4C`) |
+| 88 | 352 | metaD (`dword_10066B50`) |
+| 89 | 356 | flagA (`dword_10066B54`) |
+| 90 | 360 | nibbleMode (`dword_10066B58`) |
+| 91 | 364 | flagB (`dword_10066B5C`) |
+
+### 4.4 Банк палитр
+
+- `dword_1013DA58[]`
+- Загружается до `286` элементов (26 букв * 11 вариантов)
+
+---
-Для каждого бита mask:
- если бит установлен:
- mat_result.component = mat_a.component + (mat_b.component - mat_a.component) × t
- иначе:
- mat_result.component = mat_a.component (без интерполяции)
+## 5. Загрузка палитр (`sub_10002B40`)
-mat_result.textureIndex = mat_a.textureIndex (всегда копируется без интерполяции)
+### 5.1 Генерация имён
+
+Движок перебирает:
+
+- буквы `'A'..'Z'`
+- суффиксы: `""`, `"0"`, `"1"`, ..., `"9"`
+
+И формирует имя:
+
+- `<Letter><Suffix>.PAL`
+- примеры: `A.PAL`, `A0.PAL`, ..., `Z9.PAL`
+
+### 5.2 Индекс палитры
+
+`paletteIndex = letterIndex * 11 + variantIndex`
+
+- `letterIndex = 0..25`
+- `variantIndex = 0..10` (`""`=0, `"0"`=1, ..., `"9"`=10)
+
+### 5.3 Поведение
+
+- Если запись не найдена: `paletteSlots[idx] = 0`
+- Если найдена: payload отдаётся в рендер (`render->method+60`)
+
+---
+
+## 6. Формат `MAT0` (`Material.lib`)
+
+### 6.1 Атрибуты NRes entry
+
+`sub_10004310` использует:
+
+- `entry.type` = `MAT0`
+- `entry.attr1` (bitfield runtime-флагов)
+- `entry.attr2` (версия/вариант заголовка payload)
+- `entry.attr3` не используется в runtime-парсере
+
+Маппинг `attr1`:
+
+- bit0 (`0x01`) -> добавить флаг `0x200000` в загрузку текстур фазы
+- bit1 (`0x02`) -> `flagA=1`; при некоторых HW-условиях дополнительно OR `0x80000`
+- bits2..5 -> `nibbleMode = (attr1 >> 2) & 0xF`
+- bit6 (`0x40`) -> `flagB=1`
+
+### 6.2 Payload layout
+
+```c
+struct Mat0Payload {
+ uint16_t phaseCount;
+ uint16_t animBlockCount; // должно быть < 20, иначе "Too many animations for material."
+
+ // Если attr2 >= 2:
+ uint8_t metaA8;
+ uint8_t metaB8;
+ // Если attr2 >= 3:
+ uint32_t metaC32;
+ // Если attr2 >= 4:
+ uint32_t metaD32;
+
+ PhaseRecordByte34 phases[phaseCount];
+ AnimBlockRaw anim[animBlockCount];
+};
```
-### Режимы анимации материалов
+Если `attr2 < 2`, runtime-значения по умолчанию:
-Материал может иметь несколько фаз (phase) с разными режимами цикличности:
+- `metaA = 255`
+- `metaB = 255`
+- `metaC = 1.0f` (`0x3F800000`)
+- `metaD = 0`
-| Режим (flags & 7) | Описание |
-|-------------------|-------------------------------------|
-| 0 | Цикл: повтор с начала |
-| 1 | Ping‑pong: туда‑обратно |
-| 2 | Однократное воспроизведение (clamp) |
-| 3 | Случайный кадр (random) |
+### 6.3 `PhaseRecordByte34` -> runtime `76 bytes`
-## 2.4. Глобальный массив текстур
+Сырые 34 байта:
-Текстуры хранятся в глобальном массиве записей по **20 байт** (5 DWORD):
+```c
+struct PhaseRecordByte34 {
+ uint8_t p[18]; // параметры
+ char textureName[16];// если textureName[0]==0, текстуры нет
+};
+```
+
+Преобразование в runtime-структуру (точный порядок):
+
+| Из `p[i]` | В offset runtime | Преобразование |
+|---:|---:|---|
+| `p[0]` | `+16` | `p[0] / 255.0f` |
+| `p[1]` | `+20` | `p[1] / 255.0f` |
+| `p[2]` | `+24` | `p[2] / 255.0f` |
+| `p[3]` | `+28` | `p[3] * 0.01f` |
+| `p[4]` | `+0` | `p[4] / 255.0f` |
+| `p[5]` | `+4` | `p[5] / 255.0f` |
+| `p[6]` | `+8` | `p[6] / 255.0f` |
+| `p[7]` | `+12` | `p[7] / 255.0f` |
+| `p[8]` | `+32` | `p[8] / 255.0f` |
+| `p[9]` | `+36` | `p[9] / 255.0f` |
+| `p[10]` | `+40` | `p[10] / 255.0f` |
+| `p[11]` | `+44` | `p[11] / 255.0f` |
+| `p[12]` | `+48` | `p[12] / 255.0f` |
+| `p[13]` | `+52` | `p[13] / 255.0f` |
+| `p[14]` | `+56` | `p[14] / 255.0f` |
+| `p[15]` | `+60` | `p[15] / 255.0f` |
+| `p[16]` | `+64` | `uint32 = p[16]` |
+| `p[17]` | `+72` | `int32 = p[17]` |
+
+Текстура:
+
+- `textureName[0] == 0` -> `runtime[+68] = -1` и `runtime[+72] = -1`
+- иначе `runtime[+68] = LoadTexture(textureName, flags)`
+
+### 6.4 Runtime-запись фазы (76 байт)
```c
-struct TextureSlot { // 20 байт
- int32_t name_hash; // +0: Хэш/ID имени текстуры (-1 = свободен)
- void* texture_object; // +4: Указатель на объект текстуры D3D
- int32_t ref_count; // +8: Счётчик ссылок
- uint32_t last_release; // +12: Время последнего Release
- uint32_t extra; // +16: Дополнительный флаг
+struct MaterialPhase76 {
+ float f0; // +0
+ float f1; // +4
+ float f2; // +8
+ float f3; // +12
+ float f4; // +16
+ float f5; // +20
+ float f6; // +24
+ float f7; // +28
+ float f8; // +32
+ float f9; // +36
+ float f10; // +40
+ float f11; // +44
+ float f12; // +48
+ float f13; // +52
+ float f14; // +56
+ float f15; // +60
+ uint32_t u16; // +64
+ int32_t texSlot; // +68 (индекс в texture cache, либо -1)
+ int32_t i18; // +72
};
```
-Функция `UnloadAllTextures` обнуляет все слоты, вызывая деструктор для каждого ненулевого `texture_object`.
+### 6.5 Анимационные блоки (`animBlockCount`, максимум 19)
+
+Каждый блок в payload:
-## 2.5. Глобальный массив определений материалов
+```c
+struct AnimBlockRaw {
+ uint32_t headerRaw; // mode = headerRaw & 7; interpMask = headerRaw >> 3
+ uint16_t keyCount;
+ struct KeyRaw {
+ uint16_t k0;
+ uint16_t k1;
+ uint16_t k2;
+ } keys[keyCount];
+};
+```
-Определения материалов хранятся в глобальном массиве записей по **368 байт** (92 DWORD):
+Runtime-представление блока = 16 байт:
```c
-struct MaterialDef { // 368 байт (92 DWORD)
- int32_t name_hash; // dword_100669F0[92*i]: -1 = свободен
- int32_t ref_count; // dword_100669F4[92*i]: Счётчик ссылок
- int32_t phase_count; // dword_100669F8[92*i]: Число текстурных фаз
- void* record_ptr; // dword_100669FC[92*i]: Указатель на массив записей по 76 байт
- int32_t anim_phase_count; // dword_10066A00[92*i]: Число фаз анимации
- // +20..+367: данные фаз анимации (до 22 фаз × 16 байт)
+struct AnimBlockRuntime {
+ uint32_t mode; // headerRaw & 7
+ uint32_t interpMask;// headerRaw >> 3
+ int32_t keyCount;
+ void* keysPtr; // массив keyCount * 8
};
```
-## 2.6. Переключатели рендера (из Ngi32.dll)
+Ключи в runtime занимают 8 байт/ключ (с расширением `k0` до `uint32`).
+
+`k2` в `sub_100031F0/sub_10003680` не используется.
+
+### 6.6 Поиск и fallback
+
+При `LoadMaterial(name)`:
+
+- сначала точный поиск в `Material.lib`;
+- при промахе лог: `"Material %s not found."`;
+- fallback на `DEFAULT`;
+- если и `DEFAULT` не найден, берётся индекс `0`.
+
+---
+
+## 7. Выбор текущей material-фазы
+
+### 7.1 Интерполяция (`sub_10003030`)
+
+Интерполируются только следующие поля (по `interpMask`):
+
+- bit `0x02`: `+4,+8,+12`
+- bit `0x01`: `+20,+24,+28`
+- bit `0x04`: `+36,+40,+44`
+- bit `0x08`: `+52,+56,+60`
+- bit `0x10`: `+32`
+
+Не интерполируются и копируются из «текущей» фазы:
+
+- `+0,+16,+48,+64,+68,+72`
-Движок читает настройки из реестра Windows (`HKCU\Software\Nikita\NgiTool`). Подтверждённые ключи:
+### 7.2 Выбор по времени (`sub_100031F0`)
-| Ключ реестра | Глобальная переменная | Описание |
-|--------------------------|-----------------------|---------------------------------|
-| `Disable MultiTexturing` | `dword_1003A184` | Отключить мультитекстурирование |
-| `DisableMipmap` | `dword_1003A174` | Отключить мипмап‑фильтрацию |
-| `Force 16-bit textures` | `dword_1003A180` | Принудительно 16‑бит текстуры |
-| `UseFirstCard` | `dword_100340EC` | Использовать первую видеокарту |
-| `DisableD3DCalls` | `dword_1003A178` | Отключить вызовы D3D (отладка) |
-| `DisableDSound` | `dword_1003A17C` | Отключить DirectSound |
-| `ForceCpu` | (комбинированный) | Режим рендера: SW/HW TnL/Mixed |
+Вход:
-### Значения ForceCpu и их влияние на рендер
+- `handle` (`tableIndex|wearIndex`)
+- `animBlockIndex`
+- глобальное время `SetGameTime()` (`dword_10032A38`)
-| ForceCpu | Force SSE | Force 3DNow | Force FXCH | Force MMX |
-|----------|-----------|-------------|------------|-----------|
-| 2 | Да | Нет | Нет | Нет |
-| 3 | Нет | Да | Нет | Нет |
-| 4 | Да | Да | Нет | Нет |
-| 5 | Да | Да | Да | Да |
-| 6 | Да | Да | Да | Нет |
-| 7 | Нет | Нет | Нет | Да |
+Для каждой wear-записи хранится `startTime` (второй DWORD пары `8-byte`).
-### Практические выводы для порта
+Режимы `mode = headerRaw & 7`:
-Движок спроектирован для работы **без** следующих функций (graceful degradation):
+- `0`: loop
+- `1`: ping-pong
+- `2`: one-shot clamp
+- `3`: random (`rand() % cycleLength`)
-- Мипмапы.
-- Bilinear/trilinear фильтрация.
-- Мультитекстурирование (2‑й текстурный слой).
-- 32‑битные текстуры (fallback на 16‑бит).
-- Аппаратный T&L (software fallback).
+После выбора сегмента интерполяции `sub_10003030` строит scratch-материал (`unk_1013B300`), который возвращается через out-параметр.
+
+### 7.3 Выбор по нормализованному `t` (`sub_10003680`)
+
+Аналогично `sub_100031F0`, но time берётся как `t * cycleLength`.
+
+### 7.4 Сброс времени записи
+
+`sub_10003AE0` обновляет `startTime` конкретной wear-записи значением текущего `SetGameTime()`.
---
-## 2.7. Текстовый файл WEAR + LIGHTMAPS (World3D.dll)
+## 8. Формат `WEAR` (текст)
+
+`WEAR` хранится как текст в NRes entry типа `WEAR` (`0x52414557`), обычно имя `*.wea`.
+
+### 8.1 Грамматика
+
+```text
+<wearCount:int>\n
+<legacyId:int> <materialName>\n // повторить wearCount раз
-`World3D.dll` содержит парсер текстового файла (режим `rt`), который задаёт:
+[LIGHTMAPS\n
+<lightmapCount:int>\n
+<legacyId:int> <lightmapName>\n // повторить lightmapCount раз]
+```
+
+- `<legacyId>` читается, но как ключ не используется.
+- Идентификатором реально является имя (`materialName` / `lightmapName`).
-- список **материалов (wear)**, используемых в сцене/объекте;
-- список **лайтмап (lightmaps)**.
+### 8.2 Парсеры
-Формат читается через `fgets`/`sscanf`/`fscanf`, поэтому он чувствителен к структуре строк и ключевому слову `LIGHTMAPS`.
+1. `sub_10003B10`: файл/ресурсный режим.
+2. `sub_10003F80`: парсер из строкового буфера.
-### 2.7.1. Блок WEAR (материалы)
+### 8.3 Поведение и ошибки
-1) **Первая строка файла** — целое число:
+- `wearCount <= 0` (в текстовом файловом режиме) -> `"Illegal wear length."`
+- при невозможности открыть wear-файл/entry -> `"Wear <%s> doesn't exist."`
+- если найден блок `LIGHTMAPS` и `lightmapCount <= 0` -> `"Illegal lightmaps length."`
+- отсутствующий материал -> `"Material %s not found."` + fallback `DEFAULT`
+- отсутствующая lightmap -> `"LightMap %s not found."` и slot `-1`
-- `wearCount` (обязательно `> 0`, иначе ошибка `"Illegal wear length."`)
+### 8.4 Ограничения runtime
-2) Далее следует `wearCount` строк. Каждая строка имеет вид:
+- Таблиц в `MatManager`: максимум 70 (физический layout).
+- Жёсткой проверки на overflow таблиц в `sub_10003B10/sub_10003F80` нет.
-- `<int> <пробелы> <materialName>`
+Инструментам нужно явно валидировать `tableCount < 70`.
-Где:
+---
-- `<int>` парсится, но фактически не используется как ключ (движок обрабатывает записи последовательно).
-- `<materialName>` — имя материала, которое движок ищет в менеджере материалов.
- - Если материал не найден, пишется `"Material %s not found."` и используется fallback `"DEFAULT"`.
+## 9. Загрузка texture/lightmap по имени
-> Практическая рекомендация для инструментов: считайте `<int>` как необязательный “legacy-id”, а истинным идентификатором материала делайте строку `<materialName>`.
+Общие функции:
-### 2.7.2. Блок LIGHTMAPS
+- `sub_10004B10` — texture (`Textures.lib`)
+- `sub_10004CB0` — lightmap (`LightMap.lib`)
-После чтения wear-списка движок последовательно читает токены (`fscanf("%s")`) до тех пор, пока не встретит слово **`LIGHTMAPS`**.
+### 9.1 Валидация имени
-Затем:
+Алгоритм требует наличие `'.'` в позиции `0..16`.
-1) Читается `lightmapCount`:
+Иначе:
-- `lightmapCount` (обязательно `> 0`, иначе ошибка `"Illegal lightmaps length."`)
+- `"Bad texture name."`
+- возврат `-1`
-2) Далее следует `lightmapCount` строк вида:
+### 9.2 Palette index из суффикса
-- `<int> <пробелы> <lightmapName>`
+После точки разбирается:
-Где:
+- `L = toupper(name[dot+1])`
+- `D = name[dot+2]` (опционально)
+- `idx = (L - 'A') * 11 + (D ? (D - '0' + 1) : 0)`
-- `<int>` парсится, но фактически не используется как ключ (аналогично wear).
-- `<lightmapName>` — имя лайтмапы; если ресурс не найден, пишется `"LightMap %s not found."`.
+Если `idx < 0`, палитра не подставляется (`0`).
-### 2.7.3. Валидация имени лайтмапы (деталь движка)
+Практически в стоковых ассетах имена часто вида `NAME.0`; это даёт `idx < 0`, т.е. без палитровой привязки.
-Перед загрузкой лайтмапы выполняется проверка имени:
+### 9.3 Кэширование
-- в имени должна встречаться точка `.` **в пределах первых 16 символов**, иначе ошибка `"Bad texture name."`;
-- далее движок использует подстроку после точки в вычислениях внутренних индексов/кэша (на практике полезно придерживаться шаблона вида `NAME.A1`, `NAME.B2` и т.п.).
+- Дедупликация по `resIndex`.
+- При повторном запросе увеличивается `refCount`, `lastZeroRefTime` сбрасывается в `0`.
+- При освобождении материала `refCount` texture/lightmap уменьшается.
---
-## 2.8. Формат текстурного ассета `Texm` (Ngi32.dll)
-Текстуры из `Textures.lib` хранятся как NRes‑entries типа `0x6D786554` (`"Texm"`).
+## 10. Формат `Texm`
-### 2.8.1. Заголовок `Texm` (32 байта)
+### 10.1 Заголовок 32 байта
```c
struct TexmHeader32 {
- uint32_t magic; // 0x6D786554 ('Texm')
- uint32_t width; // base width
- uint32_t height; // base height
- uint32_t mipCount; // количество уровней
- uint32_t flags4; // наблюдаются 0 или 32
- uint32_t flags5; // наблюдаются 0 или 0x04000000
- uint32_t unk6; // служебное поле (часто 0, иногда ненулевое)
- uint32_t format; // код пиксельного формата
+ uint32_t magic; // 'Texm' = 0x6D786554
+ uint32_t width;
+ uint32_t height;
+ uint32_t mipCount;
+ uint32_t flags4;
+ uint32_t flags5;
+ uint32_t unk6;
+ uint32_t format;
};
```
-Подтверждённые `format`:
+### 10.2 Поддерживаемые `format`
-- `0` — paletted 8-bit (индекс + palette);
-- `565`, `556`, `4444` — 16-bit семейство;
-- `888`, `8888` — 32-bit семейство.
+Подтверждённые в данных:
-### 2.8.2. Layout payload
+- `0` (палитровый 8-bit)
+- `565`
+- `4444`
+- `888`
+- `8888`
-После заголовка:
+Поддерживается loader-ветками Ngi32 (может встречаться в runtime-генерации):
-1) если `format == 0`: palette блок 1024 байта (`256 × 4`);
-2) далее mip-chain пикселей;
-3) опционально chunk атласа `Page`.
+- `556`
+- `88`
-Размер mip-chain:
+### 10.3 Layout payload
-```c
-bytesPerPixel = (format == 0 ? 1 : format in {565,556,4444} ? 2 : 4);
-pixelBytes = bytesPerPixel * sum_{i=0..mipCount-1}(max(1,width>>i) * max(1,height>>i));
-```
+1. `TexmHeader32`
+2. если `format == 0`: palette table `256 * 4 = 1024` байта
+3. mip-chain пикселей
+4. опциональный `Page` chunk
-Итого «чистый» размер без `Page`:
+Расчёт:
```c
-sizeCore = 32 + (format == 0 ? 1024 : 0) + pixelBytes;
-```
+bytesPerPixel =
+ (format == 0) ? 1 :
+ (format == 565 || format == 556 || format == 4444 || format == 88) ? 2 :
+ 4;
-### 2.8.3. Опциональный `Page` chunk
+pixelCount = sum_{i=0..mipCount-1}(max(1, width>>i) * max(1, height>>i));
+sizeCore = 32 + (format == 0 ? 1024 : 0) + bytesPerPixel * pixelCount;
+```
-Если после `sizeCore` остаются байты и в этой позиции стоит magic `"Page"` (`0x65676150`), парсер `sub_1000FF60` читает таблицу subrect:
+### 10.4 `Page` chunk
```c
struct PageChunk {
- uint32_t magic; // 'Page'
- uint32_t count;
+ uint32_t magic; // 'Page'
+ uint32_t rectCount;
struct Rect16 {
int16_t x;
int16_t w;
int16_t y;
int16_t h;
- } rects[count];
+ } rects[rectCount];
};
```
-Для каждого rect рантайм строит:
+Runtime конвертирует `Rect16` в:
-- пиксельные границы (`x0,y0,x1,y1`);
-- нормализованные UV (`u0,v0,u1,v1`) с делителем `1/(width<<mipSkip)` и `1/(height<<mipSkip)`.
+- пиксельные прямоугольники;
+- UV-границы с учётом возможного `mipSkip`.
-`mipSkip` вычисляется `sub_1000F580` (уровень, с которого реально начинается загрузка в GPU в зависимости от формата/ограничений).
+### 10.5 Loader-поведение (`sub_1000FB30`)
-### 2.8.4. Palette в формате `format==0`
+- Читает header в внутренние поля (`+56..+84`).
+- Для `format==0` считывает palette и переставляет каналы в runtime-таблицу.
+- Считает `sizeCore`, находит tail.
+- `Page` разбирается только если включён флаг загрузки `0x400000` и tail содержит `Page`.
+- Может уменьшать стартовый mip (`sub_1000F580`) в зависимости от размеров/формата/флагов.
+- При `DisableMipmap == 0` и допустимых условиях может строить mips в runtime.
-В `sub_1000FB30` palette конвертируется в локальную 32-bit таблицу; байты источника читаются как BGR-порядок (четвёртый байт входной записи не используется напрямую в базовом пути), итоговая alpha зависит от флагов runtime-конфига.
+---
+
+## 11. Флаги профиля/рендера (Ngi32)
-### 2.8.5. Проверка на реальных данных
+Ключ реестра: `HKCU\Software\Nikita\NgiTool`.
-Для всех 393 entries в `Textures.lib`:
+Подтверждённые значения:
-- `magic == 'Texm'`;
-- размеры совпадают с `sizeCore` либо `sizeCore + PageChunk (+pad до 8 байт NRes)`;
-- при наличии хвоста в `sizeCore` всегда обнаруживается валидный `Page` chunk.
+- `Disable MultiTexturing`
+- `DisableMipmap`
+- `Force 16-bit textures`
+- `UseFirstCard`
+- `DisableD3DCalls`
+- `DisableDSound`
+- `ForceCpu`
+
+Они напрямую влияют на выбор texture format path, mip handling и fallback-ветки.
---
+
+## 12. Спецификация для toolchain (read/edit/write)
+
+### 12.1 Каноническая модель данных
+
+1. `MAT0`:
+- хранить исходные `attr1/attr2/attr3`;
+- хранить сырой payload + декодированную структуру;
+- при записи сохранять порядок/размеры секций точно.
+
+2. `WEAR`:
+- хранить строки wear/lightmaps как текст;
+- сохранять порядок строк;
+- допускать отсутствие блока `LIGHTMAPS`.
+
+3. `Texm`:
+- хранить header поля как есть (`flags4/flags5/unk6` не нормализовать);
+- хранить palette (если есть), mip data, `Page`.
+
+### 12.2 Правила lossless записи
+
+- Не менять значения `flags4/flags5/unk6` без явной причины.
+- Не менять `NRes` entry attrs, если цель — бинарный round-trip.
+- Для `MAT0`:
+ - `animBlockCount < 20`.
+ - `phaseCount` и фактический размер секции должны совпадать.
+ - textureName в фазе всегда укладывать в 16 байт и NUL-терминировать.
+- Для `Texm`:
+ - `magic == 'Texm'`.
+ - `mipCount > 0`, `width>0`, `height>0`.
+ - tail либо отсутствует, либо ровно один корректный `Page` chunk без лишних байт.
+
+### 12.3 Рекомендованные валидации редактора
+
+- `WEAR`:
+ - `wearCount > 0`.
+ - число строк wear соответствует `wearCount`.
+ - если есть `LIGHTMAPS`, то `lightmapCount > 0` и число строк совпадает.
+- `MAT0`:
+ - не выходить за payload при распаковке.
+ - все ссылки фаз/keys проверять на диапазоны.
+- `Texm`:
+ - `sizeCore <= payload_size`.
+ - проверка `Page` как `8 + rectCount*8`.
+
+---
+
+## 13. Проверка на реальных данных (`tmp/gamedata`)
+
+### 13.1 `Material.lib`
+
+- `905` entries, все `type=MAT0`
+- `attr2 = 6` у всех
+- `attr3 = 0` у всех
+- `phaseCount` до `29`
+- `animBlockCount` до `8` (ограничение runtime `<20` соблюдается)
+
+### 13.2 `Textures.lib`
+
+- `393` entries, все `type=Texm`
+- форматы: `8888(237), 888(52), 565(47), 4444(42), 0(15)`
+- `flags4`: `32(361), 0(32)`
+- `flags5`: `0(312), 0x04000000(81)`
+- `Page` chunk присутствует у `65` текстур
+
+### 13.3 `lightmap.lib`
+
+- `25` entries, все `Texm`
+- формат: `565`
+- `mipCount=1`
+- `flags5`: в основном `0`, встречается `0x00800000`
+
+### 13.4 `WEAR`
+
+- `439` entries `type=WEAR`
+- `attr1=0, attr2=0, attr3=1`
+- `21` entry содержит блок `LIGHTMAPS` (в текущем наборе везде `lightmapCount=1`)
+
+---
+
+## 14. Не до конца определённые семантики
+
+Эти поля нужно сохранять прозрачно:
+
+- `MAT0`:
+ - `k2` в `AnimBlockRaw::KeyRaw`
+ - точная доменная семантика `metaA/metaB/metaC/metaD`
+ - точная семантика части float-полей в `MaterialPhase76`
+- `Texm`:
+ - смысл `flags4/flags5/unk6` вне уже наблюдённых веток
+ - формат `88` в файловом контенте (поддержка есть, но в сток-данных не найден)
+
+---
+
+## 15. Минимальные псевдокоды для реализации
+
+### 15.1 `parse_mat0(payload, attr2)`
+
+```python
+def parse_mat0(payload: bytes, attr2: int):
+ cur = 0
+ phase_count = u16(payload, cur); cur += 2
+ anim_count = u16(payload, cur); cur += 2
+ if anim_count >= 20:
+ raise ValueError("Too many animations for material")
+
+ if attr2 < 2:
+ metaA, metaB, metaC, metaD = 255, 255, 0x3F800000, 0
+ else:
+ metaA = u8(payload, cur); cur += 1
+ metaB = u8(payload, cur); cur += 1
+ metaC = u32(payload, cur) if attr2 >= 3 else 0x3F800000
+ cur += 4 if attr2 >= 3 else 0
+ metaD = u32(payload, cur) if attr2 >= 4 else 0
+ cur += 4 if attr2 >= 4 else 0
+
+ phases = [payload[cur + i*34 : cur + (i+1)*34] for i in range(phase_count)]
+ cur += 34 * phase_count
+
+ anim = []
+ for _ in range(anim_count):
+ raw = u32(payload, cur); cur += 4
+ key_count = u16(payload, cur); cur += 2
+ keys = [payload[cur + k*6 : cur + (k+1)*6] for k in range(key_count)]
+ cur += 6 * key_count
+ anim.append((raw, keys))
+
+ if cur != len(payload):
+ raise ValueError("MAT0 tail bytes")
+
+ return phase_count, anim_count, metaA, metaB, metaC, metaD, phases, anim
+```
+
+### 15.2 `parse_texm(payload)`
+
+```python
+def parse_texm(payload: bytes):
+ magic, w, h, mips, f4, f5, unk6, fmt = unpack_u32x8(payload, 0)
+ if magic != 0x6D786554:
+ raise ValueError("not Texm")
+
+ bpp = 1 if fmt == 0 else (2 if fmt in (565, 556, 4444, 88) else 4)
+ pix = 0
+ mw, mh = w, h
+ for _ in range(mips):
+ pix += mw * mh
+ mw = max(1, mw >> 1)
+ mh = max(1, mh >> 1)
+
+ core = 32 + (1024 if fmt == 0 else 0) + bpp * pix
+ if core > len(payload):
+ raise ValueError("truncated")
+
+ page = None
+ if core < len(payload):
+ if core + 8 > len(payload) or payload[core:core+4] != b"Page":
+ raise ValueError("tail without Page")
+ n = u32(payload, core + 4)
+ need = 8 + n * 8
+ if core + need != len(payload):
+ raise ValueError("invalid Page size")
+ page = [unpack_i16x4(payload, core + 8 + i*8) for i in range(n)]
+
+ return (w, h, mips, fmt, f4, f5, unk6, page)
+```
+