【课后作业答案】经典解谜单机小游戏《梦之旅系列》的PFP文件解压工具源代码
本帖最后由 烟99 于 2024-11-1 21:11 编辑大家好!在那天的我讲的关于某国产游戏的PCK算法分析你学会了吗?课后作业做了吗?今天揭晓课后作业参考答案。
文件格式大致分为四部分:
1、第一部分:包文件的头部,用于识别是这种格式的包文件,占用4字节且字节内容固定,为:0x50 0x46 0x50 0x4B
2、第二部分:包内的文件数量,占用4字节,将这4个拼接成一个十六进制值就是它的真实文件数量。
3、第三部分:包文件的目录表,里面记录了文件绝对路径的大小,占用一个字节,因此这意味着文件名最长不能超过FFh(即255个字节);紧跟其后是文件的绝对路径,占用若干字节,紧跟其后是文件偏移,占用4个字节;紧跟其后是文件大小,占用4个字节。
4、第四部分:包文件的数据区,将所有文件拼接在一起形成的区域。
你答对了吗?
本工具理论上讲只要是基于PlaygroundSDK开发的游戏里的PFP文件都可以解开,我只测试了梦之旅五部曲和极乐浪子共六款游戏都没有问题,其他游戏请自行尝试。另外PlaygroundSDK 开发的游戏支持加载解包后的内容,所以不需要考虑如何把文件压回去。
工具GUI界面效果图
# 《PFPExtractetor》软件源代码
欢迎学习《PFPExtractetor》软件源代码!本源码遵循Creative Commons Attribution - NonCommercial许可协议,你可以在保留源码署名并保证不用于商业用途的前提下自由的使用、分发、修改本源码,但坚决不允许用于商业用途!
## 基本信息
源码名称:PFPExtractetor
源码版本:1.0.0
源码作者:52pojie.cn
遵循协议:Creative Commons Attribution - NonCommercial
源码语言:C#
.NET Framework框架版本:4.5
## 基本介绍
本工具用于演示PFP格式的游戏资源文件的解压缩功能,理论上讲可以解压缩游戏厂商PlayFirst开发或发行所有格式为PFP的游戏资源包文件,同时基于该厂商的PlaygroundSDK开发的其他游戏也可解压。本软件演示了PFP文件算法的文件偏移、大小、文件目录信息的存储方式和数据区的内容存储规则,对研究资源逆向基础提供了重要参考资料。
## 源码更新日志
"--------------------------------------------
2024.11.01
"--------------------------------------------
源码对外公开
## 如何编译
本软件使用了Tuple多元List,因此依赖于.NET Framework V4.5运行,原则上Visual Studio 2015就可以编译,但是本人是在Visual Studio 2022中编译的,因此建议在Visual Studio 2022中编译。
## 郑重声明
1、本工具为以源码的形式发布,软件源码版权归吾爱破解所有,未经许可,禁止擅自编译发布!转载请注明出处!
2、本工具仅用于研究和讨论游戏文件存储技术,游戏资源文件归游戏开发商所有,切勿用于商业用途,否则源码作者不承担连带责任!
3、本源码遵循Creative Commons Attribution - NonCommercial许可协议,你可以在保留源码署名并保证不用于商业用途的前提下自由的使用、分发、修改本源码,但坚决不允许用于商业用途。
## 关键源码展示
/// <summary>
/// <List> 取PFP内部文件列表
/// <param name="pfpfilePath">(文本型 欲获取内部文件列表的PFP文件) </param>
/// <returns><para>成功返回PFP文件的单个文件的目录长度、完整文件路径、、文件偏移、实际大小、压缩大小,并封装在 <List>中</para></returns>
/// </summary>
public static List<Tuple<int, string, long, long>> GetPFPInformation(string pfpfilePath)
{
List<Tuple<int, string, long, long>> pfpList = new List<Tuple<int, string, long, long>>();
using (FileStream fs = new FileStream(pfpfilePath, FileMode.Open))
{
// 检查是否为PFP文件,如果不是,抛出异常
byte[] fileHead = { 0x50, 0x46, 0x50, 0x4B }; //PFP文件头“PFPK”的Hex字节集数组
byte[] checkHead = new byte; //欲打开的PFP文件头
// 读入PFP文件头
fs.Read(checkHead, 0, fileHead.Length);
Console.WriteLine(checkHead.SequenceEqual(fileHead));
// 若两边的字节集数组内容不一样或者长度不一样则视为不是PFP文件,并抛出异常
if (!checkHead.SequenceEqual(fileHead) || fileHead.Length != checkHead.Length)
{
throw new Exception("The file\"" + pfpfilePath + "\" is not a valid PFP format file!");
}
// 头部未发现异常,开始解析文件列表
//取文件个数
byte[] fileCountBit = new byte; // 字节集状态下的数据区物理大小数值
long fileCount; // 长整型的数据区物理大小数值
// 定位到文件的0004h
fs.Seek(4, SeekOrigin.Begin);
// 取PFP内部文件数量信息
fs.Read(fileCountBit, 0, fileCountBit.Length);
// 将取到的pfp内部文件信息补满八字节,然后转换成长整型数值
fileCount = BitConverter.ToInt64(PublicFunction.EightByteConverter(fileCountBit), 0);
Console.WriteLine("PFP文件数量为:" + fileCount.ToString());
Console.WriteLine("获取完数量的偏移为:" + fs.Position.ToString("X"));
// 创建字节偏移记录,并将文件流移到08h处
long byteOffset = fs.Position;
fs.Seek(byteOffset, SeekOrigin.Begin);
Console.WriteLine("开始读取文件列表时候的偏移为:" + fs.Position.ToString("X"));
// 开始读取文件
for (int i = 0; i < fileCount; i++)
{
// 取目录长度
byte[] flieNameLenghBit = new byte;
fs.Read(flieNameLenghBit, 0, 1);
int flieNameLengh = BitConverter.ToInt32(PublicFunction.EightByteConverter(flieNameLenghBit), 0);
//偏移移动一位
byteOffset += 1;
fs.Seek(byteOffset, SeekOrigin.Begin);
// 取文件名
byte[] flieNameBit = new byte;
fs.Read(flieNameBit, 0, flieNameLengh);
string flieName = Encoding.Default.GetString(flieNameBit);
//偏移移动文件名长度位数
byteOffset += flieNameLengh;
fs.Seek(byteOffset, SeekOrigin.Begin);
// 取文件偏移
byte[] fileOffsetBit = new byte;
fs.Read(fileOffsetBit, 0, fileOffsetBit.Length);
// 四字节补到八字节,再转换成长整型数值,
long fileOffset = BitConverter.ToInt64(PublicFunction.EightByteConverter(fileOffsetBit), 0);
//偏移四个字节
byteOffset += 4;
fs.Seek(byteOffset, SeekOrigin.Begin);
// 取文件大小
byte[] fileSizehBit = new byte;
// 四字节补到八字节,再转换成长整型数值,
fs.Read(fileSizehBit, 0, fileSizehBit.Length);
long fileSizeBit = BitConverter.ToInt64(PublicFunction.EightByteConverter(fileSizehBit), 0);
//偏移四个字节
byteOffset += 4;
fs.Seek(byteOffset, SeekOrigin.Begin);
// 调试输出此文件信息
Console.WriteLine("-------------------\n" +
"读取第" + (i + 1) + "文件成功!\n" +
"===================\n" +
"文件名长度:" + flieNameLengh.ToString() + "\n" +
"文件名称:" + flieName + "\n" +
"文件偏移:0x" + fileOffset.ToString("X") + "\n" +
"文件大小:" + fileSizeBit.ToString() + "字节\n" +
"个文件后的偏移" + fs.Position.ToString("X") + "\n" +
"-------------------\n\n");
// 加入到四元List
pfpList.Add(Tuple.Create(flieNameLengh, flieName, fileOffset, fileSizeBit));
}
// 返回四元List
return pfpList;
}
}
因篇幅原因,只展示文件列表的获取,解压操作略,请到GitHub中查看完整源码。
## 意见或建议
可通过论坛回帖留言的方式反馈,也可私信该帖楼主也就是我来反馈。禁止留QQ、微信等联系方式,对利用私信留联系方式的行为将从重处罚!
https://www.52pojie.cn/thread-1977704-1-1.html
## 源码链接
暂时隐藏,五天后开放。
**** Hidden Message *****
LuoShang 发表于 2024-11-1 22:58
大佬我想问一下,如果是apk有壳,他里面的pak包会不会也有壳,会影响解压吗
你说的壳我不知道是不是说的是加密,如果有加密就要先解开加密,比较低级的加密比如异或运算,举个最常见的例子吧,植物大战僵尸,这个游戏的pak文件就是先把文件整体字节集读入,然后和十六进制F7h进行异或运算,运算后的字节集才是明文的,但是还没完,根据现有资料显示,他那个pak和这个差不多,也是前半部分是目录区,后半部分是数据区,目录区也是用一个字节表示文件名长度,也是占用若干字节记录文件名信息,后面也是记录文件大小,但是没有记录文件偏移,而且文件大小后面的八个字节没搞清楚是干什么的,然后填充了一个00才是下一个文件是信息块,所以植物大战僵尸的pak不能通过文件偏移来定位文件数据,只能自己写变量一点点的加 本帖最后由 LuoShang 于 2024-11-2 12:38 编辑
烟99 发表于 2024-11-1 23:21
你说的壳我不知道是不是说的是加密,如果有加密就要先解开加密,比较低级的加密比如异或运算,举个最常见 ...
我在其他地方找到了植物大战僵尸的pak解包软件,是可以正常使用的,那个apk我查壳是爱加密的,这种是不是很麻烦,我尝试直接丢进那个pak解包软件,是解不出来的 看到了,回复表示尊重。 谢谢大佬分享! 哇哦,感谢感谢分享 大佬我想问一下,如果是apk有壳,他里面的pak包会不会也有壳,会影响解压吗 谢谢大佬分享! 看到了,回复表示尊重。 LuoShang 发表于 2024-11-2 12:36
我在其他地方找到了植物大战僵尸的pak解包软件,是可以正常使用的,那个apk我查壳是爱加密的,这种是不是 ...
他加密的是程序文件,pak文件是资源文件,和壳没有关系