aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-31 02:00:05 +0300
committerValentin Popov <valentin@popov.link>2026-10-31 02:00:05 +0300
commitdbb59b00d13d18a8c94b2bdacd6c14b4dc20df29 (patch)
tree629fdeb957beed3f41f4c8700a5bf3df40fe1bcc /docs
parent7f8a37d22abbedf487666132a4857e13c213c944 (diff)
downloadfparkan-dbb59b00d13d18a8c94b2bdacd6c14b4dc20df29.tar.xz
fparkan-dbb59b00d13d18a8c94b2bdacd6c14b4dc20df29.zip
feat(script): execute numeric FML assignments during mission Init
Diffstat (limited to 'docs')
-rw-r--r--docs/appendices/knowledge-boundaries.md59
-rw-r--r--docs/appendices/script-vm.md104
-rw-r--r--docs/tomes/06-behavior.md27
3 files changed, 155 insertions, 35 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md
index ee40121..a64b68e 100644
--- a/docs/appendices/knowledge-boundaries.md
+++ b/docs/appendices/knowledge-boundaries.md
@@ -130,27 +130,54 @@ Iron3D и указатели `GetSuperAI` из `ai.dll` (таблица RVA `0x5
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.
+Лицензированный 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`.
+Отсутствующий `.fml` не мешает пакету с одними `Handler(19)`; другие ошибки
+чтения распространяются как VFS errors.
Нативный конструктор показывает предупреждение при координате выше `10000`, но
продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение;
на первом `FISTP qword` он явно завершает загрузку ошибкой для NaN, infinity и
значений вне signed-qword диапазона. В первом событии `Init` Rust поддерживает
-только action-форму `Handler(19)`: в семи disk header words control находится в
-`header_words[3]`, а result-save reference — в `header_words[1]`; для поддержанной
-формы оба равны `0xffffffff`. Это подтверждается перестановкой записи в
-`ai.dll!FUN_10011b20` (RVA `0x11b20`) и чтениями в исполнителе
-`FUN_10012020` (RVA `0x12020`) той же GOG-сборки. `Handler(19)` (`FUN_1000aa38`,
-RVA `0xaa38`) использует references `0..2`; `header_words[2]` его result-save
-guard-ом не проверяется. Неизвестная инструкция даёт `Unsupported` в preview,
-не применяя накопленные частичные записи; при полной загрузке это ошибка. Это
-ограничение Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные
-ссылки и типы остаются ошибками в обоих режимах.
+два доказанных действия: `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]`. Маппинг подтверждён
+`ai.dll!FUN_10011b20` (RVA `0x11b20`) и исполнителем `FUN_10012020` (RVA
+`0x12020`), SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`.
+`Handler(19)` (`FUN_1000aa38`, RVA `0xaa38`) использует references `0..2`;
+`header_words[2]` его result-save guard-ом не проверяется.
+
+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` с
+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-а.
+
+Лицензированный test загружает одну первую кампанийную миссию
+`CAMPAIGN/CAMPAIGN.00/Mission.01/data.tma` после literal writes; это не проход
+всей цепочки кампании. Обычная `Single.01` доходит до следующего blocker-а:
+`Handler(43)` в clan 1, `Init` instruction 10. Rust не перескакивает его;
+preview оставляет varset этого клана на defaults, а полный load завершает ошибкой
+с rollback. Это ещё не загрузка/запуск игры и не общий dispatcher.
Для GOG-сборки также найден статический host call path в `Init`. В
`iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index 7ebf344..49af347 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -22,13 +22,15 @@ 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(...)` и
-`FUNCTION(...)` пока сохранены за границей этого numeric contract;
+general `FUNCTION(...)` evaluation остаётся вне numeric contract, но принятые
+строки сохраняются для доказанных literal assignments первого `Init`;
GOG `MISSIONS/SCRIPTS/varset.var` даёт через него ровно 231 declaration:
31 `float` и 200 `DWORD` (от `f0` до `fY`);
loader `ai.dll!0x10001000` сначала открывает `<bundle-base>.var` и только при
`not found` откатывается к этому shared file. Runtime повторяет данный порядок
-транзакционно и публикует selected `MissionScriptVarSet` с путём/provenance, но
-ещё не исполняет declarations как VM state;
+транзакционно, создаёт typed varset cells для кланов и применяет поддержанные
+записи первого `Init`. Это начальное состояние сценария, а не исполнение общего
+VM state;
`.trf` — NRes tables, чей framing подтверждён, а field semantics местами лишь
consumer-inferred.
@@ -285,9 +287,10 @@ TMA properties остаются four raw `u32` words плюс имя, пока c
Research/economy работают в simulation: `LoadResearch` и
`CalcFullResearchCost` доказывают данные и вычислимую стоимость, но не полный
-layout prerequisites/modifiers/unlocks. Formula evaluator требует strict
-grammar/version, typed operands, deterministic numeric policy, bounded stack и
-явных errors; x87-compatible rounding нужен там, где оно выбирает ветку.
+layout prerequisites/modifiers/unlocks. General formula evaluator требует
+versioned grammar, typed operands, deterministic numeric policy, bounded stack
+и явных errors; x87-compatible rounding нужен там, где оно выбирает ветку.
+Описанный ниже literal-путь первого `Init` не закрывает общий evaluator.
### Первый фрагмент dispatcher-а: `Init` и Handler(19)
@@ -362,10 +365,12 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
показывает исходное предупреждение `MessageBoxA`; оно находится за пределами
этого числового resolver-а.
-Ограниченный Rust-исполнитель `VarSet::resolve_handler19_init` ищет только
-первое событие `Init`, проходит его инструкции по порядку и возвращает
-упорядоченные записи. Runtime применяет их к отдельной копии varset выбранного
-клана лишь после успешного прохода всего события. Отсутствующий `Init` даёт
+Ограниченный Rust-исполнитель `VarSet::resolve_init` ищет первое событие
+`Init` и проходит его инструкции по порядку. Он возвращает записи для
+поддержанного `Handler(19)` и числовых literal assignments; прочая форма
+останавливает preview на `Unsupported`. Runtime применяет упорядоченные записи
+к отдельной копии varset выбранного клана лишь после успешного прохода всего
+события. Отсутствующий `Init` даёт
статус `Missing` и не создаёт записей инициализации; пустой первый `Init` даёт
`Executed` без записей. Если форма инструкции пока не поддержана, package
остаётся доступен в preview режиме со статусом `Unsupported`, а default-значения
@@ -375,6 +380,77 @@ nearest-even `FISTP qword`, младший DWORD, преобразование D
Это правила ограниченного Rust-исполнителя, а не обнаруженное поведение
оригинального dispatcher-а.
+#### Числовые литералы `.fml` в `Init`
+
+В той же сборке `ai.dll` (`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`)
+загрузчик `FUN_10001000` открывает `.fml` рядом с базой каждого `.scr`.
+`FUN_10018f40` читает строки в таблицу `FUNCTION`; vtable slot `+0x18`
+компилирует сохранённые строки через `FUN_100152a0` (RVA `0x152a0`).
+Индексы формул — порядковые номера принятых строк, поэтому строки после
+используемого номера тоже остаются в таблице. Native reader обрабатывает
+независимые чанки до 255 байт, включая LF; CRLF нормализуется до LF, `Ctrl-Z`
+завершает ввод, а NUL скрывает остаток своего чанка для C-string поиска.
+Искатель учитывает регистр, ищет подстроку `FUNCTION` и независимо выбирает
+первую `(` и запятые в видимом чанке. Это permissive framing, а не строгий
+разбор сбалансированного вызова. Rust сохраняет принятые записи в этом порядке
+и разбирает выражение только при ссылке из `Init`; отсутствующая соседняя `.fml`
+не мешает одному лишь `Handler(19)`, но ошибка чтения, отличная от `NotFound`,
+остаётся ошибкой 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`.
+
+Проверенный native tokenizer отличается от обычного строгого decimal parser:
+`1e-2`, `1.`, `1e`, `1e+`, `1ee2` и `1e2.3` были приняты как один literal node
+и дали payload `0x3c23d70a`, `0x3f800000`, `0x3f800000`, `0x3f800000`,
+`0x3f800000` и `0x42c80000`. Для четырёх неполных exponent-форм значение
+получено из распознаваемого числового prefix; это именно проверенные строки,
+а не полное описание всех ошибок компилятора.
+
+Тип целевой ячейки сохраняется при записи: `float` (native tag `3`) принимает
+исходные `f32` bits; `DWORD` (tag `5`) проходит через `__ftol` (RVA `0x1df70`)
+с усечением к нулю и записывает младший DWORD. Поэтому literal `0.8` остаётся
+`0x3f4ccccd` в `float`, но записывается как `0` в `DWORD`; `100.0` даёт raw
+DWORD `100`. Неизвестные выражения не заменяются нулём, а запись всего первого
+`Init` применяется только после успешного прохода.
+
+Native x86 oracle на GOG `ai.dll` SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` загрузил
+реальный shared `varset.var` (231 declaration) и реальные таблицы
+`tut1_pl2.fml` (26 строк), `scr_pl_1.fml` (5 строк) и `scream.fml` (149 строк).
+В первой кампании formula `0` записывает нули в DWORD `df0..df3` (indices
+188–191); в `Single.01` formula `0` записывает нули в `dT8` и `dT9` (indices
+186–187). Строки `scream` `0..8` задают следующие пары literal → raw cell value:
+
+| Formula | Target | Literal → raw cell value |
+| ---: | --- | --- |
+| 0 | `dMaxBuilder` (`DWORD`) | `0.4 → 0` |
+| 1 | `fDifficulty` (`float`) | `0.8 → 0x3f4ccccd` |
+| 2 | `dCurrentSender` (`DWORD`) | `0.5 → 0` |
+| 3 | `dMaxTower` (`DWORD`) | `1.0 → 1` |
+| 4 | `dMaxStorage` (`DWORD`) | `10 → 10` |
+| 5 | `dMaxTransport` (`DWORD`) | `0.2 → 0` |
+| 6 | `dMaxMine` (`DWORD`) | `100 → 100` |
+| 7 | `dMaxPlant` (`DWORD`) | `400 → 400` |
+| 8 | `d7` (`DWORD`) | `2 → 2` |
+
+Rust сохраняет всю таблицу строк, но вычисляет только формулу, на которую
+ссылается поддержанная запись. Лицензированный runtime-тест подтверждает
+полную загрузку кампании Mission.01 после этих присваиваний; у обычной
+`Single.01` clan 1 после формул остаётся `Handler(43)` в instruction 10, который
+не поддержан и не обходится. Preview оставляет значения этого клана без
+частичных записей, а полный load возвращает ошибку транзакционно. Это не
+подтверждение запуска самой игры или полной VM.
+
### Первый вызов Behavior и сценарного `Init`
В той же GOG-сборке `Behavior.dll` (SHA-256
@@ -622,10 +698,10 @@ tie-векторов подтверждает реакцию `FISTP`, но не
Нижнеуровневый `VarSet::resolve_handler19` принимает уже преобразованные первые
два слова и третье исходное слово, выдаёт три типизированные записи и отклоняет
-отсутствующие, выходящие за диапазон или не-`DWORD` цели. Метод
-`resolve_handler19_init` добавляет выбор события и остановку на неподдержанной
-форме инструкции. Всё остальное содержимое `.scr` сохраняется reader-ом, но
-остаётся вне этого исполнения.
+отсутствующие, выходящие за диапазон или не-`DWORD` цели. Общий для первого
+события метод `VarSet::resolve_init` добавляет этот `Handler(19)` и формулу из
+одного numeric literal; остальные формы инструкции и содержимое `.scr`
+сохраняются reader-ом, но остаются вне этого исполнения.
### Изменение provider-состояния Control
diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md
index 48f4405..9350d94 100644
--- a/docs/tomes/06-behavior.md
+++ b/docs/tomes/06-behavior.md
@@ -168,8 +168,9 @@ handler лишь инициирует execution context (`+0x50 = 1`); его ga
младшие 32 бита, что даёт `0`. Значение `10001` также проходит дальше: оригинал
показывает предупреждение для чисел выше `10000`, но не отменяет создание AI.
-Rust исполняет только подтверждённую форму этого действия. Поддержанные записи
-применяются к состоянию лишь после успешного прохода всего первого `Init`.
+Rust исполняет подтверждённый поднабор первого `Init`: действие `Handler(19)`
+и числовые literal assignments, описанные ниже. Поддержанные записи применяются
+к состоянию лишь после успешного прохода всего первого `Init`.
Если встретилась неподдержанная форма, preview сохраняет пакет, сообщает
`Unsupported` и оставляет значения по умолчанию; при полной загрузке та же форма
становится ошибкой. Это политика Rust для ограниченного исполнителя, а не правило
@@ -177,6 +178,21 @@ Rust исполняет только подтверждённую форму эÑ
событие считается исполненным без записей. Остальные события пока не образуют
исполняемую VM.
+В `Init` встречаются и отдельные присваивания результатов формул. Их запись
+выбирает строку формулы по порядковому номеру из соседнего `.fml`; этот файл
+относится к конкретной базе сценария, а строки после нужного номера сохраняют свои позиции.
+Для первой кампанийной миссии `Mission.01` (`tut1_pl2.fml`) строка 0 записывает
+нули в `df0..df3`; в миссии `Single.01` (`scr_pl_1.fml`) строка 0 записывает
+нули в `dT8` и `dT9`.
+Клан 1 той же миссии использует первые девять строк `scream.fml`: восемь
+проверенных значений попадают в `DWORD` с усечением дробной части, а `0.8`
+сохраняется как число `float`. Пока Rust вычисляет только числовой литерал,
+начинающийся с цифры; операторы, ссылки на переменные и другие формы формул
+остаются явными ошибками. В тесте полной загрузки миссия `Mission.01`
+проходит этот `Init`; `Single.01` останавливается на следующем неподдержанном
+`Handler(43)` в clan 1, записи 10. Никакое значение не подставляется вместо
+этого действия.
+
### Первый запуск сценария
До регулярной работы Behavior настраивает расписание своего первого шага. Он
@@ -196,9 +212,10 @@ SuperAI для записей кланов, затем обновляет Behavi
от повторного входа; при занятой защите вызов пропускается, но одноразовая метка
всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт
значение времени очереди; после регистрации объектов загрузчик последовательно
-подготавливает каждый сценарный пакет и выполняет только подтверждённые действия
-`Handler(19)` из первого `Init`. Статический просмотр не запускает Behavior, но
-может сохранить подтверждённые записи `Init` в сценарном varset.
+подготавливает каждый сценарный пакет и выполняет восстановленные действия
+`Handler(19)` и числовые literal assignments из первого `Init`. Статический
+просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном
+varset.
Это полезное начало сценария, но ещё не целая игровая жизнь AI. Текущий runtime
не воспроизводит аргумент обратного вызова в `AI+0x3fc`, защиту от повторного