diff options
Diffstat (limited to 'docs/appendices/script-vm.md')
| -rw-r--r-- | docs/appendices/script-vm.md | 468 |
1 files changed, 440 insertions, 28 deletions
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 3de442d..145d2a2 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -16,21 +16,78 @@ handlers, второй — числом event records. Каждый event хра `name_len + 1` raw bytes с обязательным NUL, opaque event word и count вложенных records. Вложенный record сохраняет семь `u32` header words (в disk order), список `u32` references после шестого header word и trailing seventh word. -Никакой из этих words ещё не получает semantic name. `.fml` — текстовый +Семантика остальных слов остаётся открытой; поля control и result-save описаны +в узком `Init` contract ниже. `.fml` — текстовый symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaults. `fparkan-script::parse_varset` уже читает подтверждённые numeric `VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и -`FUNCTION(...)` пока сохранены за границей этого numeric contract; +general `FUNCTION(...)` evaluation остаются вне восстановленного контракта, +но принятые строки сохраняются для ограниченного поднабора формул первого `Init`; GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration: 31 `float` и 200 `DWORD` (от `f0` до `fY`); loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при `not found` откатывается к этому shared file. Runtime повторяет данный порядок -транзакционно и публикует selected `MissionScriptVarSet` с путём/provenance, но -ещё не исполняет declarations как VM state; +транзакционно, создаёт typed varset cells для кланов и применяет поддержанные +записи первого `Init`. Это начальное состояние сценария, а не исполнение общего +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, @@ -282,11 +339,15 @@ 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. Formula evaluator требует strict -grammar/version, typed operands, deterministic numeric policy, bounded stack и -явных errors; x87-compatible rounding нужен там, где оно выбирает ветку. +Для одного кандидата и одной 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. ### Первый фрагмент dispatcher-а: `Init` и Handler(19) @@ -302,10 +363,21 @@ image base `0x10000000`. В ней `FUN_10011f40` (RVA `0x11f40`) ищет имÑ `FUN_10011f10` передаёт выбранный индекс внутреннему циклу `FUN_10011fb0` (RVA `0x11fb0`). Цикл начинает с первой вложенной инструкции, сбрасывает индекс инструкции и глубину стека условий, затем идёт по записям в порядке файла. -В исходном `default.scr` событие `Init` содержит одну инструкцию selector `19`, -control word `0xffffffff` (действие) и result reference `0xffffffff` (результат -не сохраняется). Это наблюдаемая форма пакета; поведение оригинала для остальных -форм здесь не утверждается. +Читатель `FUN_10011b20` (RVA `0x11b20`) переставляет семь disk header words в +runtime record: `[0, 1, 2, 3, 4, 5, 6]` переходят в offsets +`[+0, +8, +0x0c, +4, +0x10, +0x18, +0x1c]`; указатель references лежит в `+0x14`. +Исполнитель `FUN_10012020` (RVA `0x12020`) берёт control из runtime `+0x04`, то +есть из `header_words[3]`, а result-save reference — из runtime `+0x08`, то есть +из `header_words[1]`. Disk word `header_words[2]` попадает в `+0x0c` и участвует +в особой ветви selector `-1`, но не является result-save полем `Handler(19)`. + +В исходном `default.scr` событие `Init` содержит действие selector `19` с +заголовком `[19, 0xffffffff, 0xffffffff, 0xffffffff, 6, 3, 0xffffffff]` и +references `224, 225, 226`. Для этого действия `header_words[3] = 0xffffffff` +(action control), а `header_words[1] = 0xffffffff` (результат не сохраняется). +Одинаковое значение в `header_words[2]` — лишь содержимое этой записи, оно не +проверяется guard-ом `Handler(19)`. Это наблюдаемая форма пакета; поведение +оригинала для остальных форм здесь не утверждается. В GOG `default.scr` события `Init` в двух AutoDemo bundles имеют по одной такой инструкции. Её ссылки `224`, `225`, `226` ведут к объявлениям `DWORD` @@ -350,10 +422,15 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D показывает исходное предупреждение `MessageBoxA`; оно находится за пределами этого числового resolver-а. -Ограниченный Rust-исполнитель `VarSet::resolve_handler19_init` ищет только -первое событие `Init`, проходит его инструкции по порядку и возвращает -упорядоченные записи. Runtime применяет их к отдельной копии varset выбранного -клана лишь после успешного прохода всего события. Отсутствующий `Init` даёт +Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие +`Init` и проходит его инструкции по порядку. Он возвращает записи для +поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо +с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма +останавливает preview на `Unsupported`. Runtime начинает вычисление с копии +defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя +формула видит прежние записи этого события. Упорядоченные записи попадают в +состояние клана только после успешного прохода всего `Init`. Отсутствующий +`Init` даёт статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт `Executed` без записей. Если форма инструкции пока не поддержана, package остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения @@ -363,6 +440,131 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение оригинального dispatcher-а. +#### Литералы и простые формулы `.fml` в `Init` + +В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`) +загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`. +`FUN_10018f40` читает строки в таблицу `FUNCTION`; vtable slot `+0x18` +компилирует сохранённые строки через `FUN_100152a0` (RVA `0x152a0`). +Индексы формул — порядковые номера принятых строк, поэтому строки после +используемого номера тоже остаются в таблице. Native reader обрабатывает +независимые чанки до 255 байт, включая LF; CRLF нормализуется до LF, `Ctrl-Z` +завершает ввод, а NUL скрывает остаток своего чанка для C-string поиска. +Искатель учитывает регистр, ищет подстроку `FUNCTION` и независимо выбирает +первую `(` и запятые в видимом чанке. Это permissive framing, а не строгий +разбор сбалансированного вызова. Rust сохраняет принятые записи в этом порядке +и разбирает выражение только при ссылке из `Init`; отсутствующая соседняя `.fml` +не мешает одному лишь `Handler(19)`, но ошибка чтения, отличная от `NotFound`, +остаётся ошибкой VFS. + +Native selector `-1` при source `-1` и нулевой глубине условий выбирает +formula assignment. В дисковом заголовке `.scr` `header_words[1]` — индекс +целевой varset-ячейки, а `header_words[6]` — индекс formula row; соответствующие +поля native record находятся по смещениям `+0x08` и `+0x1c` соответственно. +`FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки. Компилятор +`FUN_100152a0` (RVA `0x152a0`) создаёт +числовые узлы, узлы переменных и узлы операторов; числовой prefix проходит через +`FUN_10020af0` (RVA `0x20af0`) как `double`, после чего сохраняется в узле как +`float`. Для переменной компилятор вызывает vtable slot `+0x10` таблицы varset; +в проверенной GOG-сборке `ai.dll` этот вызов ведёт к поиску по RVA `0x16ad0`. +Поиск сравнивает байты имени с учётом регистра и возвращает первое совпадение. +`FUN_10015b30` (RVA `0x15b30`) читает числовой узел как `f32`, а переменную — +через `FUN_10013190` (RVA `0x13190`): type `3` даёт `float`, type `5` — +беззнаковый `DWORD`. Getter возвращает число через x87; evaluator сохраняет +его как `f32` перед операцией. Оператор `+` складывает правый +и левый операнды, а `-` вычисляет левый минус правый; после узла результат +сохраняется как `f32`. `FUN_10013650` передаёт итог типизированной ячейке через +`FUN_10012c00` (RVA `0x12c00`). + +Rust в первом `Init` принимает один числовой токен или имя ASCII-переменной, +а также ровно два таких атома с одним бинарным `+` либо `-`. Например, +`source + 2.0` и `target - source` читают значения из defaults или из предыдущих +успешных записей события. Имя ищется раньше числового преобразования, поэтому +объявление с именем `1e-2` перекрывает такую числовую запись; сравнение имён +чувствительно к регистру, а повтор объявлений выбирает первое. `DWORD` читается +как беззнаковое число и округляется до `f32` до операции. Публичный +`FormulaTable::evaluate_numeric_literal` по-прежнему принимает только литералы. +Ведущие unary `+`/`-`, `*`, другие операторы, третий атом, отсутствующие или +не-ASCII имена и зарезервированные одиночные операторы остаются `Unsupported`; +так же не поддержана ветвь копирования `source != 0xffffffff`. + +Проверенный native tokenizer отличается от обычного строгого decimal parser: +`1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node +и дали payload `0x3c23d70a`, `0x3f800000`, `0x3f800000`, `0x3f800000`, +`0x3f800000` и `0x42c80000`. Для четырёх неполных exponent-форм значение +получено из распознаваемого числового prefix; это именно проверенные строки, +а не полное описание всех ошибок компилятора. + +Тип целевой ячейки сохраняется при записи: `float` (native tag `3`) принимает +исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`) +с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся +`0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw +DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init` +публикуются только после успешного прохода. + +Native x86 oracle на GOG `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил +реальный shared `varset.var` (231 declaration) и реальные таблицы +`tut1_pl2.fml` (26 строк), `scr_pl_1.fml` (5 строк) и `scream.fml` (149 строк). +В первой кампании formula `0` записывает нули в DWORD `df0..df3` (indices +188–191); в `Single.01` formula `0` записывает нули в `dT8` и `dT9` (indices +186–187). Строки `scream` `0..8` задают следующие пары literal → raw cell value: + +| Formula | Target | Literal → raw cell value | +| ---: | --- | --- | +| 0 | `dMaxBuilder` (`DWORD`) | `0.4 → 0` | +| 1 | `fDifficulty` (`float`) | `0.8 → 0x3f4ccccd` | +| 2 | `dCurrentSender` (`DWORD`) | `0.5 → 0` | +| 3 | `dMaxTower` (`DWORD`) | `1.0 → 1` | +| 4 | `dMaxStorage` (`DWORD`) | `10 → 10` | +| 5 | `dMaxTransport` (`DWORD`) | `0.2 → 0` | +| 6 | `dMaxMine` (`DWORD`) | `100 → 100` | +| 7 | `dMaxPlant` (`DWORD`) | `400 → 400` | +| 8 | `d7` (`DWORD`) | `2 → 2` | + +Отдельный одноразовый x86 oracle на той же GOG `ai.dll` проверил ещё 13 +выражений: 9 строк контролируемой `controlled.fml`, строки 19 и 22 настоящей +`c1m3e.fml` и два последовательных вызова строки 0 настоящей `c1m4e.fml`. +Native harness записывал `f32`-результат evaluator-а, типизированную raw-запись +и getter после записи. В scratch-проверке те же входы прошли через публичные +`parse_varset`, `FormulaTable::parse` и `VarSet::resolve_init`; результат +evaluator-а и raw-слово Rust совпали с native во всех 13 случаях. Среди +проверенных границ: `2 - 3` записал в `DWORD` `0xffffffff`; чтение исходного +`DWORD 0xffffffff` дало `f32 0x4f800000`, а последующее `+ 0` записало в +`DWORD` ноль. Для настоящих строк `c1m3e` получены `fTemp = 0x40333333` и +`dTemp3 = 0`; два последовательных вызова строки 0 `c1m4e` дали +`fTemp = 0x4033335d`, затем `0x40333387`. + +Oracle принудительно устанавливал x87 control word `0x007f` на время каждого +проверяемого вызова: все 87 наблюдений показали входное и восстановленное +значение `0x027f`, активное и выходное — `0x007f`; canary-проверки прошли. +Менялись только varset-копии в памяти процесса. Эта выборка подтверждает +компиляцию, вычисление и типизированные записи ровно для перечисленных входов. +Она не запускала исходный поиск и обход события `Init` и не исполняла поздние +события или миссию; выборка не доказывает полную грамматику evaluator-а. + +Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую +ссылается поддержанная запись. Лицензированный runtime-тест подтверждает +полную загрузку кампании Mission.01 после этих присваиваний; у обычной +`Single.01` clan 1 после формул остаётся `Handler(43)` в instruction 10, который +не поддержан и не обходится. Preview оставляет значения этого клана без +частичных записей, а полный load возвращает ошибку транзакционно. В +`fparkan-assets` теперь отдельно доступны bounded TRF reader и pure cost core +для одного Unit DAT, но script/runtime пока не создают нужный research context, +не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение +запуска самой игры или полной VM. + +В других строках тех же ресурсов встречаются `0.8 + fT` (`c1m3e.fml`, row 19), +`dBuildingProtectHits - dT2` (row 22) и `fTemp + 0.00001` (`c1m4e.fml`, row 0). +Декодированные `c1m3e.scr` и `c1m4e.scr` ссылаются на них из более поздних +событий `Fort_Captured`, `PBM_BUILDING_PROTECT_Start` и +`PBM_BUILDING_INF_CAPTURE_Start`, а не из их `Init`. Игнорируемый Rust-тест +подаёт эти сохранённые строки в изменённый синтетический `Init` и использует +настоящий общий `varset.var`; это проверка resolver-а, а не утверждение о +исполнении таких выражений в исходном `Init`. Отдельный native oracle и +сравнение с Rust выше проверяют эти строки при явно заданных исходных ячейках, +но не подтверждают их исполнение в оригинальном сценарии или в живой игре. + ### Первый вызов Behavior и сценарного `Init` В той же GOG-сборке `Behavior.dll` (SHA-256 @@ -610,10 +812,54 @@ tie-векторов подтверждает реакцию `FISTP`, но не Нижнеуровневый `VarSet::resolve_handler19` принимает уже преобразованные первые два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет -отсутствующие, выходящие за диапазон или не-`DWORD` цели. Метод -`resolve_handler19_init` добавляет выбор события и остановку на неподдержанной -форме инструкции. Всё остальное содержимое `.scr` сохраняется reader-ом, но -остаётся вне этого исполнения. +отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого +события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из +одного числового или переменного атома либо двух таких атомов с одним бинарным +`+` или `-`. Остальные формы инструкции и содержимое `.scr` сохраняются +reader-ом, но остаются вне этого исполнения. + +### Начальная группировка подготовленных Control-дескрипторов + +В проверенной GOG `Behavior.dll` (SHA-256 +`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) +`FUN_1001be10` (RVA `0x1be10`) получает список через QI с кодом `0x202` от +`owner+0x5c`. Слоты `+0x50` и `+0x1c` этого интерфейса возвращают число строк и +код-различитель для строки; цикл обрабатывает порядковые номера строк +`0..count-1` по возрастанию. Метки `1`, `2` и `30` начинают группу, добавляют +кандидата и добавляют отдельный элемент к последней группе соответственно. +Запись с меткой `2` до первого заголовка попадает в отдельный вектор; метка `30` +без группы игнорируется, как и все остальные метки. + +Для источника списка проверена пара GOG DLL. В `Control.dll` SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` запрос с +кодом `0x202` отдаёт интерфейс `owner+8`; его методы-обёртки переходят к методам +чтения канонического QI `0x204`, receiver которого равен `owner+0x0c`. Метод +`Control.dll` RVA `0x8830` читает число из `[receiver+0x5b4]`, то есть +`[owner+0x5c0]`; таблица указателей находится в `[receiver+0x5b0]`, то есть +`[owner+0x5bc]`. Поле `[owner+0x5b4]` — отдельный указатель кэша, а не число +строк. Метод чтения `Control.dll` RVA `0x2c390` выбирает указатель `table[index]`, +разыменовывает указатель на дескриптор из записи `+0x48` и возвращает DWORD по +адресу этого дескриптора. Загрузчик `FUN_10021d50` (RVA `0x21d50`) сохраняет там +указатель текущего дескриптора, поэтому код-различитель — его текущая метка, а +не неизменная метка фабрики. + +В оригинальном Behavior внешняя строка имеет шаг `0x34`; её `+4` получает +порядковый номер заголовка. Строка-кандидат имеет шаг `0x58`, а её `+8` хранит +порядковый номер кандидата. Номера строк с меткой `30` добавляются в отдельный +вектор последней внешней строки. Rust публикует только эти начальные номера: +`MissionBehaviorControlGroup` и +`MissionBehaviorControlGrouping` группируют строки из уже подготовленной +Control-таблицы в её исходном порядке. `MissionBehaviorControlGroupingState` +сохраняет `original_id`, `ObjectHandle` и либо результат, либо исходную причину +недоступности Control-таблицы. Полная загрузка создаёт проекцию перед публикацией +`LoadedMissionState`; предварительный просмотр возвращает пустой список. Проверки +`behavior_control_grouping_uses_current_tags_and_table_ordinals` и игнорируемый +`full_load_builds_initial_behavior_control_groupings_for_autodemo` покрывают +искусственные граничные случаи и сохранённые номера строк AutoDemo соответственно. + +Это начальное разложение подготовленного списка, а не реконструкция полной +инициализации Behavior: отдельные свойства строки-кандидата, её числовые поля, +эффекты и обновление списка после изменений Control здесь не моделируются. ### Изменение provider-состояния Control @@ -912,6 +1158,71 @@ factory-класса. В Rust неизвестный текущий тег за CPU-вызов, ни локальные векторы не означают включения бюджетного шага в Control clock. +#### Глобальный Control getter: сырой селектор `0x0f` + +В `Control.dll` SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +`FUN_1002b410` (RVA `0x2b410`) принимает сырой селектор в `EDX` и указатель +результата из `[ESP+4]`; общий выход использует `RET 4`. Сырой селектор `0x0f` +попадает в case RVA `0x2b8f5`. Владелец хранит ссылки на объекты, добавленные +фабриками `9` (`owner+0x5cc`) и `21` (`owner+0x5d0`). + +Фабрика `21` вызывает общий getter `FUN_10021d00` (RVA `0x21d00`): selectors +`0..5` читают шесть текущих `coefficient_bits` по `this+0x54`. Общий загрузчик +RVA `0x21d50` первоначально копирует descriptor words `[11..16]` в это поле. Фабрика `9` +использует getter `FUN_100255c0` (RVA `0x255c0`), который для selector `0` +умножает коэффициент `[this+0x54]` на множитель `[this+0x11c]`. Поэтому Rust +выбирает последнюю строку каждого `factory_tag` `9` и `21` в исходном порядке +таблицы; текущий `descriptor.tag` не меняет выбранную фабрику. Если любой строки нет, +результат отсутствует до проверки полей другой строки. + +Арифметика case `0x0f` сохраняет порядок и отдельное округление каждого шага: + +```text +sum = f32(D5) +for k = 4, 3, 2, 1, 0: + sum = f32(sum + Dk) +out = f32(C0 * sum) +``` + +`Dk` — текущий коэффициент `k` из `coefficient_bits` фабрики `21`; `C0` — +selector `0` фабрики `9`, то есть +`coefficient_bits[0] * class9_selector0_scale_bits`. Общие `multiplier_bits`, `node_provider_status` и +`native_word_50_bits` в этом обратном вызове не читаются. Rust проверяет, что +входы и промежуточные результаты конечны и являются обычными числами либо +точным нулём; переполнение, денормализованные числа и отсутствующие поля +остаются типизированными ошибками. Диапазон +проверяется через `f64`, а затем операция выполняется в `f32` с округлением +после каждого сложения. Это воспроизводит описанные записи `f32`, но не +моделирует полное управляющее состояние x87. Этот getter не реализует весь Control +metadata `0x36`, Handler `43` или каталог свойств. + +Лицензионный acceptance-test загружает `MISSIONS/Autodemo.00/data.tma` с явным +`MissionStartupInput { queue_time_ms: 100_000, research_creation_gate: false }` +и вызывает публичный `query_loaded_mission_control_resource(..., 15)`. Native +вызов исходного `Control.dll` того же SHA выполнен с `executionCW=0x007f` и +получил следующие биты на Rust-подготовленном снимке входных полей: + +| `original_id` | Результат selector `0x0f` | +| --- | --- | +| `0` | `0x46572800` | +| `1` | `0x4636d000` | +| `2` | `0x467b4001` | +| `3` | `0x468ca000` | +| `4` | `0x46ec4001` | +| `5` | `0x46ec4001` | + +Для `original_id` `6` и `7` таблица Rust не содержит обоих требуемых объектов +фабрик (`factory_tag` `9` и `21`), поэтому публичный запрос возвращает `None`. +Исходный обратный вызов для этих двух объектов отдельно не запускался; ветвь +неуспеха native getter-а проверена на отдельном векторе с отсутствующим указателем. + +Сравнение шести значений проверяет формулу getter-а на исходных игровых ресурсах +и снимке входных полей, подготовленном Rust. Оно не снимает состояние фабрик, +созданных оригиналом. Время и флаг `queue_time_ms` и +`research_creation_gate` задаёт тест, поэтому эти значения не подтверждают +начальное состояние запуска AutoDemo в оригинале. + #### Фабричный tag 26: изменяемая добавка В этой сборке `Control.dll` SHA-256 @@ -1099,18 +1410,36 @@ payload. Позже та же функция `World3D+0x7cb0` повторяет времени из `DATWorld+0x32a38` (`0x7e6e`); `World3D+0x7ea0` передаёт туда же время в `0x80a9`. Setter `World3D+0x5140` записывает входной DWORD в эту ячейку. Нулевой вызов связывает интерфейсы, а ненулевой `NumericTime` запускает отдельную -ветвь активации. Конструктор AniMesh задаёт `+0x158/+0x15c/+0x160` равными `1.0`. -Обработчик модели `0x75b0/0x6fc0` привязывает интерфейсы и матрицу, Effect RVA -`0x3d30` и Control RVA `0x1ce00` сохраняют host/service указатели, а Control -`0x1fe00` меняет только локальный collision-флаг. Эти ветви не вызывают AniMesh -scale setter `0x14770` и не пишут поля масштаба. При загрузке AniMesh получает -Control secondary owner с QI selector `0xa` и сохраняет возвращённый base owner +ветвь активации. Модель занимает отдельный объект в памяти: её конструктор +AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`. +Поле `Agent+0x158` хранит указатель на Control. Проверенные ветви модели +`0x75b0/0x6fc0`, Effect RVA `0x3d30` и CollManager RVA `0x1ce00` сохраняют +матрицу и служебные указатели, оставляя масштаб модели неизменным. + +Фабрика CollObject возвращает вторичный интерфейс `object+4`. QI `0x28` +вычитает `4` и сохраняет в `Agent+0x170` основной объект с vtable `0x3c2fc`. +Его event slot `+0x08` ведёт в Control RVA `0x1f530`. При event `4/null` +обработчик кэширует семь host-интерфейсов; при наличии QI `0x203` вызывает +CollManager slot `+0x10` таблицы `0x3c178`. Этот метод, RVA `0x1c330`, +сохраняет только указатель `CollObject+0x20` в `CollManager+0x3c`. +Для свежего BULL Agent без внешнего host QI `6` при привязке даёт основной Agent +с vtable `0x201d4`. Ненулевой event `4` вызывает его slot `+0x24`, AniMesh +RVA `0x17d60`, который читает `[Agent+0x120]`; вызывающий код сохраняет +результат в `CollObject+0x24`. +Затем CollObject slot `+0x0c`, Control RVA `0x1fe00`, сбрасывает бит `0` +поля `CollObject+0x30`. Event `0x15` с первым DWORD payload `9` завершается +без вызовов и записей. Поле Control `owner+0x34` хранит указатель на CollObject; +его slot `+0x14`, RVA `0x1f670`, меняет флаги `CollObject+0x30`. +Перечисленные ветви не вызывают AniMesh setter `0x14770`. + +При загрузке AniMesh получает Control secondary owner с QI selector `0xa` +и сохраняет возвращённый base owner в `Agent+0x158`: `AniMesh.dll+0x2ff0` делает этот вызов в RVA `0x32ee..0x3300`, а `Control.dll+0x76f0` для selector `0xa` вычитает `0x14` из `ECX` и возвращает указатель. Диспетчер AniMesh RVA `0x1afb..0x1b0d` посылает через `Agent+0x158` событие `4` в slot `+0x08`. Его base-owner vtable — Control RVA `0x3d298`, и slot ведёт прямо в `0x314f0`; при нулевом payload исходный обработчик проходит -`0x7830 -> 0xf940`, поэтому FAC0 входит с constructor scales `[1, 1, 1]`. +`0x7830 -> 0xf940`, поэтому FAC0 получает Model scale `[1, 1, 1]`. Встречающиеся рядом переходники относятся к другим интерфейсам: Control owner `+4` с таблицей `0x3d254` проходит через `0x31ae0` (adjust `-4`), а owner `+8` с таблицей `0x3d1fc` — через `0x31b00` (adjust `-8`). Последний путь не является @@ -1166,6 +1495,15 @@ descriptor word `8`. Текущий tag `26` тем же общим reader-ом умножает на `M`. Это правило не меняет отдельные callbacks бюджета или запись factory-26 selector `0x500`. +Сырой selector `6` в `FUN_1002bb40` выбирает устройство по ordinal через +интерфейс `owner+0x0c`: таблица `this+0x5b0` соответствует `owner+0x5bc`. +RVA `0x2bb91` проверяет тег текущего descriptor на равенство `2`. При совпадении +RVA `0x2bb9a` загружает `device+0x174` как `f32`, сохраняет значение в выходной +буфер и возвращает `AL=1`/`RET 8`; другие текущие теги возвращают false. +Наличие поля `+0x174` подтверждено для экземпляра фабрики `2`. INTO сохраняет +исходный тип экземпляра, поэтому проверка тега descriptor отдельно не доказывает +наличие этого поля у другой фабрики. В Rust selector `6` пока не поддержан. + FCA0 передаёт в owner query порядковый номер `i`; query `0x2bb40` находит устройство `i` и вызывает его reader. Уже reader `FUN_10021d00` получает provider key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` (RVA `0x0dc40`) @@ -1173,6 +1511,80 @@ key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` ( строку `table + key*0x2c`; текущее type-1 значение находится по `+0x14` строки. Индекс устройства и ключ provider row — разные значения с разными задачами. +#### Индексный Control getter: сырой селектор `0x400` + +В той же сборке `Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +индексный query для `QueryInterface(0x204)` входит через слот `+0x18` vtable +`0x3d1b4`: `FUN_1002bb40` (RVA `0x2bb40`, `RET 8`, `ECX=owner+0x0c`, ordinal +устройства в `EDX`, selector и output pointer на стеке). При сыром selector +`0x400` функция попадает в ветвь RVA `0x2bc3c`. +Ветка получает provider pointer из `device+0x44`, отдельно читает provider key +из `device+0x04` и вызывает provider vtable slot `+0x0c` в режиме `1`, передавая +адрес ячейки selector как storage результата. После вызова значение переносится +в выходной `f32` caller-а; ветвь возвращает `AL=1` без проверки текущего +descriptor tag или factory tag. + +Поле `device+0x44` указывает на provider interface `owner+0x04` с vtable +`0x3d254`; slot `+0x0c` ведёт к `FUN_1000dc40` (RVA `0x0dc40`). Getter +использует key для строки общей таблицы, а не ordinal устройства. Для валидной +строки selector `1` читает её текущее type-1 поле `+0x14` напрямую: +`0x400` поэтому не пересчитывает долю из amount и maximum и не умножает её на +коэффициент устройства, `M` или descriptor word. В Rust тот же маршрут выглядит +так: + +```text +device = devices[device_index] +value = providers[device.key].current_type1_value +return Some(value) +``` + +Индекс устройства сначала выбирает запись, её key затем выбирает provider row; +этот indexed-вызов не является глобальным запросом `0x0f` и не является +metadata getter IID `0x16`. Проекция Rust принимает конечные нормальные значения +и точные `+0`/`-0`, сохраняя биты нулевого значения; остальные float-диапазоны и +недоступные table/key остаются типизированными ошибками. Native selector-cell +alias/fallback вне валидной записи provider не реализован: Rust не подставляет +для такого пути `0.0` или `None`. + +Шесть изолированных вызовов оригинала проверяют именно этот валидный +indexed-вызов. +В каждом ordinal устройства равен `2`, а ключ provider равен `1`, `3` или `4`; +все значения строк различаются, поэтому ключ не совпадает с ordinal и не +подменяется соседней строкой. + +| Вектор | Ordinal | Key | Raw output | AL | +| --- | ---: | ---: | ---: | ---: | +| Значение из корпуса `1.0` | 2 | 1 | `0x3f800000` | 1 | +| Доля `0.625` | 2 | 1 | `0x3f200000` | 1 | +| Отрицательное конечное `-2.5` | 2 | 3 | `0xc0200000` | 1 | +| `+0` | 2 | 1 | `0x00000000` | 1 | +| `-0` | 2 | 3 | `0x80000000` | 1 | +| Отличающаяся строка `0.0625` | 2 | 4 | `0x3d800000` | 1 | + +Вектор `0.625` специально задаёт native word `device+0x50 = 0`; он всё равно +возвращает текущее поле provider. Таблица устройства, provider и остальные +строки были синтетическими, ограждёнными входами; исполнены оригинальные +`FUN_1002bb40` и provider getter `FUN_1000dc40`, без constructors, ControlSystem +или mock callbacks. Все входные записи остались неизменными, проверены canary +области, а изменился только DWORD выхода. Для DLL с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` входной +control word `0x027f` был временно заменён на `0x007f` и восстановлен после +вызова; у всех шести ветвей `AL=1`. Выходы совпали с прямыми значениями выбранных +synthetic rows. Это не снимок состояния оригинальной фабрики или миссии: полный +IID `0x204`, интеграция Behavior и gameplay-трасса не проверены. + +Независимый разбор лицензированного AutoDemo дал восемь таблиц и 100 пар +устройство/key, но все выбранные type-1 значения там равны `1.0`. Такой набор +проверяет загрузку таблицы, ключи и доступность запросов, но не позволяет +различить выбор строки по key и по ordinal. Игнорируемый Rust acceptance test +проверяет все 100 запросов на этом корпусе и ожидает `0x3f800000`. В тесте явно +заданы `MissionStartupInput { queue_time_ms: 100_000, +research_creation_gate: false }`; эти значения являются входами Rust-проверки, +а не доказательством штатного gate оригинала. Native синтетические строки дают +отдельное доказательство выбора key. Оба результата имеют ограниченную область +и не являются сравнением полного поведения игры. + Конструктор owner `FUN_10006bf0` (RVA `0x6bf0`) задаёт scalar `+0x1a8 = 1.0` и вызывает `FUN_10014260`, который записывает `1.0` в `+0x354/+0x358`. Проверенные ветви канонического event `4/null` и FAC0 не меняют эти три поля |
