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.
This commit is contained in:
@@ -1,102 +1,421 @@
|
|||||||
# FXID
|
# FXID
|
||||||
|
|
||||||
Документ описывает контейнер ресурса эффекта и формат команд эффекта.
|
Документ описывает формат ресурса эффекта `FXID`, контракт runtime в `Effect.dll` и практические правила для инструментов чтения/конвертации/редактирования.
|
||||||
|
|
||||||
|
Цель: дать достаточную high-level спецификацию для:
|
||||||
|
|
||||||
|
- 1:1 загрузчика/рантайма эффекта;
|
||||||
|
- валидатора payload;
|
||||||
|
- бинарно-совместимого редактора;
|
||||||
|
- конвертера в промежуточный формат и обратно.
|
||||||
|
|
||||||
|
Связанный контейнер: [NRes / RsLi](nres.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3.2. Контейнер ресурса эффекта
|
## 1. Источники восстановления
|
||||||
|
|
||||||
Эффекты в игровых архивах хранятся как NRes‑entries типа:
|
Спецификация собрана по:
|
||||||
|
|
||||||
- `0x44495846` (`"FXID"`).
|
- `tmp/disassembler1/Effect.dll.c`;
|
||||||
|
- `tmp/disassembler2/Effect.dll.asm`;
|
||||||
|
- интеграционным вызовам из `tmp/disassembler1/Terrain.dll.c`;
|
||||||
|
- проверке реальных архивов `testdata/nres`.
|
||||||
|
|
||||||
Парсер эффекта находится в `Effect.dll!sub_10007650`.
|
Ключевые точки:
|
||||||
|
|
||||||
## 3.3. Формат payload эффекта
|
- parser FXID: `Effect.dll!sub_10007650`;
|
||||||
|
- core update: `Effect.dll!sub_10008120`, `sub_10006170`, `sub_10007D10`;
|
||||||
|
- export API: `CreateFxManager`, `InitializeSettings`.
|
||||||
|
|
||||||
### 3.3.1. Header (первые 60 байт)
|
---
|
||||||
|
|
||||||
|
## 2. Место формата в движке
|
||||||
|
|
||||||
|
### 2.1. Контейнер NRes
|
||||||
|
|
||||||
|
Эффект хранится как запись NRes с типом:
|
||||||
|
|
||||||
|
- `type_id = 0x44495846` (`"FXID"`).
|
||||||
|
|
||||||
|
Для всех 923 FXID-entries в `testdata/nres` подтверждено:
|
||||||
|
|
||||||
|
- `attr1 = 0`;
|
||||||
|
- `attr2 = 0`;
|
||||||
|
- `attr3 = 1`.
|
||||||
|
|
||||||
|
### 2.2. Runtime-модуль
|
||||||
|
|
||||||
|
`Effect.dll` экспортирует 2 функции:
|
||||||
|
|
||||||
|
- `CreateFxManager(int a1, int a2, int owner)`;
|
||||||
|
- `InitializeSettings()`.
|
||||||
|
|
||||||
|
`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
|
```c
|
||||||
struct FxHeader60 {
|
struct FxHeader60 {
|
||||||
uint32_t cmdCount; // +0x00
|
uint32_t cmd_count; // 0x00: число команд
|
||||||
uint32_t globalFlags; // +0x04
|
uint32_t time_mode; // 0x04: базовый режим вычисления alpha/time
|
||||||
float durationSec; // +0x08 (дальше умножается на 1000.0)
|
float duration_sec; // 0x08: длительность эффекта в секундах
|
||||||
uint32_t unk0C; // +0x0C
|
float phase_jitter; // 0x0C: амплитуда рандом-сдвига alpha (если flags bit0)
|
||||||
uint32_t flags10; // +0x10 (используются биты 0x40 и 0x400)
|
uint32_t flags; // 0x10: флаги runtime (см. таблицу ниже)
|
||||||
uint8_t reserved[0x2C];// +0x14..+0x3B
|
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`;
|
## 3.3. `flags` (`header+0x10`) — подтвержденные биты
|
||||||
- `enabled = (cmdWord >> 8) & 1` (копируется в `obj+4`).
|
|
||||||
|
|
||||||
Размер команды зависит от 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 | Размер записи |
|
Остальные биты в движке напрямую не расшифрованы на уровне high-level, но должны сохраняться 1:1.
|
||||||
|--------|---------------|
|
|
||||||
| 1 | 224 |
|
|
||||||
| 2 | 148 |
|
|
||||||
| 3 | 200 |
|
|
||||||
| 4 | 204 |
|
|
||||||
| 5 | 112 |
|
|
||||||
| 6 | 4 |
|
|
||||||
| 7 | 208 |
|
|
||||||
| 8 | 248 |
|
|
||||||
| 9 | 208 |
|
|
||||||
| 10 | 208 |
|
|
||||||
|
|
||||||
Никакого межкомандного выравнивания нет: следующая команда сразу после `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 | Кламп "не возрастать" относительно предыдущего значения |
|
||||||
|
|
||||||
- `op1` → `off_1001E78C`
|
После базового mode-преобразования применяются post-флаги `0x200` и `0x20`.
|
||||||
- `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`
|
|
||||||
|
|
||||||
`flags10 & 0x400` включает глобальный runtime-флаг менеджера эффекта (`manager+0xA0`).
|
---
|
||||||
|
|
||||||
## 3.5. Алгоритм загрузки эффекта (1:1)
|
## 4. Командный поток
|
||||||
|
|
||||||
|
## 4.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
|
```c
|
||||||
read header60
|
void FxLoad(FxInstance* fx, uint8_t* payload) {
|
||||||
ptr = data + 0x3C
|
FxHeader60* h = (FxHeader60*)payload;
|
||||||
for i in 0..cmdCount-1:
|
|
||||||
op = ptr[0] & 0xFF
|
fx->raw_header_ptr = h;
|
||||||
obj = new CommandClass(op)
|
fx->mode = h->time_mode;
|
||||||
obj->enabled = (ptr[0] >> 8) & 1
|
fx->end_ms = h->duration_sec * 1000.0f + fx->start_ms;
|
||||||
obj->raw = ptr
|
fx->scale = { h->scale_x, h->scale_y, h->scale_z };
|
||||||
manager.attach(obj)
|
fx->active_default = ((h->flags & 0x40) == 0);
|
||||||
ptr += sizeByOpcode(op)
|
|
||||||
|
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:
|
||||||
|
|
||||||
- неизвестный opcode;
|
- проверок границ буфера нет;
|
||||||
- выход за пределы буфера до обработки `cmdCount`;
|
- при `unknown opcode` указатель `ptr` не двигается (счётчик цикла движется);
|
||||||
- непустой «хвост» после `cmdCount` команд (для строгого валидатора).
|
- при `new == null` команда пропускается, но `ptr` двигается на размер opcode.
|
||||||
|
|
||||||
## 3.6. Проверка на реальных данных
|
Для toolchain рекомендуется **строгий** и **безопасный** парсер (см. раздел 7).
|
||||||
|
|
||||||
Для `testdata/nres/effects.rlb` (923 entries):
|
---
|
||||||
|
|
||||||
- `opcode` всегда в диапазоне `1..10`;
|
## 6. Runtime-жизненный цикл эффекта
|
||||||
- stream полностью покрывает payload без хвоста;
|
|
||||||
- частоты opcode:
|
## 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 без потери данных.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Проверка на реальных данных
|
||||||
|
|
||||||
|
`testdata/nres` (через `tools/msh_doc_validator.py`) :
|
||||||
|
|
||||||
|
- FXID effects: `923/923 valid`.
|
||||||
|
|
||||||
|
Дополнительно по этим 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`
|
- `1: 618`
|
||||||
- `2: 517`
|
- `2: 517`
|
||||||
- `3: 1545`
|
- `3: 1545`
|
||||||
@@ -106,7 +425,38 @@ for i in 0..cmdCount-1:
|
|||||||
- `8: 237`
|
- `8: 237`
|
||||||
- `9: 266`
|
- `9: 266`
|
||||||
- `10: 160`
|
- `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.
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.1. Архитектура материальной системы
|
## 1. Идентификаторы и сущности
|
||||||
|
|
||||||
Материальная подсистема реализована в `World3D.dll` и включает:
|
| Сущность | 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` |
|
||||||
|
|
||||||
- **Менеджер материалов** (`LoadMatManager`) — объект размером 0x470 байт (1136), хранящий до 140 таблиц материалов (поле `+572`, `this[143]`).
|
Дополнительно: палитры загружаются отдельным путём (через `SetPalettesLib` + `sub_10002B40`) и не являются `Texm`.
|
||||||
- **Библиотека палитр** (`SetPalettesLib`) — NRes‑архив с палитрами.
|
|
||||||
- **Библиотека текстур** (`SetTexturesLib`) — путь к файлу/каталогу текстур.
|
|
||||||
- **Библиотека материалов** (`SetMaterialLib`) — NRes‑архив с данными материалов.
|
|
||||||
- **Библиотека lightmap'ов** (`SetLightMapLib`) — опциональная.
|
|
||||||
|
|
||||||
### Загрузка палитр (sub_10002B40)
|
---
|
||||||
|
|
||||||
Палитры загружаются из NRes‑архива по именам. Система перебирает буквы `'A'`..'Z'` (26 категорий) × 11 суффиксов, формируя имена вида `"A<suffix>.pal"`. Каждая палитра загружается через `niOpenResFile` → `niReadData` и регистрируется как текстурный объект в графическом движке.
|
## 2. Архитектура подсистемы
|
||||||
|
|
||||||
Максимальное количество палитр: 26 × 11 = **286**.
|
### 2.1 Экспортируемые точки входа (World3D)
|
||||||
|
|
||||||
## 2.2. Запись материала (76 байт)
|
- `LoadMatManager`
|
||||||
|
- `SetPalettesLib`
|
||||||
|
- `SetTexturesLib`
|
||||||
|
- `SetMaterialLib`
|
||||||
|
- `SetLightMapLib`
|
||||||
|
- `SetGameTime`
|
||||||
|
- `UnloadAllTextures`
|
||||||
|
|
||||||
Материал представлен записью размером **76 байт** (19 DWORD). Поля восстановлены из функции интерполяции `sub_10003030` и функций `sub_100031F0` / `sub_10003680`.
|
`Set*Lib` просто копируют строки путей в глобальные буферы; валидации пути нет.
|
||||||
|
|
||||||
| Смещение | Размер | Тип | Интерполяция | Описание |
|
### 2.2 Дефолтные библиотеки (из `iron3d.dll`)
|
||||||
|----------|--------|--------|--------------|--------------------------------------|
|
|
||||||
| 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 | Нет | Дополнительный параметр |
|
|
||||||
|
|
||||||
### Маппинг компонентов на D3D Material (предположительный)
|
- `Textures.lib`
|
||||||
|
- `Material.lib`
|
||||||
|
- `LightMap.lib`
|
||||||
|
- `palettes.lib` (строка собирается как `'p' + "alettes.lib"`)
|
||||||
|
|
||||||
По аналогии со стандартной структурой `D3DMATERIAL7`:
|
### 2.3 Ключевые runtime-хранилища
|
||||||
|
|
||||||
| Компонент | Вероятное назначение | Биты интерполяции |
|
1. Менеджер материалов (`LoadMatManager`) — объект `0x470` байт.
|
||||||
|--------------|----------------------|-------------------|
|
2. Кэш текстурных объектов.
|
||||||
| A (+4..+12) | Diffuse (RGB) | 0x02 |
|
3. Кэш lightmap-объектов.
|
||||||
| B (+20..+28) | Ambient (RGB) | 0x01 |
|
4. Банк загруженных палитр.
|
||||||
| C (+36..+44) | Specular (RGB) | 0x04 |
|
5. Глобальный пул определений материалов (`MAT0`).
|
||||||
| D (+52..+60) | Emissive (RGB) | 0x08 |
|
|
||||||
| (+32) | Specular power | 0x10 |
|
|
||||||
|
|
||||||
### Поле textureIndex (+68)
|
---
|
||||||
|
|
||||||
- Значение `< 0` означает «нет текстуры» → `texture_ptr = NULL`.
|
## 3. Layout `MatManager` (0x470)
|
||||||
- Значение `≥ 0` используется как индекс в глобальном массиве текстурных объектов: `texture = texture_array[5 * textureIndex]`.
|
|
||||||
|
|
||||||
## 2.3. Алгоритм интерполяции материалов
|
Объект содержит 70 таблиц wear/lightmaps (не 140).
|
||||||
|
|
||||||
Движок поддерживает **анимацию материалов** между ключевыми кадрами. Функция `sub_10003030`:
|
|
||||||
|
|
||||||
```
|
|
||||||
Вход: mat_a (исходный), mat_b (целевой), t (фактор 0..1), mask (битовая маска)
|
|
||||||
|
|
||||||
Выход: mat_result
|
|
||||||
|
|
||||||
Для каждого бита mask:
|
|
||||||
если бит установлен:
|
|
||||||
mat_result.component = mat_a.component + (mat_b.component - mat_a.component) × t
|
|
||||||
иначе:
|
|
||||||
mat_result.component = mat_a.component (без интерполяции)
|
|
||||||
|
|
||||||
mat_result.textureIndex = mat_a.textureIndex (всегда копируется без интерполяции)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Режимы анимации материалов
|
|
||||||
|
|
||||||
Материал может иметь несколько фаз (phase) с разными режимами цикличности:
|
|
||||||
|
|
||||||
| Режим (flags & 7) | Описание |
|
|
||||||
|-------------------|-------------------------------------|
|
|
||||||
| 0 | Цикл: повтор с начала |
|
|
||||||
| 1 | Ping‑pong: туда‑обратно |
|
|
||||||
| 2 | Однократное воспроизведение (clamp) |
|
|
||||||
| 3 | Случайный кадр (random) |
|
|
||||||
|
|
||||||
## 2.4. Глобальный массив текстур
|
|
||||||
|
|
||||||
Текстуры хранятся в глобальном массиве записей по **20 байт** (5 DWORD):
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
struct TextureSlot { // 20 байт
|
// int-индексы относительно this (DWORD*), размер 284 DWORD = 0x470
|
||||||
int32_t name_hash; // +0: Хэш/ID имени текстуры (-1 = свободен)
|
// [0] vtable
|
||||||
void* texture_object; // +4: Указатель на объект текстуры D3D
|
// [1] callback iface
|
||||||
int32_t ref_count; // +8: Счётчик ссылок
|
// [2] callback data
|
||||||
uint32_t last_release; // +12: Время последнего Release
|
// [3..72] wearTablePtrs[70] // ptr на массив по 8 байт
|
||||||
uint32_t extra; // +16: Дополнительный флаг
|
// [73..142] wearCounts[70]
|
||||||
|
// [143] tableCount
|
||||||
|
// [144..213] lightmapTablePtrs[70] // ptr на массив по 4 байта
|
||||||
|
// [214..283] lightmapCounts[70]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Vtable методов (`off_100209E4`)
|
||||||
|
|
||||||
|
| Индекс | Функция | Назначение |
|
||||||
|
|---:|---|---|
|
||||||
|
| 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` для таблицы |
|
||||||
|
|
||||||
|
### 3.2 Кодирование material-handle
|
||||||
|
|
||||||
|
`uint32 handle = (tableIndex << 16) | wearIndex`.
|
||||||
|
|
||||||
|
- `HIWORD(handle)` -> индекс таблицы `0..69`
|
||||||
|
- `LOWORD(handle)` -> индекс материала в wear-таблице
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Глобальные кэши и их ёмкость
|
||||||
|
|
||||||
|
Ёмкости подтверждены границами циклов/адресов в дизассемблере.
|
||||||
|
|
||||||
|
### 4.1 Кэш текстур (`dword_1014E910`...)
|
||||||
|
|
||||||
|
- Размер слота: `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 флаги загрузки
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Функция `UnloadAllTextures` обнуляет все слоты, вызывая деструктор для каждого ненулевого `texture_object`.
|
### 4.2 Кэш lightmaps (`dword_10029C98`...)
|
||||||
|
|
||||||
## 2.5. Глобальный массив определений материалов
|
- Тот же layout `5 DWORD`
|
||||||
|
- Ёмкость: `100`
|
||||||
|
|
||||||
Определения материалов хранятся в глобальном массиве записей по **368 байт** (92 DWORD):
|
### 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 вариантов)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Загрузка палитр (`sub_10002B40`)
|
||||||
|
|
||||||
|
### 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
|
```c
|
||||||
struct MaterialDef { // 368 байт (92 DWORD)
|
struct Mat0Payload {
|
||||||
int32_t name_hash; // dword_100669F0[92*i]: -1 = свободен
|
uint16_t phaseCount;
|
||||||
int32_t ref_count; // dword_100669F4[92*i]: Счётчик ссылок
|
uint16_t animBlockCount; // должно быть < 20, иначе "Too many animations for material."
|
||||||
int32_t phase_count; // dword_100669F8[92*i]: Число текстурных фаз
|
|
||||||
void* record_ptr; // dword_100669FC[92*i]: Указатель на массив записей по 76 байт
|
// Если attr2 >= 2:
|
||||||
int32_t anim_phase_count; // dword_10066A00[92*i]: Число фаз анимации
|
uint8_t metaA8;
|
||||||
// +20..+367: данные фаз анимации (до 22 фаз × 16 байт)
|
uint8_t metaB8;
|
||||||
|
// Если attr2 >= 3:
|
||||||
|
uint32_t metaC32;
|
||||||
|
// Если attr2 >= 4:
|
||||||
|
uint32_t metaD32;
|
||||||
|
|
||||||
|
PhaseRecordByte34 phases[phaseCount];
|
||||||
|
AnimBlockRaw anim[animBlockCount];
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
## 2.6. Переключатели рендера (из Ngi32.dll)
|
Если `attr2 < 2`, runtime-значения по умолчанию:
|
||||||
|
|
||||||
Движок читает настройки из реестра Windows (`HKCU\Software\Nikita\NgiTool`). Подтверждённые ключи:
|
- `metaA = 255`
|
||||||
|
- `metaB = 255`
|
||||||
|
- `metaC = 1.0f` (`0x3F800000`)
|
||||||
|
- `metaD = 0`
|
||||||
|
|
||||||
| Ключ реестра | Глобальная переменная | Описание |
|
### 6.3 `PhaseRecordByte34` -> runtime `76 bytes`
|
||||||
|--------------------------|-----------------------|---------------------------------|
|
|
||||||
| `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 и их влияние на рендер
|
Сырые 34 байта:
|
||||||
|
|
||||||
| ForceCpu | Force SSE | Force 3DNow | Force FXCH | Force MMX |
|
```c
|
||||||
|----------|-----------|-------------|------------|-----------|
|
struct PhaseRecordByte34 {
|
||||||
| 2 | Да | Нет | Нет | Нет |
|
uint8_t p[18]; // параметры
|
||||||
| 3 | Нет | Да | Нет | Нет |
|
char textureName[16];// если textureName[0]==0, текстуры нет
|
||||||
| 4 | Да | Да | Нет | Нет |
|
};
|
||||||
| 5 | Да | Да | Да | Да |
|
```
|
||||||
| 6 | Да | Да | Да | Нет |
|
|
||||||
| 7 | Нет | Нет | Нет | Да |
|
|
||||||
|
|
||||||
### Практические выводы для порта
|
Преобразование в runtime-структуру (точный порядок):
|
||||||
|
|
||||||
Движок спроектирован для работы **без** следующих функций (graceful degradation):
|
| Из `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]` |
|
||||||
|
|
||||||
- Мипмапы.
|
Текстура:
|
||||||
- Bilinear/trilinear фильтрация.
|
|
||||||
- Мультитекстурирование (2‑й текстурный слой).
|
- `textureName[0] == 0` -> `runtime[+68] = -1` и `runtime[+72] = -1`
|
||||||
- 32‑битные текстуры (fallback на 16‑бит).
|
- иначе `runtime[+68] = LoadTexture(textureName, flags)`
|
||||||
- Аппаратный T&L (software fallback).
|
|
||||||
|
### 6.4 Runtime-запись фазы (76 байт)
|
||||||
|
|
||||||
|
```c
|
||||||
|
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
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.5 Анимационные блоки (`animBlockCount`, максимум 19)
|
||||||
|
|
||||||
|
Каждый блок в payload:
|
||||||
|
|
||||||
|
```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];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime-представление блока = 16 байт:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct AnimBlockRuntime {
|
||||||
|
uint32_t mode; // headerRaw & 7
|
||||||
|
uint32_t interpMask;// headerRaw >> 3
|
||||||
|
int32_t keyCount;
|
||||||
|
void* keysPtr; // массив keyCount * 8
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Ключи в runtime занимают 8 байт/ключ (с расширением `k0` до `uint32`).
|
||||||
|
|
||||||
|
`k2` в `sub_100031F0/sub_10003680` не используется.
|
||||||
|
|
||||||
|
### 6.6 Поиск и fallback
|
||||||
|
|
||||||
|
При `LoadMaterial(name)`:
|
||||||
|
|
||||||
|
- сначала точный поиск в `Material.lib`;
|
||||||
|
- при промахе лог: `"Material %s not found."`;
|
||||||
|
- fallback на `DEFAULT`;
|
||||||
|
- если и `DEFAULT` не найден, берётся индекс `0`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2.7. Текстовый файл WEAR + LIGHTMAPS (World3D.dll)
|
## 7. Выбор текущей material-фазы
|
||||||
|
|
||||||
`World3D.dll` содержит парсер текстового файла (режим `rt`), который задаёт:
|
### 7.1 Интерполяция (`sub_10003030`)
|
||||||
|
|
||||||
- список **материалов (wear)**, используемых в сцене/объекте;
|
Интерполируются только следующие поля (по `interpMask`):
|
||||||
- список **лайтмап (lightmaps)**.
|
|
||||||
|
|
||||||
Формат читается через `fgets`/`sscanf`/`fscanf`, поэтому он чувствителен к структуре строк и ключевому слову `LIGHTMAPS`.
|
- bit `0x02`: `+4,+8,+12`
|
||||||
|
- bit `0x01`: `+20,+24,+28`
|
||||||
|
- bit `0x04`: `+36,+40,+44`
|
||||||
|
- bit `0x08`: `+52,+56,+60`
|
||||||
|
- bit `0x10`: `+32`
|
||||||
|
|
||||||
### 2.7.1. Блок WEAR (материалы)
|
Не интерполируются и копируются из «текущей» фазы:
|
||||||
|
|
||||||
1) **Первая строка файла** — целое число:
|
- `+0,+16,+48,+64,+68,+72`
|
||||||
|
|
||||||
- `wearCount` (обязательно `> 0`, иначе ошибка `"Illegal wear length."`)
|
### 7.2 Выбор по времени (`sub_100031F0`)
|
||||||
|
|
||||||
2) Далее следует `wearCount` строк. Каждая строка имеет вид:
|
Вход:
|
||||||
|
|
||||||
- `<int> <пробелы> <materialName>`
|
- `handle` (`tableIndex|wearIndex`)
|
||||||
|
- `animBlockIndex`
|
||||||
|
- глобальное время `SetGameTime()` (`dword_10032A38`)
|
||||||
|
|
||||||
Где:
|
Для каждой wear-записи хранится `startTime` (второй DWORD пары `8-byte`).
|
||||||
|
|
||||||
- `<int>` парсится, но фактически не используется как ключ (движок обрабатывает записи последовательно).
|
Режимы `mode = headerRaw & 7`:
|
||||||
- `<materialName>` — имя материала, которое движок ищет в менеджере материалов.
|
|
||||||
- Если материал не найден, пишется `"Material %s not found."` и используется fallback `"DEFAULT"`.
|
|
||||||
|
|
||||||
> Практическая рекомендация для инструментов: считайте `<int>` как необязательный “legacy-id”, а истинным идентификатором материала делайте строку `<materialName>`.
|
- `0`: loop
|
||||||
|
- `1`: ping-pong
|
||||||
|
- `2`: one-shot clamp
|
||||||
|
- `3`: random (`rand() % cycleLength`)
|
||||||
|
|
||||||
### 2.7.2. Блок LIGHTMAPS
|
После выбора сегмента интерполяции `sub_10003030` строит scratch-материал (`unk_1013B300`), который возвращается через out-параметр.
|
||||||
|
|
||||||
После чтения wear-списка движок последовательно читает токены (`fscanf("%s")`) до тех пор, пока не встретит слово **`LIGHTMAPS`**.
|
### 7.3 Выбор по нормализованному `t` (`sub_10003680`)
|
||||||
|
|
||||||
Затем:
|
Аналогично `sub_100031F0`, но time берётся как `t * cycleLength`.
|
||||||
|
|
||||||
1) Читается `lightmapCount`:
|
### 7.4 Сброс времени записи
|
||||||
|
|
||||||
- `lightmapCount` (обязательно `> 0`, иначе ошибка `"Illegal lightmaps length."`)
|
`sub_10003AE0` обновляет `startTime` конкретной wear-записи значением текущего `SetGameTime()`.
|
||||||
|
|
||||||
2) Далее следует `lightmapCount` строк вида:
|
|
||||||
|
|
||||||
- `<int> <пробелы> <lightmapName>`
|
|
||||||
|
|
||||||
Где:
|
|
||||||
|
|
||||||
- `<int>` парсится, но фактически не используется как ключ (аналогично wear).
|
|
||||||
- `<lightmapName>` — имя лайтмапы; если ресурс не найден, пишется `"LightMap %s not found."`.
|
|
||||||
|
|
||||||
### 2.7.3. Валидация имени лайтмапы (деталь движка)
|
|
||||||
|
|
||||||
Перед загрузкой лайтмапы выполняется проверка имени:
|
|
||||||
|
|
||||||
- в имени должна встречаться точка `.` **в пределах первых 16 символов**, иначе ошибка `"Bad texture name."`;
|
|
||||||
- далее движок использует подстроку после точки в вычислениях внутренних индексов/кэша (на практике полезно придерживаться шаблона вида `NAME.A1`, `NAME.B2` и т.п.).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
## 2.8. Формат текстурного ассета `Texm` (Ngi32.dll)
|
|
||||||
|
|
||||||
Текстуры из `Textures.lib` хранятся как NRes‑entries типа `0x6D786554` (`"Texm"`).
|
## 8. Формат `WEAR` (текст)
|
||||||
|
|
||||||
### 2.8.1. Заголовок `Texm` (32 байта)
|
`WEAR` хранится как текст в NRes entry типа `WEAR` (`0x52414557`), обычно имя `*.wea`.
|
||||||
|
|
||||||
|
### 8.1 Грамматика
|
||||||
|
|
||||||
|
```text
|
||||||
|
<wearCount:int>\n
|
||||||
|
<legacyId:int> <materialName>\n // повторить wearCount раз
|
||||||
|
|
||||||
|
[LIGHTMAPS\n
|
||||||
|
<lightmapCount:int>\n
|
||||||
|
<legacyId:int> <lightmapName>\n // повторить lightmapCount раз]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<legacyId>` читается, но как ключ не используется.
|
||||||
|
- Идентификатором реально является имя (`materialName` / `lightmapName`).
|
||||||
|
|
||||||
|
### 8.2 Парсеры
|
||||||
|
|
||||||
|
1. `sub_10003B10`: файл/ресурсный режим.
|
||||||
|
2. `sub_10003F80`: парсер из строкового буфера.
|
||||||
|
|
||||||
|
### 8.3 Поведение и ошибки
|
||||||
|
|
||||||
|
- `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`
|
||||||
|
|
||||||
|
### 8.4 Ограничения runtime
|
||||||
|
|
||||||
|
- Таблиц в `MatManager`: максимум 70 (физический layout).
|
||||||
|
- Жёсткой проверки на overflow таблиц в `sub_10003B10/sub_10003F80` нет.
|
||||||
|
|
||||||
|
Инструментам нужно явно валидировать `tableCount < 70`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Загрузка texture/lightmap по имени
|
||||||
|
|
||||||
|
Общие функции:
|
||||||
|
|
||||||
|
- `sub_10004B10` — texture (`Textures.lib`)
|
||||||
|
- `sub_10004CB0` — lightmap (`LightMap.lib`)
|
||||||
|
|
||||||
|
### 9.1 Валидация имени
|
||||||
|
|
||||||
|
Алгоритм требует наличие `'.'` в позиции `0..16`.
|
||||||
|
|
||||||
|
Иначе:
|
||||||
|
|
||||||
|
- `"Bad texture name."`
|
||||||
|
- возврат `-1`
|
||||||
|
|
||||||
|
### 9.2 Palette index из суффикса
|
||||||
|
|
||||||
|
После точки разбирается:
|
||||||
|
|
||||||
|
- `L = toupper(name[dot+1])`
|
||||||
|
- `D = name[dot+2]` (опционально)
|
||||||
|
- `idx = (L - 'A') * 11 + (D ? (D - '0' + 1) : 0)`
|
||||||
|
|
||||||
|
Если `idx < 0`, палитра не подставляется (`0`).
|
||||||
|
|
||||||
|
Практически в стоковых ассетах имена часто вида `NAME.0`; это даёт `idx < 0`, т.е. без палитровой привязки.
|
||||||
|
|
||||||
|
### 9.3 Кэширование
|
||||||
|
|
||||||
|
- Дедупликация по `resIndex`.
|
||||||
|
- При повторном запросе увеличивается `refCount`, `lastZeroRefTime` сбрасывается в `0`.
|
||||||
|
- При освобождении материала `refCount` texture/lightmap уменьшается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Формат `Texm`
|
||||||
|
|
||||||
|
### 10.1 Заголовок 32 байта
|
||||||
|
|
||||||
```c
|
```c
|
||||||
struct TexmHeader32 {
|
struct TexmHeader32 {
|
||||||
uint32_t magic; // 0x6D786554 ('Texm')
|
uint32_t magic; // 'Texm' = 0x6D786554
|
||||||
uint32_t width; // base width
|
uint32_t width;
|
||||||
uint32_t height; // base height
|
uint32_t height;
|
||||||
uint32_t mipCount; // количество уровней
|
uint32_t mipCount;
|
||||||
uint32_t flags4; // наблюдаются 0 или 32
|
uint32_t flags4;
|
||||||
uint32_t flags5; // наблюдаются 0 или 0x04000000
|
uint32_t flags5;
|
||||||
uint32_t unk6; // служебное поле (часто 0, иногда ненулевое)
|
uint32_t unk6;
|
||||||
uint32_t format; // код пиксельного формата
|
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`);
|
- `556`
|
||||||
2) далее mip-chain пикселей;
|
- `88`
|
||||||
3) опционально chunk атласа `Page`.
|
|
||||||
|
|
||||||
Размер mip-chain:
|
### 10.3 Layout payload
|
||||||
|
|
||||||
|
1. `TexmHeader32`
|
||||||
|
2. если `format == 0`: palette table `256 * 4 = 1024` байта
|
||||||
|
3. mip-chain пикселей
|
||||||
|
4. опциональный `Page` chunk
|
||||||
|
|
||||||
|
Расчёт:
|
||||||
|
|
||||||
```c
|
```c
|
||||||
bytesPerPixel = (format == 0 ? 1 : format in {565,556,4444} ? 2 : 4);
|
bytesPerPixel =
|
||||||
pixelBytes = bytesPerPixel * sum_{i=0..mipCount-1}(max(1,width>>i) * max(1,height>>i));
|
(format == 0) ? 1 :
|
||||||
|
(format == 565 || format == 556 || format == 4444 || format == 88) ? 2 :
|
||||||
|
4;
|
||||||
|
|
||||||
|
pixelCount = sum_{i=0..mipCount-1}(max(1, width>>i) * max(1, height>>i));
|
||||||
|
sizeCore = 32 + (format == 0 ? 1024 : 0) + bytesPerPixel * pixelCount;
|
||||||
```
|
```
|
||||||
|
|
||||||
Итого «чистый» размер без `Page`:
|
### 10.4 `Page` chunk
|
||||||
|
|
||||||
```c
|
|
||||||
sizeCore = 32 + (format == 0 ? 1024 : 0) + pixelBytes;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.8.3. Опциональный `Page` chunk
|
|
||||||
|
|
||||||
Если после `sizeCore` остаются байты и в этой позиции стоит magic `"Page"` (`0x65676150`), парсер `sub_1000FF60` читает таблицу subrect:
|
|
||||||
|
|
||||||
```c
|
```c
|
||||||
struct PageChunk {
|
struct PageChunk {
|
||||||
uint32_t magic; // 'Page'
|
uint32_t magic; // 'Page'
|
||||||
uint32_t count;
|
uint32_t rectCount;
|
||||||
struct Rect16 {
|
struct Rect16 {
|
||||||
int16_t x;
|
int16_t x;
|
||||||
int16_t w;
|
int16_t w;
|
||||||
int16_t y;
|
int16_t y;
|
||||||
int16_t h;
|
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-таблицу.
|
||||||
В `sub_1000FB30` palette конвертируется в локальную 32-bit таблицу; байты источника читаются как BGR-порядок (четвёртый байт входной записи не используется напрямую в базовом пути), итоговая alpha зависит от флагов runtime-конфига.
|
- Считает `sizeCore`, находит tail.
|
||||||
|
- `Page` разбирается только если включён флаг загрузки `0x400000` и tail содержит `Page`.
|
||||||
### 2.8.5. Проверка на реальных данных
|
- Может уменьшать стартовый mip (`sub_1000F580`) в зависимости от размеров/формата/флагов.
|
||||||
|
- При `DisableMipmap == 0` и допустимых условиях может строить mips в runtime.
|
||||||
Для всех 393 entries в `Textures.lib`:
|
|
||||||
|
|
||||||
- `magic == 'Texm'`;
|
|
||||||
- размеры совпадают с `sizeCore` либо `sizeCore + PageChunk (+pad до 8 байт NRes)`;
|
|
||||||
- при наличии хвоста в `sizeCore` всегда обнаруживается валидный `Page` chunk.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 11. Флаги профиля/рендера (Ngi32)
|
||||||
|
|
||||||
|
Ключ реестра: `HKCU\Software\Nikita\NgiTool`.
|
||||||
|
|
||||||
|
Подтверждённые значения:
|
||||||
|
|
||||||
|
- `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)
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user