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

画完灰姑娘,我们终于要开始聊一聊 2001 版的调色板了,年末工作比较多,而且我不得不说, 2001 的调色板要比原版更加复杂。好在我还没有会一直敲碗催更的读者,我还可以慢慢研究。

目录

本系列已完结,以下是各章节说明,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. 动态图、鉴赏模式

前情提要

之前我们从 ShowEvent 那里取出了事件图的像素,由于没有识别出调色板, 所以我们只能随便定义一个调色板,看看图形是不是正确。现在图形的问题已经解决了, 接下来我们要确认事件图的调色板在哪里。

另外我们前面也提到,程序通过调色板也做出了一些动画效果,比如切换事件图时的全白渐变, 全白时,无论什么颜色索引,取出的颜色都是 0x7fff

看懂这个日志可能需要解释一下,这一步 EBX 保存了颜色索引,取出的颜色放在 AX 中,而 ollydbg 一般的做法是当寄存器的值有变化时,会注释一个新的变化值,上面这段日志,只有第一行有注释,表示取出了 0x7fff,其他都没有注释,表示 EAX 的值没有发生变化。

确定调色板

从上面开始出发,我们知道 EDX 的内容,应该就是调色板的位置,然后每两个 byte 代表一个颜色,所以这应该是一个 16 位调色板。

上面我抓到的调色板明显是一个全白色的调色板了,这个对我们来说没什么意义,我们要抓到原始颜色的调色板,经过日志、调试和 ghidra 的三管齐下,我们觉得,调色板的位置应该是在这里生出来的:

结合 ghidra 的函数定义:

void __cdecl FUN_004119e0(uint *param_1,uint *param_2,ushort param_3,uint param_4)

第一个参数是输出的调色板,是一个全局变量 0058DC50,第二个参数是「事件一」固定的「调色板数据」:

const DATA: [u32; 512] = [
  0x7AB50000, 0x5A316A73, 0x39AC49EF, 0x1CC62D4A, 0x7B927FD6, 0x770A7B4E, 0x524C66AC,
  0x2D6A41CB, 0x77BD7FFF, 0x67396F7B, 0x52945EF7, 0x42104A52, 0x318C39CE, 0x2108294A,
...
];

为什么说是「调色板数据」,这段数据是固定的,但是又不能直接当作调色板来使用, 同时这段数据也不能从文件中直接搜到,有一个解密的过程,详细的情况,我们可能要放到下一篇来说明,不过我们可以认为这段是来自文件,因为读取文件的日志中,我们可以找到这些数据:

第三个参数可以认为是常数 0x7fff,第四个参数则是可以代表数量的 0x200

FUN_004119e0 做的事情,主要就是使用固定调色板数据和常数 0x7fff,结合另一个逐步变化的全局变量 DAT_005aec70 去计算实际的调色板,这个变量指向不同的数组,所以在其他输入保持不变的情况下,画出的图像会逐步变白,或是逐步从白色复原。

所以如果我们要抓到画图用的调色板,我们只要时刻注意 FUN_004119e0 执行后 0058DC50 中数据的变化情况,就可以提取出事件图的真实调色板:

let pals = [
  0x0C63, 0x7ED6, 0x6EB5, 0x5E73, 0x5231, 0x41EE, 0x358C, 0x2929, 0x7FF7, 0x7FB4, 0x7F70,
  0x7B2C, 0x6ACE, 0x5A8E, 0x4A0D, 0x35AC, 0x7FFF, 0x7BDE, 0x739C, 0x6B5A, 0x6318, 0x5AD6,
  ...
];

我们可以看出一些端倪,最大的值是 0x7FFF,也就是说 RGB 各用了 5 个 bit,有一位没有使用,那么 RGB 三色是如何排列的呢,不想动脑子的话可以像我一样,反正只有六种组合,画出来用眼睛判断即可:

for i in 0..6 {
  paf_event_1_by_pal(&pals, i);
}
...
match index {
    0 => {
        *pixel = Rgb([r as u8, g as u8, b as u8]);
    }
    1 => {
        *pixel = Rgb([r as u8, b as u8, g as u8]);
    }
    ...
}

不需要卖关子,其实就是按 RGB 的顺序排列的,结合灰姑娘的数据,我们又可以画出宝钗了:

所以调色板我们找到了,但是它是从哪里来的呢,我们知道有三个输入条件决定调色板的实际颜色,图像的「调色板数据」,常量,以及一个全局变量指向的索引字典,这个索引字典我是从内存中直接 dump 出来的:

那么这段内容从哪里来,我目前有一些推测,还没有验证,我认为是一段程序生成的,不过今天内容已经不少了, 我们留到下次验证这个猜测,顺带证实一下对第二幅图是否有效吧。最后我们说说调色板是怎么生出来的:

let c1 = ((crt & 0x7c007c00) >> 8) | ((magic & 0x7c007c00) >> 3);
let c2 = ((crt & 0x03e003e0) >> 3) | ((magic & 0x03e003e0) << 2);
let c3 = ((crt & 0x001f001f) << 2) | ((magic & 0x001f001f) << 7);

let (parsed_c1_low, parsed_c1_high) = get_dict_parsed(&dict_ram, c1);
let (parsed_c2_low, parsed_c2_high) = get_dict_parsed(&dict_ram, c2);
let (parsed_c3_low, parsed_c3_high) = get_dict_parsed(&dict_ram, c3);
let mut parsed_c = (parsed_c1_high << 0x10) | parsed_c1_low;
parsed_c = parsed_c << 5;
parsed_c |= (parsed_c2_high << 0x10) | parsed_c2_low;
parsed_c = parsed_c << 5;
parsed_c |= (parsed_c3_high << 0x10) | parsed_c3_low;
parsed[i] = parsed_c;

fn get_dict_parsed(data: &Vec<u8>, c: u32) -> (u32, u32) {
  let dict_start = 0x56c50;
  let c1_offset1 = dict_start + (c & 0x0000ffff) as usize;
  let c1_offset2 = dict_start + (c >> 0x10) as usize;
  //println!("offset1: {:#10x}, offset2: {:#10x}", c1_offset1, c1_offset2);
  let parsed_c1 = u32_from_vec_u8(&data, c1_offset1);
  let parsed_c2 = u32_from_vec_u8(&data, c1_offset2);
  //println!("parsed1: {:#10x}, parsed2: {:#10x}", parsed_c1, parsed_c2);
  (parsed_c1, parsed_c2)
}

简单解释一下逻辑,程序先按位取出每种颜色,然后与常量对应位或,这一步像是在补齐高位的 1, 然后查表,取出的值再合并成 4 个 byte。这个做法每次取出 4 个 byte,生成 4 个 byte, 由于每个颜色两个 byte,所以循环只要 128 次。

总结

虽说画出了第一张图,但是其实我还是一头雾水,实在是想不出来为什么调色板要这样子设计,分析 DOS 版的时候,我们基本没在调色板上画什么时间,直接用猜的就解决了,而这次绕了很多弯路,一般说来,游戏程序的设计都是直来直去的,不会故意绕什么弯子,因为会有性能损失,所以这里面应该是有我们还不知道的缘由吧。

话说回来,由于 dos 版的调色板我印象中每个颜色是 6 位(0-63),而 2001 版的颜色只有 5 位 (0-31),两个版本用的颜色数也是差不多的,所以我觉得图像的粒度会比原版更大一些,当然这是我一个美术外行人的看法,就不再详细展开了。