fix(runtime): preserve native mission outcome guards
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s

This commit is contained in:
Valentin Popov committed 2026-10-20 03:20:20 +04:00
1 parent 65f1298d93
commit dc8be18458
6 files changed
+524 -68

No files matched your search

+12
View File
@@ -44,6 +44,18 @@ state ещё не восстановлены.
вывод по одному известному `ai.dll`, а не утверждение о совпадении во время
исполнения.
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
начальные поля IGame и проверки режима/состояния. Обработчик выбора миссии
выставляет режим 1 (RVA `0x29142`), а обработчик таймера — режим 3 (RVA `0x70f3`).
Небольшая тестовая x86-программа сравнивает этот обратный вызов с исходной DLL
на синтетических структурах. Пока не восстановлен сквозной путь от выбора и
инициализации миссии к первому событию Handler(30): Rust загружает миссию, но
ещё не выполняет штатный `stdInitGame`, переход состояния `5 -> 4`, запуск и
игровой цикл с полной диспетчеризацией событий. Поэтому
совпадение изолированного обратного вызова не доказывает совместимость живого
сценария.
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
table с instruction sizes, построить disassembler и сравнить выполнение
коротких scripts с оригиналом. Для evaluator-а условий отдельно проследить
+63 -18
View File
@@ -343,21 +343,20 @@ remaining script event semantics.
текстовых значений по умолчанию. `resolve_loaded_handler30` возвращает точную
команду обратного вызова `(mode=0, first, second)`, не приписывая ей
дополнительный смысл. Отдельная функция `apply_loaded_script_host_callback`
отображает в Rust состояние исхода только `(0, 0, payload=0|1)`; она не
выбирает событие `.scr` и не исполняет ветвь оригинальной игры.
моделирует в Rust состояние исхода для `(0, 0, payload=0|1)`; она не выбирает
событие `.scr` и не вызывает машинный код оригинала.
Снимок работающей GOG AutoDemo определяет обратный вызов: прочитанный без записи
указатель по `ai.dll + 0x555e4` равен `0x100611d0`, то есть `iron3d.dll +
0x611d0` при наблюдавшейся базе загрузки. Восстановленное ABI вызываемой
функции — `__cdecl(mode, command, payload)`; `Handler(30)` передаёт те же поля как
`(0, first, second)`. В режиме `mode == 0` команда `command == 0` выполняется,
только если контрольный байт по `IGame + 0xe5` равен нулю. Ветвь отказа при
значении аргумента `payload`, равном `0`, срабатывает лишь когда слово по
`IGame + 8` не равно `1`: игра запускает
`VOICE_MISSION_FAIL`, записывает `1` в это слово и обнуляет байт по
`*(IGame + 4) + 0x157`. Ветвь завершения при `payload`, равном `1`, срабатывает
лишь когда слово не равно `0`: игра запускает `VOICE_MISSION_COMPLETE`, записывает
`0` в слово и устанавливает байт в `1`. Команды 3, 4 и 5 ведут к другим
`(0, first, second)`. Обратный вызов принимает интересующую нас команду только
при `mode == 0`, `command == 0` и нулевом байте `IGame + 0xe5`. Ветвь отказа
(`payload == 0`) запускает `VOICE_MISSION_FAIL`, записывает `1` в слово `IGame + 8`
и обнуляет байт `*(IGame + 4) + 0x157`, только если прежнее слово не равно `1`.
Ветвь завершения (`payload == 1`) запускает `VOICE_MISSION_COMPLETE`, записывает
`0` в слово и устанавливает байт в `1`, только если прежнее слово не равно `0`.
Оба звуковых вызова предшествуют этим записям. Команды 3, 4 и 5 ведут к другим
ветвям игры; режим `2` выполняет отдельный вызов IGame, а ветви для режима `1`
в этой функции нет.
@@ -376,14 +375,60 @@ remaining script event semantics.
повторных вызовов, но не назначение сообщения интерфейса или смысл этих
идентификаторов; в Rust команда `1` поэтому остаётся `Unhandled`.
Сейчас `apply_loaded_script_host_callback` отображает `(0, 0, 0)` в локальное
состояние `Failed`, `(0, 0, 1)` — в `Completed`, а повтор уже записанного
состояния возвращает `NoChange`. Эта дедупликация относится только к модели
Rust и не заменяет проверки Iron3D: в Rust нет ни байта блокировки
`IGame + 0xe5`, ни слова состояния `IGame + 8`. Значение `Active` назначается
при загрузке миссии, но его соответствие исходному состоянию игры пока не
подтверждено. Команды 1, 3, 4, 5 и режим 2 остаются `Unhandled` в этом
обработчике исхода.
### Состояние обратного вызова исхода миссии
Слово `IGame + 8` создаётся не загрузчиком миссии. В исследованной GOG-сборке
`iron3d.dll` (SHA-256
`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`, image base
`0x10000000`) конструктор `FUN_1005c820` записывает туда `5` (RVA `0x5c84f`), а
байт `IGame + 0xe5` обнуляет (RVA `0x5c9f6`). При настройке игры функция
`FUN_1005cbb0` задаёт `+0xe5` выражением `(*AppState == 3)` (RVA `0x5cc4a`);
обработчик таймера главного меню выставляет режим `3` для AutoDemo (RVA `0x70f3`).
Отдельный обработчик выбора миссии записывает режим `1` (RVA `0x29142`) перед
переходом к пути `missions/single.*`. После успешной стандартной инициализации
игровой цикл записывает в `IGame + 8` значение `4` (`FUN_1005eb70`, RVA `0x5eb70`).
Этот переход запуска не выполняется при статическом просмотре миссии и здесь не
моделируется.
`MissionRequest` в Rust несёт явный `MissionMode::{SingleMission, AutoDemo}`;
режим задаёт вызывающая сторона и его нельзя выводить из имени пути. Во время
загрузки Rust инициализирует своё слово состояния значением `5`, как у нового
IGame, и сохраняет запрошенный режим; сама загрузка не вызывает нативный
конструктор.
Публичный `apply_loaded_script_host_callback` применяет те же интересующие нас
проверки: режим `AutoDemo` закрывает ветвь, а в `SingleMission` обратный вызов
меняет только слово состояния и исход Rust, если выполняется исходное условие
по этому слову. Поэтому при состоянии `4` ветвь завершения разрешена, как и
ветвь отказа, пока слово не равно `1`. Повторный отказ при состоянии `1` либо
завершение при состоянии `0` ничего не меняют; если тест намеренно
рассинхронизирует исход Rust и слово состояния, повтор оставляет это
расхождение. Так работает исходная проверка слова, а не выравнивание всех полей.
Изолированное сравнение с оригинальной DLL выполнено небольшой тестовой
x86-программой `crates/fparkan-runtime/tests/original_host_callback_oracle.cs`:
программа проверяет SHA-256, загружает `iron3d.dll` и вызывает RVA `0x611d0` с
синтетическими IGame и AppState. Для изоляции проверки она временно подставляет
синтетический IGame через глобальную переменную `DAT_1010a5f8` (RVA `0x10a5f8`)
и заменяет слот таблицы импортов звукового интерфейса (RVA `0xe42cc`)
регистратором; машинный код обратного вызова остаётся исходным. Проверка сверяет
записи полей, повторные/обратные переходы, блокирующий `+0xe5` и порядок вызова
звука. Это
подтверждает локальную семантику обратного вызова, но не запуск исходной миссии,
диспетчеризацию Handler(30) или весь игровой цикл. Rust пока не запускает
диспетчеризацию сценария автоматически. Команды 1, 3, 4, 5 и режим 2 остаются
`Unhandled` в этом обработчике исхода.
Повторить из корня репозитория в PowerShell на Windows с установленными GOG
ресурсами можно так:
```powershell
$env:FPARKAN_ORIGINAL_IRON3D_DLL = 'C:\GOG Games\Parkan - Iron Strategy\iron3d.dll'
New-Item -ItemType Directory -Force -Path target\original-host-callback-probe | Out-Null
& 'C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe' /nologo /platform:x86 /out:target\original-host-callback-probe\HostCallbackOracle.exe crates\fparkan-runtime\tests\original_host_callback_oracle.cs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& .\target\original-host-callback-probe\HostCallbackOracle.exe
exit $LASTEXITCODE
```
### Handler(8): problem-record state write
+12
View File
@@ -229,6 +229,18 @@ Tag выбирает конкретное правило. Для tag 1 запи
[`приложении о сценарной VM`](../appendices/script-vm.md#evaluator-записей-условий-ai-dll-rva-0x5180)
и [`границах знаний`](../appendices/knowledge-boundaries.md#script-vm).
### Исход миссии и состояние игры
Сообщение сценария об успехе или провале проходит ещё один рубеж — обратный
вызов в игровое приложение. У этой границы есть собственные правила: в режиме
отдельной миссии вызов может записать новый исход, а в автоматической демо-игре
та же ветвь подавляется. Повторный сигнал не обязан исправлять соседний флаг
или сохранённый Rust-исход: оригинал решает по своему слову состояния, а не по
сравнению всех полей между собой. Поэтому загрузка карты не означает, что игра
уже перешла в активный игровой цикл; это отдельный шаг запуска. Технические
смещения, начальное состояние и граница проверки описаны в
[`приложении о сценарной VM`](../appendices/script-vm.md#состояние-обратного-вызова-исхода-миссии).
### TRF и preload-данные
TRF-файлы проходят структурный разбор. `auto.trf`, `data.trf` и tutorial