Files
fparkan/docs/appendices/knowledge-boundaries.md
T
Valentin Popov b9a4d1b478
Docs Deploy / Build and Deploy MkDocs (push) Successful in 1m17s
feat(runtime): query and update loaded Control resources
2026-10-22 14:19:25 +04:00

32 KiB
Raw Blame History

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

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

Render state

Доказаны frame boundaries, world traversal, material resolve и крупные проходы. Не доказаны символами точные имена renderer vtable slots, полный набор CShade state transitions и окончательный порядок части transparent/FX/shadow subpasses.

Закрывающий эксперимент: запустить оригинал в совместимой 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 9, 15, 19 и 25: остаются прежними слот, ключ, ID компонента, множитель и класс, а коэффициенты копируются из новой записи. Искусственный 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 ещё не установлено. 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 именно в таком порядке. Rust helper принимает текущие provider и multiplier извне; он не восстанавливает их жизненный цикл или весь Control tick. В x86 oracle при x87 control word 0x007f проверены порядок, signed zero, отрицательные входы, расширенный промежуточный диапазон и subnormal return. Helper ограничивает себя normal-or-zero значениями, поэтому явно отклоняет промежуточные случаи, где результат выходит за поддерживаемый диапазон f32, даже если позднее умножение вернуло бы его в диапазон. Это ограничение поддержки Rust helper-а, а не неверный native-результат.

В Unit DAT fixture UNITS/BUILDS/MINE/min_l_n1.dat проверена реальная связка fr_l_mine.ctl + fr_l_mine.ndp: tag 26, ключ 2 в таблице из 22 строк, коэффициент 1.0. Это не запись живого 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.