aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md425
-rw-r--r--docs/appendices/script-vm.md468
-rw-r--r--docs/appendices/ui-shell.md110
-rw-r--r--docs/reference/environment-effects.md61
-rw-r--r--docs/reference/msh.md109
-rw-r--r--docs/reference/render-frame.md52
-rw-r--r--docs/tomes/03-resources.md15
-rw-r--r--docs/tomes/05-render.md14
-rw-r--r--docs/tomes/06-behavior.md301
9 files changed, 1435 insertions, 120 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index eac2321..bf32aec 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -10,6 +10,62 @@ authoring API без динамического подтверждения.
Не доказаны символами точные имена renderer vtable slots, полный набор CShade
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
+После ограничения цели рельефом интерактивная камера проверяет движение
+против каждого отрисовываемого корневого строения класса `FORT` и его
+отрисовываемых компонентов. Для этого пути используются текущая Node38-поза
+компонента и его преобразование присоединения. При попадании камера сохраняет
+прежнюю позицию, но оставляет новый поворот. Так приложение связывает мировой
+MSH-запрос со свободной preview-камерой; это не подтверждает алгоритм
+стратегической или следящей камеры оригинала.
+
+Локальный MSH-запрос `query_slot_segment` сверён с оригиналом на фабричной
+стене и дверном проёме. Runtime-адаптер `query_building_segment_world` теперь
+повторяет helper batch bit `0x0008` через связанный узел, указанный raw-словом
+`Batch20 +0x06`. При повторе он обходит Node38-поддерево: сначала допустимых
+потомков, затем сам узел. Флаги ветвления сверены с оригиналом. Синтетические
+тесты покрывают связи, отсутствие геометрии у промежуточного узла, маски,
+равные попадания и предел в 200 пересечений.
+
+Игнорируемый тест приложения загружает настоящий PLANT 4 из AutoDemo и его
+исходную CTLD-привязку Node3 (`frame_a = 0`, `frame_b = 1`, blend `0`). В
+контроллере корневого PLANT нет записей tag-21 для открытия двери. В исходной
+закрытой позе камера блокируется о дверь через обычный путь обновления
+приложения. Для второго запроса тест вручную задаёт время `1` только Node3,
+обновляет иерархию позы компонента и преобразование присоединения, затем
+проверяет, что камера проходит через проём. Это подтверждает только подачу
+выбранных поз в коллизию приложения: тест не запускает событие открытия, не
+доказывает видимое открытие двери и не сравнивает полный игровой цикл с
+оригиналом.
+
+Открыты выбор первого корня и правила состава узлов/slot-ов оригинального
+мирового запроса, сравнение полного обхода с оригиналом, а также связь с
+игровыми позами и событиями. Текущий путь приложения использует все
+отрисовываемые компоненты строений `FORT`, но их выбор ещё не сопоставлен с
+поведением оригинала для первого корня и всего мира. Совпадение со
+стратегической и следящей камерами не подтверждено
+([MSH](../reference/msh.md)).
+
+Для MSH без потока цветов type 16 подтверждён только один постоянный alpha
+override для диапазонов с CPU-цветом и raw-флагом `Batch20` `0x0008`: GOG
+`Terrain.dll`, SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`, RVA
+`0x1B0F0`, задаёт alpha `0`, если установлен bit `0x0040` и сброшен `0x0010`.
+Это покрывает `0x0148`; `0x0108`, `0x0118` и `0x0058` сохраняют alpha MAT0,
+причём в `0x0058` bit `0x0010` имеет приоритет. Поток type 16 или отсутствие
+`0x0008` исключают override. Проверка заводской сцены с фиксированной нативной
+камерой подтвердила на одном кадре входа восстановление X-образного рисунка
+двери, который виден и в оригинале; прежний Vulkan-readback показывал на этом
+месте сплошной чёрный прямоугольник. Сравнение выполнено при времени сцены
+2000 мс и трёх кадрах, запуск с Vulkan validation завершился без предупреждений
+и ошибок. Это подтверждает только структуру проёма в этой сцене: правильность
+освещения и полный попиксельный паритет не установлены.
+
+Общее fade-поведение по расстоянию в preview не реализовано. Открыты точная
+метрика расстояния, выбор первого CPU-принятого треугольника диапазона и
+эквивалентность frustum/culling-отбора оригинала; эти вопросы нельзя вывести из
+захвата заводской двери. Данные потока type 16 ещё не декодированы, поэтому
+правило выше относится только к MSH-диапазонам без этого потока цветов.
+
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
log на минимальных сценах с одним типом материала.
@@ -74,22 +130,124 @@ Iron3D и указатели `GetSuperAI` из `ai.dll` (таблица RVA `0x5
tie-векторы проверяют, как сам `FISTP` реагирует на каждый RC, но не измеряют
control word процесса.
-Отдельно проверены загрузка лицензированного AutoDemo в Rust и preview
-лицензированной кампании. AutoDemo даёт raw TMA anchor-ы
-`[(500.2857, 751.8217), (728.0, 449.0)]` и записи `[(500, 752, 0),
-(728, 449, 1)]`; preview кампании сохраняет package и статус `Unsupported`,
-не применяя частичные записи. Это подтверждает загрузчик и узкий Init resolver
-Rust.
+Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами
+`[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0),
+(728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого
+script base и поддерживает узкий набор атомарных формул в первом `Init`.
+Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки
+чтения распространяются как VFS errors.
Нативный конструктор показывает предупреждение при координате выше `10000`, но
продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение;
на первом `FISTP qword` он явно завершает загрузку ошибкой для NaN, infinity и
значений вне signed-qword диапазона. В первом событии `Init` Rust поддерживает
-только action-форму `Handler(19)` (`control word` и `result reference` равны
-`0xffffffff`). Неизвестная инструкция даёт `Unsupported` в preview, не применяя
-накопленные частичные записи; при полной загрузке это ошибка. Это ограничение
-Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные ссылки и
-типы остаются ошибками в обоих режимах.
+два доказанных действия: `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]`;
+индексы цели и формулы в 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`;
+`header_words[2]` его result-save guard-ом не проверяется.
+
+Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0`
+(RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`)
+строит числовые, переменные и операторные узлы, а 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 остаётся узкой: один числовой токен либо 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; это не проход
+всей цепочки кампании. Обычная `Single.01` доходит до следующего blocker-а:
+`Handler(43)` в clan 1, `Init` instruction 10. Rust не перескакивает его;
+preview оставляет varset этого клана на defaults, а полный load завершает ошибкой
+с rollback. В `fparkan-assets` теперь есть bounded TRF reader, pure ore/power
+core и `ResearchCandidateSet::refresh_selection`: последний получает уже
+упорядоченные entries и отдельный TRF-контекст, затем читает Unit DAT через VFS.
+Он не строит AI-каталог и не исполняет script instruction. Runtime всё ещё не
+создаёт владельца живого контекста, не передаёт реальные 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`) одна ветвь сначала проходит записи
@@ -169,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
@@ -312,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 и расчётный диапазон произведения/отношения за
@@ -419,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` и
@@ -562,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` не имеет доказанных единиц
@@ -644,26 +863,142 @@ Native interoperability подтверждается только успешны
Граница shell подтверждена exports `createShell/getIShell`, `IGUIServer`,
верхнеуровневым UI-pass и файлами `ui/*.cfg`, `DATA/TextRes.cfg`,
-`gamefont.rlb` и `sprites.lib`. RsLi framing библиотек закрыт, но widget tree,
-layout rules, glyph metrics, sprite command semantics, focus/navigation и HUD
-state machine пока не восстановлены до field-level спецификации.
-
-Закрывающий эксперимент: трассировать загрузку `shell_ctrls.cfg`,
-`menu_resources.cfg`, `cursor.cfg`, `game_resources.cfg` и `hq.cfg`, сопоставить
-GUI object factories и снять command/event captures для меню, HUD, briefing и
-диалогов.
+`gamefont.rlb` и `sprites.lib`. Для полной GOG-сборки подтверждены раздельные
+форматы: `gamefont.rlb` читается общим RsLi reader, но его native consumer этим
+не установлен; для меню подтверждена цепочка `menu_resources.cfg` → NRes
+`font.lib` → native lookup `MENU_FONT` с получением реального font-объекта.
+Reader `fparkan-font` разбирает 20-байтовый Tfnt prefix, 256 записей фиксированного
+шага и передаёт вложенный Texm общему reader-у. Native consumer подтверждает
+два целочисленных параметра, per-byte spacing и signed advance; смысл трёх glyph DWORD
+и поля заголовка по `0x0c` пока не установлен. Это не реализует выбор шрифта, текстовый
+layout или glyph drawing. `menu_resources.cfg` и шесть вариантов
+`MENU_FONT`/`GAME_FONT` сопоставлены с NRes-записями в
+[разборе shell и шрифтов](ui-shell.md). Также остаются открытыми выбор dimension
+на фактическом дисплее `1280x1024`, widget tree, layout rules, sprite command
+semantics, focus/navigation и HUD state machine.
+
+Закрывающий эксперимент: сопоставить оставшиеся детали `shell_ctrls.cfg`,
+`cursor.cfg`, `game_resources.cfg` и `hq.cfg` с GUI object factories и снять
+command/event captures для меню, HUD, briefing и диалогов.
## Research, economy and properties
-Экспорты `LoadResearch`, `CalcFullResearchCost`, TRF/preload resources и TMA
-properties доказывают отдельный слой исследований, стоимости, добычи и
-производственных параметров. Формулы стоимости, dependency graph технологий,
-inventory/economy transitions и точная типизация всех 16-byte property values
-не закрыты.
-
-Закрывающий эксперимент: сопоставить research functions с ресурсами и UI,
-снять изменения state на контролируемых покупках/исследованиях и построить
-typed schema свойств по consumers, а не по одному имени.
+Для GOG `ArealMap.dll` SHA-256
+`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`
+`CalcFullResearchCost` по RVA `0x16280` имеет подтверждённый ABI
+stdcall `[context, candidate, out1, out2]`, `RET 0x10`, и восстановленный
+алгоритм для одного candidate tree: zero-kind subtree pruning, direct preorder,
+общая дедупликация, ordered DFS по `TRF2`/`TRF3`, raw gates из `TRF1` и
+последовательное `f32` сложение. Принятый набор из 23 synthetic native cases
+включал Calc и два AI predicate; для контролируемых сравнений x87 word был
+установлен в `0x007f`. У набора synthetic context, поэтому он не измеряет
+реальную миссионную стоимость.
+
+Отдельный прямой вызов chooser-а `ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`
+`FUN_10010f90` (RVA `0x10f90`) прошёл 3 guard-case и frozen-матрицу из 15
+проверок при заданном порядке кандидатов. Тот же код использует конструктор RVA
+`0x10480`, parser первого Unit DAT дерева RVA `0x102c0` и предикаты RVA
+`0x115f0`/`0x11530`. Wrapper Handler(43) по RVA `0xd54a` и catalog loader
+`0x104e0` не вызывались. Эта матрица не подтверждает полный Handler(43),
+миссионный источник списка или штатный порядок AI-каталога.
+
+`fparkan-assets` декодирует используемые NRes chunks, сохраняя сырые TRF0 words
+и непрозрачные данные. Native cost outputs сопоставлены только с двумя raw
+words: out1 складывает TRF0 word 1 и подписан как ore, out2 складывает word 0
+и подписан как power. Смыслы остальных полей полного 32-byte callback record
+не установлены. Non-ASCII case folding research keys также остаётся открытым;
+Rust явно возвращает unsupported для таких сравнений.
+
+Rust `ResearchCandidateSet` получает ordered entries, а TRF-context передаётся
+отдельно. Ошибка чтения VFS, неверный Unit DAT magic или неполное первое
+дерево пропускают entry; missing key даёт false predicate. Cost вызывается и
+для пустой таблицы: он возвращает `EmptyResearchTable`, после чего этот entry
+пропускается с сохранением второго status.
+Другие ошибки таблицы и
+`UnsupportedNonAsciiLookup` остаются явными Result-ошибками. Уже записанные
+statuses и выбранный индекс на ошибке не откатываются; это политика Rust API,
+не доказательство полного native error ABI.
+
+Два Rust-предиката повторно обходят один декодированный tree с той же
+immutable TRF-таблицей. Общий helper может остановить второй проход при первом
+ложном флаге; native RVA `0x11530` продолжает обход и накапливает
+неиспользуемые float sums. Первый проход уже проверил ключи всех enabled
+компонентов, поэтому подтверждается только эквивалентность state/selection
+для такого входа, не идентичность callback trace или вычисления этих sums.
+
+Статический перенос владельца исследовательского контекста подтверждён по трём
+GOG-модулям. В `iron3d.dll` (SHA-256
+`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`)
+`FUN_1005ff40` (RVA `0x5ff40`) передаёт индекс клана в `FUN_100392e0` (RVA
+`0x392e0`), где он сохраняется в `row+0x1c` и копируется в `descriptor[0]`.
+Thunk RVA `0xcd0a0` ведёт к `World3D!GetNetPlayerNum` (RVA `0x9110`), а RVA
+`0xcd08e` — к `World3D!AddNewObjectToGame` (RVA `0x8310`); SHA-256
+`World3D.dll`: `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`.
+Вызов getter-а без аргументов завершается обычным `RET`; его результат становится
+arg1 следующего вызова. `AddNewObjectToGame` принимает девять DWORD, включая
+указатель на descriptor в arg9, и очищает стек через `RET 0x24`. RVA `0x83e9`
+читает arg9; ветвь class `0xb`/QI `0x502` на RVA `0x853e` загружает
+`descriptor[0]` и передаёт его в слот `+0x5c` на RVA `0x8544`;
+`MisLoad.dll` (SHA-256
+`c607762bb67db14ca530927c8e1347339b62b0a163ac90c708ee83142292e2bf`)
+`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 }`; этот флаг не выводится из `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
+`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) selector
+`0x204` по RVA `0xa490` обновляет сохранённое float-значение `+0x678`. В GOG `Control.dll`
+(SHA-256 `bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`)
+selector `0x36` проходит цепочку RVA `0x31af0` → `0xe6c0` и записывает в общую
+ячейку памяти по RVA `0x434bc`; selector `0x91` выполняет одноразовую
+инициализацию, только если функция ещё не выполнила её. В Rust пока хранятся
+только декодированные TRF-данные и сведения о владельце: эти побочные записи,
+очередь и lifetime контекста не моделируются.
+
+Открыты runtime-выполнение Handler(43), создание и lifetime research context,
+включение factory-цепочки в полный штатный startup миссии, построение AI
+candidate list и штатный порядок кандидатов, live x87 policy,
+inventory/economy transitions и точная типизация всех 16-byte TMA property
+values. Strict ore comparison и выбор первого кандидата при равенстве известны
+только для supplied-order chooser. Ignored acceptance проверяет реальные Part 1
+AI DAT prefix и `scream.trf`, но byte-sorted порядок не заявляется как native
+catalog order и не сравнивает цену с вызовом chooser-а в mission context.
+
+Для проверки оставшихся границ нужно на живой миссии установить, проходит ли
+штатный `Init` по статически восстановленной передаче owner, зафиксировать
+создание и lifetime исследовательского контекста и сравнить порядок кандидатов
+и cost outputs оригинала с Rust. Покупки, inventory transitions и typed
+property schema требуют отдельных контролируемых исследований.
## Rare branches
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 не меняют эти три поля
diff --git a/docs/appendices/ui-shell.md b/docs/appendices/ui-shell.md
index d236e14..58d4083 100644
--- a/docs/appendices/ui-shell.md
+++ b/docs/appendices/ui-shell.md
@@ -26,10 +26,103 @@ Win32 messages
`sprites.lib` и `Palettes.lib`. Конфигурации задают controls/resources/cursor,
overlays и HQ; `TextRes.cfg` связывает символические ключи с текстом.
-`gamefont.rlb` содержит две LZSS-записи (`0x040`), `sprites.lib` — 24 записи
-raw Deflate (`0x100`). `sprites.lib::INTERF8.TEX` использует известный
-`deflate_eof_plus_one` quirk. UI обязан пользоваться общим RsLi reader, а не
-отдельной похожей распаковкой.
+В demo `gamefont.rlb` содержит две LZSS-записи (`0x040`); `sprites.lib`
+содержит 24 записи raw Deflate (`0x100`) и известный `deflate_eof_plus_one`
+quirk для `INTERF8.TEX`. В полной GOG-сборке меню отдельно связывает
+`ui/menu_resources.cfg` с `ui/font.lib`, архивом NRes. Поэтому `gamefont.rlb`
+не задаёт единственный путь шрифта. Оба контейнера уже читаются общими
+[RsLi](../reference/rsli.md)- и [NRes](../reference/nres.md)-readers проекта;
+будущая UI-подсистема должна пользоваться ими, а не добавлять свою распаковку.
+
+В `menu_resources.cfg` ресурс `fonts` указывает `ui\\font.lib` с `libtype=multi`
+и `type=2`; это поля CFG, а не FourCC записей NRes. У девяти записей
+`font.lib` `type_id` равен `0x544E4F46` (`FONT`). Значение подстановки в
+`font-substitute` — строковое имя: этот CFG alias (`MENU_FONT` или `GAME_FONT`)
+связывает роль шрифта с его именем, но сам не является именем записи архива.
+Числовые ссылки находятся отдельно, в объекте `fonts`. В проверенной паре CFG и
+архива эти ссылки совпадают с исходными порядковыми номерами записей NRes
+(`EntryId`), а не с `sort_index`:
+
+| Размер CFG | Строковый alias `font-substitute` | Ссылка `fonts` | `EntryId` и запись NRes |
+| --- | --- | --- | --- |
+| `640x480` | `MENU_FONT` | `8` | `8`, `mf_640.tft` |
+| `640x480` | `GAME_FONT` | `6` | `6`, `gf_640.tft` |
+| `800x600` | `MENU_FONT` | `5` | `5`, `mf_800.tft` |
+| `800x600` | `GAME_FONT` | `3` | `3`, `gf_800.tft` |
+| `1024x768` | `MENU_FONT` | `2` | `2`, `mf_1024.tft` |
+| `1024x768` | `GAME_FONT` | `0` | `0`, `gf_1024.tft` |
+
+Пример из этой CFG: `font-substitute` связывает `MENU_FONT` со строкой
+`MENU_FONT_1024x768`, а объект `fonts` связывает уже это имя с числом `2`.
+Такое сопоставление относится только к этой конфигурации и архиву, а не является
+универсальным правилом native selector. Строка `1024x768` не доказывает выбор
+этого варианта при фактическом экране `1280x1024`.
+
+Проверены оригинальные `menu_resources.cfg` (SHA-256
+`508C3783EAF112023E46BF4617424FBB57C393BCEF328CE1B925549B0069EF97`) и
+`font.lib` (440392 bytes, SHA-256
+`E4AF5D1F0BF0464574DE654DE378766090681F610705CE53539DF9041E045D1C`).
+Шесть сопоставленных payload начинаются с `Tfnt`; это подтверждает сигнатуру
+данных, но не значение последующих полей.
+
+Связка подтверждена и в оригинальном коде. В `iron3d.dll` SHA-256
+`D1DC4EA8535E2069B0A05C9E8BE2724DD438BA44CCA7A83007D8B2E50F9E35FA` shell
+читает `shell_ctrls.cfg` в RVA `0x7410`, а инициализация в `0x78c0` читает и
+регистрирует `menu_resources.cfg` в слоте `0` ResourceManager. `services.dll` SHA-256
+`D3619DEEB4D598B16099E0263E15F9C3213F0FAA7208DC093328CFBA93FDC0EA`, RVA
+`0x92a0`, разбирает `font-substitute`, размерные значения и запись ресурса
+`type=2`. Затем `iron3d.dll` запрашивает `MENU_FONT` в RVA `0x1eb20`, ищет ресурс
+в `0x1ed06` и разыменовывает результат в `0x1ed3e`; прямая x86-трасса на
+последнем участке подтвердила ненулевой объект. Это подтверждает получение
+реального font-объекта, но не конкретной NRes-записи при `1280x1024`, её
+отрисовку или паритет шрифтов.
+
+## Фиксированный reader Tfnt
+
+В подтверждённом пути `World3D.dll` (SHA-256
+`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`)
+передаёт ordinal NRes-reader-у в RVA `0x1361f`, а затем передаёт полученный
+указатель renderer-у в RVA `0x13624`; бит `0x400000` установлен у вызова в RVA
+`0x1360d`. В `Ngi32.dll` (SHA-256
+`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`) vtable
+NRes у RVA `0x318bc` ведёт к `FUN_10011f80` (RVA `0x11f80`), которая возвращает
+`mapped_base + data_offset`. Renderer vtable у RVA `0x315e0` направляет slot
+`+0x58` в `FUN_10007dd0` (RVA `0x7dd0`), затем ресурс проходит через
+`FUN_10010c10` (RVA `0x10c10`) и хранит pointer в `FUN_1000f3d0` (RVA `0x0f3d0`).
+Loader font-объекта `FUN_1000fb30` (RVA `0x0fb30`) при флаге `0x400000`
+использует этот адрес как начало Tfnt payload; отдельная RsLi-обёртка в этом
+пути не читается.
+
+| Смещение | Что читает `fparkan-font` |
+| --- | --- |
+| `0x00..0x03` | Сигнатура `Tfnt`; reader проверяет её на границе файлового API. В наблюдавшемся native loader проверки сигнатуры не было. |
+| `0x04..0x07` | `i32`, нижняя граница ширины в native fixed-width path. |
+| `0x08..0x0b` | `i32`, вертикальный размер для native clipping/drawing. |
+| `0x0c..0x0f` | Сырые биты `u32`, скопированные в font object и участвующие в float-сложении при drawing; имя и смысл поля неизвестны. |
+| `0x10..0x13` | `i32`, прибавляемый один раз на входной байт при native text measurement. |
+| `0x14..0x1013` | 256 записей по 16 байт: три сырых DWORD и signed `i32` advance по смещению `+0x0c`. |
+| `0x1014..конец` | Обычный Texm-документ, передаваемый общему `fparkan-texm::decode_texm`. |
+
+Первые три слова glyph record имеют разные native consumers, но их общая
+семантика не восстановлена: одно слово загружается как float, другое попадает
+в сформированные vertex data, а третье не прояснилось в исследованных ветвях.
+Поэтому reader сохраняет их как `word0..word2`, а поле заголовка по смещению
+`0x0c` — как неинтерпретированные float-биты. Он не выбирает размер текстуры по разрешению
+дисплея и не задаёт диапазон размеров: в проверенных ресурсах встречаются
+`128×128` и `256×256`, а форматы, mip-цепочку и палитру проверяет общий Texm
+reader.
+
+```rust
+let font = decode_tfnt(Arc::<[u8]>::from(raw_font_payload))?;
+let glyph = font.glyphs[usize::from(input_byte)];
+let texture = font.texm;
+```
+
+Игнорируемый corpus-тест разбирает через общий NRes reader девять записей
+`ui/font.lib` и `ARIALTEX.TFT` из `sys.lib`; он сверяет известные размеры,
+Indexed8, одну mip-ступень и выборочные поля `gf_640.tft`. Это проверка байтов
+лицензированной GOG-сборки и parser contract, а не запуск shell или проверка
+отрисовки.
Ключ ресурса, legacy path, локализованный текст и исходные bytes — четыре
разные идентичности. Archive keys остаются ASCII-casefold; `TextRes.cfg`,
@@ -49,10 +142,11 @@ design resolution. Отдельно хранятся legacy-layout coordinates,
viewport, scale/letterbox policy, mouse-to-layout transform, clipping и
z-order. Cursor, sprite и hit-test обязаны применять одно преобразование.
-Font contract включает glyph image, advance, bearing/offset, line height и
-fallback glyph. Binary glyph metrics `gamefont.rlb` пока не восстановлены:
-payload читается lossless через RsLi, а семантика устанавливается по consumer
-trace.
+Font contract включает glyph image, bearing/offset, line height и fallback
+glyph. `fparkan-font` читает фиксированную таблицу Tfnt и отдаёт signed advance
+и embedded Texm; он не выполняет text measurement/layout или glyph drawing.
+`gamefont.rlb` по-прежнему читается общим RsLi reader, но его native consumer не
+установлен.
Нормализованное событие проходит modal widget, затем shell command map; лишь
разрешённая gameplay-команда превращается в World3D manual event. Held state и
diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md
index 3267a2f..799a969 100644
--- a/docs/reference/environment-effects.md
+++ b/docs/reference/environment-effects.md
@@ -256,14 +256,30 @@ Vertex shader пропускает `clip_from_world` для строк солнÑ
остаются отдельным путём. Материал экранного quad не освещается, vertex fog
отключён, а sky far-depth bit к нему не применяется.
-Native `z = 0.999` не означает, что спрайт гарантированно лежит за любой
-поверхностью. Vulkan использует `LESS_OR_EQUAL` и не записывает depth для
-прозрачного ряда. При near `0.5`, far `700` мировая поверхность на расстоянии
-`320` получает depth
-`700 / (700 - 0.5) * (1 - 0.5 / 320) ≈ 0.999151`; поэтому экранный `0.999`
-чуть ближе этой поверхности и проходит depth test перед ней. Это фиксированное
-native значение сохраняется для совместимости, но само по себе не обещает
-окклюзию солнца всей геометрией местности.
+Для мировой геометрии
+оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения
+проекционная глубина отображается в этот диапазон:
+
+```text
+world_depth = 0.1 + (0.99 - 0.1) * (clip_z / clip_w)
+```
+
+Даже дальняя гора получает глубину не больше `0.99` и закрывает экранный
+диск с `z = 0.999`. Если оставить мировой диапазон `0–1`, при near `0.5`
+и far `700` поверхность дальше примерно `292` единиц окажется позади диска:
+солнце начнёт просвечивать сквозь неё при движении камеры. Одного переноса
+нативного `z` в экранные вершины недостаточно.
+
+В Vulkan диапазон задаётся динамическим viewport отдельно для мировых
+пакетов; экранные TL и фоновые sky-пакеты сохраняют диапазон `0–1`.
+Матрица проекции и плоскости отсечения при этом не меняются. Константы
+мирового профиля записывает GOG `Terrain.dll` RVA `0x27422–0x27452` из таблицы
+`0x65088/0x6508C/0x650A0/0x650A4` (near, far, MinZ, MaxZ). GOG `Ngi32.dll`
+RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в renderer
+`+0x30/+0x34` и передаёт их `IDirect3DDevice7::SetViewport`; сборки указаны в
+[таблице бинарников](original-binaries.md). Нативные значения этих двух полей
+имеют биты `0x3DCCCCCD` и `0x3F7D70A4`.
+
## Геометрия визуальных FX-команд
@@ -294,6 +310,35 @@ D110-порядок. Все три evaluator-а принимают уже соб
`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context.
Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`.
+У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для
+локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют
+порядок `[a, u, w]`, где `u = normalize(v × a)`, `w = a × u`. Плоскость
+прямоугольника — XY, поэтому первая сторона идёт вдоль авторской оси, а
+вторая поворачивается к камере. Это сохраняет соединение между точками
+локатора при движении камеры. Если поставить ось в третий столбец,
+прямоугольник займёт поперечную плоскость и связь превратится в полоску или
+небольшой диск.
+
+Порядок подтверждён аргументами конструктора матрицы в GOG `Effect.dll`
+(сборка из [таблицы бинарников](original-binaries.md), SHA-256
+`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`):
+RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97F6–0x983C`.
+Например, `a = (1, 0, 0)` и `v = (2, 1, 0)` дают столбцы
+`(1, 0, 0)`, `(0, 0, -1)`, `(0, 1, 0)`. При нативном вызове длины последних
+двух столбцов могут немного отличаться от единицы из-за приближённой
+нормализации выбранного `g_FastProc`; порядок осей от этого не меняется.
+
+У сооружения `s_tree_58` в миссии «Конструктор» FXID `tree_ray_58` задаёт два
+слоя жёлтого луча: команды Op3 mode 1 с ресурсами `defl_y_add` и `defl_y1_add`.
+Их геометрию сверили с CPU-вызовом GOG `Effect.dll` `0x93D0`. На семи временных
+фазах и трёх направлениях камеры для каждой команды (42 выборки) вершины
+совпали с Rust evaluator с максимальной покомпонентной разницей `0.0001373291`.
+Оракул получает уже выбранные Rust позицию, ось и масштаб, а world-camera
+переводится в `cameraLocal` тем же component-wise projection, что и native
+`FastProc + 0x68`. Поэтому это подтверждает только геометрию Type 1 helper-а на
+этих входах: обновление и расписание эффекта, UV/material и пиксельное
+совпадение здесь не проверялись.
+
## Частицы Op7
`fparkan-fx::op7` вычисляет фиксированный набор частиц Op7 по body команды,
diff --git a/docs/reference/msh.md b/docs/reference/msh.md
index a28514d..0a8a2cb 100644
--- a/docs/reference/msh.md
+++ b/docs/reference/msh.md
@@ -18,6 +18,7 @@ type 9 service stream
type 10 strings and node names
type 13 Batch20 records
type 15 auxiliary stream
+type 16 optional vertex-colour data
type 17 auxiliary data
type 18 packed UV1 (optional secondary coordinates)
type 19 animation frame map
@@ -25,6 +26,9 @@ type 20 rare auxiliary table
```
Reader ищет entries по type, но сохраняет исходный порядок для roundtrip.
+Поле `ModelAsset.type16_present` отмечает только наличие entry типа 16. Данные
+payload сохраняются, но этот путь рендера пока не преобразует их в цвета
+вершин.
## Node and slot selection
@@ -149,11 +153,114 @@ struct Batch20 {
```
Byte `material_index` выбирает строку WEAR; high word at `+0x02` сохраняется
-отдельно для native overrides. Loader проверяет, что
+отдельно для native overrides. Слово `local_batch_index` остаётся raw-полем
+`+0x06`: в пути CBuilding helper retry оно вместе с базой модели задаёт
+связанный корень Node38, а у отдельной `ModelAsset` база равна нулю. Вне этого
+пути слово сохраняется без вывода о его значении. Loader проверяет, что
`index_start + index_count` остаётся внутри type 6, а каждый использованный
индекс через `base_vertex` попадает в position stream. `vertex_count` и
`local_batch_index` доступны renderer для исходного draw contract.
+## Type 7 и сегментный запрос
+
+Type 7 необязателен для обычного MSH preview. Если stream есть, каждые 16 bytes
+описывают одну грань: `tri_flags` находится по `+0`, четыре неинтерпретируемых
+слова — по `+2`, `+4`, `+6` и `+14`, а signed `normal[3]` — по `+8`, `+0xA` и
+`+0xC`. `ModelAsset.triangle_descriptors` сохраняет декодированную таблицу как
+`Option`; отсутствие Type 7 не делает статическую модель недействительной.
+
+`query_slot_segment` использует Node38 header mask, Batch20 leading DWORD
+`batch_flags | (material_index_hi << 16)` и `tri_flags`. Для каждого поля
+include mask требует все установленные биты, exclude mask пропускает запись при
+любом совпавшем бите. Descriptor cursor начинается с `Slot68.tri_start` и
+сдвигается на `Batch20.index_count / 3` после каждой записи, даже если batch
+или грань отфильтрованы. Индексы грани читаются из type 6 с учётом
+`base_vertex`.
+
+Правило восстановлено для GOG-бинарей `AniMesh.dll`
+(`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) и
+`Ngi32.dll` (`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`).
+Callback `AniMesh.dll+0x11090` переводит signed type-7 normal множителем
+`1 / 32767`; функция `AniMesh.dll+0x11EB0` записывает этот scale в глобальное
+поле. Callback вызывает FastProc slot `+0xB4`: в этой сборке он указывает на
+`Ngi32.dll+0x1D560`. После обычной загрузки DLL порог `Ngi32.dll+0x37224`
+равен нулю, поэтому plane test принимает только переход от положительного
+расстояния к неположительному (`dot(normal, end - start) < 0`, расстояние в
+начале `> 0`, в конце `<= 0`). `Batch20.batch_flags & 0x0002` разрешает повторный
+plane test в обратном направлении, только если прямой plane test не пересёк
+плоскость. Пересечение проверяется export ordinal 202
+(`Ngi32.dll+0x1E60`, `mrnPointInPoly`): проекция выбирает ось с наибольшим по
+модулю normal component, а точка проходит грань, пока ни одно edge expression
+не меньше нуля. Равенство нулю принимается, поэтому попадание на ребро или
+вершину считается попаданием. Поздний кандидат с тем же squared distance
+заменяет предыдущий; расстояние считается от исходного первого endpoint.
+
+Native helper получает точку пересечения через `RCPSS` и один шаг уточнения;
+Rust использует обычное деление `f32`, поэтому последние биты координат могут
+отличаться. Игнорируемый тест с ресурсами проверяет исходную стену, дверной
+проём и обратное направление на `fortif.rlb/fr_l_plant.msh`.
+
+### Мировой отрезок по выбранным узлам
+
+Локальный запрос выше проверяет один узел и slot модели. Runtime-функция
+`query_building_segment_world` принимает мировой отрезок и исходный список
+выбранных узлов со slot-ами и их преобразованиями `world_from_node`. Первый
+проход сохраняет порядок этого списка, переводит концы отрезка обратной
+матрицей и выбирает ближайшее попадание в мире.
+
+```text
+segment_node = inverse(world_from_node) * segment_world
+hit_world = world_from_node * hit_node
+distance² = dot(hit_world - original_start, hit_world - original_start)
+```
+
+`original_start` остаётся началом первоначального мирового отрезка, даже когда
+запрос перезапускается. Если ближайший batch имеет bit `0x0008`, его ключ
+`local_batch_index` выбирает новый корень Node38, а отрезок начинается в
+мировой точке helper-попадания и идёт к исходному концу. Runtime обходит
+метаданные этого поддерева даже у узлов без переданной геометрии; сами slot-
+запросы выполняются только для узлов, присутствующих во входном списке. Обход
+проверяет сначала допустимых потомков, затем сам узел. Header bit `0x0010`
+прекращает обход его потомков; bit `0x0004` у дочернего узла исключает всю эту ветвь, но bit
+`0x0004` у текущего корня не мешает запросу его собственной геометрии. Слово
+родителя `0xFFFF` не связывает узел с дочерним корнем, а helper-ключ `0xFFFF`
+считается ошибкой данных.
+
+В каждом проходе batch `0x0200` и type-7 triangle flags `0x0024` исключаются.
+Ближайшее попадание и разрешение равных расстояний сохраняют правила локального
+запроса; расстояние измеряется от первоначального начала. Счётчик ограничивает
+все пересечения: при посещении 200 запрос завершается ошибкой до проверки bit
+`0x0008`.
+
+Эта схема сверена с GOG `Terrain.dll` SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` (RVA
+`0x35430`, `0x35961`) и `AniMesh.dll` SHA-256
+`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e` (RVA
+`0x134D0`, `0x10A50`).
+
+Игнорируемый тест загружает настоящий объект PLANT 4 миссии
+AutoDemo и использует его исходную CTLD-привязку Node3: `frame_a = 0`,
+`frame_b = 1`, начальное смешивание `0`. В контроллере корневого PLANT нет
+записей tag-21. В
+закрытой начальной позе запрос, вызванный через обновление камеры приложения,
+останавливает движение. Затем тест вручную задаёт время `1` только для
+Node3, пересчитывает pose и преобразование присоединения и повторяет тот же
+путь обновления камеры; отрезок проходит через проём. Это проверяет подачу
+двух заданных в MSH поз в коллизию приложения, а не открытие двери событием
+миссии и не её видимое обновление в игровой сцене.
+
+Интерактивный preview создаёт отдельную запись для каждого отрисовываемого
+корневого строения класса `FORT` и его отрисовываемых компонентов. На каждом
+движении камера получает текущую Node38-иерархию компонента и его актуальное
+преобразование присоединения, переводит выбранные узлы и slot-ы в мировой
+запрос и выполняет его после ограничения цели рельефом. Попадание сохраняет
+прежнюю позицию камеры, но не отменяет новый поворот; повтор ожидающего кадра
+не применяет шаг движения второй раз. Так приложение связывает мировой
+MSH-запрос с движением свободной preview-камеры. Выбор первого корня и полный
+состав мирового списка ещё не сверены с оригиналом; игровые источники поз и
+событий, а также поведение стратегической и следящей камеры остаются
+открытыми.
+
## Сторона грани в цветовом проходе
В цветном проходе `Terrain.dll`, функция `0x100296F0`, сравнивает камеру с
diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md
index 3085731..10de209 100644
--- a/docs/reference/render-frame.md
+++ b/docs/reference/render-frame.md
@@ -68,6 +68,23 @@ Pending frame хранит пакет MAT0 для мира и, при налич
`batch` `0x108`. Такой диапазон при включённой проверке глубины не записывает
глубину. За фиксированными диапазонами окружения сохраняются их явные состояния.
+Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при
+raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном
+подтверждённом случае. В GOG `Terrain.dll` с SHA-256
+`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка
+по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен
+`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118`
+превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`,
+также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008`
+переопределения нет. Оно относится к одному draw range и не меняет общий
+материал; alpha `0` временно задаёт число индексов `0`, а при очистке
+переопределения исходное число индексов восстанавливается.
+
+Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния,
+выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и
+culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha
+MAT0, пока эти входные данные не установлены.
+
В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков
явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на
своих местах; прозрачная очередь мира сортируется от дальних элементов к
@@ -181,6 +198,41 @@ fn vertical_fov(self, aspect: f32) -> f32 {
(`forward · direction <= 1e-4`). Лучи в передней полусфере могут проецироваться
за край viewport; владелец спрайта отдельно решает, нужно ли его рисовать.
+После движения свободная камера ограничивает X и Y прямоугольником
+поверхностей карты, затем поднимается над найденным рельефом на заданный
+зазор. Границы берутся из корня пространственного индекса `TerrainWorld`,
+поэтому неиспользуемые вершины файла их не расширяют и повторного обхода
+геометрии в каждом кадре нет:
+
+```rust
+position[axis] = position[axis].clamp(min[axis], max[axis]); // axis = 0, 1
+position[2] = position[2].max(ground_height + ground_clearance);
+```
+
+Для каждого отрисовываемого корневого объекта класса `FORT` и каждого его
+отрисовываемого компонента интерактивный путь создаёт отдельную запись проверки.
+Она использует те же текущие позы узлов и преобразования присоединённых
+компонентов, что и геометрия кадра. После ограничения целевой позиции по
+TerrainWorld камера проверяет непрерывный отрезок от прежней позиции до цели.
+Если после нативных MSH-фильтров остаётся пересечение, позиция возвращается к
+началу шага, а рассчитанный из ввода новый поворот сохраняется. Маски batch и
+type-7, а также продолжение отрезка по helper-link описаны в
+[разделе MSH о сегментном запросе](msh.md#type-7-и-сегментный-запрос).
+
+Повторная отправка того же ожидающего кадра, например после изменения размера
+области отображения, использует уже подготовленное состояние и не применяет
+движение второй раз. `--legacy-camera-capture` сохраняет положение захваченной
+камеры без изменений и не запускает проверку зданий. Это диагностическое
+ограничение, а не восстановленное правило игровой или следящей камеры
+оригинала. В проверке начальная поза берётся из декодированных связей CTLD;
+проход двери отдельно проверяется после явной выборки записанного в MSH кадра 1
+только для Node3. Так проверяется вход запроса столкновения и путь камеры;
+видимая дверь не анимируется, а миссионные команды не воспроизводятся.
+
+Высота полёта сверху не ограничена. Это ограничение диагностической камеры
+превью; точные правила игровых и следящих камер оригинала остаются
+неподтверждёнными.
+
## Ограничения точного совпадения
`VulkanStaticCamera::from_legacy_d3d7` выполняет восстановление вида и проекции
diff --git a/docs/tomes/03-resources.md b/docs/tomes/03-resources.md
index a349a79..f092ece 100644
--- a/docs/tomes/03-resources.md
+++ b/docs/tomes/03-resources.md
@@ -60,6 +60,12 @@ archive object и увеличивает счётчик владельцев. Г
предметного объекта не должен напрямую закрывать архив: он зависит от данных,
но не владеет файлом как ресурсом операционной системы.
+План ресурсов считает запросы от уникальных визуальных прототипов. Если два
+слота материала ссылаются на один MAT0 с одной и той же текстурной фазой, это
+два запроса текстуры, хотя подготовленный набор хранит один ресурс. Повторные
+ссылки на `lightmap` учитываются так же. `AssetPreparationReport` и ограничения
+подготовки считают уникальные подготовленные ресурсы.
+
## Имена и пути
Большинство игровых имён сравнивается без учёта регистра в ASCII-диапазоне. Это
@@ -471,6 +477,15 @@ struct UnitDatRecord112 {
Требование обязательного terminator применяется только к полям, где оно доказано
форматом.
+У AI есть отдельный потребитель Unit DAT: исследовательский chooser читает
+заголовок с magic `F0F1`, сохраняет второй DWORD как непрозрачный и завершает
+разбор на границе первого полного корневого дерева. `immediate_child_count`
+закрывает это дерево по preorder; байты после него, включая неполную запись или
+orphan-record, chooser уже не читает. Поэтому Rust для этого пути находит
+bounded 112-byte prefix и передаёт его существующим `decode_unit_dat` и
+`unit_component_tree`. Общий декодер остаётся строгим к размеру всего файла
+`8 + 112 * N`; частный framing AI не меняет этот контракт.
+
Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records
Части 2. Все соответствуют формуле размера, `kind == 1` и
`archive_name == "objects.rlb"`.
diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md
index 84de6ee..ef7dc52 100644
--- a/docs/tomes/05-render.md
+++ b/docs/tomes/05-render.md
@@ -1045,7 +1045,19 @@ swapchain, тот же sample времени и состояние мира от
Счётчик и replay clock продвигаются только после результата `Presented`.
После шага движения камера проверяет высоту
`TerrainWorld`: над конечной поверхностью сохраняется начальный просвет, а вне
-поверхности высота не ограничивается. Mission-local `sky.ske`/`sky.wea` выбирают
+поверхности высота не ограничивается.
+
+Для каждого отрисовываемого строения (корневого объекта класса `FORT`) и для
+каждого его отрисовываемого компонента создаётся отдельная запись проверки. Она
+использует те же текущие матрицы узлов и присоединённых компонентов, что и
+геометрия.
+После ограничения целевой позиции по TerrainWorld камера проверяет непрерывный
+отрезок от прежней позиции до цели; при пересечении она остаётся в прежней
+позиции, сохраняя новый поворот. Повторная отправка ожидающего кадра не
+применяет движение второй раз. Это ограничение диагностической свободной
+камеры, а не правило игрового или следящего режима оригинала.
+
+Mission-local `sky.ske`/`sky.wea` выбирают
атмосферное расписание и sky materials, `effects.rlb/env_lightning` даёт
lightning visual; weather particles, flares, sun/moon, point lights,
projected shadows и audio events обновляются вместе с камерой и временем.
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index 48f4405..3a4e51c 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -168,8 +168,11 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga
младшие 32 бита, что даёт `0`. Значение `10001` также проходит дальше: оригинал
показывает предупреждение для чисел выше `10000`, но не отменяет создание AI.
-Rust исполняет только подтверждённую форму этого действия. Поддержанные записи
-применяются к состоянию лишь после успешного прохода всего первого `Init`.
+Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)`
+и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом —
+число или имя переменной. Поддержанные шаги сразу обновляют временную копию
+varset, поэтому следующая формула читает уже записанные значения. К состоянию
+клана записи попадают лишь после успешного прохода всего первого `Init`.
Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает
`Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма
становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило
@@ -177,8 +180,62 @@ Rust исполняет только подтверждённую форму эÑ
событие считается исполненным без записей. Остальные события пока не образуют
исполняемую VM.
+В `Init` встречаются и отдельные присваивания результатов формул. Их запись
+выбирает строку формулы по порядковому номеру из соседнего `.fml`; этот файл
+относится к конкретной базе сценария, а строки после нужного номера сохраняют свои позиции.
+Для первой кампанийной миссии `Mission.01` (`tut1_pl2.fml`) строка 0 записывает
+нули в `df0..df3`; в миссии `Single.01` (`scr_pl_1.fml`) строка 0 записывает
+нули в `dT8` и `dT9`.
+Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь
+проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8`
+сохраняется как число `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 мс. Эта
@@ -196,9 +253,10 @@ SuperAI для записей кланов, затем обновляет Behavi
от повторного входа; при занятой защите вызов пропускается, но одноразовая метка
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
значение времени очереди; после регистрации объектов загрузчик последовательно
-подготавливает каждый сценарный пакет и выполняет только подтверждённые действия
-`Handler(19)` из первого `Init`. Статический просмотр не запускает Behavior, но
-может сохранить подтверждённые записи `Init` в сценарном varset.
+подготавливает каждый сценарный пакет и выполняет восстановленные действия
+`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический
+просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
+varset.
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного
@@ -243,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, как он затем
+обновляется и как работает полный игровой цикл.
+
Список распределителя хранит не только участников, но и долю каждого. При
добавлении двух объектов оба получают по половине общего веса. Если добавить
третий, движок оставляет долю новой записи около одной трети, а оставшийся вес
@@ -468,16 +545,117 @@ Tag выбирает конкретное правило. Для tag 1 запиÑ
### TRF и preload-данные
-TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial
-variants имеют сигнатуру [NRes](../reference/nres.md) и содержат большие
-таблицы имён игровых прототипов: оружия, башен, сооружений и других объектов.
-Также найдены preload-записи, ANI и SKE resources.
+TRF-архивы используют несколько потребителей; один подтверждённый путь ведёт
+из `Handler(43)` к вычислению стоимости исследования. В проверенной сборке GOG
+`CalcFullResearchCost` из `ArealMap.dll` получает таблицу, загруженную через
+`MisLoad.dll`. Для этого пути `TRF0` содержит строки по десять сырых `u32`,
+`TRF1` — байты флагов, `TRFB` — пары «смещение имени и номер строки», а
+`TRF6` — пул строк с нулевыми окончаниями. Слово 8 строки связывает её с
+`TRFB` через младшие 16 бит; старшая половина сохраняется как неизвестное
+значение. `TRF2` и `TRF3` образуют упорядоченные списки предварительных
+исследований, которые читает этот путь стоимости. `TRF4`/`TRF5` образуют
+отдельную пару таблиц, но `CalcFullResearchCost` её не использует. Непрозрачные
+`TRF7`–`TRFA` остаются в архиве и требуются загрузчику.
+
+Модуль `fparkan-assets` разбирает этот профиль в `ResearchTrfDocument` и
+вычисляет пару результатов для одного Unit DAT:
+
+```rust
+let table = ResearchTrfDocument::decode(archive_bytes)?;
+let cost = calc_full_research_cost(&candidate, &table)?;
+```
+
+Расчёт проходит компоненты Unit DAT в порядке дерева. Компонент с нулевым
+`kind` завершает весь свой участок вместе с потомками. Для остальных ключ
+ресурса ищется среди имён `TRFB`; повтор строки добавляется лишь один раз.
+После прямых компонентов алгоритм обходит список `TRF2`/`TRF3` в его исходном
+порядке, углубляясь сразу в найденные предварительные исследования. Общая
+таблица посещений не даёт повторно добавлять узел и завершает циклы. Два бита
+флагов `TRF1` управляют включением строки и обходом её зависимостей; остальные
+сырые биты и слова не получают игровых имён без отдельного доказательства.
+
+Результат первого native-выхода подписан в отладочной строке как ore и
+суммирует слово 1 `TRF0`, второй подписан как power и суммирует слово 0. Каждое
+сложение сохраняется как `f32` в порядке найденной последовательности. В
+принятом наборе из 23 синтетических тестов оригинальных DLL проверялись
+`CalcFullResearchCost` и два AI-предиката; именно вызовы `CalcFullResearchCost`
+подтвердили порядок обхода, дедупликацию, циклы, флаги и отсечение дерева.
+Две проверки суммы показали чувствительность округления: одна и та же сумма
+`1, 2^24, -2^24` даёт `0`, а в другом порядке `2^24, -2^24, 1` даёт `1`.
+
+Эти результаты относятся к синтетическому исследовательскому контексту;
+штатный Handler(43) и запуск миссии здесь не проверялись.
+
+Владелец начальной записи исследования — индекс текущего прохода по кланам.
+В 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`, задающего число вызовов
+AI-слота `+0x38`. Передача поля подтверждена по статическому коду; выполнение
+Handler(43) и штатный запуск миссии остаются непроверенными. Технические
+сведения и адреса исходных сборок приведены в [границах знания](../appendices/knowledge-boundaries.md).
+
+Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди
+и отдельный `research_creation_gate`. Режим миссии и имя файла этот
+флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся
+`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного
+пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой
+путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку.
+Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег
+`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов
+указывают на один файл, каждый получает отдельно декодированную таблицу со
+своими строками TRF0.
+
+```rust
+MissionStartupInput {
+ queue_time_ms: 100_000,
+ research_creation_gate: true,
+}
+```
-По содержимому, порядку загрузки и consumers TRF с высокой вероятностью
-предоставляет AI/сценарному слою заранее подготовленную таблицу типов и
-связанных данных. Framing и имена подтверждены corpus-ом, но полная семантика
-каждой TRF-записи ещё не закрыта. Имена должны разрешаться через тот же
-resource registry, что и миссионные объекты.
+Это сохраняет декодированные таблицы и связь с владельцами для следующих
+этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не
+строит AI-каталог и не запускает `Handler(43)`; эти части пока не
+восстановлены.
+
+Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция
+`FUN_10010f90` (RVA `0x10f90`) прошла три guard-case и все 15 проверок.
+Конструктор находится в RVA `0x10480`, raw DAT parser — `0x102c0`,
+предикаты — `0x115f0` и `0x11530`. Матрица передавала кандидатов в заданном
+порядке. Обёртка `Handler(43)` по RVA `0xd54a` и загрузчик AI-каталога по RVA
+`0x104e0` не вызывались. Cost принадлежит `ArealMap.dll` SHA-256
+`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`,
+`CalcFullResearchCost` RVA `0x16280`; для сравнения x87 был явно задан как
+`0x007f`.
+
+`ResearchCandidateSet` держит упорядоченный каталог, выбранный индекс, байт
+scan-status и флаг `loaded` каталога; TRF-контекст передаётся отдельно.
+При наличии каталога и контекста set-status начинает со значения `1`, затем
+перед чтением каждого entry со status `0` становится `0`. Ненулевой entry
+пропускается. Первый предикат требует info words 1 и 2, соответствующие битам
+TRF1 0 и 2; второй требует words 0 и 2, то есть биты 1 и 2. Второй status
+сохраняется в entry, а cost вызывается даже при status `1`. Выбор записывается
+сразу, когда second predicate ложен и ore строго ниже текущего минимума,
+изначально `f32::from_bits(0x4b18967f)`. Равенство сохраняет первого, NaN индекс не меняет.
+
+В Rust ошибка VFS-чтения и невалидный DAT пропускают entry; неизвестный TRF-
+ключ даёт false predicate. Ошибка стоимости `EmptyResearchTable` тоже
+пропускает entry и сохраняет его второй status. Остальные ошибки таблицы и
+`UnsupportedNonAsciiLookup` возвращаются явно, причём уже сделанные изменения
+индекса и status остаются. Общий Rust-предикат может закончить обход раньше
+native callback `0x11530`, который продолжает накапливать неиспользуемые float
+sums. Первый проход уже проверил enabled keys в той же неизменной таблице,
+поэтому подтверждается эквивалентность состояния и выбора для этого входа,
+но не callback-trace parity. Это не подтверждение цены на живой миссии или
+порядка AI-каталога.
### Стабильность AI-слоя
@@ -883,21 +1061,41 @@ descriptor tag `19` и положительным selector-0 ответом; пÑ
его vtable slot `+0x10` статически указывает на общий getter RVA `0x21d00`, но
отдельный aggregate oracle этот класс не запускал. Rust хранит начальный scale
factory `9` и предоставляет `query_loaded_mission_control_resource` для selector
-`1`/`2` и `set_loaded_mission_control_multiplier` для setter-а `1`. Вызовы
-сначала проверяют живой `ObjectHandle`, затем обращаются к таблице этого объекта
-из полной загрузки миссии. Синтетические тесты проверяют границы пустой таблицы,
-нулевого знаменателя, неподдержанных factory tags и чисел за диапазоном
-нормального `f32`; лицензированный тест сверяет реальные descriptor tags и
-получает для `min_l_n1.dat` сумму `16` и отношение `1` до setter-а.
-
-В этом API пока реализованы только getter selectors `1`/`2` и setter `1`.
+`1`, `2` и `15`, а также `set_loaded_mission_control_multiplier` для setter-а
+`1`. Вызовы сначала проверяют живой `ObjectHandle`, затем обращаются к таблице
+этого объекта из полной загрузки миссии. Синтетические тесты проверяют границы
+пустой таблицы, нулевого знаменателя, отсутствующих объектов фабрик и чисел за
+диапазоном нормального `f32`; лицензированный тест сверяет реальные descriptor
+tags и получает для `min_l_n1.dat` сумму `16` и отношение `1` до setter-а.
+
+Сырой селектор `0x0f` соединяет два ранее созданных объекта владельца: у строки
+с `factory_tag = 9` берётся коэффициент и его текущий множитель, а у строки с
+`factory_tag = 21` — шесть текущих `coefficient_bits`. `INTO` может поменять
+`descriptor.tag`, не заменяя объект фабрики, поэтому query выбирает последние
+строки по `factory_tag`, а не по текущему tag. Если любой объект фабрики
+отсутствует, ответ — `None`. Иначе шесть коэффициентов складываются с индекса `5`
+к `0` с округлением до `f32` после каждого сложения; результат умножается на
+selector `0` фабрики `9` (`coefficient_bits[0] * class9_selector0_scale_bits`).
+Общие `multiplier_bits`, `node_provider_status` и `native_word_50_bits` в этой
+формуле не участвуют.
+
+Лицензионная проверка загружает AutoDemo с явно переданными
+`MissionStartupInput { queue_time_ms: 100_000, research_creation_gate: false }`
+и вызывает `query_loaded_mission_control_resource(..., 15)` для исходных
+объектов `0..5`; для `6` и `7` Rust возвращает `None`, потому что не хватает как
+минимум одного объекта фабрики. Native-биты для `0..5` приведены в технической
+главе. Проверка сверяет результат исходного геттера на снимке входных полей,
+подготовленном Rust. Время и флаг заданы самим тестом; они не доказывают
+начальный режим запуска AutoDemo в оригинале и не являются захватом его фабрик
+или состояния объекта Control.
+
Отдельный native setter selector `17`, tag-specific методы прочих factory tags,
их внешние эффекты и вызов из регулярного шага Behavior здесь не моделируются.
Числа должны оставаться конечными и нормальными либо точным нулём; произведения
-и отношения за поддерживаемым диапазоном `f32` возвращают явную ошибку, а не
-подменяются нулём или `None`. При проверке диапазона Rust оценивает операцию в
-`f64`, затем выполняет её в `f32`; это ограниченная проверка диапазона, а не
-эмуляция полного x87 accumulator-а.
+и суммы за поддерживаемым диапазоном `f32` возвращают явную ошибку, а не
+подменяются нулём или `None`. При проверке диапазона Rust сначала оценивает
+границы в `f64`, затем выполняет каждую операцию в `f32`; это сохраняет
+пошаговое округление этого getter-а, но не эмулирует весь x87 execution state.
Отдельная ветка того же GOG `AniMesh.dll` показывает, когда создаётся Behavior
для host object. В `FUN_10002ff0` (RVA `0x2ff0`, SHA-256
@@ -941,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
@@ -1354,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`. Читатель потребляет
@@ -1365,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