From 97c73118a1390c8a08cc34f4ce9435ba71df2af3 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 31 Oct 2026 04:28:05 +0400 Subject: feat(assets): decode research tables and calculate candidate costs --- docs/appendices/script-vm.md | 70 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 5 deletions(-) (limited to 'docs/appendices/script-vm.md') diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 49af347..be8c8b3 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -34,6 +34,60 @@ VM state; `.trf` — NRes tables, чей framing подтверждён, а field semantics местами лишь consumer-inferred. +### Исследовательский профиль TRF и стоимость кандидата + +Для одной ветви `Handler(43)` уже восстановлен путь вычисления стоимости. +Результат привязан к GOG `ArealMap.dll`, SHA-256 +`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`, +`CalcFullResearchCost` по RVA `0x16280`. Реальный ABI — четыре аргумента +stdcall на стеке: `[context, candidate, out1, out2]`; функция снимает с него +`0x10` байт и не принимает `candidate` через `ECX`. + +Источник этих таблиц привязан к GOG `MisLoad.dll`, SHA-256 +`c607762bb67db14ca530927c8e1347339b62b0a163ac90c708ee83142292e2bf`. +`FUN_10002fe0` (RVA `0x2fe0`) загружает required TRF chunks. Внутренний +research context разрешает ключ через `FUN_10002a40` (RVA `0x2a40`), выдаёт +32-byte row record через `FUN_10002aa0` (RVA `0x2aa0`) и возвращает ordered +dependency array через `FUN_10002cf0` (RVA `0x2cf0`). Эти методы соответствуют +виртуальным слотам context `+0x08`, `+0x0c` и `+0x24`; native Calc получает их +через таблицу вызовов, а не напрямую из файлов. + +`ResearchTrfDocument` в `fparkan-assets` повторно использует NRes reader и +сохраняет исходный архив. `TRF0.attr1` задаёт число строк по 40 байт; native +загрузчик требует `TRF0.attr2 == 3`. `TRF1` задаёт один флаговый byte на строку: +Rust воспроизводит native zero-fill при коротком payload и отклоняет более +длинный. `TRFB.attr1` задаёт пары little-endian `(u16 name_offset, u16 row)`; +имя берётся из NUL-terminated pool `TRF6`. Поиск идёт по исходному порядку и +берёт первую ASCII-case-insensitive пару. Связь строки с парой использует +только младшие 16 бит сырого слова `TRF0` по смещению `+0x20`. + +Две таблицы зависимостей имеют собственные размеры: `TRF2` считает группы, +`TRF3` содержит их плоский упорядоченный поток индексов; `TRF4`/`TRF5` образуют +отдельную пару. Только `TRF2`/`TRF3` участвуют в `CalcFullResearchCost`. +Отсутствующий `TRF3` или `TRF5` означает пустой поток лишь при нулевой сумме +счётчиков соответствующей таблицы. Required `TRF7`–`TRFA` остаются +непрозрачными. Reader проверяет фактически читаемые срезы, строки, ссылки и +размеры выделений для памяти Rust; это дополнительная безопасность, а не +утверждение, что native loader отвергает те же плохие файлы. + +Из флагового byte `TRF1` native provider строит слова record `+0`, `+4` и `+8` +из битов `(1, 0, 2)`. `CalcFullResearchCost` использует первые два как raw gate, +не присваивая им общего игрового имени: `+0` определяет, добавлять ли строку, +`+4` — раскрывать ли её зависимости. Нулевой `kind` компонента Unit DAT +отсекает весь узел с потомками. Иначе дерево сканируется в preorder; найденные +строки добавляются один раз, затем для каждой прямой строки выполняется +упорядоченный DFS по `TRF2`/`TRF3`. Общая таблица посещений завершает циклы и +удаляет повторы. + +По debug-строкам `out1` — ore и суммирует `TRF0` word 1, а `out2` — power и +суммирует word 0. Native код хранит каждый шаг суммы как `f32`; один cost-вектор +`1, 2^24, -2^24` даёт первый результат `0`, а в другом порядке +`2^24, -2^24, 1` даёт `1`. Rust core сохраняет тот же порядок накопления. В +принятом наборе из 23 синтетических вызовов оригинальных DLL были случаи +`CalcFullResearchCost` и два AI-предиката; x87 control word был установлен в +`0x007f` для контролируемых сравнений. Набор проверяет выбранные ветви на +синтетическом контексте, но не использует реальный контекст миссии. + ## Безопасная модель исполнения Целевая VM должна разделять immutable package (bytecode, symbols, events, @@ -285,9 +339,12 @@ TMA properties остаются four raw `u32` words плюс имя, пока c сохраняются `NOT USED`; corpus подтверждает `Invulnerability`, life state, `ClanID`, ore, speed и free-time properties. -Research/economy работают в simulation: `LoadResearch` и -`CalcFullResearchCost` доказывают данные и вычислимую стоимость, но не полный -layout prerequisites/modifiers/unlocks. General formula evaluator требует +Для одного кандидата и одной research-таблицы порядок prerequisite-closure, +дедупликация и две выходные стоимости `CalcFullResearchCost` восстановлены и +проверены отдельно, как описано в +[исследовательском профиле TRF](#исследовательский-профиль-trf-и-стоимость-кандидата). +Это не закрывает production-modifier/unlock data, экономические переходы или +полное исполнение Handler(43). Общий evaluator формул всё ещё требует versioned grammar, typed operands, deterministic numeric policy, bounded stack и явных errors; x87-compatible rounding нужен там, где оно выбирает ветку. Описанный ниже literal-путь первого `Init` не закрывает общий evaluator. @@ -448,8 +505,11 @@ Rust сохраняет всю таблицу строк, но вычисляе полную загрузку кампании Mission.01 после этих присваиваний; у обычной `Single.01` clan 1 после формул остаётся `Handler(43)` в instruction 10, который не поддержан и не обходится. Preview оставляет значения этого клана без -частичных записей, а полный load возвращает ошибку транзакционно. Это не -подтверждение запуска самой игры или полной VM. +частичных записей, а полный load возвращает ошибку транзакционно. В +`fparkan-assets` теперь отдельно доступны bounded TRF reader и pure cost core +для одного Unit DAT, но script/runtime пока не создают нужный research context, +не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение +запуска самой игры или полной VM. ### Первый вызов Behavior и сценарного `Init` -- cgit v1.2.3