文档原创
噬神者(GOD EATER)PC版本解包记录
详细记录噬神者PC版本游戏文件的解包方法与数据结构
# 噬神者PC版本文件结构1.7
**CODE EATER 汉化组**
https://www.haojun0823.xyz/
email@haojun0823.xyz
汉化群:461217810
https://github.com/HaoJun0823/GECV 参见GECV_II
**版本1.7,日期2024/11/23**
---
## PRES 结构
`Pres`固定开头:
- `magic_header`:`0x73657250` (`PRES`)
- `magic_1`, `magic_2`, `magic_3`:意义未知,从解包时获取,似乎永远固定(每个`int32`大小,共3个)
- `pres_config_length`:所有PRES信息的文件长度(包括下文),类型为`Int32`
- `zerozero`:`Int64`长度的0(因为游戏是32位寻址,几乎不重要)
- `country_count`:国家数量,只有1和6两种。若为1,则文件没有国家语言内容;若为6,则按 (EN, FR, IT, DE, ES, RU) 排列
`Pres`固定开头大小:4+4+4+4+4+8+4 = 32字节
---
### Pres国家(country)结构
**条件:** 如果`country_count != 1`时才存在;若`country_count == 1`,则直接从dataset开始(即country不存在)
- `dataset_offset`:集合(dataset)信息的地址指针
- `dataset_length`:整个集合的大小
`Pres`国家结构大小:0 或 (4+4)*6 = 48字节(若country不存在,则数量和指针均为0,并立刻开始读取dataset)
---
### Pres集合(dataset)结构
总是有8个,分别是:`res`, `prx`, `asset`, `unk`, `conf`, `tbl`, `text`, `rtbl`
- `data_offset`:第一个dataset的数据指针
- `data_count`:data的数量
`Pres`集合结构大小:(4+4)*8 = 64字节(若dataset不存在,则数量和指针均为0)
---
### Pres集合的数据区(data)
每个data大小为 4+4+4+4+4+4+4+4 = 32字节
- `offset`:`Int32`。偏移地址,转换为十六进制后,第一位为`B`或`F`,后七位是Pres文件内的指针(从文件开始算起)。
算法:`offset & ((1 << (32 - 4)) - 1)`
- `offset_type`(非游戏内数据):若为`B`,文件是虚拟引用,目标指针为虚拟引用的文件名,游戏运行时读取;若为`F`,根据集合情况储存文件到指定区块。假设后七位为`real_offset`,第一位为`offset_type`。
- `real_offset`(非游戏内数据):`offset`后七位的`Int32`。
- `csize`:`Int32`。data存在于pres中的大小。若文件为`B`,此处为0。
(若`dataset == tbl`,则此大小需除以4(`csize/4`),解包时需乘以4(`csize*4`))
- `conf_offset`:`Int32`。`data_conf`(数据信息)的指针位置
- `conf_count`:`Int32`。`data_conf`的数量
- `unk1`:`Int32`。未知(1,2,3),只会被游戏载入
- `unk2`:`Int32`
- `unk3`:`Int32`
- `usize`:若文件是blz4压缩,则为解压后的大小;否则与`csize`相同(用于BLZ2)
- `file_data`(非游戏内数据):若`real_offset`为`F`,从`real_offset`取`csize`大小的文件(若`dataset == tbl`,则`csize`需乘以6[存疑,目前*4即可])
- `B_file_name`(非游戏内数据):若`real_offset`为`B`,从`real_offset`读取UTF8字符串直到`00`结束
---
### Pres集合信息的数据(data_conf)
#### 特殊情况(第6、7集合,`dataset == tbl`或`dataset == text`,且data的offset为`F`(真实引用))
- 文件不可被重复引用,每个国家的语言是单独的。
- `file_data`:需用0对齐到16的整数倍,若已是16的整数倍,则补16个0。地址为`real_offset`,`offset = 'F' + real_offset`。
#### 文件信息数据块(data_conf)
- `conf_offset`指向此处的数据。
- 根据`conf_count`数量写入指针`conf_offset_{index}`:指向data的起始指针。
- 根据`conf_count`数量写入数据`conf_data_{index}`:一段UTF8编码的字符串,末尾以十六进制的`00`结束。
- 需要用0对齐到16的整数倍,若已是16的整数倍,则补16个0。
#### 若`offset_type == 'B'`
- 文件不可被重复引用,每个国家的语言是单独的。
- `B_file_name`:需用0对齐到16的整数倍,若已是16的整数倍,则补16个0。地址为`real_offset`,`offset = 'B' + real_offset`。
---
每个国家的dataset结束后,`pres_config_length` = 当前位置的指针,应为16的整数倍。
---
### 末尾文件区块
- `file_data`:若为`F`,且`dataset`不是`tbl`或`text`,文件放在此处,可被重复引用。需用0对齐到16的整数倍,若已是16的整数倍,则补16个0。被引用文件的`real_offset`为此处地址值,`offset = 'F' + real_offset`。
整个文件应为16的整数倍大小。
---
## BLZ4
### 头部
- `magic`:`0x347a6c62` (`BLZ4`)
- `unpack_size`:`int32`,解压后的大小
- `zerozero`:`int64`,8个0(推测`unpack_size`应为`int64`,但游戏不需要这么大)
- `md5`:16字节,解压后文件的MD5
### 区块
- `chunk_size`:若为0,则文件未压缩,从此处直接取到最后;否则取`chunk_size`大小的`chunk_data`。若最后为0或无数据,则结束。在PRES中所有文件都是16的整数倍。
- `chunk_data`:`UINT16`类型,根据上文取到的数据长度为zlib压缩过的数据。
### 解包过程
假设有8个区块`01234567`,按照`12345670`的顺序分别zlib解压然后提取。
### 打包过程
根据一定大小切割原始文件(建议32768,若为65535即UINT16上限,可能因压缩后变大而溢出)。假设切割出8个区块,前7个大小32768,最后一个10000,则按`01234567`顺序压缩,最后写入`12345670`。
---
## BLZ2
### 头部
- `magic`:`0x327a6c62` (`BLZ2`)
### 区块
- `chunk_size`:最大`0xff`(65535,`UINT16`)
- `chunk_data`:存储的文件数据区块,通过`Zlib.Deflate`压缩(与BLZ4不同)
### 解包过程
同BLZ4,第一区块位置仍然颠倒。
若文件大小 ≤ `0xff`,则不会被BLZ2压缩,此类文件永远只有一个区块,且`chunk_size == 文件大小`。
### zlib配置
参考 `(9, zlib.DEFLATED, -15)` (Python)
---
## BNSF / IS14
万代魔改的G722.1C。
- 解码参考:https://github.com/kode54/vgmstream/blob/master/src/meta/bnsf.c
- 编码参考:https://exvsfbce.home.blog/2020/02/04/guide-to-encoding-bnsf-is14-audio-files-converting-wav-back-to-bnsf-is14/
### 结构
- `magic`:`INT32`,固定为`BNSF`
- `file_size`:总文件大小 - 0x08
- `info`:`byte[8]`,8个字符,表示文件版本
- `conf_1`:`Int32`(未知,可能是比特率或宽度)
- `conf_2`:`Int32`,声道数(1=单声道,2=双声道)
- `conf_3`:`Int32`(未知)
- `conf_4`:`Int32`,WAV样本大小(sample)
- `conf_5`:`Int32`(未知)
- `conf_6`:`Int32`(未知)
- `conf_7`:`Int32`(未知)
- `conf_8`:`Int32`,后面所有数据的大小(总文件大小 - 0x30)
- `file_data`:被编码的数据
---
## GNF
没有明显的头特征。
- `count`:内部的DDS数量,`int32`
- `file_size_{count}`:每个`int32`,表示每个DDS文件的大小
- `dds_file`:根据上述大小依次获取
---
## QPCK
- `magic`:`0x37402858`,固定`INT32`
- `count`:`int32`,文件数量
根据文件数量依次取得:
- `offset`:`INT64`,QPCK内的偏移
- `hash`:`INT64`,数据的hash(转换为16进制字符串,游戏调用依据)
- `size`:`INT32`,文件大小
根据上述`offset`和`size`依次获取数据,命名为 `{文件顺序}_{hash的16进制16长度字符串}`。打包QPCK时需按顺序打包回去,不要修改hash。
> **注1**:可在`offset`处读取`int32`来判断文件类型。
> **注2**:QPCK实际文件结构由内部某文件决定(如开头`80 02 63 72 6C 73 66 69 6C 65 0A 52 6C 73 44 61 74 61`),目前无法解开该结构。
---
## TR2
### 头部
- `header`:`int32`,固定为`0x3272742E`
- `header_magic`:`int32`,基本固定,例如`00 00 DF 07`表示2015(新版本/PC),`00 00 DA 07`表示2010(旧版本/SONY_A),旧版本还可能有2000、1999。
- `table_name`:`byte[48]`,固定48字节,从头读到0结束。
- `table_column_inf_offset`:表信息开始的地方,绝大多数为`0x40`
- `table_column_int_count`:头信息的个数
### 表开始(0x40)
假设对象为 `row_inf`:
- `id`:`int32`,表的ID
- `offset`:`int32`,从文件开始算起,表的数据偏移
- `magic`:`int32`,表的魔术码
- `csize`:`int32`,表的大小
- `usize`:`int32`,与BLZ4中类似,但此处无压缩,永远等于`csize`
- `row_data`:定义对象,从`offset`取`csize`大小的`byte[]`
### 表配置
`0x40 + table_column_int_count * 5 * 20`(表开始结束)
如果是老版本TR2,没有此部分,需要通过每个分表重建。
若TR2没有此部分,说明该文件未被游戏调用。假设为 `row_conf`:
- `header_count`:`int32`,表ID的数据数量
- `header_id`:`int32[headercount]`,数组,表的列名称
以上内容需对齐16的整数倍,不足补0,若正好是16倍则不变。
### `row_data`(从文件0x00开始算起始指针,不能从TR2文件内部算)
- `row_name`:`byte[48]`,固定48字节,从头读到0结束
- `row_serial`:`byte[16]`,怀疑是对象类型序列化特征。前8字节为数据格式,后8字节为Bin版本。
前8字节老版本参考:`6A 61 6A 70 00 00 00 00` = "jajp"
后8字节参考:`79 6F 62 69 38` = "yobi8"(老:`00 CF 07`,新:`02 DF 07`)
- `row_type`:`byte[48]`,固定48字节,读到0结束。可能值为:`INT8`, `UINT8`, `INT16`, `UINT16`, `INT32`, `UINT32`, `FLOAT32`,以及 `ASCII`, `UTF-8`, `UTF-16LE`,可能还有 `UTF-16`(大端?)
以上为固定长度内容,然后从`0x70`开始:
- `0x70-0x73`:不要动
- `0x74`:未知
- `0x75`:对齐方式(若为2则最后补2的倍数,4则补4的倍数)
- `0x76`:一个byte,表示数据的数组大小(**2024-11-23更新**:`0x76`和`0x77`共同组成`INT16`数组大小)
- `0x77`:未知
- `0x78-0x7B`:不要动
- `row_data_count`:`0x7C-0x7F`,数据的数量
根据`row_data_count`循环读取:
- `row_data[row_data_count].offset`:`int32`,地址连续。若超出数据最大长度,则为无效指针,应标记为数据结束位置。指针可能重复同一地址(游戏编译时合并相同数据以优化)。
#### 老版本TR2存储(old_tr2_offset_area)
- `row_data[row_data_count].id`:`int32`(老版本没有开头的`table_column_int_count`,因此此处存储4字节ID,可能重复但不乱序)
- `row_data[row_data_count].offset`:`int32`,同上
- `row_data[row_data_count].length`:`int32`,数据长度。一些数据不通过`0x76`判断大小,而是通过此值除以数据类型长度(如`int16`为2,则表示有1个`int16`)。建表时需标记有效/无效数据。
**注意**:多数组文本存储方式不同(见下文)。
- `lastmark`:`int32`,结束的数据记号,即所有`row_data_data`结束时的指针
- `lastmark_zero4`:4个0,用于分割后续数据
从此处开始对齐16的整数倍,不足补0,若正好16倍则不变。
### `row_data_data`
跳转到每个`row_data[row_data_count].offset`的位置开始读取:
- 根据`row_type`决定读取长度:数字类型分别为1,1,2,2,4,4,4;文字类型(ASCII/UTF-8)读到`0x00`结束,UTF-16LE读到`0x0000`结束。注意UTF-16LE有特殊编码,不能在普通编辑器中修改。
- 根据`0x76`决定读取数量。
例如:`0x76` = 9,`row_type` = `"UINT8"`,`row_data_count` = 3,则有3个数据对象,每个对象数组长度为9,每次读1字节。
- 若数据超过指针范围,则剩余数据不存在,做记号。
### 表格展示方式
- 列:
- `row_inf.id`
- `row_inf.row_data.row_name`
- `row_inf.row_data.row_type`
- `row_inf.row_data.0x77`(作为索引)
- `key(column)`:根据`row_data_count`顺序提取`row_conf`,即 `row_conf[row_data_count].header_id`
- `value(row)`:`row_inf[id].row_data[row_data_count].row_data_data[0x76]`
> 注:参见GECV TR2 GUI EDITOR展示方法。
### 2024/05/04新发现
若`0x76`为多数组且为文本格式(ASCII/UTF8/UTF16),可能出现以下情况(示例`0x76` = 0x14(十进制20),UTF8):
FF FF 2C 00 42 00 6A 00 7D 00 8D 00 A9 00 BC 00 C9 00 D2 00 DB 00 E4 00 ED 00 F6 00 FF 00 08 01 11 01 1A 01 23 01 2C 01 35 01
FF FF E5 A4 9A E7 81 BD E5 A4 9A E9 9B A3 E7 9A 84 E6 97 A5 E5 AD 90 00 E4 BA 9E E8 8E 89 E8 8E 8E EF BC 8C E5 9B A0 E9 81 8E
E5 BA A6 E5 8B 9E E7 B4 AF E8 80 8C E5 80 92 E4 B8 8B E3 80 82 00 ……
- 第一个`FF FF`:双字节指针组
- 第二个`FF FF`:文本组
将第一个`FF FF`的地址视为0x00,则指针`2C 00`跳转到`E5 A4`,取UTF8直到`00`结束,正确文本为:
`E5 A4 9A E7 81 BD E5 A4 9A E9 9B A3 E7 9A 84 E6 97 A5 E5 AD 90 00`
第二个指针`42 00`跳转到正确文本:
`E4 BA 9E E8 8E 89 E8 8E 8E EF BC 8C E5 9B A0 E9 81 8E E5 BA A6 E5 8B 9E E7 B4 AF E8 80 8C E5 80 92 E4 B8 8B E3 80 82 00`
### 2024/05/05 老版本TR2多数组文本存储
老版本将下一级数组的存储放在`old_tr2_offset_area`层。假设`0x76` = 4,`row_data_count` = 12,则有12/4=3组数据,ID从00到02。
存储情况如下:
00 文本指针 文本长度 00 文本指针 文本长度 00 文本指针 文本长度
01 文本指针 文本长度 01 文本指针 文本长度 01 文本指针 文本长度
02 文本指针 文本长度 02 文本指针 文本长度 02 文本指针 文本长度
文本区域
文本长度不包含UTF8/ASCII的`00`和UTF16的`00 00`,但文本需要以`00`结尾。
### 如何判断TR2版本
- PC版本称为VER.2,SONY_A版本称为VER.1
- PC版本有`header_count`。若读取`header_count`为0,或`header_count`超过头的大小,则为VER.1,否则为VER.2
- 头的总长度由第一个片段的`offset`决定(即头数据的结尾)。
---
`row_data`从此处开始对齐16的整数倍,不足补0,若正好16倍则不变。