Задался я однажды целью портировать данную игру на более современные платформы. Но понятное дело игра является далеко не 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 пикселей задний фон в игре плавно скроллировался.
Для чего предназначены последние два блока в формате выяснить интуитивным путем не удалось.
Автор: Veliant