Исследование форматов игровых ресурсов на примере игры Dr.Riptide

в 18:23, , рубрики: reverse engineering, Восстановление данных, форматы файлов, метки: ,

Задался я однажды целью портировать данную игру на более современные платформы. Но понятное дело игра является далеко не open source и когда-то в далеком 1994 году разработчики брали за нее ни много ни мало 25 баксов, а посему все игровые ресурсы нужно было либо перерисовывать, либо потрошить единственный игровой архив. Чем я и занялся.

Игровой архив с именем RIPTIDE.DAT представляет собой бинарный файл собственного формата. Кстати говоря, он не является архивом, а так называемым псевдоархивом. Т.е. файлы хранятся в едином контейнере без сжатия, и имеется некоторая примитивная файловая система указывающая как обращаться к файлам внутри контейнера.
Если мы откроем этот файл в любом hex-редакторе, то увидим что записи о файлах внутри контейнера идут в самом начале, а следом сами бинарные данные. Собственно, нужно узнать сколько файлов содержит контейнер и сам формат записи. Первое на что обращаем внимание — это фиксированная длина записи, т.е. от начала имени файла в одном записи, до начала в следующей, для всех записей, одинаково и равно 0x19 (25) байт.

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Однако имя файла первой записи находится немного смещенным от начала файла, поэтому будем смотреть что находится перед ним. Т.к. обычно в компиляторах используются стандартные данные размером в 1 (byte),2 (word),4 (dword),8(qword) байт, то будем делить данные в уме примерно на блоки таких размеров. Наше внимание привлекают два dword'а 0x00001A60 и 0x00013EEC и word 0x010E, потому что они могут указывать на начало данных или размер данных потому что меньше размера самого файла. Смещения 0x00013EEC и 0x000001EС не представляют никакого интереса. Первый указывает в середину каких-то бинарных данных, второй попадает в середину записей о файлах. А вот 0x00001A60 указывает ровнехонько на бинарные данные, расположенные сразу за последней файловой записью. Поскольку это поле относится к файловой записи, то смотрим это же поле для следующей записи. Для этого прибавляем к смещению число 0x19, которое получили выше и которое является длиной файловой записи: 0x0000000A+0x19=0x00000023. В dword'е по этому смещению лежит число 0x0001594C, что тоже находится в пределах размера файла. Замечаем, что число 0x00013EEC из первой файловой записи меньше этого числа. Проверим. 0x00001A60+0x00013EEC=0x0001594C. Проверяем это на других записях и приходим к выводу, что данное поле содержит размер файла расположенного в контейнере.

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

В принципе, этого уже достаточно чтобы достать все файлы из контейнера, но посмотрим для чего предназначены остальные поля. Числа между смещением и размером много больше размера самого псевдоархива, а значит не могут быть ни адресом ни размером. Первое что приходит на ум, что это контрольная сумма, ведь было бы логично проверять данные на случай если файл окажется битым. Однако в данном случае разработчики поступили по другому. Эти поля содержат штамп времени файлов. Зачем это понадобилось остается загадкой. На примере первой файловой записи 0x1CEF2292 превращается в 15.07.1994 04:20:36 в формате DOS.
Последним неразгаданным нами значением остается лишь word 0x010E в самом начале файла. Логичней всего предположить что оно содержит количество файлов в контейнере. Это легко проверить. Берем смещение до первого файла из первой файловой записи 0x00001A60 вычитаем 2 байта на сам word, и делим на длину файловой записи в 0x19 байт и получаем ровно (00001A60-2)/19 = 010E что и требовалось доказать.
В итоге это можно записать в виде структур на языке С следующим образом:

typedef _FILE_ITEM {
	uint32_t Size;
	uint32_t TimeStamp;
	uint32_t Offset;
	char     Name[13];
} FILE_ITEM, *PFILE_ITEM;

typedef _HEADER {
	uint16_t  Count;
	FILE_ITEM Files[0];
} HEADER, *PHEADER;

После распаковки получаем 270 файлов с расширениями CMF, L, M, PCS, PCX, TXT, VOC.
Из указанных расширений нет необходимости анализировать TXT, PCX являющиеся довольно распространенными форматами. После небольшого поиска отпала необходимости в анализе CMF и VOC, которые являются звуковыми файлами. Остаются L, M и PCS. Честно сказать для чего используется PCS я так и не выяснил, да и необходимости в этом не было.

Анализируя имена файлов можно предположить, что файлы L-формата содержат графику, а M-формата содержат информацию об игровых уровнях-картах.

Формат L

Анализируя графику опираемся на то, что в любом формате где-то должны быть указаны размеры изображения и сама информация отображаемая как графика. Для анимации добавляется как минимум еще количество кадров и возможно временные интервалы между кадрами.
Опять открываем файл (а лучше несколько) в hex-редакторе. Что сразу бросается в глаза — у всей графики, которая отображается в игре статически первым байтом идет значение 0x01, а в тех что анимированные больше единицы. Таким образом делаем предположение, что это число указывает на количество кадров в файле. Далее идут два байта, после которых в большинстве случаев идут нули. Предположим что это ширина и высота. Проверим — умножаем первое на второе и получаем как раз почти длину файла за минусом как раз тех самых трех байт в начале.
Раз для описания цвета используется всего один байт на цвет, значит цвета указываются индексами в палитре, и максимальное число используемых одновременно цветов равно 256, что вполне соответствует графическим режимам того времени. В 256-цветных режимах используется палитра следующего вида:

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Для файлов в которых количество кадров больше одного, размеры каждого кадра идут сразу за графическими данными предыдущего.

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

На указанном рисунке можно легко найти смещение до второго кадра. Берем смещение до первого кадра 0x00000001 и прибавляем сначала 2 байта отведенные под размеры, а потом 0x0C*0x10 под графику. Получаем как раз 0x000000C3.

Что примечательно, для экономии места, размеры описываемых кадров могут отличаться. Для прозрачного цвета используется значение 0.

Формат M

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

Опять открываем сразу несколько M-файлов в hex-редакторе и пытаемся выявить у них схожие регионы.
После непродолжительного анализа замечаем в файле несколько основных блоков:

  • 4 байта в самом начале файла
  • большой блок данных разреженных нулями
  • блок данных размером всегда 0x8000 байт с цепочками повторяющихся данных, но почти не имеющий нулевых байт
  • небольшой блок данных разреженных нулями в конце файла
  • 4 байта в самом конце файла

Первое что было замечено, что третий блок во всех файлах размером 0x8000 байт. Второе на что было обращено внимание, что второй блок похож на массив DWORD'ов, и его длина кратна word'ам из первых четырех байт. Логично было предположить, что эти два word'а задают размеры двумерного массива dword'ов, а следом идет сам массив.
Начинаем смотреть значения этого массива. Младший байт любого dword'а в большинстве случаев не равен нулю, а вот чем ближе к старшим разрядам, тем все реже встречаются отличные от нуля значения.
Было решено отобразить данный двумерный массив как изображение, в котором 2 байт dword'а будет указывать закрашивать точку или нет.

Получилась следующая картина:
Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Которая примерно напоминает силуэты карты, и поначалу я подумал что этим байтом описывается карта для проверки на столкновения с стенами.
Далее решил отобразить цветом пиксели, для которых отличны от нуля старшие байты dword'а. Третий — красным, четвертый — желтым.

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Картина начинает проясняться. Эти значения описывают статические и динамические игровые объекты для которых имеется отдельная графика в L-файлах.

Стало понятно, что первый байт dword'а хранил индекс номер картинки в тайловой карте, которая должна была отобразиться на это месте. Но так как нигде в графических файлах не хранилось тайловых карт была предпринята попытка отобразить как изображение тот блок данных размером в 0x8000 байт. Поскольку ширину изображения я не знал изначально была получена длинная полоска толщиной в 1 пиксель. Постепенно уменьшая ширину изображения стали появляться силуэты некоторых изображений карты. При ширине изображения в 8 пикселей получил четкое изображение с явно выраженной квадратной нарезкой на тайлы. Получилась изображение шириной в 8 и высотой в 4096 пикселей.
Некоторые фрагменты представлю на картинке ниже. В качестве RGB компонент цвета использовалось значение байта, поэтому изображение получилось в оттенках серого.
К слову, в большинсте случаев все что видите на картинках является скриншотами отрендеренных HTML страниц с громадными таблицами, ячейки которой окрашивались в свой цвет. Сам же парсинг бинарных файлов осуществлялся средствами PHP. Не то чтобы я извращенец, просто лень было смотреть в сторону графически библиотек.

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Если поделить высоту тайловой карты в 4096 пикселей на 8, то получаем 512, а не 256 картинок. Таким образом, то что я посчитал маской для проверки столкновений с стенами оказалось все тем же индексом изображения в карте. Вот таким образом разработчики убили одним выстрелом двух зайцев. Т.е. младшие 256 изображений для объектов через которые нельзя проплыть, старшие же — через которые можно. И под индекс отведены 2 байта, а не один.

Рендер карты с наложенными тайлами выглядит теперь следующим образом:

Исследование форматов игровых ресурсов на примере игры Dr.Riptide

Это сейчас вся карта легко помещается на одном экране, а во времена графики с разрешением 320x200 пикселей задний фон в игре плавно скроллировался.

Для чего предназначены последние два блока в формате выяснить интуитивным путем не удалось.

Автор: Veliant

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js