From 89a0d1da6180767dd20fccc47227580b83f9a874 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Tue, 20 Oct 2026 05:15:11 +0400 Subject: fix(runtime): warm behavior before mission Init --- docs/appendices/knowledge-boundaries.md | 44 ++++++++++++-- docs/appendices/script-vm.md | 100 +++++++++++++++++++++++--------- 2 files changed, 113 insertions(+), 31 deletions(-) (limited to 'docs/appendices') diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index a45d752..8551afa 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -79,8 +79,7 @@ control word процесса. `[(500.2857, 751.8217), (728.0, 449.0)]` и записи `[(500, 752, 0), (728, 449, 1)]`; preview кампании сохраняет package и статус `Unsupported`, не применяя частичные записи. Это подтверждает загрузчик и узкий Init resolver -Rust, но не доказывает, что штатный host вызывает `Init` при старте оригинальной -миссии. +Rust. Нативный конструктор показывает предупреждение при координате выше `10000`, но продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение; @@ -92,9 +91,44 @@ Rust, но не доказывает, что штатный host вызывае Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные ссылки и типы остаются ошибками в обоих режимах. -Открыты вызов `Init` при штатном старте Iron3D и проверка другой миссии с -непустым `Init`. Не восстановлены host-ingress `Init` в игровом цикле и общий -dispatcher остальных событий. +Для GOG-сборки также найден статический host call path в `Init`. В +`iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи +кланов и вызывает для них `FUN_100392e0`, затем вызывает `FUN_10039440` (RVA +`0x39440`). Эта функция передаёт AI callback-у пару явных аргументов через +vtable slot `+0x14`. В `ai.dll!FUN_10001ae0` (RVA `0x1ae0`) аргумент callback-а +записывается в `AI+0x3fc`; затем объект `AI+0x438` получает вызов Behavior +vslot `+0x2c`. В проверенной `Behavior.dll` (SHA-256 +`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) +`FUN_1001adb0` (RVA `0x1adb0`) выполняет начальную подготовку расписания. +Метод ищет первый `Init` только при нулевом latch +`AI+0x8a8`, а при dispatch временно ставит и снимает recursion guard +`AI+0x78`. В этой ветви поэтому известны callback ingress и порядок +warm-start → `Init`, но смысл аргумента callback-а здесь не сопоставлен с Rust +`MissionMode`. + +Изолированный x86 oracle вызывает исходные `iron3d.dll!FUN_10039440`, +`ai.dll!FUN_10001ae0` и script dispatcher на синтетических записях и проверяет +режим, guard, latch, Handler(19) writes и повторный вызов. Отдельный oracle +исполняет `Behavior.dll!FUN_1001adb0` с искусственно заданной очередью, сверяя +срок, шаг LCG и очистку полей. Это подтверждает ABI и локальные переходы исходных +функций, но не доказывает, что полная штатная загрузка выбранной миссии в +оригинальном процессе доходит до этой ветви в том же порядке. Метод +`FUN_10001ae0` оставляет latch `+0x8a8` сброшенным, когда `Init` не найдено; +если оно найдено при уже занятом guard `+0x78`, dispatch пропускается, но latch +всё равно устанавливается. Rust пока запускает обработанный поднабор при каждой +полной загрузке и не моделирует аргумент callback-а `AI+0x3fc`, guard `+0x78` или +latch `+0x8a8`. + +Rust `load_mission` принимает явный `queue_time_ms`, а не читает оригинальное +время очереди. Новый `Engine` начинает локальный Behavior RNG с seed `1` и +учитывает шаги генератора этой стартовой последовательности; не восстановлены +другие потребители исходного генератора, поэтому равенство полной случайной +последовательности не утверждается. Начальная фаза выполняется после регистрации +объектов в создаваемом мире Rust; +это интеграционный выбор runtime, а не подтверждённая эквивалентность всех +host construction phases. Не проверена другая лицензированная миссия с +непустым `Init`, не восстановлены игровой tick Behavior и общий dispatcher +остальных событий. Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA `0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`), diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index d12363a..9bb33db 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -33,12 +33,13 @@ consumer-inferred. ## Безопасная модель исполнения -Новая VM разделяет immutable package (bytecode, symbols, events, constants), -per-mission variables/timers/frames, bindings logical-name/ObjectId/clan/ -research key и typed commands к World3D/Behavior/UI/campaign. После varset -defaults и bindings она dispatches Init/start, на каждом tick обновляет timers, -ставит готовые events в стабильную очередь и исполняет bounded instruction -budget. Опасное удаление идёт через World3D queue и общий deferred lifecycle. +Целевая VM должна разделять immutable package (bytecode, symbols, events, +constants), per-mission variables/timers/frames, bindings +logical-name/ObjectId/clan/research key и typed commands к +World3D/Behavior/UI/campaign. После varset defaults и bindings она будет +запускать Init/start, обновлять timers на каждом tick, ставить готовые events +в стабильную очередь и исполнять bounded instruction budget. Опасное удаление +должно идти через World3D queue и общий deferred lifecycle. До восстановления opcode table package mode читает header/strings/symbols/ event offsets/raw bytecode losslessly. Статический анализ уже выделил отдельный @@ -267,11 +268,14 @@ scheduler path внутри запущенной игры. На границе mission runtime выбранный TMA clan `first_resource` материализуется как отдельный `MissionScriptBundle`: loader нормализует -`.scr`, декодирует его тем же bounded reader-ом и публикует immutable -package вместе с clan provenance. Для первого события `Init` runtime исполняет -только подтверждённый фрагмент dispatcher-а с `Handler(19)`; остальные события -пока остаются только прочитанными данными. Ошибка чтения по-прежнему сохраняет -transactional rollback mission loader-а. +`.scr`, декодирует его тем же bounded reader-ом и сохраняет clan +provenance. Полная загрузка получает от вызывающей стороны значение +32-битного времени очереди, затем для каждого bundle вычисляет начальное +состояние Behavior и пробует выполнить только восстановленный поднабор первого +`Init` с `Handler(19)`. Остальные события пока остаются прочитанными данными. +Статический просмотр не запускает warm-start Behavior, но сохраняет результат +поддержанных `Handler(19)` writes и создаёт ячейки varset; при неподдержанной +инструкции оставляет default-значения и статус `Unsupported`. TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не задаст тип (integer/float bits/ObjectId/enum/fixed-point/index). В том числе @@ -359,34 +363,78 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение оригинального dispatcher-а. -Для проверки использованы два x86 helper-а на синтетической памяти. Первый -вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование -TMA-координат в аргументы `CreateSuperAI`. Второй загружает оригинальную -`ai.dll`, вызывает `FUN_10011f40` и `FUN_10011f10`, а записи читает штатным -getter-ом `FUN_10013570` (RVA `0x13570`). В нём проверены оба набора AutoDemo, -первая дублирующаяся запись `Init`, имя `Init\\0suffix\\0`, отсутствие записи -при пропущенном `Init` и связка `FUN_10005c70` → Handler(19): для входных DWORD -`(0xffffffff, 10001)` поля стали `(2^32, 10001.0)`, а varset получил -`(0, 10001, 2)`. Наборы AutoDemo дали `(500, 752, 0)` и `(728, 449, 1)`. -Эти синтетические вызовы подтверждают изолированные участки ABI, но не доказывают, -кто в Iron3D запускает `Init` при полном старте игровой миссии. - -Обе проверки можно повторить из корня репозитория в Windows PowerShell. Первая -вызывает оригинальный caller `iron3d.dll`; вторая — original dispatcher, setter -и getter `ai.dll`: +### Первый вызов Behavior и сценарного `Init` + +В той же GOG-сборке `Behavior.dll` (SHA-256 +`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) функция +`FUN_1001adb0` (RVA `0x1adb0`) обновляет один Behavior distributor. Она получает +текущее время через `GetQueue` и queue vslot `+0x38`. Если distributor `+0x10` +равен нулю, функция записывает туда текущее время очереди, считает начальный +deadline `+0x14` по формуле `время + 3*64 + jitter`, где `jitter` лежит в +`0..63` и получен одним шагом глобального MSVCRT LCG. В этой ветви она также +обнуляет пять слов `+0x30..+0x40`. При повторном вызове до deadline ненулевой +`+0x10` оставляет deadline прежним, не расходует новое случайное число и не +очищает эти слова. Поведение после истечения deadline этим опытом не проверено. +Если само время очереди равно нулю, `+0x10` остаётся нулевым, и повторный вызов +снова проходит ветвь начальной подготовки. + +Путь host callback в проверенной паре DLL теперь статически известен. В +`iron3d.dll` функция `FUN_1005ff40` (RVA `0x5ff40`) сначала вызывает +`FUN_100392e0` для TMA clan records, а затем для записей вызывает +`FUN_10039440` (RVA `0x39440`). Последняя передаёт AI callback-у аргументы через +vtable slot `+0x14`. Метод `ai.dll!FUN_10001ae0` (RVA `0x1ae0`) сохраняет это +слово аргумента в AI `+0x3fc`, сначала обновляет Behavior через объект `AI+0x438` +и его vslot `+0x2c`, а затем, если latch `AI+0x8a8` ещё сброшен, находит первый +`Init`. При отсутствии события latch остаётся нулевым. Если событие найдено, +метод запускает dispatcher только при нулевом recursion guard `AI+0x78`, +временно ставит guard и снимает его после dispatch; затем ставит latch, даже +если занятый guard пропустил dispatch. Это определяет одну штатную +callback-точку входа и порядок внутри неё, но не раскрывает всех смыслов +входного слова аргумента и не подменяет динамическую трассу полного запуска игры. + +`fparkan-runtime::load_mission` получает значение времени очереди явным +аргументом `queue_time_ms`; Rust не читает оригинальную очередь. Для полной +загрузки Rust после регистрации объектов запускает последовательность +подготовки Behavior → ограниченный `Init` по каждому bundle. Путь использует +локальный RNG engine, начиная с состояния `1`; его шаги LCG общие между +bundle, но остальные потребители исходного глобального RNG не моделируются. +В Rust также нет значения аргумента в `AI+0x3fc`, recursion guard `AI+0x78` +или одноразового latch `AI+0x8a8`. Поэтому это связанный начальный поднабор, а +не полная активация SuperAI и не обновление Behavior на игровых тиках. При +ошибке ограниченного `Init` Rust отклоняет полную загрузку и сохраняет прежний +мир и состояние генератора; такая политика отката принадлежит Rust loader-у. + +Для проверки использованы три x86 helper-а на синтетической памяти. Caller +oracle вызывает оригинальные `iron3d.dll` entry points и проверяет преобразование +TMA-координат в аргументы `CreateSuperAI`. AI oracle проходит оригинальный +`iron3d.dll!FUN_10039440` → `ai.dll!FUN_10001ae0` → реальный `Init` dispatcher; +также проверяет оба AutoDemo-набора, повторный/missing/busy callback и связку +`FUN_10005c70` → Handler(19). Для входных DWORD `(0xffffffff, 10001)` поля стали +`(2^32, 10001.0)`, а varset получил `(0, 10001, 2)`. Behavior oracle вызывает +`FUN_1001adb0` с искусственно заданной очередью и проверяет шаг генератора, +срок, очистку и повторный вызов. Эти проверки исполняют оригинальные DLL-функции на заданных +структурах, но не являются трассой штатного старта или последующего игрового +цикла. + +Проверки можно повторить из корня репозитория в Windows PowerShell: ```powershell $env:FPARKAN_ORIGINAL_IRON3D_DLL = 'C:\GOG Games\Parkan - Iron Strategy\iron3d.dll' $env:FPARKAN_ORIGINAL_AI_DLL = 'C:\GOG Games\Parkan - Iron Strategy\ai.dll' +$env:FPARKAN_ORIGINAL_BEHAVIOR_DLL = 'C:\GOG Games\Parkan - Iron Strategy\Behavior.dll' $env:FPARKAN_ORIGINAL_DEFAULT_SCR = 'C:\GOG Games\Parkan - Iron Strategy\MISSIONS\SCRIPTS\default.scr' New-Item -ItemType Directory -Force -Path target\handler19-native-oracles | Out-Null & "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" /nologo /platform:x86 /out:target\handler19-native-oracles\AnchorOracle.exe crates\fparkan-runtime\tests\handler19_anchor_native_oracle.cs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" /nologo /platform:x86 /out:target\handler19-native-oracles\InitOracle.exe crates\fparkan-runtime\tests\handler19_init_native_oracle.cs if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& "$env:WINDIR\Microsoft.NET\Framework\v4.0.30319\csc.exe" /nologo /platform:x86 /out:target\handler19-native-oracles\BehaviorWarmStartOracle.exe crates\fparkan-runtime\tests\behavior_warm_start_native_oracle.cs +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & .\target\handler19-native-oracles\AnchorOracle.exe if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } & .\target\handler19-native-oracles\InitOracle.exe +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +& .\target\handler19-native-oracles\BehaviorWarmStartOracle.exe exit $LASTEXITCODE ``` -- cgit v1.2.3