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