From a9f84d4be0af7ba69a647fbae3f97ad49255056c Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 31 Oct 2026 12:56:30 +0400 Subject: feat(script): evaluate additive FML expressions during Init --- docs/appendices/script-vm.md | 97 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 21 deletions(-) (limited to 'docs/appendices/script-vm.md') diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 6e48445..51dd429 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -22,8 +22,8 @@ symbol/formula oracle; `varset.var` задаёт `VAR(...)`/`STRING(...)` defaul `fparkan-script::parse_varset` уже читает подтверждённые numeric `VAR(float|DWORD, name, default)` declarations byte-safe (comments остаются opaque, поэтому legacy non-UTF-8 text не ломает загрузку); `STRING(...)` и -general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые -строки сохраняются для доказанных literal assignments первого `Init`; +general `FUNCTION(...)` evaluation остаются вне восстановленного контракта, +но принятые строки сохраняются для ограниченного поднабора формул первого `Init`; GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration: 31 `float` и 200 `DWORD` (от `f0` до `fY`); loader `ai.dll!0x10001000` сначала открывает `.var` и только при @@ -424,10 +424,13 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие `Init` и проходит его инструкции по порядку. Он возвращает записи для -поддержанного `Handler(19)` и числовых literal assignments; прочая форма -останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи -к отдельной копии varset выбранного клана лишь после успешного прохода всего -события. Отсутствующий `Init` даёт +поддержанного `Handler(19)` и формул с одним числовым или переменным атомом либо +с двумя такими атомами, соединёнными одним бинарным `+` или `-`; прочая форма +останавливает preview на `Unsupported`. Runtime начинает вычисление с копии +defaults и записывает в неё каждый успешно разрешённый шаг, поэтому поздняя +формула видит прежние записи этого события. Упорядоченные записи попадают в +состояние клана только после успешного прохода всего `Init`. Отсутствующий +`Init` даёт статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт `Executed` без записей. Если форма инструкции пока не поддержана, package остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения @@ -437,7 +440,7 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение оригинального dispatcher-а. -#### Числовые литералы `.fml` в `Init` +#### Литералы и простые формулы `.fml` в `Init` В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`) загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`. @@ -455,16 +458,35 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D остаётся ошибкой VFS. Native selector `-1` при source `-1` и нулевой глубине условий выбирает -formula assignment; в native record `header_words[1]` — индекс целевой varset -ячейки, а `header_words[6]` — индекс formula row. `FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки, -`FUN_10015b30` (RVA `0x15b30`) возвращает результат единственного literal node -kind `3`, а `FUN_10013650` передаёт его типизированной ячейке через -`FUN_10012c00` (RVA `0x12c00`). Компилятор `FUN_100152a0` получает числовой -prefix как `double` через `FUN_10020af0` (RVA `0x20af0`), затем записывает -его как `float` в node `+0x10`. Rust ограничивает эту ветвь одним -digit-starting числовым токеном с необязательным хвостом пробельных байтов; -ведущие unary `+`/`-`, variable references, операторы и другие node kinds -остаются явными `UnsupportedExpression`. +formula assignment. В дисковом заголовке `.scr` `header_words[1]` — индекс +целевой varset-ячейки, а `header_words[6]` — индекс formula row; соответствующие +поля native record находятся по смещениям `+0x08` и `+0x1c` соответственно. +`FUN_100123b0` (RVA `0x123b0`) проверяет индекс строки. Компилятор +`FUN_100152a0` (RVA `0x152a0`) создаёт +числовые узлы, узлы переменных и узлы операторов; числовой prefix проходит через +`FUN_10020af0` (RVA `0x20af0`) как `double`, после чего сохраняется в узле как +`float`. Для переменной компилятор вызывает vtable slot `+0x10` таблицы varset; +в проверенной GOG-сборке `ai.dll` этот вызов ведёт к поиску по RVA `0x16ad0`. +Поиск сравнивает байты имени с учётом регистра и возвращает первое совпадение. +`FUN_10015b30` (RVA `0x15b30`) читает числовой узел как `f32`, а переменную — +через `FUN_10013190` (RVA `0x13190`): type `3` даёт `float`, type `5` — +беззнаковый `DWORD`. Getter возвращает число через x87; evaluator сохраняет +его как `f32` перед операцией. Оператор `+` складывает правый +и левый операнды, а `-` вычисляет левый минус правый; после узла результат +сохраняется как `f32`. `FUN_10013650` передаёт итог типизированной ячейке через +`FUN_10012c00` (RVA `0x12c00`). + +Rust в первом `Init` принимает один числовой токен или имя ASCII-переменной, +а также ровно два таких атома с одним бинарным `+` либо `-`. Например, +`source + 2.0` и `target - source` читают значения из defaults или из предыдущих +успешных записей события. Имя ищется раньше числового преобразования, поэтому +объявление с именем `1e-2` перекрывает такую числовую запись; сравнение имён +чувствительно к регистру, а повтор объявлений выбирает первое. `DWORD` читается +как беззнаковое число и округляется до `f32` до операции. Публичный +`FormulaTable::evaluate_numeric_literal` по-прежнему принимает только литералы. +Ведущие unary `+`/`-`, `*`, другие операторы, третий атом, отсутствующие или +не-ASCII имена и зарезервированные одиночные операторы остаются `Unsupported`; +так же не поддержана ветвь копирования `source != 0xffffffff`. Проверенный native tokenizer отличается от обычного строгого decimal parser: `1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node @@ -477,8 +499,8 @@ digit-starting числовым токеном с необязательным исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`) с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся `0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw -DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого -`Init` применяется только после успешного прохода. +DWORD `100`. Неизвестные выражения не заменяются нулём, а записи первого `Init` +публикуются только после успешного прохода. Native x86 oracle на GOG `ai.dll` SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил @@ -500,6 +522,27 @@ Native x86 oracle на GOG `ai.dll` SHA-256 | 7 | `dMaxPlant` (`DWORD`) | `400 → 400` | | 8 | `d7` (`DWORD`) | `2 → 2` | +Отдельный одноразовый x86 oracle на той же GOG `ai.dll` проверил ещё 13 +выражений: 9 строк контролируемой `controlled.fml`, строки 19 и 22 настоящей +`c1m3e.fml` и два последовательных вызова строки 0 настоящей `c1m4e.fml`. +Native harness записывал `f32`-результат evaluator-а, типизированную raw-запись +и getter после записи. В scratch-проверке те же входы прошли через публичные +`parse_varset`, `FormulaTable::parse` и `VarSet::resolve_init`; результат +evaluator-а и raw-слово Rust совпали с native во всех 13 случаях. Среди +проверенных границ: `2 - 3` записал в `DWORD` `0xffffffff`; чтение исходного +`DWORD 0xffffffff` дало `f32 0x4f800000`, а последующее `+ 0` записало в +`DWORD` ноль. Для настоящих строк `c1m3e` получены `fTemp = 0x40333333` и +`dTemp3 = 0`; два последовательных вызова строки 0 `c1m4e` дали +`fTemp = 0x4033335d`, затем `0x40333387`. + +Oracle принудительно устанавливал x87 control word `0x007f` на время каждого +проверяемого вызова: все 87 наблюдений показали входное и восстановленное +значение `0x027f`, активное и выходное — `0x007f`; canary-проверки прошли. +Менялись только varset-копии в памяти процесса. Эта выборка подтверждает +компиляцию, вычисление и типизированные записи ровно для перечисленных входов. +Она не запускала исходный поиск и обход события `Init` и не исполняла поздние +события или миссию; выборка не доказывает полную грамматику evaluator-а. + Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую ссылается поддержанная запись. Лицензированный runtime-тест подтверждает полную загрузку кампании Mission.01 после этих присваиваний; у обычной @@ -511,6 +554,17 @@ Rust сохраняет всю таблицу строк, но вычисляе не перечисляют AI candidates и не выполняют этот handler. Это не подтверждение запуска самой игры или полной VM. +В других строках тех же ресурсов встречаются `0.8 + fT` (`c1m3e.fml`, row 19), +`dBuildingProtectHits - dT2` (row 22) и `fTemp + 0.00001` (`c1m4e.fml`, row 0). +Декодированные `c1m3e.scr` и `c1m4e.scr` ссылаются на них из более поздних +событий `Fort_Captured`, `PBM_BUILDING_PROTECT_Start` и +`PBM_BUILDING_INF_CAPTURE_Start`, а не из их `Init`. Игнорируемый Rust-тест +подаёт эти сохранённые строки в изменённый синтетический `Init` и использует +настоящий общий `varset.var`; это проверка resolver-а, а не утверждение о +исполнении таких выражений в исходном `Init`. Отдельный native oracle и +сравнение с Rust выше проверяют эти строки при явно заданных исходных ячейках, +но не подтверждают их исполнение в оригинальном сценарии или в живой игре. + ### Первый вызов Behavior и сценарного `Init` В той же GOG-сборке `Behavior.dll` (SHA-256 @@ -760,8 +814,9 @@ tie-векторов подтверждает реакцию `FISTP`, но не два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из -одного numeric literal; остальные формы инструкции и содержимое `.scr` -сохраняются reader-ом, но остаются вне этого исполнения. +одного числового или переменного атома либо двух таких атомов с одним бинарным +`+` или `-`. Остальные формы инструкции и содержимое `.scr` сохраняются +reader-ом, но остаются вне этого исполнения. ### Начальная группировка подготовленных Control-дескрипторов -- cgit v1.2.3