fix(runtime): preserve native mission outcome guards
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
Docs Deploy / Build and Deploy MkDocs (push) Successful in 37s
This commit is contained in:
6 files changed
+524
-68
No files matched your search
@@ -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-а условий отдельно проследить
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user