Files
fparkan/docs/appendices/knowledge-boundaries.md
T
Valentin Popov 7ba8a0d84e
Docs Deploy / Build and Deploy MkDocs (push) Successful in 38s
feat(fx): play ambient Control sound commands
2026-10-31 22:06:49 +04:00

100 KiB
Raw Blame History

Границы знания

Этот раздел перечисляет области, где контракт ещё не закрыт полностью. Они не мешают безопасному чтению и lossless сохранению, но не должны превращаться в authoring API без динамического подтверждения.

Render state

Доказаны frame boundaries, world traversal, material resolve и крупные проходы. Не доказаны символами точные имена 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-поза компонента и его преобразование присоединения. При попадании камера сохраняет прежнюю позицию, но оставляет новый поворот. Так приложение связывает мировой MSH-запрос со свободной preview-камерой; это не подтверждает алгоритм стратегической или следящей камеры оригинала.

Локальный MSH-запрос query_slot_segment сверён с оригиналом на фабричной стене и дверном проёме. Runtime-адаптер query_building_segment_world теперь повторяет helper batch bit 0x0008 через связанный узел, указанный raw-словом Batch20 +0x06. При повторе он обходит Node38-поддерево: сначала допустимых потомков, затем сам узел. Флаги ветвления сверены с оригиналом. Синтетические тесты покрывают связи, отсутствие геометрии у промежуточного узла, маски, равные попадания и предел в 200 пересечений.

Игнорируемый тест приложения загружает настоящий PLANT 4 из AutoDemo и его исходную CTLD-привязку Node3 (frame_a = 0, frame_b = 1, blend 0). В контроллере корневого PLANT нет записей tag-21 для открытия двери. В исходной закрытой позе камера блокируется о дверь через обычный путь обновления приложения. Для второго запроса тест вручную задаёт время 1 только Node3, обновляет иерархию позы компонента и преобразование присоединения, затем проверяет, что камера проходит через проём. Это подтверждает только подачу выбранных поз в коллизию приложения: тест не запускает событие открытия, не доказывает видимое открытие двери и не сравнивает полный игровой цикл с оригиналом.

Открыты выбор первого корня и правила состава узлов/slot-ов оригинального мирового запроса, сравнение полного обхода с оригиналом, а также связь с игровыми позами и событиями. Текущий путь приложения использует все отрисовываемые компоненты строений FORT, но их выбор ещё не сопоставлен с поведением оригинала для первого корня и всего мира. Совпадение со стратегической и следящей камерами не подтверждено (MSH).

Кривые alpha и метрика расстояния порталов реализованы для MSH без type 16 (описание кадра). Native CPU clipping может выбрать другую первую вершину; это приближение явно считается как portal_fade_packet_selection_approx.

Данные type 16 ещё не декодированы. Внутренний ракурс «Конструктора» после настоящего входа игрока подтверждает исчезновение чёрной поверхности в проходе; наружные окна обеих миссий сверены на расстояниях около 60/85/110, а ближние окна — с внутренней стороны. Внутренние дальние ракурсы и clipping на краю кадра ещё не приняты; общей готовности R2 эти пары не доказывают.

Закрывающий эксперимент: запустить оригинал в совместимой Windows/DirectX среде, перехватить DirectDraw/Direct3D calls и surface flips, сохранить state log на минимальных сценах с одним типом материала.

FXID field-level semantics

Control Op1 пока использует цвет и радиус без случайных добавок команды; это явное приближение считается в render_coverage control_op1_jitter_unsupported. Масштаб радиуса длиной преобразованного направления и нижняя замена 0,01 восстановлены.

Control Op2 воспроизводит modes 0/2; modes 1/3, body+76 и неподдержанные time modes/привязки считаются как render_coverage control_op2_unsupported. Управление фазой экземпляра командами Control остаётся до его подключения в runtime.

Размеры команд, 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 по-прежнему загружается с raw TMA anchor-ами [(500.2857, 751.8217), (728.0, 449.0)] и записями [(500, 752, 0), (728, 449, 1)]. Rust теперь также читает optional .fml sibling для каждого script base и поддерживает узкий набор атомарных формул в первом Init. Отсутствующий .fml не мешает пакету с одними Handler(19); другие ошибки чтения распространяются как VFS errors.

Нативный конструктор показывает предупреждение при координате выше 10000, но продолжает работу. Rust моделирует числовую запись, а не это UI-предупреждение; на первом FISTP qword он явно завершает загрузку ошибкой для NaN, infinity и значений вне signed-qword диапазона. В первом событии Init Rust поддерживает два доказанных действия: 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]; индексы цели и формулы в 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; header_words[2] его result-save guard-ом не проверяется.

Formula table загружается из <bundle-base>.fml; native FUN_100123b0 (RVA 0x123b0) проверяет ordinal, compiler FUN_100152a0 (RVA 0x152a0) строит числовые, переменные и операторные узлы, а 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 остаётся узкой: один числовой токен либо 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; это не проход всей цепочки кампании. Обычная Single.01 доходит до следующего blocker-а: Handler(43) в clan 1, Init instruction 10. Rust не перескакивает его; preview оставляет varset этого клана на defaults, а полный load завершает ошибкой с rollback. В fparkan-assets теперь есть bounded TRF reader, pure ore/power core и ResearchCandidateSet::refresh_selection: последний получает уже упорядоченные entries и отдельный TRF-контекст, затем читает Unit DAT через VFS. Он не строит AI-каталог и не исполняет script instruction. Runtime всё ещё не создаёт владельца живого контекста, не передаёт реальные 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 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.

Отдельный режимный бит 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 событий также остаются открытыми.

loaded_mission_behavior_control_groupings публикует только начальную группировку подготовленной Control-таблицы. Ветка с меткой 2 исходного Behavior.dll!FUN_1001be10 (RVA 0x1be10) дополнительно вызывает FUN_1001b4b0 и обращается к свойствам через QI 0x204, включая запросы с номерами 0x700, 6 и 0x400. Результат 0x700 попадает в строку кандидата по смещению +0x3c, результат 6 — в +0x40, а 0x400 может обнулить второе поле. Их доменный смысл, множитель 0x700, агрегаты Behavior QI 0x204 и прочие числовые поля кандидата не восстановлены. Rust не подставляет множитель 0x700, нулевые агрегаты, выбранный индекс или эффект. Список для метки 30 не встречается в ресурсах AutoDemo и проверяется только на искусственном наборе.

Игнорируемая проверка AutoDemo сравнивает порядковые номера записей начальной группировки, отдельно выведенные по снимку текущих меток дескрипторов, с состоянием Rust после полной загрузки. Это подтверждает согласованность разбора подготовленной таблицы и правила группировки на этих ресурсах, но не является захватом состояния Behavior оригинала и не устанавливает, когда список прикрепляется или обновляется в живом объекте. Полный штатный путь загрузки и запуска, агрегаты Behavior QI 0x204, обновление после изменений Control, Handler(43) и полный игровой цикл остаются отдельными границами.

Начальные свойства 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, запуск и игровой цикл с полной диспетчеризацией событий. Поэтому совпадение изолированного обратного вызова не доказывает совместимость живого сценария.

Закрывающий эксперимент: найти dispatcher loop в ai.dll, сопоставить jump table с instruction sizes, построить disassembler и сравнить выполнение коротких scripts с оригиналом. Для evaluator-а условий отдельно проследить создание записей this + 0x34, выполнить те же входные данные в оригинале и сверить байт завершения, порядок поиска, ранние выходы и граничный случай payload == count.

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: 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 и внешние эффекты их методов не включены. Структурное чтение записи CTLD с меткой 17 ещё не восстанавливает её обработчики и игровое поведение. Для 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 описаны в техническом подразделе. MSH-часть проверки подтверждает начальный снимок: сборка runtime parent-ов остаётся статическим выводом по 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 в текущее и обработанное количество; при подтверждённых единичных множителях это 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, распределение сообщает ошибку и не публикует ни одной частичной записи. Отдельный 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 и 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-объекты. Матрица выходов находится в техническом разборе. Независимый разбор 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 и 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 задавалась явно. Отдельный native-вектор класса 4 вызывает 0x314f0 через синтетический owner +4 и переходник 0x31ae0. Он сравнивает raw f32 слова абсолютных часов и baseline, вычисленный до округления u32 в f32: для 16777217 это 0x4b800000/0x4b7fff9d, для 16777219 — 0x4b800002/0x4b7fff9f. Последовательность 100, 101 оставляет +0xe4 нулём после первого вызова, поэтому старое условие +0xe4 == 0 добавляет начальную команду повторно: счётчик +0x370 переходит с 1 на 2. Это путь тестового интерфейса owner +4; статическая цепочка AniMesh Agent+0x158 ведёт к primary Control owner slot +0x08 напрямую в 0x314f0. Геометрические callbacks подставлены явно, матрица identity и масштаб единичный, таблицы устройств и shapes пусты. Вектор подтверждает ABI/арифметику часов, но не реальную матрицу и callbacks из миссии или полный Unit DAT loader.

Источник времени и доставка прослежены статически: World3D RVA 0x6bf0 вычисляет часы из timeGetTime() с игровым смещением и поправкой на паузу, затем выполняет первый обход 6/1, шаг мировой физики и второй обход 6/0x1c. При создании объекта World3D RVA 0x7cb0 вызывает slot +0x38 с literal-null payload в 0x7d1a, а затем передаёт в тот же slot DWORD из DATWorld+0x32a38 в 0x7e6e; RVA 0x7ea0 повторяет передачу часов в 0x80a9. Setter RVA 0x5140 записывает этот DWORD в DATWorld+0x32a38. AniMesh RVA 0x2ff0 получает для Agent+0x158 Control base owner по QI selector 0xa, после чего Agent RVA 0x1a80 dispatch-ит событие 4 через его primary slot +0x08 прямо в Control.dll+0x314f0. Это и отдельный синтетический вызов через owner +4 — подтверждённые интерфейсные пути, но пока не единая трасса игрового старта. Control RVA 0x7830 численно преобразует unsigned DWORD в f32 перед primary slot +0x58; фабрика выбирает RVA 0xcbb0 для класса 9 и RVA 0xbcf0 для остальных. Статические вызовы не подтверждают их исполнение на миссии: связывание реальных интерфейсов, активация устройств и shapes, остальные ветви 0xbcf0 и автоматическое изменение provider через clock и бюджет Rust остаются открытыми. Источник контактного коэффициента прослежен статически, но не исполнен. Подробные векторы и статические границы приведены в техническом подразделе о 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 случаи.

Для обычной начальной загрузки входной масштаб единичный. Конструктор модели 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.

Смысл 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, затем FCA0 при построении build_control_device_projection_with_class. MissionControlOwnerProfile::load_projection сохраняет отдельный результат FAC0, а fca0_projection — статус следующего прохода. FCA0 проверяет класс владельца owner +0x50: класс 9 сразу возвращает неизменённую копию; это не условие по device factory tag 9. Любой известный класс, отличный от 9, включая 0, проходит общий расчёт. В Rust MissingNativeControlClass означает, что проекция не восстановила метаданные класса, а не то, что оригинальная DLL не содержит класс. Fac0Unavailable применяется только к не-классу-9 ветви, потому что класс 9 выходит до проверки FAC0.

FCA0 суммирует selector 0xe00 в обратном порядке по устройствам с текущим descriptor tag 5. Изолированный x86-вектор исполняет оригинальный reader 0x21d00 и FCA0. Статическая проверка двух factory vtables подтверждает общий reader 0x21d00 и setter 0x21d50 для tags 5/26; provider getter 0x0dc40 использует owner +4, а canonical QueryInterface(0x204) задаёт owner +0x38 -> owner +0x0c. В каноническом constructor/event-4 пути scalar +0x1a8 и means +0x354/+0x358 равны 1.0. На исходном Control.dll с SHA-256 bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e FCA0 подтверждена по RVA 0x0fca0 при x87 control word 0x007f. Слова [0,1,2,6,7,8,9,10,11] в canonical-default векторе имеют биты 40200000 c0a00000 40c00000 3f800000 40000000 41200000 41e70000 c22d4000 40e70000.

Лицензированный тест full_load_builds_effective_control_device_table_for_mine_unit_dat настоящего min_l_n1.dat теперь проверяет class 3, fca0_projection = Ok(()), 11 устройств и все 25 provider rows. Его девять target words [0,1,2,6,7,8,9,10,11] равны 40000000 40000000 40000000 00000000 00000000 00000000 00000000 00000000 00000000; word 26 после FAC0 остаётся 0x7f7fffff, а оригинальный блок, FAC0 агрегаты 0x470833fc/0x4897e712/0x4d9452cb и 25 значений строк сохраняются. Это Rust full-load результат на оригинальных ресурсах, а не сравнение полного native Unit DAT loader-а или gameplay. Rust helper поддерживает только конечные входы и промежуточные значения домена normal-or-zero; прочие битовые случаи остаются вне заявленного диапазона.

Полный динамический жизненный цикл после начальной загрузки не подтверждён: поздние изменения owner scalar/means, activation, игровые часы, Control tick и placement не сведены в одну проверенную трассу и не включены в Rust-проекцию. Отдельные constructor, setter и loader-пути уже прослежены; физические единицы слов профиля и накопителей остаются неизвестны. Для закрытия границы нужны динамическая трассировка и сравнение полного Unit DAT load с оригинальным loader-ом при одинаковых ресурсах и условиях. Подробные RVA, QI/provider layout, арифметика и пределы векторов приведены в описании изолированного прохода FAC0 и разборе FCA0.

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. Для полной GOG-сборки подтверждены раздельные форматы: gamefont.rlb читается общим RsLi reader, но его native consumer этим не установлен; для меню подтверждена цепочка menu_resources.cfg → NRes font.lib → native lookup MENU_FONT с получением реального font-объекта. Reader fparkan-font разбирает 20-байтовый Tfnt prefix, 256 записей фиксированного шага и передаёт вложенный Texm общему reader-у. Native consumer подтверждает два целочисленных параметра, per-byte spacing и signed advance; смысл трёх glyph DWORD и поля заголовка по 0x0c пока не установлен. Это не реализует выбор шрифта, текстовый layout или glyph drawing. menu_resources.cfg и шесть вариантов MENU_FONT/GAME_FONT сопоставлены с NRes-записями в разборе shell и шрифтов. Также остаются открытыми выбор dimension на фактическом дисплее 1280x1024, widget tree, layout rules, sprite command semantics, focus/navigation и HUD state machine.

Закрывающий эксперимент: сопоставить оставшиеся детали shell_ctrls.cfg, cursor.cfg, game_resources.cfg и hq.cfg с GUI object factories и снять command/event captures для меню, HUD, briefing и диалогов.

Research, economy and properties

Для GOG ArealMap.dll SHA-256 a25f9254ebb339d70e2e07c8121950191e9584ca0858d8e9c2e2b6c671edf6ea CalcFullResearchCost по RVA 0x16280 имеет подтверждённый ABI stdcall [context, candidate, out1, out2], RET 0x10, и восстановленный алгоритм для одного candidate tree: zero-kind subtree pruning, direct preorder, общая дедупликация, ordered DFS по TRF2/TRF3, raw gates из TRF1 и последовательное f32 сложение. Принятый набор из 23 synthetic native cases включал Calc и два AI predicate; для контролируемых сравнений x87 word был установлен в 0x007f. У набора synthetic context, поэтому он не измеряет реальную миссионную стоимость.

Отдельный прямой вызов chooser-а ai.dll SHA-256 c19cd6cd31cd2896f8c4ba0f0e700dd3408aec8182daaf4863d98a7d233a2abc FUN_10010f90 (RVA 0x10f90) прошёл 3 guard-case и frozen-матрицу из 15 проверок при заданном порядке кандидатов. Тот же код использует конструктор RVA 0x10480, parser первого Unit DAT дерева RVA 0x102c0 и предикаты RVA 0x115f0/0x11530. Wrapper Handler(43) по RVA 0xd54a и catalog loader 0x104e0 не вызывались. Эта матрица не подтверждает полный Handler(43), миссионный источник списка или штатный порядок AI-каталога.

fparkan-assets декодирует используемые NRes chunks, сохраняя сырые TRF0 words и непрозрачные данные. Native cost outputs сопоставлены только с двумя raw words: out1 складывает TRF0 word 1 и подписан как ore, out2 складывает word 0 и подписан как power. Смыслы остальных полей полного 32-byte callback record не установлены. Non-ASCII case folding research keys также остаётся открытым; Rust явно возвращает unsupported для таких сравнений.

Rust ResearchCandidateSet получает ordered entries, а TRF-context передаётся отдельно. Ошибка чтения VFS, неверный Unit DAT magic или неполное первое дерево пропускают entry; missing key даёт false predicate. Cost вызывается и для пустой таблицы: он возвращает EmptyResearchTable, после чего этот entry пропускается с сохранением второго status. Другие ошибки таблицы и UnsupportedNonAsciiLookup остаются явными Result-ошибками. Уже записанные statuses и выбранный индекс на ошибке не откатываются; это политика Rust API, не доказательство полного native error ABI.

Два Rust-предиката повторно обходят один декодированный tree с той же immutable TRF-таблицей. Общий helper может остановить второй проход при первом ложном флаге; native RVA 0x11530 продолжает обход и накапливает неиспользуемые float sums. Первый проход уже проверил ключи всех enabled компонентов, поэтому подтверждается только эквивалентность state/selection для такого входа, не идентичность callback trace или вычисления этих sums.

Статический перенос владельца исследовательского контекста подтверждён по трём GOG-модулям. В iron3d.dll (SHA-256 d1dc4ea8535e2069b0a05c9e8be2724dd438ba44cca7a83007d8b2e50f9e35fa) FUN_1005ff40 (RVA 0x5ff40) передаёт индекс клана в FUN_100392e0 (RVA 0x392e0), где он сохраняется в row+0x1c и копируется в descriptor[0]. Thunk RVA 0xcd0a0 ведёт к World3D!GetNetPlayerNum (RVA 0x9110), а RVA 0xcd08e — к World3D!AddNewObjectToGame (RVA 0x8310); SHA-256 World3D.dll: 17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e. Вызов getter-а без аргументов завершается обычным RET; его результат становится arg1 следующего вызова. AddNewObjectToGame принимает девять DWORD, включая указатель на descriptor в arg9, и очищает стек через RET 0x24. RVA 0x83e9 читает arg9; ветвь class 0xb/QI 0x502 на RVA 0x853e загружает descriptor[0] и передаёт его в слот +0x5c на RVA 0x8544; MisLoad.dll (SHA-256 c607762bb67db14ca530927c8e1347339b62b0a163ac90c708ee83142292e2bf) 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 }; этот флаг не выводится из 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 ddfbdbde07832358fe6e35e01c8dba9b58e7313789498b6049ed51d4ed9baf99) selector 0x204 по RVA 0xa490 обновляет сохранённое float-значение +0x678. В GOG Control.dll (SHA-256 bcb86318089028548a178b03a21accdce15df0cc85ddd91674cf3c2f55012c6e) selector 0x36 проходит цепочку RVA 0x31af0 → 0xe6c0 и записывает в общую ячейку памяти по RVA 0x434bc; selector 0x91 выполняет одноразовую инициализацию, только если функция ещё не выполнила её. В Rust пока хранятся только декодированные TRF-данные и сведения о владельце: эти побочные записи, очередь и lifetime контекста не моделируются.

Открыты runtime-выполнение Handler(43), создание и lifetime research context, включение factory-цепочки в полный штатный startup миссии, построение AI candidate list и штатный порядок кандидатов, live x87 policy, inventory/economy transitions и точная типизация всех 16-byte TMA property values. Strict ore comparison и выбор первого кандидата при равенстве известны только для supplied-order chooser. Ignored acceptance проверяет реальные Part 1 AI DAT prefix и scream.trf, но byte-sorted порядок не заявляется как native catalog order и не сравнивает цену с вызовом chooser-а в mission context.

Для проверки оставшихся границ нужно на живой миссии установить, проходит ли штатный Init по статически восстановленной передаче owner, зафиксировать создание и lifetime исследовательского контекста и сравнить порядок кандидатов и cost outputs оригинала с Rust. Покупки, inventory transitions и typed property schema требуют отдельных контролируемых исследований.

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.