红楼梦十二金钗游戏资源分析(二十三)

相信很多朋友等这一天很久了,是的,我们这次一口气把 Redevent.paf 讲完。

目录

本系列已完结,以下是各章节说明,17 之前是 dos 版相关,之后是 2001 版:

  1. 背景、简单分析
  2. 显存位置
  3. 事件图保存算法: LZW
  4. 调色板
  5. MGP2
  6. 结局图
  7. 事件图中的眼睛
  8. 音频文件
  9. 按位读取
  10. 循环之前
  11. 读取循环
  12. 重现 LZW
  13. PAT 的图形格式
  14. STAFF 调色板
  15. 字体文件
  16. 脚本解密
  17. 版本比较
  18. 第一张图
  19. 调色板1
  20. 第二张图
  21. 调色板2
  22. 调色板处理
  23. 静态事件图、结局图
  24. 动态图、鉴赏模式

总体结构

如同大宇的 MKF,同门的 PAT,总体上 Redevent.paf 也是一个小文件集合,包含了什么, 我们稍后就讲。

MGFILE

文件头以 MGFILE 开头,记录了一些概要信息,比如 0ch-0fh 记录了「小文件」个数, 20h-23h 记录了文件表的偏移量,文件表中,每一项的长度是固定的四行(40h), 我们姑且称为文件索引项。 有这些信息,我们就可以把文件大卸八块:

let mut items = Vec::new();
let offset = u32_from_vec_u8(&self.content, 0x20);
let count = u32_from_vec_u8(&self.content, 0xc);
let item_len = 0x40;
for i in 0..count {
    let start = (offset + i * item_len) as usize;
    let end = start + item_len as usize;
    let item = IndexItem::from(&self.content, start);
    println!(
        "index: {:03}, type: {}, {}, head: {:08x}, body: {:08x}, len: {:08x}",
        i,
        item.type_str,
        item.body_head_string,
        item.head_offset,
        item.body_offset,
        item.body_len
    );
    items.push(item);
}

小文件类型

大卸八块之后,根据小文件的头部标识,大概有这么几种,我简单摘抄一下:

index: 000(000), head offset: 00000400, body head: 50415432, len: 240
index: 001(001), head offset: 00000840, body head: A1CB8C8C, len: 232588
index: 002(002), head offset: 00039880, body head: A1CB8C8C, len: 232588
index: 036(024), head offset: 007cc100, body head: A1CB5C8A, len: 232028
index: 123(07b), head offset: 01b2c2c0, body head: A1CB8C8C, len: 232588
index: 124(07c), head offset: 01b65300, body head: 80020000, len: 11316
index: 125(07d), head offset: 01b68340, body head: A1CB691C, len: 7273
index: 126(07e), head offset: 01b6a380, body head: A1CB691C, len: 7273
index: 339(153), head offset: 01d1c4c0, body head: A1CBD70A, len: 2775
index: 340(154), head offset: 01d1d100, body head: 63000000, len: 19780
index: 341(155), head offset: 01d22140, body head: ACF5BCD3, len: 336
index: 342(156), head offset: 01d22580, body head: 39298800, len: 0
index: 343(157), head offset: 01d345c0, body head: A1CB1EBB, len: 310046
index: 344(158), head offset: 01d80200, body head: A1CB70BB, len: 310128
index: 356(164), head offset: 02110500, body head: A1CB5CBB, len: 310108
index: 357(165), head offset: 0215c140, body head: 80020000, len: 1472
index: 358(166), head offset: 0215c980, body head: 39298800, len: 0
index: 359(167), head offset: 021705c0, body head: A1CB5CBB, len: 310108
index: 360(168), head offset: 021bc200, body head: A1CB7FBB, len: 310143
index: 361(169), head offset: 02207e40, body head: 39298800, len: 81120
index: 362(16a), head offset: 0221be80, body head: illegal, len: 3452816845

A1CB 开头的档都是图档,每一串图档后,都会跟一个数字开头的小文件, 这个文件用来说明前面图档的信息,比如对应哪个调色板,有些长度为 0 的, 我们就不看了,还有两个 167h168h 只有图像,没有调色板,我们也放一放, 剩下比较特殊的,还有第一个,第 155h 个,第 169h 个,我们一个一个来。

PAT2000

PAT2000 是第一个小文件,有一个明显的 PAT2000 的字样, 45ch-45fh 记录的是调色板的小文件序号, 顺带一提 169h 也是文件中的最后一个有效小文件。

我们之前提过,文件索引项,除了在末尾的文件表中有, 每个小文件的开头也会重复记一次,所以 PAT2000 的实际开始位置在 400h, 但是内容在 440h

分类

PAT2000 中 458h-45bh 记录的是另外一个有意思的小文件(155h), 为了展示它的意思,我甚至要换一个编辑器:

好了,我们有官方的命名了,这个其实也剧透了一些内容,文件包含了三种类型的资源, 事件图、眼睛图、结局图,现在叫眼睛不合适了,我们改叫动态图吧, 不过 DOS 是叫 EYE 的。

调色板

169h 的调色板,之前我们也分析过,简单总结就是,每套调色板 520 个 byte, 前 8 个 byte 似乎没什么用,后面 512 个 byte,每两个 byte 代表一个颜色, 我们之前分析过,是 RRRRRGGGGGOBBBBB 格式。

图档

图档以 A1CB 开头,接下来 4 个 byte 是长度, 后面的 280h168h 就是图档的长和宽(640x360),有多组, 从 89ch 开始就是第一张图的的数据了, 前 6 个 byte 代表一行像素点的起始位置和长度,看代码可能更清楚一些:

let start = image_item.body_offset + 0x1c;
let mut current = start;
loop {
    let y = u16_from_vec_u8(&self.content, current) as u32;
    let start_x = u16_from_vec_u8(&self.content, current + 2) as u32;
    let current_width = u16_from_vec_u8(&self.content, current + 4) as u32;
    /*
    println!(
        "image id: {}, x: {}, y: {}, width: {}",
        image_id, start_x, y, current_width
    );
    */
    for x_offset in 0..current_width {
        let pix_offset = current + 6 + x_offset as usize;
        let pix_val = self.content[pix_offset] as usize;
        let pixel = image_buffer.get_pixel_mut(start_x + x_offset, y);
        *pixel = pal.color_vec[pix_val];
    }
    current += current_width as usize + 6;
    if y >= height - 1 {
        break;
    }
}

图档信息说明

一串图档后,都会有一个小文件来记录前面图档的信息,这个文件没有明显的标识, 我们只能根据之前档案是否为图档来推测,记录的内容是图像的长宽、大小、 以及最关键的调色板对应关系:

let width = u32_from_vec_u8(&self.content, offset);
let height = u32_from_vec_u8(&self.content, offset + 4);
let len = u32_from_vec_u8(&self.content, offset + 0x1c);
let image_id = u32_from_vec_u8(&self.content, offset + 0x24) as usize;
let pal_id = u32_from_vec_u8(&self.content, offset + 0x28) as usize;
println!(
    "image width: {}, height: {}, len: {}, image: {}, pal: {}",
    width, height, len, image_id, pal_id
);

总结

有了以上这些信息,相信解出 2001 版的资源已经不难了, 但是似乎我们还没办法画出动图,因为虽然我们有了动态资源,但是, 其实我们没有眼睛的位置,和对应关系,文件里似乎也没有这些信息, 这些我们就下次再聊吧。

前面我们提到的,167h168h 只有图像,没有调色板, 其实这两张也已经算进结局图里了,这样结局图多了两张,是不是很激动, 其实没什么好激动的,因为就是这种:

说个题外话,这两个人物可能是画出来的,并不是真人出演, 这点我还真是看走眼了这么多年,后来看了原画作者的一些其他图,感觉非常相似, 不晓得有多少玩家和我一样眼拙。