数据结构解析
解析植物大战僵尸的内存以及存档文件数据结构
如无特殊说明默认使用游戏版本: 1.0.0.1051.
内存数据读取
指针表
参考资料: 站外链接 - 内存地址
其中比较齐全的一篇: Lazuplis-Mei/pvzclass/MemoryAddressList
本站同步的镜像文件: MemoryAddressList.txt (2020-11-23)
使用说明
数字用十六进制数表示, 未加额外前缀. [x] 表示地址 x 处的数据.
从基址起每向右一层偏移一级, 纵线顶部为对应的偏移值, 横线右侧为对应数据的最后一级偏移.
数据类型默认为 4 个字节的有/无符号整型 (int / unsigned int), 常见的还有 1 个字节的字符/字节/布尔型(0 表示假其他表示真) (char / byte / bool), 以及 4 个字节的浮点型 (float) 和 8 个字节的双精度浮点型 (double).
同时存在 ASCII UNICODE 等编码方式的字符串.
编译器的优化过程中出于字节对齐考虑也可能会使用 4 个字节来存储 bool 型变量.
以阳光数值为例, 查阅可得到 [[[0x6A9EC0] +0x768] +0x5560] 这个表达式, 它的含义为: 读取地址 6A9EC0 的值记为 a, 读取地址 a+768 的值记为 b, 读取地址 b+5560 的值记为 c, c 即为当前阳光数. 其中中间变量 a 和 b 为指针类型, 目标数值 c 按照整型来解析.
对于对象序列, “+s 下一个” 中的 s 为每个对象的结构体大小. 读取序列中的第 i (0 <= i < 1024) 个元素时需要给最后一级偏移额外加上 s * i. 比如位于序列第 42 (十进制) 个位置的僵尸的属性倒计时: [[[[0x6A9EC0] +0x768] +0x90] +0x68 +0x15C*42].
Cheat Engine
打开游戏进程, 手动添加地址, 选择目标数值类型, 勾选指针类型, 按需求增加或者移除偏移, 修改基址和各级偏移量.
在这个例子中, 读取地址 6A9EC0 得到 001E9E10, 读取地址 001E9E10+768 得到 13E7FBC0, 读取地址 13E7FBC0+5560 (即 13E85120) 得到目标数值 9990. 中间变量均为指针, 结果按整型解析.
C++ / Win32 API
使用操作系统提供的接口函数 ReadProcessMemory 来读取进程的内存数据.
可能用到的 Windows API 有:
需要安装 C++ 编译器 (Visual Studio 或者 MinGW-w64) 和 Windows SDK.
为了更好的支持中文, 应该事先定义 UNICODE _UNICODE 宏, 或者直接使用 W 版的函数.
一个简单的例子
#include <iostream>
#include <Windows.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "kernel32.lib")
int main()
{
HWND hwnd;
DWORD pid;
HANDLE handle;
hwnd = FindWindowA("MainWindow", "Plants vs. Zombies");
GetWindowThreadProcessId(hwnd, &pid);
handle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
unsigned int pvz_base;
unsigned int main_object;
int32_t sun;
ReadProcessMemory(handle, LPCVOID(0x6a9ec0), &pvz_base, 4, nullptr);
ReadProcessMemory(handle, LPCVOID(pvz_base + 0x768), &main_object, 4, nullptr);
ReadProcessMemory(handle, LPCVOID(main_object + 0x5560), &sun, 4, nullptr);
std::cout << "current sun: " << sun << std::endl;
CloseHandle(handle);
return 0;
}
稍微复杂点的例子
#ifndef UNICODE
#define UNICODE
#endif
#ifndef _UNICODE
#define _UNICODE
#endif
#include <iostream>
#include <initializer_list>
#include <Windows.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib, "kernel32.lib")
static_assert(sizeof(uintptr_t) == 4);
HWND hwnd = nullptr; // 窗口句柄
DWORD pid = 0; // 进程标识
HANDLE handle = nullptr; // 进程句柄
// 查找游戏窗口打开进程句柄
bool FindGame()
{
hwnd = FindWindowW(L"MainWindow", L"Plants vs. Zombies");
if (hwnd == nullptr)
return false;
GetWindowThreadProcessId(hwnd, &pid);
if (pid == 0)
return false;
handle = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
return handle != nullptr;
}
// 关闭游戏进程句柄
void CloseGame()
{
if (handle != nullptr)
CloseHandle(handle);
}
// 读取内存的函数模板
template <typename T>
T ReadMemory(std::initializer_list<uintptr_t> addr)
{
T result = T();
uintptr_t buffer = 0;
for (auto it = addr.begin(); it != addr.end(); it++)
{
if (it != addr.end() - 1)
{
unsigned long read_size = 0;
int ret = ReadProcessMemory(handle, //
(const void *)(buffer + *it), //
&buffer, //
sizeof(buffer), //
&read_size); //
if (ret == 0 || sizeof(buffer) != read_size)
return T();
}
else
{
unsigned long read_size = 0;
int ret = ReadProcessMemory(handle, //
(const void *)(buffer + *it), //
&result, //
sizeof(result), //
&read_size); //
if (ret == 0 || sizeof(result) != read_size)
return T();
}
}
return result;
}
int main(int argc, char const *argv[])
{
std::wcout.imbue(std::locale("chs"));
if (!FindGame())
return -1;
// 读取阳光数值
auto sun = ReadMemory<int>({0x6a9ec0, 0x768, 0x5560});
std::wcout << L"当前阳光: " << sun << std::endl;
// 读取场上植物信息
auto plants_offset = ReadMemory<unsigned int>({0x6a9ec0, 0x768, 0xac});
auto plants_count_max = ReadMemory<unsigned int>({0x6a9ec0, 0x768, 0xb0});
for (size_t i = 0; i < plants_count_max; ++i)
{
auto plant_dead = ReadMemory<bool>({plants_offset + 0x141 + 0x14c * i});
auto plant_squished = ReadMemory<bool>({plants_offset + 0x142 + 0x14c * i});
auto plant_type = ReadMemory<int>({plants_offset + 0x24 + 0x14c * i});
auto plant_row = ReadMemory<int>({plants_offset + 0x1c + 0x14c * i});
auto plant_col = ReadMemory<int>({plants_offset + 0x28 + 0x14c * i});
if (!plant_dead && !plant_squished)
std::wcout << L"序号: " << i << L" "
<< L"类型: " << plant_type << L" "
<< L"所在行: " << (plant_row + 1) << L" "
<< L"所在列: " << (plant_col + 1) << L" "
<< std::endl;
}
auto plants_next_pos = ReadMemory<unsigned int>({0x6a9ec0, 0x768, 0xb8});
auto plants_count = ReadMemory<unsigned int>({0x6a9ec0, 0x768, 0xbc});
std::wcout << L"下一个未被占用的植物位置: " << plants_next_pos << std::endl;
std::wcout << L"当前植物总数: " << plants_count << std::endl;
CloseGame();
return 0;
}
存档文件结构
参考资料: 站外链接 - 存档结构
用户列表存档
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x0000 | 4 | 文件头 0x0000000E |
| 0x0004 | 2 | 用户数量 |
后跟若干个结构: (每个代表一位用户)
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x0000 | 2 | 用户名长度 |
| 0x0002 | N | 用户名 (ASCII) |
| 0x0002+N | 4 | 用户序号 (内部) |
| 0x0006+N | 4 | 用户序号 (体现在文件名) |
用户数据存档
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x0000 | 4 | 文件头 0x0000000C |
| 0x0004 | 4 | 冒险模式当前关卡 [1, 50] |
| 0x0008 | 4 | 金钱数量 (显示数量要 x10) |
| 0x000C | 4 | 冒险模式已完成次数 |
| 0x0010 | 4x5 | 五个生存模式完成旗帜数 (最高 5) |
| 0x0024 | 4x5 | 五个生存困难模式完成旗帜数 (最高 10) |
| 0x0038 | 4x5 | 五个生存无尽模式完成旗帜数 |
| 0x004C | 4x20 | 二十个迷你小游戏奖杯 (0/1) |
| 0x009C | 4x13 | 十三个隐藏小游戏奖杯 (0/1) |
| 0x00D0 | 4 | 隐藏小游戏松鼠奖杯 (0/1) |
| 0x00D4 | 4 | 智慧树高度 |
| 0x00D8 | 4x9 | 九个砸罐子模式奖杯 (0/1) |
| 0x00FC | 4 | 砸罐子无尽轮数 |
| 0x0100 | 4x9 | 九个我是僵尸模式奖杯 (0/1) |
| 0x0124 | 4 | 我是僵尸无尽轮数 |
| 0x0128 | 4 | 隐藏小游戏销售奖杯 (0/1) |
| 0x012C | 4 | 隐藏小游戏介绍奖杯 (0/1) |
| 0x0130 | 112 | (未知) |
| 0x01A0 | 4x8 | 已购买八张紫卡 (0/1) |
| 0x01C0 | 4 | 已购买模仿者 (0/1) |
| 0x01C4 | 4 | (未知) |
| 0x01C8 | 4x3 | 三盆金盏花上次购买距离 20000101 的天数 (0) |
| 0x01D4 | 4 | 已购买金水壶 (0/1) |
| 0x01D8 | 4 | 花肥数量 +1000 (0) |
| 0x01DC | 4 | 杀虫剂数量 +1000 (0) |
| 0x01E0 | 4 | 已购买音乐盒 (0/1) |
| 0x01E4 | 4 | 已购买手套 (0/1) |
| 0x01E8 | 4 | 已购买蘑菇园 (0/1) |
| 0x01EC | 4 | 已购买手推车 (0/1) |
| 0x01F0 | 4 | 蜗牛醒来的时间戳, 持续三分钟 (0) |
| 0x01F4 | 4 | 卡槽升级次数 (0~4) |
| 0x01F8 | 4 | 已购买池塘清洁车 (0/1) |
| 0x01FC | 4 | 已购买屋顶清洁车 (0/1) |
| 0x0200 | 4 | 剩余钉耙数量 |
| 0x0204 | 4 | 已购买水族馆 (0/1) |
| 0x0208 | 4 | 巧克力数量 +1000 (0) |
| 0x020C | 4 | 已购买智慧树 (0/1) |
| 0x0210 | 4 | 树肥数量 +1000 (0) |
| 0x0214 | 4 | 已购买坚果包扎术 (0/1) |
| 0x0218 | 220 | (未知) |
| 0x02F4 | 4 | 蜗牛吃巧克力的时间戳, 持续一小时 (0) |
| 0x02F8 | 4 | 蜗牛横坐标 |
| 0x02FC | 4 | 蜗牛纵坐标 |
| 0x0300 | 4 | 解锁小游戏模式 (0/1) |
| 0x0304 | 4 | 解锁解谜模式 (0/1) |
| 0x0308 | 4 | 迷你游戏开启提示标记 (0/1) |
| 0x030C | 4 | 解谜模式砸罐子开启提示标记 (0/1) |
| 0x0310 | 4 | 解谜模式我是僵尸开启提示标记 (0/1) |
| 0x0314 | 4 | 生存模式开启提示标记 (0/1) |
| 0x0318 | 4 | 隐藏小游戏开启提示标记 (0/1) |
| 0x031C | 4 | 冒险模式完成提示标记 (0/1) |
| 0x0320 | 4 | 拥有墨西哥卷 (0/1) |
| 0x0324 | 4 | 蜗牛是否睡着 (0/1) |
| 0x0328 | 8 | (未知) |
| 0x0330 | 4 | 禅境花园植物数量 |
若花园植物数量不为零则后跟若干个结构: (0x58)
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x00 | 4 | 植物类型 |
| 0x04 | 4 | 花园位置 |
| 0x08 | 4 | 所在列数 |
| 0x0C | 4 | 所在行数 |
| 0x10 | 4 | 左右方向 |
| 0x14 | 4 | (未知) |
| 0x18 | 4 | 上次浇水的时间戳 (0) |
| 0x1C | 4 | (未知) |
| 0x20 | 4 | 颜色 |
| 0x24 | 4 | 施肥次数 |
| 0x28 | 4 | 浇水次数 |
| 0x2C | 4 | 需要的浇水次数 |
| 0x30 | 4 | 满足方式 (3.杀虫 4.听音乐) |
| 0x34 | 4 | (未知) |
| 0x38 | 4 | 上次满足的时间戳 (0) |
| 0x3C | 4 | (未知) |
| 0x40 | 4 | 上次施肥的时间戳 (0) |
| 0x44 | 4 | (未知) |
| 0x48 | 4 | 上次使用巧克力的时间戳 (0) |
| 0x4C | 4 | (未知) |
| 0x50 | 4 | (未知) |
| 0x54 | 4 | (未知) |
若为年度版则后跟: (Steam 版成就数量为 21 个)
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x00 | 2x20 | 二十个成就完成 (0/1) |
| 0x28 | 1 | 同意使用僵尸大头贴 (0/1) |
| 0x29 | 4 | 大头贴数量 |
若大头贴数量不为零则后跟若干个结构: (0x48)
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x00 | 4 | 0xFFFFFFFF |
| 0x04 | 4 | 皮肤颜色 |
| 0x08 | 4 | 衣服类型 |
| 0x0C | 4 | 衣服颜色 |
| 0x10 | 4 | 眼部类型 |
| 0x14 | 4 | 眼部颜色 |
| 0x18 | 4 | 配件类型 |
| 0x1C | 4 | 配件颜色 |
| 0x20 | 4 | 胡子类型 |
| 0x24 | 4 | 胡子颜色 |
| 0x28 | 4 | 头发类型 |
| 0x2C | 4 | 头发颜色 |
| 0x30 | 4 | 眼镜类型 |
| 0x34 | 4 | 眼镜颜色 |
| 0x38 | 4 | 帽子类型 |
| 0x3C | 4 | 帽子颜色 |
| 0x40 | 4 | 背景类型 |
| 0x44 | 4 | 背景颜色 |
年度版继续后跟:
| 偏移 | 大小 | 描述 |
|---|---|---|
| 0x00 | 20 | (未知) |
| 0x14 | 1 | 已经创建过大头贴 (0/1) |
关卡进度存档
(略)
*.pak 文件格式
整个文件与 0xF7 进行异或运算.
文件头:
4 字节, 魔数, 0xBAC04AC0.
4 字节, 版本, 0x00000000.
索引区:
若干个如下结构:
1 字节, 标记, 0x00/0x80.
1 字节, 路径长度 x.
x 字节, 文件路径.
4 字节, 文件大小.
8 字节, FILETIME 时间.
数据区:
按照索引区顺序排列的文件内容.