На волне ностальгического удовольствия или Funcom, подожди, нормально же общались

Вместо предисловия.

Все любят "ремейки" и "ремастереды" (спорное заявление, которое никогда бы не могло быть правдой). Тем не менее и даже возможно более, кому-то из нас они помогают пережить вновь те замечательные минуты или часы, которые мы когда-то потратили на каждую из таких историй. Нашей историей будет Дюна, в её неоклассическом исполнении группой замечательных ребят.

Какие цели преследует данная статья? Если коротко - никакие. В мои планы входит попытка хорошенько поковыряться в ремейке данной игры и с вероятностью равной более чем 100% узнать что-то новенькое для себя и скорее всего для вас, хотите вы этого или нет.

Почему это может быть интересно именно вам? Всё дело в том, что любой проект написанный не вами, это потенциально взгляд с другой стороны, на общие проблемы того, как делаются игры. Кроме того, это будет интересно просто потому, что автор статьи ни в коем разе не является профессиональным разработчиком игр, плохо знает предметную область и инструментарий для создания игр, достаточно плохо знает язык реализации данного ремейка, его потуги будут смешны, а жизнь ничтожна. Согласитесь, не это ли топ контент?

Хватит трепаться, перейдем к делу.

Что вообще из себя представляет этот ремейк. Если охарактеризовать его кратко, это всё та же Dune II : Battle for Arrakis, вышедшая на множестве платформ своего времени от всеми известной Westwood Studios, но с некоторыми "новыми новшествами", и "удобными удобностями". Реализация использует пакеты с данными от оригинальной игры, об этом заблаговременно сказано на данной странице. Описание того, как собрать всё это добро под вашу целевую платформу в этом документе, а если вы возжелали делать сборку из какой-либо интегрированной среды разработки, то к вашим услугам вот эта папочка.

Выбрав для себя максимально удобный способ работы с исходным текстом и настроив его сборку, можно начать изучение. Мой вам совет, никогда не пытайтесь понять сложные вещи за раз, это не работает. Поэтому для себя я выберу вариант в котором для начала мы просто посмотрим дерево исходного кода и попытаемся сложить о нем какое-то предвзятое впечатление, которое скорее всего будет ошибочным (надеюсь этого удастся избежать или хотя бы минимизировать потери). Посмотрим на общую структуру исходного текста проекта, обычно для c/c++ проектов это две директория одна с файлами *.cpp/etc которая в данном случае называется src, вторая с файлами *.h/etc в данном случае include (синие гиперссылки во вложенных документах как_бы_говорят_нам что это директории, и что в них что-то есть), так же там есть поддиректории тестового кода, файлы необходимые для системами сборки, etc. Внимательно осмотрев оба списка, мы замечаем что в нем есть директории с одинаковыми названиями, и это не просто так. В таких директориях сосредоточены части игры, которые решили вынести как отдельные, в плане организации, сущности. Если присмотреться к этому всему ещё лучше, то из названия этих директорий угадывается их приблизительная роль в игре, например директории enet и Network содержат код ответственный за возможность играть в данную игру по сети, а директория FileClasses код ответственный за работу с данными игры (теми самыми, которые мы берем из оригинальной версии игры) и многое-многое другое. Но давайте сосредоточимся на чем-нибудь конкретном в этом изобилии и покопошимся всласть.

Немного подумав, над тем что бы мы с вами могли начать исследовать первое, так чтобы в последующем эти исследования не стояли особняком от будущих исследований, было принято решения начать с кода который работает с теми самыми пакетами данных из оригинальной версии. Эти пакеты, в сути своей, являются архивами. Внутри себя они содержат набор "файлов" (скорее всего сохраненных в бинарном виде, учитывая природу подобных видов файлов), за 5 минут серфинга в интернете мне не удалось найти оригинального автора этого формата файлов, материалы с этой страницы утверждают что это этот тип архива является расширенной версией этого типа архива, а ещё есть вот этот материал, который говорит нам, что зачастую PAK архив может быть просто zip архивом и что реализации его могут разниться. Если немного почитать материал с этой страницы, особенно про судебное разбирательство, становится понятно почему сложно найти автора данного типа архива, как указано на этой странице, и почему у данного типа файлов с расширением PAK может быть так много реализаций компрессоров/декомпрессоров которые никак друг с другом не связаны. После этого маленького отступления, просто давайте посмотрим код. Начнем с Pakfile.h. В нем мы видим вот такой комментарий:

/// A class for reading PAK-Files.

/** This class can be used to read PAK-Files. PAK-Files are archive files used by Dune2. The files inside the PAK-File can an be read through SDL_RWops.*/

И сразу все становится понятно, если до сих пор вы ничего не понимали, если же вы не понимаете и после этого, тот тут написано что этот класс предназначен для чтения PAK файлов. А чуть ниже, ещё подробней сказано, что этот класс может быть использован для чтения PAK файлов, что PAK файлы это архивные файлы используемые в Dune2 и что файлы внутри PAK файла могут быть прочитаны через SDL_RWops. Из всего этого становится понятно, что реализация использует библиотеку SDL для своих утилитарных нужд. Давайте глянем что из себя представляет SDL_RWops. Если обратить внимание на ссылку SDL_RWops в ней есть некий намек на то, что SDL_RWops является потоковым типом. Если же почитать саму документацию, то мы узнаем что SDL_RWops представляет из себя структуру, которая предоставляет абстрактный интерфейс на потоковый ввод-вывод. Так же нам говорят, что приложения могут в общем-то игнорировать внутренности этой структуры и могут рассматривать их как непрозрачные указатели, но в то же время нас предупреждают о том, что детали важны для низкоуровневого кода если потребуется реализовать что-то из этих внутренностей. Вы наверное уже чувствуете, как боль и сомнение подбираются к вашему островку спокойствия и безмятежности, это нормально. Если пояснить проще, по сути, это просто структура в которую "напихали" полей, некоторые из которых являются указателями (в данном случае указателями на функции). Так же на этой странице документации есть табличка с названием полей структуры, их описанием и размерностью типа в котором будет сохранен указатель, и ссылочками на ремарки ко всему этому добру, ремарки достаточно полезны, потому что они дают еще немного информации о том, что из себя представляет SDL_RWops, a именно то, что SDL_RWops предоставляет интерфейс для чтения, записи и поиска данных в потоке, без необходимости вызывающему обладать знаниями откуда вообще приходят данные, т.е. для примера это может быть буфер памяти, файл на диске, или сетевое соединение с сервером, без необходимости вызывающей стороной изменять интерфейс для потребления данных. Пожалуй на этом можно остановиться, в попытках пояснить что из себя представляет SDL_RWops, но мы еще будем периодически обращаться к этой странице документации. Вновь взглянем на код из Pakfile.h :

class Pakfile { private: /// Internal structure for representing one file in this PAK-File struct PakFileEntry { uint32_t startOffset; uint32_t endOffset; std::string filename; }; /// Internal structure used by opened SDL_RWop struct RWopData { Pakfile* curPakfile; unsigned int fileIndex; size_t fileOffset; }; ...

В приватной секции класса мы видим две занимательные структуры, одна с названием PakFileEntry комментарий к которой гласит - "Внутренняя структура для представления одного файла в этом PAK файле". А вторая RWopData, комментарий к которой звучит как - "Внутренняя структура которая использована открытым SDL_RWops". В свою очередь PakFileEntry имеет в себе начало и конец смещения как целочисленные поля, и имя файла в виде строки. RWopData - указатель на текущий Pakfile, целочисленное поле для индекса файла и опять же смещение для файла. Пока вроде бы выглядит всё достаточно понятно, даже дядя Володя?!

Дальше идет публичная секция с открытым интерфейсом этого класса выглядит она вот так:

public: Pakfile(const std::string& pakfilename, bool write = false); ~Pakfile(); const std::string& getFilename(unsigned int index) const; /// Number of files in this pak-File. /** Returns the number of files in this pak-File. \return Number of files. */ inline int getNumFiles() const { return fileEntries.size(); }; SDL_RWops* openFile(const std::string& filename); bool exists(const std::string& filename) const; void addFile(SDL_RWops* rwop, const std::string& filename);

Сладкая парочка конструктор и деструктор класса. При этом конструктор принимает параметры, один из которых имя PAK файла переданное как константная ссылка на строку, второй булево значение с именем write, которое по всей видимости регулирует возможность записи в PAK файл, по умолчанию оно возведено в false. Далее идет объявление метода getFilename, скорее всего он нужен для того чтобы получать имя файла по какому-то индексу в PAK файле. Следом идет метод для получения количества файлов в PAK файле, о чем на любезно сообщает комментарий, называется он getNumFiles. Метод openFile который по переданному имени файла скорее всего его собственно открывает и возвращает указатель на него как указатель на структуру SDL_RWops. Метод exists, судя из названия, будет проверять наличие файла в пакетном файле принимая параметр название файла. И последний метод открытого интерфейса класса - это addFile который видимо позволяет добавить фаил в пакетный файл, этот метод принимает два параметра один указатель на SDL_RWops структуру, а второй имя файла. Этот кусочек исходного кода выглядит на первый взгляд тоже достаточно понятно. Осталась последняя приватная секция в которой прячутся члены этого класса, которые представляют собой данные этого класса. Посмотрим и на них:

private: static size_t ReadFile(SDL_RWops* pRWop, void *ptr, size_t size, size_t n); static size_t WriteFile(SDL_RWops *pRWop, const void *ptr, size_t size, size_t n); static Sint64 SizeFile(SDL_RWops *pRWop); static Sint64 SeekFile(SDL_RWops *pRWop, Sint64 offset, int whence); static int CloseFile(SDL_RWops *pRWop); void readIndex(); bool write; SDL_RWops * fPakFile; std::string filename; char* writeOutData; int numWriteOutData; std::vector<PakFileEntry> fileEntries; };

Пришло время вернуться к странице документации по SDL_RWops, так как первых пять статических функций ничто иное, как кастомная реализация интерфейса SDL_RWops, представляющие из себя функции обратного вызова. Описание параметров для каждой можно прочитать в документации по SDL_RWops. Далее идет метод readIndex, скорее всего носящий утилитарный характер вспомогательной функции для реализации внутренней работы с индексированием среди файлов внутри PAK файла, ну или возможно индекса смещений, понятнее станет совсем скоро, когда мы посмотрим реализацию Pakfile.h. Далее идут те самые члены класса которые в себе содержат всю мякоть этого класса, т.е. его данные. Как упоминалось ранее, write это явный флаг указывающий на возможность писать в объект PAK файла. Указатель fPakFile, строка filename, символьный указатель writeOutData, целочисленная переменная numWriteOutData и вектор с названием fileEntries который хранит в себе элементы типа PakFileEntry, да вы все правильно помните, эта та самая структура внутреннего представления для файлов внутри PAK файла.

А теперь давайте смотреть на реализацию Pakfile.cpp, там очень интересно :3. Проще начать с конструктора:

/// Constructor for Pakfile /** The PAK-File to be read/write is specified by the pakfilename-parameter. If write==false the file is opened for read-only and is closed in the destructor. If write==true the file is opened write-only and written out in the destructor. \param pakfilename Filename of the *.pak-File. \param write Specified if the PAK-File is opened for reading or writing (default is false). */ Pakfile::Pakfile(const std::string& pakfilename, bool write) : write(write), fPakFile(nullptr), filename(pakfilename), writeOutData(nullptr), numWriteOutData(0) { if(write == false) { // Open for reading if( (fPakFile = SDL_RWFromFile(filename.c_str(), "rb")) == nullptr) { THROW(std::invalid_argument, "Pakfile::Pakfile(): Cannot open " + pakfilename + "!"); } try { readIndex(); } catch (std::exception&) { SDL_RWclose(fPakFile); throw; } } else { // Open for writing if( (fPakFile = SDL_RWFromFile(filename.c_str(), "wb")) == nullptr) { THROW(std::invalid_argument, "Pakfile::Pakfile(): Cannot open " + pakfilename + "!"); } } }

Комментарий к конструктору гласит приблизительно следующее: - "PAK файл будет прочитан или записан будучи определенным по параметру pakfilename. Если write == false, этот файл открыт только для чтения и закрыт в деструкторе. Если write == true, этот файл открыт только для записи и записывается в деструкторе". Тело конструктора довольно простое, в зависимости от того как был выставлен параметр write, обрабатывается та или иная вертка потока выполнения, инициализируя нашу внутреннюю структуру SDL_RWops с именем fPakFile используя функцию SDL_RWFromFile, если после попытки это сделать в обоих случаях fPakFile равен нулевому указателю, мы бросаем исключение с помощь вариативного макроса THROW. На самом деле самая интересная часть этого конструктора которая помогает нам продвинуться дальше в попытках понять что происходит, это внутренний закрытый метод реализации readIndex, давайте его и посмотрим:

void Pakfile::readIndex() { while(1) { PakFileEntry newEntry; if(SDL_RWread(fPakFile,(void*) &newEntry.startOffset, sizeof(newEntry.startOffset), 1) != 1) { THROW(std::runtime_error, "Pakfile::readIndex(): SDL_RWread() failed!"); } //pak-files are always little endian encoded newEntry.startOffset = SDL_SwapLE32(newEntry.startOffset); if(newEntry.startOffset == 0) { break; } while(1) { char tmp; if(SDL_RWread(fPakFile,&tmp,1,1) != 1) { THROW(std::runtime_error, "Pakfile::readIndex(): SDL_RWread() failed!"); } if(tmp == '\0') { break; } else { newEntry.filename += tmp; } } if(fileEntries.empty() == false) { fileEntries.back().endOffset = newEntry.startOffset - 1; } fileEntries.push_back(newEntry); } Sint64 filesize = SDL_RWsize(fPakFile); if(filesize < 0) { THROW(std::runtime_error, "Pakfile::readIndex(): SDL_RWsize() failed!"); } fileEntries.back().endOffset = static_cast<size_t>(filesize) - 1; }

Что мы имеем внутри? А внутри мы имеем два бесконечных цикла, при этом один из них вложен в другой. В объемлющем цикле мы создаем переменную типа PakFileEntry которая, как мы помним является внутренним представлением записи (файла) внутри PAK файла. Далее мы просто начинаем читать эти записи по одной попутно проверяя возможность этого чтения. Для этого мы используем функцию SDL_RWread. Если по какой-то причине, прочитать не удается просто кидаем исключение через уже известный нам макрос THROW. Если же все нормально, мы переходим к вот этому коду:

//pak-files are always little endian encoded newEntry.startOffset = SDL_SwapLE32(newEntry.startOffset);

Зачем я акцентирую внимание на этом фрагменте? Наверно вы уже догадались, просто посмотрев на комментарий к нему, в котором сказано, что PAK файл всегда закодирован с порядком байт little endian. Если вдруг вам интересно как происходит преобразование little endian порядка в ваш нативный порядок целевой хост машины, вы можете посмотреть это тут, а документацию по самой функции тут SDL_SwapLE32. Теперь когда мы добросовестно походили по предложенным ссылочкам и впитали в себя то, что можно было впитать, или просто освежили в памяти то, что и так знали, мы можем пойти дальше.

Далее идет проверка, не является ли стартовое смещение равным нулю, если это произошло мы просто выходим из цикла. Если же нет, заходим во внутренний цикл, который нужен для того чтобы сформировать имена файлов, происходит это побайтно. Как только мы достигли байта который указывает на признак конца символьной строки мы покидаем внутренний цикл, вычисляем смещение конца файла (это происходит со смещением на один проход цикла) попутно записывая его в соответствующее поле структуры newEntry и записываем заполненную структуру в вектор fileEntries. Все это продолжается до того момента пока мы не прочитаем весь PAK файл, а далее с помощью функции SDL_RWsize мы вычисляем размер всего PAK файла, проводим минимальные проверки и вычисляем конечное смещение последнего файла в PAK файле.

Функции обратного вызова.

Теперь можно посмотреть быстренько функции обратного вызова которые используются при ручной сборке экземпляра SDL_RWops в открытом методе openFile. Быстренько, потому как эта статья уже распухла как PAK файловая опухоль, в том числе из-за вот таких не смешных каламбуров. Стоит сразу оговориться, что Pakfile::writeFile мы опустим, т.к. это связано с особенность реализации openFile, и попросту этот метод просто заглушка возвращающая нуль. Смотрим:

int Pakfile::CloseFile(SDL_RWops *pRWop) { if((pRWop == nullptr) || (pRWop->hidden.unknown.data1 == nullptr) || (pRWop->type != PAKFILE_RWOP_TYPE)) { return -1; } RWopData* pRWopData = static_cast<RWopData*>(pRWop->hidden.unknown.data1); delete pRWopData; pRWop->hidden.unknown.data1 = nullptr; SDL_FreeRW(pRWop); return 0; }

Pakfile::CloseFile реализация проста и ожидаема для типичного терминатора, сначала идут минимальные проверки на нулевой указатель и соответствие переданного указателя на SDL_RWops тому, что он соответствует нашему типу таких указателей, да это то самое магическое число в Pakfile.h, а именно:

#define PAKFILE_RWOP_TYPE 0x9A5F17EC

На самом деле, документация по SDL_RWops, для поля type предполагает ограниченное количество типов, и для своих реализаций предоставляет символьную константу SDL_RWOPS_UNKNOWN, так что такое самовольное выдумывание значения для поля type может вызвать проблему при стечение некоторых обстоятельств. Но вернемся к методу Packfile::CloseFile. После того как мы проверили что переданный указатель на SDL_RWops нужного типа и не является нулевым указателем, и одно из его полей также не является нулевым указателем (скрытое поле для дополнительных данных). В общем после всех этих проверок мы просто начинаем освобождать память и указатели на внутренние данные класса, которые обслуживают переданный по указателю экземпляр SDL_RWops. Следующая функция обратного вызова Pakfile::SizeFile:

Sint64 Pakfile::SizeFile(SDL_RWops *pRWop) { if((pRWop == nullptr) || (pRWop->hidden.unknown.data1 == nullptr) || (pRWop->type != PAKFILE_RWOP_TYPE)) { return -1; } RWopData* pRWopData = static_cast<RWopData*>(pRWop->hidden.unknown.data1); Pakfile* pPakfile = pRWopData->curPakfile; if(pPakfile == nullptr) { return -1; } if(pRWopData->fileIndex >= pPakfile->fileEntries.size()) { return -1; } return static_cast<Sint64>(pPakfile->fileEntries[pRWopData->fileIndex].endOffset - pPakfile->fileEntries[pRWopData->fileIndex].startOffset + 1); }

Как и в прошлом методе первая, как и во всех методах которые из себя представляют функции обратного вызова, проверка входного значения на то, что он не нулевой указатель, и что он нужного типа. Далее из дополнительного скрытого поля вы достаем данные, если кто забыл это данные представленные в виде нашей внутренней структуры RWopData которая есть ни что иное как представление открытого SDL_RWops. Из указателя на RWopData получаем указатель на текущий Pakfile, и далее просто опять проверяем не является ли он нулевым. Проверяем тот факт, что fileIndex из RWopData не равен и не больше количества записей в fileEntries, так как это было бы не нормальным поведением в виду выхода за пределы диапазона. Ну и если все эти превентивные проверки прошли успешно, то пытаемся вернуть размер для переданного SDL_RWops получая разность из его конечного и начального смещения плюс поправка на единицу. Теперь настало время для Pakfile::SeekFile:

Sint64 Pakfile::SeekFile(SDL_RWops *pRWop, Sint64 offset, int whence) { if((pRWop == nullptr) || (pRWop->hidden.unknown.data1 == nullptr) || (pRWop->type != PAKFILE_RWOP_TYPE)) { return -1; } RWopData* pRWopData = static_cast<RWopData*>(pRWop->hidden.unknown.data1); Pakfile* pPakfile = pRWopData->curPakfile; if(pPakfile == nullptr) { return -1; } if(pRWopData->fileIndex >= pPakfile->fileEntries.size()) { return -1; } Sint64 newOffset; switch(whence) { case SEEK_SET: { newOffset = offset; } break; case SEEK_CUR: { newOffset = pRWopData->fileOffset + offset; } break; case SEEK_END: { newOffset = pPakfile->fileEntries[pRWopData->fileIndex].endOffset - pPakfile->fileEntries[pRWopData->fileIndex].startOffset + 1 + offset; } break; default: { return -1; } break; } if(newOffset > (pPakfile->fileEntries[pRWopData->fileIndex].endOffset - pPakfile->fileEntries[pRWopData->fileIndex].startOffset + 1)) { return -1; } pRWopData->fileOffset = static_cast<size_t>(newOffset); return newOffset; }

Пропускаем начало метода, потому что это те же проверки что и в прошлом методе. Начнем с того места где определяется новая локальная переменная которая будет хранить новое значения смещения newOffset. По сути, этот метод как в реализации по умолчанию, так и в реализации в классе Pakfile является аналогом fseek() из стандартной библиотеки языка C, но относительно SDL_RWops. Так же как и в fseek() у нас есть три символические константы (RW_SEEK_SET == 0, RW_SEEK_CUR == 1 и RW_SEEK_END == 2) для указания того места откуда будет производится поиск новой позиции смещения. Забавно, но факт, что во фрагменте и соответственно коде данного ремейка, эти константы не используются, а используются константы того самого fseek() без приставки RW_. Теперь когда более-менее понятно, что это за метод и как он работает, перейдем к следующему и последнему методу внутренней закрытой реализации класса - Pakfile::ReadFile:

size_t Pakfile::ReadFile(SDL_RWops* pRWop, void *ptr, size_t size, size_t n) { if((pRWop == nullptr) || (ptr == nullptr) || (pRWop->hidden.unknown.data1 == nullptr) || (pRWop->type != PAKFILE_RWOP_TYPE)) { return 0; } int bytes2read = size*n; RWopData* pRWopData = static_cast<RWopData*>(pRWop->hidden.unknown.data1); Pakfile* pPakfile = pRWopData->curPakfile; if(pPakfile == nullptr) { return 0; } if(pRWopData->fileIndex >= pPakfile->fileEntries.size()) { return 0; } uint32_t readstartoffset = pPakfile->fileEntries[pRWopData->fileIndex].startOffset + pRWopData->fileOffset; if(readstartoffset > pPakfile->fileEntries[pRWopData->fileIndex].endOffset) { return 0; } if(readstartoffset + bytes2read > pPakfile->fileEntries[pRWopData->fileIndex].endOffset) { bytes2read = pPakfile->fileEntries[pRWopData->fileIndex].endOffset + 1 - readstartoffset; // round to last full block bytes2read /= size; bytes2read *= size; if(bytes2read == 0) { return 0; } } if(SDL_RWseek(pPakfile->fPakFile,readstartoffset,SEEK_SET) < 0) { return 0; } if(SDL_RWread(pPakfile->fPakFile,ptr,bytes2read,1) != 1) { return 0; } pRWopData->fileOffset += bytes2read; return bytes2read/size; }

Опять же освежаем в памяти что из себя должна представлять функция обратного вызова read для SDL_RWops и соотносим это с нашей реализацией. Количество байт для чтения вычисляется как размер элемента умноженный на количество этих элементов для чтения, так мы получаем переменную bytes2read. Для того чтобы вычислить начальное положение смещения для чтения мы получаем индекс файла в открытом SDL_RWops и по нему отыскиваем в индексе представленном вектором запись соответствующую этому индексу, и далее берем startOffset этой записи и прибавляем к ней смещение текущей открытой SDL_RWops записи. А далее опять идут различные проверки чтобы обеспечить корректность чтения и не залезть на следующую запись и не прочитать чего-то лишнего. Затем после того как всё проверено и подкорректировано если в этом есть необходимость мы занимаемся уже вполне знакомыми нам делами, а именно - устанавливаем курсор на смещение readstartoffset попутно проверяя не является ли оно нулевым через функцию SDL_RWseek. Теперь дело осталось за малым - прочитать все это через SDL_RWread, что и делается дальше и подкорректировать поле fileOffset из открытого дата SDL_RWops'a прибавив к нему bytes2read так как к этому моменту мы уже их прочитали и смещение поменялось. Соответственно, чтобы вернуть количество прочитанных элементов как этого требует описание функции, мы делим количество прочитанных байтов bytes2read на размер одного элемента size.

Методы открытого интерфейса класса Pakfile.

И вот это свершилось, мы добрались до методов которые будут использоваться как открытый интерфейс к классу (да, всё ещё к тому же самому классу Kappa ). Так как мы уже затрагивали ранее один из этим методов, а именно Packfile::openFile начнем с него. Комментарий к нему выглядит вот так:

/// Opens a file in this PAK-File.

/** This method opens the file specified by filename. It is only allowed if the Pakfile is opened for reading. The returned SDL_RWops-structure can be used readonly with SDL_RWread, SDL_RWsize, SDL_RWseek and SDL_RWclose. No writing is supported. If the Pakfile is opened for writing this method returns nullptr.<br> NOTICE: The returned SDL_RWops-Structure is only valid as long as this Pakfile-Object exists. It gets invalid as soon as Pakfile:~Pakfile() is executed.

\param filename The name of this file

\return SDL_RWops for this file

*/

"Этот метод открывает файл определенный по filename. Он доступен только для PAK файлов которые открыты для чтения. Возвращаемся этим методом структура SDL_RWops может быть использована только для чтения функциями SDL_RWread, SDL_RWsize, SDL_RWseek и SDL_RWclose. Запись не поддерживается. Если PAK файл открыт а режиме для записи этот метод возвращает nullptr. ПРИМЕЧАНИЕ: Возвращенна SDL_RWops структура действительна только до того момента, пока существует этот PAK файл объект. Она становится недействительной как только будет выполнен Pakfile::~Pakfile()".

Комментарий более чем описывает предназначение метода, поэтому просто давайте посмотрим что там внутри:

SDL_RWops* Pakfile::openFile(const std::string& filename) { if(write == true) { // reading files is not allowed return nullptr; } // find file int index = -1; for(unsigned int i=0;i<fileEntries.size();i++) { if(filename == fileEntries[i].filename) { index = i; break; } } if(index == -1) { return nullptr; } // alloc RWop SDL_RWops *pRWop; if((pRWop = SDL_AllocRW()) == nullptr) { return nullptr; } // alloc RWopData RWopData* pRWopData = new RWopData(); pRWop->type = PAKFILE_RWOP_TYPE; pRWopData->curPakfile = this; pRWopData->fileOffset = 0; pRWopData->fileIndex = index; pRWop->read = Pakfile::ReadFile; pRWop->write = Pakfile::WriteFile; pRWop->size = Pakfile::SizeFile; pRWop->seek = Pakfile::SeekFile; pRWop->close = Pakfile::CloseFile; pRWop->hidden.unknown.data1 = (void*) pRWopData; return pRWop; }

Первый условный блок проверяет не выставлен ли флаг write в true, потому что нам этого не надо. Потом, сопоставляя переданное имя файла с именем файла в fileEntries получаем индекс, проверяем чтобы он был положительный, а потом просто берем и создаем вручную SDL_RWops! Для этого аллоцируем под структуру память используя SDL_AllocRW, попутно проверяя что память действительно выделена. Создаем экземпляр RWopData и заполняем его. А потом заполняем SDL_RWops структуру и возвращаем её, файл открыт господа! Это было просто, идем дальше. Пробежимся быстро по двум простым методам:

bool Pakfile::exists(const std::string& filename) const { for(unsigned int i=0;i<fileEntries.size();i++) { if(filename == fileEntries[i].filename) { return true; } } return false; }

Pakfile::exists метод для проверки того факта, что файл уже существует внутри PAK файла. В нем мы просто в цикле пробегаем по fileEntries и сопоставляем переданное в метод имя файла с теми что есть в fileEntries, нашли возвращаем true, нет false.

const std::string& Pakfile::getFilename(unsigned int index) const { if(index >= fileEntries.size()) { THROW(std::invalid_argument, "Pakfile::getFilename(%ud): This Pakfile has only %ud entries!", index, fileEntries.size()); } return fileEntries[index].filename; }

Pakfile::getFilename метод получения имени файла, по его индексу. А теперь последний метод, чуть посложнее.

void Pakfile::addFile(SDL_RWops* rwop, const std::string& filename) { if(write == false) { THROW(std::runtime_error, "Pakfile::addFile(): Pakfile is opened for read-only!"); } if(rwop == nullptr) { THROW(std::invalid_argument, "Pakfile::addFile(): rwop==nullptr is not allowed!"); } size_t filelength = static_cast<size_t>(SDL_RWsize(rwop)); char* extendedBuffer; if((extendedBuffer = (char*) realloc(writeOutData,numWriteOutData+filelength)) == nullptr) { throw std::bad_alloc(); } else { writeOutData = extendedBuffer; } if(SDL_RWread(rwop,writeOutData + numWriteOutData,1,filelength) != filelength) { // revert the buffer to the original size char* shrinkedBuffer; if((shrinkedBuffer = (char*) realloc(writeOutData,numWriteOutData)) == nullptr) { // shrinking the buffer should not fail THROW(std::runtime_error, "Pakfile::addFile(): realloc failed!"); } writeOutData = shrinkedBuffer; THROW(std::runtime_error, "Pakfile::addFile(): SDL_RWread failed!"); } PakFileEntry newPakFileEntry; newPakFileEntry.startOffset = numWriteOutData; newPakFileEntry.endOffset = numWriteOutData + filelength - 1; newPakFileEntry.filename = filename; fileEntries.push_back(newPakFileEntry); numWriteOutData += filelength; SDL_RWseek(rwop,0,SEEK_SET); }

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

/// Adds a file to this PAK-File

/** This methods adds the SDL_RWop File to this PAK-File. The used name is specified by filename. If the Pakfile is read-only this method has no effect.

\param rwop Data to add (the SDL_RWop can be read-only but must support seeking)

\param filename This is the filename the data is added with*/

"Добавляет файл в PAK файл. Этот метод добавляет SDL_RWop файл в этот PAK файл. Ипользовав имя определенное по filename. Если PAK файл открыт только для чтения метод не имеет эффекта."

Вначале метода идут две проверки, одна на то, что файл открыт для записи, а вторая на то, что переданный SDL_RWops не нулевой указатель. Далее вычисляется длинна файла filelength через SDL_RWsize. Далее подготавливаем буфер для последующего чтения в него SLD_RWops. Если проверка на выделение памяти прошла успешно, extendedBuffer присваивается writeOutData. Читаем SDL_RWops в наш буфер, попутно проверяя корректность этого чтения, и если есть необходимость восстанавливаем длинну буфера. А дальше мы просто создаем запись типа PakFileEntry инициализируем её поля, и записываем в наш PAK файл, обновляем значение numWriteOutData на длинну файла и выставляем курсор для нашего SDL_RWops.

В общем-то, это почти всё. Остался один деструктор. Но перед тем как его посмотреть я бы хотел дополнить статью относительно того, что из себя вообще представляют PAK файлы. Выше уже об этом сказано, и о том, что реализация может разниться от случая к случаю. Как показал этот пример, в случае Westwood Studios их вариант, не имеет сжатия, не имеет шифрования (как вы увидите уже прямо сейчас). Еще немного поискав информации в интернете я нашел вот такой wiki портал, с множеством описаний игровых форматов файлов и не только. В том числе там есть и описание формата PAK файла всех трех ревизий от Westwood Studios.

Pakfile::~Pakfile() { if(write == true) { // calculate header size int headersize = 0; for(unsigned int i = 0; i < fileEntries.size(); i++) { headersize += 4; headersize += fileEntries[i].filename.length() + 1; } headersize += 4; // write out header for(unsigned int i = 0; i < fileEntries.size(); i++) { #if SDL_BYTEORDER == SDL_BIG_ENDIAN Uint32 startoffset = SDL_Swap32(fileEntries[i].startOffset + headersize); #else Uint32 startoffset = fileEntries[i].startOffset + headersize; #endif SDL_RWwrite(fPakFile,(char*) &startoffset,sizeof(Uint32),1); SDL_RWwrite(fPakFile,fileEntries[i].filename.c_str(), fileEntries[i].filename.length() + 1,1); } Uint32 tmp = 0; SDL_RWwrite(fPakFile,(char*) &tmp, sizeof(Uint32), 1); // write out data SDL_RWwrite(fPakFile,writeOutData,numWriteOutData,1); } if(fPakFile != nullptr) { SDL_RWclose(fPakFile); } if(writeOutData != nullptr) { free(writeOutData); writeOutData = nullptr; numWriteOutData = 0; } }

Сначала мы проверяем открыт ли PAK файл для записи, вычисляем размер заголовка, далее записываем заголовок не забывая поменять порядок байт если это необходимо, в порядке смещение + имя файла, после того как вы записали все эти пары, отделяем заголовок от данных записывая 0. Далее пишутся данные. Перед закрытием проверяем не нулевой указатель ли наш Pakfile. А далее просто очищаем всю вручную выделенную память и устанавливаем начальные значения для внутренних переменных. Вот в общем-то и всё.

Итоги.

  • Искушенный в программировании человек увидит недостатки данной реализации, как в техническом, так и в плане проектирования, да они есть... Но в защиту можно сказать, что это типичный ситуативный код, автор не пытался сделать библиотеку на все случаи жизни для работы с PAK файлами от Westwood Studios. А просто решал насущные проблемы, по мере их поступления.
  • Данный код, является примером работы с SDL_RWops, а также примером того, как можно организовать ваши ресурсы в виде одного бинарного архива.
  • Проблема организации ресурсов(ассетов) игры имеется во всех играх.

Надеюсь кому-то было интересно прочитать всё это. Оставляйте комментарии если кто-то хочет увидеть продолжение раскопок в этой и других играх :3

1313
4 комментария
Комментарий удалён модератором

Ну главное что интересно, значит есть смысл это продолжать периодически )

1
Ответить

Блэт, это дфт, а не хабр.

3
Ответить

Да, мы пришли деградировать, а тут это.

Ответить