aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-31 11:56:30 +0300
committerValentin Popov <valentin@popov.link>2026-10-31 11:56:30 +0300
commita9f84d4be0af7ba69a647fbae3f97ad49255056c (patch)
tree30b93c50bb12b88e204a491b23da70ab05729d9e /docs
parent2a3b826c263683a391e8eb7993b255666e518427 (diff)
downloadfparkan-a9f84d4be0af7ba69a647fbae3f97ad49255056c.tar.xz
fparkan-a9f84d4be0af7ba69a647fbae3f97ad49255056c.zip
feat(script): evaluate additive FML expressions during Init
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md71
-rw-r--r--docs/appendices/script-vm.md97
-rw-r--r--docs/tomes/06-behavior.md26
3 files changed, 147 insertions, 47 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index ab6c1f8..7fb6798 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -133,7 +133,7 @@ control word процесса.
Лицензированный AutoDemo по-прежнему загружается с raw TMA anchor-ами
`[(500.2857, 751.8217), (728.0, 449.0)]` и записями `[(500, 752, 0),
(728, 449, 1)]`. Rust теперь также читает optional `.fml` sibling для каждого
-script base и поддерживает numeric literal assignments в первом `Init`.
+script base и поддерживает узкий набор атомарных формул в первом `Init`.
Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки
чтения распространяются как VFS errors.
@@ -144,9 +144,11 @@ script base и поддерживает numeric literal assignments в первÐ
два доказанных действия: `Handler(19)` и formula assignment selector `-1` при
source `-1`, нулевой глубине условий и корректных индексах. У `Handler(19)`
control находится в `header_words[3]`, а `header_words[1]` — result-save
-reference; оба равны `0xffffffff` в поддержанной форме. У formula assignment
-`header_words[1]` — индекс целевой varset ячейки, `header_words[2]` — source,
-а индекс formula row занимает `header_words[6]`. Маппинг подтверждён
+reference; оба равны `0xffffffff` в поддержанной форме. В дисковом заголовке
+formula assignment `header_words[1]` — индекс целевой varset ячейки,
+`header_words[2]` — source, а индекс formula row занимает `header_words[6]`;
+индексы цели и формулы в native record находятся по смещениям `+0x08` и `+0x1c`
+соответственно. Маппинг подтверждён
`ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA
`0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
`Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`;
@@ -154,23 +156,58 @@ reference; оба равны `0xffffffff` в поддержанной форме
Formula table загружается из `<bundle-base>.fml`; native `FUN_100123b0`
(RVA `0x123b0`) проверяет ordinal, compiler `FUN_100152a0` (RVA `0x152a0`)
-строит numeric literal node, а evaluator `FUN_10015b30` (RVA `0x15b30`)
-возвращает его raw `f32`. Target type `3` сохраняет эти bits; type `5` проходит
-через `FUN_10012c00` (RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD
-с усечением к нулю. Изолированный x86 oracle на оригинальном `ai.dll` и
-настоящих ресурсах подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
+строит числовые, переменные и операторные узлы, а evaluator `FUN_10015b30`
+(RVA `0x15b30`) в проверенных путях `+` и `-` считывает значения переменных и
+сохраняет результаты как `f32` при x87 control word `0x007f`. В GOG-сборке
+`ai.dll` с SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` поиск
+переменной через varset vtable slot `+0x10` ведёт на RVA `0x16ad0`: сравнение
+байтов точное, регистр учитывается, при повторах берётся первое объявление.
+Getter `FUN_10013190` (RVA `0x13190`) читает type `3` как `f32`, type `5` как
+беззнаковый `DWORD` и возвращает число через x87. Затем evaluator сохраняет
+его как `f32` до операции. Оператор `+`
+считает right+left, `-` — left-right; результат также сохраняется как `f32`.
+Target type `3` сохраняет эти bits; type `5` проходит через `FUN_10012c00`
+(RVA `0x12c00`) и `__ftol` (RVA `0x1df70`) к raw DWORD с усечением к нулю.
+Изолированный x86 oracle на оригинальном `ai.dll` и настоящих ресурсах
+подтвердил таблицы `tut1_pl2`/`scr_pl_1`/`scream` с
26/5/149 строками и 15 присваиваний: кампанийные `df0..df3` и `Single.01`
`dT8/dT9` получают `0`; `scream` задаёт `fDifficulty = 0.8f` и остальные
проверенные DWORD `0, 0, 1, 10, 0, 100, 400, 2`.
-Граница Rust остаётся узкой: ссылка должна вести на один digit-starting numeric
-token и типизированную `float`/`DWORD` ячейку. Неизвестные expression nodes,
-переменные, unary operators, составные выражения и source-copy ветвь
-`source != 0xffffffff` остаются явными ошибками; native source-copy переносит
-всю varset record, не только числовое значение. В preview package сохраняется,
-но `Unsupported` не применяет частичные writes; полная загрузка отклоняет весь
-новый мир и сохраняет предыдущие world/RNG. Это политика Rust, не правило
-оригинального dispatcher-а.
+Граница Rust остаётся узкой: один числовой токен либо ASCII-имя переменной,
+или два таких атома с одним бинарным `+`/`-`. Имя проверяется раньше числового
+преобразования, сравнивается с учётом регистра и выбирает первое совпадение;
+публичный `FormulaTable::evaluate_numeric_literal` принимает только числовые
+литералы. Временная копия значений начинается со значений по умолчанию и
+обновляется после каждого успешного действия `Init`, поэтому следующая формула
+видит предыдущие записи. Нет
+поддержки неизвестных операторов, унарных форм, третьего атома, отсутствующих
+или не-ASCII имён и source-copy ветви `source != 0xffffffff`; native source-copy
+переносит всю запись varset. Preview сохраняет package, но не применяет частичные
+writes; полный load отклоняет новый мир и сохраняет прежние world/RNG. Это
+политика ограниченного Rust resolver-а, а не правило оригинального dispatcher-а.
+
+Ресурсные строки с переменными найдены в поздних событиях: `c1m3e.fml` row 19
+(`0.8 + fT`) вызывается из `Fort_Captured`, row 22
+(`dBuildingProtectHits - dT2`) — из `PBM_BUILDING_PROTECT_Start`, а `c1m4e.fml`
+row 0 (`fTemp + 0.00001`) — из `PBM_BUILDING_INF_CAPTURE_Start`. Декодирование
+этих `.scr` не показывает эти формулы в первом `Init`. Игнорируемая проверка
+читает настоящие `.fml` и `varset.var`, но вставляет строки в синтетический
+`Init`; она проверяет resolver, а не оригинальное исполнение. Одноразовый x86
+oracle на исходной GOG `ai.dll` с SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` проверил
+13 отдельных присваиваний: 9 контролируемых выражений и 4 вызова с настоящими
+строками `.fml` (`c1m3e` rows 19/22, `c1m4e` row 0 дважды подряд). В каждом векторе
+native harness записывал вычисленный `f32`, типизированное raw-слово и getter;
+scratch-проход через публичный Rust parser/resolver совпал со всеми 13
+векторами по `f32`-результату и raw-записи. Для native вызовов control word
+временно принудительно устанавливался в `0x007f`: 87 наблюдений сохранили
+`0x027f` на входе и после восстановления,
+а canary-проверки прошли. Менялись только varset-копии в памяти. Это
+подтверждает компиляцию формулы, локальное вычисление и typed writes для этих
+входов, но не запуск исходного поиска и обхода события `Init`, исполнение поздних
+событий или поведение живой миссии.
Лицензированный test загружает одну первую кампанийную миссию
`CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход
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` сначала открывает `<bundle-base>.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-дескрипторов
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index 1a2cedf..3a4e51c 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -169,8 +169,10 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga
показывает предупреждение для чисел выше `10000`, но не отменяет создание AI.
Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)`
-и числовые literal assignments, описанные ниже. Поддержанные записи применяются
-к состоянию лишь после успешного прохода всего первого `Init`.
+и формулы из одного атома либо двух атомов с бинарным `+` или `-`. Здесь атом —
+число или имя переменной. Поддержанные шаги сразу обновляют временную копию
+varset, поэтому следующая формула читает уже записанные значения. К состоянию
+клана записи попадают лишь после успешного прохода всего первого `Init`.
Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает
`Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма
становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило
@@ -186,12 +188,18 @@ Rust исполняет подтверждённый поднабор перво
нули в `dT8` и `dT9`.
Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь
проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8`
-сохраняется как число `float`. Пока Rust вычисляет только числовой литерал,
-начинающийся с цифры; операторы, ссылки на переменные и другие формы формул
-остаются явными ошибками. В тесте полной загрузки миссия `Mission.01`
-проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном
-`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо
-этого действия.
+сохраняется как число `float`. Например, если `source` равен `10`, выражение
+`source + 2.0` записывает `12`; следующее `target - source` читает новое значение
+`target` и получает `2`. Каждая операция работает с `f32`: значение ячейки
+`DWORD` сначала читается как беззнаковое число и переводится в `f32`, а результат
+сохраняет тип целевой ячейки. Поддерживаются только один или два атома и один
+бинарный `+` либо `-`; произвольная грамматика формул и остальная VM не
+восстановлены. В отдельной точечной проверке вычисления и записи оригинальной
+DLL и Rust совпали для 13 выбранных формул. Эта сверка проверяет только
+эти значения и не запускает исходную миссию. Тест полной загрузки подтверждает,
+что `Mission.01` проходит этот `Init`; у `Single.01` следующий неподдержанный
+`Handler(43)` встречается в clan 1, записи 10. Никакое значение не подставляется
+вместо этого действия.
### Первый запуск сценария
@@ -246,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
значение времени очереди; после регистрации объектов загрузчик последовательно
подготавливает каждый сценарный пакет и выполняет восстановленные действия
-`Handler(19)` и числовые literal assignments из первого `Init`. Статический
+`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический
просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
varset.