简介:“cri packed file maker”是一款面向游戏开发者、资源分析人员及MOD爱好者的CPK文件处理工具,用于解包、查看和管理以CRI File System方式打包的加密资源。工具界面直观,支持将包含CPK文件的文件夹直接拖放处理,只需指定输出路径即可自动解包,内置解密逻辑,使用者无需关心算法细节即可处理常见加密CPK包,适合游戏调试、资源提取和二次创作等场景。
资源包共15个文件,约939KB,包含程序主执行文件exe、核心动态库dll、英文说明书chm、配置settings、csv/xls列表以及若干bmp示例图片等,兼顾程序运行、参数配置与操作参考。压缩包内还附带CRI File System相关工具手册和示例目录,便于初学用户对照学习整个解包流程。
目前已有3339人学习下载,对于需要接触CPK格式或研究游戏资源结构的用户而言,这套小体积工具包提供了较完整的一站式方案,能显著降低解包与解密门槛;配套文档和示例又进一步提升了自学效率,适合作为日常处理CPK文件的常备工具。
1. CRI Packed File Maker 到底在打包什么:一个单文件搞定资源索引、对齐与加密
接手一个跨平台游戏项目时,我第一次看到资源目录里躺着几十个 .cpk 文件,同事只说了一句“这是 CRI Packed File Maker 打的包,你照着打包就行”。当时的感觉是:这东西像个黑匣子,双击工具拖个目录、点个按钮,出来的单文件就能被引擎直接读走;可一旦参数不对,游戏在加载页卡住,日志里只有一个看不懂的归档错误,连从哪查起都不知道。CRI Packed File Maker 是 CRI 官方出的 CPK(CRI Packed)归档打包工具,用来把工程里的音频、视频、贴图、脚本等资源整理成单一索引文件,并提供对齐、压缩、加密和增量更新能力。这篇笔记适合两类人:要给项目接资源包管线的客户端或工具链工程师,以及想读懂别人 CPK 结构做兼容或移植的开发者。
2. 拆开一个 .cpk 看内部结构:头部字段表与 TOC 目录怎么读
用工具之前,我建议先弄明白它到底在写什么。CPK 不是把一堆文件简单拼起来就完事,它在包的最前面维护了一张“目录”,读取端靠这张目录做随机读、按 ID 查文件、按偏移取数据。所以你在 Maker 里点的每一个参数,本质都是在决定这张目录怎么写、文件怎么摆。
2.1 从 “CPK ” 魔数开始的头部:一张可对照的字段表
所有 CPK 文件都以 4 个字节CPK(0x43 0x50 0x4B 0x20)开头,紧跟着是一段固定结构的头部。下面是社区解包工具里最常见的 v2/v3 头部布局,按小端序读取:
| 偏移 | 类型 | 字段 | 含义 |
|---|---|---|---|
| 0x00 | char[4] | signature | 魔数 “CPK ” |
| 0x04 | u32 | header_size | 头部自身长度 |
| 0x08 | u64 | file_size | 整个 CPK 文件大小 |
| 0x10 | u64 | toc_offset | TOC 目录区在文件中的偏移 |
| 0x18 | u32 | toc_size | TOC 目录区大小 |
| 0x1C | u32 | file_num | 包内文件总数 |
| 0x20 | u32 | align | 文件起始偏移的对齐字节数 |
| 0x24 | u8 | sjis | 文件名编码标记,非 0 表示 SJIS |
| 0x25 | u16 | ver | CPK 版本,常见 0x0100 / 0x0200 / 0x0300 |
| 0x27 | u8 | update | 是否为增量更新包 |
| 0x28 | u8 | encrypt | 包内文件是否加密 |
这里要特别说明:这个布局是从社区逆向笔记里整理出来的,v1 以及个别官方工具版本会把 64 位字段缩成 32 位,字段偏移也会有出入。所以你拿这个表去对任何 CPK 文件之前,先做好“布局可能不匹配”的心理准备,后面第 2.3 节的脚本里我会加合理性检查,就是为了兜这个底。
2.2 TOC 目录四件套:CpkInfo / TocInfo / ItocInfo / EtocInfo 各管什么
头部之后就是 TOC 目录区。TOC 里通常包含四段信息,各自职责完全不同:
- CpkInfo:包级元信息,对应头部里那些字段的“完整声明”,包括文件数、对齐、版本、编码、加密标记。
- TocInfo:文件级目录,一个文件一条记录,包含压缩后大小(FileSize)、解压后大小(ExtractSize)、文件在包内的偏移(FileOffset)、属性(Attrib)、文件 ID(ID)以及文件名或文件名哈希。
- ItocInfo:索引目录,按文件名或 ID 的哈希值建索引,读取端拿到哈希就能直接定位到 TocInfo 里的记录,不需要逐条做字符串比较。
- EtocInfo:扩展目录,记录被删除的文件、快捷方式条目等,增量更新和差异包主要靠它。
TocInfo 里的字段值得多看一眼。FileSize 和 ExtractSize 分开存,是因为文件可能被压缩存放:读取端先读 FileSize 知道要从包内拿多少字节,再看 ExtractSize 决定解压后分配多大缓冲区。FileOffset 则让随机读变成一次 seek 加一次 read,不用扫描整个包。这也是 CPK 在游戏里广泛使用的原因之一——大包也能做到接近 O(1) 的资源定位。
ItocInfo 有个容易被忽略的设计:打包时可以选“保存完整文件名”还是“只保存 64 位哈希”。只存哈希能省不少空间,但代价是调试时看不到文件名,社区解包工具也显示不出可读路径。我一般只在正式发布包上开哈希,开发期全开完整文件名,省得自己拿十六进制编辑器对着 ID 猜文件是谁。
2.3 用一段最小脚本给 CPK 做“体检”:cpk_meta.py
打出的包对不对,别急着塞进游戏里验证。先写个小脚本把头部读出来,对照字段表看一遍,很多问题在出包那一刻就能发现。下面是我常用的体检脚本:
import re import struct import sys import zlib def read_cpk_meta(path): with open(path, 'rb') as f: head = f.read(0x40) if len(head) < 0x40: raise ValueError('文件太短,不是有效的 CPK') if head[0:4] != b'CPK ': raise ValueError('魔数错误:文件应以 "CPK " 开头') # 头部按 v2/v3 常见布局读取,小端序; # v1 或个别工具版本的 64 位字段可能只有 32 位,读完必须做合理性检查。 header_size = struct.unpack_from('<I', head, 0x04)[0] file_size = struct.unpack_from('<Q', head, 0x08)[0] toc_offset = struct.unpack_from('<Q', head, 0x10)[0] toc_size = struct.unpack_from('<I', head, 0x18)[0] file_num = struct.unpack_from('<I', head, 0x1C)[0] align = struct.unpack_from('<I', head, 0x20)[0] sjis = head[0x24] ver = struct.unpack_from('<H', head, 0x25)[0] update = head[0x27] encrypt = head[0x28] actual_size = f.seek(0, 2) print(f'header_size : {header_size}') print(f'file_size : {file_size} (实际文件 {actual_size})') print(f'toc_offset : {toc_offset}') print(f'toc_size : {toc_size}') print(f'file_num : {file_num}') print(f'align : {align}') print(f'sjis : {sjis}') print(f'ver : {ver:#06x}') print(f'update : {update}') print(f'encrypt : {encrypt}') # 合理性检查:TOC 区域必须落在文件范围内, # 不满足时说明当前解析布局不适用于这个 CPK 版本。 if toc_offset == 0 or toc_offset + toc_size > actual_size: print('警告:TOC 区域超出文件范围,当前布局可能不匹配这个版本') return f.seek(toc_offset) toc_raw = f.read(toc_size) # TOC 可能整体被 zlib 压缩,也可能明文存储,两种都要处理 if toc_raw[:2] in (b'\x78\x9c', b'\x78\x01', b'\x78\xda'): toc = zlib.decompress(toc_raw) print('TOC 压缩方式:zlib(已解压再做字段扫描)') else: toc = toc_raw print('TOC 压缩方式:未压缩') # v2/v3 的字段名会以 ASCII 形式出现在 TOC 流里, # 扫出来可以快速确认目录里到底写了哪些信息 keys = re.findall(rb'[A-Za-z_]{4,}', toc) print('TOC 字段名样本:', [k.decode('utf-8', 'replace') for k in keys[:15]]) if __name__ == '__main__': read_cpk_meta(sys.argv[1])脚本逻辑不复杂:先用struct.unpack_from按小端读头部各字段,u32 用I、u64 用Q、u16 用H,然后做“TOC 区域是否落在文件内”的合理性检查,最后读 TOC 区域并尝试 zlib 解压,再用正则把里面的字段名样本扫出来。
用法就一行:python3 cpk_meta.py xxx.cpk。输出里重点看三处:file_size是否和实际文件大小接近,file_num是否等于你往包里放的文件数,ver是不是读取端支持的版本。如果file_size差得很离谱,先别怀疑脚本,大概率是版本布局不同,参考第 5 章的排查方法处理。
提示:这个脚本把 v2/v3 当默认布局,遇到 v1 或特殊版本产物时会打警告。它定位是“体检”,不是完整解析器,别指望它解出文件内容。
3. 用 CRI Packed File Maker 产出第一个包:GUI 与命令行的完整流程
理解了文件结构,再用 Maker 就顺手很多。整个流程可以拆成三步:准备输入目录、配置输出与参数、构建并验证。官方工具的主界面一般按“打包、更新、差异、查看”这几个功能分成页签,具体文字和布局随版本有出入,但操作逻辑是稳定的。
3.1 GUI 打包含路:建工程、拖目录、选输出
在 GUI 里打一个最简单的包,我一般按下面几步走:
- 在打包页新建工程,或者直接把整个资源目录拖进内容区。先把目录里的临时文件清掉,后面会解释为什么这步很重要。
- 设置输出路径和包名,输出路径不要放在输入目录内部,否则可能把上一次的包也当成素材。
- 确认“是否保留相对目录结构”。保留的话,读取端能拿到完整路径;不保留则所有文件平铺,同名文件会互相覆盖,这个选项选错很容易造成资源互相顶掉。
- 参数区选择对齐、压缩类型、文件名编码。第一次打建议全用默认,先把流程跑通,参数调优放到第 4 章。
- 点 Build 开始打包,完成后打开日志,确认 file_num 和总大小符合预期。
- 用官方自带的 Viewer 打开刚生成的 CPK,看目录树、预览几个关键资源,这一步是对“目录写没写对”的人工验证。
这里有个容易误会的点:Maker 会在输出目录生成日志和中间文件,如果你把输出目录设成输入目录的子目录,这些生成物会被下一次打包扫进去。很多“包越打越大、里面净是奇怪文件”的问题,根源就在这里。
3.2 命令行批处理:用配置文件和文件清单驱动构建
要发版就得出包,反复在 GUI 里点按钮不现实。常见做法是官方工具提供命令行入口,配合一份在 GUI 里保存好的配置文件,把整个打包过程脚本化。下面是我常用的批处理骨架:
#!/bin/bash # 站点变量按实际环境改;官方命令行工具的入口名和开关名, # 以你手上的版本帮助信息为准,这里只是常见命名习惯。 MAKER_CLI="/path/to/cpk_maker_cli" CFG_FILE="./build/cpk_config.json" # GUI 里保存好的打包配置 LIST_FILE="./build/filelist.txt" # 文件清单,每行一个相对路径 OUT_CPK="./build/asset_v3.cpk" # 1) 构建前:确认输入清单里的文件都存在,缺一个就退出 while read -r rel; do [ -f "$rel" ] || { echo "missing: $rel"; exit 1; } done < "$LIST_FILE" # 2) 构建:-cfg 复用 GUI 调好的参数,避免命令行写一长串开关 "$MAKER_CLI" -cfg "$CFG_FILE" -list "$LIST_FILE" -o "$OUT_CPK" || exit 1 # 3) 构建后:体检产物,重点看 file_num、align、encrypt 是否符合预期 python3 cpk_meta.py "$OUT_CPK" || exit 1这个脚本的要点是“前后都有检查”:构建前检查清单里的文件都在,构建后跑一遍体检脚本。很多人只写中间那一行调用,出包失败后连是输入问题还是参数问题都分不清。
注意:不同官方工具版本的命令行入口名和开关拼写会有出入,拿到新版本先不带参数跑一次,让工具自己打印帮助信息,别照抄旧脚本里的开关名。
3.3 校验产物的三个方法:官方查看、脚本对照、解包回读
一个包打完,我至少做三层校验才敢说它能用。第一层是官方 Viewer 打开看目录树和预览,这验证的是“包里目录结构是否正确”;第二层是 cpk_meta.py 对照头部字段,验证 file_num、align、encrypt 这些关键值和预期一致;第三层是拿社区解包工具把包解开,再和原始素材目录做数量和校验值比对:
# 数量比对:源目录和解包目录的文件数应该一致 src_count=$(find ./src_asset -type f | wc -l) out_count=$(find ./unpacked -type f | wc -l) echo "src=$src_count unpacked=$out_count" # 关键文件比对:抽几个代表性文件做 md5 对照 md5sum ./src_asset/sound/bgm_01.hca ./unpacked/sound/bgm_01.hca md5sum ./src_asset/ui/menu_btn.png ./unpacked/ui/menu_btn.png特别提醒:社区解包工具对加密包基本是废的,要么报错要么解出一堆乱码。所以拿社区工具做校验之前,先确认头部里的 encrypt 字段是 0;如果 encrypt 是 1,老老实实用官方 Viewer 验证,别浪费时间折腾第三方工具。
4. 把参数调明白:Align、压缩算法、文件名编码与加密怎么选
同一批素材,不同参数打出来的包在体积和加载表现上能差出好几个档次。这一章把 Maker 里最关键的几个参数逐个说透。
4.1 Align 对齐怎么定:从光盘扇区到移动端块
Align 决定包内每个文件起始偏移必须是 align 的整数倍。读取端按块读介质时,文件如果跨块,就要多读一次,所以对齐值通常要匹配目标介质的 IO 粒度。选多大,看你的包最终跑在哪:
| 目标平台 | 常用 align | 原因 |
|---|---|---|
| 光盘 | 2048 | 按扇区对齐,避免跨扇区读取 |
| 卡带类 | 512 | 匹配存储介质的读取块 |
| 移动端 | 16 到 128 | 兼顾块对齐与空间浪费 |
| PC/SSD | 16 或不做 | 随机读性能主要靠索引,对齐收益有限 |
对齐和空间浪费是直接挂钩的:对齐越大,文件尾部填充的无效字节越多,平均每个文件浪费半个 align。一个 5000 文件的包,align 从 128 改到 2048,可能凭空多出好几 MB 的填充。所以别盲目照抄官方默认值,先搞清楚读取端读文件的最小 IO 粒度。
4.2 压缩策略:为什么视频和音频不要开压缩
Maker 的压缩选项常见有 zlib、LZ4、LZMA 和“不压缩”。zlib 是默认,压缩率适中、解压速度也稳;LZ4 更快但压缩率低;LZMA 压缩率高但慢,适合一次性解压进内存的资源。这里最大的坑是对音频和视频流开压缩。
| 资源类型 | 建议策略 | 原因 |
|---|---|---|
| 脚本、JSON、文本配置 | zlib 或 LZMA | 文本压缩收益明显 |
| 贴图(dds/png 等) | zlib 或 LZ4 | dds 收益大,png 本身已压缩收益小 |
| 音频流(HCA/ADX 等) | 不压缩 | 本身就是压缩流,二次压缩几乎无收益 |
| 视频流(USM 等) | 不压缩 | 同上,还白白增加解压 CPU 开销 |
我见过一个包全开 LZMA,结果比不压缩还大 2%,就是被一批音频拖累的。这不是玄学,是已压缩流二次压缩后的必然结果。按文件类型区分策略,比一刀切开满要靠谱得多。
4.3 文件名编码与加密开关:两个最容易埋雷的选项
文件名编码只有两个选择,SJIS 或 UTF-8,但选错就是灾难。读取端的引擎或中间件库在加载时会按固定编码解析 TOC 里的文件名,打包端和读取端编码不一致,表现就是 TOC 里文件名变成乱码、按名字查找资源全部落空。老项目里 SJIS 很常见,新项目基本全线 UTF-8,动手前先确认读取端支持什么,别想当然。
加密开关则关系到整个调试流程。打开后包内文件会被内容加密处理,社区工具全部失效,出问题只能靠官方工具查。而且加密不是免费的,每次读资源多一层解密开销。我的习惯是开发期和测试期一律不开,只有到发布候选版本才评估要不要开。
注意:加密包对所有社区解包工具来说都是黑匣子,调试期尽量别开,否则你会发现自己连“确认包内容对不对”这件事都做不了。
5. 打包避坑:五个让游戏读不出资源的翻车现场与排查
下面五条是我见过的、也是最容易反复出现的坑,每条都按“现象 → 原因 → 解决”来写,方便你出了问题直接对号入座。
5.1 游戏直接读不出归档:编码与版本不匹配
现象:游戏启动后加载资源失败,日志里只有一句类似“无法打开归档”的错误,包明明存在,路径也对。
原因:一般有两个。一是文件名编码选错,读取端按 UTF-8 解析 SJIS 文件名,或者反过来,导致 TOC 里的文件名全是乱码、索引匹配失败;二是包版本太高,旧版读取端库只支持 v1/v2,而工具默认输出了 v3。
解决:先用官方 Viewer 打开包,看 TOC 里的文件名是否正常显示;然后读头部ver字段,对照读取端支持的版本范围。编码切换重打,版本则要么在 Maker 里降级输出,要么升级读取端库,二选一。
5.2 能解包却校验不过:加密开关和“打进了自己”
现象:社区解包工具能解开包,文件也都在,但引擎侧就是校验不过,或者运行时读出的资源和原始素材对不上。
原因:两个常见来源。一是包开了加密,社区工具恰好支持这种变体,解是解开了,但引擎侧带校验逻辑,内容和头部的加密标记对不上;二是输出目录被放在输入目录里,第二次打包时把上一轮生成的 CPK 和日志当素材打进去了,包里混进了自己。
解决:先看头部 encrypt 字段,确认包是否加密;再把输出目录挪到输入目录外面,重新生成文件清单,看 file_num 是否和预期一致。
5.3 Update 增量包被当成全量包发布,启动即缺资源
现象:包打出来了,本地测试没问题,发出去后玩家启动即缺文件,报错集中在某些基础资源上。
原因:Make 的更新模式生成的包只包含“新增和变更的文件”,读取端要先挂载母包才能正常工作。有人为了省时间,用 Update 模式出了包,却把它当成全量包单独发布。
解决:发布全量资源时用全量构建模式,别用更新模式。确定要做增量更新时,母包和新包必须一起发布,并且要让读取端先加载母包再加载增量包。
5.4 压缩开满包反而变大:对已压缩流做了二次压缩
现象:把压缩级别调到最高,重新打包后体积不但没降,个别包还变大了。
原因:包里有大量音频流、视频流这类已压缩数据,对它们再做 zlib 或 LZMA,压缩器无法进一步缩减,还会写入额外的压缩头和解压信息,体积反而膨胀。
解决:按文件类型区分压缩策略,音频、视频一律不压缩,只对脚本、配置、贴图这类原始数据开压缩。打包日志里通常有各文件的压缩前后大小,回看一下就知道哪些文件在倒贴空间。
5.5 命令行批量打包随机失败:控制台代码页与文件清单
现象:命令行批处理打包时偶尔失败,报错是“打不开某个输入文件”,但同一个文件在 GUI 里拖进去又能正常打包。
原因:控制台/终端的默认字符编码对中文或日文路径不友好,命令行直接传长路径或带特殊字符的路径时,路径被转码认错;另外清单文件的编码如果和打包配置里的文件名编码不一致,也会出现同样问题。
解决:别在命令行直接传路径列表,把相对路径写进清单文件,确认清单文件的编码和打包配置一致;路径统一用相对路径,避免盘符和特殊字符带来的转码差异。
6. 把打包嵌进构建流水线:清单驱动与体检门禁
6.1 用文件清单驱动构建,别再扫描目录
我后来带项目的做法是:打包输入不直接丢目录给 Maker,而是由版本管理产物生成一份清单文件,每行一个相对路径。这样临时文件、日志、旧包永远不会被扫进去,增量出包时的可读性也好很多。清单还能顺手做 diff,两次构建之间哪些文件变了,一目了然。
6.2 把体检脚本做成出包门禁
把 cpk_meta.py 接进持续集成流程,出包后自动跑,断言 file_num 等于预期值、align 是目标值、encrypt 为 0(除非发布期明确要求加密)。字段一旦被某个配置改动弄歪,门禁直接变红,而不是等测试同学在加载页卡住才发现。这一步成本半小时,省下的全是线上排查时间。
6.3 什么时候才需要考虑自研打包器
官方 Maker 能覆盖绝大多数场景:有 GUI、可命令行、支持增量,社区里能解 CPK 的工具也最多。只有当你要做读取端无法支持的私有加密、要把资源按运行时频率重新排布,或者要嵌入到跨平台构建链且不想依赖官方工具时,才值得自研。而且自研的第一条验收标准永远是把官方工具打的包解开,再谈自己写出能被读取端认的包。我现在每次交接打包任务,都先让接手的人跑一遍 cpk_meta.py,养成“出包先体检、再进游戏”的习惯。这个习惯救过我几次,希望帮到你。
本文还有配套的精品资源,点击获取