2026-02-11 21:12:05 +00:00
# FXID
2026-02-11 22:06:56 +00:00
Документ фиксирует спецификацию р е с у р с а эффекта `FXID` на уровне, достаточном для:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- 1:1 загрузки и исполнения в совместимом runtime;
- построения валидатора payload;
- создания lossless-конвертера (`binary -> IR -> binary` );
- создания редактора с безопасным редактированием полей.
2026-02-11 21:50:33 +00:00
Связанный контейнер: [NRes / RsLi ](nres.md ).
---
2026-02-11 22:06:56 +00:00
## 1. Источники и статус восстановления
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Спецификация восстановлена по:
2026-02-11 21:50:33 +00:00
- `tmp/disassembler1/Effect.dll.c` ;
- `tmp/disassembler2/Effect.dll.asm` ;
- интеграционным вызовам из `tmp/disassembler1/Terrain.dll.c` ;
- проверке реальных архивов `testdata/nres` .
2026-02-11 22:06:56 +00:00
Ключевые функции:
2026-02-11 21:50:33 +00:00
- parser FXID: `Effect.dll!sub_10007650` ;
2026-02-11 22:06:56 +00:00
- 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` .
2026-02-11 21:12:05 +00:00
---
2026-02-11 22:06:56 +00:00
## 2. Контейнер и runtime API
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 2.1. NRes entry
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
FXID хранится как NRes-entry:
2026-02-11 21:50:33 +00:00
- `type_id = 0x44495846` (`"FXID"` ).
2026-02-11 22:06:56 +00:00
Наблюдение по датасету (923 эффекта):
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
- `attr1 = 0` , `attr2 = 0` , `attr3 = 1` .
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
## 2.2. Export API `Effect.dll`
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
Экспортируются:
2026-02-11 21:12:05 +00:00
2026-02-11 21:50:33 +00:00
- `CreateFxManager(int a1, int a2, int owner)` ;
- `InitializeSettings()` .
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
`CreateFxManager` создаёт manager-объект (`0xB8` байт), инициализирует через `sub_10003AE0` , возвращает интерфейсный указатель (`base + 4` ).
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 2.3. Интерфейс менеджера
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Рабочая vtable (`off_1001E478` ):
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
| Смещение | Функция | Назначение |
2026-02-11 21:50:33 +00:00
|---|---|---|
2026-02-11 22:06:56 +00:00
| +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 |
2026-02-11 21:50:33 +00:00
| +0x20 | `sub_100047D0` | Установить scale |
| +0x24 | `sub_10004830` | Установить позицию |
2026-02-11 22:06:56 +00:00
| +0x28 | `sub_10004930` | Установить matrix transform |
| +0x2C | `sub_10004B00` | Restart/retime |
| +0x38 | `sub_10004BA0` | Duration modifier |
2026-02-11 21:50:33 +00:00
| +0x3C | `sub_10004BD0` | Start/Enable |
| +0x40 | `sub_10004C10` | Stop/Disable |
2026-02-11 22:06:56 +00:00
| +0x44 | `sub_10004C50` | Bind emitter/context |
| +0x48 | `sub_10004D50` | С б р о с frame flags |
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
`Terrain.dll` использует `QueryInterface(id=19)` для получения рабочего интерфейса.
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 3. Бинарный формат FXID payload
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
В с е значения little-endian.
2026-02-11 21:50:33 +00:00
## 3.1. Header (60 байт, `0x3C`)
2026-02-11 21:12:05 +00:00
```c
struct FxHeader60 {
2026-02-11 22:06:56 +00:00
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
2026-02-11 21:12:05 +00:00
};
```
2026-02-11 21:50:33 +00:00
Командный поток начинается строго с `offset = 0x3C` .
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
## 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` (битовая карта)
| Бит | Маска | Наблюдаемое поведение |
2026-02-11 21:50:33 +00:00
|---|---:|---|
2026-02-11 22:06:56 +00:00
| 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` ).
2026-02-11 21:50:33 +00:00
---
2026-02-11 21:12:05 +00:00
2026-02-11 21:50:33 +00:00
## 4. Командный поток
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
## 4.1. Общий формат команды
Каждая команда:
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
- `uint32 cmd_word` ;
- далее body фиксированного размера по opcode.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
`cmd_word` :
2026-02-11 21:50:33 +00:00
- `opcode = cmd_word & 0xFF` ;
- `enabled = (cmd_word >> 8) & 1` ;
2026-02-11 22:06:56 +00:00
- `bits 9..31` в датасете нулевые, но их надо сохранять 1:1.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Выравнивания между командами нет.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 4.2. Размеры
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
| Opcode | Размер записи |
|---:|---:|
| 1 | 224 |
| 2 | 148 |
| 3 | 200 |
| 4 | 204 |
| 5 | 112 |
| 6 | 4 |
| 7 | 208 |
| 8 | 248 |
| 9 | 208 |
| 10 | 208 |
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 4.3. Opcode -> runtime-класс (vtable)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
| Opcode | `new(size)` | vtable |
2026-02-11 21:50:33 +00:00
|---:|---:|---|
2026-02-11 22:06:56 +00:00
| 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` |
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 4.4. Общий вызовной контракт команды
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
После создания команды (`sub_10007650` ):
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
1. `cmd->enabled = cmd_word.bit8` .
2. `cmd->Init(fx_queue, fx_instance)` (`vfunc +4` ).
3. команда добавляется в список инстанса.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
В runtime cycle:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- `vfunc +8` : update/compute (bool);
- `vfunc +12` : emission/render callback;
- `vfunc +20` : toggle active;
- `vfunc +16` /`+24` : служебные функции (зависят от opcode).
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 5. Загрузка FXID (engine-accurate)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
`sub_10007650` :
2026-02-11 21:12:05 +00:00
```c
2026-02-11 21:50:33 +00:00
void FxLoad(FxInstance* fx, uint8_t* payload) {
FxHeader60* h = (FxHeader60*)payload;
2026-02-11 22:06:56 +00:00
fx->raw_header = h;
2026-02-11 21:50:33 +00:00
fx->mode = h->time_mode;
2026-02-11 22:06:56 +00:00
fx->end_ms = fx->start_ms + h->duration_sec * 1000.0f;
fx->scale = {h->scale_x, h->scale_y, h->scale_z};
2026-02-11 21:50:33 +00:00
fx->active_default = ((h->flags & 0x40) == 0);
uint8_t* ptr = payload + 0x3C;
2026-02-11 22:06:56 +00:00
for (uint32_t i = 0; i < h->cmd_count; ++i) {
2026-02-11 21:50:33 +00:00
uint32_t w = * (uint32_t * )ptr;
uint8_t op = (uint8_t)(w & 0xFF);
2026-02-11 22:06:56 +00:00
Command* cmd = CreateByOpcode(op, ptr); // может вернуть null
if (cmd) {
2026-02-11 21:50:33 +00:00
cmd->enabled = (w >> 8) & 1;
2026-02-11 22:06:56 +00:00
if (h->flags & 0x400) fx->manager_flags |= 0x0100;
if ((h->flags & 0x400) || cmd->enabled) fx->manager_flags |= 0x0010;
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
cmd->Init(fx->queue, fx);
2026-02-11 21:50:33 +00:00
fx->commands.push_back(cmd);
}
2026-02-11 22:06:56 +00:00
ptr += size_by_opcode(op); // без bounds checks в оригинале
2026-02-11 21:50:33 +00:00
}
}
```
2026-02-11 22:06:56 +00:00
Критичные edge-case оригинала:
- bounds checks отсутствуют;
- при unknown opcode `ptr` не двигается (`advance = 0` );
- при `new == null` команда пропускается, но `ptr` двигается.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Фактический `advance` в `sub_10007650` задан hardcoded в DWORD:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- `op1:+56` , `op2:+37` , `op3:+50` , `op4:+51` , `op5:+28` ,
- `op6:+1` , `op7:+52` , `op8:+62` , `op9:+52` , `op10:+52` ,
- `default:+0` .
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 6. Runtime lifecycle
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- `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` .
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Event-codes `sub_10003D30` :
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- `4` : bootstrap/time init;
- `20` : range-removal + index repair;
- `23` : set manager bit0;
- `24` : clear manager bit0;
- `28` : main tick.
---
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 7. Общий тип `ResourceRef64`
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Для opcode `2/3/4/5/7/8/9/10` присутствует ссылка вида:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
```c
struct ResourceRef64 {
char archive[32]; // null-terminated ASCII, case-insensitive compare
char name[32]; // null-terminated ASCII
};
```
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Поведение loader'а :
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- о б а имени обязаны быть непустыми;
- кэширование по `(_strcmpi archive, _strcmpi name)` ;
- загрузка/резолв через manager resource API.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Наблюдение по данным:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- для `opcode 2` : обычно `sounds.lib` + `*.wav` ;
- для остальных: обычно `material.lib` + material name.
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 8. Полная карта body по opcode (field-level)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Смещения указаны от начала команды (включая `cmd_word` ).
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 8.1. Opcode 1 (`off_1001E78C`, size=224)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Основные методы:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- init: `sub_1000F4B0` ;
- update: `sub_1000F6E0` ;
- emit: `nullsub_2` ;
- toggle: `sub_1000F490` .
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
```c
struct FxCmd01 {
uint32_t word; // +0
uint32_t mode; // +4 (enum, см. ниже)
float t_start; // +8
float t_end; // +12
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
float p0_min[3]; // +16..24
float p0_max[3]; // +28..36
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
float p1_min[3]; // +40..48
float p1_max[3]; // +52..60
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
float q0_min[4]; // +64..76
float q0_max[4]; // +80..92
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
float q0_rand_span[4]; // +96..108 (все 4 читаются в sub_1000F6E0)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
float scalar_min; // +112
float scalar_max; // +116
float scalar_rand_amp; // +120
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
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` ).
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 8.7. Opcode 7 (`off_1001E228`, size=208)
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Методы:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- 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)
2026-02-11 21:50:33 +00:00
1. записать `FxHeader60` ;
2026-02-11 22:06:56 +00:00
2. `cmd_count = commands.len()` ;
3. команды сериализуются как `cmd_word + fixed-body` ;
4. размер payload: `0x3C + sum(size(op_i))` ;
5. без хвостовых байт.
## 10.4. Editor (lossless)
Правила:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- все поля little-endian;
- не менять fixed size команды;
- не добавлять padding;
- сохранять неизвестные биты (`cmd_word` , `header.flags` ) copy-through;
- для частично-известных полей поддерживать режим `opaque` .
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
## 10.5. IR/JSON (рекомендуемая форма)
2026-02-11 21:50:33 +00:00
```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": [
{
2026-02-11 22:06:56 +00:00
"opcode": 8,
"word_raw": 264,
2026-02-11 21:50:33 +00:00
"enabled": 1,
2026-02-11 22:06:56 +00:00
"fields": {
"mode": 1065353216,
"eval_t0": 0.0,
"eval_t1": 1.0,
"resource": {"archive": "material.lib", "name": "fire_smoke"}
},
"opaque_extra_hex": "..."
2026-02-11 21:50:33 +00:00
}
]
}
2026-02-11 21:12:05 +00:00
```
2026-02-11 22:06:56 +00:00
---
## 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` .
2026-02-11 21:12:05 +00:00
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 12. Практический чек-лист 1:1
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
Для runtime-порта:
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
- реализовать `FxHeader60` и parser `sub_10007650` ;
- реализовать opcode-классы с методами как в vtable;
- учитывать start/stop/restart контракт manager API;
- воспроизвести `sub_10005C60` + post-flags (`0x20` , `0x200` );
- воспроизвести event loop `sub_10003D30(case 28)` .
2026-02-11 21:12:05 +00:00
2026-02-11 22:06:56 +00:00
Для toolchain:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- strict validator по разделу 10.1;
- canonical writer по разделу 10.3;
- field-aware editor + opaque fallback для неизвестных зон.
2026-02-11 21:12:05 +00:00
---
2026-02-11 22:06:56 +00:00
## 13. Что считать «полной» совместимостью
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Практический критерий завершения:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
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` ) совпадают с оригиналом.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Эта страница покрывает весь наблюдаемый контракт формата/рантайма и полную карту body-полей по всем opcode.
2026-02-11 21:50:33 +00:00
---
2026-02-11 22:06:56 +00:00
## 14. Что осталось до «абсолютных 100%»
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Для практического 1:1 (парсер/writer/runtime на известном контенте) покрытие уже достаточно.
Для «абсолютных 100%» на любых входах и во всех краевых режимах остаются 3 пункта:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
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-образцы.
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
Что нужно собрать, чтобы закрыть это полностью:
2026-02-11 21:50:33 +00:00
2026-02-11 22:06:56 +00:00
- frame-by-frame dump из оригинального runtime (alpha, manager flags, per-command state);
- контрольные прогоны при фиксированном `dt` и seed;
- минимум по одному р е с у р с у на каждую редкую ветку (`op6` , op1-tail с ненулевыми `+136..223` ).