Files
fparkan/docs/tomes/08-evidence.md
T
Valentin Popov aa51f3574d chore: simplify project to engine docs and tests
Remove planning and acceptance scaffolding while retaining the native Vulkan mission preview, format readers, runtime algorithms, and ordinary Rust tests. Keep the book aligned with the runnable project and validate checked-in shaders without generated tool metadata.
2026-09-06 05:22:12 +04:00

27 KiB
Raw Blame History

VIII. Устройство оригинальной программы

Здесь собраны детали, полезные при чтении оригинальных DLL: экспортируемые функции, соглашения вызова, адреса и конфигурационные файлы. Адрес относится к конкретной сборке; сравнить её можно по хэшам модулей.

ABI и границы модулей

Базовый binary profile

Все исследованные модули -- 32-битные PE для x86, собранные C++-компилятором эпохи MSVC6. Публичная граница сочетает именованные exports, фабрики C++- объектов, singleton getters и дальнейшие вызовы через vtable.

Для binary shim необходимо учитывать:

  • __cdecl и __stdcall у свободных функций;
  • __thiscall у методов, где this передаётся в ECX;
  • очистку stack, видимую по ret N;
  • точный порядок virtual slots;
  • multiple-interface pointer adjustments;
  • 4-byte alignment и native little-endian types;
  • отсутствие безопасного ABI для STL-контейнеров между современным и старым compiler-ом.

Внутренний новый движок не обязан использовать этот ABI. Он нужен только compatibility layer, который принимает старые DLL-facing interfaces, старый порядок slots и старые ownership rules.

Публичная поверхность DLL

В 15 DLL обнаружено 313 exports:

AniMesh.dll     2    ArealMap.dll    9
Behavior.dll    3    Control.dll     5
Effect.dll      2    Joystick.dll    6
MisLoad.dll     2    Net.dll        37
Ngi32.dll     145    Terrain.dll    13
Wizard.dll      1    World3D.dll    72
ai.dll          2    iron3d.dll      8
services.dll    6

Демоверсия содержит 1 126 imported function slots, а полные Части 1 и 2 -- 1 134. Они включают Win32 runtime, DirectX и межмодульные связи. Большое число exports Ngi32.dll состоит из активного объектного API, математических/resource functions и legacy compatibility stubs.

Compatibility headers должны фиксировать symbol, ordinal, decorated или undecorated name и signature конкретной сборки. Смысловое имя недостаточно: порядок exports и calling convention входят в бинарный контракт.

Композиционный и сервисный слой

iron3d.dll экспортирует восемь функций:

createShell        deleteShell
createGame         deleteGame
createSubsystems   deleteSubsystems
getIGame           getIShell

services.dll публикует шесть getters:

getDisplay
getGUIServer
getNetManager
getResManager
getSoundServer
getTimer

Эти getters возвращают shared interfaces. Caller не должен конструировать concrete implementation или уничтожать singleton напрямую. Для совместимости важны не только адреса функций, но и порядок startup/shutdown, owner/refcount transitions и реакция на failure paths: отсутствие sound device, ошибка display, прерванная загрузка миссии и normal shutdown.

Предметные фабрики

AniMesh:   LoadAgent, LoadAniMesh
ArealMap:  CreateArealMap, CreateSystemArealMap, GetSystemArealMap,
           CreateHallWay, CreateObjectFromScheme, CreateObjectsForDebug,
           CalcFullResearchCost, Debug_TestSchemeType, ShowDebugVector
Behavior:  CreateBehaviour, CreateDistributor, PressDebugKey
Control:   InitializeSettings, LoadControlSystem, LoadPhysicalModel,
           CreateCollManager, CreateCollObject
Effect:    InitializeSettings, CreateFxManager
MisLoad:   CreateMissionData, LoadResearch
AI:        CreateSuperAI, GetSuperAI
Wizard:    CreateWizard
Terrain:   CreateAtmosphere, CreateLightManager, CreatePrimitives,
           CreatePrimitives2, CreateShader, GetShade, GetWorld,
           LoadCamera, stdGetCurrentCamera2, stdSetCurrentCamera2

Фабрика возвращает interface pointer. Конкретный размер объекта и layout остаются внутренними; внешнему коду важны vtable, QueryInterface-подобная negotiation, lifetime methods и правила владения.

World3D export families

72 exports World3D.dll группируются по назначению:

lifecycle: stdInitGame, stdCloseGame, stdCalculateGame, stdRenderGame
objects: CreateObject, AddObjectToGame, AddNewObjectToGame,
         CreateMirrorObject, AddMirrorObjectToGame, AddNewMirrorToGame,
         DeleteGameObject, KillGameObject, CreateQueue, GetQueue
camera:  LoadCamera, stdSetCurrentCamera, stdGetCurrentCamera
input:   UpdateManualEventsList, ClearManualEventsList, stdClearKeyboard,
         converters, scan/string functions, key lock/query, mouse shift
clock:   SetGameTime, PauseGameTime, ResumeGameTime, GetGameTime family
network: netCreateNetWatcher, GetNetPlayerNum and mirror/player helpers
resources/render: material, texture, lightmap and end-of-render helpers
settings/state: CreateGameSettings, SetGameRender, SetStateForGameObjects

World3D является главным местом, где внешний ABI превращается в game loop: input обновляет manual events, calculation проходит queue/world traversal, deferred deletion откладывает фактическое уничтожение объектов, render читает подготовленный snapshot, а end-of-render helpers закрывают временные ресурсы.

Net и Joystick

Net.dll экспортирует создание instance/interface и 33 операции transport lifecycle: provider/session enumeration, setup, create/join/close, player operations, send/receive, latency, addresses, queue size, lobby и netZipData/netUnZipData.

Joystick.dll имеет компактную границу:

QueryJoy
CreateJoy
ReleaseJoy
SetJoyRange
PeekJoyMessage
GetJoyCaps

Эти модули легче всего заменить adapter-ами, потому что их публичная поверхность достаточно узкая. Для native interoperability сохраняются исходные signatures; modern runtime может использовать внутренние typed interfaces.

Ngi32 export families

145 exports Ngi32.dll включают:

resource archives: niOpenResFile, niOpenResFileEx, niOpenResInMem,
                   niCreateResFile, rsOpenLib, rsFind, rsLoad
renderer:          niGetD3DDriverAmount, niSelectD3DDriver,
                   niGetD3DDriverCaps, niGetD3DVideoModeList,
                   niCreate3DRender, niGet3DRender, niGetMaxTextureSize
audio:             niCreate3DSound, niGet3DSound, niGet3DSoundCaps,
                   niMuteSound, rsLoadWave
platform:          allocation, clocks, fixed-memory helpers
math/geometry:     plane, ray, polygon and volume intersection routines
CPU dispatch:      g_FastProc, niGetProcAddress and feature detection
legacy ABI:        n3d*, vrt*, bsp* compatibility entries

Экспорт переменной g_FastProc требует особого shim: consumer получает адрес таблицы, а не результат функции.

Подтверждённые RVA

Адреса указаны как RVA конкретной исследованной сборки:

World3D stdCalculateGame    0x139A0
World3D stdRenderGame       0x13BD0
World3D sendEndOfRender     0x13D90
World3D UpdateManualEvents  0x10E10
World3D ClearManualEvents   0x11180
World3D DeleteGameObject    0x087B0
Ngi32 g_FastProc            0x3A058

iron3d.dll вызывает calculation около RVA 0x5FA94, 0x604C1, 0x6086B, render около 0x60B2F, а manual-event update находится в Win32 message path около 0xA3759.

RVA используются только для сопоставления и трассировки этой версии. Runtime implementation не должна встраивать их как постоянные игровые идентификаторы. Таблица внутренних RVA хранится по SHA-256 конкретного модуля.

Сводка hashes и import/export оригинального движка вынесена в отдельную страницу. Текущее состояние реализации и границы live Vulkan path описаны в справочнике render frame и в исходном коде адаптера.

Подтверждённые hashes неизменённых DLL:

World3D.dll 17e4a3089b2583a8cf2356c9db0390b1aba138356a09130d79b4e7e4791da61e
Ngi32.dll   bab9840d94f4e4e74ffc26677724fa896cf4823845504d09a9e025f80016edf5

Для GOG World3D.dll с этим hash RVA export-ов такие: stdCalculateGame=0x139A0, stdRenderGame=0x13BD0, sendEndOfRender=0x13D90, stdSetCurrentCamera=0x13E60 и stdGetCurrentCamera=0x13E80.

stdRenderGame(camera) сначала вызывает экспорт Terrain stdSetCurrentCamera2(camera), затем сохраняет текущий camera pointer в глобальном состоянии World3D. После этого виден запрос camera interface через selector 6, запрос связанного service через selector 264, renderer/world boundary slots и traversal render queues. В конце pointer очищается; dispatch end-of-render callbacks вынесен также в отдельный sendEndOfRender. Это доказывает порядок передачи camera и границы frame lifecycle, но не layout camera object, не матрицы projection/view и не значения viewport selectors.

Отдельная проверка GOG Terrain.dll (AF87D1B2E728A0BE73C52BE3B44CC196AB46DA7799F25A15D40F8C9B0B425EAD, 499 712 bytes) уточняет receiver side. stdSetCurrentCamera2 находится по RVA 0x4FD40: при инициализированном Terrain он требует у переданного объекта interface selector 18 и вызывает slot +12 результата. Он не записывает переданный pointer в stdGetCurrentCamera2. Последний возвращает Terrain global, который внутренний initialization path устанавливает результатом selector 8 на Terrain object. Следовательно, selector 18, slot +12 и global selector 8 должны оставаться именованными evidence boundary до dynamic capture; считать stdGetCurrentCamera2 getter-ом переданной camera было бы ошибкой.

Статический анализ той же GOG базы уточняет рабочий контракт CBufferingCamera. Метод Terrain RVA 0x4D740 копирует ровно 64 байта (16 dword) в component offset +0x10. Frame-preparation метод RVA 0x4D9C0 получает viewport rectangle через virtual slot +0x3C, выводит width, height, centre и aspect, а projection читает через camera interface. Для projection type 0 он использует float по component offset +0x234 в tan(angle / 2); для type 2 получает five-float block через slot +0x70; иной non-zero type завершается Not supported projection type. Это доказывает зависимость projection от live camera/viewport, но не row/column convention, единицы угла, near/far mapping, handedness или initial camera selection. Важно, что строки ICamera::SetTransformMatrix (RVA 0x4F830) и ICamera::GetTransformMatrix (RVA 0x4F850) ведут только в obsolete-call stubs и не дают usable ABI.

Live elevated read-only probe впервые подтвердил relocation-aware runtime связь: Terrain.dll был загружен по 0x02510000, global base + 0x7355C содержал non-null 0x0B37DF08, а первый dword этого объекта был 0x025765B4 — ровно relocated off_100665B4 из LoadCamera construction path. Это доказывает live camera object с outer vtable, но одновременно исправляет прежнее слишком сильное сопоставление offsets: raw read global + 0x10 не дал finite 4x4 matrix, а +0x234 был zero в данном sample. Receiver static procedures 0x4D740/ 0x4D9C0 и exported global ещё не доказаны как один layout без interface adjustment; их offsets остаются unassigned до recovery selector relationship.

Локальная IDA-база уточняет адреса этой связи: stdGetCurrentCamera2 — это шестибайтный getter по RVA 0x4FD80, который возвращает dword по RVA 0x7355C. Единственный найденный direct static initializer этого global — функция Terrain по RVA 0x4D4D0: она запрашивает selector 8 у своего this и сохраняет полученный interface pointer. Это доказывает адрес хранения и путь заполнения, но не разрешает трактовать pointer как конкретный layout камеры либо читать его как runtime evidence без доступа к процессу на том же уровне привилегий.

Elevated live sampling теперь доказывает, что direct static xref не исчерпывает runtime writers: за 25 секунд autoplay global переключился между тремя heap pointer, все с relocated outer vtables 0x025765B4/0x02576558. У двух объектов paired blocks +0x2C/+0x3C/+0x4C и +0x6C/+0x7C/+0x8C синхронно несли world-like translation, например (491.562, 761.551, 7.361); третий давал normalized-looking (0.098, 0.018, 0.856) и не совпадал с paired block. Наблюдение согласуется с автоматическими camera switches, но не маркирует mode; оно запрещает называть 0x4D4D0 единственным runtime writer и требует recovery indirect/unanalyzed write path.

Outer vtable off_100665B4 теперь даёт exact transform adjustment для world-like sample. Slot +0x54 вызывает у subobject outer + 4 slot +0x20 с selector 0, затем копирует returned +0x0C/+0x1C/+0x2C. В live объекте selector field outer + 0x10 был 0xFFFFFFFF; реализация selector 0 при этом возвращает outer + 0x20. Значит observed triple outer + 0x2C/+0x3C/+0x4C — доказанная translation часть active affine transform, а не корреляция. Selector 2 возвращает paired block outer + 0x60; outer slot +0x70 применяет atan2 к его axis values, что доказывает orientation-angle path. Названия полей, angle order и handedness пока не установлены, но raw affine transform и translation можно сохранять как backend-neutral camera pose без догадок.

Vtable и interface negotiation

Вызовы вида object->vfunc(offset) доказывают порядок slots, даже когда имя метода неизвестно. Renderer slots около +0x28, +0x30, +0x34 окружают world traversal; camera и viewport получаются через selector-based interface calls; shared objects используют ранний slot как AddRef-подобную операцию.

Правила реконструкции:

  1. Зафиксировать byte offset slot и число аргументов.
  2. Найти все call sites и типы передаваемых значений.
  3. Отделить доказанное поведение от назначенного имени.
  4. Построить C-compatible shim vtable с точным порядком.
  5. Внутри adapter-а перевести вызов в современный typed interface.

Нельзя добавлять virtual destructor в начало reconstructed interface: это сдвинет все slots.

ABI-матрица Частей 1 и 2

Во всех пятнадцати DLL совпадают export names, ordinals и import sets. Общее число exports остаётся 313. Обе полные части содержат 1 134 imported function slots; значение 1 126 относится к демоверсии и хранится отдельно.

Побайтно идентичны девять DLL:

ai.dll
Behavior.dll
Joystick.dll
MisLoad.dll
Net.dll
Ngi32.dll
Terrain.dll
Wizard.dll
World3D.dll

Пересобраны AniMesh.dll, ArealMap.dll, Control.dll, Effect.dll, iron3d.dll, services.dll.

Изменение export RVA:

AniMesh    2 / 2
Control    5 / 5
iron3d     8 / 8
services   6 / 6
ArealMap   0 / 9
Effect     0 / 2

Нулевое изменение export RVA не доказывает идентичность тела функции: ArealMap.dll и Effect.dll имеют изменённый .text при прежних адресах exports. Compatibility headers фиксируют внешний ABI один раз, но внутренняя таблица адресов, тестов и semantic deltas выбирается по build fingerprint.

Файловая поверхность

Каталог как внешний API

Оригинальная установка -- не просто набор assets. Имена файлов, относительные пути, регистр, конфигурационные ключи и разделение библиотек образуют внешний контракт. Совместимый движок должен принимать каталог без переименования и предварительной распаковки.

Основные root-файлы включают executable и 15 DLL, Iron_3D.ini, Comp.ini, Behavior.ini, ArealMap.ini, BuildDat.lst, input/preload descriptions и набор .rlb/.lib архивов:

objects.rlb
system.rlb
static.rlb
effects.rlb
Material.lib
Textures.lib
LightMap.lib
Palettes.lib
sounds.lib
voices.lib

Parser конфигураций должен сохранять неизвестные keys и секции, поддерживать quoted strings, хранить provenance значения и отличать absent key от explicit default.

Iron_3D.ini

Демоверсия содержит секции [CS], [MULTIPLAYER], [TEMP] и [LEVEL_RATIO].

DISPLAY_WIDTH=640          DISPLAY_HEIGHT=480
BITDEPTH=16                CURRENT_D3DCARD=0
WINDOW_MODE=0              FORCE_SOFTWARE_CURSOR=1
RENDER_QUALITY=2           REFLECTIONS=0
EMBOSS_BUMP=0              EMBM=0
PLAY_CD_MUSIC=1            MOUSE_SENS=100
JOY_SENS=100               MOUSE_REV_Y=0
JOY_REV_Y=0                JOY_ENABLE=0
SUBTITLES=1

FORCE_CD_SOUND хранит строку пути. Multiplayer задаёт default IP, login и password. [TEMP] содержит normalization и offence/defence ranges, [LEVEL_RATIO] -- коэффициенты сложности 0.5, 0.7, 1.0.

Parser не должен считать имена регистрозависимыми без отдельного доказательства. Effective value, raw value и факт присутствия ключа хранятся раздельно.

Comp.ini: реестр компонентов

Формат строки:

<CID> <DLL-name> <Function-name> [comment]

Подтверждённая таблица:

0 terrain.dll LoadLandscape
1 terrain.dll LoadBuilding
2 terrain.dll LoadCamera
3 animesh.dll LoadAgent
4 animesh.dll LoadAgent
5 terrain.dll CreateAtmosphere
6 terrain.dll CreateShader
7 misload.dll LoadResearch

World3D использует этот файл как динамический component registry. Standalone runtime может сопоставить CID внутренним фабрикам, но compatibility loader должен поддерживать исходные DLL/function strings и комментарии //.

Behavior.ini и ArealMap.ini

Demo Behavior.ini задаёт logging, debug rendering и controller switches:

LogFile=Behavior.log       SaveLog=0
MaxErrorLevel=1            DefErrorLevel=2
LookBugMode=0              ShowVectors=0
NoZBuffer=0                LockBehaviour=0
UseDebugKey=1              GiveDefaultOrder=0
DefaultOrderPhase=10       DeterminMode=0
ImmortalHero=0             UseWizard=1

Код Behavior также ищет дополнительные PathFind_* и network parameters. В demo-файле они отсутствуют, следовательно используются compiled defaults или другой источник; нельзя приписывать им произвольные значения.

ArealMap.ini содержит log switches, ShowAreals, Areal_NoZBuffer, HallWay_NoZBuffer, EdgeUp и RunBehDebug.

Миссии, UI и сохранения

Типичный каталог миссии содержит:

data.tma
mission.cfg
briefing.cfg
messages.cfg

mission.cfg -- текстовое описание именованных resource objects. Блок начинается object <name>, содержит desc, library, libtype, числовой type и произвольные именованные параметры, затем end. В демоверсии через него определяются ambient music loops/variations и другие mission services.

briefing.cfg и messages.cfg относятся к пользовательскому представлению и текстовым событиям. Binary TMA остаётся источником placement и properties; эти файлы дополняют, а не заменяют его.

Отдельные поверхности:

MISSIONS/SCRIPTS/*.scr, *.fml, *.trf, varset.var
MISSIONS/dispatcher.ini
ui/shell_ctrls.cfg
ui/menu_resources.cfg
ui/cursor.cfg
ui/game_resources.cfg
ui/hq.cfg
DATA/TextRes.cfg
SAVE/saveslots.cfg

Dispatcher демоверсии содержит секцию [COMPLETE]; полные части расширяют campaign state и набор миссионных файлов. UI-config следует читать отдельным generic object/config parser-ом, сохраняя порядок блоков и неизвестные fields. TextRes.cfg связывает ключи с локализованными строками.

Save slot list не является полным savegame state. Для полной совместимости нужно отдельно восстановить binary save payload, campaign dispatcher и serialization world/script/AI/RNG.

Правила файловой совместимости

  • Поддерживать / и \ во входных legacy paths.
  • Разрешать paths относительно root игры и mission context.
  • Сохранять исходное написание для log и roundtrip.
  • Использовать ASCII case-insensitive lookup внутри архивов.
  • Учитывать CP1251/ANSI строки там, где встречается локализованный текст.
  • Не применять Unicode normalization к фиксированным resource names.
  • Различать физически отсутствующий файл и отсутствующий entry в существующем архиве.
  • Не требовать одинакового регистра имени файла на case-sensitive системах: resolver строит индекс каталога.

Все найденные конфигурации должны иметь schema с defaults, provenance и признаком present. Это позволяет отличить исходный default от явно заданного пользователем значения.

Различия файловой поверхности Частей 1 и 2

Часть 2 добавляет ui_factory.lib -- NRes с шестью Texm entries. ui/minimap.lib увеличен примерно с 6,95 до 10,10 МБ. gamefont.rlb и sprites.lib побайтно совпадают между частями.

Iron_3D.ini Части 2 добавляет ключи SFX_VOLUME, CD_VOLUME, DEBUG_KEYS_ON, меняет некоторые defaults (MOUSE_SENS, MAP_ALPHA128) и локализует строки login/password. Это подтверждает правило schema + provenance: parser хранит не только effective value, но и признак присутствия ключа в конкретной сборке.

BuildDat.lst Части 2 использует более полные пути под UNITS\BUILDS\AI\...; category masks при этом остаются логическим контрактом, а physical path -- частью content profile.

TextRes.cfg и TextRes.dll значительно расширены. Localized text, resource identifier и path normalization должны оставаться разными слоями: локализация текста не меняет ASCII-casefold policy имён entries.

Дальнейшее чтение

Глоссарий, открытые вопросы, сценарная VM, сохранения и кампания, интерфейс игры.