diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 318 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 274 | ||||
| -rw-r--r-- | docs/reference/atmosphere.md | 65 | ||||
| -rw-r--r-- | docs/reference/environment-effects.md | 215 | ||||
| -rw-r--r-- | docs/reference/msh.md | 50 | ||||
| -rw-r--r-- | docs/reference/render-frame.md | 227 | ||||
| -rw-r--r-- | docs/tomes/05-render.md | 419 | ||||
| -rw-r--r-- | docs/tomes/06-behavior.md | 165 |
8 files changed, 1432 insertions, 301 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index 59b2ffd..103769c 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -10,6 +10,11 @@ authoring API без динамического подтверждения. Не доказаны символами точные имена renderer vtable slots, полный набор CShade state transitions и окончательный порядок части transparent/FX/shadow subpasses. +Дистанционная кривая тумана по квадратам границ подтверждена в живом CShade. +Обычный GPU fog color из горизонта подтверждён исполнением; выбор проходов по +native shade-state и их временные изменения цвета пока заменены общим туманом мира; +это приближение считается как `render_coverage fog_pass_eligibility_approx`. + После ограничения цели рельефом интерактивная камера проверяет движение против каждого отрисовываемого корневого строения класса `FORT` и его отрисовываемых компонентов. Для этого пути используются текущая Node38-поза @@ -45,33 +50,57 @@ MSH-запрос со свободной preview-камерой; это не пÐ стратегической и следящей камерами не подтверждено ([MSH](../reference/msh.md)). -Для MSH без потока цветов type 16 подтверждён только один постоянный alpha -override для диапазонов с CPU-цветом и raw-флагом `Batch20` `0x0008`: GOG -`Terrain.dll`, SHA-256 -`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`, RVA -`0x1B0F0`, задаёт alpha `0`, если установлен bit `0x0040` и сброшен `0x0010`. -Это покрывает `0x0148`; `0x0108`, `0x0118` и `0x0058` сохраняют alpha MAT0, -причём в `0x0058` bit `0x0010` имеет приоритет. Поток type 16 или отсутствие -`0x0008` исключают override. Проверка заводской сцены с фиксированной нативной -камерой подтвердила на одном кадре входа восстановление X-образного рисунка -двери, который виден и в оригинале; прежний Vulkan-readback показывал на этом -месте сплошной чёрный прямоугольник. Сравнение выполнено при времени сцены -2000 мс и трёх кадрах, запуск с Vulkan validation завершился без предупреждений -и ошибок. Это подтверждает только структуру проёма в этой сцене: правильность -освещения и полный попиксельный паритет не установлены. - -Общее fade-поведение по расстоянию в preview не реализовано. Открыты точная -метрика расстояния, выбор первого CPU-принятого треугольника диапазона и -эквивалентность frustum/culling-отбора оригинала; эти вопросы нельзя вывести из -захвата заводской двери. Данные потока type 16 ещё не декодированы, поэтому -правило выше относится только к MSH-диапазонам без этого потока цветов. +Кривые alpha и опорная вершина порталов сверены для MSH без type 16 +([описание кадра](../reference/render-frame.md#прозрачность-порталов-msh)). Native `RSQRTSS` +воспроизводится на x86-64; другие архитектуры и нулевая дистанция используют +явные `packet_distance_native_rsqrt_unsupported` / `packet_zero_distance_fallback`. + +Данные type 16 ещё не декодированы и считаются в `msh_type16_colors_unsupported`; +в статических сценах 29 миссий GOG (1736 загрузок моделей) этот поток отсутствует. +Его декодирование отложено до первого реального ненулевого счётчика. +Внутренний ракурс «Конструктора» после +настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе; +наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние +окна — с внутренней стороны. Окно «Конструктора» на правом краю кадра при +дистанции около 85 совпадает; внутренние дальние ракурсы и остальные случаи +отсечения ещё не приняты. Общей готовности R2 эти пары не доказывают. + +Старые захваты без `geometry_projection` используют состояние камеры после +обхода мира, которое может отсекать близкие поверхности. Это отмечается +`captured_projection_unverified`; для приёмки таких поверхностей нужен новый захват. + +Захваты без `sky_uv_clock` не подтверждают начальное положение облаков, +звёзд и фона. Replay использует elapsed clock приложения и считает +это как `captured_sky_phase_unverified`; для приёмки неба нужен новый захват. Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state log на минимальных сценах с одним типом материала. +Горизонтальные грани Land.msh с флагом `0x20000` используют исходную карту Du/Dv, окно shifted-камеры и квадратный размер текстуры, проверенные исполнением DLL. Диапазон глубины, небо/туман/FX основного вида и соответствие размеров при обрезанном capture остаются приближениями: `planar_reflection_render_context_approx`; неперспективная камера API использует обычное зеркало. +Raw queue clock задаёт фазу волн отдельно от времени суток и sky elapsed; старые захваты без него считаются в `captured_water_phase_unverified`. +Один target отражает ближайшую активную высоту воды; остальные высоты и неплоские грани считаются в `planar_reflection_other_planes_unsupported` и `planar_reflection_nonplanar_unsupported` и сохраняют обычный terrain shader. +`terrain_face8_pipeline_unverified` теперь считает только исходные грани с `0x8` без `0x20000`; сама семантика оставшихся специальных проходов ещё не установлена. + ## FXID field-level semantics +Control Op1 пока использует цвет и радиус без случайных добавок команды; +это явное приближение считается в `render_coverage control_op1_jitter_unsupported`. +Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены. +Локальный флаг `0x80000000` сохраняет свет на MSH владельца, исключая его +из общего света рельефа. Мерцание жёлтого источника над заводом ещё использует +авторские цвет и радиус; порядок общего native RNG пока не воспроизводится. + +Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные +time modes/привязки считаются как `render_coverage control_op2_unsupported`. +Управление фазой экземпляра командами Control остаётся до его подключения в runtime. +Часы экземпляра поддерживают modes 0–3, смену режима и отложенный перезапуск; +preview создаёт их в момент ноль. Динамический manager A8 заменён нулём +(`control_fx_manager_flag_stub`); очередь update с шагом 50 ms ещё не подключена. +При расширении выбранных начальных строк все пропущенные команды считаются +в `control_fx_unsupported`, а time modes 4–17 — в `control_fx_requires_game_state`; +причина и владелец выводятся в лог. Неразобранные FXID считаются отдельно в `control_fx_invalid_documents`. + Размеры команд, resource references, lifecycle, flags families и используемые time modes известны. Не закрыто значение каждого поля body opcodes 1--10, отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких @@ -133,7 +162,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 +173,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 +185,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; это не проход @@ -184,6 +250,34 @@ core и `ResearchCandidateSet::refresh_selection`: последний получ создаёт владельца живого контекста, не передаёт реальные AI candidates и не выбирает результат Handler(43). Это не общий dispatcher и не загрузка игры. +Начальный массив слота AI `+0x38` теперь восстановлен отдельно от этого +исследовательского пути. В `iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_100392e0` (RVA `0x392e0`) вызывает слот для каждого значения счётчика +`second_resource.tag`. В `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` метод +RVA `0x2010` передаёт `AI+0x83c` в `FUN_10002980` (RVA `0x2980`), где +`FUN_100048f0` (RVA `0x48f0`) добавляет переданный DWORD. Вызывающая сторона +повторно передаёт `0xffffffff`; эта функция не записывает значение обратно. +Поэтому подтверждён только начальный массив sentinel-слов. Неизвестно, какие +поздние обработчики читают эти ячейки и заменяет ли кто-либо их ID. В начальном +массиве нет назначенных ID объектов; дальнейший смысл этих слов пока не +установлен. Сам массив не является списком +`BehaviorDistributorState.registered_objects` и не определяет владельца +контекста исследований. + +Rust хранит этот снимок как `MissionAiStartupState` в состоянии загруженной +миссии. Полная загрузка проходит по всем TMA-кланам, включая кланы без пакета +сценария и без пути таблицы исследований; при предпросмотре список состояний AI +остаётся пустым. Знаковое поле `second_resource.tag` читается по сырым битам как +`u32`. Бюджет в 1 000 000 DWORD на миссию и выделение памяти с обработкой ошибки +защищают только память Rust; это ограничение не приписывается оригинальному +движку. Искусственные Rust-тесты проверяют обе формы TMA-клана, индекс клана, +независимые массивы, пустой путь сценария, предпросмотр, чтение тега как +беззнакового числа, лимит и откат при ошибке. Проверка на оригинальных ресурсах +для `Single.01` подтверждает состояния обоих кланов (`9/9`) до выполнения +`Init`; это не полный запуск миссии и не исполнение исходных DLL. + Для GOG-сборки также найден статический host call path в `Init`. В `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи кланов и вызывает для них `FUN_100392e0`, затем вызывает `FUN_10039440` (RVA @@ -425,7 +519,9 @@ Global Control getters `1/2` и setter `1` проверены в native x86 orac этого класса native oracle не запускал. Rust API обращается к строкам проекции конкретного живого `ObjectHandle`; это запрос к уже загруженной таблице, а не исполнение Control или Behavior tick. Setter `17`, остальные factory tags и -внешние эффекты их методов не включены. Для tag `9` selector `0` использует +внешние эффекты их методов не включены. Структурное чтение записи CTLD с +меткой 17 ещё не восстанавливает её обработчики и игровое поведение. +Для tag `9` selector `0` использует отдельный scale `+0x11c`; остальные поддержанные классы используют коэффициент напрямую. Пустой набор и нулевой знаменатель остаются native no-result, а неподдержанный factory tag и расчётный диапазон произведения/отношения за @@ -469,6 +565,41 @@ MSH-часть проверки подтверждает начальный сн остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие изменения в полном Control/animation tick-е не проверялись. +Устройство фабрики `2` через selector `6` читает `+0x174` только при текущем descriptor tag `2`, +затем проверяет, что устройство создано фабрикой `2` и поле доступно. Значение +должно быть конечным нормальным числом или нулём любого знака. Строки в +`[27..35)` и `[35..43)` читаются до первого NUL: хвост игнорируется, +ASCII-регистр не важен. После изменения пустая строка оставляет старые биты; +пустая пара конструктора уже совпадает и не вызывает пересчёт. Проверка не +использует список имён: нужен один корневой mesh `BULL` из `ObjectsRegistry`, +без дочерних компонентов и записей timeline; все типизированные записи должны +иметь tag `17`, а эффекты и таблица NDP — разбираться полностью. Число строк +NDP произвольно, включая пустую таблицу. + +Сканер возвращает коды выбранной начальной группы в исходном порядке. Допустимы +`1/4/12`. `Some([])` означает, что по полному разбору данных команд нет: группа +отсутствует, пуста или сырое беззнаковое значение индекса не меньше числа +групп. `None` означает неподдержанный либо неполный разбор; повреждённые данные +дают ошибку. Для выбранного opcode `4` нужны FXID-типы `2/3/4/5/7/8/9` и +допустимые CPT-смещения внутри компонента; типы `1/6/10` остаются недоступными. +Непредставленный ключ или число не превращаются в ноль. Если распознанный +ресурс повреждён, новая миссия завершается ошибкой до публикации, сохраняя +прежние мир и RNG. + +Загрузка лицензированной AutoDemo и Rust-запрос selector `6` охватили 12 +устройств и семь пар с битовыми значениями: +`bb_m_01=43f98000`, `bm_m_01=44228000`, `bl_l_01=436a0000`, +`br_b_01=44c80000`, `bf_b_01=443b8000`, `bl_m_01=443b4000`, +`bm_l_01=43340000`. Все семь значений совпали с замороженными арифметическими +векторами. Пятнадцать изолированных вызовов selector `35` оригинальной DLL +(включая семь однострочных BULL-таблиц) проверили арифметику при x87 control +word `0x007f`, но не создавали полный native World. Таблица Control-устройств +Rust по-прежнему строится при полной загрузке +Unit DAT. CPU-проекция не воспроизводит временное создание объектов оригинала и +его побочные эффекты FX, звука и RNG, размещение в World или следующий Control +tick. Полное размещение объектов, игровой tick, Handler 43 и другие +неподдержанные форматы остаются открытыми. + Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно. Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и @@ -532,12 +663,48 @@ callbacks. Licensed двухстадийный узел шахты подтвеÑ новый API при доле `0.5`, без FX и window path. Гибель root/critical узла и неизвестные flags/aggregate также остаются явными ошибками. -Rust query обращается к загруженному объекту и поддерживает `0xe00` только для -текущего descriptor tag `26` с factory tag `26`; другие активные typed tags и -factory paths остаются явными границами. Запрос читает текущее type-1 значение. -Licensed `min_l_n1.dat` подтверждает изменение `0xe00` и Behavior `0x1002` с -`1.0` до `0.5` после явного уменьшения количества вдвое; чтение получает -текущее значение provider, который также меняет явный бюджетный шаг. +Rust query обращается к загруженному объекту и поддерживает `0xe00` для пар +текущего descriptor tag и factory tag `(5,5)`, `(5,26)`, `(26,5)` и `(26,26)`; +прочие специализированные tag/factory пути остаются явными границами. Запрос +читает текущее type-1 значение provider. Licensed `min_l_n1.dat` подтверждает +изменение `0xe00` и Behavior `0x1002` с `1.0` до `0.5` после явного уменьшения +количества вдвое; чтение получает текущее значение provider, который также +меняет явный бюджетный шаг. + +Indexed selector `0x400` в том же Control IID `0x204` возвращает текущее type-1 +значение строки provider, выбранной через key устройства, при любом descriptor +и factory tag. Rust принимает конечное normal-or-zero значение, включая оба +знака нуля, и сохраняет его биты; недоступная таблица, отсутствующий key и +значения вне этой численной области дают типизированную ошибку. В оригинальном +`Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` ветвь +`FUN_1002bb40` (RVA `0x2bc3c`) читает provider pointer `device+0x44` и key +`device+0x04`, вызывает provider slot `+0x0c` в режиме `1` и не проверяет +descriptor/factory tag. Rust-проекция читает сохранённый снимок; она не вычисляет +value из amount, maximum, coefficients или множителя `M`. Native selector-cell +alias/fallback за пределами валидной записи и поведение native при невалидном +key остаются неисследованными; Rust не подставляет для них `0.0` или `None`. +Это покрывает перечисленные query-пути. Полный интерфейс IID `0x204`, его +взаимодействие со всеми Behavior-вызовами и полный игровой tick остаются за +границей подтверждённого поведения. + +Объём свидетельств для `0x400` ограничен и состоит из трёх частей. Native +oracle на настоящем `Control.dll` проверил шесть выходов по raw bits для +ограждённого синтетического owner/provider table; он вызвал indexed query и +реальный provider getter, но не создавал оригинальные factory-объекты. Матрица +выходов находится в [техническом разборе](script-vm.md#индексный-control-getter-сырой-селектор-0x400). +Независимый разбор AutoDemo обнаружил восемь таблиц и 100 устройств с +доступными ключами, но все выбранные provider values равны `1.0`; такой набор +подтверждает загрузку и область ключей, однако не различает, какую именно +строку выбирает алгоритм. Игнорируемый Rust-тест опрашивает эти 100 устройств и +проверяет те же биты, не выдавая равенство ресурсных данных за доказательство +индексирования. + +Вместе native-векторы и resource acceptance не подтверждают состояние +оригинальных factory instances, полный контракт IID `0x204`, полный +Behavior-call flow или живую миссию/gameplay. Поведение при ошибочных native +указателях и ключах, а также selector-cell alias/fallback вне валидной строки +по-прежнему не описано; Rust сохраняет typed errors на этой границе. Native oracle проверил изолированные пути распределения, обновления класса `4`, стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` на interleaved parents `[-1, 0, 0, 1, 3]` с callback-порядком `1, 3, 4, 2` и @@ -675,16 +842,19 @@ resource-device значения во время игры. `0x5780/0x57c0` участвуют в векторе; Rust воспроизводит его raw output bits и отдельные cache, order, clamp и signed-zero случаи. -Для обычной начальной загрузки scale-input тоже закрыт. AniMesh constructor -оставляет `+0x158/+0x15c/+0x160` равными `1.0`; перед Control event `4` World3D -проходит Agent event `0`, AniMesh RVA `0x1a80` вызывает базовый return `0x17df0` -и передаёт event `4/null` модели, FX, CollManager и CollObject. Их проверенные -ветви `0x75b0/0x6fc0`, Effect RVA `0x3d30`, Control RVA `0x1ce00` и `0x1fe00` -не вызывают AniMesh setter `0x14770` и не меняют outer scale. Поэтому FAC0 в -стандартном `LoadAgent` получает `[1, 1, 1]`. Другой маршрут -`AddNewObjectToGame`/mirror к setter и последующие placement/tick остаются -отдельными границами. В synthetic x86-векторе по-прежнему явно заданы `[2,3,5]`; -он не является источником scale для полного load. +Для обычной начальной загрузки входной масштаб единичный. Конструктор модели +AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`; +`Agent+0x158` хранит указатель на отдельный Control. Перед Control event `4` +World3D проходит Agent event `0`: AniMesh RVA `0x1a80` вызывает базовый return +`0x17df0` и передаёт event `4/null` модели, FX, CollManager и CollObject. +Проверенные ветви модели `0x75b0/0x6fc0`, Effect `0x3d30` и CollManager +`0x1ce00` не меняют масштаб. CollObject получает событие через основной +интерфейс `0x3c2fc`, slot `+0x08 -> 0x1f530`: он кэширует host-интерфейсы, +а необязательный вызов CollManager `0x1c330` сохраняет только указатель +`CollObject+0x20` в `CollManager+0x3c`. Поэтому FAC0 получает `[1, 1, 1]`. +Отдельные пути `AddNewObjectToGame`/mirror и последующие placement/tick остаются +за этой границей. В synthetic x86-векторе scales `[2,3,5]` заданы отдельно. +Подробная привязка интерфейсов приведена в [разборе Script VM](script-vm.md). Смысл `Slot68 +0x30/+0x34`, tag-27 cache DWORD, owner accumulators `+0x530`, `+0x534`, `+0x538` и результата mutable word `26` не имеет доказанных единиц @@ -839,22 +1009,34 @@ arg1 следующего вызова. `AddNewObjectToGame` принимает `FUN_10002a30` (RVA `0x2a30`, `RET 8`) записывает его в `inner+8`. TMA tag `row+0x20` ограничивает число вызовов AI slot `+0x38`, но не задаёт владельца. +Создание AI не фильтруется по наличию сценарного пути. В `iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов и вызывает для каждой +`FUN_100392e0` (RVA `0x392e0`); в `ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` +`CreateSuperAI` (RVA `0xf710`) выделяет AI и вызывает конструктор по RVA +`0x1000` без проверки пути сценария. Создание исследовательского контекста +следует за успешным созданием AI и требует включённого флага создания +исследований и непустого `second_resource.path` до первого NUL. Переданный в +`descriptor[0]` индекс клана, а не `second_resource.tag`, задаёт владельца; +тег остаётся счётчиком начальных вызовов AI slot `+0x38`. + В Rust эта граница представлена только загрузкой таблиц. Полный `load_mission` требует явный `MissionStartupInput { queue_time_ms, -research_creation_gate }`; gate не выводится из `MissionMode` или имени пути. -При `true` загрузчик проходит по успешно разобранным пакетам сценариев, берёт -реальный `second_resource.path` соответствующего TMA-клана до первого NUL, -отклоняет не-ASCII путь, затем сохраняет отдельный `ResearchTrfDocument` с -`clan_index` и нормализованным путём. Пустая строка, выключенный gate или -отсутствие пакета сценариев пропускают чтение. Повтор одного пути разными -владельцами приводит к отдельным буферам строк TRF0. Обычные тесты на -искусственных данных проверяют владельцев, повторное декодирование, пропуски, -ошибки и откат; игнорируемая проверка оригинальных ресурсов связывает реальные -`Campaign.00/Mission.01` пары путей с индексами владельцев. Это не загружает -полный исследовательский контекст оригинала. -Успешно разобранный пакет сценариев здесь обозначает только границу текущей -настройки Rust, а не совпадение с полным нативным жизненным циклом -`CreateSuperAI`. +research_creation_gate }`; этот флаг не выводится из `MissionMode` или имени +пути. Если флаг равен `true`, загрузчик проходит по существующим +`MissionAiStartupState` всех TMA-кланов, в том числе без сценарного пакета, +читает их `second_resource.path` до первого NUL и отклоняет не-ASCII путь. +Пустая строка пропускается; выключенный флаг и статический preview не читают +TRF. На каждого +владельца сохраняется отдельный `ResearchTrfDocument` с нормализованным путём, +даже когда разные кланы указывают на один файл. Обычные тесты на искусственных +данных проверяют владение у кланов со сценариями и без них, повторное +декодирование, пропуски, ошибки и откат; игнорируемая проверка оригинальных +ресурсов связывает реальные `Campaign.00/Mission.01` пары путей с индексами владельцев. +Rust пока сохраняет только декодированные данные и связь с владельцем: он не +создаёт полный исследовательский контекст, не строит AI-каталог и не исполняет +`Handler(43)`. Нативный metadata loader нельзя считать простым копированием таблицы. В GOG `Behavior.dll` (SHA-256 diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index 790729b..f7f8b97 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-дескрипторов @@ -1355,18 +1410,36 @@ payload. Позже та же функция `World3D+0x7cb0` повторяет времени из `DATWorld+0x32a38` (`0x7e6e`); `World3D+0x7ea0` передаёт туда же время в `0x80a9`. Setter `World3D+0x5140` записывает входной DWORD в эту ячейку. Нулевой вызов связывает интерфейсы, а ненулевой `NumericTime` запускает отдельную -ветвь активации. Конструктор AniMesh задаёт `+0x158/+0x15c/+0x160` равными `1.0`. -Обработчик модели `0x75b0/0x6fc0` привязывает интерфейсы и матрицу, Effect RVA -`0x3d30` и Control RVA `0x1ce00` сохраняют host/service указатели, а Control -`0x1fe00` меняет только локальный collision-флаг. Эти ветви не вызывают AniMesh -scale setter `0x14770` и не пишут поля масштаба. При загрузке AniMesh получает -Control secondary owner с QI selector `0xa` и сохраняет возвращённый base owner +ветвь активации. Модель занимает отдельный объект в памяти: её конструктор +AniMesh RVA `0x68a0` задаёт `Model+0x158/+0x15c/+0x160 = 1.0`. +Поле `Agent+0x158` хранит указатель на Control. Проверенные ветви модели +`0x75b0/0x6fc0`, Effect RVA `0x3d30` и CollManager RVA `0x1ce00` сохраняют +матрицу и служебные указатели, оставляя масштаб модели неизменным. + +Фабрика CollObject возвращает вторичный интерфейс `object+4`. QI `0x28` +вычитает `4` и сохраняет в `Agent+0x170` основной объект с vtable `0x3c2fc`. +Его event slot `+0x08` ведёт в Control RVA `0x1f530`. При event `4/null` +обработчик кэширует семь host-интерфейсов; при наличии QI `0x203` вызывает +CollManager slot `+0x10` таблицы `0x3c178`. Этот метод, RVA `0x1c330`, +сохраняет только указатель `CollObject+0x20` в `CollManager+0x3c`. +Для свежего BULL Agent без внешнего host QI `6` при привязке даёт основной Agent +с vtable `0x201d4`. Ненулевой event `4` вызывает его slot `+0x24`, AniMesh +RVA `0x17d60`, который читает `[Agent+0x120]`; вызывающий код сохраняет +результат в `CollObject+0x24`. +Затем CollObject slot `+0x0c`, Control RVA `0x1fe00`, сбрасывает бит `0` +поля `CollObject+0x30`. Event `0x15` с первым DWORD payload `9` завершается +без вызовов и записей. Поле Control `owner+0x34` хранит указатель на CollObject; +его slot `+0x14`, RVA `0x1f670`, меняет флаги `CollObject+0x30`. +Перечисленные ветви не вызывают AniMesh setter `0x14770`. + +При загрузке AniMesh получает Control secondary owner с QI selector `0xa` +и сохраняет возвращённый base owner в `Agent+0x158`: `AniMesh.dll+0x2ff0` делает этот вызов в RVA `0x32ee..0x3300`, а `Control.dll+0x76f0` для selector `0xa` вычитает `0x14` из `ECX` и возвращает указатель. Диспетчер AniMesh RVA `0x1afb..0x1b0d` посылает через `Agent+0x158` событие `4` в slot `+0x08`. Его base-owner vtable — Control RVA `0x3d298`, и slot ведёт прямо в `0x314f0`; при нулевом payload исходный обработчик проходит -`0x7830 -> 0xf940`, поэтому FAC0 входит с constructor scales `[1, 1, 1]`. +`0x7830 -> 0xf940`, поэтому FAC0 получает Model scale `[1, 1, 1]`. Встречающиеся рядом переходники относятся к другим интерфейсам: Control owner `+4` с таблицей `0x3d254` проходит через `0x31ae0` (adjust `-4`), а owner `+8` с таблицей `0x3d1fc` — через `0x31b00` (adjust `-8`). Последний путь не является @@ -1422,6 +1495,77 @@ descriptor word `8`. Текущий tag `26` тем же общим reader-ом умножает на `M`. Это правило не меняет отдельные callbacks бюджета или запись factory-26 selector `0x500`. +Сырой selector `6` в `FUN_1002bb40` выбирает устройство по ordinal через +интерфейс `owner+0x0c`: таблица `this+0x5b0` соответствует `owner+0x5bc`. +RVA `0x2bb91` проверяет тег текущего descriptor на равенство `2`. При совпадении +RVA `0x2bb9a` загружает `device+0x174` как `f32`, сохраняет значение в выходной +буфер и возвращает `AL=1`/`RET 8`; другие текущие теги возвращают false. +Наличие поля `+0x174` подтверждено для экземпляра фабрики `2`. INTO сохраняет +исходный тип экземпляра, поэтому проверка тега descriptor отдельно не доказывает +наличие этого поля у другой фабрики. Rust selector `6` сначала проверяет +текущий descriptor tag: тег не `2` даёт `Ok(None)`; затем требуется известная +фабрика `2` и конечное нормальное число либо ноль любого знака в `+0x174`. В +`Control.dll` (SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) поле +получает ноль в конструкторе по RVA `0x294c0`; чтение, сравнение пары строк и +пересчёт находятся по RVA `0x2bb40`, `0x29940` и `0x296f0`. + +Typed record хранит две ASCII C-строки в words `[27..35)` и `[35..43)`. Строка +заканчивается на первом NUL: байты за ним игнорируются, а регистр при сравнении +не учитывается. Если новая, изменённая пара содержит пустую строку, native +recalc завершается до `CreateObject` и selector `35`, поэтому сохраняются +прежние биты. Пустая пара конструктора уже совпадает с текущей и вообще не +запускает recalc. Иначе append и первая активная запись `INTO` применяют +результат в исходном порядке. Для нераспознанной формы поле остаётся +недоступным; ошибка разбора распознанного, но повреждённого ресурса прерывает +новую загрузку до публикации. + +В общем случае native selector 35 не равен сумме сырых Max: +`Control.dll` getter `0x0dc40` (selector 3) читает Max по `descriptor+4`, где +указатель descriptor лежит в начале runtime provider row, затем умножает его +на масштабы владельца `+0x660` и `+0x548`. Функция `0x13620` складывает выходы +в порядке строк с округлением `f32`; при `row+0x24 != 0xffffffff` добавляется +индексированный вклад. Конструктор `0xb240` задаёт runtime `row+0x24` +служебное значение `0xffffffff`. Только в свежей форме поддержанного BULL оба +масштаба равны `1`, а все `row+0x24` равны +`0xffffffff`, поэтому формула сводится к NDP Max сумме. + +Пересчёт создаёт временный объект запросом `9` через `World3D.dll+0x7cb0`. +Загрузчик `Control.dll+0x32280` формирует для его корня входной контекст с +полем `+0xc8 = 0xffffffff`. Для разрешённых opcode `1` и `12`, а +также `4` только после FXID/CPT-проверки, подтверждённые callbacks сохраняют +входы суммы: `Control.dll` RVA `0x43d0` меняет только локальные слова; в +`AniMesh.dll` (`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) +RVA `0x5500/0x93c0` меняют узлы и список модели, а `0x5320` не пишет её +масштабы. Это статическая граница свежего пути, не снимок живого native World +после всех creation callbacks. + +Проверка opcode `4` читает только выбранные строки и принимает FXID-типы +`2/3/4/5/7/8/9` с распознанными ASCII-ссылками и CPT offsets внутри компонента; +типы `1/6/10`, отсутствующие ресурсы и непроверенные offsets оставляют поле +недоступным. В `Effect.dll` (SHA-256 +`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`) проверены +инициализаторы типов `2/3/4/5/7/8/9` по RVA `0x12d10/0x103b0/0x108c0/0x28a0/ +0x1720/0x11230`; тип `9` разделяет путь типа `3`. + +`AssetManager::initial_control_effect_opcodes` собирает исходный порядок кодов +выбранной начальной группы до фильтра opcode `4`. `Some([])` означает, что +по структуре данных команд нет: группа отсутствует, пуста или её беззнаковый +индекс не меньше числа групп. Условия отдельных строк этот читатель не +вычисляет. `None` означает неподдержанный либо +неполный формат, а повреждённая структура возвращает ошибку. Такая разница не +позволяет принять неизвестный разбор за пустую сумму. + +Ресурсные callbacks в этой цепочке привязаны также к следующим образам. +Записи палитры и звуковые либо графические буферы имеют отдельное хранение; +проверенные начальные вызовы не меняют входы суммы NDP. + +| Модуль | SHA-256 | Существенные RVA | +|---|---|---| +| `World3D.dll` | `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e` | `0x7cb0` — создание; `0x4b10/0x4cb0` — текстуры и lightmap | +| `Ngi32.dll` | `bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5` | `0x11f80` — чтение ресурса; `0x11410` — счётчик палитры; `0xd620/0xe900` — звуковые буферы | +| `Terrain.dll` | `af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` | `0x166a0/0x17ac0` — получение указателя на числовое поле World | + FCA0 передаёт в owner query порядковый номер `i`; query `0x2bb40` находит устройство `i` и вызывает его reader. Уже reader `FUN_10021d00` получает provider key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` (RVA `0x0dc40`) @@ -1429,6 +1573,80 @@ key из поля устройства `+0x04`. Provider getter `FUN_1000dc40` ( строку `table + key*0x2c`; текущее type-1 значение находится по `+0x14` строки. Индекс устройства и ключ provider row — разные значения с разными задачами. +#### Индексный Control getter: сырой селектор `0x400` + +В той же сборке `Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +индексный query для `QueryInterface(0x204)` входит через слот `+0x18` vtable +`0x3d1b4`: `FUN_1002bb40` (RVA `0x2bb40`, `RET 8`, `ECX=owner+0x0c`, ordinal +устройства в `EDX`, selector и output pointer на стеке). При сыром selector +`0x400` функция попадает в ветвь RVA `0x2bc3c`. +Ветка получает provider pointer из `device+0x44`, отдельно читает provider key +из `device+0x04` и вызывает provider vtable slot `+0x0c` в режиме `1`, передавая +адрес ячейки selector как storage результата. После вызова значение переносится +в выходной `f32` caller-а; ветвь возвращает `AL=1` без проверки текущего +descriptor tag или factory tag. + +Поле `device+0x44` указывает на provider interface `owner+0x04` с vtable +`0x3d254`; slot `+0x0c` ведёт к `FUN_1000dc40` (RVA `0x0dc40`). Getter +использует key для строки общей таблицы, а не ordinal устройства. Для валидной +строки selector `1` читает её текущее type-1 поле `+0x14` напрямую: +`0x400` поэтому не пересчитывает долю из amount и maximum и не умножает её на +коэффициент устройства, `M` или descriptor word. В Rust тот же маршрут выглядит +так: + +```text +device = devices[device_index] +value = providers[device.key].current_type1_value +return Some(value) +``` + +Индекс устройства сначала выбирает запись, её key затем выбирает provider row; +этот indexed-вызов не является глобальным запросом `0x0f` и не является +metadata getter IID `0x16`. Проекция Rust принимает конечные нормальные значения +и точные `+0`/`-0`, сохраняя биты нулевого значения; остальные float-диапазоны и +недоступные table/key остаются типизированными ошибками. Native selector-cell +alias/fallback вне валидной записи provider не реализован: Rust не подставляет +для такого пути `0.0` или `None`. + +Шесть изолированных вызовов оригинала проверяют именно этот валидный +indexed-вызов. +В каждом ordinal устройства равен `2`, а ключ provider равен `1`, `3` или `4`; +все значения строк различаются, поэтому ключ не совпадает с ordinal и не +подменяется соседней строкой. + +| Вектор | Ordinal | Key | Raw output | AL | +| --- | ---: | ---: | ---: | ---: | +| Значение из корпуса `1.0` | 2 | 1 | `0x3f800000` | 1 | +| Доля `0.625` | 2 | 1 | `0x3f200000` | 1 | +| Отрицательное конечное `-2.5` | 2 | 3 | `0xc0200000` | 1 | +| `+0` | 2 | 1 | `0x00000000` | 1 | +| `-0` | 2 | 3 | `0x80000000` | 1 | +| Отличающаяся строка `0.0625` | 2 | 4 | `0x3d800000` | 1 | + +Вектор `0.625` специально задаёт native word `device+0x50 = 0`; он всё равно +возвращает текущее поле provider. Таблица устройства, provider и остальные +строки были синтетическими, ограждёнными входами; исполнены оригинальные +`FUN_1002bb40` и provider getter `FUN_1000dc40`, без constructors, ControlSystem +или mock callbacks. Все входные записи остались неизменными, проверены canary +области, а изменился только DWORD выхода. Для DLL с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` входной +control word `0x027f` был временно заменён на `0x007f` и восстановлен после +вызова; у всех шести ветвей `AL=1`. Выходы совпали с прямыми значениями выбранных +synthetic rows. Это не снимок состояния оригинальной фабрики или миссии: полный +IID `0x204`, интеграция Behavior и gameplay-трасса не проверены. + +Независимый разбор лицензированного AutoDemo дал восемь таблиц и 100 пар +устройство/key, но все выбранные type-1 значения там равны `1.0`. Такой набор +проверяет загрузку таблицы, ключи и доступность запросов, но не позволяет +различить выбор строки по key и по ordinal. Игнорируемый Rust acceptance test +проверяет все 100 запросов на этом корпусе и ожидает `0x3f800000`. В тесте явно +заданы `MissionStartupInput { queue_time_ms: 100_000, +research_creation_gate: false }`; эти значения являются входами Rust-проверки, +а не доказательством штатного gate оригинала. Native синтетические строки дают +отдельное доказательство выбора key. Оба результата имеют ограниченную область +и не являются сравнением полного поведения игры. + Конструктор owner `FUN_10006bf0` (RVA `0x6bf0`) задаёт scalar `+0x1a8 = 1.0` и вызывает `FUN_10014260`, который записывает `1.0` в `+0x354/+0x358`. Проверенные ветви канонического event `4/null` и FAC0 не меняют эти три поля diff --git a/docs/reference/atmosphere.md b/docs/reference/atmosphere.md index 65ed176..dba20c2 100644 --- a/docs/reference/atmosphere.md +++ b/docs/reference/atmosphere.md @@ -50,7 +50,25 @@ key_time = floor(duration * (key_hour * 60 + key_minute) * 60 / 86400) theta = (clamp((now - birth_start) / lifetime, 0, 1) * 1.2 - 0.1) * pi ``` -Затем к нему применяется матрица рождения. У солнца и луны независимые +Затем к нему применяется матрица рождения как к вектору-столбцу. Если `A` и +`B` — углы рождения, а `x = cos(theta)` и `z = -sin(theta)`, результат равен + +```text +[x*cos(A) + z*sin(A)*sin(B), + x*sin(A) - z*cos(A)*sin(B), + z*cos(B)] +``` + +Порядок важен для всего освещения: транспонированное умножение переносит +солнце на другую сторону мира и меняет освещённые склоны, блики и тени. +В GOG `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) +матрицу строит RVA `0x4A550`, а вектор преобразует `CSun::Update`, RVA +`0x4B570`. Живой замер после 282184 мс при длительности 525000 мс и углах +`[90, 30]` дал направление `[-0.4950135, -0.1408745, -0.8573891]`; +Rust-тест проверяет этот результат с допуском `3e-6`. + +У солнца и луны независимые интервалы и независимые направления: при отсутствии активной луны нельзя подменять её направление противоположностью солнца. `SunFrame::color` — это единственный primary directional RGB. Второй источник получает направление @@ -131,6 +149,31 @@ gradient и clouds row 2. Для nebula/stars/clouds используются UV directional RGB из `sky.packed[0]`. Это четыре фиксированных pass, а не три независимых цветных слоя. +Все три текстурных слоя движутся. Обновление GOG `CSky` +(`Terrain.dll`, RVA `0x48B90`, запись UV `0x4942A–0x495B7`; +[сборка и SHA-256](original-binaries.md)) берёт разность текущего `u32` clock +и сохранённого поля объекта `+8` с переполнением. Это отдельные миллисекунды +жизни неба, а не абсолютная фаза суток из SKE. + +| Слой | Период | Добавка к исходным UV | +|---|---:|---| +| Фон | 300 000 мс | одинаковый сдвиг U и V | +| Звёзды | 100 000 мс | одинаковый сдвиг U и V | +| Облака | 50 000 мс | сдвиг только U | + +Добавка равна остатку от деления elapsed clock на период, умноженному на +нативный коэффициент `f32` (`3.3333333e-6`, `1e-5`, `2e-5`). Вершинные UV +каждый раз вычисляются от позиции купола заново; накопление приращений +создавало бы дрейф. `SkyMesh::update_uvs` получает текущие миллисекунды кадра +и сохраняет геометрию купола. В работающем оригинале проверены UV всех трёх +слоёв, включая разность clock с переполнением. + +Захват сохраняет `sky_uv_clock` на входе в рендер неба, проверяя его по +фактическим UV вершины зенита и clock того же кадра. Replay удерживает эту +фазу вместе с фазой суток. Без такого поля текстуры движутся по elapsed clock +приложения; начальная фаза оригинала неизвестна и явно считается как +`captured_sky_phase_unverified`. + Цвет первого screen-gradient и последнего пояса вычисляет `SkyFrame::screen_gradient(yaw_radians, glare_rgb_delta)`. Native `CSky` сначала округляет `(yaw + pi) * 180 / pi` режимом nearest-even, выбирает сектор @@ -167,21 +210,27 @@ RGB palette 12 с минимумом `80/255` на канал и alpha `150/255` ## Туман и свет -Нативное обновление `CSky` записывает две дальности и один packed RGB. +Нативное обновление `CSky` записывает две дальности и нижнюю границу освещения. `FogFrame` повторяет этот контракт: ```text fog.start = sky.values[0] * 700 fog.end = sky.values[1] * 700 -fog.color = RGB(sky.packed[1]) / 255 +lighting_floor = RGB(sky.packed[1]) / 255 ``` Поля не скрывают порядок исходных параметров: значения сохраняются в -`SkySample`, а преобразование выполняется только в `fog_frame`. Vulkan path -передаёт цвет и дальности в frame uniforms; vertex shader вычисляет линейный -коэффициент по расстоянию до камеры, а fragment shader смешивает RGB материала -с fog color в ветвях combiner, которые используют туман. Конкретный draw state -может обходить это смешивание. +`SkySample`, а преобразование выполняется только в `fog_frame`. Нижняя граница +освещения ограничивает результат расчёта света снизу; она не задаёт цвет тумана. +Обычный цвет тумана выбирается из `sky.colors[0..4]` по направлению камеры, +как цвет горизонта. Это подтверждено в `CSky::Render` и живым совпадением +его значения с `D3DRENDERSTATE_FOGCOLOR` в том же игровом тике. + +Vulkan path передаёт эти параметры раздельно. Vertex shader вычисляет +коэффициент по квадратам расстояния и границ, а fragment shader смешивает +RGB материала с цветом тумана. Конкретный native draw state может обходить +смешивание или временно менять его цвет, например на чёрный для отдельного +прохода. Полный выбор таких проходов ещё не воспроизведён. Цвет primary света солнца — RGB palette 12, нормированный делением на 255 и умноженный на trailing parameter 2. `SunFrame::direction` — нормированный diff --git a/docs/reference/environment-effects.md b/docs/reference/environment-effects.md index 799a969..2a17838 100644 --- a/docs/reference/environment-effects.md +++ b/docs/reference/environment-effects.md @@ -218,6 +218,41 @@ OneShot -> воспроизвести разовый звук молни отдельный spatial source, применяет DirectSound range/pan и текущий listener. Отсутствие устройства вывода не должно останавливать симуляцию. +### Звуки Control + +Команда Op2 может принадлежать не погоде, а экземпляру эффекта на узле модели. +`fparkan-fx::ambient_sound` хранит её временное окно и предыдущее значение +фазы. В режиме 0 разовый звук начинается при строгом пересечении начала окна: +`previous <= start && start < phase`, если фаза движется вперёд. Значение +`phase == 1` для этой проверки заменяется нулём и подготавливает следующий +цикл. Режим 2 включает loop на обеих границах окна; внутри окна уже работающий +источник обновляется, а не запускается заново. Выход за окно останавливает его. + +Позиция интерполируется между двумя локальными точками и проходит через ту же +матрицу владельца, что и графические команды. Минимальная и максимальная +дистанции остаются постоянными; частота интерполируется по фазе окна. +Каждая команда получает отдельный источник звука, даже если ресурс совпадает +с ресурсом соседней команды. Дождь сохраняет свой loop. Перемещение слушателя, +потеря фокуса и завершение приложения затрагивают все эти источники. + +Эти ветви восстановлены в GOG `Effect.dll` (сборка из +[таблицы бинарников](original-binaries.md)): init RVA `0x12D10`, update +`0x12EB0`. Живой loop `sounds.lib:f_defl.wav` передаёт в звуковой backend +диапазон `12/80`, частоту `1` и мировую позицию из матрицы владельца. +В режиме 2 признак loop берётся из первого DWORD body, а не из command flag. + +Обычное создание эффекта Control сохраняет time mode заголовка и задаёт +постоянную фазу нулём. Raw selector строки служит ID экземпляра, а не номером +режима времени. Поэтому `f_comp_sfx` с time mode 0 и началом окна `0.001` +остаётся тихим до управляющей команды Control. Preview такие команды ещё не +исполняет. Периодические звуки с time mode 2 уже следуют общим часам и текущей +позе узла. `render_coverage control_op2_supported` считает подготовленные +команды, а `control_sound_start/stop` показывает фактические переходы. + +Неизвестные backend modes 1/3, ненулевое поле body+76, неподдержанное время +или привязка владельца пропускаются с `render_coverage control_op2_unsupported`. +Они не заменяются разовым звуком или произвольным loop. + ## Цвет солнца Солнце берёт цвет спрайта и прозрачность бликов из `palette[10]` расписания @@ -236,11 +271,19 @@ packed: [key.colors[10], key.colors[14]], `AtmosphereFrame` пока не несёт этот бит обновления, поэтому sampler выдаёт интерполированный цвет при каждом запросе. -## Экранная геометрия солнца и луны +## Отрисовка солнца, луны и бликов + +`CSun::Render` получает текущий viewport, проецирует направление солнца или +луны и собирает quad уже в его пикселях. `Terrain.dll` VA `0x1004AA40` передаёт +эту геометрию через shade callback; native FVF `0x1c4` задаёт +`XYZRHW | DIFFUSE | SPECULAR | TEX1`. Конвертер `0x10020700` сохраняет входные +`x`, `y` и `z`, а в `RHW` записывает `1.0`; исходный `z` равен `0.999`. Callback +включает depth test и выключает запись depth, поэтому более близкая поверхность +может закрыть спрайт, а сам спрайт не меняет глубину для последующих слоёв. -`CSun::Render` сначала переводит направление небесного тела в пиксели текущего -viewport, затем собирает quad с native XYZRHW-позицией. При положительной высоте -viewport Vulkan пиксели переводятся в clip-space так: +В Vulkan ряды 3 и 4 сохраняют этот contract отдельным bit 8 material mode. +CPU каждый кадр преобразует viewport-пиксели к clip space текущего размера +swapchain: ```rust position = [ @@ -250,13 +293,17 @@ position = [ ]; ``` -Vertex shader пропускает `clip_from_world` для строк солнца и луны. CPU каждый -кадр пересчитывает центр и размер quad по текущему viewport; старые пиксельные -координаты не переносятся на новый размер. World-space billboards бликов -остаются отдельным путём. Материал экранного quad не освещается, vertex fog -отключён, а sky far-depth bit к нему не применяется. +Положительная высота Vulkan viewport отображает эти координаты в тот же +top-down экран. Vertex shader пропускает мировую матрицу только для этого +режима: + +```glsl +vec4 clip_position = screen_tl + ? vec4(in_position, 1.0) + : frame.clip_from_world * vec4(in_position, 1.0); +``` -Для мировой геометрии +Материал остаётся unlit, fog factor равен нулю. Для мировой геометрии оригинал задаёт viewport `MinZ = 0.1`, `MaxZ = 0.99`: после отсечения проекционная глубина отображается в этот диапазон: @@ -280,35 +327,42 @@ RVA `0x7170`, участок `0x71E7–0x723C`, копирует MinZ/MaxZ в re [таблице бинарников](original-binaries.md). Нативные значения этих двух полей имеют биты `0x3DCCCCCD` и `0x3F7D70A4`. - -## Геометрия визуальных FX-команд - -`fparkan-fx::fx_visual` формирует кадры для двух подтверждённых вариантов -`f_gener_ball`. Op4 принимает mode 0 с положительным знаменателем в `body+0xC4` -и выдаёт camera-facing quad; расстояние камеры считается через native -component-wise projection родительской матрицы. Op9 принимает mode 2: -селектор `body+0xC4` со значением 0, 1 или 2 отображается в native flag base -3, 5 или 6 и выбирает одну из трёх сеток; категория render flags выбирает её -детализацию. `body+0xC8` выбирает UV path: -нулевое значение проецирует позицию вершины на XY, ненулевое использует -фиксированные координаты quad и cap. Кадр сохраняет MAT0 resource, фазу, -opacity, вершины и triangle indices для renderer. - -Evaluator получает готовые command body, phase, camera point и внешнюю -матрицу. Выбор Control row, manager gates, owner attachment, header jitter и -global RNG остаются задачей владельца. `fparkan-game` пока использует для Op9 -категорию 0 с наибольшей детализацией; выбор категории по projected size ещё -не реализован. - -Для Op3 поддерживаются body-типы 0, 1 и 2. Поля отсчитываются от -`FxCommand.raw_body`, то есть после 4-байтового command word. Типы 0 и 1 -используют общие position/scale/phase/opacity tracks и camera-facing основу; -тип 1 и тип 2 также интерполируют ось, а тип 2 строит basis через native -D110-порядок. Все три evaluator-а принимают уже собранную матрицу владельца и -не выбирают Control row или attachment. Текущий путь для типов 0 и 1 -предполагает native render context 0: `Effect.dll` проверяет общий gate -`context + 0x60 < 8`, а API этих evaluator-ов не выбирает другой context. -Evaluator типа 2 принимает индекс context явно и учитывает этот gate в `active`. +Sky-background bit 4 продолжает помещать слои на дальнюю плоскость. Отдельные +flare rows 5 и 6 пока используют world-space billboard builder: для них не +подтверждён этот же native TL путь. + +## Эффекты Control + +Погодное расписание `sky.ske` — только один источник эффектов. У объектов +миссии таблица Control (`.ctl`) связывает строки с FXID-командами. Текущий +preview выбирает начальную группу Control и исполняет только строки без +predicate, chain и state gate. Условия игровых событий и переходы между +группами принадлежат полному игровому Control runtime и здесь не моделируются. + +Type 4 row задаёт локальную basis эффекта через три точки CTL point list. Для +валидной привязки первая точка также задаёт owner node. Путь MSH выбирает позу +этого узла из текущей pose hierarchy; для внешнего mounted-компонента к ней +добавляется поза socket. После этого применяются object transform, header +scale и локальная матрица FX. Подробнее о порядке матриц см. +[описание MSH](msh.md#control-fx-и-owner-transform). + +Renderer preview поддерживает FXID-команды Op3 modes 0–2, Op4, Op7, Op8 и Op9. +Op3 mode0 строит camera-facing поверхность из анимируемых локальных позиции и +масштаба; Op7/Op8 обновляют отдельные элементы particle pool; Op9 создаёт +индексированную полусферическую сетку. Op1 вычисляется отдельно как список +point lights: selector 1 без `flags & 0x80000000` освещает мир, а с этим битом — +собственное строение или юнит. Локальный источник нельзя удалять из освещения +MSH вместе с исключением из общего списка рельефа. Preview сохраняет исходные +flags для projected shadows и передаёт в renderer область действия корневого +объекта. Область ноль означает общий свет; у локального источника и диапазонов +его модели одинаковый ненулевой идентификатор. + +Body discriminator Op3 читается из первого DWORD `FxCommand.raw_body`; offsets +в evaluator-е относительны к данным после четырёхбайтового command word. Mode 1 +добавляет анимируемую ось к camera-facing basis, mode 2 использует native +D110 basis. Для modes 0 и 1 preview предполагает render context 0, который +проходит общий gate `Effect.dll` `0x106C0`; их API не выбирает другой context. +Evaluator mode 2 принимает индекс context и учитывает gate при расчёте `active`. У Op3 mode 1 ось задаёт длину прямоугольника, а не его нормаль. Для локального вектора к камере `v` и анимируемой оси `a` столбцы базиса имеют @@ -359,27 +413,88 @@ RVA `0x93D0`, ветка mode 1 с `0x9739`, сборка столбцов `0x97 семена отдельных экземпляров лишь детерминированно заменяют process-global состояние и не воспроизводят общий порядок вызовов. +`f_gener_ball` evaluator покрывает Op4 mode 0 с положительным знаменателем в +`body+0xC4` и Op9 mode 2. Для Op9 selectors `body+0xC4` со значениями 0, 1 и 2 +отображаются в native flag bases 3, 5 и 6. Нулевой `body+0xC8` проецирует UV на +XY, ненулевой выбирает статические quad/cap координаты. Op4 вычисляет локальное +расстояние камеры тем же component-wise projection, что использует native path. + +FX-команды одной Control-строки используют общие часы экземпляра +`fparkan_fx::clock::FxInstanceClock`. Время хранится в миллисекундах `f32`, +отдельно от длительности документа: mode 0 возвращает постоянную фазу +(при создании ноль), mode 1 — `(now − origin) / duration`, mode 2 повторяет +цикл, mode 3 возвращает `1 − (now − origin) / duration`. +Header flags `0x200` и `0x20` преобразуют незажатую фазу; command path +`0x8120` зажимает результат только после преобразований. +Вызов `set_time` меняет начало отсчёта и режим экземпляра, сохраняя режим +документа. Запрос перезапуска (`Effect.dll` RVA `0x7B40`, instance bit `0x20`) +срабатывает после конца текущего времени жизни: следующий update начинает +новый цикл в своём времени и возвращает режим документа. Это отдельный флаг, +не header `0x20`, складывающий фазу треугольником. Правила режимов, gates +`0x80/0x100` и установления времени сверены вызовом эталонной DLL с x87 CW +`0x007F`; отложенный переход закреплён Rust-тестом по ветви update `0x8120`. +Все экземпляры статического preview пока создаются в момент ноль; геометрия, +свет и звук получают одну фазу на обновление сцены. Команды Control ещё не +меняют эти часы, а нативная очередь обновления с шагом 50 ms не моделируется. +В [эталонной Effect.dll](original-binaries.md) RVA `0x5C60` режимы 4–17 +читают состояние владельца или провайдера; режим 14 тоже входит в эту группу. +Пропуски при расширении выбранных начальных Control-строк учитываются по +командам: `control_fx_requires_game_state` для этих режимов и +`control_fx_unsupported` для остальных непреобразованных команд. Диагностика +сохраняет причину, ресурс, объект, компонент, строку и номер команды. +`control_fx_invalid_documents` отдельно считает неразобранные FXID, где число +команд неизвестно. Счётчики суммируются по компонентам миссии; они не заменяют +учёт ещё не выбранных Control-групп и условных строк в фазе G. +Effect renderer проверяет header bit `0x800` только при +render selector 0; native AniMesh component path был захвачен с selector 1, +поэтому preview использует selector 1 и не подавляет такие visual commands. +Этот capture также показал `0xD00` FX entries в 53-entry manager. Динамическое +состояние manager `A8` пока заменено нулём; экземпляры с gates `0x80/0x100` +считаются в `control_fx_manager_flag_stub`. Header bit `0x10` относится к activation lifecycle и сам по себе +не отключает draw. +Header jitter и random shift хранятся один раз на Control row и совместно +применяются к его sibling-командам; отдельный детерминированный поток каждой +строки сохраняет эту связь, но не повторяет порядок глобального native RNG. +Неразобранные группы, event predicates и неразрешённые owner attachments +пропускаются. + +Для visual FX material берётся из resource reference команды. MAT0 выбирает +фазу по normalized FX phase, независимо от wall-clock анимации world materials. +Command opacity передаётся vertex alpha, а FX MAT0 alpha не умножает её второй +раз; texture alpha продолжает участвовать в shader. Текущая preview-ветка +использует SourceAlpha и read-only depth state и допускает только MAT0-фазы, +проходящие локальный predicate additive-RGB approximation. Семантика native +`flag40` и directional-light ветки остаётся открытой. + +Generated Op9 mesh разделяется на отдельный draw range для каждого native +quad и cap. Каждый range получает ключ по камере до первой индексированной +вершины, в порядке убывания расстояния. Равные ключи используют midpoint +insertion из native queue, поэтому их порядок не является стабильным по +первоначальному времени поступления. Preview использует category 0 как +highest-detail mesh; projected-size LOD и полное визуальное совпадение размера +и формы пока не реализованы. + ## Частицы Op8 `fparkan-fx::smoke` разбирает body Op8, хранит кольцевой пул команды и выдаёт quad-ы с ориентацией к камере для активных слотов. Нативная нормализованная -фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего события; -сравнение строгое (`schedule_base + interval < update_clock`). Все события -catch-up в одном обновлении проверяются по тому же нормализованному интервалу; -после события нативный код продвигает базу расписания интервалом, вычисленным -по сырой фазе эффекта, а возраст слота считает по нормализованному интервалу. -Когда фаза равна единственной границе окна, деление `0/0` даёт NaN, который -нативный unordered-путь сравнения ограничивает нулём. +фаза окна ограничивается диапазоном 0–1 и задаёт интервал проверки следующего +события; сравнение строгое (`schedule_base + interval < update_clock`). Все +события catch-up в одном обновлении проверяются по тому же нормализованному +интервалу; после события нативный код продвигает базу расписания интервалом, +вычисленным по сырой фазе эффекта, а возраст слота считает по нормализованному +интервалу. Когда фаза равна единственной границе окна, деление `0/0` даёт NaN, +который нативный unordered-путь сравнения ограничивает нулём. Нулевая база расписания служит признаком начального состояния: обновление сохраняет текущую опору эмиттера и базу времени, не создавая частицу. Если первое время равно нулю, следующее обновление повторно захватывает опору. При событии скорость опоры считается от сохранённой позиции до текущей за время от базы расписания; слот смещает сохранённую опору на `скорость * due_interval`. -Обновление без событий не переносит сохранённую опору к текущему владельцу. -Откат часов корректирует только базу расписания до текущего времени: слоты, -опора и три потока случайных -чисел не сбрасываются. +При нулевой скорости оригинал подставляет ось X владельца и для направления, +и для этого смещения. Обновление без событий не переносит сохранённую опору к +текущему владельцу. Откат часов корректирует только базу расписания до текущего +времени: слоты, опора и три потока случайных чисел не сбрасываются. Evaluator принимает отдельные начальные состояния для эмиттера, знаковых отклонений и материала. Переход каждого потока совпадает с общим native LFSR, diff --git a/docs/reference/msh.md b/docs/reference/msh.md index 0a8a2cb..873cae1 100644 --- a/docs/reference/msh.md +++ b/docs/reference/msh.md @@ -28,7 +28,10 @@ type 20 rare auxiliary table Reader ищет entries по type, но сохраняет исходный порядок для roundtrip. Поле `ModelAsset.type16_present` отмечает только наличие entry типа 16. Данные payload сохраняются, но этот путь рендера пока не преобразует их в цвета -вершин. +вершин; приложение считает затронутые диапазоны в `msh_type16_colors_unsupported`. +В статических сценах всех 29 миссий GOG ни одна из 1736 загрузок моделей +не содержит этого потока. Это ограничение проверенного набора ресурсов; +неиспользуемые модели и будущие игровые состояния сюда не входят. ## Node and slot selection @@ -95,6 +98,51 @@ translation суммируется, а rotations умножаются; послÐ запускает игровой Control/AI и не обещает полного совпадения с runtime или всеми x87 rounding modes. +## Control clock и pose компонентов + +Статический Vulkan preview начинает с позы, вычисленной из сохранённых `.ctl` +bindings. Пока не задан `--static-animation-frame`, при каждом новом сэмпле +сцены приложение сэмплирует поддержанные tag 21 записи для каждого экземпляра +компонента и заново строит его MSH node hierarchy; time и transition используют +общий runtime clock. Динамический sampler принимает tag 21 mode `0`, +controller flags `5` (raw value `0xFFFFFFFF` трактуется как `5`), +разрешённый gate provider и static row mapping: оба +variable index равны `0xFFFFFFFF`, previous-row flag не установлен, transition +rate конечен и положителен. + +Fallback к статической CTLD-позе всего экземпляра компонента срабатывает только +при следующих ошибках sampler: `UnsupportedTag21Mode`, +`UnsupportedVariableMapping`, `MissingGateProvider`, +`GateProviderKeyOutOfRange`, `MissingGateProviderValue` и +`GateProviderCountMismatch`. Он заменяет весь динамический результат экземпляра, +а не одну запись. Остальные sampling errors завершают обновление сцены; например, +`UnsupportedPreviousRowMapping` и `InvalidTransitionRate` не включают fallback. + +`--control-clock-ms <u32>` воспроизводит общий MSH/FX clock линейно: первый из +`--frames` успешно представленных кадров получает `0`, последний — заданное +время. Replay clock привязан к числу представленных кадров: повторная подготовка +того же кадра, включая swapchain recreation, использует прежний sample. +`--static-animation-frame <u16>` отключает динамическую MSH выборку и задаёт один +frame всем компонентам. Этот override не включает AI и не превращает preview +в полный игровой Control runtime. + +## Control FX и owner transform + +Для выбранной Control effect group runtime берёт строки без predicate, chain и +state gate. Type 4 row offsets выбирают три точки из CTL point list; позиции +точек задают локальное начало, а их направления — локальную basis. К basis +применяются FX header scale и локальные random shifts. Затем матрица проходит +через mission rotation/translation и object scale, optional mounted-component +pose, owner-node pose и локальную матрицу эффекта. Поэтому FX mounted-компонента +следует её текущему MSH owner pose, а не остаётся привязанным к mission root. + +Когда point record задаёт допустимый owner node, его pose разрешается из MSH +hierarchy. Для mounted external components node pose вычисляется относительно +component root и применяется вместе с уже рассчитанным mount pose. Internal +`INTO` child controller/equipment links пока не интерпретируются как MSH socket; +если owner pose нельзя разрешить, его FX пропускается. Нативное сочетание child +component scale, equipment attachment и CPT basis остаётся неустановленным. + ## Vertex streams Основные vertex streams имеют фиксированный source index. У type 3 `attr1` diff --git a/docs/reference/render-frame.md b/docs/reference/render-frame.md index 10de209..8bd548f 100644 --- a/docs/reference/render-frame.md +++ b/docs/reference/render-frame.md @@ -6,6 +6,11 @@ ## Порядок кадра +Сам рельеф образует полноценную графическую сцену: список размещённых объектов +может быть пустым. Наличие MSH-компонентов объектов не является условием +отрисовки карты. Проверочные TMA без объектов позволяют отдельно загружать +карты ресурсов, для которых в установленной игре нет штатной миссии. + ```text системные сообщения и ввод -> расчёт симуляции @@ -24,22 +29,119 @@ затенения, вызывается завершение рендерера, снимается `in_render`, затем рассылается `end-of-render`. -При retry кадра после пересоздания swapchain сохраняется прежний pending -sample, включая уже вычисленные позы и состояния FX и материалов. Для нового -drawable обновляются aspect, проекция и геометрия, зависящая от viewport; pose -берётся из pending camera. Новые события управления камерой остаются в -input controller и применяются следующим sample. Экранные billboards погоды -повторно проецируются из закэшированных мировых головы и хвоста, без второго -шага симуляции окружения и повторной отправки звуковых событий. Пауза нулевого -drawable не добавляет время. - -Pending frame хранит пакет MAT0 для мира и, при наличии GPU-окружения, отдельный -пакет неба. Пакеты содержат выбранные селектор и категорию конвейера, фазовые -коэффициенты материала, четыре UV-преобразования и кортеж фаз дополнительных -текстур. Повторное применение пакета при retry или нескольких resize не выбирает -фазу заново и не продвигает RNG. Перед отрисовкой сначала применяются материалы -мира и их порядок прозрачности, затем MAT0 неба, после чего viewport-зависимый -проход неба записывает свои переопределения. +В интерактивном preview часы материалов и окружения прибавляют полное время +между обновлениями. Ограничение шага в 100 мс остаётся у движения свободной +камеры и не обрезает эти часы. В режиме replay окружение получает разницу +последних применённых значений общего control clock. + +Если swapchain просит повторить ещё не представленный кадр, preview сохраняет +его общий clock, уже вычисленные позы, FX/RNG и фазы материалов. При изменении +drawable обновляются камера, aspect и viewport, после чего sky, environment +geometry и projected shadows пересобираются для нового размера. +Положение и ориентация камеры берутся из сохранённого состояния +ожидающего кадра; новый ввод мыши применяется при следующем сэмпле. Сэмпл +погоды остаётся прежним: экранные billboards дождя и снега повторно +проецируются из закэшированных мировых головы и хвоста, без второго шага +симуляции окружения или повторной отправки звуковых событий. Следующий такт +выбирается после успешного представления кадра; пауза нулевого drawable не +добавляет время. + +## Прозрачность порталов MSH + +GOG `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`), +RVA `0x1B0F0`, заменяет diffuse alpha пакета без type 16 с batch bit `0x0008`. +При `0x0010` аргумент кривой — `sqrt(distance)`, границы — `2.6 / fov` и +`7.7999997 / fov`, наклон — `fov / (7.7999997 - 2.6)`. Без этого бита +`0x0040` задаёт ноль; иначе границы — `75 * 1.3 / fov`, `95 * 1.3 / fov`, +наклон — `fov / (95 * 1.3 - 75 * 1.3)`. До ближней границы alpha равна нулю, +после дальней — единице. Внутри перехода alpha равна произведению смещения от +ближней границы на наклон. Это замена alpha MAT0, а не дополнительный множитель. + +Метод расстояния `0x246A0` через `0x247B0` читает первую индексированную +вершину пакета, преобразует её в мир и вычисляет квадрат расстояния до камеры. +Текущий Ngi32 dispatch `FastProc+0x2C` указывает на RVA `0x1D6D0`: +`RSQRTSS` возвращает приближение обратного корня, которое умножается на квадрат +расстояния. Получается приближённое евклидово расстояние; дополнительный `FSQRT` +ветви `0x0010` выполняется уже над ним. Хеш Ngi32 — +`bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5`. +Скаляр камеры в шестом float getter `0x4F5F0` и поле CShade `+0xBB4` +проверены в работающем оригинале: оба содержат текущий горизонтальный FOV. + +`legacy_msh_portal_alpha` хранит кривую в `fparkan-render`. Её опорная вершина +принадлежит входному пакету: выбирать вместо неё первую видимую точку нельзя. +В живом краевом ракурсе AutoDemo первая вершина окна находится за правой +границей viewport (`NDC x ≈ 1,048`), а следующая — внутри (`x ≈ 0,815`). +Оригинал всё равно использует первую: расстояние `106,74783` даёт alpha +`0,51991343`. Выбор следующей изменил бы alpha примерно на `0,026`. +Передача пакета renderer-у происходит после записи этой alpha. + +Адаптер изолирует инструкцию `RSQRTSS`; на x86-64 расстояние и alpha этого +ракурса совпадают с оригиналом побитно. Обычный `sqrt` давал alpha +`0,5196797`: при упаковке прозрачности этого достаточно для отличия отдельных +пикселей. Прежнее предположение о выборе вершины после clipping и связанный +счётчик `portal_fade_packet_selection_approx` удалены. На других архитектурах +сохраняется переносимый корень с явным `packet_distance_native_rsqrt_unsupported`; +совпадение камеры с вершиной использует нулевую дистанцию и +`packet_zero_distance_fallback` вместо native `0 × infinity`. +Поток type 16 ещё не декодирован. Проверка краевого окна не закрывает остальные +внутренние ракурсы завода. + +## Дистанционная кривая тумана + +Программный `CShade::ShadeIndexedStrided` в GOG Terrain.dll сравнивает квадрат +дистанции с квадратами ближней и дальней границ. По RVA `0x2EEED–0x2EF45` +видимость равна `1 - (distance² - near²) / (far² - near²)` между границами, +единице перед ближней и нулю за дальней. Такой же закон использует отдельный +путь цвета пакета `0x2F230`. Линейная интерполяция по самой дистанции давала +слишком плотный туман в середине диапазона. + +В работающем оригинале границы 0 и `0x442389E6`, квадрат дистанции +`0x47707B04` дали видимость `0x3F5B2B94`. Rust-тест проверяет передаваемые GPU +квадраты границ по этому наблюдению. GLSL вычисляет квадрат расстояния вершины, +после чего растеризатор интерполирует коэффициент по треугольнику. +`native_fog.effective.sink_rgb` совпадает с `sky.packed[1]`, нижней границей +освещения CShade. Обычный GPU fog color подтверждён отдельно: `CSky::Render` +по RVA `0x48341` задаёт renderer default из цвета горизонта. В живом замере +`0xFF404064` дошёл до `IDirect3DDevice7::SetRenderState(34)` в том же игровом +тике. В другом ракурсе палитра `[0xFFA8A5CE, 0xFFABA9D5, 0xFFBAA9D5, +0xFFABA9D5]` дала default `0xFFA9A7D1`. Это не покрывает временные изменения +цвета отдельных проходов: например, native render path устанавливает чёрный +цвет и затем восстанавливает сохранённый. +Выбор проходов по native shade-state пока приближён общим туманом мира и +считается как `render_coverage fog_pass_eligibility_approx`. + +## Радиус Control Op1 + +GOG `Effect.dll` (SHA-256 +`386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789`), +RVA `0xF6E0`, преобразует направление текущей матрицей, сохраняет его длину +перед нормализацией и умножает на неё интерполированный локальный радиус. +Умножение видно по RVA `0xFB13`; перед LightManager `+0x10` неположительный +результат заменяется на `0.01`. В живом «Конструкторе» для синего источника +завода локальный радиус 7 и длина `0x409B1F91` дали радиус `0x4207BB9F`. +Это же значение проверяется Rust-тестом общего Op1 evaluator. + +Случайные добавки цвета и радиуса ещё не воспроизводятся; приложение сообщает +число таких Control-команд как `control_op1_jitter_unsupported` и использует +их авторские значения. У синего источника в проверенном захвате добавки нулевые. + +Флаг источника `0x80000000` отделяет локальный свет объекта от общего света +мира. В эталонной `Terrain.dll` (SHA-256 +`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead`) +`CLightManager` RVA `0x4C1D0` ведёт отдельные числа всех активных и общих +источников; второй счётчик исключает этот бит. Живой вход `CShade` RVA +`0x2E4A0` подтверждает различие: завод получает жёлтый источник с flags +`0xA0000000`, а рельеф — только общие источники. Для выбранной вершины крыши +native diffuse `1,205386/1,095748/1,893271` и specular после переноса переполнения +`0,160597/0,147096/0,358346` воспроизводятся формулой shader с теми же входами +с отклонением менее `0,0002`. Менять формулу блика для этого дефекта не нужно. + +В Vulkan каждый свет хранит `owner_scope` в последнем DWORD 48-байтовой +SSBO-записи; область диапазона передаётся через `firstInstance`. Нулевая +область света доступна всем диапазонам, ненулевая — только совпадающему +владельцу. Terrain и геометрия без владельца имеют область ноль. Один общий +буфер сохраняется; отдельные наборы света для каждого строения не создаются. ## Элемент отрисовки @@ -69,21 +171,16 @@ Pending frame хранит пакет MAT0 для мира и, при налич глубину. За фиксированными диапазонами окружения сохраняются их явные состояния. Для MSH без потока цветов type 16 диапазон, цвет которого вычисляет CPU, при -raw-флаге `Batch20` `0x0008` получает переопределение alpha только в одном -подтверждённом случае. В GOG `Terrain.dll` с SHA-256 -`af87d1b2e728a0be73c52be3b44cc196ab46da7799f25a15d40f8c9b0b425ead` проверка -по RVA `0x1B0F0` задаёт alpha `0`, когда установлен bit `0x0040` и сброшен -`0x0010`; поэтому для `0x0148` alpha равна нулю. Для `0x0108` и `0x0118` -превью сохраняет alpha MAT0; для `0x0058` действует приоритет bit `0x0010`, -также оставляя alpha MAT0. При наличии type 16 или отсутствии `0x0008` -переопределения нет. Оно относится к одному draw range и не меняет общий +raw-флаге `Batch20` `0x0008` получает переопределение alpha по +[дистанционной кривой портала](#прозрачность-порталов-msh). Bit `0x0010` +имеет приоритет; иначе bit `0x0040` задаёт нулевую alpha, а оставшаяся +ветвь использует границы 75/95 с поправкой на FOV. При наличии type 16 или +отсутствии `0x0008` переопределения нет. Оно относится к одному draw range и не меняет общий материал; alpha `0` временно задаёт число индексов `0`, а при очистке переопределения исходное число индексов восстанавливается. -Общий fade по расстоянию пока не реализован. Открыты точная метрика расстояния, -выбор первого CPU-принятого треугольника диапазона и эквивалентность frustum- и -culling-отбору оригинала. Поэтому состояния `0x0108` и `0x0118` оставляют alpha -MAT0, пока эти входные данные не установлены. +Fade обновляется с камерой каждый кадр и сохраняет исходную опорную вершину +диапазона, в том числе когда часть окна выходит за границу экрана. В превью с фиксированной камерой диапазоны туманности и звёзд, купола и облаков явно помечаются как прозрачный фон. Непрозрачные диапазоны мира остаются на @@ -117,6 +214,10 @@ RVA `0x13D90`. Эти данные подтверждают только порÑ путь превью принимает входные данные с матрицей `selector0_words`, полученные через [процедуру захвата нативного кадра](../../tools/native-frame-capture/README.md). +Формат захвата сохраняет локальную матрицу `selector0_words` и, когда она +доступна, записывает `effective_world_matrix_words` с маркером пространства +`world`. + Процедура захвата записывает наблюдавшийся путь миссии и значение `atmosphere_seconds`, если проверка потока, поколения и часов прошла успешно. В превью с фиксированной камерой эта абсолютная фаза расписания используется, @@ -144,6 +245,22 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std `REFLECTION_SHIFTED`. Это свидетельство ABI фабрики, а не восстановленный контракт матрицы вида или проекции. +## Представление готового кадра + +Vulkan сохраняет явно выбранный режим, если поверхность его поддерживает. +Приложение предпочитает MAILBOX: готовый новый кадр заменяет ожидающий, +а показ остаётся привязан к развёртке. При отсутствии MAILBOX выбирается +IMMEDIATE, затем FIFO. IMMEDIATE допускает разрывы изображения, зато не +навязывает ожидание вертикальной развёртки; FIFO остаётся запасным режимом. +Семантика режимов описана в [спецификации Vulkan](https://docs.vulkan.org/refpages/latest/refpages/source/VkPresentModeKHR.html). + +Оконная ветка GOG Ngi32.dll (SHA-256 из [таблицы модулей](original-binaries.md)) +по RVA `0x6E1B` вызывает `IDirectDrawSurface7::Blt` с `DDBLT_WAIT`. +Эта ветка наблюдалась при нативных захватах. Флаг ждёт готовности блиттера, +а не задаёт интервал развёртки — см. [описание Microsoft](https://learn.microsoft.com/en-us/windows/win32/api/ddraw/nf-ddraw-idirectdrawsurface7-blt). +Современный композитор и драйвер всё равно могут влиять на фактический показ; +это правило выбора API, а не гарантия одинаковой задержки двух движков. + ## Предпросмотр по захвату камеры D3D7 `fparkan-game --legacy-camera-capture <json>` принимает матрицу камеры @@ -156,6 +273,17 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std `right - left` на `bottom - top`. Размер окна превью закреплён: если при запуске или после ненулевого изменения окна физический размер не совпадает с заданным, приложение завершает работу, а не растягивает изображение. + +Проекция во время рисования может отличаться от восстановленного состояния +камеры после обхода мира. В GOG у `CStridedPrimitive::RenderVB` (Terrain RVA +`0x1D110`) renderer содержит `near = 0.5`, а на границе World3D RVA `0x13CE4` +уже `near = 5`. Поэтому захват отдельно сохраняет `geometry_projection` при +первом вызове геометрии с той же мировой матрицей камеры. Поток, поколение, +renderer, viewport, far и FOV сверяются с последующей границей; только +подтверждённая запись используется при воспроизведении. Иначе пол в трёх +единицах от камеры может исчезнуть из-за чужой ближней плоскости. Старые JSON +без этой записи сохраняют прежнее поведение с явным счётчиком +`captured_projection_unverified`; их нужно переснять для проверки близких поверхностей. Сравнивайте результат с фрагментом исходного PNG по прямоугольнику `[left, top, right, bottom]`. В этом режиме `FreeFlightCamera` не создаётся, но при повторной отрисовке обновляются окружение и проецируемые тени для @@ -163,14 +291,26 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std события, слушателю задаются захваченные позиция, направление вперёд и верхняя ось; начальная сортировка диапазонов мира использует ту же позицию. +Если в JSON присутствует пара `effective_world_matrix_space: "world"` и +`effective_world_matrix_words`, превью использует мировую матрицу. Матрица +`selector0_words` остаётся локальной и используется как запасной вариант только +при отсутствии мировых полей, если `parented_camera` не равно `true`. Для +родительской камеры без эффективной мировой матрицы вход отклоняется. + Захват может содержать наблюдавшийся путь миссии и проверенное значение `atmosphere_seconds`; последнее используется только при отсутствии явного параметра командной строки. Это значение фиксирует фазу расписания атмосферы -при сравнении с неподвижной камерой, тогда как FX продолжает получать реальное +при сравнении с неподвижной камерой, тогда как FX по умолчанию получает реальное время кадра. `simulation_time_seconds`, погода, RNG и полное состояние миссии остаются неизвестными, поэтому совпадение камеры и фазы не подтверждает визуального совпадения с нативным кадром. +Параметр `--control-clock-ms` воспроизводит единые часы MSH/FX от нуля до +выбранного значения в миллисекундах за указанное число кадров. Без этого +параметра часы идут в реальном времени. Для `--readback-sequence-out` этот +управляемый режим обязателен. Он не восстанавливает +`simulation_time_seconds`, погоду, RNG или полное состояние миссии. + Для неба превью вычисляет направление как `atan2(forward.x, forward.y)`. Оно совпадает со вторым выходным значением, вычисляемым в `Terrain.dll` по RVA `0x4F9C0`; при выборе сектора экранного градиента `CSky` добавляет π по RVA @@ -183,15 +323,9 @@ RVA `0x4EBE0` использует соглашение о вызовах `__std `1.3` рад — параметры, наблюдавшиеся в проверенном контексте GOG Ngi32 ([формула native projection](../tomes/05-render.md#камера-и-размещение-в-оригинале)). Для отношения ширины к высоте `a` превью вычисляет вертикальный угол -```rust -fn vertical_fov(self, aspect: f32) -> f32 { - 2.0 * ((self.horizontal_fov * 0.5).tan() / aspect).atan() -} -``` - -Код заменяет невалидный `aspect` на отношение 16:9; горизонтальный угол -остаётся `1.3` рад после изменения размера окна, а новый viewport и матрица -камеры применяются при следующей отрисовке. +`2 * atan(tan(horizontal_fov / 2) / a)`. Поэтому горизонтальный угол остаётся +`1.3` рад после изменения размера окна; новый viewport и матрица камеры +применяются при следующей отрисовке. Проекция направления не сравнивает косинус угла с ближней дистанцией: она отбрасывает только лучи за камерой и почти параллельные плоскости изображения @@ -254,6 +388,17 @@ D3D7 в `fparkan-render`, а затем один раз меняет знак в - переход на запасной набор возможностей устройства; - квантование времени симуляции. -Для отладки требуется детерминированный захват кадра: состояние камеры, -идентификаторы видимых объектов, список элементов отрисовки, ключи конвейера, -матрицы и хэши промежуточных буферов. +Параметр `--readback-sequence-out` сохраняет необработанные байты каждого +отправленного кадра; в имя файла записываются индекс кадра, значение общих +управляемых часов и размер изображения. Байты соответствуют выбранному формату +изображения swapchain Vulkan и не гарантируют порядок каналов R, G, B, A. Числовое +формат выводится как `readback_format`; расширение `.rgba` само по себе порядок +каналов не определяет. Последовательность не содержит структурированных ID +видимых объектов, списка элементов отрисовки, ключей конвейера, матриц или +хэшей промежуточных буферов. + +При преобразовании кадра в PNG сохраняется исходный RGB, а прозрачность +выключается: окно показывает готовое непрозрачное изображение. Альфа-канал +GPU может содержать результат смешивания поверхностей; повторное применение +его к готовому цвету затемнило бы кадр ещё раз. Поэтому конвертер и сборщик +сравнительных пар игнорируют эту альфу, а raw-файл сохраняют без изменений. diff --git a/docs/tomes/05-render.md b/docs/tomes/05-render.md index ef7dc52..1329268 100644 --- a/docs/tomes/05-render.md +++ b/docs/tomes/05-render.md @@ -362,30 +362,14 @@ local.set_translation(pose.position); world[n] = world[parent(n)] * local; ``` -Статический Vulkan preview берёт CTLD `.ctl` из явной ссылки прототипа. -Поддерживаемые Control timeline с tag 21 сэмплируются при запуске на native -clock `0`; разрешённые NDP providers передают конечные type-1 значения. -Неподдерживаемый режим или variable mapping, а также отсутствующее или -неразрешённое gate value оставляют компонент на статических CTLD bindings. В -timeline участвуют только включённые строки; при повторном node ID поздняя -строка заменяет раннюю. - -Без `--static-animation-frame` preview сохраняет экземпляр Control и при -каждом redraw сэмплирует поддерживаемый timeline на текущем native clock. -Обновлённая Node38-поза перепроецирует видимую геометрию в заранее выделенный -vertex span, оставляя индексы и draw ranges неизменными. Если в пути Unit DAT -компонента есть такой Control, заново вычисляется его EXTO mount socket и -перепроецируется дочерняя геометрия; дочерний mount использует preorder-порядок -Unit DAT. Внутренние INTO-компоненты здесь не считаются глобальными MSH -socket mounts. Обновлённые вершины и shadow-треугольники попадают в общий -mesh upload перед следующим кадром. Culling sphere восстанавливается от -начальной native sphere и расширяется до текущих shadow-треугольников; это -`shortcut:`, потому что native обновление bounding sphere для анимированной -геометрии пока не установлено. Это путь сэмплирования Control-позы, не AI или -анимация эффектов. - -Явный `--static-animation-frame N` переопределяет timeline и задаёт `N` всем -узлам, пропуская runtime Control-сэмпл. +Статический Vulkan preview сначала строит начальную позу по CTLD `.ctl`. +В ней участвуют только включённые строки; при повторе node ID поздняя строка +заменяет раннюю, а узлы без настройки остаются на кадре `0`. Без +`--static-animation-frame` preview сэмплирует поддержанные Control timelines +при каждом новом сэмпле сцены. Флаг `--static-animation-frame N` задаёт `N` +всем узлам и пропускает runtime Control sample. Это визуальный preview позы, +а не полный Control/AI runtime. + MSH sampler выбирает type 19 frame округлением `(time - 0.5)` к ближайшему целому с ties-to-even, затем интерполирует выбранный type 8 key и следующий key в исходном `time`; это portable-вариант native x87 conversion в default @@ -515,6 +499,35 @@ Material manager кэширует разобранный MAT0 и texture handles лучше вычислять на экземпляр материала, если random offset или локальное время различаются между объектами; immutable phase data остаются общими. +### Почему проём становится прозрачным вблизи + +Портал здания — поверхность, которая закрывает интерьер издалека и постепенно +исчезает при приближении камеры. Если оставить ей постоянную непрозрачность, +вместо окна или перехода получится тёмный прямоугольник. Прозрачность материала +здесь заменяется значением, вычисленным для всего пакета треугольников; alpha +текстуры продолжает участвовать в смешивании. + +Для MSH без потока цветов type 16 правило включает флаг batch `0x0008`. +В обычной ветви при горизонтальном FOV `1.3` поверхность прозрачна до расстояния +75 и непрозрачна после 95; между ними alpha растёт линейно. При другом FOV обе +границы умножаются на `1.3 / FOV`. Расстояние берётся до первой индексированной +вершины входного пакета, а не до центра здания. Эта точка может находиться за +экраном: видимая часть окна всё равно сохраняет её прозрачность. Если при повороте +камеры выбирать новую видимую точку, окно начнёт менять прозрачность без движения +игрока. + +Флаг `0x0010` выбирает другую кривую: сравнивается квадратный корень уже +вычисленного расстояния, границы равны `2.6 / FOV` и `7.7999997 / FOV`. +Например, при FOV `1.04` поверхность на расстоянии 25 имеет alpha примерно +`0.5`, а на расстоянии 5 полностью исчезает. Флаг `0x0040` без `0x0010` +сразу задаёт нулевую alpha; при наличии обоих побеждает `0x0010`. + +FParkan пересчитывает эту alpha при каждом обновлении камеры, после анимации +модели. Нулевое значение временно выключает диапазон индексов; при удалении +камеры диапазон восстанавливается. Расстояние использует тот же приближённый +обратный корень, что и оригинал: округление влияет на записанную alpha. +Подробности вычисления приведены в [описании кадра](../reference/render-frame.md#прозрачность-порталов-msh). + ## Texm: текстуры, mip-уровни и атласы `Texm` -- основной формат изображений. Он хранится в `Textures.lib`, @@ -646,15 +659,15 @@ diffuse texture ломает LOD, atlas coordinates и динамическую Vulkan preview строит проецируемые тени для объектов, отбрасывающих тени, отбрасывает сферы вне шести clip-плоскостей с радиусом `sphere + 100` и сохраняет порядок видимых кандидатов; frame builder обрабатывает не более 20 -объектов. Текущая реализация выделяет страницы в atlas `256 x 256` с 28 page -slots и использует одно сглаживание. Preview применяет native defaults -`RobShadowsOn = true` и `BldShadowsOn = false`: объект kind `4` проверяет -первый флаг, остальные kinds — второй. Поэтому роботы BTLU остаются объектами, -отбрасывающими тени, а объекты FORT и STAT их не создают. Preview пока не -загружает `shade.cfg`, поэтому значения из пользовательского файла не меняют -список объектов, отбрасывающих тени. Проверка действует только на проецируемые -тени: отдельный список объектов для затенения солнца по-прежнему включает -FORT, BTLU и STAT. Вызов preview передаёт `fade = 1.0` и LOD defaults: +объектов, отбрасывающих тени. Текущая реализация выделяет страницы в atlas +`256 x 256` с 28 page slots и использует одно сглаживание. Preview применяет +native defaults `RobShadowsOn = true` и `BldShadowsOn = false`: объект kind `4` +проверяет первый флаг, остальные kinds — второй. Поэтому роботы BTLU остаются +объектами, отбрасывающими тени, а объекты FORT и STAT их не создают. Preview +пока не загружает `shade.cfg`, поэтому значения из пользовательского файла не +меняют список объектов, отбрасывающих тени. Проверка действует только на +проецируемые тени: отдельный список объектов для затенения солнца по-прежнему +включает FORT, BTLU и STAT. Вызов preview передаёт `fade = 1.0` и LOD defaults: minimum LOD `1` и detail multiplier `1.0` отдельно для зданий и роботов. Проекция учитывает вклад directional и point lights. @@ -727,6 +740,19 @@ struct FxHeader60 { локальную точку опоры, scale -- базовый масштаб экземпляра. Unknown flags и settings ID сохраняются. +У каждого экземпляра эффекта есть своё начало отсчёта. Например, эффект +длительностью четыре секунды, созданный на 135-й секунде, на 137-й находится +в середине цикла: `(137 − 135) / 4 = 0,5`. Режим может идти вперёд, +повторяться, идти назад или сохранять постоянную фазу. Вызывающий код может +сменить режим экземпляра, не меняя документ. Запрос перезапуска ждёт конца +текущего цикла и начинает новый при следующем обновлении. + +Эти правила реализованы в `fparkan-fx::clock`. Приложение передаёт одну фазу +геометрии, свету и звуку одной Control-строки; повторная отправка кадра не +пересчитывает её и не расходует случайные числа повторно. В статической сцене +все экземпляры пока рождаются в момент ноль. Управляющие команды Control и +очередь обновления с шагом 50 миллисекунд остаются задачами игрового цикла. + Каждая команда начинается с `uint32_t command_word`: ```text @@ -823,6 +849,64 @@ emit не должен повторно менять игровое состоя типизировать header и resource references, а body редких commands сохранять raw до подтверждения field-level semantics. +В `fparkan-game` выбранные Control FX проходят отдельный transparent path. +Начальная CTL group запускает только строки без predicates, chain или state +gate. Type 4 point list задаёт локальную матрицу; текущие MSH owner и mounted +node poses входят в transform до локальной FX basis. Реализованные visuals +охватывают Op3 modes 0–2, Op4, Op7/8 particle pools и Op9 generated mesh. Op1 +selector 1 добавляет point lights в освещение. Native flag `0x80000000` +ограничивает источник собственным объектом: такая лампа освещает завод, но +не рельеф вокруг него. Это не запрет на обычный расчёт света. Потеря локального +жёлтого источника над трубой делала крышу завода заметно холоднее оригинала. +Renderer сопоставляет владельца лампы и диапазона MSH; общие источники +доступны всем поверхностям. Исходные flags сохраняются также для projected +shadows. Радиус источника масштабируется длиной его преобразованного +направления до нормализации. Например, локальный радиус 7 при масштабе узла +около 4,85 освещает область радиусом около 34. Потеря этого множителя оставляла +синий свет завода почти незаметным. Неположительный итог заменяется на 0,01. +Случайные добавки к цвету и радиусу пока явно считаются в +`render_coverage control_op1_jitter_unsupported`. + +Эффект может одновременно рисовать поверхность и издавать звук. Для Op2 +приложение создаёт отдельный звуковой источник у каждого экземпляра команды: +несколько одинаковых механизмов слышны одновременно и не заменяют дождь. +Позиция следует анимируемому узлу, а фаза включает, обновляет и останавливает +loop. Обычная строка Control создаёт экземпляр с нулевой постоянной фазой; +звуки с таким режимом ожидают отдельной управляющей команды. Периодические +звуки работают по часам уже в preview. Устройство вывода остаётся в приложении, +а правила переходов и параметров — в `fparkan-fx::ambient_sound` +([подробнее](../reference/environment-effects.md#звуки-control)). + +Туман также зависит от расстояния нелинейно: между ближней и дальней границами +движок интерполирует по квадрату дистанции. При границах 0 и 700 точка в 350 +получает четверть тумана, а не половину. Это сохраняет контраст среднего плана. +Effect renderer подавляет header bit `0x800` только при +render selector 0. Native AniMesh component path захвачен с selector 1, а в +его 53-entry manager присутствуют FX entries с header `0xD00`; preview +использует selector 1 и пропускает такие visual commands. Изменения manager +`A8`, которые задают runtime phase/header gate, пока заменены нулём с явным +счётчиком `control_fx_manager_flag_stub`. Header +bit `0x10` относится к activation lifecycle и не выключает draw. + +Для visual effect renderer использует MAT0, указанный самой командой. Его +phase выбирается по normalized FX phase, а не по wall-clock clock материала +мира. FX opacity остаётся в vertex alpha, MAT0 alpha для этой FX binding +фиксируется на единице, texture alpha остаётся в shader. Preview передаёт FX +через SourceAlpha/read-only-depth state. MAT0 phases проходят локальный +eligibility predicate additive-RGB approximation; native `flag40` и +directional-light branch остаются неразрешёнными. + +Op9 генерирует индексированную полусферу с отдельными vertex и UV данными и +отправляет каждый quad и cap как отдельный draw range. Native packet distance +callback берёт первую индексированную вершину (`Terrain.dll:0x246A0` через +`0x247B0`), преобразует её согласно режиму packet и использует евклидово +расстояние до камеры. Queue размещает большие расстояния раньше меньших; +равный ключ вставляется в найденный binary-search midpoint, поэтому порядок +равных packets не стабилен по времени поступления. Preview повторяет этот ключ +и insertion order. Renderer category 0 остаётся highest-detail mesh; native +category вычисляется из projected sphere size и quality table, поэтому +фиксированная категория не является native LOD parity. + ## Полный кадр Крупный вход в world render проходит через `World3D::stdRenderGame`. Доказан @@ -947,6 +1031,12 @@ sun-ray queries поэтому используют исходные коордÐ она не равна одному лишь обратному преобразованию камеры. В проверенном GOG контексте Ngi32 использовались `n=0.5`, `z=700`, `f=1.3`. +Ближняя плоскость относится к конкретному проходу. После рисования движок +может вернуть прежние настройки камеры: захват только в конце мира видел +`n=5` и при воспроизведении вырезал пол перед игроком. Формат захвата поэтому +хранит проверенную проекцию вызова геометрии отдельно от состояния камеры; +подробности приведены в [описании кадра](../reference/render-frame.md#предпросмотр-по-захвату-камеры-d3d7). + У GOG `Terrain.dll + 0x7355c` хранит указатель на активную внешнюю камеру. Её selector 0 возвращает 16 слов начиная с `+0x20`, перенос находится в словах 3, 7, 11. Интерфейс `LoadCamera` расположен отдельно, по `+0x134` объекта. @@ -962,11 +1052,39 @@ Node38, а диапазоны граней — в 68-байтовых Slot68. В проверяются на единственное назначение, а выходящий за массив, повторный или неподдерживаемый диапазон считается ошибкой загрузки. -Первый рендер карты, terrain-запросы высоты и пересечения, receiver-ы теней и -обрезка контуром зданий используют только LOD0. Отбор сохраняет порядок -исходных граней, а контурный проход отдельно переводит индекс спроецированной -грани обратно в исходный. LOD1 пока не подставляется по расстоянию: такой -переход не реализован. +Рисуемый уровень выбирается по ячейкам, а не по расстоянию до отдельного +треугольника. Начало сетки находится в первых двух float заголовка Slot68, +размер ячейки — в границах первого слота LOD0. Атрибут 2 потока Node38 задаёт +число ячеек по X; индекс ячейки равен `x * count_y + y`. Приложение проверяет, +что границы всех слотов образуют эту регулярную сетку. Все 33 карты GOG +проходят такую проверку, включая вырожденные грани на общей границе ячеек: +принадлежность грани берётся из слота, а не угадывается по её центру. + +Координаты камеры переводятся в целые координаты сетки округлением вниз. +Если `dx * dx + dy * dy >= 9` и горизонтальный FOV не меньше 0,5 радиана, +выбирается LOD1. При увеличении масштаба, отсутствии дальнего слота или +наличии геометрического patch остаётся LOD0. Камера за границей карты не +прижимается к крайней ячейке для этого расчёта. Правило восстановлено по +GOG `Terrain.dll` ([сборка и SHA-256](../reference/original-binaries.md)): +`CLandscape::Render`, RVA `0x11340`, выбор уровня `0x11878–0x11A03`. +Исполнение оригинала подтверждает выбор дальнего слота, а getter камеры +`Ngi32.dll` RVA `0x4D960` возвращает именно FOV. + +Основания строений сохраняют подробную геометрию. Нативная регистрация +patch (`Terrain.dll` RVA `0x12E20`) проверяет его треугольники против каждой +ячейки через `mrnParaIntersectsPoly`. Поэтому учитываются и соседние ячейки, +которых касается край треугольника. Перед проверкой координаты нормализуются +в `f32` относительно центра ячейки; замена этой операции сравнением мировых +границ меняет результат на общих рёбрах. На «Конструкторе» игнорируемый +ресурсный тест сверяет все 22 ячейки с принудительным LOD0 с набором, +полученным в живом оригинале. + +Vulkan получает оба уровня один раз. Диапазоны индексов разделены по +материалу и исходной ячейке; на каждом кадре включается только выбранный +уровень. Сгенерированные отмостки остаются включены. Terrain-запросы высоты +и пересечения, receiver-ы теней и обрезка контуром зданий используют +канонический LOD0. Отбор сохраняет порядок исходных граней, а контурный проход +отдельно переводит индекс спроецированной грани обратно в исходный. ```rust let assignments = mesh.face_lod_assignments()?; @@ -984,9 +1102,10 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { исходные фрагменты рельефа вместе с треугольниками отмостки в одном наборе. `TerrainWorld::with_basement_foundation` принимает этот набор во владение и устанавливает те же координаты и маски в BVH высотных запросов и пересечений. -Предпросмотр миссии на Vulkan пока строит только LOD0: связывание BAS-записей -с этой подготовкой ещё не подключено, поэтому наличие API не означает, что -геометрия миссии уже обрезана. +Предпросмотр миссии связывает BAS-записи с этой подготовкой до загрузки +материалов. Полученная поверхность общая для изображения, теней и запросов; +поправка размещения основания один раз применяется к корню строения и его +присоединённым компонентам. Внешнее кольцо должно лежать на области LOD0, а каждый его сегмент — оставаться покрытым текущей поверхностью. Непокрытый сегмент или пересекающиеся контуры @@ -997,13 +1116,33 @@ for (face, assignment) in mesh.faces.iter().zip(assignments.iter()) { ## Материалы ландшафта -Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Старший байт -`TerrainFace28.material_tag` выбирает материал первой таблицы, младший — -второй. `0xff` в старшем байте означает отсутствие первого слоя. +Рядом с `Land.msh` лежат `Land1.wea` и `Land2.wea`. Младший байт +`TerrainFace28.material_tag` выбирает базовый материал `Land1.wea`, старший — +дополнительный материал той же таблицы. `0xff` в старшем байте означает +отсутствие дополнительного слоя. `Land2.wea` содержит соответствующие этим +двум строкам detail-материалы. В AutoDemo встречаются теги `0102`, `0203`, `ff01`, `ff02`, `ff03`. Менеджер World3D получает селектор `(table_index << 16) | material_index` и возвращает выбранную фазу MAT0 размером 76 байт. +Таблица, строка и блок анимации — разные части запроса. В быстром проходе +с двумя текстурами `CShade` запрашивает для Land2 блок анимации **1**. Он может +выбирать другую текстуру, а не только другие коэффициенты освещения: +например, в «Конструкторе» это `L09M.0` вместо `L09.0`. Подмена блока первым +приводит к умножению базового изображения на неподходящую detail-текстуру и +меняет яркость и контраст земли. `TerrainMaterialSelection` сохраняет номер +блока отдельно от packed material key; приложение использует его при загрузке +дескриптора и в последующих кадрах. Если второго блока нет, общий MAT0 sampler +возвращается к первому, как оригинальный `GetMaterialPhase`. + +В GOG `Terrain.dll` (сборка из [таблицы бинарников](../reference/original-binaries.md)) +быстрая ветвь находится в RVA `0x1A7AC–0x1A7FB`. Живое исполнение передаёт +handle `0x00010002`, block `1` и устанавливает режим текстур `9`; первая +текстура умножается на diffuse, вторая — на результат с множителем два. +Ближний дополнительный проход с порогом `260 / FOV` относится к fallback без +этого режима. На проверенном устройстве он не выбирается, поэтому переносить +его дистанционный порог в быстрый проход нельзя. + Type 18 в `Land.msh` оригинал называет microtexture mapping chunk; он содержит четырёхбайтовые записи. Type 11 хранит пары `u16`, выбранные таблицей terrain slots. Их первое поле — ключ shade cache, а не номер строки @@ -1021,12 +1160,103 @@ lightmap state передаются в `VulkanStaticMaterial`, а shader посÐ сэмплирует эти четыре стадии. Animation clock обновляет выбранные фазы и коэффициенты без пересборки terrain mesh, сохраняя native type-14 overlay mask. +### Отражение на воде + +Поверхность воды нельзя рисовать тем же умножением текстур, что и землю. +В проходе ландшафта GOG Terrain.dll флаг грани `0x20000` участвует в выборе +отражения; при включённом EMBM живой водный кадр использует texture mode 10. +Оригинал создаёт для этого отдельную камеру `REFLECTION_SHIFTED`. +В [эталонной сборке](../reference/original-binaries.md) выбор грани находится +в Terrain.dll RVA `0x28A50`, а подготовка EMBM-пакета — в RVA `0x1B540`. + +Для горизонтальной воды на высоте `h` камера отражения находится в точке +`[eye_x, eye_y, 2h − eye_z]`. Но она не повторяет наклон основной камеры: +смотрит вертикально вверх, а её окно лежит на прямоугольнике водоёма. Поэтому +текстура отражения привязана к местности, а не к экрану игрока. В плоскости +воды координаты изображения равны `U = 1 − Ynorm`, `V = 1 − Xnorm`. + +Terrain.dll RVA `0x4EFF0` передаёт shifted-проекции пять значений: +`[min_y − eye_y, min_x − eye_x, span_y, span_x, eye_z − h]`. Например, +для наблюдателя `[1200, 800, 185]`, воды `150` и границ +`[1000, 500, 1500, 1200]` это `[-300, -200, 700, 500, 35]`. +Матрица камеры и эти аргументы проверены исполнением оригинальной функции +при двух положениях и разных поворотах основной камеры. Оракул подменяет +ввод-вывод очереди, ландшафта и камеры; сам renderer и матрица глубины в эту +проверку не входят. Vulkan-проекция сохраняет near/far основного вида; +Rust-тест отдельно проверяет углы водоёма и границы глубины. + +Отражённый вид меняет сторону отсечения и отбрасывает геометрию ниже воды. +Небо и оставшаяся сцена попадают в отдельную цветовую текстуру с собственной +глубиной. В основном проходе водные треугольники читают её по координатам +водоёма с добавленным смещением волн. Для неперспективной камеры API остаётся +обычное зеркало с экранными координатами. + +Размер изображения отражения выбирается отдельно от размера основного кадра. +Terrain.dll в конструкторе CShade (RVA `0x278E0`, цикл с `0x27FC7`) берёт +ширину и высоту renderer через Ngi32.dll RVA `0x7FB0`. Сторона квадратной +текстуры — наибольшая степень двойки, не превосходящая меньшую из этих +величин и 1024; нижний предел равен 2. Например, экран 1280×1024 даёт +отражение 1024×1024, а 1280×720 — 512×512. Getter размеров и исходный цикл +выбора проверены исполнением DLL на десяти входах; создание нативной текстуры +в этом оракуле пропущено. Vulkan создаёт цвет и глубину с этой стороной и +задаёт каждому проходу собственные viewport и область очистки. Так уменьшение +отражения не обрезает основной кадр и воспроизводит исходный масштаб его +пикселей, от которого зависит чёткость берегов и ряби. + +Цветовой target переходит из attachment layout в shader-read layout после +записи. Следующий зеркальный проход ждёт предыдущего чтения этой текстуры; +основной и отражённый виды используют разные слоты uniform buffer. Во время +записи отражения привязан отдельный нейтральный descriptor, чтобы проход +не читал собственное вложение. + +Волны не требуют готовой текстуры из ресурсов: Terrain.dll RVA `0x2BF50` +создаёт карту 128×128 со знаковыми парами Du/Dv. Для нормированных координат +текселя `x` и `y` угол равен `(x + y − 1) × 12,566360473632812`, а компоненты — +округлённые `64 cos(угол)` и `64 sin(угол)`. Все 32 768 байт Rust-генератора +сверены с исполнением этой функции DLL при игровом x87 CW `0x007F`. + +Рябь привязана к водоёму, поэтому не плывёт вслед за экраном. Конструктор +ландшафта собирает прямоугольник всех отражающих граней; U меняется против +мировой Y, V — против X. Эти координаты повторяются сто раз, к обеим прибавляется +`(queue_clock % 10000) / 10000`. Это самостоятельный clock: время суток и +относительный clock неба не задают фазу волн. Захват хранит исходный +`sky_uv_clock.raw_clock`; ограниченный replay фиксирует его, интерактивный +продолжает с этого значения. Старые захваты без raw clock явно считаются в +`captured_water_phase_unverified`. + +Карта Du/Dv смещает координаты следующей текстуры с диагональными коэффициентами +`0,01`, без изменения яркости. Это правило +[Direct3D BUMPENVMAP](https://learn.microsoft.com/en-us/windows/win32/direct3d9/bump-mapping-formulas). +В Vulkan карта хранится в двух каналах несжатой текстуры и возвращается к +знаковым значениям перед вычислением смещения. + +Матрица положения камеры, её окно, размер текстуры и координаты волн восстановлены, но весь +контекст отражённого вида ещё не повторён. Диапазон глубины, небо, туман и FX +заимствуются из основного вида. Для захвата с обрезанным viewport ещё не +подтверждено соответствие размера окна FParkan полному renderer оригинала. +Эти ограничения учитываются в `planar_reflection_render_context_approx`; +совпадение генератора волн и окна не доказывает совпадения итогового отражения. +Один target обслуживает ближайшую +активную высоту воды; другие высоты и негоризонтальные поверхности явно +считаются как неподдержанные. Это промежуточный водный проход, а не полная +приёмка воды или лавы. + ## Реализация Vulkan `LegacyPipelineState` хранит blend, depth, cull и alpha-test. Из этих значений получается ключ Vulkan pipeline. Alpha reference передаётся отдельно через push constant; он не требует нового pipeline. +Набор pipelines создаётся до первого draw. Для каждого исходного состояния +готовятся его авторский вариант, прозрачный вариант с сохранённым alpha-test +и пять категорий MAT0 с обеими политиками глубины. Cull и decal bias остаются +исходными; одинаковые ключи объединяются. Смена категории материала или +прозрачности диапазона выбирает готовый ключ, сохраняя descriptors, buffers и +framebuffers. Vulkan `VkPipelineCache` принадлежит renderer, используется всеми +созданиями pipelines и переживает пересоздание swapchain. На диск он пока не +сохраняется. Счётчик `render_pipeline_cache` показывает число созданных +pipelines, включая новые наборы после resize. + Рабочий путь `fparkan-game` загружает terrain и все выбранные mission roots (по умолчанию весь список объектов), разрешая локальные material slots через WEAR и MAT0 до объединения геометрии. Индексы GPU имеют тип `u32`: вся карта @@ -1034,55 +1264,80 @@ WEAR и MAT0 до объединения геометрии. Индексы GPU `u16`. Terrain передаёт в shader базовый, detail и overlay stages, lightmap и animation phase; object batches сохраняют их legacy blend/depth/alpha state. -После загрузки world path создаёт интерактивную free-flight камеру (`WASD`, -`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и собирает -environment frame для каждого подготовленного кадра. Event loop один раз -сэмплирует камеру, общий animation clock, геометрию и окружение перед запросом -redraw; redraw потребляет это состояние. Если surface требует пересоздать -swapchain, тот же sample времени и состояние мира отправляются повторно. -Перед повторной отправкой projection камеры и viewport-зависимые environment -и shadow geometry обновляются для нового размера без продвижения сцены. -Счётчик и replay clock продвигаются только после результата `Presented`. -После шага движения камера проверяет высоту -`TerrainWorld`: над конечной поверхностью сохраняется начальный просвет, а вне -поверхности высота не ограничивается. - -Для каждого отрисовываемого строения (корневого объекта класса `FORT`) и для -каждого его отрисовываемого компонента создаётся отдельная запись проверки. Она -использует те же текущие матрицы узлов и присоединённых компонентов, что и -геометрия. -После ограничения целевой позиции по TerrainWorld камера проверяет непрерывный -отрезок от прежней позиции до цели; при пересечении она остаётся в прежней -позиции, сохраняя новый поворот. Повторная отправка ожидающего кадра не -применяет движение второй раз. Это ограничение диагностической свободной -камеры, а не правило игрового или следящего режима оригинала. +При первоначальной загрузке одинаковые текстуры разных material slots +используют один GPU image. Ключ включает размеры, RGBA8 и всю авторскую +mip-цепочку: совпадения только базового изображения недостаточно. Хеш ускоряет +поиск, а полное сравнение данных исключает ошибочное объединение при коллизии. +Слоты сохраняют собственные descriptors, UV и параметры материала. При первой +смене общей текстуры renderer создаёт отдельный image только для изменяемого +слота; следующие фазы обновляют его на месте. Поэтому анимация одного материала +не меняет соседний. Строка `render_texture_upload` показывает число слотов, +начальных GPU images и отделений при обновлении. CPU-копии исходных пикселей +в материалах эта оптимизация не объединяет. + +Рельеф образует неизменяемое начало общего vertex buffer. Control-анимация, +FX и окружение обновляют следующие вершины, а новые индексы нужны только +геометрии проекционных теней в конце index buffer. Поэтому кадр записывает +лишь эти изменяемые части. При росте буфера новая память получает все данные, +включая неизменяемое начало; уменьшение числа теней не уменьшает выделенную +ёмкость. Это сохраняет номера вершин, диапазоны отрисовки и порядок проходов. + +Перед записью общих ресурсов renderer ждёт fences всех отправленных кадров. +Одного fence текущего кадрового слота недостаточно: другой кадр ещё может +читать ту же память. Ожидание всего устройства остаётся на пересоздании +swapchain, диагностическом readback и завершении работы. Загрузка изменённых +текстур пока использует отдельную синхронную transfer-команду. Строка +`render_geometry_upload` показывает записанные vertex/index bytes и объём, +который потребовался бы при полной перезаливке, без начальной загрузки. +После загрузки world path создаёт интерактивную free-flight камеру (`WASD`, +`E/Q`, `Shift`, `Ctrl`, RMB relative-look), обновляет listener и на каждом +кадре собирает environment frame. Для каждого отрисовываемого строения +(корневого объекта класса `FORT`) и для каждого его отрисовываемого компонента +создаётся отдельная запись проверки. Она использует те же текущие матрицы узлов +и присоединённых компонентов, что и геометрия. После ограничения целевой +позиции по TerrainWorld камера проверяет непрерывный отрезок от прежней позиции +до цели; при пересечении +она остаётся в прежней позиции, сохраняя новый поворот. Повторная отправка +ожидающего кадра не применяет движение второй раз. Это ограничение +диагностической свободной камеры, а не правило игрового или следящего режима +оригинала. Mission-local `sky.ske`/`sky.wea` выбирают атмосферное расписание и sky materials, `effects.rlb/env_lightning` даёт lightning visual; weather particles, flares, sun/moon, point lights, projected shadows и audio events обновляются вместе с камерой и временем. `--atmosphere-seconds` задаёт старт времени, `--frames 0` оставляет цикл -бесконечным, `--control-clock-ms N` воспроизводит общий clock анимации MSH и -материалов вместе с окружением от нуля до `N` за заданное число кадров и требует -как минимум два кадра. -`--preview-roots` служит только диагностическим ограничителем. +бесконечным, а `--preview-roots` служит только диагностическим ограничителем. `--legacy-camera-capture` выбирает воспроизводимую камеру без free-flight -управления: запись selector 0 задаёт D3D7 transform, clip параметры и native -viewport RECT `[left, top, right, bottom]`. Путь сохраняет оси basis из view -matrix вместе с roll, переносит полученный `clip_from_world` в preview camera, -переводит RECT в физический drawable размер `(right - left) × (bottom - top)` -и отклоняет несовпадение размера при -создании или resize вместо растягивания кадра. Результат нужно сравнивать с -тем же crop исходного PNG `[left, top, right, bottom]`. Контроллер -`FreeFlightCamera` в этом режиме не создаётся. Подготовка кадра обновляет +управления. `selector0_words` содержит локальную матрицу камеры. Если запись +содержит `effective_world_matrix_words` с маркером +`effective_world_matrix_space="world"`, preview использует эту мировую +матрицу. При `parented_camera=true` отсутствие мировой матрицы — ошибка; +локальный selector 0 остаётся legacy fallback только для записей без признака +parented и без мировой матрицы. +Если native helper явно применяет выбранную мировую камеру, setter работает в +mode 2, а restore возвращает исходную локальную матрицу в mode 1. Clip +параметры и native viewport RECT `[left, top, right, bottom]` идут из той же +записи. Путь сохраняет оси basis из view matrix вместе с roll, переносит +полученный `clip_from_world` в preview camera, переводит RECT в физический +drawable размер `(right - left) × (bottom - top)` и отклоняет несовпадение +размера при создании или resize вместо растягивания кадра. Результат нужно +сравнивать с тем же crop исходного PNG `[left, top, right, bottom]`. Контроллер +`FreeFlightCamera` в этом режиме не создаётся, но redraw продолжает обновлять environment и projected shadows от captured camera. Перед environment update и его audio events listener получает position, forward и up captured camera; -начальная сортировка world draw использует её же position. Phase capture может +начальная сортировка world draw использует её же position. Облака в обычном +запуске движутся с отдельным 50-секундным циклом UV; фон и звёзды имеют свои +периоды. Изменение цвета неба по расписанию не заменяет это движение. +Phase capture может содержать наблюдаемый mission path и проверенный `atmosphere_seconds`; viewer использует эту абсолютную фазу по умолчанию, если не указан явный CLI override, -и удерживает sample атмосферы на ней через readback frames. Без -`--control-clock-ms` FX получает реальный frame time; с параметром environment -и материалы используют replay clock. Это не полное simulation time: `weather`, RNG и +и удерживает sample атмосферы на ней через readback frames. Дополнительный +`sky_uv_clock` фиксирует положение движущихся текстур на том же кадре; +без него их начальная фаза не подтверждена. Фазы FX следуют +выбранному control clock: без `--control-clock-ms` он обновляется по elapsed +time, а с явным значением readback frames переигрывают заданный clock. Это не +полное simulation time: `weather`, RNG и остальное mission state остаются неизвестными, поэтому совпадение камеры и фазы не доказывает visual parity с native frame. diff --git a/docs/tomes/06-behavior.md b/docs/tomes/06-behavior.md index 3af2c21..6edfb7a 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,15 +188,54 @@ 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. Никакое значение не подставляется +вместо этого действия. ### Первый запуск сценария +Перед настройкой первого шага каждый AI получает начальный массив DWORD. В +`iron3d.dll` SHA-256 +`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa` +`FUN_100392e0` (RVA `0x392e0`) повторяет вызов слота AI `+0x38` столько раз, +сколько задаёт `second_resource.tag` этой строки TMA. Этот слот ведёт в +`ai.dll` SHA-256 +`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: +метод RVA `0x2010` передаёт коллекцию при `AI+0x83c` в `FUN_10002980` +(RVA `0x2980`), а она добавляет один DWORD через `FUN_100048f0` (RVA +`0x48f0`). Каждый вызов копирует `0xffffffff`; входное слово не меняется. +Поэтому тег `3` даёт `[0xffffffff, 0xffffffff, 0xffffffff]`. Это число +начальных ячеек, а не назначение трёх ID. + +Изолированная x86-проверка вызовом оригинальной `ai.dll` подтвердила пары +«число слотов/ёмкость» `0/0`, `1/17`, `17/17` и `18/34`. Все добавленные слова +равны `0xffffffff`, а входное значение, контрольные поля, память вне заголовка +массива и указатель стека сохраняются. Это подтверждает добавление начального +слова, но не работу последующих обработчиков. + +Rust хранит такой снимок в отдельном `MissionAiStartupState` для каждого TMA-клана, +а не в пакете сценария. При полной загрузке (`Full`) создаётся запись для каждого +клана, в том числе если у него нет пути сценария или пути таблицы исследований. Счётчик +берётся из сырых битов `second_resource.tag` как `u32` у обеих форм записи — +`Standard` и `Spatial`; нулевой счётчик даёт существующее состояние с пустым +списком. Статический режим предпросмотра не создаёт состояний AI. + +Загрузка отклоняет суммарный объём свыше 1 000 000 DWORD и резервирует память +проверяемой операцией; это защита Rust от чрезмерного выделения памяти, а не +ограничение оригинальной игры. Rust сохраняет начальные слова, но не +восстанавливает работу последующих обработчиков и не назначает этим словам ID +объектов. Этот массив также не является списком +`BehaviorDistributorState.registered_objects`. + До регулярной работы Behavior настраивает расписание своего первого шага. Он берёт текущее 32-битное время игровой очереди и назначает проверку через три интервала по 64 мс, добавляя небольшую случайную задержку от 0 до 63 мс. Эта @@ -213,7 +254,7 @@ SuperAI для записей кланов, затем обновляет Behavi всё равно устанавливается. В полной загрузке Rust вызывающая сторона передаёт значение времени очереди; после регистрации объектов загрузчик последовательно подготавливает каждый сценарный пакет и выполняет восстановленные действия -`Handler(19)` и числовые literal assignments из первого `Init`. Статический +`Handler(19)` и поддержанные формульные присваивания из первого `Init`. Статический просмотр не запускает Behavior, но может сохранить эти записи `Init` в сценарном varset. @@ -546,8 +587,14 @@ let cost = calc_full_research_cost(&candidate, &table)?; штатный Handler(43) и запуск миссии здесь не проверялись. Владелец начальной записи исследования — индекс текущего прохода по кланам. -Движок сохраняет его в `descriptor[0]`, а затем переносит в контекст, чтобы -связать запрос с AI-данными того же клана. +В GOG-сборке `iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) проходит записи кланов +и вызывает для каждой `FUN_100392e0` (RVA `0x392e0`), которая создаёт AI; +`ai.dll!CreateSuperAI` (RVA `0xf710`) выделяет объект и вызывает конструктор +по RVA `0x1000`, не проверяя наличие сценарного пути. Для исследовательского +пути нужны отдельный флаг включения и непустой `second_resource.path`; наличие +сценарного пакета условием не является. Индекс клана сохраняется в +`descriptor[0]` и переносится в контекст, чтобы связать запрос с AI-данными +того же клана. Это значение отдельно от аргумента 1 `AddNewObjectToGame`, берущегося из `GetNetPlayerNum`, и от TMA-тега `second_resource`, задающего число вызовов @@ -557,12 +604,14 @@ Handler(43) и штатный запуск миссии остаются непÑ Полный Rust-загрузчик принимает явный `MissionStartupInput`: время очереди и отдельный `research_creation_gate`. Режим миссии и имя файла этот -флаг не заменяют. При включённом флаге таблица загружается только для клана, -чей сценарный пакет уже прочитан и успешно разобран; путь берётся из второго -ресурса этого клана как C-строка. Пустая C-строка пропускается, а не-ASCII -байты дают явную ошибку. Клан индексируется по порядку в TMA, тег ресурса на -владение не влияет. Если несколько кланов указывают на один файл, каждый -получает отдельно декодированную таблицу с собственными строками TRF0. +флаг не заменяют. При включённом флаге загрузчик использует уже имеющееся +`MissionAiStartupState` каждого TMA-клана, даже если у него нет сценарного +пакета; путь берётся из второго ресурса как C-строка до первого NUL. Пустой +путь и выключенный флаг пропускают чтение, а не-ASCII байты дают явную ошибку. +Предпросмотр тоже не читает TRF. Клан индексируется по порядку в TMA, а тег +`second_resource` задаёт число слотов AI, не владельца. Если несколько кланов +указывают на один файл, каждый получает отдельно декодированную таблицу со +своими строками TRF0. ```rust MissionStartupInput { @@ -571,9 +620,10 @@ MissionStartupInput { } ``` -Это сохраняет исходные таблицы и их связь с владельцами для следующих этапов. -Оно ещё не создаёт полный исследовательский контекст оригинала и не запускает -`Handler(43)`; эта обработка пока остаётся явно неподдержанной. +Это сохраняет декодированные таблицы и связь с владельцами для следующих +этапов. Оно ещё не создаёт полный исследовательский контекст оригинала, не +строит AI-каталог и не запускает `Handler(43)`; эти части пока не +восстановлены. Отдельная frozen-матрица непосредственно вызвала chooser GOG `ai.dll` SHA-256 `c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc`: функция @@ -1089,8 +1139,31 @@ NDP, а `FUN_1000a100` (RVA `0xa100`) добавляет их в общую та последовательность provider-строк: её длина берётся из NDP, она не дополняется до числа узлов MSH и не участвует во второй перенумерации ключа. -Так этот селектор включён в цепочку добычи руды оригинала. В той же сборке -`Behavior.dll` (SHA-256 +Индексный selector `0x400` выдаёт значение выбранной строки provider как есть. +Сначала общий индекс устройств выбирает дескриптор, затем его ключ выбирает +строку NDP; это разные номера. Результатом становится текущее type-1 значение +этой строки, а не запас `CurrentOre`, предел или значение после умножения на +коэффициент и `M`. Например, provider-строка с `0.625` возвращает `0.625`, а +записанный в ней ноль тоже остаётся успешным результатом. Rust использует уже +подготовленную таблицу и сообщает отдельную ошибку, если provider-таблица или +строка по ключу недоступна. Этот запрос читает снимок состояния и сам не меняет +таблицу и не запускает её обновление. + +У правила есть два разных подтверждения. Разбор AutoDemo нашёл восемь таблиц +Control и 100 устройств; все 100 значений по ключам устройств в этом корпусе +равны `1.0`, поэтому Rust-проверка подтверждает реальные ключи и доступность +строк, но сама по себе не различает выбор по ключу и по номеру устройства. +Отдельная x86-проверка исходного `Control.dll` включала шесть вызовов на +синтетической таблице, где значения строк были заданы вручную и различались. +Например, при номере устройства `2`, ключе `1` и нулевом слове +`device+0x50` оригинал вернул `0.625` из выбранной строки. Точные биты ответов +приведены в [техническом разборе](../appendices/script-vm.md#индексный-control-getter-сырой-селектор-0x400). +Этот опыт не создавал оригинальные factory-объекты и не исполнял полную миссию: +полный интерфейс IID `0x204`, интеграция Behavior и игровое поведение остаются +непроверенными. + +В цепочке добычи руды оригинал использует описанный выше selector `0x300`. +В той же сборке `Behavior.dll` (SHA-256 `ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) `FUN_100186b0` (RVA `0x186b0`) перечисляет устройства типа `0x1a`, а `FUN_100198e0` (RVA `0x198e0`) суммирует успешные запросы IID `0x204` с selector @@ -1369,6 +1442,30 @@ descriptor через `INTO`. Так разные части Control могут загруженной таблицы; обновление пока не подключено к игровым часам и не заменяет полный Control tick. +Устройство фабрики `2` имеет поле selector `6`, `+0x174`; конструктор задаёт ему +ноль. Позднее Control может пересчитать его по паре строк записи и отдельному +прототипу `BULL`. Только для поддержанной свежей формы одного корневого mesh +`BULL` из `objects.rlb`, без дочерних компонентов, его NDP-строки сводятся к +сумме Max; например, `[1.5, 2.25]` +превращается в `3.75` с округлением `f32` после каждого шага: + +```rust +let mut total = 0.0_f32; +for row in ndp_rows { + total += row.max_volume; +} +``` + +Пустая таблица даёт ноль; физическую единицу этому числу не приписываем. Пара +сравнивается как две C-строки до первого NUL: хвост не учитывается, регистр не +важен. Если после изменения любая строка пуста, старое `+0x174` сохраняется; +пустая пара конструктора не вызывает пересчёт, потому что уже совпадает. Новые +значения применяются при добавлении и первой активной записи `INTO` в исходном +порядке. Непредставленные входы оставляют селектор недоступным, а ошибка чтения +узнаваемого, но повреждённого ресурса отменяет новую загрузку и сохраняет +прежние мир и RNG. Общая формула selector 35 и условия свежей формы разобраны в +[техническом приложении](../appendices/script-vm.md). + Отдельный путь объекта класса `4` умножает интервал на контактный коэффициент и передаёт изменение строке. В проверенном примере коэффициент `1`, интервал `2`, и значение строки уменьшается на `2`; этот коэффициент не является ставкой @@ -1502,7 +1599,7 @@ offsets 119–121. В `fr_l_gener.ctl` найдено 35 строк type 4, вк `f_gener_ball` с offset 85. Control.dll `+0x8B10` передаёт указатель текущей записи диспетчеру `+0x2D4B0`. -Для tags `2`, `3`, `4`, `5`, `8`, `10`, `21` и `27` подтверждён общий читатель +Для tags `2`, `3`, `4`, `5`, `8`, `10`, `17`, `21` и `27` подтверждён общий читатель `+0x21D50`: таблица виртуальных методов tag `2` (`+0x3CBD8`) вызывает его через обёртку `+0x29650`, остальные перечисленные vtable указывают прямо на него в позиции `+0x1C`. Читатель потребляет @@ -1513,6 +1610,28 @@ Control.dll `+0x8B10` передаёт указатель текущей запÐ а также отказ при оборванном завершающем NUL. Это подтверждает структурный формат, но не назначает смысл всем коэффициентам устройств. +Путь записи с меткой `17` проверен на GOG `Control.dll` с SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`: +ветвь `0x11` фабрики `+0x2D4B0` вызывает конструктор `+0x24760`, который +устанавливает указатель на виртуальную таблицу `0x1003C840`. Ячейка `+0x1C` +этой таблицы ссылается прямо на общий читатель `+0x21D50`. В ресурсах +`weapon.rlb:bm_m_01.ctl` и `weapon.rlb:bm_l_01.ctl` таблица привязок +заканчивается на смещении `0x120`, где начинается запись с меткой `17`. +Общий префикс размером `0xB0` байт включает саму метку; счётчик по `+0xAC` и +слово длины в этих двух ресурсах равны нулю, поэтому указатель чтения +переходит на `0x1D4`. Там начинаются `0x54` байта заголовка таблиц эффектов; +таблицы содержат соответственно `4`, `2`, `2` и `2` строки по 100 байт и +заканчиваются на `0x620`. Записи из реестра объектов `bm_m_01` и `bm_l_01` +при обычной подготовке ресурсов сохраняют явно указанные списки точек CPT +(`bm_m_a.cpt` и `bm_l_a.cpt`) и таблицы узлов NDP (`bm_m_01.ndp` и +`bm_l_01.ndp`); в первой таблице эффектов находятся три строки type 4. +Синтетический тест проверяет непустые массивы значений и строки у записи с +меткой 17 и следующей записи с меткой 4, затем переход к таблице эффектов. +Если считать 17 неизвестной, чтение остановится перед +ней; общий разбор позволяет пройти запись и прочитать следующие таблицы, +оставляя слова tag 17 числами без толкования. Обработчики этой метки и их +игровое поведение ещё не восстановлены. + Resolver прототипов хранит явную registry-ссылку на `.cpt` отдельно от ссылки на `.ctl`. Если строки type 4 разобраны и явная ссылка на CPT есть, подготовка читает ровно указанные archive/name; имя CPT не выводится из имени CTL. В CPT |
