fix(runtime): execute the first supported Init event
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s

This commit is contained in:
Valentin Popov committed 2026-10-20 04:11:35 +04:00
1 parent dc8be18458
commit 31fd9d2ea9
8 files changed
+1448 -106

No files matched your search

+52
View File
@@ -44,6 +44,58 @@ state ещё не восстановлены.
вывод по одному известному `ai.dll`, а не утверждение о совпадении во время
исполнения.
Для начального сценарного события закрыт более узкий путь. В проверенной GOG
сборке `ai.dll` SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` функции
`FUN_10011f40` (RVA `0x11f40`) и `FUN_10011f10` (RVA `0x11f10`) выбирают первое
имя-событие C-string `Init` и запускают его инструкции по порядку. Для координат
из TMA найден и проверен ещё один участок: `iron3d.dll` SHA-256
`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`,
`FUN_100392e0` (RVA `0x392e0`) выполняет x87 `FISTP qword` и передаёт младшие
DWORD координат пятым и шестым аргументами `CreateSuperAI`. Затем
`ai.dll!FUN_10005c70` (RVA `0x5c70`) преобразует DWORD в `float`, а `Handler(19)`
возвращает его к DWORD усечением.
Две x86-проверки вызвали эти оригинальные DLL-функции на изолированной
синтетической памяти. Caller oracle подтвердил AutoDemo-координаты
`(500.2857, 751.8217) -> (500, 752)`, ties-to-even в nearest-режиме и переход
`-0.6 -> 0xffffffff`. AI oracle проверил оба набора AutoDemo anchor-ов,
дублирующий `Init`, имя `Init\\0suffix\\0`, отсутствие события, а также
нативную связку `FUN_10005c70` → `Handler(19)`: DWORD `(0xffffffff, 10001)` стал
полями `(2^32, 10001.0)` и varset-записями `(0, 10001, 2)`. Эти проверки
подтверждают только изолированные участки ABI.
В одном read-only snapshot запущенной GOG AutoDemo были связаны записи кланов
Iron3D и указатели `GetSuperAI` из `ai.dll` (таблица RVA `0x55398`). Клан 0 имел
исходные float-координаты `(500.2857, 751.8217)` и поля AI
`(+0x80, +0x84, +0x7c) = (500.0, 752.0, 0)`; клан 1 имел `(728.0, 449.0)` и
`(728.0, 449.0, 1)`. Первый набор связывает те же значения в одном процессе и
выбирает ближайшее округление среди направлений округления x87. Синтетические
tie-векторы проверяют, как сам `FISTP` реагирует на каждый RC, но не измеряют
control word процесса.
Отдельно проверены загрузка лицензированного AutoDemo в Rust и preview
лицензированной кампании. AutoDemo даёт raw TMA anchor-ы
`[(500.2857, 751.8217), (728.0, 449.0)]` и записи `[(500, 752, 0),
(728, 449, 1)]`; preview кампании сохраняет package и статус `Unsupported`,
не применяя частичные записи. Это подтверждает загрузчик и узкий Init resolver
Rust, но не доказывает, что штатный host вызывает `Init` при старте оригинальной
миссии.
Нативный конструктор показывает предупреждение при координате выше `10000`, но
продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение;
на первом `FISTP qword` он явно завершает загрузку ошибкой для NaN, infinity и
значений вне signed-qword диапазона. В первом событии `Init` Rust поддерживает
только action-форму `Handler(19)` (`control word` и `result reference` равны
`0xffffffff`). Неизвестная инструкция даёт `Unsupported` в preview, не применяя
накопленные частичные записи; при полной загрузке это ошибка. Это ограничение
Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные ссылки и
типы остаются ошибками в обоих режимах.
Открыты вызов `Init` при штатном старте Iron3D и проверка другой миссии с
непустым `Init`. Не восстановлены host-ingress `Init` в игровом цикле и общий
dispatcher остальных событий.
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
начальные поля IGame и проверки режима/состояния. Обработчик выбора миссии
+127 -48
View File
@@ -268,8 +268,9 @@ scheduler path внутри запущенной игры.
На границе mission runtime выбранный TMA clan `first_resource`
материализуется как отдельный `MissionScriptBundle`: loader нормализует
`<base>.scr`, декодирует его тем же bounded reader-ом и публикует immutable
package вместе с clan provenance. Это именно wiring входных данных, не VM
execution: Init и остальные events пока не dispatch-ятся, а ошибка чтения сохраняет
package вместе с clan provenance. Для первого события `Init` runtime исполняет
только подтверждённый фрагмент dispatcher-а с `Handler(19)`; остальные события
пока остаются только прочитанными данными. Ошибка чтения по-прежнему сохраняет
transactional rollback mission loader-а.
TMA properties остаются four raw `u32` words плюс имя, пока consumer/schema не
@@ -283,58 +284,136 @@ layout prerequisites/modifiers/unlocks. Formula evaluator требует strict
grammar/version, typed operands, deterministic numeric policy, bounded stack и
явных errors; x87-compatible rounding нужен там, где оно выбирает ветку.
### Handler(19): AutoDemo Init varset initialization
### Первый фрагмент dispatcher-а: `Init` и Handler(19)
`Handler(19)` is the twentieth VM-table entry at GOG `ai.dll` VA `0x1000aa38`.
It is the only instruction in the `Init` event of the two `default.scr` bundles
referenced by `MISSIONS\\Autodemo.00\\data.tma`; together those bundles account for
the observed 18 named script events. Each instruction references varset records
`224`, `225`, and `226`: `ClanBaseX`, `ClanBaseY`, and `ClanID` respectively.
Этот контракт относится к проверенной сборке GOG: `ai.dll`, SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`, PE32 с
image base `0x10000000`. В ней `FUN_10011f40` (RVA `0x11f40`) ищет имя
события как C-строку: сравнивает байты до первого NUL, с учётом регистра, и
возвращает индекс первого совпадения. Поле `name_len` при поиске не используется.
Если совпадения нет, поиск возвращает `-1`; `FUN_10011f10` (RVA `0x11f10`) не
вызывает обработчик для такого индекса. Поэтому повторные записи `Init` не
исполняются все подряд: выбирается первая.
The original writes three raw DWORD values in order. It converts the VM fields
at `+0x80` and `+0x84` through the x87 `__ftol` helper and stores the resulting
words into references 0 and 1. It copies the raw word from `+0x7c` into reference
2, then clears VM field `+0x50`. The default targets are `DWORD` records; the
shared setter preserves the incoming word for that type. Therefore this is not
a license to replace the first two conversions with Rust float casts: their
rounding behavior remains an x87 compatibility boundary until it has captured
test vectors.
`FUN_10011f10` передаёт выбранный индекс внутреннему циклу `FUN_10011fb0`
(RVA `0x11fb0`). Цикл начинает с первой вложенной инструкции, сбрасывает индекс
инструкции и глубину стека условий, затем идёт по записям в порядке файла.
В исходном `default.scr` событие `Init` содержит одну инструкцию selector `19`,
control word `0xffffffff` (действие) и result reference `0xffffffff` (результат
не сохраняется). Это наблюдаемая форма пакета; поведение оригинала для остальных
форм здесь не утверждается.
The missing source-field provenance is now constrained by the public creation
boundary. `CreateSuperAI` at `ai.dll` VA `0x1000f710` allocates `0x8b0` bytes
and calls constructor `0x10001000` with its first eight arguments. That
constructor calls `0x10006340(this + 0x7c, clan_id, base_x, base_y)`: these are
the fields later read by `Handler(19)`. `base_x` and `base_y` are unsigned and
the constructor rejects values greater than `10000`. The actual `__ftol` helper
at `0x1001df70` saves the x87 control word, sets its rounding-control bits to
truncate, executes `fistp qword`, then restores the control word.
В GOG `default.scr` события `Init` в двух AutoDemo bundles имеют по одной такой
инструкции. Её ссылки `224`, `225`, `226` ведут к объявлениям `DWORD`
`ClanBaseX`, `ClanBaseY` и `ClanID`. `Handler(19)` (двадцатая запись таблицы,
VA `0x1000aa38`, RVA `0xaa38`) пишет их по порядку: поля SuperAI `+0x80` и
`+0x84` переводит через x87 helper `__ftol`, а поле `+0x7c` копирует как
неизменённое слово. После вызова он очищает поле SuperAI `+0x50`; соответствующее
смещение в VM, начинающейся с SuperAI `+8`, равно `+0x48`. Поскольку цели имеют
тип `DWORD`, общий setter сохраняет полученные слова без дополнительного
преобразования. Helper `__ftol` в `ai.dll` (VA `0x1001df70`, RVA `0x1df70`)
сохраняет x87 control word, временно задаёт округление к нулю, выполняет
`FISTP qword` и восстанавливает control word.
Consequently the runtime resolves and retains this one proved vertical slice
during mission loading: each selected clan's TMA anchor is accepted only in the recovered
`0..=10000` base range, truncated through the recovered x87 rule, and paired
with its zero-based clan index. For every `Init` instruction whose selector is
`Handler(19)`, `VarSet::resolve_handler19` produces the three per-clan DWORD
writes. The runtime then materializes an independent declaration-ordered value
array for each selected clan and applies those writes, so later recovered
handlers can consume `ClanBaseX`, `ClanBaseY`, and `ClanID` as runtime cells
rather than loader defaults. Other Init selectors and all other events remain
decoded but unexecuted. AutoDemo validates the path end-to-end: its non-integral first
anchor (`500.2857`) yields captured `ClanBaseX=500`, and the live GOG process
contains two initialized SuperAI entries `(500, 752, 0)` and `(728, 449, 1)`;
the Rust loader reports `script_init_states=2` and `script_varset_states=2`.
Входные координаты идут через два DLL. В проверенной сборке `iron3d.dll`
(SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`)
функция `FUN_100392e0` преобразует две координаты TMA из `float` в signed
64-bit через x87 `FISTP qword` (RVA `0x39307` и `0x3932f`), а пятый и шестой
аргументы `CreateSuperAI` получает как младшие DWORD этих результатов. Десятый
аргумент — указатель callback. В прямом вызове оригинала пара
`(500.2857, 751.8217)` дала `(500, 752)`, а tie-векторы `(500.5, 501.5)` при
ближайшем режиме дали `(500, 502)`. Например, `-0.6` сначала округляется до
`-1`, а его младший DWORD равен `0xffffffff`.
`GetSuperAI` returns element `n` of the 64-pointer global table at preferred
`ai.dll + 0x55398` for `n <= 63`. A read-only capture of the running GOG
AutoDemo process observed `(500, 752, 0)` for entry 0 and `(728, 449, 1)` for
entry 1 at fields
`(+0x80, +0x84, +0x7c)`. These values are integral samples, not a rounding
profile.
`CreateSuperAI` в `ai.dll` (VA `0x1000f710`, RVA `0xf710`) выделяет `0x8b0`
байт и вызывает конструктор `FUN_10001000`. Тот передаёт `clan_id`, `base_x` и
`base_y` в `FUN_10006340`, которая заполняет поля позднее читаемые
`Handler(19)`. Значения больше `10000` вызывают исходное предупреждение через
`MessageBoxA`, после которого конструктор продолжает работу; это не отказ и не
ограничение координат. `FUN_10005c70` (RVA `0x5c70`) затем расширяет DWORD до
qword, загружает его через x87 и записывает `float` в поля структуры `+4` и
`+8` — в этой цепочке это SuperAI `+0x80` и `+0x84`. Поэтому `0xffffffff`
превращается в `2^32` при округлении до single precision.
Сам `Handler(19)` применяет `__ftol` с усечением к этим полям и берёт младший
DWORD: для `2^32` получается `0`, а для `10001.0` — `10001`.
The Rust reader exposes `VarSet::resolve_handler19`. It accepts the already
converted first two words and the third raw word, produces three typed writes,
and rejects missing, out-of-range, or non-`DWORD` targets. The runtime only
binds it to the proven creation/anchor path above; it does not guess the
remaining script event semantics.
Rust повторяет весь числовой путь до DWORD, записываемого в `VarSet`:
nearest-even `FISTP qword`, младший DWORD, преобразование DWORD в `f32`, затем
усечение результата до qword и его младший DWORD. В частности, это сохраняет
наблюдаемый переход `-0.6 -> 0xffffffff -> 2^32 -> 0` и не отвергает `10001`.
Координата, не конечная или не представимая в signed qword на первом шаге,
остаётся явной ошибкой: поведение такой области здесь не моделируется. Rust не
показывает исходное предупреждение `MessageBoxA`; оно находится за пределами
этого числового resolver-а.
Ограниченный Rust-исполнитель `VarSet::resolve_handler19_init` ищет только
первое событие `Init`, проходит его инструкции по порядку и возвращает
упорядоченные записи. Runtime применяет их к отдельной копии varset выбранного
клана лишь после успешного прохода всего события. Отсутствующий `Init` даёт
статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт
`Executed` без записей. Если форма инструкции пока не поддержана, package
остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения
ячеек сохраняются; при полной загрузке миссии такая же форма возвращает ошибку с
индексами события и инструкции. Предшествующие частичные записи не применяются.
Повреждённая ссылка или цель неправильного типа остаётся ошибкой в обоих режимах.
Это правила ограниченного 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`:
```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_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 }
& .\target\handler19-native-oracles\AnchorOracle.exe
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& .\target\handler19-native-oracles\InitOracle.exe
exit $LASTEXITCODE
```
Caller oracle проверяет AutoDemo пару `(500.2857, 751.8217)`, четыре режима
x87 на tie-векторах, границу `10000` и младший DWORD. AI oracle отдельно
проверяет, что `FUN_10005c70(0xffffffff, 10001)` даёт поля `(2^32, 10001.0)`,
а обработчик записывает `(0, 10001, 2)`.
`GetSuperAI` возвращает элемент `n` из таблицы на 64 указателя по RVA `0x55398`
в `ai.dll`, если `n <= 63`. В режиме только чтения у запущенной GOG AutoDemo
одновременно сняты исходные координаты кланов в `iron3d.dll` и поля созданных
SuperAI в этой таблице. Для первого клана raw TMA-слова `0x43fa2492` и
`0x443bf497` декодируются как `(500.2857, 751.8217)`, а соответствующие поля
SuperAI `+0x80`, `+0x84`, `+0x7c` содержали `(500.0, 752.0, 0)`. Для второго
клана сырые координаты и поля совпали: `(728.0, 449.0, 1)`. Связанный снимок
исходных и конечных значений исключает усечение, направление вверх и направление
вниз в реально запущенной игре. Отдельный caller oracle вызывает оригинальный
`FUN_100392e0` при явно заданных x87 RC-режимах, включая nearest; сравнение
tie-векторов подтверждает реакцию `FISTP`, но не измеряет control word процесса.
Профиль живой игры выводится из связанной пары raw X/Y → SuperAI.
Нижнеуровневый `VarSet::resolve_handler19` принимает уже преобразованные первые
два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет
отсутствующие, выходящие за диапазон или не-`DWORD` цели. Метод
`resolve_handler19_init` добавляет выбор события и остановку на неподдержанной
форме инструкции. Всё остальное содержимое `.scr` сохраняется reader-ом, но
остаётся вне этого исполнения.
### Аргументы и обратный вызов Handler(30)