Files
fparkan/docs/appendices/knowledge-boundaries.md
T

633 lines
59 KiB
Markdown
Raw Normal View History

2026-06-22 01:58:51 +04:00
# Границы знания
Этот раздел перечисляет области, где контракт ещё не закрыт полностью. Они не
мешают безопасному чтению и lossless сохранению, но не должны превращаться в
authoring API без динамического подтверждения.
## Render state
Доказаны frame boundaries, world traversal, material resolve и крупные проходы.
Не доказаны символами точные имена renderer vtable slots, полный набор CShade
state transitions и окончательный порядок части transparent/FX/shadow subpasses.
Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX
среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state
log на минимальных сценах с одним типом материала.
## FXID field-level semantics
Размеры команд, resource references, lifecycle, flags families и используемые
time modes известны. Не закрыто значение каждого поля body opcodes 1--10,
отсутствующий во всех проверенных каталогах opcode 6 и точные формулы редких
time modes.
Закрывающий эксперимент: изменять по одному полю копии эффекта, воспроизводить
его в контролируемой сцене и логировать runtime command object, emitted
primitives, sound events и reads в `Effect.dll`.
## Script VM
Доступны packages, symbols, event sections, variable declarations и version
checks. Полная instruction grammar `.scr`, semantics opcodes и serialization
state ещё не восстановлены.
Для GOG `ai.dll` статически восстановлены пять ветвей evaluator-а 16-byte
записей условий по RVA `0x5180`, включая проверки типов, поиск объектов,
короткие выходы из обхода списков и запись байта завершения. Однако не найден
код-производитель этого массива и контракт его владельца с runtime. Reader
`FUN_10011b20` хранит описатели событий и вложенных записей `.scr` во внутренних
контейнерах с шагом `0x14` и `0x20`; сами дисковые структуры имеют переменную
длину из-за length-prefixed имён и списков ссылок. Связь этих контейнеров с
массивом evaluator-а не доказана. Виртуальные методы,
через которые проходят проверки, остаются внешней границей, а сам evaluator
не сравнивался с работающим оригиналом. Описание этих ветвей — статический
вывод по одному известному `ai.dll`, а не утверждение о совпадении во время
исполнения.
Для начального сценарного события закрыт более узкий путь. В проверенной GOG
сборке `ai.dll` SHA-256
`c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc` функции
`FUN_10011f40` (RVA `0x11f40`) и `FUN_10011f10` (RVA `0x11f10`) выбирают первое
имя-событие C-string `Init` и запускают его инструкции по порядку. Для координат
из TMA найден и проверен ещё один участок: `iron3d.dll` SHA-256
`d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`,
`FUN_100392e0` (RVA `0x392e0`) выполняет x87 `FISTP qword` и передаёт младшие
DWORD координат пятым и шестым аргументами `CreateSuperAI`. Затем
`ai.dll!FUN_10005c70` (RVA `0x5c70`) преобразует DWORD в `float`, а `Handler(19)`
возвращает его к DWORD усечением.
Две x86-проверки вызвали эти оригинальные DLL-функции на изолированной
синтетической памяти. Caller oracle подтвердил AutoDemo-координаты
`(500.2857, 751.8217) -> (500, 752)`, ties-to-even в nearest-режиме и переход
`-0.6 -> 0xffffffff`. AI oracle проверил оба набора AutoDemo anchor-ов,
дублирующий `Init`, имя `Init\\0suffix\\0`, отсутствие события, а также
нативную связку `FUN_10005c70` → `Handler(19)`: DWORD `(0xffffffff, 10001)` стал
полями `(2^32, 10001.0)` и varset-записями `(0, 10001, 2)`. Эти проверки
подтверждают только изолированные участки ABI.
В одном read-only snapshot запущенной GOG AutoDemo были связаны записи кланов
Iron3D и указатели `GetSuperAI` из `ai.dll` (таблица RVA `0x55398`). Клан 0 имел
исходные float-координаты `(500.2857, 751.8217)` и поля AI
`(+0x80, +0x84, +0x7c) = (500.0, 752.0, 0)`; клан 1 имел `(728.0, 449.0)` и
`(728.0, 449.0, 1)`. Первый набор связывает те же значения в одном процессе и
выбирает ближайшее округление среди направлений округления x87. Синтетические
tie-векторы проверяют, как сам `FISTP` реагирует на каждый RC, но не измеряют
control word процесса.
Отдельно проверены загрузка лицензированного AutoDemo в Rust и preview
лицензированной кампании. AutoDemo даёт raw TMA anchor-ы
`[(500.2857, 751.8217), (728.0, 449.0)]` и записи `[(500, 752, 0),
(728, 449, 1)]`; preview кампании сохраняет package и статус `Unsupported`,
не применяя частичные записи. Это подтверждает загрузчик и узкий Init resolver
Rust.
Нативный конструктор показывает предупреждение при координате выше `10000`, но
продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение;
на первом `FISTP qword` он явно завершает загрузку ошибкой для NaN, infinity и
значений вне signed-qword диапазона. В первом событии `Init` Rust поддерживает
только action-форму `Handler(19)` (`control word` и `result reference` равны
`0xffffffff`). Неизвестная инструкция даёт `Unsupported` в preview, не применяя
накопленные частичные записи; при полной загрузке это ошибка. Это ограничение
Rust исполнителя, не правило оригинального dispatcher-а. Ошибочные ссылки и
типы остаются ошибками в обоих режимах.
Для GOG-сборки также найден статический host call path в `Init`. В
`iron3d.dll!FUN_1005ff40` (RVA `0x5ff40`) одна ветвь сначала проходит записи
кланов и вызывает для них `FUN_100392e0`, затем вызывает `FUN_10039440` (RVA
`0x39440`). Эта функция передаёт AI callback-у пару явных аргументов через
vtable slot `+0x14`. В `ai.dll!FUN_10001ae0` (RVA `0x1ae0`) аргумент callback-а
записывается в `AI+0x3fc`; затем объект `AI+0x438` получает вызов Behavior
vslot `+0x2c`. В проверенной `Behavior.dll` (SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`)
`FUN_1001adb0` (RVA `0x1adb0`) выполняет начальную подготовку расписания.
Метод ищет первый `Init` только при нулевом latch
`AI+0x8a8`, а при dispatch временно ставит и снимает recursion guard
`AI+0x78`. В этой ветви поэтому известны callback ingress и порядок
warm-start → `Init`, но смысл аргумента callback-а здесь не сопоставлен с Rust
`MissionMode`.
Изолированный x86 oracle вызывает исходные `iron3d.dll!FUN_10039440`,
`ai.dll!FUN_10001ae0` и script dispatcher на синтетических записях и проверяет
режим, guard, latch, Handler(19) writes и повторный вызов. Отдельный oracle
исполняет `Behavior.dll!FUN_1001adb0` с искусственно заданной очередью, сверяя
срок, шаг LCG и очистку полей. Это подтверждает ABI и локальные переходы исходных
функций, но не доказывает, что полная штатная загрузка выбранной миссии в
оригинальном процессе доходит до этой ветви в том же порядке. Метод
`FUN_10001ae0` оставляет latch `+0x8a8` сброшенным, когда `Init` не найдено;
если оно найдено при уже занятом guard `+0x78`, dispatch пропускается, но latch
всё равно устанавливается. Rust пока запускает обработанный поднабор при каждой
полной загрузке и не моделирует аргумент callback-а `AI+0x3fc`, guard `+0x78` или
latch `+0x8a8`.
Rust `load_mission` принимает явный `queue_time_ms`, а не читает оригинальное
время очереди. Новый `Engine` начинает локальный Behavior RNG с seed `1` и
учитывает шаги генератора этой стартовой последовательности; не восстановлены
другие потребители исходного генератора, поэтому равенство полной случайной
последовательности не утверждается. Начальная фаза выполняется после регистрации
объектов в создаваемом мире Rust;
это интеграционный выбор runtime, а не подтверждённая эквивалентность всех
host construction phases. Не проверена другая лицензированная миссия с
непустым `Init`. Вызов `Behavior.dll!FUN_10019e80` (RVA `0x19e80`, SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) исполнялся
на синтетических интерфейсах. Путь членства в реальных distributor-списках
связан с полями TMA и Unit DAT: `World3D!CreateObject` передаёт selector
и class word через дескриптор в Behavior (RVA `0x7e55`, `0x7e62`), а
`Behavior.dll!FUN_10005e60` (RVA `0x5e60`) проверяет высокий бит registration
key, AI и distributor. Selector из первого DWORD после имени ресурса выбирает
слот `GetSuperAI`; таблица этой функции находится в `ai.dll` RVA `0x55398`.
Хеши `MisLoad.dll`, `iron3d.dll`, `ArealMap.dll`, `World3D.dll` и
`Behavior.dll`, использованных для цепочки, перечислены в
[справочнике TMA](../reference/tma.md).
Отдельный режимный бит Behavior нельзя выводить из `MissionMode`. Конструктор
Behavior (`Behavior.dll`, RVA `0x3600`) вызывает `World3D!stdGetGameMode` (RVA
`0x14750`), который читает host-global по RVA `0x795130` в `World3D.dll` (SHA-256
`17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e`). В
обычном пути `stdInitGame` (RVA `0x13e90`, запись режима около `0x14040`)
формируется отдельное host-значение; в наблюдавшемся запуске AutoDemo getter
вернул `4`, и бит `0x2` был снят. Это объясняет соответствующий gate при
создании Behavior, но не устанавливает отображение между host-режимом и Rust
`MissionMode`.
Наблюдение обычного запуска лицензированной GOG AutoDemo подтвердило четыре
добавления с ключами `0x80000003..0x80000006` в два клановых списка. Полная
загрузка Rust сохраняет соответствующие `ObjectHandle` в точном доступном слоте
`GetSuperAI` до стартового `Init`; preview не создаёт членств. Лицензированный
тест связывает эти записи с TMA ID 4–7, selector и Unit DAT class. Это
подтверждает только состав списков, не точную эквивалентность всей загрузки или
регулярного шага Behavior.
Нативный `Behavior.dll!Add` (RVA `0x1aac0`) и `normalize` (RVA `0x1ac90`)
проверены прямыми x86-вызовами на синтетических записях при x87 control word
`0x007f`, измеренном также у игры на входах обоих методов. Rust повторяет
начальные доли и нормализацию отдельными операциями `f32`, а повторную запись
сопоставляет по `ObjectHandle`, меняя только долю существующего участника.
Пять DWORD после доли хранятся сырыми; ненулевое первое слово закрепляет вес,
смысл четырёх следующих не установлен. Лицензированный AutoDemo-тест проверяет
список TMA-объектов и получившиеся начальные доли по кланам, но это не сравнение
живых весов миссии с полями исходных runtime-записей. Не восстановлено соединение
с реальными интерфейсами ArealMap, нужными `FUN_10019e80`; полный Behavior tick
flow и общий dispatcher событий также остаются открытыми.
Начальные свойства `CurrentOre`/`MaximumOre` восстановлены отдельно от
регулярного обмена. В проверенных `Behavior.dll` (SHA-256
`ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99`) RVA
`0x8790` выбирает начальные значения по class word, `0xb110` регистрирует
дескрипторы кодов 4/5, `0xb470` пишет только код 5, а `0xb5b0` читает код 4
как дополнительное слово и код 5 как основное. `MisLoad.dll` (SHA-256
`c607762bb67db14ca530927c8e1347339b62b0a163ac90c708ee83142292e2bf`) RVA
`0x40b0` выбирает первое свойство TMA с точным именем `CurrentOre` и передаёт
его `raw_value[1]`; setter сохраняет биты напрямую. В AutoDemo snapshot на этих
входах было восемь объектов Unit DAT, у всех raw `CurrentOre` равен нулю;
четыре объекта класса `0x01008000` не вошли в distributor, но остались среди
инициализированных Rust-состояний. Поэтому путь нулевого значения наблюдался
наживую, но ненулевое TMA-значение в живой миссии пока не проверено.
Синтетический x86 oracle вызвал `Behavior.dll!FUN_10008790` напрямую для классов
`0x80000004`, `0x80000008`, `0x01002000`, `0x01004000` и `0x01008000`; перед
вызовом в записи были значения `CurrentOre=123` и `MaximumOre=-1`, так что
проверено их перезаписывание. Тест выставляет `a64=1`, чтобы отключить отложенный
side effect обёртки setter-а. Это подтверждает ветви initializer-а, но не весь
путь его запуска и связи с остальными фазами объекта. Другой изолированный x86
oracle проверяет ненулевое raw-слово `CurrentOre`, только чтение `MaximumOre` и
перестановку строк дескрипторов на синтетических записях свойств.
Не проверен селектор 4 для внешнего config-like ресурса: в наблюдавшемся
AutoDemo его вызова нет, а установленный `Behavior.ini` не содержит
`Mine_MaxOre`, `Storage_MaxOre` или `Transport_MaxOre`. Поэтому Rust пока
воспроизводит доказанные class defaults (500, 4000 и 2000) и не заявляет
поддержку конфигурационного переопределения. Отдельно не доказано, что каждый
Unit DAT создаёт тот же Behavior-компонент во всех host factory paths. Rust
ограничивает новую запись полными TMA-объектами с Unit DAT; preview, прямые
prototype roots без Unit DAT и отложенные spawn после исходной загрузки вне
этой модели. Путь в `ArealMap.dll` (SHA-256
`a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea`) к
`CreateObject` по RVA `0x14fd0`/`0x15290` показывает выбор World3D factory
selector `3` или `4` по старшему биту. Для ветки GOG `AniMesh.dll` (SHA-256
`65659bf67f20e5be77d67a5b2f59968f61b94293143807fa59ca451c560c4f7e`) RVA
`0x2ff0` показывает одну ветку создания: после QueryInterface для IID `0x204`
выполняется отдельный импорт `GetQueue()`, а на возвращённом объекте вызывается
метод через vtable slot `+0x30`. `CreateBehaviour` вызывается только при
ненулевом результате этого метода и host class `3` или `4`; class `4` получает
адреса полей prototype `+0x140` и `+0x160`. Семантика метода slot `+0x30` не
установлена. Другие factory paths, нулевой результат и spawn после исходной
загрузки остаются за границами подтверждённой ветки.
Для `Handler(30)` отдельно подтверждены обратный вызов `iron3d.dll` RVA
`0x611d0` (SHA-256 `d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa`),
начальные поля IGame и проверки режима/состояния. Обработчик выбора миссии
выставляет режим 1 (RVA `0x29142`), а обработчик таймера — режим 3 (RVA `0x70f3`).
Небольшая тестовая x86-программа сравнивает этот обратный вызов с исходной DLL
на синтетических структурах. Пока не восстановлен сквозной путь от выбора и
инициализации миссии к первому событию Handler(30): Rust загружает миссию, но
ещё не выполняет штатный `stdInitGame`, переход состояния `5 -> 4`, запуск и
игровой цикл с полной диспетчеризацией событий. Поэтому
совпадение изолированного обратного вызова не доказывает совместимость живого
сценария.
2026-06-22 01:58:51 +04:00
Закрывающий эксперимент: найти dispatcher loop в `ai.dll`, сопоставить jump
table с instruction sizes, построить disassembler и сравнить выполнение
коротких scripts с оригиналом. Для evaluator-а условий отдельно проследить
создание записей `this + 0x34`, выполнить те же входные данные в оригинале и
сверить байт завершения, порядок поиска, ранние выходы и граничный случай
`payload == count`.
2026-06-22 01:58:51 +04:00
## Saves and campaign state
Найдены `saveslots.cfg` и `missions/dispatcher.ini`, но binary savegame payload,
serialization World3D/AI/script/RNG и migration rules не закрыты.
Нужны сохранения оригинала в контролируемых состояниях: старт миссии, изменение
позиции, здоровья, order/path, FX/timer, script variable, research/economy,
mission completion, pause и non-default game time.
## Physical/control formats
CTLD и связанные resources структурно читаются, count patterns и variants
известны. Не названы все секции, shape types, coefficients и точный contact
solver. То же относится к редким MSH auxiliary streams и части CTPT/NDPR flags.
### Класс Control при загрузке Unit DAT
Для двух подтверждённых полных Unit-DAT путей Rust теперь сохраняет класс,
который передаётся конструктору Control, вместе с ветвью-источником. Лицензированный
`min_l_n1.dat` проходит high-bit route и даёт класс `3`; synthetic-тесты покрывают
оба маршрута, известные FourCC и отсутствующие root metadata. Отдельное
исполнение проверило только чистый getter `Terrain.dll+0x33ae0` (SHA-256 и точный
RVA приведены в технической главе). Остальная цепочка через ArealMap, World3D,
Terrain/AniMesh и создание controller восстановлена по статическим инструкциям,
таблицам и конфигурации. Она не означает исполнения оригинального полного
loader-а, Control activation, движения или периодического tick.
Не моделируются произвольный ненулевой host с неудачным QueryInterface, preview
без полного Unit DAT, отложенный spawn и другие host/factory routes. В частности,
generic failed-query путь native оставляет class `0` и не читает FourCC; это не
эквивалентно canonical no-host lookup корневой registry entry. Остаточные вопросы
об активации и игровом поведении описаны в технической главе.
Одна цепочка 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-позиции записи. Полная загрузка миссии
в FParkan строит упорядоченную базовую проекцию таблицы
Control для объектов с Unit DAT: корневые записи добавляются, `EXTO` добавляет
строки с ключами глобальной MSH-иерархии, а `INTO` обновляет первую строку
выбранного индекса либо добавляет список при sentinel `-1`. Проверка полной
загрузки на оригинальном `min_l_n1.dat` получает 11 строк: 10 корневых записей,
четыре замены `INTO` и одну добавленную строку дочернего `EXTO`. Ветви C4/C8 и
добавление `EXTO` подтверждены машинными инструкциями оригинальных модулей, но
полный результат FParkan не сравнивался с таблицей из запущенной игры. Тест на
искусственном дереве также проверяет ID после пропущенного поддерева с raw kind
`0`. Проекция хранит общие поля и происхождение записей; она не исполняет все
обработчики Control.
Control различает добавление устройства и применение одной записи к уже
выбранному устройству. Исполнение оригинальной 32-битной DLL на искусственных
данных подтверждает общие поля переопределения для получателей tags `2`, `3`,
`4`, `5`, `8`, `9`, `10`, `15`, `19`, `21`, `25` и `27`: остаются прежними слот,
ключ, ID компонента, множитель и класс, а коэффициенты копируются из новой
записи. Обёртка tag `2` по RVA `0x29650` дополнительно пересчитывает два кэша
и обнуляет массив состояния map-строк; это выполнено на native DLL, но пока
не моделируется в базовой Rust-проекции. Её callback `0x29940` проверен только
с неизменной пустой парой ресурсов; стрельба не проверена.
Искусственный tag `9`-объект создан
без FX-хоста (`+0x3c == 0`), а запись не содержит строк таблицы map; поэтому
работа FX-хоста здесь не проверялась. В 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, а не
трактоваться как пустой список.
Global Control getters `1/2` и setter `1` проверены в native x86 oracle для
искусственных factory/descriptor pairs `9`, `15` и `19`, а в Rust реализованы
для текущего descriptor tag `19` с factory tags `9`, `15`, `19` и `25`. Для tag
`25` общий getter slot `+0x10` подтверждён чтением vtable, но агрегатный вызов
этого класса native oracle не запускал. Rust API обращается к строкам проекции
конкретного живого `ObjectHandle`; это запрос к уже загруженной таблице, а не
исполнение Control или Behavior tick. Setter `17`, остальные factory tags и
внешние эффекты их методов не включены. Для tag `9` selector `0` использует
отдельный scale `+0x11c`; остальные поддержанные классы используют коэффициент
напрямую. Пустой набор и нулевой знаменатель остаются native no-result, а
неподдержанный factory tag и расчётный диапазон произведения/отношения за
нормальным диапазоном `f32` становятся явной ошибкой Rust.
Проекция полного mission load недоступна для неполного списка typed records,
Control resource у non-geometric prototype и некоторых неразрешённых EXTO node
mappings; preview и объекты без Unit DAT root её не строят. Её соответствие
полной загрузке игры для всех типов компонентов и factory paths ещё не
установлено. Полный Rust load AutoDemo проверяет доступность всех восьми таблиц
и числа строк `14, 12, 15, 13, 12, 12, 11, 11`; это не сравнение с живой
таблицей оригинала. Setter `1` меняет multiplier `+0x4c` выбранных строк, но расписание
и lifetime этих объектов после конструктора и в отложенной загрузке остаются
непроверенными.
Для `Control.dll` (SHA-256
`bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e`) tag-26
getter selector `0x300` восстановлен локально: RVA `0x21d00` читает provider по
ключу `fixed_words[1]`, затем умножает provider, CTLD coefficient `fixed_words[11]`
и поле объекта `+0x4c` именно в таком порядке. Provider getter RVA `0xdc40`
читает type-1 поле `+0x14` строки, которую `FUN_10008b10` (RVA `0x8b10`)
добавляет из NDP в таблицу с шагом `0x2c`; `FUN_1000b240` (RVA `0xb240`)
инициализирует это поле как `1.0`. Начальный Rust snapshot повторяет порядок
NDP строк корневого ресурса и примонтированных `EXTO` (без первой строки
последнего), сохраняя исходные таблицу и строку как provenance. Ключ устройства
сначала меняется по подтверждённому MSH mapping для `EXTO`, затем это готовое
значение применяется напрямую как индекс provider sequence. Длина этой
sequence определяется NDP: она не дополняется по числу узлов модели и ключ не
пересчитывается ещё раз по числу provider rows. `INTO` provider строк не
добавляет. Для примонтированного `EXTO` с пустым NDP невозможно безопасно
применить native skip-first путь, поэтому Rust явно сообщает unavailable;
таблица из одной строки допустима и даёт нулевой вклад. Если обязательный NDP
sidecar недоступен, запрос `0xe00` сообщает это отдельно, оставляя независимые
агрегатные selectors `1/2` доступными.
Поля `parent_index`, Control flags и начальный stage count для NDP-строк
проецируются из AniMesh по глобальному ordinal; точные правила, native ABI
векторы и лицензированная root/`EXTO` fixture описаны в
[техническом подразделе](script-vm.md#начальная-msh-часть-строки-provider).
MSH-часть проверки подтверждает начальный снимок: сборка runtime parent-ов
остаётся статическим выводом по RVA `AniMesh.dll+0xa460`, а последующие
изменения в полном Control/animation tick-е не проверялись.
Для provider-строки Rust хранит amount `+0x08`, накопитель `+0x10`, обработанное
количество `+0x0c`, долю `+0x14`, стадию `+0x18` и flags `+0x28` раздельно.
Инициализатор `Control.dll+0xf940` кладёт результат selector `3` в текущее и
обработанное количество; при подтверждённых единичных множителях это raw
`max_volume` NDP. Таблица также хранит capacity aggregate `+0x58c`, текущий
aggregate `+0x590`, dirty `+0x598` и начальный gate `+0x618`.
Прямой API `apply_loaded_mission_control_node_delta` сохраняет native четвёртый
аргумент `0`: меняет amount, долю, флаг истощения, aggregate и dirty, оставляя
`+0x10` нетронутым. Публикация `0x200` отражается в CPU-проекции соответствующего
узла модели по правилу исходного обработчика; стадия и соседние узлы не меняются.
Обработанное количество при этой прямой операции остаётся прежним.
API `distribute_loaded_mission_control_provider_amount` переносит
`Control+0x10ba0`: proportional mode делит добавку по свободной ёмкости, а
вычитание распределяет по текущим количествам; sequential заполняет строки
вперёд, снимает с конца и использует исходный запрос даже если общая ёмкость
округлилась вниз. Row updater вызывается с четвёртым аргументом `1`, поэтому
`+0x10` получает фактическую разницу amount. После распределения обязателен
`0x11130(root, true)`.
Три API работают только с CPU-проекцией и не подключены к GPU/window path или
игровому Control clock. Отдельный
`advance_loaded_mission_control_budget` переносит `0x2d340` для фабричных
callbacks `9`, `15`, `19`, `26` и общей default-пары; category lookup использует
текущий descriptor tag из подтверждённого префикса `0..=34`. Ставки сохраняются
в `+0x5dc/+0x5e0`. Шаг выполняется транзакционно, а неподдержанный
специализированный callback, provider или диапазон числа возвращает ошибку без частичного
изменения. Тест с нулевым знаменателем генератора подтверждает откат после уже
выполненного изменения потребителем. Пустая таблица всё равно записывает в оба поля отрицательный ноль;
неактивный callback не требует provider lookup. Class-9 constructor начинает
шесть значений `+0x98..+0xac` с `1.0` и шесть дельт `+0xb0..+0xc4` с нуля;
`INTO` сохраняет эти runtime-массивы. Gate `+0x618` изначально
выключен; событие `7` задаёт его по правилу `gate = (arg == 2)` и вызывает
`0x2dc20`, только если gate изменился. События Control в Rust пока не
исполняются, поэтому ресурсный шаг вызывается явно и не подключён к часам.
Capacity aggregate выведен при подтверждённых единичных множителях Unit DAT;
изменение масштабов `0x9ee0`/`0x9f90` не реализовано. Из-за округления `+0x590`
может стать больше сохранённого `+0x58c`; распределение сохраняет этот signed
capacity по native формуле, не вводя отдельного запрета. Если итоговый stage walk
нуждается в неизвестном состоянии, depletion lifecycle, FX или host callback,
распределение сообщает ошибку и не публикует ни одной частичной записи.
Отдельный
2026-10-29 17:30:41 +04:00
`update_loaded_mission_control_node_stage` реализует наследование parent ratio
и stage, ceiling-арифметику, CPU visible stage и копию processed amount.
Отдельная `propagate_loaded_mission_control_node_stage` переносит `0x11130`:
после обновления строки она сканирует более поздние provider-строки и обходит
найденные поддеревья в native depth-first порядке. Для родителей
`[-1, 0, 0, 1, 3]` порядок — `1, 3, 4, 2`. Переносит унаследованный Control
ratio/stage, флаги `0x80/0x100` и drain количества; маска `0x140` пропускает
только перезапись flags, не drain и рекурсию. Ухудшение определяется ростом
стадии либо флагом истощения `0x10` уже обновлённого родителя. Отдельный
`update_loaded_mission_control_node_stage` сохраняет одиночную семантику
`0x11220`.
Пропагация клонирует Control provider-таблицу и публикует её только после
успешного обхода. Неизвестная parent-связь в просматриваемом суффиксе,
lifecycle/visibility/FX callback или неподдержанное численное значение дают
ошибку без частичных изменений. `update_effects = true` отклоняется, если
стадия любого обработанного узла меняется и требуются неисполненные FX/host
callbacks. Licensed двухстадийный узел шахты подтверждает переход `0 → 1` через
новый 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, который также меняет явный бюджетный шаг.
Native oracle проверил изолированные пути распределения, обновления класса `4`,
стадийную арифметику `0x11220`, обход дочерних provider-строк `0x11130` на
interleaved parents `[-1, 0, 0, 1, 3]` с callback-порядком `1, 3, 4, 2` и
visibility transition `0x11920` с исходными AniMesh callbacks. Отдельный
бюджетный вектор исполняет `0x2d340` → `0x2dca0` → tag 15 `0x22bb0` →
`0x10ba0`: подтверждены коэффициенты `0.5` и `1`, точные amount/ratio и
приоритет категории перед порядком устройств. Положительный вклад генератора
в этом векторе подставлен mock-ом. Отдельный вектор с исходным генератором
tag `19` (`0x22980`, `0x229a0`, `0x22a60`) проверяет полную и ограниченную
rate, изменение provider и собственного множителя генератора; отрицательные
параметры и нулевой знаменатель не проверены. Новый оригинальный вектор
`0x2d340` смешивает tag `19` и tag `26`: supply `0.125` покрывает половину
demand `0.25`, итоговые множители равны `0.6875` и `0.5`, а выходы —
`0x80000000` и `0x3e800000`. Тот же oracle подтверждает tag-26 selector `0x500`,
общий reader `0x21d50` и сохранение поля `+0xa4` при `INTO`. Rust-тест
повторяет бюджетную пару и разрешает tag `26` в projection INTO guard, оставляя
теги `1`, `17` и `30` явными границами. Native-векторы бюджетного шага
подтверждают текущие ставки `+0x5dc/+0x5e0`, включая значения `0.2`/`0.4` у
потребителя tag 15, пустой результат `-0.0` и невырожденную пару исходных
устройств tag 19/tag 15 при `dt = 0.375`; Rust-тест повторяет поля этой пары
на синтетической таблице. Rust переносит class-9 rate/update и шестизначную
adjustment `0x25a90`, tag-26 rate/update и setter selector `0x500`, общую
default callback-пару `0x21860/0x21890`, а также category map только для current
descriptor tags `0..=34`. Остальные ещё не описанные специализированные
callbacks остаются явными ошибками.
Лицензированный `min_l_n1.dat` содержит исходные factory/current tags
`[19, 19, 9, 15, 12, 12, 13, 13, 26, 25, 21]`. Публичный API оставляет всю
таблицу без изменений при не конечном интервале. Для конечного интервала
ignored Rust-тест проходит budget по всем 11 строкам без фильтра. При `dt = 0.5`
обе output rates равны `0x3c23d70a`, первые два множителя tag `19` равны
`0x3f7fd70a`, остальные множители остаются `1.0`, а добавка tag `26` остаётся
нулевой. Эта проверка подтверждает только CPU-проекцию реальной таблицы; она
не запускает native полный mission tick и не подключает Control clock.
Успешные бюджетные native-векторы используют синтетические
NDP/Node38-строки и подтверждают локальные переходы и порядок вызовов, а не
полный Control tick на миссии. Прямые oracle-вызовы `0x11220` для одиночной
строки используют `updateEffects = false`, что исключает ветвь Control FX и
`0x11af0`; в отдельном сценарии класса `9` `0xfca0` возвращается сразу после
смены видимости, поэтому связанное с ним движение не проверено. Хотя у строки
Control выставлен flag `0x102`, слово флагов ANI `+0x14` и производные байты
`+0x111..+0x113` изначально равны нулю. Вызовы AniMesh `0x93c0` и `0xa2a0` обновляют производную геометрию и
видимость, это отдельная непроверенная граница, а не FX callbacks. Распределение
и бюджетный шаг вызывают обязательный `0x11130(root, true)`, но их проверенные
векторы имеют одну строку без смены стадии; они не доказывают исполнение FX или
`0x11af0`. Бюджетная
часть активного `0xbcf0` исполнена на синтетическом хосте класса `10`:
при периоде `500` и нулевом RNG проверены пропуск до deadline и на равенстве
`437.5`, затем ресурсный `dt = 0.5` при времени `500`. На том же ограниченном
хосте без устройств seed `0x12345678` подтверждает новые RNG draw при временах
`500`, `501`, `502`, `503` и ресурсные clocks `0`, `0`, `502`, `502`: deadline
пересчитывается при каждом принятом активном вызове. Проверены также точное
равенство с ненулевым RNG, расход RNG при принудительном `+0xd4` и сохранение
RNG/счётчика в неактивной ветви. Движение и очереди исключены условиями этих
векторов; активация `+0xe0` задана явно. Отдельный вектор
класса `4` исполняет событие `4` через `0x31ae0` → `0x314f0`, проверяя
активность, начальные часы `time - 100`, округление DWORD в `f32` и одну
начальную команду. Геометрические callbacks подставлены явно, таблицы
устройств и shapes пусты; полный загрузочный путь не проверен. Источник времени
и доставка прослежены статически:
World3D RVA `0x6bf0` вычисляет часы из `timeGetTime()` с игровым смещением и
поправкой на паузу, затем выполняет первый обход `6/1`, шаг мировой физики и
второй обход `6/0x1c`. AniMesh RVA `0x1370` передаёт часы Control; общий
обработчик Control RVA `0x7830` численно преобразует unsigned DWORD в `f32`
перед primary slot `+0x58`. Фабрика выбирает RVA `0xcbb0` для класса `9` и
RVA `0xbcf0` для остальных. Это подтверждает статическую цепочку вызовов, но не
её исполнение на миссии: связывание интерфейсов, активация устройств и shapes,
остальные ветви `0xbcf0` и
автоматическое изменение provider через clock и бюджет Rust остаются открытыми. Источник контактного коэффициента прослежен статически,
но не исполнен. Подробные векторы и статические границы приведены в
[техническом подразделе о provider-состоянии Control](script-vm.md#изменение-provider-состояния-control).
Отдельный API `advance_loaded_mission_resources` принимает явное время очереди
и обновляет представленные поля `CurrentOre`, строк распределителя и Control
selector `1`. Тот же расчёт вызывают `step_headless` и `frame` с явным
clock очереди; подготовка ресурсов и мировой шаг завершаются атомарно.
Оконное приложение пока использует static preview. Этот путь не обновляет
Control amount, профили или special object-state gate. Если обязательный
запрос позднего объекта не поддержан, транзакция возвращает ошибку без применения
подготовленных изменений. AutoDemo-тест подтверждает связь настоящих профилей с
этим шагом и проверяет откат при ошибке ресурсов или мировой очереди,
но не полный паритет миссии; отдельный aggregate getter Behavior RVA
`0xa490` в oracle не исполнен.
Behavior selectors `0x1002` и `0x1005` используют значения выбранного
профиля из `behpsp.res`, разобранного до создания нового мира. `0x1002` складывает
только текущие descriptor tag `26` запросы `0xe00`, затем применяет
`Transfer_Ore_OffBoard`; `0x1005` возвращает `Transfer_Power_Out`. Это начальный
снимок профиля и provider-строк. Полная загрузка сохраняет результат у
`ObjectHandle` и возвращает
ошибку, если известный код класса не имеет корректной записи; класс,
отсутствующий в таблице выбора, сохраняет начальные значения конструктора.
Preview и объекты без полного Unit DAT не получают профильное состояние.
Native oracle подтвердил соответствия кода класса выбранной записи, разбор
`.var` и применение второй области данных;
оригинальный aggregate getter Behavior RVA `0xa490` остаётся статически
исследованным, но не исполнен в oracle.
В x86 oracle при x87 control word `0x007f` проверены порядок, signed zero,
отрицательные входы, расширенный промежуточный диапазон и subnormal return.
Rust запрос ограничивает себя normal-or-zero значениями, поэтому явно отклоняет
промежуточные случаи, где результат выходит за поддерживаемый диапазон f32,
даже если позднее умножение вернуло бы его в диапазон. Это ограничение
поддержки Rust snapshot, а не неверный native-результат.
В Unit DAT fixture `UNITS/BUILDS/MINE/min_l_n1.dat` проверена реальная связка
`fr_l_mine.ctl` + `fr_l_mine.ndp`: tag `26`, ключ `2` в локальной NDP таблице
из `22` строк, коэффициент `1.0`. Игнорируемый runtime-тест выполняет полный
загрузочный путь, подтверждает root/`EXTO` размеры `22` и `4` и итоговые `25`
provider rows после пропуска первого `EXTO` row. Он вызывает Rust query по
восстановленному native правилу адресации и получает начальное значение `1.0`;
getter оригинальной DLL здесь не исполняется. Это не запись живого
resource-device значения во время игры.
### Профиль владельца Control из CTLD
Общий проход оригинального `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 или реакцией на движение.
2026-06-22 01:58:51 +04:00
## DirectPlay wire
DirectPlay lifecycle и имена игровых messages известны. Wire framing, payload
schema, reliability flags и `netZipData` требуют записи обмена двух
оригинальных клиентов.
Native interoperability подтверждается только успешным обменом original client
<-> compatibility implementation в обе стороны.
## Shell, HUD, шрифты и локализация
Граница shell подтверждена exports `createShell/getIShell`, `IGUIServer`,
верхнеуровневым UI-pass и файлами `ui/*.cfg`, `DATA/TextRes.cfg`,
`gamefont.rlb` и `sprites.lib`. RsLi framing библиотек закрыт, но widget tree,
layout rules, glyph metrics, sprite command semantics, focus/navigation и HUD
state machine пока не восстановлены до field-level спецификации.
Закрывающий эксперимент: трассировать загрузку `shell_ctrls.cfg`,
`menu_resources.cfg`, `cursor.cfg`, `game_resources.cfg` и `hq.cfg`, сопоставить
GUI object factories и снять command/event captures для меню, HUD, briefing и
диалогов.
## Research, economy and properties
Экспорты `LoadResearch`, `CalcFullResearchCost`, TRF/preload resources и TMA
properties доказывают отдельный слой исследований, стоимости, добычи и
производственных параметров. Формулы стоимости, dependency graph технологий,
inventory/economy transitions и точная типизация всех 16-byte property values
не закрыты.
Закрывающий эксперимент: сопоставить research functions с ресурсами и UI,
снять изменения state на контролируемых покупках/исследованиях и построить
typed schema свойств по consumers, а не по одному имени.
## Rare branches
- `Land.map poly_count > 0`;
- RsLi adaptive methods `0x080` и `0x0A0`;
- Texm formats 556 и 88;
- FX opcode 6;
- редкие material flags и MSH auxiliary streams.
Такие ветки реализуются по бинарному коду и synthetic tests, а статус
corpus-verified получают только после реального файла или runtime trace.