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

+89 -4
View File
@@ -542,10 +542,95 @@ component как ordered raw-string/resource provenance, но не для при
этим строкам смысловых имён до трассировки private update methods.
Runtime сохраняет ordered raw Unit DAT records рядом с каждым mission object
draft. Это создаёт проверяемую границу передачи данных от loader-а к будущему
Control consumer-у: никакой компонент пока не получает имя `Control` только
по `kind`, `parent_or_link` или description; semantic binding появится лишь
после trace private update/load methods.
draft. Теперь подтверждена связь Unit DAT с descriptor-ом AniMesh, который
передаётся в Control: порядок узлов задаётся деревом immediate-child counts,
а `parent_or_link` попадает в descriptor как локальное поле. Его смысл зависит
от 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
для host object. В `FUN_10002ff0` (RVA `0x2ff0`, SHA-256