aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md234
-rw-r--r--docs/appendices/script-vm.md255
-rw-r--r--docs/tomes/06-behavior.md160
3 files changed, 550 insertions, 99 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index 07ba5d7..bf32aec 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -133,7 +133,7 @@ control word процесса.
Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами
`[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0),
(728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого
-script base и поддерживает numeric literal assignments в первом `Init`.
+script base и поддерживает узкий набор атомарных формул в первом `Init`.
Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки
чтения распространяются как VFS errors.
@@ -144,9 +144,11 @@ script base и поддерживает numeric literal assignments в первÐ
два доказанных действия: `Handler(19)` и formula assignment selector `-1` при
source `-1`, нулевой глубине условий и корректных индексах. У `Handler(19)`
control находится в `header_words[3]`, а `header_words[1]` — result-save
-reference; оба равны `0xffffffff` в поддержанной форме. У formula assignment
-`header_words[1]` — индекс целевой varset ячейки, `header_words[2]` — source,
-а индекс formula row занимает `header_words[6]`. Маппинг подтверждён
+reference; оба равны `0xffffffff` в поддержанной форме. В дисковом заголовке
+formula assignment `header_words[1]` — индекс целевой varset ячейки,
+`header_words[2]` — source, а индекс formula row занимает `header_words[6]`;
+индексы цели и формулы в native record находятся по смещениям `+0x08` и `+0x1c`
+соответственно. Маппинг подтверждён
`ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA
`0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
`Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`;
@@ -154,23 +156,58 @@ reference; оба равны `0xffffffff` в поддержанной форме
Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0`
(RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`)
-строит numeric literal node, а evaluator `FUN_10015b30` (RVA `0x15b30`)
-возвращает его raw `f32`. Target type `3` сохраняет эти bits; type `5` проходит
-через `FUN_10012c00` (RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD
-с усечением к нулю. Изолированный x86 oracle на оригинальном `ai.dll` и
-настоящих ресурсах подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
+строит числовые, переменные и операторные узлы, а evaluator `FUN_10015b30`
+(RVA `0x15b30`) в проверенных путях `+` и `-` считывает значения переменных и
+сохраняет результаты как `f32` при x87 control word `0x007f`. В GOG-сборке
+`ai.dll` с SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` поиск
+переменной через varset vtable slot `+0x10` ведёт на RVA `0x16ad0`: сравнение
+байтов точное, регистр учитывается, при повторах берётся первое объявление.
+Getter `FUN_10013190` (RVA `0x13190`) читает type `3` как `f32`, type `5` как
+беззнаковый `DWORD` и возвращает число через x87. Затем evaluator сохраняет
+его как `f32` до операции. Оператор `+`
+считает right+left, `-` — left-right; результат также сохраняется как `f32`.
+Target type `3` сохраняет эти bits; type `5` проходит через `FUN_10012c00`
+(RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD с усечением к нулю.
+Изолированный x86 oracle на оригинальном `ai.dll` и настоящих ресурсах
+подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
26/5/149 строками и 15 присваиваний: кампанийные `df0..df3` и `Single.01`
`dT8/dT9` получают `0`; `scream` задаёт `fDifficulty = 0.8f` и остальные
проверенные DWORD `0, 0, 1, 10, 0, 100, 400, 2`.
-Граница Rust остаётся узкой: ссылка должна вести на один digit-starting numeric
-token и типизированную `float`/`DWORD` ячейку. Неизвестные expression nodes,
-переменные, unary operators, составные выражения и source-copy ветвь
-`source != 0xffffffff` остаются явными ошибками; native source-copy переносит
-всю varset record, не только числовое значение. В preview package сохраняется,
-но `Unsupported` не применяет частичные writes; полная загрузка отклоняет весь
-новый мир и сохраняет предыдущие world/RNG. Это политика Rust, не правило
-оригинального dispatcher-а.
+Граница Rust остаётся узкой: один числовой токен либо ASCII-имя переменной,
+или два таких атома с одним бинарным `+`/`-`. Имя проверяется раньше числового
+преобразования, сравнивается с учётом регистра и выбирает первое совпадение;
+публичный `FormulaTable::evaluate_numeric_literal` принимает только числовые
+литералы. Временная копия значений начинается со значений по умолчанию и
+обновляется после каждого успешного действия `Init`, поэтому следующая формула
+видит предыдущие записи. Нет
+поддержки неизвестных операторов, унарных форм, третьего атома, отсутствующих
+или не-ASCII имён и source-copy ветви `source != 0xffffffff`; native source-copy
+переносит всю запись varset. Preview сохраняет package, но не применяет частичные
+writes; полный load отклоняет новый мир и сохраняет прежние world/RNG. Это
+политика ограниченного Rust resolver-а, а не правило оригинального dispatcher-а.
+
+Ресурсные строки с переменными найдены в поздних событиях: `c1m3e.fml` row 19
+(`0.8 + fT`) вызывается из `Fort_Captured`, row 22
+(`dBuildingProtectHits - dT2`) — из `PBM_BUILDING_PROTECT_Start`, а `c1m4e.fml`
+row 0 (`fTemp + 0.00001`) — из `PBM_BUILDING_INF_CAPTURE_Start`. Декодирование
+этих `.scr` не показывает эти формулы в первом `Init`. Игнорируемая проверка
+читает настоящие `.fml` и `varset.var`, но вставляет строки в синтетический
+`Init`; она проверяет resolver, а не оригинальное исполнение. Одноразовый x86
+oracle на исходной GOG `ai.dll` с SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` проверил
+13 отдельных присваиваний: 9 контролируемых выражений и 4 вызова с настоящими
+строками `.fml` (`c1m3e` rows 19/22, `c1m4e` row 0 дважды подряд). В каждом векторе
+native harness записывал вычисленный `f32`, типизированное raw-слово и getter;
+scratch-проход через публичный Rust parser/resolver совпал со всеми 13
+векторами по `f32`-результату и raw-записи. Для native вызовов control word
+временно принудительно устанавливался в `0x007f`: 87 наблюдений сохранили
+`0x027f` на входе и после восстановления,
+а canary-проверки прошли. Менялись только varset-копии в памяти. Это
+подтверждает компиляцию формулы, локальное вычисление и typed writes для этих
+входов, но не запуск исходного поиска и обхода события `Init`, исполнение поздних
+событий или поведение живой миссии.
Лицензированный test загружает одну первую кампанийную миссию
`CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход
@@ -184,6 +221,34 @@ core и `ResearchCandidateSet::refresh_selection`: последний получ
создаёт владельца живого контекста, не передаёт реальные AI candidates и не
выбирает результат Handler(43). Это не общий dispatcher и не загрузка игры.
+Начальный массив слота AI `+0x38` теперь восстановлен отдельно от этого
+исследовательского пути. В `iron3d.dll` SHA-256
+`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`
+`FUN_100392e0` (RVA `0x392e0`) вызывает слот для каждого значения счётчика
+`second_resource.tag`. В `ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` метод
+RVA `0x2010` передаёт `AI+0x83c` в `FUN_10002980` (RVA `0x2980`), где
+`FUN_100048f0` (RVA `0x48f0`) добавляет переданный DWORD. Вызывающая сторона
+повторно передаёт `0xffffffff`; эта функция не записывает значение обратно.
+Поэтому подтверждён только начальный массив sentinel-слов. Неизвестно, какие
+поздние обработчики читают эти ячейки и заменяет ли кто-либо их ID. В начальном
+массиве нет назначенных ID объектов; дальнейший смысл этих слов пока не
+установлен. Сам массив не является списком
+`BehaviorDistributorState.registered_objects` и не определяет владельца
+контекста исследований.
+
+Rust хранит этот снимок как `MissionAiStartupState` в состоянии загруженной
+миссии. Полная загрузка проходит по всем TMA-кланам, включая кланы без пакета
+сценария и без пути таблицы исследований; при предпросмотре список состояний AI
+остаётся пустым. Знаковое поле `second_resource.tag` читается по сырым битам как
+`u32`. Бюджет в 1 000 000 DWORD на миссию и выделение памяти с обработкой ошибки
+защищают только память Rust; это ограничение не приписывается оригинальному
+движку. Искусственные Rust-тесты проверяют обе формы TMA-клана, индекс клана,
+независимые массивы, пустой путь сценария, предпросмотр, чтение тега как
+беззнакового числа, лимит и откат при ошибке. Проверка на оригинальных ресурсах
+для `Single.01` подтверждает состояния обоих кланов (`9/9`) до выполнения
+`Init`; это не полный запуск миссии и не исполнение исходных DLL.
+
Для GOG-сборки также найден статический host call path в `Init`. В
`iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи
кланов и вызывает для них `FUN_100392e0`, затем вызывает `FUN_10039440` (RVA
@@ -262,6 +327,26 @@ Behavior (`Behavior.dll`, RVA `0x3600`) вызывает `World3D!stdGetGameMode
с реальными интерфейсами ArealMap, нужными `FUN_10019e80`; полный Behavior tick
flow и общий dispatcher событий также остаются открытыми.
+`loaded_mission_behavior_control_groupings` публикует только начальную
+группировку подготовленной Control-таблицы. Ветка с меткой `2` исходного
+`Behavior.dll!FUN_1001be10` (RVA `0x1be10`) дополнительно вызывает
+`FUN_1001b4b0` и обращается к свойствам через QI `0x204`, включая запросы с
+номерами `0x700`, `6` и `0x400`. Результат `0x700` попадает в строку кандидата
+по смещению `+0x3c`, результат `6` — в `+0x40`, а `0x400` может обнулить второе
+поле. Их доменный смысл, множитель `0x700`, агрегаты Behavior QI `0x204` и
+прочие числовые поля кандидата не восстановлены. Rust не подставляет множитель
+`0x700`, нулевые агрегаты, выбранный индекс или эффект. Список для метки `30` не
+встречается в ресурсах AutoDemo и проверяется только на искусственном наборе.
+
+Игнорируемая проверка AutoDemo сравнивает порядковые номера записей начальной
+группировки, отдельно выведенные по снимку текущих меток дескрипторов, с
+состоянием Rust после полной загрузки. Это подтверждает согласованность разбора
+подготовленной таблицы и правила группировки на этих ресурсах, но не является
+захватом состояния Behavior оригинала и не устанавливает, когда список
+прикрепляется или обновляется в живом объекте. Полный штатный путь загрузки и
+запуска, агрегаты Behavior QI `0x204`, обновление после изменений Control,
+`Handler(43)` и полный игровой цикл остаются отдельными границами.
+
Начальные свойства `CurrentOre`/`MaximumOre` восстановлены отдельно от
регулярного обмена. В проверенных `Behavior.dll` (SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) RVA
@@ -405,7 +490,9 @@ Global Control getters `1/2` и setter `1` проверены в native x86 orac
этого класса native oracle не запускал. Rust API обращается к строкам проекции
конкретного живого `ObjectHandle`; это запрос к уже загруженной таблице, а не
исполнение Control или Behavior tick. Setter `17`, остальные factory tags и
-внешние эффекты их методов не включены. Для tag `9` selector `0` использует
+внешние эффекты их методов не включены. Структурное чтение записи CTLD с
+меткой 17 ещё не восстанавливает её обработчики и игровое поведение.
+Для tag `9` selector `0` использует
отдельный scale `+0x11c`; остальные поддержанные классы используют коэффициент
напрямую. Пустой набор и нулевой знаменатель остаются native no-result, а
неподдержанный factory tag и расчётный диапазон произведения/отношения за
@@ -512,12 +599,48 @@ callbacks. Licensed двухстадийный узел шахты подтвеÑ
новый API при доле `0.5`, без FX и window path. Гибель root/critical узла и
неизвестные flags/aggregate также остаются явными ошибками.
-Rust query обращается к загруженному объекту и поддерживает `0xe00` только для
-текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и
-factory paths остаются явными границами. Запрос читает текущее type-1 значение.
-Licensed `min_l_n1.dat` подтверждает изменение `0xe00` и Behavior `0x1002` с
-`1.0` до `0.5` после явного уменьшения количества вдвое; чтение получает
-текущее значение provider, который также меняет явный бюджетный шаг.
+Rust query обращается к загруженному объекту и поддерживает `0xe00` для пар
+текущего descriptor tag и factory tag `(5,5)`, `(5,26)`, `(26,5)` и `(26,26)`;
+прочие специализированные tag/factory пути остаются явными границами. Запрос
+читает текущее type-1 значение provider. Licensed `min_l_n1.dat` подтверждает
+изменение `0xe00` и Behavior `0x1002` с `1.0` до `0.5` после явного уменьшения
+количества вдвое; чтение получает текущее значение provider, который также
+меняет явный бюджетный шаг.
+
+Indexed selector `0x400` в том же Control IID `0x204` возвращает текущее type-1
+значение строки provider, выбранной через key устройства, при любом descriptor
+и factory tag. Rust принимает конечное normal-or-zero значение, включая оба
+знака нуля, и сохраняет его биты; недоступная таблица, отсутствующий key и
+значения вне этой численной области дают типизированную ошибку. В оригинальном
+`Control.dll` с SHA-256
+`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` ветвь
+`FUN_1002bb40` (RVA `0x2bc3c`) читает provider pointer `device+0x44` и key
+`device+0x04`, вызывает provider slot `+0x0c` в режиме `1` и не проверяет
+descriptor/factory tag. Rust-проекция читает сохранённый снимок; она не вычисляет
+value из amount, maximum, coefficients или множителя `M`. Native selector-cell
+alias/fallback за пределами валидной записи и поведение native при невалидном
+key остаются неисследованными; Rust не подставляет для них `0.0` или `None`.
+Это покрывает перечисленные query-пути. Полный интерфейс IID `0x204`, его
+взаимодействие со всеми Behavior-вызовами и полный игровой tick остаются за
+границей подтверждённого поведения.
+
+Объём свидетельств для `0x400` ограничен и состоит из трёх частей. Native
+oracle на настоящем `Control.dll` проверил шесть выходов по raw bits для
+ограждённого синтетического owner/provider table; он вызвал indexed query и
+реальный provider getter, но не создавал оригинальные factory-объекты. Матрица
+выходов находится в [техническом разборе](script-vm.md#индексный-control-getter-сырой-селектор-0x400).
+Независимый разбор AutoDemo обнаружил восемь таблиц и 100 устройств с
+доступными ключами, но все выбранные provider values равны `1.0`; такой набор
+подтверждает загрузку и область ключей, однако не различает, какую именно
+строку выбирает алгоритм. Игнорируемый Rust-тест опрашивает эти 100 устройств и
+проверяет те же биты, не выдавая равенство ресурсных данных за доказательство
+индексирования.
+
+Вместе native-векторы и resource acceptance не подтверждают состояние
+оригинальных factory instances, полный контракт IID `0x204`, полный
+Behavior-call flow или живую миссию/gameplay. Поведение при ошибочных native
+указателях и ключах, а также selector-cell alias/fallback вне валидной строки
+по-прежнему не описано; Rust сохраняет typed errors на этой границе.
Native oracle проверил изолированные пути распределения, обновления класса `4`,
стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` на
interleaved parents `[-1, 0, 0, 1, 3]` с callback-порядком `1, 3, 4, 2` и
@@ -655,16 +778,19 @@ resource-device значения во время игры.
`0x5780/0x57c0` участвуют в векторе; Rust воспроизводит его raw output bits и
отдельные cache, order, clamp и signed-zero случаи.
-Для обычной начальной загрузки scale-input тоже закрыт. AniMesh constructor
-оставляет `+0x158/+0x15c/+0x160` равными `1.0`; перед Control event `4` World3D
-проходит Agent event `0`, AniMesh RVA `0x1a80` вызывает базовый return `0x17df0`
-и передаёт event `4/null` модели, FX, CollManager и CollObject. Их проверенные
-ветви `0x75b0/0x6fc0`, Effect RVA `0x3d30`, Control RVA `0x1ce00` и `0x1fe00`
-не вызывают AniMesh setter `0x14770` и не меняют outer scale. Поэтому FAC0 в
-стандартном `LoadAgent` получает `[1, 1, 1]`. Другой маршрут
-`AddNewObjectToGame`/mirror к setter и последующие placement/tick остаются
-отдельными границами. В synthetic x86-векторе по-прежнему явно заданы `[2,3,5]`;
-он не является источником scale для полного load.
+Для обычной начальной загрузки входной масштаб единичный. Конструктор модели
+AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`;
+`Agent+0x158` хранит указатель на отдельный Control. Перед Control event `4`
+World3D проходит Agent event `0`: AniMesh RVA `0x1a80` вызывает базовый return
+`0x17df0` и передаёт event `4/null` модели, FX, CollManager и CollObject.
+Проверенные ветви модели `0x75b0/0x6fc0`, Effect `0x3d30` и CollManager
+`0x1ce00` не меняют масштаб. CollObject получает событие через основной
+интерфейс `0x3c2fc`, slot `+0x08 -> 0x1f530`: он кэширует host-интерфейсы,
+а необязательный вызов CollManager `0x1c330` сохраняет только указатель
+`CollObject+0x20` в `CollManager+0x3c`. Поэтому FAC0 получает `[1, 1, 1]`.
+Отдельные пути `AddNewObjectToGame`/mirror и последующие placement/tick остаются
+за этой границей. В synthetic x86-векторе scales `[2,3,5]` заданы отдельно.
+Подробная привязка интерфейсов приведена в [разборе Script VM](script-vm.md).
Смысл `Slot68 +0x30/+0x34`, tag-27 cache DWORD, owner accumulators `+0x530`,
`+0x534`, `+0x538` и результата mutable word `26` не имеет доказанных единиц
@@ -819,22 +945,34 @@ arg1 следующего вызова. `AddNewObjectToGame` принимает
`FUN_10002a30` (RVA `0x2a30`, `RET 8`) записывает его в `inner+8`. TMA tag
`row+0x20` ограничивает число вызовов AI slot `+0x38`, но не задаёт владельца.
+Создание AI не фильтруется по наличию сценарного пути. В `iron3d.dll` SHA-256
+`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`
+`FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов и вызывает для каждой
+`FUN_100392e0` (RVA `0x392e0`); в `ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`
+`CreateSuperAI` (RVA `0xf710`) выделяет AI и вызывает конструктор по RVA
+`0x1000` без проверки пути сценария. Создание исследовательского контекста
+следует за успешным созданием AI и требует включённого флага создания
+исследований и непустого `second_resource.path` до первого NUL. Переданный в
+`descriptor[0]` индекс клана, а не `second_resource.tag`, задаёт владельца;
+тег остаётся счётчиком начальных вызовов AI slot `+0x38`.
+
В Rust эта граница представлена только загрузкой таблиц. Полный
`load_mission` требует явный `MissionStartupInput { queue_time_ms,
-research_creation_gate }`; gate не выводится из `MissionMode` или имени пути.
-При `true` загрузчик проходит по успешно разобранным пакетам сценариев, берёт
-реальный `second_resource.path` соответствующего TMA-клана до первого NUL,
-отклоняет не-ASCII путь, затем сохраняет отдельный `ResearchTrfDocument` с
-`clan_index` и нормализованным путём. Пустая строка, выключенный gate или
-отсутствие пакета сценариев пропускают чтение. Повтор одного пути разными
-владельцами приводит к отдельным буферам строк TRF0. Обычные тесты на
-искусственных данных проверяют владельцев, повторное декодирование, пропуски,
-ошибки и откат; игнорируемая проверка оригинальных ресурсов связывает реальные
-`Campaign.00/Mission.01` пары путей с индексами владельцев. Это не загружает
-полный исследовательский контекст оригинала.
-Успешно разобранный пакет сценариев здесь обозначает только границу текущей
-настройки Rust, а не совпадение с полным нативным жизненным циклом
-`CreateSuperAI`.
+research_creation_gate }`; этот флаг не выводится из `MissionMode` или имени
+пути. Если флаг равен `true`, загрузчик проходит по существующим
+`MissionAiStartupState` всех TMA-кланов, в том числе без сценарного пакета,
+читает их `second_resource.path` до первого NUL и отклоняет не-ASCII путь.
+Пустая строка пропускается; выключенный флаг и статический preview не читают
+TRF. На каждого
+владельца сохраняется отдельный `ResearchTrfDocument` с нормализованным путём,
+даже когда разные кланы указывают на один файл. Обычные тесты на искусственных
+данных проверяют владение у кланов со сценариями и без них, повторное
+декодирование, пропуски, ошибки и откат; игнорируемая проверка оригинальных
+ресурсов связывает реальные `Campaign.00/Mission.01` пары путей с индексами владельцев.
+Rust пока сохраняет только декодированные данные и связь с владельцем: он не
+создаёт полный исследовательский контекст, не строит AI-каталог и не исполняет
+`Handler(43)`.
Нативный metadata loader нельзя считать простым копированием таблицы. В GOG
`Behavior.dll` (SHA-256
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index 26ac234..145d2a2 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -22,8 +22,8 @@ symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaul
`fparkan-script::parse_varset` уже читает подтверждённые numeric
`VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются
opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и
-general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые
-строки сохраняются для доказанных literal assignments первого `Init`;
+general `FUNCTION(...)` evaluation остаются вне восстановленного контракта,
+но принятые строки сохраняются для ограниченного поднабора формул первого `Init`;
GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration:
31 `float` и 200 `DWORD` (от `f0` до `fY`);
loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при
@@ -424,10 +424,13 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие
`Init` и проходит его инструкции по порядку. Он возвращает записи для
-поддержанного `Handler(19)` и числовых literal assignments; прочая форма
-останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи
-к отдельной копии varset выбранного клана лишь после успешного прохода всего
-события. Отсутствующий `Init` даёт
+поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо
+с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма
+останавливает preview на `Unsupported`. Runtime начинает вычисление с копии
+defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя
+формула видит прежние записи этого события. Упорядоченные записи попадают в
+состояние клана только после успешного прохода всего `Init`. Отсутствующий
+`Init` даёт
статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт
`Executed` без записей. Если форма инструкции пока не поддержана, package
остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения
@@ -437,7 +440,7 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение
оригинального dispatcher-а.
-#### Числовые литералы `.fml` в `Init`
+#### Литералы и простые формулы `.fml` в `Init`
В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`)
загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`.
@@ -455,16 +458,35 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
остаётся ошибкой VFS.
Native selector `-1` при source `-1` и нулевой глубине условий выбирает
-formula assignment; в native record `header_words[1]` — индекс целевой varset
-ячейки, а `header_words[6]` — индекс formula row. `FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки,
-`FUN_10015b30` (RVA `0x15b30`) возвращает результат единственного literal node
-kind `3`, а `FUN_10013650` передаёт его типизированной ячейке через
-`FUN_10012c00` (RVA `0x12c00`). Компилятор `FUN_100152a0` получает числовой
-prefix как `double` через `FUN_10020af0` (RVA `0x20af0`), затем записывает
-его как `float` в node `+0x10`. Rust ограничивает эту ветвь одним
-digit-starting числовым токеном с необязательным хвостом пробельных байтов;
-ведущие unary `+`/`-`, variable references, операторы и другие node kinds
-остаются явными `UnsupportedExpression`.
+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
@@ -477,8 +499,8 @@ digit-starting числовым токеном с необязательным Ñ
исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`)
с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся
`0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw
-DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого
-`Init` применяется только после успешного прохода.
+DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init`
+публикуются только после успешного прохода.
Native x86 oracle на GOG `ai.dll` SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил
@@ -500,6 +522,27 @@ Native x86 oracle на GOG `ai.dll` SHA-256
| 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 после этих присваиваний; у обычной
@@ -511,6 +554,17 @@ Rust сохраняет всю таблицу строк, но вычисляеÑ
не перечисляют 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
@@ -760,8 +814,52 @@ tie-векторов подтверждает реакцию `FISTP`, но не
два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет
отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого
события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из
-одного numeric literal; остальные формы инструкции и содержимое `.scr`
-сохраняются reader-ом, но остаются вне этого исполнения.
+одного числового или переменного атома либо двух таких атомов с одним бинарным
+`+` или `-`. Остальные формы инструкции и содержимое `.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
@@ -1312,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`). Последний путь не является
@@ -1379,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`)
@@ -1386,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 не меняют эти три поля
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index aa8fd96..3a4e51c 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -169,8 +169,10 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga
показывает предупреждение для чисел выше `10000`, но не отменяет создание AI.
Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)`
-и числовые literal assignments, описанные ниже. Поддержанные записи применяются
-к состоянию лишь после успешного прохода всего первого `Init`.
+и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом —
+число или имя переменной. Поддержанные шаги сразу обновляют временную копию
+varset, поэтому следующая формула читает уже записанные значения. К состоянию
+клана записи попадают лишь после успешного прохода всего первого `Init`.
Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает
`Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма
становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило
@@ -186,15 +188,54 @@ Rust исполняет подтверждённый поднабор перво
нули в `dT8` и `dT9`.
Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь
проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8`
-сохраняется как число `float`. Пока Rust вычисляет только числовой литерал,
-начинающийся с цифры; операторы, ссылки на переменные и другие формы формул
-остаются явными ошибками. В тесте полной загрузки миссия `Mission.01`
-проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном
-`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо
-этого действия.
+сохраняется как число `float`. Например, если `source` равен `10`, выражение
+`source + 2.0` записывает `12`; следующее `target - source` читает новое значение
+`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки
+`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат
+сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один
+бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не
+восстановлены. В отдельной точечной проверке вычисления и записи оригинальной
+DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только
+эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает,
+что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный
+`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется
+вместо этого действия.
### Первый запуск сценария
+Перед настройкой первого шага каждый AI получает начальный массив DWORD. В
+`iron3d.dll` SHA-256
+`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`
+`FUN_100392e0` (RVA `0x392e0`) повторяет вызов слота AI `+0x38` столько раз,
+сколько задаёт `second_resource.tag` этой строки TMA. Этот слот ведёт в
+`ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`:
+метод RVA `0x2010` передаёт коллекцию при `AI+0x83c` в `FUN_10002980`
+(RVA `0x2980`), а она добавляет один DWORD через `FUN_100048f0` (RVA
+`0x48f0`). Каждый вызов копирует `0xffffffff`; входное слово не меняется.
+Поэтому тег `3` даёт `[0xffffffff, 0xffffffff, 0xffffffff]`. Это число
+начальных ячеек, а не назначение трёх ID.
+
+Изолированная x86-проверка вызовом оригинальной `ai.dll` подтвердила пары
+«число слотов/ёмкость» `0/0`, `1/17`, `17/17` и `18/34`. Все добавленные слова
+равны `0xffffffff`, а входное значение, контрольные поля, память вне заголовка
+массива и указатель стека сохраняются. Это подтверждает добавление начального
+слова, но не работу последующих обработчиков.
+
+Rust хранит такой снимок в отдельном `MissionAiStartupState` для каждого TMA-клана,
+а не в пакете сценария. При полной загрузке (`Full`) создаётся запись для каждого
+клана, в том числе если у него нет пути сценария или пути таблицы исследований. Счётчик
+берётся из сырых битов `second_resource.tag` как `u32` у обеих форм записи —
+`Standard` и `Spatial`; нулевой счётчик даёт существующее состояние с пустым
+списком. Статический режим предпросмотра не создаёт состояний AI.
+
+Загрузка отклоняет суммарный объём свыше 1 000 000 DWORD и резервирует память
+проверяемой операцией; это защита Rust от чрезмерного выделения памяти, а не
+ограничение оригинальной игры. Rust сохраняет начальные слова, но не
+восстанавливает работу последующих обработчиков и не назначает этим словам ID
+объектов. Этот массив также не является списком
+`BehaviorDistributorState.registered_objects`.
+
До регулярной работы Behavior настраивает расписание своего первого шага. Он
берёт текущее 32-битное время игровой очереди и назначает проверку через три
интервала по 64 мс, добавляя небольшую случайную задержку от 0 до 63 мс. Эта
@@ -213,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
значение времени очереди; после регистрации объектов загрузчик последовательно
подготавливает каждый сценарный пакет и выполняет восстановленные действия
-`Handler(19)` и числовые literal assignments из первого `Init`. Статический
+`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический
просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
varset.
@@ -260,6 +301,25 @@ Rust-тест повторно читает лицензированную миÑ
TMA-записями и проверяет реальные `ObjectHandle`, но помечен `#[ignore]`, чтобы
не требовать оригинальных ресурсов от остальных разработчиков.
+После подготовки Control-таблицы можно разобрать и её начальную группировку для
+Behavior. Важны текущая метка записи и её порядковый номер в общей таблице:
+метка `1` начинает пустую группу, метка `2` становится кандидатом последней
+группы, а метка `30` записывает строку в отдельный список той же группы.
+Кандидат, встретившийся до первого заголовка, остаётся в списке без группы;
+ранняя метка `30` игнорируется. Например,
+для текущих меток `[2, 30, 1, 30, 2]` получаются незагруппированный кандидат
+`[0]` и группа с заголовком `2`, кандидатом `[4]` и строкой с меткой `30` под
+порядковым номером `[3]`. Все номера начинаются с нуля и отсчитываются по общей
+Control-таблице, а не по ключам устройств или записям CTLD.
+
+Полная загрузка Rust сохраняет это начальное состояние рядом с подготовленными
+Control-таблицами по исходному TMA ID и `ObjectHandle`. Если таблица недоступна,
+проекция сохраняет причину ошибки; доступная пустая таблица означает пустую
+группировку. При предварительном просмотре список группировок не создаётся. Здесь
+разбирается только подготовленный порядок текущих Control-дескрипторов. Проекция
+не устанавливает, когда список присоединяется к живому Behavior, как он затем
+обновляется и как работает полный игровой цикл.
+
Список распределителя хранит не только участников, но и долю каждого. При
добавлении двух объектов оба получают по половине общего веса. Если добавить
третий, движок оставляет долю новой записи около одной трети, а оставшийся вес
@@ -527,8 +587,14 @@ let cost = calc_full_research_cost(&candidate, &table)?;
штатный Handler(43) и запуск миссии здесь не проверялись.
Владелец начальной записи исследования — индекс текущего прохода по кланам.
-Движок сохраняет его в `descriptor[0]`, а затем переносит в контекст, чтобы
-связать запрос с AI-данными того же клана.
+В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов
+и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI;
+`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор
+по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского
+пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие
+сценарного пакета условием не является. Индекс клана сохраняется в
+`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными
+того же клана.
Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из
`GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов
@@ -538,12 +604,14 @@ Handler(43) и штатный запуск миссии остаются непÑ
Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди
и отдельный `research_creation_gate`. Режим миссии и имя файла этот
-флаг не заменяют. При включённом флаге таблица загружается только для клана,
-чей сценарный пакет уже прочитан и успешно разобран; путь берётся из второго
-ресурса этого клана как C-строка. Пустая C-строка пропускается, а не-ASCII
-байты дают явную ошибку. Клан индексируется по порядку в TMA, тег ресурса на
-владение не влияет. Если несколько кланов указывают на один файл, каждый
-получает отдельно декодированную таблицу с собственными строками TRF0.
+флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся
+`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного
+пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой
+путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку.
+Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег
+`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов
+указывают на один файл, каждый получает отдельно декодированную таблицу со
+своими строками TRF0.
```rust
MissionStartupInput {
@@ -552,9 +620,10 @@ MissionStartupInput {
}
```
-Это сохраняет исходные таблицы и их связь с владельцами для следующих этапов.
-Оно ещё не создаёт полный исследовательский контекст оригинала и не запускает
-`Handler(43)`; эта обработка пока остаётся явно неподдержанной.
+Это сохраняет декодированные таблицы и связь с владельцами для следующих
+этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не
+строит AI-каталог и не запускает `Handler(43)`; эти части пока не
+восстановлены.
Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция
@@ -1070,8 +1139,31 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та
последовательность provider-строк: её длина берётся из NDP, она не дополняется
до числа узлов MSH и не участвует во второй перенумерации ключа.
-Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке
-`Behavior.dll` (SHA-256
+Индексный selector `0x400` выдаёт значение выбранной строки provider как есть.
+Сначала общий индекс устройств выбирает дескриптор, затем его ключ выбирает
+строку NDP; это разные номера. Результатом становится текущее type-1 значение
+этой строки, а не запас `CurrentOre`, предел или значение после умножения на
+коэффициент и `M`. Например, provider-строка с `0.625` возвращает `0.625`, а
+записанный в ней ноль тоже остаётся успешным результатом. Rust использует уже
+подготовленную таблицу и сообщает отдельную ошибку, если provider-таблица или
+строка по ключу недоступна. Этот запрос читает снимок состояния и сам не меняет
+таблицу и не запускает её обновление.
+
+У правила есть два разных подтверждения. Разбор AutoDemo нашёл восемь таблиц
+Control и 100 устройств; все 100 значений по ключам устройств в этом корпусе
+равны `1.0`, поэтому Rust-проверка подтверждает реальные ключи и доступность
+строк, но сама по себе не различает выбор по ключу и по номеру устройства.
+Отдельная x86-проверка исходного `Control.dll` включала шесть вызовов на
+синтетической таблице, где значения строк были заданы вручную и различались.
+Например, при номере устройства `2`, ключе `1` и нулевом слове
+`device+0x50` оригинал вернул `0.625` из выбранной строки. Точные биты ответов
+приведены в [техническом разборе](../appendices/script-vm.md#индексный-control-getter-сырой-селектор-0x400).
+Этот опыт не создавал оригинальные factory-объекты и не исполнял полную миссию:
+полный интерфейс IID `0x204`, интеграция Behavior и игровое поведение остаются
+непроверенными.
+
+В цепочке добычи руды оригинал использует описанный выше selector `0x300`.
+В той же сборке `Behavior.dll` (SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`)
`FUN_100186b0` (RVA `0x186b0`) перечисляет устройства типа `0x1a`, а
`FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector
@@ -1483,7 +1575,7 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк
`f_gener_ball` с offset 85.
Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`.
-Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель
+Для tags `2`, `3`, `4`, `5`, `8`, `10`, `17`, `21` и `27` подтверждён общий читатель
`+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через
обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в
позиции `+0x1C`. Читатель потребляет
@@ -1494,6 +1586,28 @@ Control.dll `+0x8B10` передаёт указатель текущей запÐ
а также отказ при оборванном завершающем NUL. Это подтверждает структурный
формат, но не назначает смысл всем коэффициентам устройств.
+Путь записи с меткой `17` проверен на GOG `Control.dll` с SHA-256
+`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`:
+ветвь `0x11` фабрики `+0x2D4B0` вызывает конструктор `+0x24760`, который
+устанавливает указатель на виртуальную таблицу `0x1003C840`. Ячейка `+0x1C`
+этой таблицы ссылается прямо на общий читатель `+0x21D50`. В ресурсах
+`weapon.rlb:bm_m_01.ctl` и `weapon.rlb:bm_l_01.ctl` таблица привязок
+заканчивается на смещении `0x120`, где начинается запись с меткой `17`.
+Общий префикс размером `0xB0` байт включает саму метку; счётчик по `+0xAC` и
+слово длины в этих двух ресурсах равны нулю, поэтому указатель чтения
+переходит на `0x1D4`. Там начинаются `0x54` байта заголовка таблиц эффектов;
+таблицы содержат соответственно `4`, `2`, `2` и `2` строки по 100 байт и
+заканчиваются на `0x620`. Записи из реестра объектов `bm_m_01` и `bm_l_01`
+при обычной подготовке ресурсов сохраняют явно указанные списки точек CPT
+(`bm_m_a.cpt` и `bm_l_a.cpt`) и таблицы узлов NDP (`bm_m_01.ndp` и
+`bm_l_01.ndp`); в первой таблице эффектов находятся три строки type 4.
+Синтетический тест проверяет непустые массивы значений и строки у записи с
+меткой 17 и следующей записи с меткой 4, затем переход к таблице эффектов.
+Если считать 17 неизвестной, чтение остановится перед
+ней; общий разбор позволяет пройти запись и прочитать следующие таблицы,
+оставляя слова tag 17 числами без толкования. Обработчики этой метки и их
+игровое поведение ещё не восстановлены.
+
Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки
на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка
читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT