diff options
| author | Valentin Popov <valentin@popov.link> | 2026-10-29 21:51:25 +0300 |
|---|---|---|
| committer | Valentin Popov <valentin@popov.link> | 2026-10-29 21:51:25 +0300 |
| commit | aa6d4b8537405628d77f0928eb6fb3e3ba9a826b (patch) | |
| tree | 718c251b2b95d84b2e41e41eca5072e003289436 /docs/appendices | |
| parent | cc5c6414ce68e93a9b7f3c95cc07e9120b400fc1 (diff) | |
| download | fparkan-aa6d4b8537405628d77f0928eb6fb3e3ba9a826b.tar.xz fparkan-aa6d4b8537405628d77f0928eb6fb3e3ba9a826b.zip | |
feat(runtime): project load-time Control profile values
Diffstat (limited to 'docs/appendices')
| -rw-r--r-- | docs/appendices/knowledge-boundaries.md | 54 | ||||
| -rw-r--r-- | docs/appendices/script-vm.md | 137 |
2 files changed, 183 insertions, 8 deletions
diff --git a/docs/appendices/knowledge-boundaries.md b/docs/appendices/knowledge-boundaries.md index be2277a..3d97927 100644 --- a/docs/appendices/knowledge-boundaries.md +++ b/docs/appendices/knowledge-boundaries.md @@ -540,14 +540,52 @@ resource-device значения во время игры. ### Профиль владельца Control из CTLD -Смысл отдельных слов, последующие изменения mutable-копии и чтения в Control -`fca0/fac0` остаются открытыми. Загрузочный тест проверяет CPU-проекцию, но не -исполнение native полного загрузчика или игрового tick. Точные writer, RVA и -указатели описаны в [техническом разделе о загрузке CTLD](script-vm.md#профиль-владельца-control-при-загрузке-ctld). - -Закрывающий эксперимент: трассировать `LoadControlSystem`, -`LoadPhysicalModel`, `CreateCollManager` и создание collision objects; связать -каждый изменяемый field с созданным shape, contact или реакцией на движение. +Общий проход оригинального `Control.dll!FUN_1000fac0` (RVA `0x0fac0`, SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) проверен +прямым x86-вызовом под control word `0x007f` на synthetic backing. Оригинальный +`AniMesh.dll` query `0x51f0` и методы диапазона component ID `0` +`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. + +Смысл `Slot68 +0x30/+0x34`, tag-27 cache DWORD, owner accumulators `+0x530`, +`+0x534`, `+0x538` и результата mutable word `26` не имеет доказанных единиц +измерения. Неизвестные слова остаются raw; их нельзя подписывать как массу или +площадь. Synthetic x86 vector напрямую записывает scales `[2, 3, 5]` в +`AniMesh +0x158/+0x15c/+0x160`; это не значения обычной миссии. Проверка +стандартных `ArealMap`/`World3D` и AniMesh callback-ветвей до FAC0 подтвердила, +что setter не вызывается, поэтому полный начальный loader использует defaults +конструктора `[1, 1, 1]`. Конкретный отдельный путь World3D `AddNewObjectToGame` +и mirror-варианты передают свой transform-вектор setter-у; это отдельная +граница, как и более поздние placement/tick. + +Полный Rust loader теперь вызывает FAC0 при построении +`build_control_device_projection` и публикует результат в +`MissionControlOwnerProfile::load_projection`. Игнорируемый corpus-тест +`full_load_builds_effective_control_device_table_for_mine_unit_dat` на лицензированном +`min_l_n1.dat` проверяет 11 устройств, все 25 соответствий provider ordinal +модели и упорядоченные значения `row +0x20`. Его агрегаты — +`0x470833fc/0x4897e712/0x4d9452cb` для `+0x530/+0x534/+0x538`; исходный и +начальный mutable word `26` равны `0x7f7fffff`. Это подтверждает Rust full-load +результат на оригинальных ресурсах, но не совпадение с полным нативным load +оригинала. Поведение позже при placement/tick и физический смысл raw-полей +остаётся неизвестным. Точные RVA, owner layout и векторы приведены в +[описании изолированного прохода FAC0](script-vm.md#изолированный-проход-fac0). + +Остаётся сравнить этот полный mine load с original loader при тех же входных +данных и продолжить трассировку `LoadControlSystem`, `LoadPhysicalModel`, +`CreateCollManager` и collision objects, если потребуется связать profile words +с shapes, contacts или реакцией на движение. ## DirectPlay wire diff --git a/docs/appendices/script-vm.md b/docs/appendices/script-vm.md index d47a64d..c86e562 100644 --- a/docs/appendices/script-vm.md +++ b/docs/appendices/script-vm.md @@ -977,6 +977,143 @@ Ignored Rust-тест также проходит полный неотфильÑ профилю. Это подтверждает декодирование и CPU-проекцию, но не исполнение native полного загрузчика. +#### Изолированный проход FAC0 + +В оригинальном `Control.dll` SHA-256 +`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e` +функция `FUN_1000fac0` (RVA `0x0fac0`) рассчитывает начальное состояние +mutable-копии CTLD профиля. x86 oracle вызывает её с указателем owner в `ECX` +через короткий stdcall-to-fastcall переходник. Вектор создаёт owner исходным +конструктором RVA `0x31490`, но памяти для профиля, provider rows, device list и +AniMesh модели выделяет искусственно. Код FAC0 и выбранные методы DLL при этом +остаются оригинальными. + +В используемой раскладке owner функция читает и записывает следующие поля: + +| Смещение owner | Что находится в тестовом ABI | +| --- | --- | +| `+0x20` | указатель на AniMesh; в отдельном векторе здесь null | +| `+0x46c` | указатель на исходный блок из 27 слов профиля | +| `+0x470` | изменяемая копия из 27 слов; её word `26` находится по `+0x4d8` | +| `+0x530`, `+0x534`, `+0x538` | три накопителя профиля | +| `+0x55c`, `+0x560` | указатель на строки provider и их count; шаг строки — `0x2c` | +| `+0x5b4` | необязательный указатель cache; используемый DWORD расположен по `+0x0c` | +| `+0x5bc`, `+0x5c0` | массив указателей на устройства и его count | + +Это описание ABI-вектора, а не отдельная публичная Rust layout-структура. + +Для чтения MSH функции используется оригинальный `AniMesh.dll` SHA-256 +`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`: +`FUN_100051f0` (RVA `0x51f0`) запрашивает значение узла по selector `0x0f` или +`0x10`, а `FUN_10005780` / `FUN_100057c0` (RVA `0x5780` / `0x57c0`) дают +начало и длину диапазона глобальных узлов component ID `0`. В model data функция +`0x51f0` читает Node38 mapping и использует два выбранных DWORD Slot68 `+0x30` +и `+0x34` при вычислении selectors. Это opaque `opaque[0]` и `opaque[1]`, а +не доказанные физические коэффициенты или значения, которые query возвращает без +преобразования. + +Slot выбирается первым словом `Node38.slot_index[state * 5 + lod]`, то есть для +начального профиля — state `0`, LOD `0` и byte offset `+0x08` в Node38. +`0xffff` обозначает отсутствие slot и даёт нулевые selectors без умножения на +scales. В Rust проекции другой непригодный индекс или нечитаемая запись — +неизвестное сопоставление, а не известный нулевой slot. Для валидной ссылки Rust +использует `selected_slot` и сохраняет эту границу через `Option`. + +Если три входных `f32` из AniMesh `+0x158`, `+0x15c`, `+0x160` обозначить +`s158`, `s15c`, `s160`, а два raw Slot68 слова — `v30` и `v34`, порядок native +умножений задаёт selectors: + +```text +selector F = (s15c * s158) * v30 +selector 10 = ((s160 * s15c) * s158) * v34 +``` + +Затем для каждой provider row по её global-node ordinal вычисляется +`base = row.mobility_per_volume * selector 10`. `+0x534` накапливает selector +`10`, `+0x530` — selector `F`, а `+0x538` — `row +0x20` после начального +расчёта и device contributions. Если ordinal попадает в диапазон component ID +`0`, `base` добавляется к **исходному** profile word `26`; накопление начинается +из original CTLD copy, а не из прежнего mutable word `+0x4d8`. + +Ненулевой cache pointer `owner +0x5b4` указывает на raw cache DWORD `+0x0c`. +Он добавляет в каждый `row +0x20` значение `cache27 * selector F`. В +проекции Rust последняя tag `27` запись начального append задаёт raw cache из +`fixed_words[11]`; последующий tag `27` в `INTO` обновляет существующее значение, +а другой tag его не меняет. Если `INTO` встречается до создания cache, результат +профиля помечается недоступным и остаётся таким после более позднего append. +Cache отсутствует и cache со значением ноль численно дают одинаковую добавку; +это не превращает пропущенную ссылку в нулевые данные. + +После начального прохода устройства читаются в обратном порядке. Каждое +устройство берёт индекс provider row из ключа `+0x04`, а DWORD descriptor +`+0x1c` прибавляет и к выбранному `row +0x20`, и к `+0x538`. Накопители +складываются в наблюдаемом порядке одиночной точности; перестановка или +переассоциация слагаемых меняет биты. + +После вычитания `+0x538` из подготовленного word `26` отрицательный результат +записывается как положительный ноль. Ненегативный результат, включая `-0.0`, +сохраняет свои исходные биты. Если AniMesh pointer отсутствует, оригинальный +word `26` просто копируется в mutable copy, и функция возвращает до этого +вычитания. Ни слова профиля, ни cache, ни Slot68 DWORD пока не получили названий +«масса» или «площадь». + +Объединённый вектор под x87 control word `0x007f` задаёт scales `[2, 3, 5]`, +Slot68 raw words `[7, 11]`, коэффициенты двух provider rows `[2, 3]`, диапазон +component `0` `[first=1, count=1]`, cache `27 = 2`, device keys `[1, 0]` с +descriptor values `[5.25, 0.5]`, original word `26 = 2000` и прежнюю mutable +копию word `26 = -999`. Исходные функции выдают: + +| Поле | Десятичное значение | Raw bits | +| --- | ---: | --- | +| `owner +0x530` | `84` | `0x42a80000` | +| `owner +0x534` | `660` | `0x44250000` | +| `owner +0x538` | `1823.75` | `0x44e3f800` | +| `owner +0x4d8` (mutable word `26`) | `1166.25` | `0x4491c800` | +| provider row `+0x20`, row `0` | `744.5` | `0x443a2000` | +| provider row `+0x20`, row `1` | `1079.25` | `0x4486e800` | + +Другие векторы показывают различие отсутствующего и нулевого cache, положительный +ноль после negative clamp, сохранение signed zero и прямое сложение в source +order: `2^24 + 1 - 2^24` даёт `0`, тогда как группировка `2^24 - 2^24 + 1` +дала бы `1`. Rust helper повторяет эти тестовые биты в поддерживаемом домене +normal-or-zero; он отклоняет непредставимые значения и промежуточные результаты, +а не заявляет parity для каждого x87 режима или всех допустимых битовых payload. + +Этот x86-вектор подтверждает арифметику оригинальной функции на synthetic +backing. Значения `[2, 3, 5]` относятся только к нему; источник scales в +обычной загрузке — другой, отдельно проверенный путь, привязанный к GOG +сборкам: `ArealMap.dll` SHA-256 +`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`, +`World3D.dll` `17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`, +`AniMesh.dll` `65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`, +`Effect.dll` `386a3e8db1ce23f61957825d633c7dae1ed40c25cab0cf10a534385fd7cee789` +и `Control.dll` `bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`. +Сначала `AniMesh::FUN_10002ff0` (RVA `0x2ff0`) вызывает `LoadAniMesh`, затем — +`LoadControlSystem` на RVA `0x32e7`. После возврата из этой фабрики обычный +`ArealMap` путь `0x14fd0/0x15290` вызывает `World3D.CreateObject` +(`0x7cb0/0x7a50`), который до возврата посылает Agent event `0` через primary +vtable slot `+0x38`. Agent RVA `0x1a80` сначала вызывает базовый обработчик +`0x17df0`, затем рассылает компонентам event `4` с null payload. Конструктор +AniMesh задаёт `+0x158/+0x15c/+0x160` равными `1.0`. +Обработчик модели `0x75b0/0x6fc0` привязывает интерфейсы и матрицу, Effect RVA +`0x3d30` и Control RVA `0x1ce00` сохраняют host/service указатели, а Control +`0x1fe00` меняет только локальный collision-флаг. Эти ветви не вызывают AniMesh +scale setter `0x14770` и не пишут поля масштаба. После них Control проходит +`0x31b00 -> 0x314f0 -> 0x7830 -> 0xf940`, то есть в FAC0 входит с constructor +scales `[1, 1, 1]`. Это подтверждено только для обычной начальной загрузки и +rebind; отдельные AddNewObject/mirror scale setters и более поздние placement/ +tick пути в этот вывод не входят. + +Теперь полный Rust loader вызывает FAC0 в `build_control_device_projection` +после сборки provider rows и устройств. Игнорируемый тест +`full_load_builds_effective_control_device_table_for_mine_unit_dat` прогнан на +лицензированном `min_l_n1.dat`: он проверяет 11 устройств, сопоставление всех +25 provider rows и весь упорядоченный вектор `row +0x20`. Агрегаты mine равны +`+0x530 = 0x470833fc`, `+0x534 = 0x4897e712`, `+0x538 = 0x4d9452cb`; исходное и +начальное mutable word `26` оба равны `0x7f7fffff`. Это измеренный результат +Rust loader-а на оригинальных ресурсах, но не сравнение полного load результата +с оригинальным loader-ом. Имена и физические единицы полей остаются неизвестны. + #### Входной класс Control при загрузке Unit DAT Этот класс задаётся до разбора CTLD и не выводится из device tag или MSH- |
