《大航海家4》国王满意度 100% 锁定工具:从逆向到开源
最近在玩《大航海家 4》(Patrician IV: Rise of a Dynasty),游戏里最折磨人的设定莫过于国王满意度:完成他们的任务、送礼才能涨,但随时间流逝又会慢慢掉回去。全地图七八个国王要挨个哄,实在太麻烦。
于是我想:能不能写个工具,一键把所有国王的满意度锁死在 100%?
本以为这是个"搜数值 → 锁定"的十分钟小活,结果折腾了整整一天才搞定。这篇文章记录完整过程,给同样想改这类"实时计算数值"的朋友做个参考。
前置:先把"AI 驱动 Cheat Engine"的环境搭起来
这次我全程用的不是纯手动点 CE,而是通过 cheatengine-mcp-tcp-bridge 这个开源项目,把 Cheat Engine 暴露成 MCP 工具,让 AI 代理可以直接调用 CE 的 173 个工具(扫描内存、读写、反汇编、断点……)。
配置流程(对应作者博客里的教程):
- 把
ce_mcp_tcp_x64.dll复制到 CE 安装根目录 - 管理员打开 CE,执行
ce_mcp_bridge.lua,桥接在17171端口监听 - 装 Python 的
mcp包,把mcp_cheatengine.py配进 AI 客户端的 MCP 服务器列表 - 附加游戏进程,开搞
折腾到一半我还发现这个桥接有个 bug:
between(范围扫描)永远返回 0 结果,因为 Lua 脚本里firstScan/nextScan的第二个边界值被硬编码成了nil。修好它之后顺手给上游提了个 PR,这又是后话了。
第一回合:精确扫描,全军覆没
游戏界面显示"瓦特斯劳公爵 93%“这样的整数。我按照经典套路开始扫描:
- 4 字节整数扫描 93:命中 21922 个地址
- 等满意度变成 92,
next scan精确过滤:剩 14 个 - 再等变 91……0 个。全灭了。
换 float 精确值扫描、换范围扫描、换"减小了"扫描、换送礼跳变瞬间抓取……每一种方法都遵循同样的剧本:筛到个位数后,下一轮归零。
当时一度怀疑:这个游戏是不是把满意度做成"实时计算的派生值”,内存里根本没有这个数?
转折:0-1 浮点数 + 每帧重算
就在我要放弃时,转机来自一个细节。我无意中发现:
满意度对象在内存里是一个结构体,其中一个字段的值是 0.7381 这种 0-1 之间的浮点数 —— 界面显示的 73% 其实是它 ×100 取整的结果!
也就是说,游戏里存的是 0-1 的 float(73% = 0.7381),不是 0-100 的整数。这就是之前所有扫描失败的根本原因 —— 我一直在 55~100 这个区间里找,而这个值从来不在那里。
更"惊喜"的是:这个 float 字段被游戏每帧重算写入。我手动把 1.0 写进去,下一秒就被覆盖回 0.72。
单次写入没用,那就高频写入:用 Lua 起一个 10ms 定时器持续写 1.0,压过游戏的重算频率。
界面显示 100% 了。
从"一个国王"到"所有国王"
锁住一个国王不够,游戏里有 17 个国王对象。每次重启游戏地址还会变,总不能每次都手动扫吧?
突破口在对象结构。所有国王对象共享同一个虚函数表指针(vtable):
- vtable 地址:
0x00B36DF0(在 exe 的固定数据段,每次启动都不变) - 对象布局:
+0x00vtable,+0x10满意度(0-1 float),+0x14伴随值(0-1 float)
于是思路变成:扫描所有指向这个 vtable 的地址 → 就是全部国王对象 → 全部锁定。
实测一次性找到 17 个对象,与 CE 手动观察完全吻合。
最终工具:双击即用
为了让工具脱离 CE 也能用,我把它写成了独立的 Python 脚本(纯标准库,无第三方依赖):
# 核心逻辑(简化版)
VTABLE = 0x00B36DF0 # 国王对象 vtable,每次启动固定
SAT_OFF1, SAT_OFF2 = 0x10, 0x14 # 满意度字段偏移
# 1. 附加进程
pid = find_process("Patrician4_addon.exe")
h = open_process(pid)
# 2. 全内存扫描 vtable 特征 → 定位所有国王对象
objs = find_vtable_objects(h) # 17 个
# 3. 高频线程锁 1.0
def locker():
while not stop.is_set():
for o in objs:
write_mem(h, o + SAT_OFF1, FLOAT_1_0)
write_mem(h, o + SAT_OFF2, FLOAT_1_0)
time.sleep(0.01) # 10ms 压过游戏每帧重算
工具全流程(附加 → 扫描 → 锁定)实测 2.4 秒完成:
[+] 找到游戏进程 PID = 22544
[*] 正在扫描国王对象 (vtable=0xB36DF0)...
[+] 发现 17 个国王满意度对象:
对象 0x100ABA08 当前满意度 100.0%
对象 0x11461B50 当前满意度 100.0%
...(全部 17 个)
[+] 正在锁定全部满意度为 100%(每 10ms 写入)...
几个小坑记录一下:
- 扫描范围要覆盖到
0xFFFEFFFF。有 3 个国王对象在0xFA90xxxx这种高地址区,默认只扫到0x7FFEFFFF会漏掉 - 匹配用
bytes.find而不是逐字节 unpack。全内存逐字节解包要卡好几分钟,bytes.find定位候选 + 4 字节对齐校验只要 2 秒 - 需要管理员权限才能写游戏进程内存(游戏通常也是管理员运行)
- 别对高频字段下硬件断点,每帧触发会把游戏打崩
附加收获:顺手给开源仓库提了个 PR
前面提到桥接的 between 扫描 bug,修复之后我干脆把 PR 也提了:
- 仓库:
HollyZoe/cheatengine-mcp-tcp-bridge - PR:#1 fix: pass both range values to firstScan/nextScan for between scans
- 改动:
ce_mcp_bridge.lua两处,把between的第二个边界值从硬编码nil改为正确解析v1;v2 - 验证:修复前
between 80;90返回 0 结果,修复后返回 230 万
总结
这次逆向主要收获了一套解决"实时计算数值"问题的通用方法:
| 现象 | 结论 |
|---|---|
| 界面显示 73%,但怎么扫都找不到 73 | 真实值可能是 0.73(0-1 float),显示值只是取整 |
| 改完数值马上被还原 | 游戏每帧重算,需要高频写入压过它 |
| 地址每次重启都会变 | 找不变的锚点(vtable / AOB 特征)自动重定位 |
| 网上没有现成方案 | 不是没人做过,而是这类"派生值"确实难搞 |
如果你也在跟"实时计算数值"较劲,记住这三板斧:换存储格式(0-1 float)、高频冻结、找不变特征。
工具成品可下载:patrician4-lock.zip(解压后双击 run_lock.bat 即用,需管理员权限)。