aboutsummaryrefslogtreecommitdiff
path: root/docs/appendices/script-vm.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/appendices/script-vm.md')
-rw-r--r--docs/appendices/script-vm.md468
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 не меняют эти три поля