骑砍1成就系统逆向研究

文章发布时间:

最后更新时间:

文章总字数:
2.1k

页面浏览: 加载中...

问题描述

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=9F2000DTCompiler=com.apple.compilers.llvm.clang.1_0指向 Xcode 9.4 时代的 Clang 工具链,时间差不多是2018年左右

从SteamDB上其实也能看到最近两次的更新,最近一次是2023年,但内容都是DLC相关的,再上一次就是2018年,由此可见后面基本上也是不可能再去管了

steamdb

操作码分析

通过创意工坊的Achievement grab剧本,可以直接定位到战团脚本引擎用于解锁成就的内部指令:操作码 372

menus.txt 中可以看到连续的调用样例:

1
2
3
4
372 1 1
372 1 2
...
372 1 80

由此可以得出:

  • 372 是用于成就的脚本操作码
  • 形如 372 1 N:中间的 1 表示参数个数,N=1..80 为成就数字 ID(枚举见 module_constants.py
  • 引擎将数字 ID 映射为字符串成就 ID(如 HOLY_DIVERGET_UP_STAND_UP 等),随后调用 Steam 成就接口解锁并 StoreStats 持久化

在官方 Module System 1.171 源码中也有明确定义:

  • header_operations.py 定义了 370/371/372:
1
2
3
get_achievement_stat = 370 # (get_achievement_stat, <destination>, <achievement_id>, <stat_index>),
set_achievement_stat = 371 # (set_achievement_stat, <achievement_id>, <stat_index>, <value>),
unlock_achievement = 372 # (unlock_achievement, <achievement_id>),

由此可知 372 即为解锁成就的操作码,1 后面的数字为achievement_id

  • module_constants.py 将成就定义为 1..80 的枚举:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# NORMAL ACHIEVEMENTS

ACHIEVEMENT_NONE_SHALL_PASS = 1,
ACHIEVEMENT_MAN_EATER = 2,
ACHIEVEMENT_THE_HOLY_HAND_GRENADE = 3,
ACHIEVEMENT_LOOK_AT_THE_BONES = 4,
ACHIEVEMENT_KHAAAN = 5,
ACHIEVEMENT_GET_UP_STAND_UP = 6,
ACHIEVEMENT_BARON_GOT_BACK = 7,
ACHIEVEMENT_BEST_SERVED_COLD = 8,
ACHIEVEMENT_TRICK_SHOT = 9,
ACHIEVEMENT_GAMBIT = 10,
ACHIEVEMENT_OLD_SCHOOL_SNIPER = 11,
ACHIEVEMENT_CALRADIAN_ARMY_KNIFE = 12,
ACHIEVEMENT_MOUNTAIN_BLADE = 13,
ACHIEVEMENT_HOLY_DIVER = 14,
ACHIEVEMENT_FORCE_OF_NATURE = 15,

# SKILL RELATED ACHIEVEMENTS

ACHIEVEMENT_BRING_OUT_YOUR_DEAD = 16,
ACHIEVEMENT_MIGHT_MAKES_RIGHT = 17,
ACHIEVEMENT_COMMUNITY_SERVICE = 18,
ACHIEVEMENT_AGILE_WARRIOR = 19,
ACHIEVEMENT_MELEE_MASTER = 20,
ACHIEVEMENT_DEXTEROUS_DASTARD = 21,
ACHIEVEMENT_MIND_ON_THE_MONEY = 22,
ACHIEVEMENT_ART_OF_WAR = 23,
ACHIEVEMENT_THE_RANGER = 24,
ACHIEVEMENT_TROJAN_BUNNY_MAKER = 25,

# MAP RELATED ACHIEVEMENTS

ACHIEVEMENT_MIGRATING_COCONUTS = 26,
ACHIEVEMENT_HELP_HELP_IM_BEING_REPRESSED = 27,
ACHIEVEMENT_SARRANIDIAN_NIGHTS = 28,
ACHIEVEMENT_OLD_DIRTY_SCOUNDREL = 29,
ACHIEVEMENT_THE_BANDIT = 30,
ACHIEVEMENT_GOT_MILK = 31,
ACHIEVEMENT_SOLD_INTO_SLAVERY = 32,
ACHIEVEMENT_MEDIEVAL_TIMES = 33,
ACHIEVEMENT_GOOD_SAMARITAN = 34,
ACHIEVEMENT_MORALE_LEADER = 35,
ACHIEVEMENT_ABUNDANT_FEAST = 36,
ACHIEVEMENT_BOOK_WORM = 37,
ACHIEVEMENT_ROMANTIC_WARRIOR = 38,

# POLITICALLY ORIENTED ACHIEVEMENTS

ACHIEVEMENT_HAPPILY_EVER_AFTER = 39,
ACHIEVEMENT_HEART_BREAKER = 40,
ACHIEVEMENT_AUTONOMOUS_COLLECTIVE = 41,
ACHIEVEMENT_I_DUB_THEE = 42,
ACHIEVEMENT_SASSY = 43,
ACHIEVEMENT_THE_GOLDEN_THRONE = 44,
ACHIEVEMENT_KNIGHTS_OF_THE_ROUND = 45,
ACHIEVEMENT_TALKING_HELPS = 46,
ACHIEVEMENT_KINGMAKER = 47,
ACHIEVEMENT_PUGNACIOUS_D = 48,
ACHIEVEMENT_GOLD_FARMER = 49,
ACHIEVEMENT_ROYALITY_PAYMENT = 50,
ACHIEVEMENT_MEDIEVAL_EMLAK = 51,
ACHIEVEMENT_CALRADIAN_TEA_PARTY = 52,
ACHIEVEMENT_MANIFEST_DESTINY = 53,
ACHIEVEMENT_CONCILIO_CALRADI = 54,
ACHIEVEMENT_VICTUM_SEQUENS = 55,

# MULTIPLAYER ACHIEVEMENTS

ACHIEVEMENT_THIS_IS_OUR_LAND = 56,
ACHIEVEMENT_SPOIL_THE_CHARGE = 57,
ACHIEVEMENT_HARASSING_HORSEMAN = 58,
ACHIEVEMENT_THROWING_STAR = 59,
ACHIEVEMENT_SHISH_KEBAB = 60,
ACHIEVEMENT_RUIN_THE_RAID = 61,
ACHIEVEMENT_LAST_MAN_STANDING = 62,
ACHIEVEMENT_EVERY_BREATH_YOU_TAKE = 63,
ACHIEVEMENT_CHOPPY_CHOP_CHOP = 64,
ACHIEVEMENT_MACE_IN_YER_FACE = 65,
ACHIEVEMENT_THE_HUSCARL = 66,
ACHIEVEMENT_GLORIOUS_MOTHER_FACTION = 67,
ACHIEVEMENT_ELITE_WARRIOR = 68,

# COMBINED ACHIEVEMENTS

ACHIEVEMENT_SON_OF_ODIN = 69,
ACHIEVEMENT_KING_ARTHUR = 70,
ACHIEVEMENT_KASSAI_MASTER = 71,
ACHIEVEMENT_IRON_BEAR = 72,
ACHIEVEMENT_LEGENDARY_RASTAM = 73,
ACHIEVEMENT_SVAROG_THE_MIGHTY = 74,

ACHIEVEMENT_MAN_HANDLER = 75,
ACHIEVEMENT_GIRL_POWER = 76,
ACHIEVEMENT_QUEEN = 77,
ACHIEVEMENT_EMPRESS = 78,
ACHIEVEMENT_TALK_OF_THE_TOWN = 79,
ACHIEVEMENT_LADY_OF_THE_LAKE = 80,

还有触发器代码

  • module_game_menus.py:
1
2
3
(unlock_achievement, ACHIEVEMENT_LOOK_AT_THE_BONES)
(set_achievement_stat, ACHIEVEMENT_THE_BANDIT, 1, ":number_of_caravan_raids")
(get_achievement_stat, ":v", ACHIEVEMENT_BEST_SERVED_COLD, 0)

菜单事件触发即刻执行
第一行解锁一次性成就
第二行把本地变量:number_of_caravan_raids写入成就THE_BANDIT的统计项index=1
第三行把成就BEST_SERVED_COLD的index=0进度读入变量:v,便于后续判断或展示

  • module_simple_triggers.py:
1
(unlock_achievement, ACHIEVEMENT_ABUNDANTFEAST)

simple_trigger用于周期/条件性的集中检查,条件满足时统一解锁,避免在多处逻辑里重复判断与调用

  • module_scripts.py:
1
2
3
4
(get_achievement_stat, ":s", ACHIEVEMENT_SHISH_KEBAB, 0)
(val_add, ":s", 1)
(set_achievement_stat, ACHIEVEMENT_SHISH_KEBAB, 0, ":s")
(unlock_achievement, ACHIEVEMENT_SHISH_KEBAB)

成就进度更新,先读当前进度→累加→回写
达到阈值时再解锁

通过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 等符号引用,也搜不到 SetAchievementStoreStats 或 80 个成就 ID 的字符串,甚至没有 dlopen/dlsym 的动态加载痕迹,只有一些通用文本(如 steam_idui_authenticating_with_steam(Steam Workshop))以及 @rpath/libsteam_api.dylib 字符串本体(无交叉引用)

这说明 macOS 版虽然把 Steam 库随包带上并在加载命令里声明了依赖,但实际上并未在代码路径中调用成就相关接口,同时缺少脚本操作码 372 的处理逻辑,成就系统在该构建目标中处于未启用状态

所以目前看来这个问题是解决不了了,除非T社给源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
steamapps/common/MountBlade Warband 
❯ otool -L "Mount and Blade.app/Contents/MacOS/Mount and Blade"

Mount and Blade.app/Contents/MacOS/Mount and Blade:
/usr/lib/libcurl.4.dylib (compatibility version 7.0.0, current version 9.0.0)
@rpath/libsteam_api.dylib (compatibility version 1.0.0, current version 1.0.0)
@rpath/libfmodex.dylib (compatibility version 1.0.0, current version 1.0.0)
@rpath/GLEW.framework/Versions/2.1.0/GLEW (compatibility version 2.1.0, current version 2.1.0)
@rpath/SDL2.framework/Versions/A/SDL2 (compatibility version 1.0.0, current version 8.0.0)
/System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa (compatibility version 1.0.0, current version 22.0.0)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.50.4)
/System/Library/Frameworks/CoreServices.framework/Versions/A/CoreServices (compatibility version 1.0.0, current version 822.31.0)

最终方案

上虚拟机Parallels Desktop或者CrossOver运行Windows版吧😂

我感觉Parallels Desktop更好用点,而且这种老游戏对性能要求也没那么高,基本都能玩