骑砍1成就系统逆向研究
最后更新时间:
文章总字数:
页面浏览: 加载中...
问题描述
Mount & Blade: Warband(骑马与砍杀:战团)的macOS版本自 2014 年上线起便无法解锁任意 Steam 成就,而 Windows 版一切正常。多年来社区一直都有人在说无法正常解锁成就,但并没有任何实际有效解决方案,基本上已经可以判断为就是macOS版本本身不支持,但我还是想深入研究一下根本原因
相关讨论
Not Getting Achievements MacOs | TaleWorlds Forums
Achievements not working [Mac] :: Mount & Blade: Warband Help and Support
I am getting no Steam achievements on MacOS : r/mountandblade
Achievements not being unlocked :: Mount & Blade: Warband Help and Support
问题分析
首先我在创意工坊订阅Achievement grab这个能够解锁所有成就的剧本,在macOS版打开后仍然没有解锁成就,这即可说明就是macOS版本本身的问题
打开macOS版本的Mount and Blade.app里面的Info.plist
BuildMachineOSBuild=17G65(macOS 10.13.6 High Sierra),DTPlatformBuild=9F2000+DTCompiler=com.apple.compilers.llvm.clang.1_0指向 Xcode 9.4 时代的 Clang 工具链,时间差不多是2018年左右
从SteamDB上其实也能看到最近两次的更新,最近一次是2023年,但内容都是DLC相关的,再上一次就是2018年,由此可见后面基本上也是不可能再去管了

操作码分析
通过创意工坊的Achievement grab剧本,可以直接定位到战团脚本引擎用于解锁成就的内部指令:操作码 372
在 menus.txt 中可以看到连续的调用样例:
1 | |
由此可以得出:
- 372 是用于成就的脚本操作码
- 形如
372 1 N:中间的1表示参数个数,N=1..80为成就数字 ID(枚举见module_constants.py) - 引擎将数字 ID 映射为字符串成就 ID(如
HOLY_DIVER、GET_UP_STAND_UP等),随后调用 Steam 成就接口解锁并StoreStats持久化
在官方 Module System 1.171 源码中也有明确定义:
header_operations.py定义了 370/371/372:
1 | |
由此可知 372 即为解锁成就的操作码,1 后面的数字为achievement_id
module_constants.py将成就定义为 1..80 的枚举:
1 | |
还有触发器代码
module_game_menus.py:
1 | |
菜单事件触发即刻执行
第一行解锁一次性成就
第二行把本地变量:number_of_caravan_raids写入成就THE_BANDIT的统计项index=1
第三行把成就BEST_SERVED_COLD的index=0进度读入变量:v,便于后续判断或展示
module_simple_triggers.py:
1 | |
simple_trigger用于周期/条件性的集中检查,条件满足时统一解锁,避免在多处逻辑里重复判断与调用
module_scripts.py:
1 | |
成就进度更新,先读当前进度→累加→回写
达到阈值时再解锁
通过IDA也能找到 Windows 版本的导入与关键函数地址,作为上述机制的技术依据
Windows 版本数据(mb_warband.exe)
| 符号 | 地址 |
|---|---|
| SteamAPI_Init | 0x7be60c |
| SteamUserStats | 0x7be5f0 |
| SteamUser | 0x7be624 |
| SteamAPI_RunCallbacks | 0x7be608 |
| SteamAPI_Shutdown | 0x7be618 |
| SteamAPI_RestartAppIfNecessary | 0x7be61c |
| SteamClient | 0x7be610 |
| SteamUtils | 0x7be5f8 |
| SteamAPI_RegisterCallback | 0x7be5fc |
| SteamAPI_RegisterCallResult | 0x7be600 |
| SteamAPI_UnregisterCallback | 0x7be5f4 |
| SteamAPI_UnregisterCallResult | 0x7be628 |
| SteamGameServer_Init | 0x7be5ec |
| SteamGameServer_RunCallbacks | 0x7be614 |
| SteamGameServer_Shutdown | 0x7be620 |
| SteamUGC | 0x7be630 |
成就系统关键函数
| 功能 | 地址 |
|---|---|
| 成就管理器构造函数 | 0x49BF40 |
| 获取用户统计 | 0x49B410 |
| 成就解锁回调 | 0x49B4B0 |
| 接收Steam统计 | 0x49BCE0 |
成就相关字符串
| 字符串 | 地址 |
|---|---|
| “Achievement ‘%s’ unlocked!” | 0x80e66c |
| “Achievement ‘%s’ progress callback” | 0x80e63c |
| “Received stats and achievements from Steam” | 0x80e6e4 |
成就ID字符串地址
| 成就ID | 地址 |
|---|---|
| GET_UP_STAND_UP | 0x80ebf8 |
| TALKING_HELPS | 0x80e944 |
| GOOD_SAMARITAN | 0x80ea10 |
| HELP_HELP_IM_BEING_REPRESSED | 0x80ea84 |
| COMMUNITY_SERVICE | 0x80eb2c |
| HOLY_DIVER | 0x80eb78 |
| FORCE_OF_NATURE | 0x80eb68 |
| MAN_EATER | 0x80ec3c |
| BARON_GOT_BACK | 0x80ebe8 |
| BEST_SERVED_COLD | 0x80ebd4 |
| MOUNTAIN_BLADE | 0x80eb84 |
| THE_HOLY_HAND_GRENADE | 0x80ec24 |
| CALRADIAN_ARMY_KNIFE | 0x80eb94 |
| KHAAAN | 0x80ec08 |
| GAMBIT | 0x80ebc0 |
| OLD_SCHOOL_SNIPER | 0x80ebac |
| TRICK_SHOT | 0x80ebc8 |
| DEXTEROUS_DASTARD | 0x80eaf8 |
| MELEE_MASTER | 0x80eb0c |
| MIGHT_MAKES_RIGHT | 0x80eb40 |
| AGILE_WARRIOR | 0x80eb1c |
| ART_OF_WAR | 0x80ead8 |
| MIND_ON_THE_MONEY | 0x80eae4 |
| THE_RANGER | 0x80eacc |
| BRING_OUT_YOUR_DEAD | 0x80eb54 |
| TROJAN_BUNNY_MAKER | 0x80eab8 |
| HAPPILY_EVER_AFTER | 0x80e9bc |
| HEART_BREAKER | 0x80e9ac |
| PUGNACIOUS_D | 0x80e928 |
| OLD_DIRTY_SCOUNDREL | 0x80ea5c |
| I_DUB_THEE | 0x80e988 |
| ROMANTIC_WARRIOR | 0x80e9d0 |
| MORALE_LEADER | 0x80ea00 |
| MIGRATING_COCONUTS | 0x80eaa4 |
| SARRANIDIAN_NIGHTS | 0x80ea70 |
| ABUNDANT_FEAST | 0x80e9f0 |
| BOOK_WORM | 0x80e9e4 |
| MEDIEVAL_TIMES | 0x80ea20 |
| LOOK_AT_THE_BONES | 0x80ec10 |
| GOLD_FARMER | 0x80e91c |
| SOLD_INTO_SLAVERY | 0x80ea30 |
| THE_BANDIT | 0x80ea50 |
| GOT_MILK | 0x80ea44 |
| ROYALITY_PAYMENT | 0x80e908 |
| MEDIEVAL_EMLAK | 0x80e8f8 |
| NONE_SHALL_PASS | 0x80ec48 |
| AUTONOMOUS_COLLECTIVE | 0x80e994 |
| CALRADIAN_TEA_PARTY | 0x80e8e4 |
| CONCILIO_CALRADI | 0x80e8bc |
| KINGMAKER | 0x80e938 |
| MANIFEST_DESTINY | 0x80e8d0 |
| THE_GOLDEN_THRONE | 0x80e96c |
| VICTUM_SEQUENS | 0x80e8ac |
| TALK_OF_THE_TOWN | 0x80e724 |
| MAN_HANDLER | 0x80e754 |
| SASSY | 0x80e980 |
| QUEEN | 0x80e740 |
| GIRL_POWER | 0x80e748 |
| LADY_OF_THE_LAKE | 0x80e710 |
| EMPRESS | 0x80e738 |
| KNIGHTS_OF_THE_ROUND | 0x80e954 |
| CHOPPY_CHOP_CHOP | 0x80e804 |
| MACE_IN_YER_FACE | 0x80e7f0 |
| THE_HUSCARL | 0x80e7e4 |
| THROWING_STAR | 0x80e860 |
| SPOIL_THE_CHARGE | 0x80e884 |
| SHISH_KEBAB | 0x80e854 |
| HARASSING_HORSEMAN | 0x80e870 |
| EVERY_BREATH_YOU_TAKE | 0x80e818 |
| ELITE_WARRIOR | 0x80e7bc |
| GLORIOUS_MOTHER_FACTION | 0x80e7cc |
| LAST_MAN_STANDING | 0x80e830 |
| THIS_IS_OUR_LAND | 0x80e898 |
| RUIN_THE_RAID | 0x80e844 |
| SON_OF_ODIN | 0x80e7b0 |
| IRON_BEAR | 0x80e788 |
| KASSAI_MASTER | 0x80e794 |
| SVAROG_THE_MIGHTY | 0x80e760 |
| KING_ARTHUR | 0x80e7a4 |
| LEGENDARY_RASTAM | 0x80e774 |
macOS 版本
实测 macOS 主程序(Mount and Blade.app/Contents/MacOS/Mount and Blade,x86_64,基址 0x100000000)在 otool -L 中确实列出了 @rpath/libsteam_api.dylib,bundle 内也能在 Contents/Resources/ 找到同名动态库libsteam_api.dylib
但用 IDA 检查导入表与字符串后可以确认,主程序没有任意 SteamAPI_* 或 ISteamUserStats 等符号引用,也搜不到 SetAchievement、StoreStats 或 80 个成就 ID 的字符串,甚至没有 dlopen/dlsym 的动态加载痕迹,只有一些通用文本(如 steam_id、ui_authenticating_with_steam、(Steam Workshop))以及 @rpath/libsteam_api.dylib 字符串本体(无交叉引用)
这说明 macOS 版虽然把 Steam 库随包带上并在加载命令里声明了依赖,但实际上并未在代码路径中调用成就相关接口,同时缺少脚本操作码 372 的处理逻辑,成就系统在该构建目标中处于未启用状态
所以目前看来这个问题是解决不了了,除非T社给源码
1 | |
最终方案
上虚拟机Parallels Desktop或者CrossOver运行Windows版吧😂
我感觉Parallels Desktop更好用点,而且这种老游戏对性能要求也没那么高,基本都能玩