docs(control): explain component device overrides
Docs Deploy / Build and Deploy MkDocs (push) Successful in 2m4s

This commit is contained in:
Valentin Popov committed 2026-10-22 12:49:24 +04:00
1 parent ccb5af0e37
commit 5c400e7e40
3 files changed
+133 -16

No files matched your search

+27
View File
@@ -247,6 +247,33 @@ CTLD и связанные resources структурно читаются, coun
известны. Не названы все секции, shape types, coefficients и точный contact известны. Не названы все секции, shape types, coefficients и точный contact
solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags. solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags.
Одна цепочка Unit DAT → AniMesh descriptor → Control теперь восстановлена
статически в [главе о Behavior и Control](../tomes/06-behavior.md): raw
`parent_or_link` копируется в descriptor. В non-`EXTO` registry path parent ID
разрешается в индекс устройства и сдвигает локальный индекс только когда parent
ID не равен `-1` и IID `0x202` доступен; иначе используется raw локальный индекс.
В этом потоке нулевой raw kind пропускает узел вместе с потомками, поэтому общий
assigned ID нельзя получать из preorder-позиции записи. Это закрывает только
передачу полей в native loader-е; Rust ещё должен строить такую же проекцию.
Control loader также различает добавление устройств и применение одной записи
к уже выбранному устройству. Для tag `19` найденные поля и ветви описаны в
главе, но эффективное слияние пока не реализовано. В Rust parser-е
`Unsupported { typed_record_index: Some(_) }` означает, что разбор остановился
внутри списка typed records; без индекса остановки unsupported может относиться
к effect-tail после уже разобранного списка. Эти случаи нужно различать.
Ветка default native-фабрики вызывает конструктор по RVA `0x20800`, поэтому
неизвестный Rust parser-у tag не доказывает отказ оригинального loader-а. Подготовка assets
сейчас пропускает Control refs у non-geometric prototype, поэтому отсутствие
подготовленного CTL должно оставаться явным unavailable result, а не
трактоваться как пустой список.
Глобальные Control selectors `1/2` и setter selector `1` по tag `19` описаны
там же только по статическому разбору `Control.dll`; execution oracle для этих
глобальных вызовов нет. Остаются открытыми проверка effective merge на живых
объектах, полный охват registry types и host factory paths, а также runtime
жизненный цикл множителя `+0x4c`.
Для `Control.dll` (SHA-256 Для `Control.dll` (SHA-256
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26 `bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26
getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по
+17 -12
View File
@@ -448,21 +448,28 @@ struct UnitDatRecord112 {
char archive_name[32]; // +0x00 char archive_name[32]; // +0x00
char resource_name[32]; // +0x20 char resource_name[32]; // +0x20
uint32_t kind; // +0x40, в корпусе всегда 1 uint32_t kind; // +0x40, в корпусе всегда 1
int32_t parent_or_link; // +0x44 int32_t parent_or_link; // +0x44, context-dependent local index
char description[32]; // +0x48 char description[32]; // +0x48
uint32_t tail0; // +0x68, opaque uint32_t tail0; // +0x68, opaque
uint32_t tail1; // +0x6C, opaque uint32_t tail1; // +0x6C, immediate_child_count
}; };
#pragma pack(pop) #pragma pack(pop)
``` ```
Во всех проверенных records `archive_name == "objects.rlb"` и `kind == 1`. Во всех проверенных records `archive_name == "objects.rlb"` и `kind == 1`.
Поле `parent_or_link` встречается как `-1`, `0`, `1` и другие небольшие индексы `tail1` задаёт `immediate_child_count` — число непосредственных дочерних
и связывает компоненты составного unit; точная предметная классификация ссылки записей при построении дерева Unit DAT; оно не opaque. Поле `parent_or_link`
ещё не закрыта. `description` -- человекочитаемое описание компонента. В Части 2 встречается как `-1`, `0`, `1` и другие небольшие индексы. В подтверждённой
есть поля `description[32]`, полностью заполненные без NUL; это валидная bounded цепочке Unit DAT → AniMesh → Control оно становится локальным индексом. Для
string длиной 32 байта. Требование обязательного terminator применяется только `INTO` индекс задаёт устройство Control относительно первого устройства
к полям, где оно доказано форматом. `tail0` и `tail1` нельзя нормализовать. родительского компонента, а `EXTO` обрабатывается отдельной ветвью. Поэтому
поле нельзя трактовать как универсальный parent ID или одинаковую ссылку для
всех registry types. Детали передачи описаны в [главе о Behavior и Control](06-behavior.md).
`tail0` сохраняется как raw DWORD без установленной семантики. `description` --
человекочитаемое описание компонента. В Части 2 есть поля `description[32]`,
полностью заполненные без NUL; это валидная bounded string длиной 32 байта.
Требование обязательного terminator применяется только к полям, где оно доказано
форматом.
Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records Проверено 425 файлов / 5 219 records Части 1 и 676 файлов / 8 145 records
Части 2. Все соответствуют формуле размера, `kind == 1` и Части 2. Все соответствуют формуле размера, `kind == 1` и
@@ -470,10 +477,8 @@ string длиной 32 байта. Требование обязательног
При построении mission prototype graph FParkan сохраняет для каждого root При построении mission prototype graph FParkan сохраняет для каждого root
упорядоченный список этих raw records. Список передаётся в упорядоченный список этих raw records. Список передаётся в
`MissionObjectDraft` без преобразования `kind`, `parent_or_link`, description `MissionObjectDraft` с исходными `kind`, `parent_or_link`, description и tails;
или tails в предполагаемые роли. Так runtime уже располагает исходными данными эти данные пока не объединяются в таблицу эффективных устройств Control.
составного unit для будущих Control/physics/AniMesh consumers, но не выдаёт
структурное сходство за доказанную семантику.
Каждый graph edge от `UnitDatRoot` к effective component и далее к его MSH Каждый graph edge от `UnitDatRoot` к effective component и далее к его MSH
dependency также несёт `unit_component_index`. Это индекс исходной записи в dependency также несёт `unit_component_index`. Это индекс исходной записи в
+89 -4
View File
@@ -542,10 +542,95 @@ component как ordered raw-string/resource provenance, но не для при
этим строкам смысловых имён до трассировки private update methods. этим строкам смысловых имён до трассировки private update methods.
Runtime сохраняет ordered raw Unit DAT records рядом с каждым mission object Runtime сохраняет ordered raw Unit DAT records рядом с каждым mission object
draft. Это создаёт проверяемую границу передачи данных от loader-а к будущему draft. Теперь подтверждена связь Unit DAT с descriptor-ом AniMesh, который
Control consumer-у: никакой компонент пока не получает имя `Control` только передаётся в Control: порядок узлов задаётся деревом immediate-child counts,
по `kind`, `parent_or_link` или description; semantic binding появится лишь а `parent_or_link` попадает в descriptor как локальное поле. Его смысл зависит
после trace private update/load methods. от registry type и состояния Control manager-а; в частности, для `INTO` это
локальный индекс устройства, а `EXTO` проходит отдельную ветвь.
### От Unit DAT к Control descriptor
В GOG `ArealMap.dll` (SHA-256
`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`)
`FUN_10016a10` (RVA `0x16a10`) читает запись Unit DAT в дерево узлов по
`0x78` байт: перед сырыми полями узла находятся указатель и количество детей,
а байты файла копируются начиная с node `+0x0c`. `FUN_10015290` (RVA
`0x15290`) передаёт каждый непосредственный узел корня в `FUN_10014b90`
(RVA `0x14b90`) с начальным parent ID `0`.
Для узла с ненулевым raw kind `FUN_10014b90` формирует descriptor размером
`0x4c`: две строки ресурса копируются из node `+0x0c` и `+0x2c`, поле
descriptor `+0x40` получает parent ID, `+0x44` — DWORD node `+0x50`, а
`+0x48` начинается со значения `-1`. Из-за смещения raw-полей на `0x0c`
node `+0x50` — это ровно `parent_or_link` по смещению `0x44` в записи Unit
DAT. Затем `ArealMap` посылает owner-у событие `6 / 0x80000020` через vtable
slot `+0x34`.
В `AniMesh.dll` (SHA-256
`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`)
dispatcher `FUN_10001370` (RVA `0x1370`) передаёт это событие в
`FUN_10003760` (RVA `0x3760`). Тот выбирает следующий свободный ID, записывает
его в descriptor `+0x48` и сохраняет descriptor в списке компонентов.
Вернувшись после virtual call, `FUN_10014b90` передаёт этот ID как parent ID
рекурсивного обхода детей. Поэтому родительская связь Control descriptor-а
строится по фактически назначенному ID в обходе дерева, а локальный индекс
устройства приходит из `parent_or_link`.
У этого обхода есть ранний выход: если raw kind в node `+0x4c` равен нулю,
узел и его дети пропускаются. AniMesh начинает со служебного root ID `0`, но
из-за пропусков и других записей назначенный ID нельзя в общем случае считать
равным индексу узла или номеру его записи в Unit DAT.
Для registry type `INTO` и других не-`EXTO` значений `FUN_10003760` разрешает
parent ID через Control IID `0x202`, virtual slot `+0x44`, только если parent ID
не равен `-1` и интерфейс IID `0x202` доступен. `Control.dll`
`FUN_1002ecc0` (RVA `0x2ecc0`) возвращает индекс первого устройства, чьё поле
`+0x08` равно этому ID, или `-1`, если совпадения нет; AniMesh прибавляет
возвращённое значение к локальному индексу `+0x44` без отдельной проверки
`-1`. Если parent ID равен `-1` или IID `0x202` недоступен, используется сам
локальный индекс без смещения. Отсутствие устройства с parent ID — отдельный
случай: native path даёт индекс на единицу меньше локального, и Rust-проекции
нужно проверить это поведение явно. `EXTO` проходит отдельную ветвь, поэтому
это правило нельзя распространять на все registry types.
### Как Control добавляет и переопределяет устройства
В `Control.dll` (SHA-256
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`)
`FUN_10008b10` (RVA `0x8b10`) выбирает путь по полю descriptor `+0xc8`.
Значение `-1` запускает цикл по typed records; фабрика `FUN_1002d4b0` (RVA
`0x2d4b0`) создаёт по устройству на запись и сохраняет их порядок. Иное
значение вызывает `FUN_1002d890` (RVA `0x2d890`) только один раз; цикл по
typed-record count в этой ветви отсутствует. Следовательно, подтверждённый
override path применяет первую typed record и не добавляет новый объект.
Так модуль меняет характеристики уже существующего слота, не увеличивая число
устройств. Неизвестный tag в Rust CTLD parser приводит к `Unsupported` и может
оставить typed-record list неполным. Native factory имеет отдельный default
путь `0x20800`, поэтому такой результат парсера сам по себе не доказывает отказ
оригинального loader-а и не позволяет объявить Rust merge полным.
Для tag `19` фабрика создаёт объект с его исходными классом и vtable. Базовый
конструктор `FUN_100218b0` задаёт multiplier `+0x4c = 1.0`. Override через
`FUN_1002d890` вызывает у выбранного объекта vtable slot `+0x1c`, то есть
`FUN_10021d50` с флагом инициализации `0`. Этот вызов не создаёт объект и не
заменяет класс или vtable. Он сохраняет уже существующий ключ `+0x04`; сохраняет
текущее значение multiplier `+0x4c` (которое могло измениться после
конструктора); и копирует коэффициенты из CTLD в блок `+0x54`. Поле `+0x50`
обновляется лишь когда слово записи `+0x18` не равно `-1`. Сопоставленные
записи значений прибавляются к текущим полям объекта `+0x1c/+0x20`, а не
сбрасывают их. Поэтому «переопределение» здесь означает обновить часть данных
того же устройства, с ограничениями конкретного метода.
Статический разбор глобального Control interface показывает aggregate getters
`1/2` (`FUN_1002b410`, RVA `0x2b410`) и setter `1` (`FUN_1002ba30`, RVA
`0x2ba30`) для устройств tag `19`. Для конечных значений getter `1` вычисляет
`sum(base * multiplier) / sum(base)`, а getter `2` — `sum(base)`. При встрече
отрицательного base selector `1` сразу возвращает `1.0`, а selector
`2` — этот base. Если подходящих записей нет или сумма base неположительна,
getter возвращает неуспех без записи результата. Setter пишет multiplier `+0x4c`
только устройствам tag `19` с положительным selector-0 значением; пустой набор
возвращает успех. Это вывод статического анализа для конечных входных значений,
без отдельного native execution oracle для глобальных selectors.
Отдельная ветка того же GOG `AniMesh.dll` показывает, когда создаётся Behavior Отдельная ветка того же GOG `AniMesh.dll` показывает, когда создаётся Behavior
для host object. В `FUN_10002ff0` (RVA `0x2ff0`, SHA-256 для host object. В `FUN_10002ff0` (RVA `0x2ff0`, SHA-256