Pull to refresh

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

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

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



Однако имя файла первой записи находится немного смещенным от начала файла, поэтому будем смотреть что находится перед ним. Т.к. обычно в компиляторах используются стандартные данные размером в 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. Проверяем это на других записях и приходим к выводу, что данное поле содержит размер файла расположенного в контейнере.




В принципе, этого уже достаточно чтобы достать все файлы из контейнера, но посмотрим для чего предназначены остальные поля. Числа между смещением и размером много больше размера самого псевдоархива, а значит не могут быть ни адресом ни размером. Первое что приходит на ум, что это контрольная сумма, ведь было бы логично проверять данные на случай если файл окажется битым. Однако в данном случае разработчики поступили по другому. Эти поля содержат штамп времени файлов. Зачем это понадобилось остается загадкой. На примере первой файловой записи 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-цветных режимах используется палитра следующего вида:



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



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

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

Формат M

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

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


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

Получилась следующая картина:


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



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

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



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

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



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

Для чего предназначены последние два блока в формате выяснить интуитивным путем не удалось.
Tags:
Hubs:
+45
Comments 12
Comments Comments 12

Articles