汉化 - 《灵魂触发者》的图片导出导入

时逢元旦假日, 灵魂触发者的汉化版本也发布了。 http://hi.baidu.com/pspbahhteam/item/eb1a6ef2cba6c0e41a111f6a 10月4日发售的《灵魂触发者(SOL)》在10月10日宣布开始汉化,到发布汉化差不多有3个月了 (测试了差不多有1个月),本人也有幸担任破解一职。 说是破解,其实本人这次主要负责的是图片部分。 我们这里也主要讲讲图片导出的部分。 由于在 SOL 中文件都是打包在 .TPK 文件里,这里的 .TPK 其实是压缩过的,除去 12 字节的文件头用 zlib 便可以轻松解压,解压出来的数据也可以很容易解包出子文件,其中有我们关注的重点—— txb 文件(关于 TPK 的解包这里不多讨论)。 随意打开几个txb文件: MAIN_TOP.TXB SOLTANK.TXB txb 观察我们可以发现,txb 文件其实也是一种打包方式, 里面包含了 ptm 文件。 根据经验,我先统计了 txb 文件内 ptm 文件的总数,然后去对照 txb 的文件头,发现最前面的 4 个字节就是文件总数。 然后后面发现一堆数据,但是我们暂时不管,一般来说这些数据可能和文件大小文件偏移之类的有关,但是这里我们观察可以发现 txb 里面的数据是没有压缩的,最外的 tpk 打包都压缩了一次一般也不会再压一次。 直接跳到第一个文件开始处,我们就用 MAIN_TOP.TXB 举例吧,第一个文件开始处是 0xf0。 这里首先记录的是文件名,而且通过观察可以发现文件名区段占用的都是 0x40 字节,这可以定义一个常量了。 接着我们观察每个txb文件发现几乎每个文件除了最开始4个字节后面的8字节都是相同的,所以我猜想这相同的8字节是某种常量,文件头也就出来了: 4 字节 8 字节 文件总数 未知常量 然后比较最开始到第一个文件处我们大致可以看出:除去文件头 (12 字节),到第一个文件记录处,一共 0xe4 个字节,我们用它除以 文件总数 0x39 ,结果刚刚好是 4 。 再多观察几个文件我们可以发现,文件开始总是在 0x10 的倍数的偏移上,所以这个txb文件从最开始到第一个文件的长度总是 0x10 的倍数,不足的地方用 0 补齐即可。 继续观察 ptm 文件的数据,很简单 0x40 字节大小的文件名区段和后面的数据区段。 这里我们看看第一个文件: btl_gage_00.ptm 文件数据区域是 0x130 - 0x27f 大小刚刚好是 0x150 (0x27f + 1 - 0x130),正好和文件头后面的对应的 50 01 00 00 4 字节代表的整数数据相符合,继续观察几个不同大小的ptm文件,发现也符合这个规律,所以我们可以得出结论,文件头后面的数据 所以我们得到txb文件计算首文件地址和得到每个文件大小的方法 ( txb_header->filesize[i] ) 的方法: [code lang="cpp"] //结构定义 typedef struct _TXB_HEADER { unsigned long filecount; unsigned long unknow1; unsigned long unknow2; unsigned long filesize[1]; } TXB_HEADER; [/code] [code lang="cpp"] unsigned long count = ((TXB_HEADER *)data)->filecount; unsigned long offset = 12 + count * 4; if (offset % 0x10) offset = ((unsigned long)(offset / 0x10) + 1) * 0x10; [/code] 这样我们就可以解出来每个ptm文件了,导入ptm的过程只是简单反过来就好了,不多说了。 ptm 在这里ptm文件就是 SOL 主要所用的图片文件,我们就以btl_gage_00.ptm文件为例,给大家说说ptm文件的一些基本的结构。 一般来说,图片文件都是有文件信息头的,这个文件信息头包含了这个图片的一些基础信息,包括宽高等等。 通过观察几个ptm,我们通过对数据的经验可以很容易的发现文件头是 0x10 个字节,而后面的则是图片的数据。 这里我单独截出来供观察: 确实,明显文件头占了0x10个字节,这里数据分水岭很明显,还有一处很明显的地方在于 0x110 处,我们可以发现后面的数据明显和前面的不是相同的,由于这里的数据很像是颜色,而且占0x40个字节,很可能就是16色图片的调色板 (一般来说图片数据保护RGBA,每个一个字节,每色就占4字节了) 而且,再观察头,我们可以注意到最后4字节就是 10 01 00 00 (0x0110) 正好是到后面数据的偏移,也印证了我的对于后面可能是调色板的猜想,不过具体是不是还是得实践证明,这里只是猜的八九不离十了。 现在,我们对于ptm文件的不明白的地方主要还是在于文件头部分,除了最开始的 4 字节 PTMD 文件标识和最后 4 字节的调色板偏移外,我们还有 8 字节的数据不了解。可别小看这 8 字节,也正是这 8 字节决定了这个图片的最基本属性。 由于我们已经知道 0x110 后面的部分是调色板了,除去文件头 0x10 字节,剩下就是 0x100 的数据是真正的图片数据,我们可以先把这些数据单独提出来,用 CrystalTile2 (CT2) 等工具查看一下,由于还没有上调色板,所以只能选 4bpp 简单观察一下图片,但是这张图太小,我们不足以得到足够多的有效信息,故我们转看下面的 main_bg_00.ptm 文件: 直接跳到调色板的偏移 0x20010 处,我们这次发现这个图片的调色板会更加丰富,有256色,估计这个调色板相关的属性也是在文件头中保存着。 对应256色也就说明这这个图片的图片数据在没有上调色板前可以说成是 8bpp 的,用CT2这类工具简单查看一下,我们可以发现: 这个图片是以block的模式渲染的,大小为 512 x 256,每个block为 16 x 8,这很类似 gim 图片格式的快速模式 (理论上确实能更快显示出图片(雾)),也是每一段数据为一个block,而不是像png、bmp等图片格式都是按照一定的行顺序排下去的。 知道这些信息后我们就可以用一定的算法将这个 8bpp 的图片转换成常用的图片格式了: 这个算法就是简单的对应调色板对应的颜色就好了,值得注意的是还需要对照游戏对应一下颜色通道,比如这个调色板的颜色就是和png储存的颜色的B、R通道互换了,颜色中的代表B、R的2个字节简单调换一下即可。 由于是block渲染,我们可以知道这些同样大小的图片的宽度和高度,block宽度、高度很可能都是只和一个小小的参数有关,而没有单独指定的。 通过对多几个ptm文件的处理观察,我们可以发现:
  • 16色图片block的宽度都是32
  • 256色图片block的宽度都是16
在文件头的第9个字节为图片(调色板)的类型,如果为 0x04 则是16色调色板,如果为 0x05 则是256色调色板。 然后是在文件头的第8个字节中的高4位为图片的宽度属性,这里要列出来比较多,就不一一列举了。 比如 main_bg_00.ptm 这个文件,我们可以发现在其文件头第8个字节中的高4位数值为9,由于在这种渲染模式下宽度可能都是确定的,我们不妨就当所有这样的情况的都和这个文件一样为512宽度,而高度的计算仅仅就是占用的图片像素点数 (8bpp中1个字节为1点,4bpp中1个字节为2点) 除去宽度罢了。 同时我们还可以发现block高度都为8。 所以可以得到这样的一个ptm文件头结构和一些常量: [code lang="cpp"] typedef struct _PTM_HEADER { char magic[4]; unsigned char unknow1[3]; unsigned char width_flag; //need & 0xf0 unsigned char palette_flag; unsigned char unknow2[3]; unsigned long palette_offset; unsigned char image_data[1]; } PTM_HEADER; #define BLOCK_HEIGHT 8 #define PALETTE_FLAG_256_COLOR 0x05 #define PALETTE_FLAG_16_COLOR 0x04 #define WIDTH_FLAG_512 0x90 #define WIDTH_FLAG_256 0x80 ... [/code] 并且写出转换数据的代码 (这里由于要导出32位png,所以把调色板的数据也放进去了)。 其中值得注意的是BLOCK_WIDTH和BLOCK_SIZE并不是一个常量,由于一开始以为是个常量所以这里写的是大写,后来发现并不是如此,而是与调色板大小有关(这个宽度在前文中提到过),linecount是每行block的数量。 [code lang="cpp"] for (int ib = 0; ib < block_count; ib++) { if (ptm->palette_flag == PALETTE_FLAG_256_COLOR) { memcpy(block, ptm->image_data + ib * BLOCK_SIZE, BLOCK_SIZE); } else if (ptm->palette_flag == PALETTE_FLAG_16_COLOR) { for (int i = 0; i < BLOCK_SIZE; i+=2) { unsigned char colordata = *(unsigned char *)(ptm->image_data + (ib * BLOCK_SIZE / 2) + (i / 2)); block[i] = colordata & 0x0f; block[i+1] = ((colordata & 0xf0) >> 4); } } for (int i = 0; i < BLOCK_SIZE; i++) { int x = (ib % linecount) * BLOCK_WIDTH + (i % BLOCK_WIDTH); int y = ((unsigned long)(ib / linecount)) * BLOCK_HEIGHT + (((unsigned long)(i / BLOCK_WIDTH))); int offset = y * BLOCK_WIDTH * linecount + x; raw_image_data[offset] = palette[block[i]]; } } [/code] 只要我们通过对更多样例ptm文件进行分析就可以写出基本通用的 SOL 的ptm文件转换代码了。这里就不把代码全部贴出来了,没太多意义。 另外,如果想要导入回去,我相信已经不是什么难事了,需要注意的是需要提前用其他工具 (如 colga) 转换好修改好的图片的颜色总数,这样才能做出新的调色板供其使用(为了尽可能的不修改图片的文件头和大小,这样可以保证不会出什么大问题)