数据结构解析

解析植物大战僵尸的内存以及存档文件数据结构

如无特殊说明默认使用游戏版本: 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. 中间变量均为指针, 结果按整型解析.

CE读取阳光

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 时间.

数据区:

按照索引区顺序排列的文件内容.