Quake1和Quake2的源码都已经公开了,Github上可找到。源码里边有对pak文件解析的代码,不过我从这里paklib找到一个别人独立出来的一个库,代码不多:包括创建pak文件、向pak文件中添加资源、从pak文件中提取资源等。我看了下,然后把一些东西记录下来,以便以后忘记后回顾用。Quake3的资源包格式是pk3的,这种格式可以直接改成zip后缀,然后解压就可以得到里边的资源,pak格式的文件就无法通过此方法来获取里边的资源,所以得通过代码来解析。
pak文件是无压缩的,所以将资源打包进pak中并不会减小资源的体积,但是将所有资源打包到一个pak中方便携带,也可以阻止一些常规玩家随意修改资源文件。
那个paklib中的代码就不贴出来讲了,有兴趣的可以从那个链接中下载下来看看。这里只是讲下其中几个操作的原理:
要弄懂操作的原理,首先得清楚pak包的格式,它的格式其实很简单,给张示意图看下就懂了
// pak文件头,占12个字节
struct pakheader {
char head[4]; // 开头4个字节,固定存放"PACK"4个字符,用于确定是否为pak资源文件
unsigned int dir_offset; // 描述除结尾那个数组外,前面的所有字节数,用于确定结尾数组的起始位置
unsigned int dir_length; // 描述结尾那个数组总共多少个字节,除以数组元素的大小就等于中间存放资源的个数了
};
// 数组元素结构
struct dirsection {
char file_name[56]; // 资源名称,包括路径和文件名,不超过56字节
unsigned int file_position; // 资源在整个pak文件中的位置,也就是从第几个字节开始
unsigned int file_length; // 资源的大小,也就是该资源在pak中占几个字节
};
有了上面的信息,我们就可以创建一个空的pak文件,然后向文件中添加资源,再从文件中获取资源。
创建pak文件:
const char* pak = "resource.pak";
// 在磁盘中创建一个resource.pak文件(假设磁盘中还不存在该文件)
FILE* fp = fopen(pak, "wb");
struct pakheader header = {
.head = "PACK",
.dir_offset = 0;
.dir_length = 0;
};
// 将文件头信息写到pak文件中
fwrite(&header, sizeof(header), 1, fp);
向pak文件中添加资源。资源可以是任意的文件,声音、图片、文本等。添加资源其实也很简单,步骤就是:1、将资源通过fopen()打开到一个文件句柄中。2、从这个句柄中将数据fread()到一个内存中。3、将该内存中的数据fwrite()到pak文件句柄中。4、更新pak文件头和结尾数组的信息就行了。注意:资源在pak中是可以带"路径"的,例如将foo.png添加到pak中的images/foo.png。代码和paklib中差不多,可能不全,仅供演示。
const char* pak = "resource.pak";
const char* file = "foo.png";
FILE* fPak = fopen(pak, "wb");
FILE* fFile = fopen(file, "rb");
// 获取资源大小
fseek(fFile, 0, SEEK_END);
int iFileLength = ftell(fFile);
// 将文件句柄中的数据读到内存中
char buffer[iFileLength];
rewind(fFile);
fread(buffer, 1, iFileLength, fFile);
struct pakheader header;
struct dirsection section;
fread(&header, sizeof(header), 1, fPak);
if (header.dir_length == 0) { // pak文件中还不存在一个资源
// 将内存中的数据写到pak文件句柄中
fseek(fPak, sizeof(header), SEEK_SET);
fwrite(buffer, 1, iFileLength, fPak);
// 更新pak文件头
header.dir_offset = sizeof(header) + sizeof(char) * iFileLength;
header.dir_length = sizeof(struct dirsection);
fseek(fPak, 0, SEEK_SET);
fwrite(&header, sizeof(header), 1, fPak);
// 更新pak文件结尾数组
strcpy(section.file_name, "images/foo.png");
section.file_position = sizeof(header);
section.file_length = iFileLength;
fseek(fPak, header.dir_offset, SEEK_SET);
fwrite(§ion, sizeof(section), 1, fPak);
} else { // pak文件中已有资源
// 读取结尾数组的数据到内存中
int count = header.dir_length / sizeof(struct dirsection) - sizeof(header);
struct dirsection sections[count + 1];
fseek(fPak, header.dir_offset, SEEK_SET);
fread(sections, sizeof(struct dirsection), count);
// 将内存中的数据写到pak文件句柄中
fseek(fPak, header.dir_offset, SEEK_SET);
fwrite(buffer, 1, iFileLength, fPak);
// 更新pak文件头
header.dir_offset = header.dir_offset + iFileLength;
header.dir_length = header.dir_length + sizeof(struct dirsection);
rewind(fPak);
fwrite(&header, sizeof(header), 1, fPak);
// 更新结尾数组
strcpy(sections[count].file_name, "images/foo.png");
sections[count].file_position = header.dir_offset - iFileLength;
sections[count].file_length = iFileLength;
fseek(fPak, header.dir_offset, SEEK_SET);
fwrite(sections, sizeof(struct dirsection), count + 1, fPak);
}
fclose(fPak);
fclose(fFile);
从pak文件中读取资源到内存中或解析出来。通过给定的资源名称(和pak中的一样,包括路径),从pak中读取资源到内存中的操作和添加操作差不多,先读取结尾的数组,然后遍历该数组,判断数组元素中的file_name和所给的资源名称是否相同,最后根据该元素的相关信息从pak文件句柄中读取数据到内存中,也可以将内存中的数据fwrite()到一个文件,这样就解析到磁盘了。因为是用字符来查找元素的,所以无法通过二分法来查找,只能遍历,因此效率会有点低。
const char* szDstFile = "images/foo.png";
FILE* fPak = fopen("resource.pak", "r");
struct pakheader header;
fread(&header, sizeof(header), 1, fPak);
int count = header.dir_offset / sizeof(struct dirsection) - sizeof(header);
struct dirsection sections[count];
fseek(fPak, header.dir_offset, SEEK_SET);
fread(§ions, sizeof(struct dirsection), count, fPak);
FILE* fFile = fopen("foo.png", "wb");
for (int i = 0; i < count; i++) {
if (strcmp(sections[i].file_name, szDstFile) == 0) {
char buffer[section[i].file_length];
fseek(fPak, sections[i].file_position, SEEK_SET);
fread(buffer, sections[i].file_length, 1, fPak);
fwrite(buffer, sections[i].file_length, 1, fFile);
break;
}
}
fclose(fPak);
fclose(fFile);
上面的代码很乱,也没经过调试,不过原理就是这样了。 : )