diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 162 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 138 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 90 |
3 files changed, 301 insertions, 89 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index b313375..bf32aec 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -133,7 +133,7 @@ control word процесса. Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами `[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0), (728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого -script base и поддерживает numeric literal assignments в первом `Init`. +script base и поддерживает узкий набор атомарных формул в первом `Init`. Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки чтения распространяются как VFS errors. @@ -144,9 +144,11 @@ script base и поддерживает numeric literal assignments в первÐ два доказанных действия: `Handler(19)` и formula assignment selector `-1` при source `-1`, нулевой глубине условий и корректных индексах. У `Handler(19)` control находится в `header_words[3]`, а `header_words[1]` — result-save -reference; оба равны `0xffffffff` в поддержанной форме. У formula assignment -`header_words[1]` — индекс целевой varset ячейки, `header_words[2]` — source, -а индекс formula row занимает `header_words[6]`. Маппинг подтверждён +reference; оба равны `0xffffffff` в поддержанной форме. В дисковом заголовке +formula assignment `header_words[1]` — индекс целевой varset ячейки, +`header_words[2]` — source, а индекс formula row занимает `header_words[6]`; +индексы цели и формулы в native record находятся по смещениям `+0x08` и `+0x1c` +соответственно. Маппинг подтверждён `ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA `0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`. `Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`; @@ -154,23 +156,58 @@ reference; оба равны `0xffffffff` в поддержанной форме Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0` (RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`) -строит numeric literal node, а evaluator `FUN_10015b30` (RVA `0x15b30`) -возвращает его raw `f32`. Target type `3` сохраняет эти bits; type `5` проходит -через `FUN_10012c00` (RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD -с усечением к нулю. Изолированный x86 oracle на оригинальном `ai.dll` и -настоящих ресурсах подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с +строит числовые, переменные и операторные узлы, а evaluator `FUN_10015b30` +(RVA `0x15b30`) в проверенных путях `+` и `-` считывает значения переменных и +сохраняет результаты как `f32` при x87 control word `0x007f`. В GOG-сборке +`ai.dll` с SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` поиск +переменной через varset vtable slot `+0x10` ведёт на RVA `0x16ad0`: сравнение +байтов точное, регистр учитывается, при повторах берётся первое объявление. +Getter `FUN_10013190` (RVA `0x13190`) читает type `3` как `f32`, type `5` как +беззнаковый `DWORD` и возвращает число через x87. Затем evaluator сохраняет +его как `f32` до операции. Оператор `+` +считает right+left, `-` — left-right; результат также сохраняется как `f32`. +Target type `3` сохраняет эти bits; type `5` проходит через `FUN_10012c00` +(RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD с усечением к нулю. +Изолированный x86 oracle на оригинальном `ai.dll` и настоящих ресурсах +подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с 26/5/149 строками и 15 присваиваний: кампанийные `df0..df3` и `Single.01` `dT8/dT9` получают `0`; `scream` задаёт `fDifficulty = 0.8f` и остальные проверенные DWORD `0, 0, 1, 10, 0, 100, 400, 2`. -Граница Rust остаётся узкой: ссылка должна вести на один digit-starting numeric -token и типизированную `float`/`DWORD` ячейку. Неизвестные expression nodes, -переменные, unary operators, составные выражения и source-copy ветвь -`source != 0xffffffff` остаются явными ошибками; native source-copy переносит -всю varset record, не только числовое значение. В preview package сохраняется, -но `Unsupported` не применяет частичные writes; полная загрузка отклоняет весь -новый мир и сохраняет предыдущие world/RNG. Это политика Rust, не правило -оригинального dispatcher-а. +Граница Rust остаётся узкой: один числовой токен либо ASCII-имя переменной, +или два таких атома с одним бинарным `+`/`-`. Имя проверяется раньше числового +преобразования, сравнивается с учётом регистра и выбирает первое совпадение; +публичный `FormulaTable::evaluate_numeric_literal` принимает только числовые +литералы. Временная копия значений начинается со значений по умолчанию и +обновляется после каждого успешного действия `Init`, поэтому следующая формула +видит предыдущие записи. Нет +поддержки неизвестных операторов, унарных форм, третьего атома, отсутствующих +или не-ASCII имён и source-copy ветви `source != 0xffffffff`; native source-copy +переносит всю запись varset. Preview сохраняет package, но не применяет частичные +writes; полный load отклоняет новый мир и сохраняет прежние world/RNG. Это +политика ограниченного Rust resolver-а, а не правило оригинального dispatcher-а. + +Ресурсные строки с переменными найдены в поздних событиях: `c1m3e.fml` row 19 +(`0.8 + fT`) вызывается из `Fort_Captured`, row 22 +(`dBuildingProtectHits - dT2`) — из `PBM_BUILDING_PROTECT_Start`, а `c1m4e.fml` +row 0 (`fTemp + 0.00001`) — из `PBM_BUILDING_INF_CAPTURE_Start`. Декодирование +этих `.scr` не показывает эти формулы в первом `Init`. Игнорируемая проверка +читает настоящие `.fml` и `varset.var`, но вставляет строки в синтетический +`Init`; она проверяет resolver, а не оригинальное исполнение. Одноразовый x86 +oracle на исходной GOG `ai.dll` с SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` проверил +13 отдельных присваиваний: 9 контролируемых выражений и 4 вызова с настоящими +строками `.fml` (`c1m3e` rows 19/22, `c1m4e` row 0 дважды подряд). В каждом векторе +native harness записывал вычисленный `f32`, типизированное raw-слово и getter; +scratch-проход через публичный Rust parser/resolver совпал со всеми 13 +векторами по `f32`-результату и raw-записи. Для native вызовов control word +временно принудительно устанавливался в `0x007f`: 87 наблюдений сохранили +`0x027f` на входе и после восстановления, +а canary-проверки прошли. Менялись только varset-копии в памяти. Это +подтверждает компиляцию формулы, локальное вычисление и typed writes для этих +входов, но не запуск исходного поиска и обхода события `Init`, исполнение поздних +событий или поведение живой миссии. Лицензированный test загружает одну первую кампанийную миссию `CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход @@ -184,6 +221,34 @@ core и `ResearchCandidateSet::refresh_selection`: последний получ создаёт владельца живого контекста, не передаёт реальные AI candidates и не выбирает результат Handler(43). Это не общий dispatcher и не загрузка игры. +Начальный массив слота AI `+0x38` теперь восстановлен отдельно от этого +исследовательского пути. В `iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_100392e0` (RVA `0x392e0`) вызывает слот для каждого значения счётчика +`second_resource.tag`. В `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` метод +RVA `0x2010` передаёт `AI+0x83c` в `FUN_10002980` (RVA `0x2980`), где +`FUN_100048f0` (RVA `0x48f0`) добавляет переданный DWORD. Вызывающая сторона +повторно передаёт `0xffffffff`; эта функция не записывает значение обратно. +Поэтому подтверждён только начальный массив sentinel-слов. Неизвестно, какие +поздние обработчики читают эти ячейки и заменяет ли кто-либо их ID. В начальном +массиве нет назначенных ID объектов; дальнейший смысл этих слов пока не +установлен. Сам массив не является списком +`BehaviorDistributorState.registered_objects` и не определяет владельца +контекста исследований. + +Rust хранит этот снимок как `MissionAiStartupState` в состоянии загруженной +миссии. Полная загрузка проходит по всем TMA-кланам, включая кланы без пакета +сценария и без пути таблицы исследований; при предпросмотре список состояний AI +остаётся пустым. Знаковое поле `second_resource.tag` читается по сырым битам как +`u32`. Бюджет в 1 000 000 DWORD на миссию и выделение памяти с обработкой ошибки +защищают только память Rust; это ограничение не приписывается оригинальному +движку. Искусственные Rust-тесты проверяют обе формы TMA-клана, индекс клана, +независимые массивы, пустой путь сценария, предпросмотр, чтение тега как +беззнакового числа, лимит и откат при ошибке. Проверка на оригинальных ресурсах +для `Single.01` подтверждает состояния обоих кланов (`9/9`) до выполнения +`Init`; это не полный запуск миссии и не исполнение исходных DLL. + Для GOG-сборки также найден статический host call path в `Init`. В `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи кланов и вызывает для них `FUN_100392e0`, затем вызывает `FUN_10039440` (RVA @@ -713,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` не имеет доказанных единиц @@ -877,22 +945,34 @@ arg1 следующего вызова. `AddNewObjectToGame` принимает `FUN_10002a30` (RVA `0x2a30`, `RET 8`) записывает его в `inner+8`. TMA tag `row+0x20` ограничивает число вызовов AI slot `+0x38`, но не задаёт владельца. +Создание AI не фильтруется по наличию сценарного пути. В `iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов и вызывает для каждой +`FUN_100392e0` (RVA `0x392e0`); в `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` +`CreateSuperAI` (RVA `0xf710`) выделяет AI и вызывает конструктор по RVA +`0x1000` без проверки пути сценария. Создание исследовательского контекста +следует за успешным созданием AI и требует включённого флага создания +исследований и непустого `second_resource.path` до первого NUL. Переданный в +`descriptor[0]` индекс клана, а не `second_resource.tag`, задаёт владельца; +тег остаётся счётчиком начальных вызовов AI slot `+0x38`. + В Rust эта граница представлена только загрузкой таблиц. Полный `load_mission` требует явный `MissionStartupInput { queue_time_ms, -research_creation_gate }`; gate не выводится из `MissionMode` или имени пути. -При `true` загрузчик проходит по успешно разобранным пакетам сценариев, берёт -реальный `second_resource.path` соответствующего TMA-клана до первого NUL, -отклоняет не-ASCII путь, затем сохраняет отдельный `ResearchTrfDocument` с -`clan_index` и нормализованным путём. Пустая строка, выключенный gate или -отсутствие пакета сценариев пропускают чтение. Повтор одного пути разными -владельцами приводит к отдельным буферам строк TRF0. Обычные тесты на -искусственных данных проверяют владельцев, повторное декодирование, пропуски, -ошибки и откат; игнорируемая проверка оригинальных ресурсов связывает реальные -`Campaign.00/Mission.01` пары путей с индексами владельцев. Это не загружает -полный исследовательский контекст оригинала. -Успешно разобранный пакет сценариев здесь обозначает только границу текущей -настройки Rust, а не совпадение с полным нативным жизненным циклом -`CreateSuperAI`. +research_creation_gate }`; этот флаг не выводится из `MissionMode` или имени +пути. Если флаг равен `true`, загрузчик проходит по существующим +`MissionAiStartupState` всех TMA-кланов, в том числе без сценарного пакета, +читает их `second_resource.path` до первого NUL и отклоняет не-ASCII путь. +Пустая строка пропускается; выключенный флаг и статический preview не читают +TRF. На каждого +владельца сохраняется отдельный `ResearchTrfDocument` с нормализованным путём, +даже когда разные кланы указывают на один файл. Обычные тесты на искусственных +данных проверяют владение у кланов со сценариями и без них, повторное +декодирование, пропуски, ошибки и откат; игнорируемая проверка оригинальных +ресурсов связывает реальные `Campaign.00/Mission.01` пары путей с индексами владельцев. +Rust пока сохраняет только декодированные данные и связь с владельцем: он не +создаёт полный исследовательский контекст, не строит AI-каталог и не исполняет +`Handler(43)`. Нативный metadata loader нельзя считать простым копированием таблицы. В GOG `Behavior.dll` (SHA-256 diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 6e48445..145d2a2 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -22,8 +22,8 @@ symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaul `fparkan-script::parse_varset` уже читает подтверждённые numeric `VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и -general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые -строки сохраняются для доказанных literal assignments первого `Init`; +general `FUNCTION(...)` evaluation остаются вне восстановленного контракта, +но принятые строки сохраняются для ограниченного поднабора формул первого `Init`; GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration: 31 `float` и 200 `DWORD` (от `f0` до `fY`); loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при @@ -424,10 +424,13 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие `Init` и проходит его инструкции по порядку. Он возвращает записи для -поддержанного `Handler(19)` и числовых literal assignments; прочая форма -останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи -к отдельной копии varset выбранного клана лишь после успешного прохода всего -события. Отсутствующий `Init` даёт +поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо +с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма +останавливает preview на `Unsupported`. Runtime начинает вычисление с копии +defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя +формула видит прежние записи этого события. Упорядоченные записи попадают в +состояние клана только после успешного прохода всего `Init`. Отсутствующий +`Init` даёт статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт `Executed` без записей. Если форма инструкции пока не поддержана, package остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения @@ -437,7 +440,7 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение оригинального dispatcher-а. -#### Числовые литералы `.fml` в `Init` +#### Литералы и простые формулы `.fml` в `Init` В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`) загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`. @@ -455,16 +458,35 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D остаётся ошибкой VFS. Native selector `-1` при source `-1` и нулевой глубине условий выбирает -formula assignment; в native record `header_words[1]` — индекс целевой varset -ячейки, а `header_words[6]` — индекс formula row. `FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки, -`FUN_10015b30` (RVA `0x15b30`) возвращает результат единственного literal node -kind `3`, а `FUN_10013650` передаёт его типизированной ячейке через -`FUN_10012c00` (RVA `0x12c00`). Компилятор `FUN_100152a0` получает числовой -prefix как `double` через `FUN_10020af0` (RVA `0x20af0`), затем записывает -его как `float` в node `+0x10`. Rust ограничивает эту ветвь одним -digit-starting числовым токеном с необязательным хвостом пробельных байтов; -ведущие unary `+`/`-`, variable references, операторы и другие node kinds -остаются явными `UnsupportedExpression`. +formula assignment. В дисковом заголовке `.scr` `header_words[1]` — индекс +целевой varset-ячейки, а `header_words[6]` — индекс formula row; соответствующие +поля native record находятся по смещениям `+0x08` и `+0x1c` соответственно. +`FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки. Компилятор +`FUN_100152a0` (RVA `0x152a0`) создаёт +числовые узлы, узлы переменных и узлы операторов; числовой prefix проходит через +`FUN_10020af0` (RVA `0x20af0`) как `double`, после чего сохраняется в узле как +`float`. Для переменной компилятор вызывает vtable slot `+0x10` таблицы varset; +в проверенной GOG-сборке `ai.dll` этот вызов ведёт к поиску по RVA `0x16ad0`. +Поиск сравнивает байты имени с учётом регистра и возвращает первое совпадение. +`FUN_10015b30` (RVA `0x15b30`) читает числовой узел как `f32`, а переменную — +через `FUN_10013190` (RVA `0x13190`): type `3` даёт `float`, type `5` — +беззнаковый `DWORD`. Getter возвращает число через x87; evaluator сохраняет +его как `f32` перед операцией. Оператор `+` складывает правый +и левый операнды, а `-` вычисляет левый минус правый; после узла результат +сохраняется как `f32`. `FUN_10013650` передаёт итог типизированной ячейке через +`FUN_10012c00` (RVA `0x12c00`). + +Rust в первом `Init` принимает один числовой токен или имя ASCII-переменной, +а также ровно два таких атома с одним бинарным `+` либо `-`. Например, +`source + 2.0` и `target - source` читают значения из defaults или из предыдущих +успешных записей события. Имя ищется раньше числового преобразования, поэтому +объявление с именем `1e-2` перекрывает такую числовую запись; сравнение имён +чувствительно к регистру, а повтор объявлений выбирает первое. `DWORD` читается +как беззнаковое число и округляется до `f32` до операции. Публичный +`FormulaTable::evaluate_numeric_literal` по-прежнему принимает только литералы. +Ведущие unary `+`/`-`, `*`, другие операторы, третий атом, отсутствующие или +не-ASCII имена и зарезервированные одиночные операторы остаются `Unsupported`; +так же не поддержана ветвь копирования `source != 0xffffffff`. Проверенный native tokenizer отличается от обычного строгого decimal parser: `1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node @@ -477,8 +499,8 @@ digit-starting числовым токеном с необязательным Ñ исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`) с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся `0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw -DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого -`Init` применяется только после успешного прохода. +DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init` +публикуются только после успешного прохода. Native x86 oracle на GOG `ai.dll` SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил @@ -500,6 +522,27 @@ Native x86 oracle на GOG `ai.dll` SHA-256 | 7 | `dMaxPlant` (`DWORD`) | `400 → 400` | | 8 | `d7` (`DWORD`) | `2 → 2` | +Отдельный одноразовый x86 oracle на той же GOG `ai.dll` проверил ещё 13 +выражений: 9 строк контролируемой `controlled.fml`, строки 19 и 22 настоящей +`c1m3e.fml` и два последовательных вызова строки 0 настоящей `c1m4e.fml`. +Native harness записывал `f32`-результат evaluator-а, типизированную raw-запись +и getter после записи. В scratch-проверке те же входы прошли через публичные +`parse_varset`, `FormulaTable::parse` и `VarSet::resolve_init`; результат +evaluator-а и raw-слово Rust совпали с native во всех 13 случаях. Среди +проверенных границ: `2 - 3` записал в `DWORD` `0xffffffff`; чтение исходного +`DWORD 0xffffffff` дало `f32 0x4f800000`, а последующее `+ 0` записало в +`DWORD` ноль. Для настоящих строк `c1m3e` получены `fTemp = 0x40333333` и +`dTemp3 = 0`; два последовательных вызова строки 0 `c1m4e` дали +`fTemp = 0x4033335d`, затем `0x40333387`. + +Oracle принудительно устанавливал x87 control word `0x007f` на время каждого +проверяемого вызова: все 87 наблюдений показали входное и восстановленное +значение `0x027f`, активное и выходное — `0x007f`; canary-проверки прошли. +Менялись только varset-копии в памяти процесса. Эта выборка подтверждает +компиляцию, вычисление и типизированные записи ровно для перечисленных входов. +Она не запускала исходный поиск и обход события `Init` и не исполняла поздние +события или миссию; выборка не доказывает полную грамматику evaluator-а. + Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую ссылается поддержанная запись. Лицензированный runtime-тест подтверждает полную загрузку кампании Mission.01 после этих присваиваний; у обычной @@ -511,6 +554,17 @@ Rust сохраняет всю таблицу строк, но вычисляеÑ не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение запуска самой игры или полной VM. +В других строках тех же ресурсов встречаются `0.8 + fT` (`c1m3e.fml`, row 19), +`dBuildingProtectHits - dT2` (row 22) и `fTemp + 0.00001` (`c1m4e.fml`, row 0). +Декодированные `c1m3e.scr` и `c1m4e.scr` ссылаются на них из более поздних +событий `Fort_Captured`, `PBM_BUILDING_PROTECT_Start` и +`PBM_BUILDING_INF_CAPTURE_Start`, а не из их `Init`. Игнорируемый Rust-тест +подаёт эти сохранённые строки в изменённый синтетический `Init` и использует +настоящий общий `varset.var`; это проверка resolver-а, а не утверждение о +исполнении таких выражений в исходном `Init`. Отдельный native oracle и +сравнение с Rust выше проверяют эти строки при явно заданных исходных ячейках, +но не подтверждают их исполнение в оригинальном сценарии или в живой игре. + ### Первый вызов Behavior и сценарного `Init` В той же GOG-сборке `Behavior.dll` (SHA-256 @@ -760,8 +814,9 @@ tie-векторов подтверждает реакцию `FISTP`, но не два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из -одного numeric literal; остальные формы инструкции и содержимое `.scr` -сохраняются reader-ом, но остаются вне этого исполнения. +одного числового или переменного атома либо двух таких атомов с одним бинарным +`+` или `-`. Остальные формы инструкции и содержимое `.scr` сохраняются +reader-ом, но остаются вне этого исполнения. ### Начальная группировка подготовленных Control-дескрипторов @@ -1355,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`). Последний путь не является @@ -1422,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`) diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 6684b13..3a4e51c 100644 --- a/docs/tomes/06-behavior.md +++ b/docs/tomes/06-behavior.md @@ -169,8 +169,10 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga показывает предупреждение для чисел выше `10000`, но не отменяет создание AI. Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)` -и числовые literal assignments, описанные ниже. Поддержанные записи применяются -к состоянию лишь после успешного прохода всего первого `Init`. +и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом — +число или имя переменной. Поддержанные шаги сразу обновляют временную копию +varset, поэтому следующая формула читает уже записанные значения. К состоянию +клана записи попадают лишь после успешного прохода всего первого `Init`. Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает `Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило @@ -186,15 +188,54 @@ Rust исполняет подтверждённый поднабор перво нули в `dT8` и `dT9`. Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8` -сохраняется как число `float`. Пока Rust вычисляет только числовой литерал, -начинающийся с цифры; операторы, ссылки на переменные и другие формы формул -остаются явными ошибками. В тесте полной загрузки миссия `Mission.01` -проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном -`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо -этого действия. +сохраняется как число `float`. Например, если `source` равен `10`, выражение +`source + 2.0` записывает `12`; следующее `target - source` читает новое значение +`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки +`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат +сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один +бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не +восстановлены. В отдельной точечной проверке вычисления и записи оригинальной +DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только +эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает, +что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный +`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется +вместо этого действия. ### Первый запуск сценария +Перед настройкой первого шага каждый AI получает начальный массив DWORD. В +`iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_100392e0` (RVA `0x392e0`) повторяет вызов слота AI `+0x38` столько раз, +сколько задаёт `second_resource.tag` этой строки TMA. Этот слот ведёт в +`ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: +метод RVA `0x2010` передаёт коллекцию при `AI+0x83c` в `FUN_10002980` +(RVA `0x2980`), а она добавляет один DWORD через `FUN_100048f0` (RVA +`0x48f0`). Каждый вызов копирует `0xffffffff`; входное слово не меняется. +Поэтому тег `3` даёт `[0xffffffff, 0xffffffff, 0xffffffff]`. Это число +начальных ячеек, а не назначение трёх ID. + +Изолированная x86-проверка вызовом оригинальной `ai.dll` подтвердила пары +«число слотов/ёмкость» `0/0`, `1/17`, `17/17` и `18/34`. Все добавленные слова +равны `0xffffffff`, а входное значение, контрольные поля, память вне заголовка +массива и указатель стека сохраняются. Это подтверждает добавление начального +слова, но не работу последующих обработчиков. + +Rust хранит такой снимок в отдельном `MissionAiStartupState` для каждого TMA-клана, +а не в пакете сценария. При полной загрузке (`Full`) создаётся запись для каждого +клана, в том числе если у него нет пути сценария или пути таблицы исследований. Счётчик +берётся из сырых битов `second_resource.tag` как `u32` у обеих форм записи — +`Standard` и `Spatial`; нулевой счётчик даёт существующее состояние с пустым +списком. Статический режим предпросмотра не создаёт состояний AI. + +Загрузка отклоняет суммарный объём свыше 1 000 000 DWORD и резервирует память +проверяемой операцией; это защита Rust от чрезмерного выделения памяти, а не +ограничение оригинальной игры. Rust сохраняет начальные слова, но не +восстанавливает работу последующих обработчиков и не назначает этим словам ID +объектов. Этот массив также не является списком +`BehaviorDistributorState.registered_objects`. + До регулярной работы Behavior настраивает расписание своего первого шага. Он берёт текущее 32-битное время игровой очереди и назначает проверку через три интервала по 64 мс, добавляя небольшую случайную задержку от 0 до 63 мс. Эта @@ -213,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт значение времени очереди; после регистрации объектов загрузчик последовательно подготавливает каждый сценарный пакет и выполняет восстановленные действия -`Handler(19)` и числовые literal assignments из первого `Init`. Статический +`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном varset. @@ -546,8 +587,14 @@ let cost = calc_full_research_cost(&candidate, &table)?; штатный Handler(43) и запуск миссии здесь не проверялись. Владелец начальной записи исследования — индекс текущего прохода по кланам. -Движок сохраняет его в `descriptor[0]`, а затем переносит в контекст, чтобы -связать запрос с AI-данными того же клана. +В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов +и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI; +`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор +по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского +пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие +сценарного пакета условием не является. Индекс клана сохраняется в +`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными +того же клана. Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из `GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов @@ -557,12 +604,14 @@ Handler(43) и штатный запуск миссии остаются непÑ Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди и отдельный `research_creation_gate`. Режим миссии и имя файла этот -флаг не заменяют. При включённом флаге таблица загружается только для клана, -чей сценарный пакет уже прочитан и успешно разобран; путь берётся из второго -ресурса этого клана как C-строка. Пустая C-строка пропускается, а не-ASCII -байты дают явную ошибку. Клан индексируется по порядку в TMA, тег ресурса на -владение не влияет. Если несколько кланов указывают на один файл, каждый -получает отдельно декодированную таблицу с собственными строками TRF0. +флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся +`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного +пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой +путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку. +Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег +`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов +указывают на один файл, каждый получает отдельно декодированную таблицу со +своими строками TRF0. ```rust MissionStartupInput { @@ -571,9 +620,10 @@ MissionStartupInput { } ``` -Это сохраняет исходные таблицы и их связь с владельцами для следующих этапов. -Оно ещё не создаёт полный исследовательский контекст оригинала и не запускает -`Handler(43)`; эта обработка пока остаётся явно неподдержанной. +Это сохраняет декодированные таблицы и связь с владельцами для следующих +этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не +строит AI-каталог и не запускает `Handler(43)`; эти части пока не +восстановлены. Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция |
