From 70ed6480c2b2b2ecab4956216c1e8e85b0938b4c Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Wed, 11 Feb 2026 21:50:33 +0000 Subject: 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. --- docs/specs/fxid.md | 480 ++++++++++++++++++++++---- docs/specs/materials-texm.md | 784 +++++++++++++++++++++++++++++++++---------- 2 files changed, 1016 insertions(+), 248 deletions(-) (limited to 'docs/specs') diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md index d4ff66d..65bf7f1 100644 --- a/docs/specs/fxid.md +++ b/docs/specs/fxid.md @@ -1,102 +1,421 @@ # FXID -Документ описывает контейнер ресурса эффекта и формат команд эффекта. +Документ описывает формат ресурса эффекта `FXID`, контракт runtime в `Effect.dll` и практические правила для инструментов чтения/конвертации/редактирования. + +Цель: дать достаточную high-level спецификацию для: + +- 1:1 загрузчика/рантайма эффекта; +- валидатора payload; +- бинарно-совместимого редактора; +- конвертера в промежуточный формат и обратно. + +Связанный контейнер: [NRes / RsLi](nres.md). + +--- + +## 1. Источники восстановления + +Спецификация собрана по: + +- `tmp/disassembler1/Effect.dll.c`; +- `tmp/disassembler2/Effect.dll.asm`; +- интеграционным вызовам из `tmp/disassembler1/Terrain.dll.c`; +- проверке реальных архивов `testdata/nres`. + +Ключевые точки: + +- parser FXID: `Effect.dll!sub_10007650`; +- core update: `Effect.dll!sub_10008120`, `sub_10006170`, `sub_10007D10`; +- export API: `CreateFxManager`, `InitializeSettings`. --- -## 3.2. Контейнер ресурса эффекта +## 2. Место формата в движке + +### 2.1. Контейнер NRes + +Эффект хранится как запись NRes с типом: + +- `type_id = 0x44495846` (`"FXID"`). + +Для всех 923 FXID-entries в `testdata/nres` подтверждено: -Эффекты в игровых архивах хранятся как NRes‑entries типа: +- `attr1 = 0`; +- `attr2 = 0`; +- `attr3 = 1`. -- `0x44495846` (`"FXID"`). +### 2.2. Runtime-модуль -Парсер эффекта находится в `Effect.dll!sub_10007650`. +`Effect.dll` экспортирует 2 функции: -## 3.3. Формат payload эффекта +- `CreateFxManager(int a1, int a2, int owner)`; +- `InitializeSettings()`. -### 3.3.1. Header (первые 60 байт) +`CreateFxManager` выделяет объект (`0xB8` байт), инициализирует его через `sub_10003AE0`, возвращает **интерфейсный указатель** (смещение `+4` от базового объекта). + +### 2.3. COM-подобный интерфейс + +Внешний код (например, `Terrain.dll`) получает рабочий интерфейс через `QueryInterface(id=19)` и далее вызывает методы vtable `off_1001E478`. + +Ключевые методы интерфейса менеджера (по vtable): + +| Vtable offset | Функция | Назначение (high-level) | +|---|---|---| +| +0x10 | `sub_10004320` | Открыть/закэшировать ресурс эффекта (`archive + name`) | +| +0x14 | `sub_10004590` | Создать runtime-инстанс эффекта по шаблону | +| +0x18 | `sub_10004780` | Удалить инстанс по id | +| +0x1C | `sub_100047B0` | Установить режим интерполяции/времени | +| +0x20 | `sub_100047D0` | Установить scale | +| +0x24 | `sub_10004830` | Установить позицию | +| +0x28 | `sub_10004930` | Установить матрицу transform | +| +0x2C | `sub_10004B00` | Перезапуск с mode | +| +0x38 | `sub_10004BA0` | Модификатор длительности | +| +0x3C | `sub_10004BD0` | Start/Enable | +| +0x40 | `sub_10004C10` | Stop/Disable | +| +0x44 | `sub_10004C50` | Привязать emitter/context | +| +0x48 | `sub_10004D50` | Сброс frame-флагов | +| +0x08 | `sub_10003D30` | Системные event-коды (tick/reset/remove-range) | + +Этого контракта достаточно, чтобы корректно встроить FXID-рантайм в движок. + +--- + +## 3. Бинарный формат payload FXID + +Все числа little-endian. + +## 3.1. Header (60 байт, `0x3C`) ```c struct FxHeader60 { - uint32_t cmdCount; // +0x00 - uint32_t globalFlags; // +0x04 - float durationSec; // +0x08 (дальше умножается на 1000.0) - uint32_t unk0C; // +0x0C - uint32_t flags10; // +0x10 (используются биты 0x40 и 0x400) - uint8_t reserved[0x2C];// +0x14..+0x3B + uint32_t cmd_count; // 0x00: число команд + uint32_t time_mode; // 0x04: базовый режим вычисления alpha/time + float duration_sec; // 0x08: длительность эффекта в секундах + float phase_jitter; // 0x0C: амплитуда рандом-сдвига alpha (если flags bit0) + uint32_t flags; // 0x10: флаги runtime (см. таблицу ниже) + uint32_t settings_id; // 0x14: id категории/настройки (используется low8) + float rand_shift_x; // 0x18: рандомный сдвиг (если flags bit3) + float rand_shift_y; // 0x1C + float rand_shift_z; // 0x20 + float pivot_x; // 0x24: опорная точка/anchor + float pivot_y; // 0x28 + float pivot_z; // 0x2C + float scale_x; // 0x30: базовый scale + float scale_y; // 0x34 + float scale_z; // 0x38 }; ``` -Поток команд начинается строго с `offset 0x3C`. +Командный поток начинается строго с `offset = 0x3C`. -### 3.3.2. Командный поток +## 3.2. Поля header: подтверждённая семантика -Каждая команда начинается с `uint32 cmdWord`, где: +- `cmd_count`: + - engine итерируется ровно `cmd_count` раз; + - дополнительных ограничений в оригинале нет. +- `time_mode`: + - начальный runtime-mode (`effect+0x14`), участвует в `sub_10005C60`. +- `duration_sec`: + - переводится в миллисекунды как `duration_ms = duration_sec * 1000.0`. +- `phase_jitter`: + - при `flags & 0x1` к вычисленному alpha добавляется рандом в диапазоне `[-phase_jitter/2, +phase_jitter/2]`. +- `settings_id`: + - `sub_1000EC40` использует только `settings_id & 0xFF` как индекс таблицы настроек. +- `rand_shift_*`: + - при `flags & 0x8` добавляется рандомный сдвиг к позиции эффекта. +- `pivot_*`: + - используется как опорная точка в ветках проверки видимости/окклюзии (`sub_10007D10`). +- `scale_*`: + - копируется в runtime (`this+56..64`) и участвует в построении матрицы в `sub_10007C90`. -- `opcode = cmdWord & 0xFF`; -- `enabled = (cmdWord >> 8) & 1` (копируется в `obj+4`). +## 3.3. `flags` (`header+0x10`) — подтвержденные биты -Размер команды зависит от opcode и прибавляется в **байтах** (`add edi, ...` в ASM): +| Бит | Маска | Поведение | +|---|---:|---| +| 0 | `0x0001` | Включает random phase jitter (`phase_jitter`) | +| 3 | `0x0008` | Включает random positional shift (`rand_shift_*`) | +| 4 | `0x0010` | Участвует в ветках видимости/окклюзии в `sub_10006170`/`sub_10007D10` | +| 5 | `0x0020` | Треугольная ремап-функция alpha в `sub_10005C60` | +| 6 | `0x0040` | Инвертирует начальное активное состояние (`this+324 = !(flags&0x40)`) | +| 7 | `0x0080` | Условная фильтрация по manager-флагу day/night | +| 8 | `0x0100` | Инверсная day/night фильтрация | +| 9 | `0x0200` | Домножение alpha на нормализованное время жизни | +| 10 | `0x0400` | Включает manager-глобальный флаг (`manager+0xA0` bit1) | +| 11 | `0x0800` | Меняет поведение ветки `sub_10007D10` (gating для checks) | +| 12 | `0x1000` | Проставляет manager-state bit0x10 в `sub_10006170` | -| Opcode | Размер записи | -|--------|---------------| -| 1 | 224 | -| 2 | 148 | -| 3 | 200 | -| 4 | 204 | -| 5 | 112 | -| 6 | 4 | -| 7 | 208 | -| 8 | 248 | -| 9 | 208 | -| 10 | 208 | +Остальные биты в движке напрямую не расшифрованы на уровне high-level, но должны сохраняться 1:1. -Никакого межкомандного выравнивания нет: следующая команда сразу после `size(opcode)`. +## 3.4. `time_mode` (`header+0x04`) — режимы `sub_10005C60` -## 3.4. Runtime-классы команд (vtable mapping) +Поддерживаются коды `0..17`. -В `sub_10007650` для каждого opcode создаётся объект конкретного типа: +| mode | Логика | +|---:|---| +| 0 | Константа (значение из runtime-поля) | +| 1 | Линейно: `(t - t0) / (t1 - t0)` | +| 2 | Цикл `frac((t - t0)/(t1 - t0))` | +| 3 | Обратная линейная: `1 - (t - t0)/(t1 - t0)` | +| 4 | Значение из внешнего queue/world-запроса | +| 5..8 | Нормированные отношения компонент вектора (camera/world path) | +| 9..12 | Альтернативный набор нормированных отношений | +| 13 | `1 - value` из queue-запроса по объекту | +| 14 | `1 - value` из параметра queue id=49 | +| 15 | max из двух нормированных длин | +| 16 | Кламп "не убывать" относительно предыдущего значения | +| 17 | Кламп "не возрастать" относительно предыдущего значения | + +После базового mode-преобразования применяются post-флаги `0x200` и `0x20`. + +--- -- `op1` → `off_1001E78C` -- `op2` → `off_1001F048` -- `op3` → `off_1001E770` -- `op4` → `off_1001E754` -- `op5` → `off_1001E360` -- `op6` → `off_1001E738` -- `op7` → `off_1001E228` -- `op8` → `off_1001E71C` -- `op9` → `off_1001E700` -- `op10` → `off_1001E24C` +## 4. Командный поток -`flags10 & 0x400` включает глобальный runtime-флаг менеджера эффекта (`manager+0xA0`). +## 4.1. Формат записи команды -## 3.5. Алгоритм загрузки эффекта (1:1) +Каждая команда начинается с `uint32 cmd_word`. + +Биты: + +- `opcode = cmd_word & 0xFF`; +- `enabled = (cmd_word >> 8) & 1`; +- в реальных данных `bits 9..31 == 0` (но редактор должен сохранять весь word как есть). + +Никакого межкомандного выравнивания нет: следующая команда начинается сразу после `size(opcode)`. + +## 4.2. Размеры записей по opcode + +| Opcode | Размер записи (байт) | Размер тела после `cmd_word` | +|---:|---:|---:| +| 1 | 224 | 220 | +| 2 | 148 | 144 | +| 3 | 200 | 196 | +| 4 | 204 | 200 | +| 5 | 112 | 108 | +| 6 | 4 | 0 | +| 7 | 208 | 204 | +| 8 | 248 | 244 | +| 9 | 208 | 204 | +| 10 | 208 | 204 | + +## 4.3. Opcode -> runtime-класс + +В `sub_10007650` для opcode создаются объекты: + +| Opcode | `operator new` | Runtime vtable | +|---:|---:|---| +| 1 | `0xF0` | `off_1001E78C` | +| 2 | `0xA0` | `off_1001F048` | +| 3 | `0xFC` | `off_1001E770` | +| 4 | `0x104` | `off_1001E754` | +| 5 | `0x54` | `off_1001E360` | +| 6 | `0x1C` | `off_1001E738` | +| 7 | `0x48` | `off_1001E228` | +| 8 | `0xAC` | `off_1001E71C` | +| 9 | `0x100` | `off_1001E700` | +| 10 | `0x48` | `off_1001E24C` | + +Важно: payload команды хранится как сырой указатель и разбирается runtime-методами класса. + +## 4.4. Внутренний вызовной контракт команд + +После создания каждой команды менеджер: + +1. Проставляет `enabled` из `cmd_word.bit8` в поле `obj+4`. +2. Вызывает инициализацию команды (`vfunc +4`) с аргументами `(queue, manager)`. +3. Добавляет команду в массив команд эффекта. + +В update-cycle менеджер вызывает: + +- `vfunc +8`: вычисление/обновление команды (bool); +- `vfunc +12`: callback при render/emission; +- `vfunc +20`: toggle активности; +- `vfunc +24`: обновление transform-context (для части opcode no-op). + +--- + +## 5. Алгоритм загрузки FXID (engine-accurate) + +Псевдокод `sub_10007650`: ```c -read header60 -ptr = data + 0x3C -for i in 0..cmdCount-1: - op = ptr[0] & 0xFF - obj = new CommandClass(op) - obj->enabled = (ptr[0] >> 8) & 1 - obj->raw = ptr - manager.attach(obj) - ptr += sizeByOpcode(op) +void FxLoad(FxInstance* fx, uint8_t* payload) { + FxHeader60* h = (FxHeader60*)payload; + + fx->raw_header_ptr = h; + fx->mode = h->time_mode; + fx->end_ms = h->duration_sec * 1000.0f + fx->start_ms; + fx->scale = { h->scale_x, h->scale_y, h->scale_z }; + fx->active_default = ((h->flags & 0x40) == 0); + + uint8_t* ptr = payload + 0x3C; + + for (uint32_t i = 0; i < h->cmd_count; i++) { + uint32_t w = *(uint32_t*)ptr; + uint8_t op = (uint8_t)(w & 0xFF); + + Command* cmd = CreateCommandByOpcode(op, ptr); // может вернуть null + + if (cmd != null) { + cmd->enabled = (w >> 8) & 1; + + if (h->flags & 0x400) + fx->manager_flags |= 0x0100; // внутренний bit + + if ((h->flags & 0x400) || cmd->enabled) + fx->manager_flags |= 0x0010; + + cmd->Attach(fx->queue, fx); + fx->commands.push_back(cmd); + } + + ptr += size_by_opcode(op); // в оригинале без checks + } +} +``` + +Поведение оригинала, важное для 1:1: + +- проверок границ буфера нет; +- при `unknown opcode` указатель `ptr` не двигается (счётчик цикла движется); +- при `new == null` команда пропускается, но `ptr` двигается на размер opcode. + +Для toolchain рекомендуется **строгий** и **безопасный** парсер (см. раздел 7). + +--- + +## 6. Runtime-жизненный цикл эффекта + +## 6.1. Инициализация + +- `sub_10007470`: конструктор instance; +- инициализируются матрицы/scale/флаги; +- начальный `mode` берётся из header. + +## 6.2. Tick и обновление + +Основной тик идёт через `sub_10003D30(case 28)`: + +1. обновление времени manager; +2. обход активных FX instances; +3. для каждого инстанса `sub_10006170`: + - gating по `flags`/queue-state; + - вычисление alpha через `sub_10005C60`; + - вызов `sub_10008120` (update/bounds/command-pass); + - при необходимости `sub_10007D10` (эмиссия/рендерный callback). + +## 6.3. Start/Stop/Restart API + +- Start: `sub_10004BD0` -> `sub_10007A30(..., 1, now)`; +- Stop: `sub_10004C10` -> `sub_10007A30(..., 0, now)`; +- Restart/retime: `sub_10004B00`, `sub_10004BA0`. + +## 6.4. Manager event-codes (`sub_10003D30`) + +Обработанные коды: + +- `4`: bootstrap + установка текущего времени; +- `20`: удаление диапазона объектов в queue и корректировка индексов; +- `23`: выставить manager-flag bit0; +- `24`: сбросить manager-flag bit0; +- `28`: основной per-frame update. + +--- + +## 7. Спецификация для инструментов + +## 7.1. Reader (strict) + +Рекомендуемый строгий парсер: + +1. проверить `len(payload) >= 60`; +2. прочитать `cmd_count`; +3. `ptr = 0x3C`; +4. для каждой команды: + - требовать `ptr + 4 <= len`; + - прочитать `opcode`; + - `opcode` должен быть в `1..10`; + - `ptr + size(opcode) <= len`; + - `ptr += size(opcode)`; +5. в strict-режиме требовать `ptr == len(payload)`. + +Такой алгоритм совпадает с валидатором `tools/msh_doc_validator.py`. + +## 7.2. Reader (engine-compatible) + +Для byte-level совместимости с оригиналом можно поддержать legacy-режим: + +- без bounds-check (как `Effect.dll`); +- с toleration на `unknown opcode` (но это потенциально unsafe). + +## 7.3. Editor (без потери совместимости) + +Безопасные операции: + +- менять `header`-поля (mode, duration, flags, scale, pivot); +- менять `enabled` через `cmd_word.bit8`; +- удалять/вставлять команды с корректным пересчётом `cmd_count` и сдвигом stream; +- сохранять command-body как opaque bytes, если нет полного field-level декодера. + +Правила: + +- всегда little-endian; +- не менять размеры записей opcode; +- не вставлять padding между командами; +- для неизвестных битов `cmd_word` и `header.flags` использовать copy-through. + +## 7.4. Writer (canonical) + +Каноническая сборка payload: + +1. записать `FxHeader60`; +2. `cmd_count = len(commands)`; +3. для каждой команды записать `cmd_word` + body фиксированного размера для opcode; +4. итоговый размер должен быть `0x3C + sum(size(opcode_i))`; +5. без хвоста. + +## 7.5. Конвертация в промежуточный JSON + +Рекомендуемая структура для round-trip: + +```json +{ + "header": { + "time_mode": 1, + "duration_sec": 2.5, + "phase_jitter": 0.2, + "flags": 22, + "settings_id": 785, + "rand_shift": [0.0, 0.0, 0.0], + "pivot": [0.0, 0.0, 0.0], + "scale": [1.0, 1.0, 1.0] + }, + "commands": [ + { + "opcode": 3, + "enabled": 1, + "word_raw": 259, + "body_hex": "..." + } + ] +} ``` -Ошибка формата: +`body_hex` хранит opaque payload без потери данных. -- неизвестный opcode; -- выход за пределы буфера до обработки `cmdCount`; -- непустой «хвост» после `cmdCount` команд (для строгого валидатора). +--- + +## 8. Проверка на реальных данных -## 3.6. Проверка на реальных данных +`testdata/nres` (через `tools/msh_doc_validator.py`) : -Для `testdata/nres/effects.rlb` (923 entries): +- FXID effects: `923/923 valid`. -- `opcode` всегда в диапазоне `1..10`; -- stream полностью покрывает payload без хвоста; -- частоты opcode: +Дополнительно по этим 923 payload: + +- `cmd_count`: min `0`, max `81`, avg `5.13`; +- `duration_sec`: min `0.0`, max `60.0`, avg `2.46`; +- `opcode` распределение: - `1: 618` - `2: 517` - `3: 1545` @@ -106,7 +425,38 @@ for i in 0..cmdCount-1: - `8: 237` - `9: 266` - `10: 160` - - `6` в этом наборе не встретился, но поддерживается парсером. + - `6`: не встречен, но поддержан parser. +- `cmd_word`: + - `bits 9..31` не использованы в датасете; + - `bit8` встречается для части opcode (особенно `3`, `7`, `9`). --- +## 9. Известные пробелы (не блокируют 1:1 container/runtime) + +1. Полная человеко-читаемая семантика **внутренних полей command body** для каждого opcode не завершена. +2. Для части битов `header.flags` есть только functional-наблюдение без финального gameplay-имени. +3. Высокие биты `settings_id` используются как есть (runtime читает low8); их предметное имя не зафиксировано. + +Это не мешает: + +- корректно читать/валидировать/пересобирать FXID; +- делать lossless редактирование; +- воспроизводить lifecycle менеджера и update-loop 1:1 на уровне контракта. + +--- + +## 10. Минимальный чек-лист реализации + +Для 1:1-порта движка: + +- реализовать `FxHeader60` и stream parser по размерам opcode; +- реализовать менеджер API (раздел 2.3); +- реализовать tick-path `03D30(case 28)` -> `06170` -> `08120`/`07D10`; +- учитывать флаги `0x40`, `0x400`, `0x800`, `0x1000`, `0x80/0x100`, `0x20`, `0x200`. + +Для инструментов: + +- strict validator по разделу 7.1; +- canonical writer по разделу 7.4; +- opaque-представление command-body для безопасного round-trip. 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.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"` + +И формирует имя: + +- `.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 +\n + \n // повторить wearCount раз -`World3D.dll` содержит парсер текстового файла (режим `rt`), который задаёт: +[LIGHTMAPS\n +\n + \n // повторить lightmapCount раз] +``` + +- `` читается, но как ключ не используется. +- Идентификатором реально является имя (`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` нет. -- ` <пробелы> ` +Инструментам нужно явно валидировать `tableCount < 70`. -Где: +--- -- `` парсится, но фактически не используется как ключ (движок обрабатывает записи последовательно). -- `` — имя материала, которое движок ищет в менеджере материалов. - - Если материал не найден, пишется `"Material %s not found."` и используется fallback `"DEFAULT"`. +## 9. Загрузка texture/lightmap по имени -> Практическая рекомендация для инструментов: считайте `` как необязательный “legacy-id”, а истинным идентификатором материала делайте строку ``. +Общие функции: -### 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 из суффикса -- ` <пробелы> ` +После точки разбирается: -Где: +- `L = toupper(name[dot+1])` +- `D = name[dot+2]` (опционально) +- `idx = (L - 'A') * 11 + (D ? (D - '0' + 1) : 0)` -- `` парсится, но фактически не используется как ключ (аналогично wear). -- `` — имя лайтмапы; если ресурс не найден, пишется `"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< 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) +``` + -- cgit v1.2.3