aboutsummaryrefslogtreecommitdiff
path: root/docs/specs/fxid.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/specs/fxid.md')
-rw-r--r--docs/specs/fxid.md846
1 files changed, 100 insertions, 746 deletions
diff --git a/docs/specs/fxid.md b/docs/specs/fxid.md
index 7dd1d4b..22d02d8 100644
--- a/docs/specs/fxid.md
+++ b/docs/specs/fxid.md
@@ -1,89 +1,20 @@
# FXID
-Документ фиксирует спецификацию ресурса эффекта `FXID` на уровне, достаточном для:
-
-- 1:1 загрузки и исполнения в совместимом runtime;
-- построения валидатора payload;
-- создания lossless-конвертера (`binary -> IR -> binary`);
-- создания редактора с безопасным редактированием полей.
+`FXID` — бинарный формат эффекта в движке Parkan: Iron Strategy.
+Эта страница задаёт контракт формата и исполнения на уровне, достаточном для 1:1 порта рендера/симуляции эффектов и для lossless-инструментов.
Связанный контейнер: [NRes / RsLi](nres.md).
----
-
-## 1. Источники и статус восстановления
-
-Спецификация восстановлена по:
-
-- `tmp/disassembler1/Effect.dll.c`;
-- `tmp/disassembler2/Effect.dll.asm`;
-- интеграционным вызовам из `tmp/disassembler1/Terrain.dll.c`;
-- проверке реальных архивов `testdata/nres`.
-
-Ключевые функции:
-
-- parser FXID: `Effect.dll!sub_10007650`;
-- 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. Контейнер и runtime API
-
-### 2.1. NRes entry
-
-FXID хранится как NRes-entry:
-
-- `type_id = 0x44495846` (`"FXID"`).
+## 1. Контейнер
-Наблюдение по датасету (923 эффекта):
+- Тип ресурса в `NRes`: `0x44495846` (`FXID`).
+- Значения `attr1/attr2/attr3` в типовых игровых данных стабильны, но при редактуре их нужно сохранять как есть.
-- `attr1 = 0`, `attr2 = 0`, `attr3 = 1`.
-
-### 2.2. Export API `Effect.dll`
-
-Экспортируются:
-
-- `CreateFxManager(int a1, int a2, int owner)`;
-- `InitializeSettings()`.
-
-`CreateFxManager` создаёт manager-объект (`0xB8` байт), инициализирует через `sub_10003AE0`, возвращает интерфейсный указатель (`base + 4`).
-
-### 2.3. Интерфейс менеджера
-
-Рабочая vtable (`off_1001E478`):
-
-| Смещение | Функция | Назначение |
-|---|---|---|
-| +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` | Установить 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` | Bind emitter/context |
-| +0x48 | `sub_10004D50` | Сброс frame flags |
-
-`Terrain.dll` использует `QueryInterface(id=19)` для получения рабочего интерфейса.
-
----
-
-## 3. Бинарный формат FXID payload
+## 2. Бинарный формат
Все значения little-endian.
-### 3.1. Header (60 байт, `0x3C`)
+### 2.1. Заголовок (60 байт)
```c
struct FxHeader60 {
@@ -105,94 +36,26 @@ struct FxHeader60 {
};
```
-Командный поток начинается строго с `offset = 0x3C`.
-
-### 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` | 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. Общий формат команды
+Поток команд начинается строго с `offset = 0x3C`.
+
+### 2.2. Команда
Каждая команда:
-- `uint32 cmd_word`;
-- далее body фиксированного размера по opcode.
+1. `uint32 cmd_word`
+2. body фиксированного размера, зависящего от `opcode`
-`cmd_word`:
+Поля `cmd_word`:
-- `opcode = cmd_word & 0xFF`;
-- `enabled = (cmd_word >> 8) & 1`;
-- `bits 9..31` в датасете нулевые, но их надо сохранять 1:1.
+- `opcode = cmd_word & 0xFF`
+- `enabled = (cmd_word >> 8) & 1`
+- `bits 9..31` нужно сохранять 1:1
Выравнивания между командами нет.
-### 4.2. Размеры
+### 2.3. Размеры команд
-| Opcode | Размер записи |
+| Opcode | Размер |
|---:|---:|
| 1 | 224 |
| 2 | 148 |
@@ -205,630 +68,121 @@ Post-обработка после mode:
| 9 | 208 |
| 10 | 208 |
-### 4.3. Opcode -> runtime-класс (vtable)
+## 3. Смысл заголовка
-| 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` |
+- `cmd_count`: число команд в потоке.
+- `time_mode`: способ вычисления текущего коэффициента эффекта.
+- `duration_sec`: длительность (в рантайме переводится в миллисекунды).
+- `phase_jitter`: амплитуда случайного фазового сдвига.
+- `flags`: флаги поведения (видимость, альфа-модификаторы, режимы гейтинга).
+- `settings_id`: индекс профиля/настроек эффекта.
+- `rand_shift_*`: случайный пространственный сдвиг.
+- `pivot_*`: локальная опора.
+- `scale_*`: базовый масштаб инстанса эффекта.
-### 4.4. Общий вызовной контракт команды
+## 4. Флаги заголовка
-После создания команды (`sub_10007650`):
+Практически важные биты:
-1. `cmd->enabled = cmd_word.bit8`.
-2. `cmd->Init(fx_queue, fx_instance)` (`vfunc +4`).
-3. команда добавляется в список инстанса.
+- `0x0001`: случайный сдвиг фазы
+- `0x0008`: случайный пространственный сдвиг (`rand_shift_*`)
+- `0x0010`: ветки видимости/окклюзии
+- `0x0020`: треугольный ремап альфы
+- `0x0040`: инверсия исходного active-state
+- `0x0080`, `0x0100`: фильтрация по времени суток
+- `0x0200`: умножение альфы на нормализованное время жизни
+- `0x0400`, `0x1000`: дополнительные биты состояния менеджера эффекта
+- `0x0800`: дополнительный гейтинг
-В runtime cycle:
+Неизвестные биты должны сохраняться без изменений.
-- `vfunc +8`: update/compute (bool);
-- `vfunc +12`: emission/render callback;
-- `vfunc +20`: toggle active;
-- `vfunc +16`/`+24`: служебные функции (зависят от opcode).
+## 5. `time_mode` (0..17)
----
+База:
-## 5. Загрузка FXID (engine-accurate)
+- `tn = (now - start) / (end - start)`
+- `prev = предыдущая вычисленная альфа`
-`sub_10007650`:
+Поддерживаемые семейства режимов:
-```c
-void FxLoad(FxInstance* fx, uint8_t* payload) {
- FxHeader60* h = (FxHeader60*)payload;
-
- fx->raw_header = h;
- fx->mode = h->time_mode;
- 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) {
- uint32_t w = *(uint32_t*)ptr;
- uint8_t op = (uint8_t)(w & 0xFF);
-
- Command* cmd = CreateByOpcode(op, ptr); // может вернуть null
- if (cmd) {
- cmd->enabled = (w >> 8) & 1;
-
- if (h->flags & 0x400) fx->manager_flags |= 0x0100;
- if ((h->flags & 0x400) || cmd->enabled) fx->manager_flags |= 0x0010;
-
- cmd->Init(fx->queue, fx);
- fx->commands.push_back(cmd);
- }
-
- ptr += size_by_opcode(op); // без bounds checks в оригинале
- }
-}
-```
-
-Критичные edge-case оригинала:
-
-- bounds checks отсутствуют;
-- при unknown opcode `ptr` не двигается (`advance = 0`);
-- при `new == null` команда пропускается, но `ptr` двигается.
-
-Фактический `advance` в `sub_10007650` задан hardcoded в DWORD:
-
-- `op1:+56`, `op2:+37`, `op3:+50`, `op4:+51`, `op5:+28`,
-- `op6:+1`, `op7:+52`, `op8:+62`, `op9:+52`, `op10:+52`,
-- `default:+0`.
-
----
+- константный режим;
+- линейный (`tn`), обратный (`1-tn`), циклический (`fract(tn)`);
+- режимы от внешних параметров мира/очереди;
+- режимы на основе норм векторов состояния;
+- режимы с ограничением вниз/вверх относительно `prev`.
-## 6. Runtime lifecycle
+После вычисления:
-- `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`.
+- при `flags & 0x0200` применяется `alpha *= tn`;
+- при `flags & 0x0020` применяется triangular remap.
-Event-codes `sub_10003D30`:
+## 6. Resource-ссылки внутри команд
-- `4`: bootstrap/time init;
-- `20`: range-removal + index repair;
-- `23`: set manager bit0;
-- `24`: clear manager bit0;
-- `28`: main tick.
-
----
-
-## 7. Общий тип `ResourceRef64`
-
-Для opcode `2/3/4/5/7/8/9/10` присутствует ссылка вида:
+Для opcode `2/3/4/5/7/8/9/10` используется ссылка:
```c
struct ResourceRef64 {
- char archive[32]; // null-terminated ASCII, case-insensitive compare
- char name[32]; // null-terminated ASCII
-};
-```
-
-Поведение loader'а:
-
-- оба имени обязаны быть непустыми;
-- кэширование по `(_strcmpi archive, _strcmpi name)`;
-- загрузка/резолв через manager resource API.
-
-Наблюдение по данным:
-
-- для `opcode 2`: обычно `sounds.lib` + `*.wav`;
-- для остальных: обычно `material.lib` + material name.
-
----
-
-## 8. Полная карта body по opcode (field-level)
-
-Смещения указаны от начала команды (включая `cmd_word`).
-
-### 8.1. Opcode 1 (`off_1001E78C`, size=224)
-
-Основные методы:
-
-- init: `sub_1000F4B0`;
-- update: `sub_1000F6E0`;
-- emit: `nullsub_2`;
-- toggle: `sub_1000F490`.
-
-```c
-struct FxCmd01 {
- uint32_t word; // +0
- uint32_t mode; // +4 (enum, см. ниже)
- float t_start; // +8
- float t_end; // +12
-
- float p0_min[3]; // +16..24
- float p0_max[3]; // +28..36
-
- float p1_min[3]; // +40..48
- float p1_max[3]; // +52..60
-
- float q0_min[4]; // +64..76
- float q0_max[4]; // +80..92
-
- float q0_rand_span[4]; // +96..108 (все 4 читаются в sub_1000F6E0)
-
- 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")
+ char archive[32];
+ char name[32];
};
```
-Замечания по полям 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`.
+- строки ASCII, нуль-терминированные;
+- сравнение имён регистронезависимое;
+- обычно:
+ - `opcode 2`: `sounds.lib` + `*.wav`
+ - остальные: `material.lib` + имя материала/эффекта.
-`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
+## 7. Runtime-контракт исполнения
- 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
+1. Заголовок копируется в runtime-состояние.
+2. Вычисляется `end_time`.
+3. Для каждой команды создаётся runtime-объект по `opcode`.
+4. В объект копируется `enabled`.
+5. Объект инициализируется контекстом эффекта.
- 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
+1. Вычисляется текущий коэффициент/альфа по `time_mode` и `flags`.
+2. Выполняется update каждой команды.
+3. Выполняется emit/render часть активных команд.
+4. Применяются события Start/Stop/Restart.
- float active_t0; // +32
- float active_t1; // +36
+## 8. Строгий парсер (рекомендуемый)
- float v0_min[3]; // +40..48
- float v0_max[3]; // +52..60
+1. Проверить `len(payload) >= 60`.
+2. Прочитать `cmd_count`.
+3. Идти от `ptr = 0x3C`.
+4. Для каждой команды:
+ - проверить `ptr + 4 <= len`;
+ - прочитать `opcode`;
+ - проверить, что `opcode` поддержан;
+ - проверить `ptr + size(opcode) <= len`;
+ - сдвинуть `ptr += size(opcode)`.
+5. Проверить `ptr == len(payload)`.
- float pow0[3]; // +64..72
+## 9. Writer и редактор
- float v1_min[3]; // +76..84
- float v1_max[3]; // +88..96
+Для lossless-совместимости:
- 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`).
-
-### 8.7. Opcode 7 (`off_1001E228`, size=208)
-
-Методы:
-
-- 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 = commands.len()`;
-3. команды сериализуются как `cmd_word + fixed-body`;
-4. размер payload: `0x3C + sum(size(op_i))`;
-5. без хвостовых байт.
-
-### 10.4. Editor (lossless)
-
-Правила:
-
-- все поля little-endian;
-- не менять fixed size команды;
+- сохранять все неизвестные поля/биты;
+- не менять фиксированные размеры команд;
- не добавлять padding;
-- сохранять неизвестные биты (`cmd_word`, `header.flags`) copy-through;
-- для частично-известных полей поддерживать режим `opaque`.
-
-### 10.5. IR/JSON (рекомендуемая форма)
-
-```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": 8,
- "word_raw": 264,
- "enabled": 1,
- "fields": {
- "mode": 1065353216,
- "eval_t0": 0.0,
- "eval_t1": 1.0,
- "resource": {"archive": "material.lib", "name": "fire_smoke"}
- },
- "opaque_extra_hex": "..."
- }
- ]
-}
-```
-
----
-
-## 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`.
-
----
-
-## 12. Практический чек-лист 1:1
-
-Для runtime-порта:
-
-- реализовать `FxHeader60` и parser `sub_10007650`;
-- реализовать opcode-классы с методами как в vtable;
-- учитывать start/stop/restart контракт manager API;
-- воспроизвести `sub_10005C60` + post-flags (`0x20`, `0x200`);
-- воспроизвести event loop `sub_10003D30(case 28)`.
-
-Для toolchain:
-
-- strict validator по разделу 10.1;
-- canonical writer по разделу 10.3;
-- field-aware editor + opaque fallback для неизвестных зон.
-
----
-
-## 13. Что считать «полной» совместимостью
-
-Практический критерий завершения:
-
-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`) совпадают с оригиналом.
-
-Эта страница покрывает весь наблюдаемый контракт формата/рантайма и полную карту body-полей по всем opcode.
-
----
-
-## 14. Что осталось до «абсолютных 100%»
+- пересчитывать только `cmd_count` и размеры контейнера;
+- сохранять порядок команд.
-Для практического 1:1 (парсер/writer/runtime на известном контенте) покрытие уже достаточно.
-Для «абсолютных 100%» на любых входах и во всех краевых режимах остаются 3 пункта:
+## 10. Что требуется для 1:1 переноса
-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-образцы.
+1. Полная поддержка opcode `1..10`.
+2. Точный контракт вычисления `time_mode` и `flags`.
+3. Точное поведение `ResourceRef64`.
+4. Повторяемый RNG и одинаковая политика плавающей точки.
-Что нужно собрать, чтобы закрыть это полностью:
+## 11. Статус валидации
-- frame-by-frame dump из оригинального runtime (alpha, manager flags, per-command state);
-- контрольные прогоны при фиксированном `dt` и seed;
-- минимум по одному ресурсу на каждую редкую ветку (`op6`, op1-tail с ненулевыми `+136..223`).
+- Формальные инварианты FXID зафиксированы в `tools/msh_doc_validator.py` и `tools/fxid_abs100_audit.py`.
+- В текущем рабочем окружении нет полного набора игровых архивов (`testdata` без payload), поэтому массовая повторная проверка корпуса здесь не выполнялась.