Neople 是一家韩国的游戏公司,地下城勇士 DNF 就是出自他之手。

1. 解包

DNF 使用的资源包是 NPK 格式,即「NeoplePack」的缩写,于是推测这篇分析也许能用在 Neople 其他的几款游戏上,呵呵~~。

struct NPK_Header
{
    char flag[16]; // 文件标识 "NeoplePack_Bill"
    int count; // 包内文件的数目
};

struct NPK_Index
{
    DWORD offset; // 文件的包内偏移量
    DWORD size; // 文件的大小
    char name[256];// 文件名
};

NPK 文件就是由一个 NPK_Header 和 N 个 NPK_Index 以及实际的数据组成的。

注:NPK分加密(ImagePacks4 文件夹下)和未加密(ImagePacks2 文件夹下)的两个版本,加密的版本需要用

puchikon@neople dungeon and fighter DNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNFDNF

(共 256 字节,最后一字节是 '/0')异或文件名部分。

2. 格式转换

解包之后得到的文件基本都是「.img」格式,但根据文件标识的不同,可分为「Neople Image File」和「Neople Img File」。

「Neople Image File」的格式

struct NImageF_Header
{
    char flag[24]; // 文件标识 "Neople Image File"
    int unknown1; // 未知数据,也许是版本号什么的?
    int index_count; // 索引项的数目,也可以理解为包含的小图片的个数。
};

struct NImageF_Index
{
    DWORD dwType; // 目前已知的类型有 0x0E(1555格式) 0x0F(4444格式) 0x10(8888格式) 0x11(不包含任何数据,可能是指内容同上一帧)
    DWORD dwCompress; // 目前已知的类型有 0x06(zlib压缩) 0x05(未压缩)
    int width; // 宽度
    int height; // 高度
    int size; // 压缩时size为压缩后大小,未压缩时size为转换成8888格式时占用的内存大小
    int key_x; // X关键点,当前图片在整图中的X坐标
    int key_y; // Y关键点,当前图片在整图中的Y坐标
    int max_width; // 整图的宽度
    int max_height; // 整图的高度,有此数据是为了对其精灵
    BYTE Data[实际长度]; // 紧跟在索引后面的就是实际的数据(将数据放在索引结构中只是为了向大家展示索引与数据的关系。)
};

「Neople Image File」由一个 NImageF_Header 和 N 个 NImageF_Index 结构组成。

注:对于类型 0x11 其索引表仅包含前 2 项,即:

struct NImageF_Index_0x11
{
    DWORD dwType; // 类型0x11
    DWORD dwCompress; // 可取很多种数值,作用未知(其中零最为常见)
};

「Neople Img File」的格式:

struct NImgF_Header
{
    char flag[16]; // 文件标识 "Neople Img File"
    int index_size; // 以字节为单位,索引表的大小
    int unknown1; // 未知1
    int unknown2; // 未知2
    int index_count;// 索引表项数目
};

struct NImgF_Index
{
    DWORD dwType; // 与"NImageF_Index"是一致的,要看注释往上翻。
    DWORD dwCompress;
    int width;
    int height;
    int size;
    int key_x;
    int key_y;
    int max_width;
    int max_height;
};

「Neople Img File」是由一个 NImgF_Header 和 N 个 NImgF_Index 索引表 + 连续存储的实际数据组成。换句话说就是,「Neople Image File」的索引与自己的数据挨在一起,而「Neople Img File」的索引表和数据是分开存放的。

3. 总结

从几个未知数据来看,还不能算是完美的格式分析,实在是遗憾。(现在的网游越加壳越厉害,看来我得再多研究研究脱壳啦。)

不过对于资源提取还是不成问题的。按住惯例上传一张提取图片。

提取工具的代码放到 GitHub 上了:点击访问

虽然上文已经有比较详尽的分析了,但是真正实现好一个资源提取工具还是花了我两天的时间。这里把需要注意的地方记录下来。

3.1 npk 包的格式

struct NPK_Header
{
    char flag[16]; // 文件标识 "NeoplePack_Bill"
    int count; // 包内文件的数目
};

struct NPK_Index
{
    unsigned int offset; // 文件的包内偏移量
    unsigned int size; // 文件的大小
    char name[256];// 文件名
};

char decord_flag[256] = "puchikon@neople dungeon and fighter DNF";

解 npk 包非常好处理,读取完 NPK_Header 紧接着根据里面的 count 数目循环读取 NPK_Index,读取完毕后,就可以根据里面的 offset 定位到指定位置读取 img 文件。现在的 dnf 包 npk 包内的文件名是加密过的,要用 decord_flag 异或 NPK_Index 中的 name 才能获取实际文件名。decord_flag 总共有 256 个字节,剩余部分用「DNF」三个字母填满,最后一个字节置 0。读取文件名时可以像这样解密:

char temp[256] = {0};
    fread(temp, 256, 1, fp);
    for (int i = 0; i < 256; ++i) {
    index.name[i] = temp[i] ^ decord_flag[i];
}

3.2 img 文件格式

struct NImgF_Header
{
    char flag[16]; // 文件标石"Neople Img File"
    int index_size; // 索引表大小,以字节为单位
    int unknown1;
    int unknown2;
    int index_count;// 索引表数目
};

struct NImgF_Index
{
    unsigned int dwType; //目前已知的类型有 0x0E(1555格式) 0x0F(4444格式) 0x10(8888格式) 0x11(不包含任何数据,可能是指内容同上一帧)
    unsigned int dwCompress; // 目前已知的类型有 0x06(zlib压缩) 0x05(未压缩)
    int width; // 宽度
    int height; // 高度
    int size; // 压缩时size为压缩后大小,未压缩时size为转换成8888格式时占用的内存大小
    int key_x; // X关键点,当前图片在整图中的X坐标
    int key_y; // Y关键点,当前图片在整图中的Y坐标
    int max_width; // 整图的宽度
    int max_height; // 整图的高度,有此数据是为了对齐精灵
};

img 文件也是一系列图片的合集,它里面还包含很多有用的信息,比如图片的坐标(用于对齐),这个数据是我们想正常使用这个图片所必须的。也正是由于现有的工具都没有提供方便的批量导出和该数据的处理功能,我才想自己写个提取工具的。

img 文件是由一个 header + 多个连续的索引表 + 实际图片数据组成的。读取图片数据需要跳过 header(固定大小)和索引表(header.index_size标识)。

我们读取的文件大小是由 NImgF_Index.size 决定的,如果 dwCompress 为 6 则表示图片有经过 zlib 压缩,这时 size 表示压缩后大小。如果为 5 表示没有压缩,这时 size 表示转换成 8888 格式所占内存大小(也就是说,如果 dwType 为 0x0e 或是 0x0f,size 要除 2)

如果有压缩,需要 zlib 解压:

int ret = uncompress(temp_zlib_data, &zlib_len, temp_file_data, size);

注意,temp_zlib_data 是一个足够大的缓存区,zlib_len 传入的是缓存区的大小。

读取完的数据是图片像素数据,接下来要写入到 png 图片中(看个人需要 bmp 什么的也可以)

libpng 的使用(包含颜色格式之间的转换代码):

FILE *fp = fopen(file_name, "wb");
png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

info_ptr = png_create_info_struct(png_ptr);
if (setjmp(png_jmpbuf(png_ptr)))
{
    printf("[write_png_file] Error during init_io");
    return;
}
png_init_io(png_ptr, fp);

/* write header */
if (setjmp(png_jmpbuf(png_ptr)))
{
    printf("[write_png_file] Error during writing header");
    return;
}

png_set_IHDR(png_ptr, info_ptr, width, height,
8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

png_write_info(png_ptr, info_ptr);

/* write bytes */
if (setjmp(png_jmpbuf(png_ptr)))
{
    printf("[write_png_file] Error during writing bytes");
    return;
}

row_pointers = (png_bytep*)malloc(height*sizeof(png_bytep));
for(int i = 0; i < height; i++)
{
    row_pointers[i] = (png_bytep)malloc(sizeof(unsigned char)* 4 * width);
    for(int j = 0; j < width; ++j) { // png is rgba switch (type) { case ARGB_1555://1555 row_pointers[i][j * 4 + 0] = ((data[i * width * 2 + j * 2 + 1] & 127) >> 2) << 3; // red
        row_pointers[i][j * 4 + 1] = (((data[i * width * 2 + j * 2 + 1] & 0x0003) << 3) | ((data[i * width * 2 + j * 2] >> 5) & 0x0007)) << 3; // green
        row_pointers[i][j * 4 + 2] = (data[i * width * 2 + j * 2] & 0x003f) << 3; // blue row_pointers[i][j * 4 + 3] = (data[i * width * 2 + j * 2 + 1] >> 7) == 0 ? 0 : 255; // alpha
        break;
    case ARGB_4444://4444
        row_pointers[i][j * 4 + 0] = (data[i * width * 2 + j * 2 + 1] & 0x0f) << 4; // red row_pointers[i][j * 4 + 1] = ((data[i * width * 2 + j * 2 + 0] & 0xf0) >> 4) << 4; // green
        row_pointers[i][j * 4 + 2] = (data[i * width * 2 + j * 2 + 0] & 0x0f) << 4;; // blue row_pointers[i][j * 4 + 3] = ((data[i * width * 2 + j * 2 + 1] & 0xf0) >> 4) << 4; // alpha
        break;
    case ARGB_8888://8888
        row_pointers[i][j * 4 + 0] = data[i * width * 4 + j * 4 + 2]; // red
        row_pointers[i][j * 4 + 1] = data[i * width * 4 + j * 4 + 1]; // green
        row_pointers[i][j * 4 + 2] = data[i * width * 4 + j * 4 + 0]; // blue
        row_pointers[i][j * 4 + 3] = data[i * width * 4 + j * 4 + 3]; // alpha
        break;
    case ARGB_NONE:// 占位,无图片资源
        break;
    default:
        printf("error known type:%d\n", type);
        break;
        }
    }
}
png_write_image(png_ptr, row_pointers);

/* end write */
if (setjmp(png_jmpbuf(png_ptr))) {
    printf("[write_png_file] Error during end of write");
    return;
}
png_write_end(png_ptr, NULL);
// 别忘记释放内存
png_destroy_write_struct(&png_ptr, &info_ptr);

/* cleanup heap allocation */
for (int j=0; j < height; j++)
free(row_pointers[j]);
free(row_pointers);

fclose(fp);

新版的是像素数据去重放上面然后根据索引归位而已。