Перейти к содержанию

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

Этот раздел перечисляет области, где контракт ещё не закрыт полностью. Они не мешают безопасному чтению и 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.

Отдельный режимный бит 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 class 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.

Одна цепочка 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 и внешние эффекты их методов не включены. Для 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-е не проверялись.

Для 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 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.

Отдельный 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 значения во время игры.

Закрывающий эксперимент: трассировать LoadControlSystem, LoadPhysicalModel, CreateCollManager и создание collision objects; связать каждый изменяемый field с созданным shape, contact или реакцией на движение.

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.