aboutsummaryrefslogtreecommitdiff
path: root/docs/appendices/script-vm.md
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-20 00:45:58 +0300
committerValentin Popov <valentin@popov.link>2026-10-20 00:45:58 +0300
commit37db376dd3b259cfefdb7081770ca82bdc7542dd (patch)
tree033c50733cee522e7105abd2f2df0b893a14e762 /docs/appendices/script-vm.md
parentbcedda6e93d228593be7bef1552369aa5b70b623 (diff)
downloadfparkan-37db376dd3b259cfefdb7081770ca82bdc7542dd.tar.xz
fparkan-37db376dd3b259cfefdb7081770ca82bdc7542dd.zip
docs(script): describe native condition evaluation
Diffstat (limited to 'docs/appendices/script-vm.md')
-rw-r--r--docs/appendices/script-vm.md86
1 files changed, 80 insertions, 6 deletions
diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md
index 0e0217f..50e0f2b 100644
--- a/docs/appendices/script-vm.md
+++ b/docs/appendices/script-vm.md
@@ -42,12 +42,12 @@ budget. Опасное удаление идёт через World3D queue и оÐ
До восстановления opcode table package mode читает header/strings/symbols/
event offsets/raw bytecode losslessly. Статический анализ уже выделил отдельный
-five-way evaluator condition records (`ai.dll` VA `0x10005180`): tags `1..5`,
-type guards, object lookup и completion flag. Это не следует выдавать за
-instruction dispatcher или jump table `.scr`: bytecode opcode table всё ещё
-требует отдельного доказательства. Unknown opcode нельзя пропустить как один
-byte: это ломает синхронизацию. Для каждого доказанного opcode фиксируются
-number, size, operands, control flow, effects, errors и минимальный test.
+evaluator записей условий (`ai.dll` RVA `0x5180`). Его подтверждённые ветви и
+точные границы описаны ниже; он не является dispatcher-ом инструкций или jump
+table `.scr`. Bytecode opcode table всё ещё требует отдельного доказательства.
+Unknown opcode нельзя пропустить как один byte: это ломает синхронизацию. Для
+каждого доказанного opcode фиксируются number, size, operands, control flow,
+effects, errors и минимальный test.
GOG `ai.dll` доказывает этот framing двумя consumer-ами: loader по
`0x10001000` открывает `<bundle>.scr`, `varset.var`, `<bundle>.fml`, затем
@@ -65,6 +65,80 @@ sentinel records. Поэтому `ScriptInstruction::dispatch_selector()` воз
(`Handler(0)`, VA `0x10008034`) только устанавливает current context и flag
`+0x50 = 1`; это не даёт ему игрового имени и не заменяет runtime trace.
+### Evaluator записей условий: `ai.dll` RVA `0x5180`
+
+Ниже описан отдельный слой готовых условий, а не декодирование instructions.
+Результат привязан к GOG `ai.dll` версии `1.00`, SHA-256
+`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`;
+PE image base — `0x10000000`, адрес функции — VA `0x10005180` / RVA `0x5180`.
+Функция занимает 910 байт и имеет единственного прямого caller-а
+`FUN_10004ee0` (RVA `0x4ee0`). Это статический вывод по машинному коду; сам
+evaluator и его внешний объектный интерфейс здесь не запускались.
+
+Функция принимает signed index в массив записей по `this + 0x34`. Проверенный
+доступ `FUN_100070c0` возвращает 16-byte запись:
+
+| Смещение | Поле | Роль в evaluator-е |
+| ---: | --- | --- |
+| `+0x00` | `u32 tag` | Выбор одной из пяти ветвей |
+| `+0x04` | `u32 subtype` | Дополнительная проверка формата ветви |
+| `+0x08` | payload | Ключ объекта или индекс списка, в зависимости от tag |
+| `+0x0c` | byte completion | Записывается в `1` только после успешной проверки |
+
+Общие проверки допускают только пары `(tag 1..3, subtype 0)` и
+`(tag 4..5, subtype 1)`. Неизвестный tag и неподходящий subtype не завершают
+условие. При провале evaluator не очищает и не меняет байт завершения; при
+успехе он устанавливает его в `1`. Вызывающая функция `0x4ee0` отдельно
+пропускает уже завершённые записи, поэтому этот пропуск нельзя приписывать
+самому evaluator-у. Прямая запись evaluator-а в запись — только этот байт, но
+вызываемые виртуальные методы могут иметь внешние эффекты.
+
+Пары tag/subtype проверяются так:
+
+| Запись | Условие установки флага завершения |
+| --- | --- |
+| `(1, 0)` | Менеджер объектов, полученный из контекста `this + 0x60` по полю `+0x35c`, ищет payload через виртуальный слот `+0x1c`; поиск должен вернуть null. Найденный объект оставляет условие незавершённым. |
+| `(2, 0)` | Поиск payload через тот же слот `+0x1c` должен найти объект. Затем его слот `+0x10` должен вернуть `0`, либо слот `+0x18` должен заполнить переданную структуру результата так, чтобы её первый `u32` был `1`, `0x12` или `0x13`. |
+| `(3, 0)` | Поиск должен найти объект; его слот `+0x44` должен вернуть тот же `u32`, что и первый word контекста, на который указывает поле `this + 0x60`. |
+| `(4, 1)` | Payload выбирает 32-byte запись в списке `this + 0x4c`; вложенный в неё список дочерних ссылок находится по смещению `+0x0c`. Ни один проверяемый дочерний ключ не должен разрешиться в объект. |
+| `(5, 1)` | Используется тот же список. Отсутствующий объект пропускается; найденный объект проходит проверку, только если его слот `+0x10` вернул `0`. Первый найденный объект с ненулевым результатом завершает обход неуспехом. |
+
+Для tag 2 структура размером `0x138` байт инициализируется функцией
+`FUN_10004330`, затем передаётся в slot `+0x18`. x87-значение, которое
+возвращает этот вызов, инструкция `FSTP ST0` по RVA `0x5256` сразу удаляет со
+стека FPU. Следовательно, ветвь принимает решение только по первому word
+выходной структуры, а не по возвращённому числовому значению.
+
+У tag 4 и 5 есть важная граница индекса. Сравнение payload с количеством
+элементов — unsigned `CMP` с условием `JA`: только `payload > count` сразу
+оставляет запись незавершённой. Значение `payload == count` проходит эту
+проверку и доходит до проверенного доступа `FUN_10002e20`; тот отвергает
+`index >= count` и выбрасывает C++ exception. Это не эквивалентно обычной проверке
+`payload < count` с безопасным отказом для всех неверных индексов.
+
+Сами дочерние ссылки списка читаются как signed `i32`. Ровно `-1` — sentinel,
+который пропускается. Остальные значения проверяются как индексы в контекстном
+массиве `context + 0x10` с 32-byte элементами; в элементе по `+0x04` берётся
+ключ следующего поиска объекта. Для tag 4 первый найденный объект немедленно
+прерывает обход с незавершённым результатом; для tag 5 обход прекращается на
+первом найденном объекте с ненулевым результатом slot `+0x10`. Если обход
+достигает конца без такого запрета, соответствующая ветвь устанавливает byte
+completion.
+
+Из этих ветвей нельзя выводить языковые имена вроде «объект уничтожен» или
+«объект принадлежит игроку»: в коде подтверждены только поиски, виртуальные
+вызовы, сравнения и коды результата. Неизвестно и то, какая функция создаёт
+массив 16-byte записей. Считыватель `FUN_10011b20` (RVA `0x11b20`) отдельно
+разбирает `.scr` с переменной длиной: событие содержит длину и байты имени,
+счётчик вложенных записей, а вложенная запись — семь заголовочных `u32` и список
+ссылок. При разборе reader хранит описатели событий с шагом `0x14` и описатели
+вложенных записей с шагом `0x20`; это шаги элементов внутренних контейнеров, а не
+фиксированные размеры записей на диске. Связь этих контейнеров с массивом
+`this + 0x34` не найдена, поэтому приравнивать их нельзя. Не установлены и
+объект-владелец массива, соглашение его runtime-вызова и предметные эффекты
+внешних виртуальных вызовов; границы перечислены в
+[`knowledge-boundaries.md`](knowledge-boundaries.md).
+
`Handler(1)` — второй table entry, VA `0x10007fd0`, — не создаёт игровую
команду. Он сохраняет active VM context, берёт один instruction-derived index
через current event/instruction offsets `+0x48/+0x4c`, а затем разрешает его