aboutsummaryrefslogtreecommitdiff
path: root/docs/specs
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs')
-rw-r--r--docs/specs/fxid.md936
1 files changed, 654 insertions, 282 deletions
diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md
index 65bf7f1..957f95b 100644
--- a/docs/specs/fxid.md
+++ b/docs/specs/fxid.md
@@ -1,382 +1,730 @@
# FXID
-Документ описывает формат ресурса эффекта `FXID`, контракт runtime в `Effect.dll` и практические правила для инструментов чтения/конвертации/редактирования.
+Документ фиксирует спецификацию ресурса эффекта `FXID` на уровне, достаточном для:
-Цель: дать достаточную high-level спецификацию для:
-
-- 1:1 загрузчика/рантайма эффекта;
-- валидатора payload;
-- бинарно-совместимого редактора;
-- конвертера в промежуточный формат и обратно.
+- 1:1 загрузки и исполнения в совместимом runtime;
+- построения валидатора payload;
+- создания lossless-конвертера (`binary -> IR -> binary`);
+- создания редактора с безопасным редактированием полей.
Связанный контейнер: [NRes / RsLi](nres.md).
---
-## 1. Источники восстановления
+## 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`.
+- runtime loop: `sub_10003D30(case 28)`, `sub_10006170`, `sub_10008120`, `sub_10007D10`;
+- alpha/time: `sub_10005C60`;
+- exports: `CreateFxManager`, `InitializeSettings`.
+
+Проверка по данным:
+
+- `923/923` FXID payload валидны в `testdata/nres`.
---
-## 2. Место формата в движке
+## 2. Контейнер и runtime API
-### 2.1. Контейнер NRes
+## 2.1. NRes entry
-Эффект хранится как запись NRes с типом:
+FXID хранится как NRes-entry:
- `type_id = 0x44495846` (`"FXID"`).
-Для всех 923 FXID-entries в `testdata/nres` подтверждено:
+Наблюдение по датасету (923 эффекта):
-- `attr1 = 0`;
-- `attr2 = 0`;
-- `attr3 = 1`.
+- `attr1 = 0`, `attr2 = 0`, `attr3 = 1`.
-### 2.2. Runtime-модуль
+## 2.2. Export API `Effect.dll`
-`Effect.dll` экспортирует 2 функции:
+Экспортируются:
- `CreateFxManager(int a1, int a2, int owner)`;
- `InitializeSettings()`.
-`CreateFxManager` выделяет объект (`0xB8` байт), инициализирует его через `sub_10003AE0`, возвращает **интерфейсный указатель** (смещение `+4` от базового объекта).
+`CreateFxManager` создаёт manager-объект (`0xB8` байт), инициализирует через `sub_10003AE0`, возвращает интерфейсный указатель (`base + 4`).
-### 2.3. COM-подобный интерфейс
+## 2.3. Интерфейс менеджера
-Внешний код (например, `Terrain.dll`) получает рабочий интерфейс через `QueryInterface(id=19)` и далее вызывает методы vtable `off_1001E478`.
+Рабочая vtable (`off_1001E478`):
-Ключевые методы интерфейса менеджера (по vtable):
-
-| Vtable offset | Функция | Назначение (high-level) |
+| Смещение | Функция | Назначение |
|---|---|---|
-| +0x10 | `sub_10004320` | Открыть/закэшировать ресурс эффекта (`archive + name`) |
-| +0x14 | `sub_10004590` | Создать runtime-инстанс эффекта по шаблону |
-| +0x18 | `sub_10004780` | Удалить инстанс по id |
-| +0x1C | `sub_100047B0` | Установить режим интерполяции/времени |
+| +0x08 | `sub_10003D30` | Event dispatcher (`4/20/23/24/28`) |
+| +0x10 | `sub_10004320` | Открыть/закэшировать FX resource |
+| +0x14 | `sub_10004590` | Создать runtime instance |
+| +0x18 | `sub_10004780` | Удалить instance |
+| +0x1C | `sub_100047B0` | Установить time/interp mode |
| +0x20 | `sub_100047D0` | Установить scale |
| +0x24 | `sub_10004830` | Установить позицию |
-| +0x28 | `sub_10004930` | Установить матрицу transform |
-| +0x2C | `sub_10004B00` | Перезапуск с mode |
-| +0x38 | `sub_10004BA0` | Модификатор длительности |
+| +0x28 | `sub_10004930` | Установить matrix transform |
+| +0x2C | `sub_10004B00` | Restart/retime |
+| +0x38 | `sub_10004BA0` | Duration modifier |
| +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) |
+| +0x44 | `sub_10004C50` | Bind emitter/context |
+| +0x48 | `sub_10004D50` | Сброс frame flags |
-Этого контракта достаточно, чтобы корректно встроить FXID-рантайм в движок.
+`Terrain.dll` использует `QueryInterface(id=19)` для получения рабочего интерфейса.
---
-## 3. Бинарный формат payload FXID
+## 3. Бинарный формат FXID payload
-Все числа little-endian.
+Все значения little-endian.
## 3.1. Header (60 байт, `0x3C`)
```c
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
+ uint32_t cmd_count; // 0x00
+ uint32_t time_mode; // 0x04
+ float duration_sec; // 0x08
+ float phase_jitter; // 0x0C
+ uint32_t flags; // 0x10
+ uint32_t settings_id; // 0x14
+ float rand_shift_x; // 0x18
+ float rand_shift_y; // 0x1C
+ float rand_shift_z; // 0x20
+ float pivot_x; // 0x24
+ float pivot_y; // 0x28
+ float pivot_z; // 0x2C
+ float scale_x; // 0x30
+ float scale_y; // 0x34
+ float scale_z; // 0x38
};
```
Командный поток начинается строго с `offset = 0x3C`.
-## 3.2. Поля header: подтверждённая семантика
-
-- `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`.
-
-## 3.3. `flags` (`header+0x10`) — подтвержденные биты
-
-| Бит | Маска | Поведение |
+## 3.2. Header-поля (подтвержденная семантика)
+
+- `cmd_count`: число команд (engine итерирует ровно столько шагов).
+- `time_mode`: базовый режим вычисления alpha/time (`sub_10005C60`).
+- `duration_sec`: в runtime -> `duration_ms = duration_sec * 1000`.
+- `phase_jitter`: используется при `flags & 0x1`.
+- `flags`: runtime-gating/alpha/visibility (см. ниже).
+- `settings_id`: в `sub_1000EC40` используется `settings_id & 0xFF`.
+- `rand_shift_*`: используется при `flags & 0x8`.
+- `pivot_*`: используется в ветках `sub_10007D10`.
+- `scale_*`: копируется в runtime scale и влияет на матрицы.
+
+## 3.3. `flags` (битовая карта)
+
+| Бит | Маска | Наблюдаемое поведение |
|---|---:|---|
-| 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`.
+| 0 | `0x0001` | Random phase jitter (`phase_jitter`) |
+| 3 | `0x0008` | Random positional shift (`rand_shift_*`) |
+| 4 | `0x0010` | Visibility/occlusion ветки |
+| 5 | `0x0020` | Triangular remap в `sub_10005C60` |
+| 6 | `0x0040` | Инверсия начального active-state |
+| 7 | `0x0080` | Day/night filter (ветка A) |
+| 8 | `0x0100` | Day/night filter (ветка B, инверсия) |
+| 9 | `0x0200` | Alpha *= normalized lifetime |
+| 10 | `0x0400` | Установка manager bit1 (`+0xA0`) |
+| 11 | `0x0800` | Изменение gating в `sub_10007D10` |
+| 12 | `0x1000` | Установка manager-state bit `0x10` |
+
+Нерасшифрованные биты должны сохраняться 1:1.
+
+## 3.4. `time_mode` (`0..17`)
+
+Обозначения (`sub_10005C60`):
+
+- `t0 = instance.start_ms`, `t1 = instance.end_ms`;
+- `tn = (now_ms - t0) / (t1 - t0)`;
+- `prev = instance.cached_alpha` (`v4+52` в дизассембле).
+
+Режимы:
+
+- `0`: constant (`instance.alpha_const`, поле `v4+40`);
+- `1`: `tn`;
+- `2`: `fract(tn)`;
+- `3`: `1 - tn`;
+- `4`: external value из queue/world API (manager `+36`, id из `this+104[a2]`);
+- `5`: `|param33.xyz| / |param17.vecA.xyz|`;
+- `6`: `param33.x / param17.vecA.x`;
+- `7`: `param33.y / param17.vecA.y`;
+- `8`: `param33.z / param17.vecA.z`;
+- `9`: `|param36.xyz| / |param17.vecB.xyz|`;
+- `10`: `param36.x / param17.vecB.x`;
+- `11`: `param36.y / param17.vecB.y`;
+- `12`: `param36.z / param17.vecB.z`;
+- `13`: `1 - external_resource_value`;
+- `14`: `1 - queue_param(49)`;
+- `15`: `max(norm(param33/vecA), norm(param36/vecB))`;
+- `16`: external (`mode 4`) с нижним clamp к `prev` (`0` не зажимается);
+- `17`: external (`mode 4`) с верхним clamp к `prev` (`1` не зажимается).
+
+Post-обработка после mode:
+
+- если `flags & 0x200`: `alpha *= tn`;
+- если `flags & 0x20`: triangular remap (`alpha = (alpha < 0.5 ? alpha : 1-alpha) * 2`).
---
## 4. Командный поток
-## 4.1. Формат записи команды
+## 4.1. Общий формат команды
+
+Каждая команда:
-Каждая команда начинается с `uint32 cmd_word`.
+- `uint32 cmd_word`;
+- далее body фиксированного размера по opcode.
-Биты:
+`cmd_word`:
- `opcode = cmd_word & 0xFF`;
- `enabled = (cmd_word >> 8) & 1`;
-- в реальных данных `bits 9..31 == 0` (но редактор должен сохранять весь word как есть).
-
-Никакого межкомандного выравнивания нет: следующая команда начинается сразу после `size(opcode)`.
+- `bits 9..31` в датасете нулевые, но их надо сохранять 1:1.
-## 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.2. Размеры
-## 4.3. Opcode -> runtime-класс
+| Opcode | Размер записи |
+|---:|---:|
+| 1 | 224 |
+| 2 | 148 |
+| 3 | 200 |
+| 4 | 204 |
+| 5 | 112 |
+| 6 | 4 |
+| 7 | 208 |
+| 8 | 248 |
+| 9 | 208 |
+| 10 | 208 |
-В `sub_10007650` для opcode создаются объекты:
+## 4.3. Opcode -> runtime-класс (vtable)
-| Opcode | `operator new` | Runtime vtable |
+| Opcode | `new(size)` | 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` |
+| 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. Общий вызовной контракт команды
-## 4.4. Внутренний вызовной контракт команд
+После создания команды (`sub_10007650`):
-После создания каждой команды менеджер:
+1. `cmd->enabled = cmd_word.bit8`.
+2. `cmd->Init(fx_queue, fx_instance)` (`vfunc +4`).
+3. команда добавляется в список инстанса.
-1. Проставляет `enabled` из `cmd_word.bit8` в поле `obj+4`.
-2. Вызывает инициализацию команды (`vfunc +4`) с аргументами `(queue, manager)`.
-3. Добавляет команду в массив команд эффекта.
+В runtime cycle:
-В update-cycle менеджер вызывает:
-
-- `vfunc +8`: вычисление/обновление команды (bool);
-- `vfunc +12`: callback при render/emission;
-- `vfunc +20`: toggle активности;
-- `vfunc +24`: обновление transform-context (для части opcode no-op).
+- `vfunc +8`: update/compute (bool);
+- `vfunc +12`: emission/render callback;
+- `vfunc +20`: toggle active;
+- `vfunc +16`/`+24`: служебные функции (зависят от opcode).
---
-## 5. Алгоритм загрузки FXID (engine-accurate)
+## 5. Загрузка FXID (engine-accurate)
-Псевдокод `sub_10007650`:
+`sub_10007650`:
```c
void FxLoad(FxInstance* fx, uint8_t* payload) {
FxHeader60* h = (FxHeader60*)payload;
- fx->raw_header_ptr = h;
+ fx->raw_header = 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->end_ms = fx->start_ms + h->duration_sec * 1000.0f;
+ 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++) {
+ 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) {
+ Command* cmd = CreateByOpcode(op, ptr); // может вернуть null
+ if (cmd) {
cmd->enabled = (w >> 8) & 1;
- if (h->flags & 0x400)
- fx->manager_flags |= 0x0100; // внутренний bit
-
- if ((h->flags & 0x400) || cmd->enabled)
- fx->manager_flags |= 0x0010;
+ if (h->flags & 0x400) fx->manager_flags |= 0x0100;
+ if ((h->flags & 0x400) || cmd->enabled) fx->manager_flags |= 0x0010;
- cmd->Attach(fx->queue, fx);
+ cmd->Init(fx->queue, fx);
fx->commands.push_back(cmd);
}
- ptr += size_by_opcode(op); // в оригинале без checks
+ ptr += size_by_opcode(op); // без bounds checks в оригинале
}
}
```
-Поведение оригинала, важное для 1:1:
+Критичные edge-case оригинала:
+
+- bounds checks отсутствуют;
+- при unknown opcode `ptr` не двигается (`advance = 0`);
+- при `new == null` команда пропускается, но `ptr` двигается.
-- проверок границ буфера нет;
-- при `unknown opcode` указатель `ptr` не двигается (счётчик цикла движется);
-- при `new == null` команда пропускается, но `ptr` двигается на размер opcode.
+Фактический `advance` в `sub_10007650` задан hardcoded в DWORD:
-Для toolchain рекомендуется **строгий** и **безопасный** парсер (см. раздел 7).
+- `op1:+56`, `op2:+37`, `op3:+50`, `op4:+51`, `op5:+28`,
+- `op6:+1`, `op7:+52`, `op8:+62`, `op9:+52`, `op10:+52`,
+- `default:+0`.
---
-## 6. Runtime-жизненный цикл эффекта
+## 6. Runtime lifecycle
-## 6.1. Инициализация
+- `sub_10007470`: ctor instance.
+- `sub_10003D30(case 28)`: per-frame update manager.
+- `sub_10006170`: gate + alpha/time + command updates.
+- `sub_10008120` / `sub_10007D10`: update/render branches.
+- Start/Stop: `sub_10004BD0` / `sub_10004C10`.
-- `sub_10007470`: конструктор instance;
-- инициализируются матрицы/scale/флаги;
-- начальный `mode` берётся из header.
+Event-codes `sub_10003D30`:
-## 6.2. Tick и обновление
+- `4`: bootstrap/time init;
+- `20`: range-removal + index repair;
+- `23`: set manager bit0;
+- `24`: clear manager bit0;
+- `28`: main tick.
+
+---
-Основной тик идёт через `sub_10003D30(case 28)`:
+## 7. Общий тип `ResourceRef64`
-1. обновление времени manager;
-2. обход активных FX instances;
-3. для каждого инстанса `sub_10006170`:
- - gating по `flags`/queue-state;
- - вычисление alpha через `sub_10005C60`;
- - вызов `sub_10008120` (update/bounds/command-pass);
- - при необходимости `sub_10007D10` (эмиссия/рендерный callback).
+Для opcode `2/3/4/5/7/8/9/10` присутствует ссылка вида:
-## 6.3. Start/Stop/Restart API
+```c
+struct ResourceRef64 {
+ char archive[32]; // null-terminated ASCII, case-insensitive compare
+ char name[32]; // null-terminated ASCII
+};
+```
-- Start: `sub_10004BD0` -> `sub_10007A30(..., 1, now)`;
-- Stop: `sub_10004C10` -> `sub_10007A30(..., 0, now)`;
-- Restart/retime: `sub_10004B00`, `sub_10004BA0`.
+Поведение loader'а:
-## 6.4. Manager event-codes (`sub_10003D30`)
+- оба имени обязаны быть непустыми;
+- кэширование по `(_strcmpi archive, _strcmpi name)`;
+- загрузка/резолв через manager resource API.
-Обработанные коды:
+Наблюдение по данным:
-- `4`: bootstrap + установка текущего времени;
-- `20`: удаление диапазона объектов в queue и корректировка индексов;
-- `23`: выставить manager-flag bit0;
-- `24`: сбросить manager-flag bit0;
-- `28`: основной per-frame update.
+- для `opcode 2`: обычно `sounds.lib` + `*.wav`;
+- для остальных: обычно `material.lib` + material name.
---
-## 7. Спецификация для инструментов
+## 8. Полная карта body по opcode (field-level)
-## 7.1. Reader (strict)
+Смещения указаны от начала команды (включая `cmd_word`).
-Рекомендуемый строгий парсер:
+## 8.1. Opcode 1 (`off_1001E78C`, size=224)
-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`.
+- init: `sub_1000F4B0`;
+- update: `sub_1000F6E0`;
+- emit: `nullsub_2`;
+- toggle: `sub_1000F490`.
-## 7.2. Reader (engine-compatible)
+```c
+struct FxCmd01 {
+ uint32_t word; // +0
+ uint32_t mode; // +4 (enum, см. ниже)
+ float t_start; // +8
+ float t_end; // +12
-Для byte-level совместимости с оригиналом можно поддержать legacy-режим:
+ float p0_min[3]; // +16..24
+ float p0_max[3]; // +28..36
-- без bounds-check (как `Effect.dll`);
-- с toleration на `unknown opcode` (но это потенциально unsafe).
+ float p1_min[3]; // +40..48
+ float p1_max[3]; // +52..60
-## 7.3. Editor (без потери совместимости)
+ float q0_min[4]; // +64..76
+ float q0_max[4]; // +80..92
-Безопасные операции:
+ float q0_rand_span[4]; // +96..108 (все 4 читаются в sub_1000F6E0)
-- менять `header`-поля (mode, duration, flags, scale, pivot);
-- менять `enabled` через `cmd_word.bit8`;
-- удалять/вставлять команды с корректным пересчётом `cmd_count` и сдвигом stream;
-- сохранять command-body как opaque bytes, если нет полного field-level декодера.
+ float scalar_min; // +112
+ float scalar_max; // +116
+ float scalar_rand_amp; // +120
-Правила:
+ float color_rgb[3]; // +124..132 (вызов manager+16)
+
+ float opaque_tail6[6]; // +136..156 (сохранять 1:1; в датасете почти всегда 0)
+
+ char opt_archive[32]; // +160..191 (редко, напр. "material.lib")
+ char opt_name[32]; // +192..223 (редко, напр. "light_w")
+};
+```
+
+Замечания по полям op1:
+
+- `+108` не резерв: участвует в random-выборке как 4-я компонента блока `+96..108`;
+- `+136..156` не читается vtable-методами класса `off_1001E78C` в `Effect.dll` (init/update/toggle/accessor), но должно сохраняться 1:1;
+- редкий кейс с ненулевыми `+136..156` и строками `+160/+192` зафиксирован в `effects.rlb:r_lightray_w`.
+
+`mode` (`+4`) -> параметры вызова manager (`sub_1000F4B0`):
+
+- `1 -> create_kind=1, flags=0x80000000`;
+- `2/5 -> create_kind=1, flags=0x00000000`;
+- `3 -> create_kind=3, flags=0x00000000`;
+- `4 -> create_kind=4, flags=0x00000000`;
+- `6 -> create_kind=1, flags=0xA0000000`;
+- `7 -> create_kind=1, flags=0x20000000`.
+
+## 8.2. Opcode 2 (`off_1001F048`, size=148)
+
+Основные методы:
+
+- init: `sub_10012D10`;
+- update: `sub_10012EB0`;
+- emit: `nullsub_2`;
+- toggle: `sub_10013170`.
+
+```c
+struct FxCmd02 {
+ uint32_t word; // +0
+ uint32_t mode; // +4 (0..3; влияет на sub_100065A0 mapping)
+ float t_start; // +8
+ float t_end; // +12
+
+ float a_min[3]; // +16..24
+ float a_max[3]; // +28..36
+
+ float b_min[3]; // +40..48
+ float b_max[3]; // +52..60
+
+ float c0_base; // +64
+ float c1_base; // +68
+ float c2_base; // +72
+ float c2_max; // +76
+
+ uint32_t param_910; // +80 (передаётся в manager cmd=910)
+
+ ResourceRef64 ref; // +84..147 (обычно sounds.lib + wav)
+};
+```
+
+`mode` -> внутренний map в `sub_100065A0`:
+
+- `0 -> 0`, `1 -> 512`, `2 -> 2`, `3 -> 514`.
+
+## 8.3. Opcode 3 (`off_1001E770`, size=200)
+
+Методы:
+
+- init: `sub_100103B0`;
+- update: `sub_100105F0`;
+- emit: `sub_100106C0`.
+
+```c
+struct FxCmd03 {
+ uint32_t word; // +0
+ uint32_t mode; // +4
+
+ float alpha_source; // +8 (>=0: norm time, <0: global time)
+ float alpha_pow_a; // +12
+ float alpha_pow_b; // +16
+
+ float out_min; // +20
+ float out_max; // +24
+ float out_pow; // +28
+
+ float active_t0; // +32
+ float active_t1; // +36
+
+ float v0_min[3]; // +40..48
+ float v0_max[3]; // +52..60
+
+ float pow0[3]; // +64..72
+
+ float v1_min[3]; // +76..84
+ float v1_max[3]; // +88..96
+
+ float v2_min[3]; // +100..108
+ float v2_max[3]; // +112..120
+
+ float pow1[3]; // +124..132
+
+ ResourceRef64 ref; // +136..199
+};
+```
+
+## 8.4. Opcode 4 (`off_1001E754`, size=204)
+
+Layout как opcode 3 + последний коэффициент:
+
+```c
+struct FxCmd04 {
+ FxCmd03 base; // +0..199
+ float dist_norm_inv_base; // +200 (используется в sub_100108C0/100109B0)
+};
+```
+
+`sub_100108C0`: `obj->inv = 1.0 / raw[200]`.
+
+## 8.5. Opcode 5 (`off_1001E360`, size=112)
+
+Методы:
+
+- init: `sub_100028A0`;
+- update: `sub_10002A20`;
+- emit: `sub_10002BE0`;
+- context update: `sub_10003070`.
+
+```c
+struct FxCmd05 {
+ uint32_t word; // +0
+ uint32_t mode; // +4 (в данных обычно 1)
+ uint32_t unused_08; // +8 (в текущем коде opcode5 не читается)
+ uint32_t unused_0C; // +12 (в текущем коде opcode5 не читается)
+
+ float active_t0; // +16
+ uint32_t max_segments; // +20
+ float active_t1_min; // +24
+ float active_t1_max; // +28
+
+ float step_norm; // +32
+ float segment_len; // +36
+ float alpha_source; // +40 (>=0 norm, <0 random)
+ float alpha_pow; // +44
+
+ ResourceRef64 ref; // +48..111
+};
+```
+
+## 8.6. Opcode 6 (`off_1001E738`, size=4)
+
+Только `cmd_word`:
+
+```c
+struct FxCmd06 {
+ uint32_t word; // +0
+};
+```
+
+`init/update/emit` фактически no-op (`sub_100030B0` возвращает `0`).
-- всегда little-endian;
-- не менять размеры записей opcode;
-- не вставлять padding между командами;
-- для неизвестных битов `cmd_word` и `header.flags` использовать copy-through.
+## 8.7. Opcode 7 (`off_1001E228`, size=208)
-## 7.4. Writer (canonical)
+Методы:
-Каноническая сборка payload:
+- init: `sub_10001720`;
+- update: `sub_10001230`;
+- emit: `sub_10001300`;
+- element accessor: `sub_10002780`.
+
+```c
+struct FxCmd07 {
+ uint32_t word; // +0
+ uint32_t mode; // +4
+
+ float eval_min; // +8
+ float eval_max; // +12
+ float eval_pow; // +16
+
+ float active_t0; // +20
+ float active_t1; // +24
+
+ float phase_span; // +28
+ float phase_rate; // +32
+
+ uint32_t count_a; // +36
+ uint32_t count_b; // +40
+
+ float set0_min[3]; // +44..52
+ float set0_max[3]; // +56..64
+ float set0_rand[3]; // +68..76
+ float set0_pow[3]; // +80..88
+
+ float set1_min[3]; // +92..100
+ float set1_max[3]; // +104..112
+ float set1_rand[3]; // +116..124
+ float set1_pow[3]; // +128..136
+
+ float gravity_or_drag_k; // +140
+
+ ResourceRef64 ref; // +144..207
+};
+```
+
+## 8.8. Opcode 8 (`off_1001E71C`, size=248)
+
+Методы:
+
+- init: `sub_10011230`;
+- update: `sub_100115C0`;
+- emit: `sub_10012030`.
+
+```c
+struct FxCmd08 {
+ uint32_t word; // +0
+ uint32_t mode; // +4
+
+ float eval_t0; // +8
+ float eval_t1; // +12
+
+ float gate_t0; // +16
+ float gate_t1; // +20
+
+ float period_min; // +24
+ float period_max; // +28
+ float phase_pow; // +32
+
+ uint32_t slots; // +36
+
+ float set0_min[3]; // +40..48
+ float set0_max[3]; // +52..60
+ float set0_rand[3]; // +64..72
+
+ float set1_min[3]; // +76..84
+ float set1_max[3]; // +88..96
+ float set1_rand[3]; // +100..108
+
+ float set2_rand[3]; // +112..120
+ float set2_pow[3]; // +124..132
+
+ float rmax_set0[3]; // +136..144 (bound/radius calc)
+ float rmax_set1[3]; // +148..156 (bound/radius calc)
+ float rmax_set2[3]; // +160..168 (bound/radius calc)
+
+ float render_pow[3]; // +172..180
+
+ ResourceRef64 ref; // +184..247
+};
+```
+
+## 8.9. Opcode 9 (`off_1001E700`, size=208)
+
+Layout как opcode 3 с двумя final-полями:
+
+```c
+struct FxCmd09 {
+ FxCmd03 base; // +0..199
+ uint32_t render_kind; // +200 (0/1/2 -> 3/5/6 in sub_100138C0)
+ uint32_t render_flag; // +204 (0 -> добавляет bit 0x08000000)
+};
+```
+
+Методы:
+
+- init/update как у opcode 3 (`sub_100103B0`, `sub_100105F0`);
+- emit: `sub_100138C0` -> формирует код рендера и вызывает `sub_100106C0`.
+
+## 8.10. Opcode 10 (`off_1001E24C`, size=208)
+
+Body-layout совпадает с opcode 7 (`FxCmd07`), но другой runtime класс.
+
+- init: `sub_10001A40`;
+- update: `sub_10001230`;
+- emit: `sub_10001300`;
+- element accessor: `sub_10002830`.
+
+Наблюдение по данным:
+
+- `mode` (`+4`) встречается как `16` или `32`.
+
+---
+
+## 9. Runtime-специфика по opcode (важные отличия)
+
+## 9.1. Opcode 1
+
+- создаёт handle через manager (`vfunc +48`);
+- задаёт флаги handle (`vfunc +52`);
+- в update пушит:
+ - позиционный вектор 1 (`vfunc +32`),
+ - позиционный вектор 2 (`vfunc +36`),
+ - 4-компонентный параметр (`vfunc +12`),
+ - scalar+rgb (`vfunc +16`).
+
+## 9.2. Opcode 2
+
+- `ResourceRef64` резолвится через `sub_100065A0` (режим-зависимая загрузка, в данных обычно `sounds.lib`/`wav`);
+- использует manager-команду id `910`.
+
+## 9.3. Opcode 3/4/9
+
+- общий core-emitter в `sub_100106C0`;
+- opcode 4 добавляет нормализацию по `raw+200`;
+- opcode 9 добавляет переключение render-кода (`raw+200/+204`).
+
+## 9.4. Opcode 5
+
+- держит массив внутренних сегментов (`332` байта/элемент, ctor `sub_100099F0`);
+- context-matrix приходит через `vfunc +24` (`sub_10003070`).
+
+## 9.5. Opcode 7/10
+
+- общий update/render (`sub_10001230`, `sub_10001300`);
+- разные внутренние element-форматы:
+ - opcode 7: `204` байта/элемент (`sub_100092D0`),
+ - opcode 10: `492` байта/элемент (`sub_1000BB40`).
+
+## 9.6. Opcode 8
+
+- самый тяжёлый спавнер, хранит ring/slot-структуры;
+- emit фаза (`sub_10012030`) использует `mode`, `render_pow`, per-slot transforms.
+
+---
+
+## 10. Спецификация инструментов
+
+## 10.1. Reader (strict)
+
+Алгоритм:
+
+1. `len(payload) >= 60`;
+2. читаем `cmd_count`;
+3. `ptr = 0x3C`;
+4. цикл `cmd_count`:
+ - `ptr + 4 <= len`;
+ - `opcode in 1..10`;
+ - `ptr + size(opcode) <= len`;
+ - `ptr += size(opcode)`;
+5. strict-tail: `ptr == len(payload)`.
+
+## 10.2. Reader (engine-compatible)
+
+Legacy-режим (опасный, только при необходимости byte-совместимости):
+
+- без bounds-check;
+- tolerant к unknown opcode как в оригинале.
+
+## 10.3. Writer (canonical)
1. записать `FxHeader60`;
-2. `cmd_count = len(commands)`;
-3. для каждой команды записать `cmd_word` + body фиксированного размера для opcode;
-4. итоговый размер должен быть `0x3C + sum(size(opcode_i))`;
-5. без хвоста.
+2. `cmd_count = commands.len()`;
+3. команды сериализуются как `cmd_word + fixed-body`;
+4. размер payload: `0x3C + sum(size(op_i))`;
+5. без хвостовых байт.
+
+## 10.4. Editor (lossless)
+
+Правила:
-## 7.5. Конвертация в промежуточный JSON
+- все поля little-endian;
+- не менять fixed size команды;
+- не добавлять padding;
+- сохранять неизвестные биты (`cmd_word`, `header.flags`) copy-through;
+- для частично-известных полей поддерживать режим `opaque`.
-Рекомендуемая структура для round-trip:
+## 10.5. IR/JSON (рекомендуемая форма)
```json
{
@@ -392,71 +740,95 @@ void FxLoad(FxInstance* fx, uint8_t* payload) {
},
"commands": [
{
- "opcode": 3,
+ "opcode": 8,
+ "word_raw": 264,
"enabled": 1,
- "word_raw": 259,
- "body_hex": "..."
+ "fields": {
+ "mode": 1065353216,
+ "eval_t0": 0.0,
+ "eval_t1": 1.0,
+ "resource": {"archive": "material.lib", "name": "fire_smoke"}
+ },
+ "opaque_extra_hex": "..."
}
]
}
```
-`body_hex` хранит opaque payload без потери данных.
+---
+
+## 11. Проверка на реальных данных
+
+`testdata/nres`:
+
+- FXID payload: `923`;
+- валидация parser'а: `923/923 valid`.
+
+Распределение opcode:
+
+- `1: 618`
+- `2: 517`
+- `3: 1545`
+- `4: 202`
+- `5: 31`
+- `6: 0` (в датасете не встречен, но поддержан)
+- `7: 1161`
+- `8: 237`
+- `9: 266`
+- `10: 160`
+
+Подтверждённые `ResourceRef64` оффсеты:
+
+- op2 `+84`, op3/4/9 `+136`, op5 `+48`, op7/10 `+144`, op8 `+184`.
+
+Для op1 найден редкий расширенный хвост (`+160/+192`) в `effects.rlb:r_lightray_w`:
+
+- `material.lib` / `light_w`.
---
-## 8. Проверка на реальных данных
+## 12. Практический чек-лист 1:1
-`testdata/nres` (через `tools/msh_doc_validator.py`) :
+Для runtime-порта:
-- FXID effects: `923/923 valid`.
+- реализовать `FxHeader60` и parser `sub_10007650`;
+- реализовать opcode-классы с методами как в vtable;
+- учитывать start/stop/restart контракт manager API;
+- воспроизвести `sub_10005C60` + post-flags (`0x20`, `0x200`);
+- воспроизвести event loop `sub_10003D30(case 28)`.
-Дополнительно по этим 923 payload:
+Для toolchain:
-- `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`
- - `4: 202`
- - `5: 31`
- - `7: 1161`
- - `8: 237`
- - `9: 266`
- - `10: 160`
- - `6`: не встречен, но поддержан parser.
-- `cmd_word`:
- - `bits 9..31` не использованы в датасете;
- - `bit8` встречается для части opcode (особенно `3`, `7`, `9`).
+- strict validator по разделу 10.1;
+- canonical writer по разделу 10.3;
+- field-aware editor + opaque fallback для неизвестных зон.
---
-## 9. Известные пробелы (не блокируют 1:1 container/runtime)
+## 13. Что считать «полной» совместимостью
-1. Полная человеко-читаемая семантика **внутренних полей command body** для каждого opcode не завершена.
-2. Для части битов `header.flags` есть только functional-наблюдение без финального gameplay-имени.
-3. Высокие биты `settings_id` используются как есть (runtime читает low8); их предметное имя не зафиксировано.
+Практический критерий завершения:
-Это не мешает:
+1. Парсер и writer дают byte-identical round-trip для всех 923 FXID.
+2. Runtime-порт выдаёт совпадающие state transitions на одинаковом `dt/seed` (по ключевым полям instance + command state).
+3. Все opcode `1..10` поддержаны (включая `6`, даже если отсутствует в текущем датасете).
+4. `ResourceRef64` и mode-ветки (`op1`, `op2`, `op9`) совпадают с оригиналом.
-- корректно читать/валидировать/пересобирать FXID;
-- делать lossless редактирование;
-- воспроизводить lifecycle менеджера и update-loop 1:1 на уровне контракта.
+Эта страница покрывает весь наблюдаемый контракт формата/рантайма и полную карту body-полей по всем opcode.
---
-## 10. Минимальный чек-лист реализации
+## 14. Что осталось до «абсолютных 100%»
-Для 1:1-порта движка:
+Для практического 1:1 (парсер/writer/runtime на известном контенте) покрытие уже достаточно.
+Для «абсолютных 100%» на любых входах и во всех краевых режимах остаются 3 пункта:
-- реализовать `FxHeader60` и stream parser по размерам opcode;
-- реализовать менеджер API (раздел 2.3);
-- реализовать tick-path `03D30(case 28)` -> `06170` -> `08120`/`07D10`;
-- учитывать флаги `0x40`, `0x400`, `0x800`, `0x1000`, `0x80/0x100`, `0x20`, `0x200`.
+1. FP-детерминизм: оригинал опирается на x87-style вычисления; SSE/fast-math могут давать расхождения в alpha/таймингах.
+2. RNG parity: используется `sub_10002220` (16-bit генератор) и глобальные seed-состояния; для bit-exact воспроизведения нужны контрольные трассы оригинала.
+3. Редкие ветки данных: в текущем датасете нет opcode `6`, и почти не встречаются хвосты op1 (`+136..223`); для исчерпывающей валидации нужны дополнительные FXID-образцы.
-Для инструментов:
+Что нужно собрать, чтобы закрыть это полностью:
-- strict validator по разделу 7.1;
-- canonical writer по разделу 7.4;
-- opaque-представление command-body для безопасного round-trip.
+- frame-by-frame dump из оригинального runtime (alpha, manager flags, per-command state);
+- контрольные прогоны при фиксированном `dt` и seed;
+- минимум по одному ресурсу на каждую редкую ветку (`op6`, op1-tail с ненулевыми `+136..223`).