diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 425 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 468 | ||||
| -rw-r--r-- | docs/appendices/ui-shell.md | 110 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 61 | ||||
| -rw-r--r-- | docs/reference/msh.md | 109 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 52 | ||||
| -rw-r--r-- | docs/tomes/03-resources.md | 15 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 14 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 301 |
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 |
