- 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.
18 KiB
FXID
Документ описывает формат ресурса эффекта FXID, контракт runtime в Effect.dll и практические правила для инструментов чтения/конвертации/редактирования.
Цель: дать достаточную high-level спецификацию для:
- 1:1 загрузчика/рантайма эффекта;
- валидатора payload;
- бинарно-совместимого редактора;
- конвертера в промежуточный формат и обратно.
Связанный контейнер: NRes / RsLi.
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.
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)
struct FxHeader60 {
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.
3.2. Поля header: подтверждённая семантика
cmd_count:- engine итерируется ровно
cmd_countраз; - дополнительных ограничений в оригинале нет.
- engine итерируется ровно
time_mode:- начальный runtime-mode (
effect+0x14), участвует вsub_10005C60.
- начальный runtime-mode (
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.
- копируется в runtime (
3.3. flags (header+0x10) — подтвержденные биты
| Бит | Маска | Поведение |
|---|---|---|
| 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 |
Остальные биты в движке напрямую не расшифрованы на уровне high-level, но должны сохраняться 1:1.
3.4. time_mode (header+0x04) — режимы sub_10005C60
Поддерживаются коды 0..17.
| 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.
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. Внутренний вызовной контракт команд
После создания каждой команды менеджер:
- Проставляет
enabledизcmd_word.bit8в полеobj+4. - Вызывает инициализацию команды (
vfunc +4) с аргументами(queue, manager). - Добавляет команду в массив команд эффекта.
В 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:
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):
- обновление времени manager;
- обход активных FX instances;
- для каждого инстанса
sub_10006170:- gating по
flags/queue-state; - вычисление alpha через
sub_10005C60; - вызов
sub_10008120(update/bounds/command-pass); - при необходимости
sub_10007D10(эмиссия/рендерный callback).
- gating по
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)
Рекомендуемый строгий парсер:
- проверить
len(payload) >= 60; - прочитать
cmd_count; ptr = 0x3C;- для каждой команды:
- требовать
ptr + 4 <= len; - прочитать
opcode; opcodeдолжен быть в1..10;ptr + size(opcode) <= len;ptr += size(opcode);
- требовать
- в 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:
- записать
FxHeader60; cmd_count = len(commands);- для каждой команды записать
cmd_word+ body фиксированного размера для opcode; - итоговый размер должен быть
0x3C + sum(size(opcode_i)); - без хвоста.
7.5. Конвертация в промежуточный JSON
Рекомендуемая структура для round-trip:
{
"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: min0, max81, avg5.13;duration_sec: min0.0, max60.0, avg2.46;opcodeраспределение:1: 6182: 5173: 15454: 2025: 317: 11618: 2379: 26610: 1606: не встречен, но поддержан parser.
cmd_word:bits 9..31не использованы в датасете;bit8встречается для части opcode (особенно3,7,9).
9. Известные пробелы (не блокируют 1:1 container/runtime)
- Полная человеко-читаемая семантика внутренних полей command body для каждого opcode не завершена.
- Для части битов
header.flagsесть только functional-наблюдение без финального gameplay-имени. - Высокие биты
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.