aboutsummaryrefslogtreecommitdiff
path: root/docs/appendices
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 02:20:20 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 02:20:20 +0300
commitdc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2 (patch)
tree998ad76276ac1a942ccd784bdd9ea2203d40b8a1 /docs/appendices
parent65f1298d936eb8d8cd2ea9c5578c17d6ec75ba36 (diff)
downloadfparkan-dc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2.tar.xz
fparkan-dc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2.zip
fix(runtime): preserve native mission outcome guards
Diffstat (limited to 'docs/appendices')
-rw-r--r--docs/appendices/knowledge-boundaries.md12
-rw-r--r--docs/appendices/script-vm.md81
2 files changed, 75 insertions, 18 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index ad7bc13..af89d49 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -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-а условий отдельно проследить
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index a2a2a3d..0bffbe5 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -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