aboutsummaryrefslogtreecommitdiff
path: root/docs/appendices
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 03:11:35 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 03:11:35 +0300
commit31fd9d2ea9b7fb01a952eef38a2af31eceebbe86 (patch)
tree8ad10aa8b2ab02ed298c9b62b050c5fa73d7d382 /docs/appendices
parentdc8be18458d6bd69e8b47b10bcbe7ddf4ed257f2 (diff)
downloadfparkan-31fd9d2ea9b7fb01a952eef38a2af31eceebbe86.tar.xz
fparkan-31fd9d2ea9b7fb01a952eef38a2af31eceebbe86.zip
fix(runtime): execute the first supported Init event
Diffstat (limited to 'docs/appendices')
-rw-r--r--docs/appendices/knowledge-boundaries.md52
-rw-r--r--docs/appendices/script-vm.md187
2 files changed, 185 insertions, 54 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index af89d49..a45d752 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -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 и проверки режима/состояния. Обработчик выбора миссии
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index 0bffbe5..d12363a 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -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
-
-`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.
-
-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.
-
-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.
-
-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`.
-
-`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.
-
-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.
+### Первый фрагмент dispatcher-а: `Init` и Handler(19)
+
+Этот контракт относится к проверенной сборке GOG: `ai.dll`, SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`, PE32 с
+image base `0x10000000`. В ней `FUN_10011f40` (RVA `0x11f40`) ищет имя
+события как C-строку: сравнивает байты до первого NUL, с учётом регистра, и
+возвращает индекс первого совпадения. Поле `name_len` при поиске не используется.
+Если совпадения нет, поиск возвращает `-1`; `FUN_10011f10` (RVA `0x11f10`) не
+вызывает обработчик для такого индекса. Поэтому повторные записи `Init` не
+исполняются все подряд: выбирается первая.
+
+`FUN_10011f10` передаёт выбранный индекс внутреннему циклу `FUN_10011fb0`
+(RVA `0x11fb0`). Цикл начинает с первой вложенной инструкции, сбрасывает индекс
+инструкции и глубину стека условий, затем идёт по записям в порядке файла.
+В исходном `default.scr` событие `Init` содержит одну инструкцию selector `19`,
+control word `0xffffffff` (действие) и result reference `0xffffffff` (результат
+не сохраняется). Это наблюдаемая форма пакета; поведение оригинала для остальных
+форм здесь не утверждается.
+
+В 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.
+
+Входные координаты идут через два 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`.
+
+`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`.
+
+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)