# Reverse Engineering игры Parkan Железная стратегия 1998
x86 Registers Image
## Сборка проекта Проект написан на C# под `.NET 9` Вам должно хватить `dotnet build` для сборки всех проектов отдельно. Все приложения кросс-платформенные, в том числе UI. ### Состояние проекта - Поддержка всех `NRes` файлов - звуки, музыка, текстуры, карты и другие файлы. Есть документация. - Поддержка всех `TEXM` текстур. Есть документация. - Поддержка файлов миссий `.tma`. - Поддержка шрифтов TFNT. - Поддержка файлов скриптов `.scr`. - Поддержка файлов параметров `.var`. - Поддержка файлов схем объектов `.dat`. ### Структура проекта Внимание! Проект делается как небольшой PET, поэтому тут может не быть - чёткой структуры - адекватных названий - комментариев Я конечно стараюсь, но ничего не обещаю. ## Для Reverse Engineering-а использую Ghidra ### Наблюдения - Игра использует множество стандартных библиотек, в частности stl_port, vc++6 и другие. Если хотите что-то изучить в игре, стоит поискать по строкам и сигнатурам, что именно используется в конкретной `dll`. - Строки в основном используются двух форматов - `char*` и `std::string`. Последняя состоит из 16 байт - `undefined4, char* data, int length, int capacity`. - В игре очень много `inline` функции, которые повторяются по куче раз в бинарнике. - Игра загружает и выгружает свои `dll` файлы по несколько раз, так что дебаг с `Memory Map` очень затруднён. - Игра активно и обильно течёт по памяти, оставляя после чтения файлов их `MapViewOfFile` и подобные штуки. - Игра нормально не работает на Win10. Мне помог dgVoodoo. Хотя с ним не работает `MisEditor`. ## Как быстро найти текст среди всех файлов игры ```shell grep -rl --include="*" "s_tree_05" . ``` ## Как быстро найти байты среди всех файлов игры ```shell grep -rlU $'\x73\x5f\x74\x72\x65\x65\x5f\x30\x35' . ``` ## Как работает игра Главное меню: Игра сканирует хардкод папку `missions` на наличие файлов миссий. (буквально 01, 02, 03 и т.д.) Сначала игра читает название миссии из файла `descr` - тут название для меню. - Одиночные игры - `missions/single.{index}/descr` - Тренировочные миссии - `missions/tutorial.{index}/descr` - Кампания - `missions/campaign/campaign.{index1}/descr` * Далее используются подпапки - `missions/campaign/campaign.{index1}/mission.{index2}/descr` Как только игра не находит файл `descr`, заканчивается итерация по папкам (понял, т.к. пробуется файл 05 - он не существует). Загрузка миссии: Читается файл `ui/game_resources.cfg` Из этого файла загружаются ресурсы - `library = "ui\\ui.lib"` - загружается файл `ui.lib` - `library = "ui\\font.lib"` - загружается файл `font.lib` - `library = "sounds.lib"` - загружается файл `sounds.lib` - `library = "voices.lib"` - загружается файл `voices.lib` Затем игра читает `save/saveslots.cfg` - тут слоты сохранения Затем `Comp.ini` - тут системные функции, которые используются для загрузки объектов. ``` IComponent ** LoadSomething(undefined4, undefined4, undefined4, undefined4) ``` - `Host.url` - этого файла нет - `palettes.lib` - тут палитры, но этот NRes пустой - `system.rlb` - не понятно что - `Textures.lib` - тут текстуры - `Material.lib` - тут какие-то материалы - не понятно - `LightMap.lib` - видимо это карты освещения - не понятно - `sys.lib` - не понятно - `ScanCode.dsc` - текстовый файл с мапом клавиш - `command.dsc` - текстовый файл с мапом клавиш Тут видимо идёт конфигурация ввода - `table_1.man` - текстовый файл - `table_2.man` - текстовый файл - `hero.man` - текстовый файл - `addition.man` - текстовый файл - Снова `table_1.man` - Снова `table_1.man` - `M1.tbl` - текстовый файл - Снова `table_2.man` - Снова `table_2.man` - `M2.tbl` - текстовый файл - Снова `hero.man` - Снова `hero.man` - `HERO.TBL` - Снова `addition.man` - `ui/hq.cfg` - Снова `ui/hq.cfg` Дальше непосредственно читается миссия - `mission.cfg` - метадата миссии - `units\\units\\prebld\\scr_pre1.dat` из метаданных `object prebuild` - `cp` файл (грузятся подряд все) - Опять `ui/hq.cfg` - `mistips.mis` - описание для игрока (экран F1) - `scancode.dsc` - хз - `command.dsc` - хз - `ui_hero.man` - хз - `ui_bots.man` - хз - `ui_hq.man` - хз - `ui_other.man` - хз - Цикл чтения курсоров * `ui/cursor.cfg` - тут настройки курсора. * `ui/{name}` - курсор - Снова `mission.cfg` - метадата миссии - `descr` - название - `data/textres.cfg` - конфиг текстов - Снова `mission.cfg` - метадата миссии - Ещё раз `mission.cfg` - метадата миссии - `ui/minimap.lib` - NRes с текстурами миникарты. - `messages.cfg` - Tutorial messages УРА НАКОНЕЦ-ТО `data.tma` - Из `.tma` берётся LAND строка (я её так назвал) - `DATA\\MAPS\\SC_3\\land1.wea` - `DATA\\MAPS\\SC_3\\land2.wea` - `BuildDat.lst` - Behaviour will use these schemes to Build Fortification - `DATA\\MAPS\\SC_3\\land.map` - `DATA\\MAPS\\SC_3\\land.msh` - `effects.rlb` Цикл по кланам из `.tma` - `MISSIONS\\SCRIPTS\\screampl.scr` - `varset.var` - `MISSIONS\\SCRIPTS\\varset.var` - `MISSIONS\\SCRIPTS\\screampl.fml` - `missions/single.01/sky.ske` - `missions/single.01/sky.wea` Дальше начинаются объекты игры - `"UNITS\\BUILDS\\BUNKER\\mbunk01.dat"` - cp файл ## Загрузка `cp` файлов `cp` файл - схема. Он содержит дерево частей объекта. `cp` файл читается в `ArealMap.dll/CreateObjectFromScheme` В зависимости от типа объекта внутри схемы (байты 4..8) выбирается функция, с помощью которой загружается схема. Функция выбирается на основе файла `Comp.ini`. - Для ClassBuilding (0x80000000) - вызывается функция c классом 3 (по таблице ниже Building). - Для всех остальных - функция с классом 4 (по таблице ниже Agent). На основе файла `Comp.ini` и первом вызове внутри функции `World3D.dll/CreateObject` ремаппинг id: | Logic ID | ClassName | Function | |:--------:|:------------:|--------------------------------| | 1 | Landscape | `terrain.dll LoadLandscape` | | 2 | Agent | `animesh.dll LoadAgent` | | 3 | Building | `terrain.dll LoadBuilding` | | 4 | Agent | `animesh.dll LoadAgent` | | 5 | Camera | `terrain.dll LoadCamera` | | 7 | Atmosphere | `terrain.dll CreateAtmosphere` | | 9 | Agent | `animesh.dll LoadAgent` | | 10 | Agent | `animesh.dll LoadAgent` | | 11 | Research | `misload.dll LoadResearch` | | 12 | Agent | `animesh.dll LoadAgent` | Будет дополняться по мере реверса. Всем этим функциям передаётся `nres_file_name, nres_entry_name, 0, player_id` ## `fr FORT` файл Всегда 0x80 байт Содержит 2 ссылки на файлы: - `.bas` - `.ctl` - вызывается `LoadAgent` ## `.msh` ### Описание ниже валидно только для моделей роботов и зданий. ##### Land.msh использует другой формат, хотя 03 файл это всё ещё точки. Загружается в `AniMesh.dll/LoadAniMesh` - Тип 01 - заголовок. Он хранит список деталей (submesh) в разных LOD ``` нулевому элементу добавляется флаг 0x1000000 Содержит 2 ссылки на файлы анимаций (короткие - файл 13, длинные - файл 08) Если интерполируется анимация -0.5s короче чем magic1 у файла 13 И у файла есть OffsetIntoFile13 И ushort значение в файле 13 по этому оффсету > IndexInFile08 (это по-моему выполняется всегда) Тогда вместо IndexInFile08 используется значение из файла 13 по этому оффсету (второй байт) ``` - Тип 02 - описание одного LOD Submesh ``` Вначале идёт заголовок 0x8C (140) байт В заголовке: 8 Vector3 (x,y,z) - bounding box 1 Vector4 - center 1 Vector3 - bottom 1 Vector3 - top 1 float - xy_radius Далее инфа про куски меша ``` - Тип 03 - это вершины (vertex) - Тип 06 - индексы треугольников в файле 03 - Тип 04 - скорее всего какие-то цвета RGBA или типа того - Тип 08 - меш-анимации (см файл 01) ``` Индексируется по IndexInFile08 из файла 01 либо по файлу 13 через OffsetIntoFile13 Структура: Vector3 position; float time; // содержит только целые секунды short rotation_x; // делится на 32767 short rotation_y; // делится на 32767 short rotation_z; // делится на 32767 short rotation_w; // делится на 32767 --- Игра интерполирует анимацию между текущим стейтом и следующим по time. Если время интерполяции совпадает с исходным time, жёстко берётся первый стейт из 0x13. Если время интерполяции совпадает с конечным time, жёстко берётся второй стейт из 0x13. Если ни то и ни другое, тогда t = (time - souce.time) / (dest.time - source.time) ``` - Тип 12 - microtexture mapping - Тип 13 - короткие меш-анимации (почему я это не дописал?) ``` Буквально (hex) 00 01 01 02 ... ``` - Тип 0A - ссылка на части меша, не упакованные в текущий меш (например у бункера 4 и 5 части хранятся в parts.rlb) ``` Не имеет фиксированной длины. Хранит строки в следующем формате. Игра обращается по индексу, пропуская суммарную длину и пропуская 4 байта на каждую строку (длина). т.е. буквально файл выглядит так 00 00 00 00 - пустая строка 03 00 00 00 - длина строки 1 73 74 72 00 - строка "str" + null terminator .. и повторяется до конца файла Кол-во элементов из файла 01 должно быть равно кол-ву строк в этом файле, хотя игра это не проверяет. Если у элемента эта строка равна "central", ему выставляется флаг (flag |= 1) ``` ## `.wea` Загружается в `World3D.dll/LoadMatManager` По сути это текстовый файл состоящий из 2 частей: - Материалы ``` {count} {id} {name} ``` - Карты освещения ``` LIGHTMAPS {count} {id} {name} ``` Может как-то анимироваться. Как - пока не понятно. ## Lightmap (TEXM) У текстур использующихся как карты освещения специальные имена. `some_name.00` или `some_name.0` Первая цифра после `.` это группа палет (всего 0x11), вторая цифра индекс в этой группе. ``` All lightmaps named whatever.0 → they all end up with no DirectDraw palette attached. ``` ## Люблю разработчиков У них получилось сделать LightSource и MaterialDescriptor чётко одинаковыми по размеру (0x5c) из-за чего я потерял 3 дня прыгая по указателям ## `FXID` - файл эффектов По сути представляет собой последовательный список саб-эффектов идущих друг за другом. Всего существует 9 (1..9) видов эффектов: !описать по мере реверса Выглядит так, словно весь файл это тоже эффект сам по себе. ``` 0x00-0x04 Type (игра обрезает 1 байт, и поддерживает только значения 1-9) 0x04-0x08 unknown 0x08-0x0C unknown 0x0C-0x10 unknown 0x10-0x14 EffectTemplateFlags ... 0x30-0x34 ScaleX 0x34-0x38 ScaleY 0x38-0x3C ScaleZ enum EffectTemplateFlags : uint32_t { EffectTemplateFlag_RandomizeStrength = 0x0001, // used in UpdateDust EffectTemplateFlag_RandomOffset = 0x0008, // random worldTransform offset EffectTemplateFlag_TriangularShape = 0x0020, // post-process strength as 0→1→0 EffectTemplateFlag_OnlyWhenEnvBit0Off = 0x0080, // gating in ComputeEffectStrength EffectTemplateFlag_OnlyWhenEnvBit0On = 0x0100, // gating in ComputeEffectStrength EffectTemplateFlag_MultiplyByLife = 0x0200, // multiply strength by life progress EffectTemplateFlag_EnvFlag2_IfNotSet = 0x0800, // if NOT set → envFlags |= 0x02 EffectTemplateFlag_EnvFlag10_IfSet = 0x1000, // if set → envFlags |= 0x10 EffectTemplateFlag_AlwaysEmitDust = 0x0010, // ignore piece state / flags EffectTemplateFlag_IgnorePieceState = 0x8000, // treat piece default state as OK EffectTemplateFlag_AutoDeleteAtFull = 0x0002, // delete when strength >= 1 EffectTemplateFlag_DetachAfterEmit = 0x0004, // detach from attachment after update }; ``` # ЕСЛИ ГДЕ-ТО ВИДИШЬ PTR_func_ret1_no_args, ТО ЭТО ShaderConfig В рендеринге порядок дата-стримов такой position normal color # Внутренняя система ID - `1` - unknown (implemented by CLandscape) видимо ILandscape - `3` - unknown (implemented by CAtmosphere) видимо IAtmosphere - `4` - IShader - `5` - ITerrain - `6` - IGameObject - `7` - ISettings - `8` - ICamera - `9` - IQueue - `10` - IControl - `0xb` - IAnimation - `0xc` - IShadeStatsBuilder (придумал сам implemented by CShade) - `0xd` - IMatManager - `0xe` - ILightManager - `0xf` - IShade - `0x10` - IBehaviour - `0x11` - IBasement - `0x12` - ICamera2 или IBufferingCamera - `0x13` - IEffectManager - `0x14` - IPosition - `0x15` - IAgent - `0x16` - ILifeSystem - `0x17` - IBuilding - точно он, т.к. ArealMap.CreateObject на него проверяет - `0x18` - IMesh2 - `0x19` - IManManager - `0x20` - IJointMesh - `0x21` - IShadowProcessor (придумал сам implemented by CShade) - `0x22` - unknown (implement by CLandscape) - `0x23` - IGameSettingsRoot - `0x24` - IGameObject2 - `0x25` - unknown (implemented by CAniMesh) - `0x26` - unknown (implemented by CAniMesh and CControl and CWizard) - `0x28` - ICollObject - `0x29` - IPhysicalModel - `0x101` - I3DRender - `0x102` - ITexture (writable) - `0x103` - IColorLookup - `0x104` - IBitmapFont - `0x105` - INResFile - `0x106` - NResFileMetadata - `0x107` - I3DSound - `0x108` - IListenerTransform - `0x109` - ISoundPool - `0x10a` - ISoundBuffer - `0x10c` - ICDPlayer - `0x10d` - IVertexBuffer - `0x201` - IWizard - `0x202` - IItemManager - `0x203` - ICollManager - `0x301` - IArealMap - `0x302` - ISystemArealMap - `0x303` - IHallway - `0x304` - IDistributor - `0x401` - ISuperAI - `0x501` - MissionData - `0x502` - ResTree - `0x700` - INetWatcher - `0x701` - INetworkInterface ## SuperAI = Clan с точки зрения индексации Т.е. у каждого клана свой SuperAI ## Опции World3D.dll содержит функцию CreateGameSettings. Она создаёт объект настроек и далее вызывает методы в соседних библиотеках. - Terrain.dll - InitializeSettings - Effect.dll - InitializeSettings - Control.dll - InitializeSettings Остальные наверное не трогают настройки. | Resource ID | wOptionID | Name | Default | Description | |:-----------:|:---------------:|:--------------------------:|:-------:|--------------------| | 1 | 100 (0x64) | "Texture detail" | | | | 2 | 101 (0x65) | "3D Sound" | | | | 3 | 102 (0x66) | "Mouse sensitivity" | | | | 4 | 103 (0x67) | "Joystick sensitivity" | | | | 5 | !not a setting! | "Illegal wOptionID" | | | | 6 | 104 (0x68) | "Wait for retrace" | | | | 7 | 105 (0x69) | "Inverse mouse X" | | | | 8 | 106 (0x6a) | "Inverse mouse Y" | | | | 9 | 107 (0x6b) | "Inverse joystick X" | | | | 10 | 108 (0x6c) | "Inverse joystick Y" | | | | 11 | 109 (0x6d) | "Use BumpMapping" | | | | 12 | 110 (0x6e) | "3D Sound quality" | | | | 13 | 90 (0x5a) | "Reverse sound" | | | | 14 | 91 (0x5b) | "Sound buffer frequency" | | | | 15 | 92 (0x5c) | "Play sound buffer always" | | | | 16 | 93 (0x5d) | "Select best sound device" | | | | ---- | 30 (0x1e) | ShadeConfig | | из файла shade.cfg | | ---- | (0x8001e) | | | добавляет AniMesh | ## Goodies ```c++ // Тип положения объекта в пространстве. Используется при расчёте эффектов, // расстановке объектов и для получения/выдачи их матриц трансформации. enum EPlacementType { // 0: Полная мировая матрица, ориентированная "смотрю на цель". // Входная matrix — это мировая матрица (look-at к какой-то цели). // Движок при необходимости переводит её в локальное/костное пространство // (через inverse(parent/joint) * world и т.п.). // Смысл: "Построй мне мировую матрицу так, чтобы я смотрел на цель; // если я привязан к кости — учти это." Placement_WorldLookAtTarget = 0, // 1: Мировая матрица, где основная ось объекта (this->direction) // выровнена по направлению к цели. // Входная matrix — мировая; логика выравнивания использует внутренний // вектор direction и целевую точку/направление. // Смысл: "Используй мой внутренний direction как основную ось // и как можно лучше направь её в сторону цели." Placement_WorldAlignDirectionToTarget = 1, // 2: Мировая матрица, полностью задаётся внутренним состоянием объекта. // Входная matrix трактуется как "standalone world" — готовая мировая // матрица объекта. Движок лишь переводит её во внутреннее локальное/ // костное пространство (inverse(parent/joint) * world), без look-at // и без выравнивания по цели. // Смысл: "Вот моя конечная мировая матрица. Просто встрои её в иерархию, // не трогая ориентацию дополнительной логикой." Placement_WorldFromStoredTransform = 2, // 3: Мировая матрица, ориентированная вдоль вектора движения, но с учётом цели. // Используется предыдущая мировая позиция + текущая + target, чтобы // построить базис: основная ось — направление движения, вспомогательная — // направление к цели. // Смысл: "Ориентируй меня по вектору моего движения, но также учитывай цель." Placement_WorldAlignMotionToTarget = 3 }; ``` ## Контакты Вы можете связаться со мной в [Telegram](https://t.me/bird_egop).