Files
fparkan/docs/specs/fxid.md
Valentin Popov 615891d550
All checks were successful
Test / Lint (pull_request) Successful in 46s
Test / Test (pull_request) Successful in 48s
Test / Lint (push) Successful in 48s
Test / Test (push) Successful in 49s
feat: обновить заголовки разделов в документации по FXID и NRes для улучшения структуры
2026-02-11 22:10:43 +00:00

26 KiB
Raw Blame History

FXID

Документ фиксирует спецификацию ресурса эффекта FXID на уровне, достаточном для:

  • 1:1 загрузки и исполнения в совместимом runtime;
  • построения валидатора payload;
  • создания lossless-конвертера (binary -> IR -> binary);
  • создания редактора с безопасным редактированием полей.

Связанный контейнер: 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;
  • 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").

Наблюдение по датасету (923 эффекта):

  • 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

Все значения little-endian.

3.1. Header (60 байт, 0x3C)

struct FxHeader60 {
    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 итерирует ровно столько шагов).
  • 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. Общий формат команды

Каждая команда:

  • uint32 cmd_word;
  • далее body фиксированного размера по opcode.

cmd_word:

  • opcode = cmd_word & 0xFF;
  • enabled = (cmd_word >> 8) & 1;
  • bits 9..31 в датасете нулевые, но их надо сохранять 1:1.

Выравнивания между командами нет.

4.2. Размеры

Opcode Размер записи
1 224
2 148
3 200
4 204
5 112
6 4
7 208
8 248
9 208
10 208

4.3. Opcode -> 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

4.4. Общий вызовной контракт команды

После создания команды (sub_10007650):

  1. cmd->enabled = cmd_word.bit8.
  2. cmd->Init(fx_queue, fx_instance) (vfunc +4).
  3. команда добавляется в список инстанса.

В runtime cycle:

  • vfunc +8: update/compute (bool);
  • vfunc +12: emission/render callback;
  • vfunc +20: toggle active;
  • vfunc +16/+24: служебные функции (зависят от opcode).

5. Загрузка FXID (engine-accurate)

sub_10007650:

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.

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.

Event-codes sub_10003D30:

  • 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 присутствует ссылка вида:

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.
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")
};

Замечания по полям 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.
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.
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 + последний коэффициент:

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.
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:

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.
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.
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-полями:

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 (рекомендуемая форма)

{
  "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%»

Для практического 1:1 (парсер/writer/runtime на известном контенте) покрытие уже достаточно.
Для «абсолютных 100%» на любых входах и во всех краевых режимах остаются 3 пункта:

  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-образцы.

Что нужно собрать, чтобы закрыть это полностью:

  • frame-by-frame dump из оригинального runtime (alpha, manager flags, per-command state);
  • контрольные прогоны при фиксированном dt и seed;
  • минимум по одному ресурсу на каждую редкую ветку (op6, op1-tail с ненулевыми +136..223).