【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
5、【游戏MOD】【骑砍2】跨平台MOD构建环境(五)/:元数据查看工具与跨版本适配
背景
上篇《跨平台MOD构建环境(四)/:跨平台编译与通用打包》讲清了流水线最后两步:compile.sh 通过三个 MSBuild 全局属性完成跨平台编译-------p:GamePath 注入参考程序集路径、-p:EnableWindowsTargeting=true 允许 Linux 编 net472、-p:ModulePackagePath= 关闭工程自带的 Windows 打包步骤;整解失败自动回退逐工程;pack.sh 按描述符 modules[] 把 DLL 放进 bin/Win64_Shipping_Client/、把资产铺到模块根并压缩成 zip。同时强调了 Directory.Build.props 里的 <Private>false</Private>------它是「旧 DLL 复制进 bin/ 导致假通过」这一跨版本第一坑的解法。
主链路讲完后,本篇换个角度,讲一个专门为「跨游戏版本适配」准备的小工具:元数据查看工具 。MOD 从旧版本移植到新版本时,最常见的失败是某个字段/属性/方法被游戏改名,而这个工具用来在不运行游戏、不加载依赖的前提下,直接从参考程序集里查出类型与成员签名。
骑砍2
🎯 为什么只需读元数据
第二篇说过,参考程序集是只含元数据、不含 IL(Intermediate Language,中间语言)程序体的 DLL。而编译期恰好只需要「有哪些类型、成员签名是什么」,不需要方法体------所以直接用参考程序集分析,就能回答「新版本里这个成员还叫这个名吗、参数变了吗」。
| 对象 | 内容 | 能回答的问题 |
|---|---|---|
| 参考程序集 | 仅元数据(类型/字段/属性/方法签名) | 成员是否改名、签名是否变化 |
| 运行程序集 | 元数据 + IL | 方法「怎么做的」(行为) |

只读参考程序集还附带一个好处:不需要解析依赖 。读取时用 PEReader(Portable Executable,可移植可执行文件的读取器)配合 MetadataReader,只解析元数据表,不会去加载被引用的程序集,因此即便依赖缺失也能跑。
🧩 两种模式与用法
工具对外只有两个动作:type(打印某类型的字段/属性/方法)与 find(按子串反查成员所在类型)。
sh
# 打印某类型的完整签名
./meta.sh TaleWorlds.MountAndBlade.dll type TaleWorlds.MountAndBlade.MBMissile
# 反查含某子串的成员(不知道成员在哪个类型时用)
./meta.sh TaleWorlds.CampaignSystem.dll find PartyLimit
外层脚本 meta.sh 负责繁琐的准备工作:定位参考程序集根(优先留档,其次缓存)、必要时构建工具、再把参数透传给工具本体。
sh
# meta.sh:定位 refs + 按需构建工具(示例)
REFS=$(ls -d "$VENDOR_DIR"/*/ 2>/dev/null | head -1)
[ -n "$REFS" ] || REFS=$(ls -d "$REFS_DIR"/*/ 2>/dev/null | head -1)
exec dotnet "$TOOL_DLL" "$REFS" "$1" "$2" "$3"
🧱 处理管线
工具本体的管线很短:定位 DLL → 用 PEReader 打开 → 取 MetadataReader → 遍历 TypeDefinitions → 按模式输出。

其中关键的一步是解码签名 。元数据里的方法签名是压缩编码的,需要实现一个 ISignatureTypeProvider 来把它还原成可读字符串:
csharp
// 解码签名(示例)
var md = pe.GetMetadataReader();
var provider = new TypeProvider(md);
// 每个方法:先 DecodeSignature,再打印返回类型与参数类型
var sig = mdef.DecodeSignature(provider, null);
var pars = string.Join(", ", sig.ParameterTypes);
Console.WriteLine($" {vis} {stat}{sig.ReturnType} {md.GetString(mdef.Name)}({pars})");
type 模式会依次打印三块内容:-- fields --、-- properties --、-- methods --,每行都带可见性(public/private)与静态标记,形如 public static void Foo(Bar),足够用来比对版本间的成员差异。
🔬 find 模式是怎么反查的
find 模式不知道目标成员在哪个类型,所以它遍历每个类型,把字段、属性、方法的名字逐一与查询子串做「包含」比较,命中就输出「类型名:成员列表」。
csharp
// find 模式(示例)
foreach (var fh in td.GetFields())
{
var nm = md.GetString(md.GetFieldDefinition(fh).Name);
if (nm.Contains(query, StringComparison.OrdinalIgnoreCase)) hits.Add($"field {nm}");
}
// 属性/方法同理:hits.Add($"prop {nm}") / hits.Add($"method {nm}")
这也解释了前面的一条局限:find 匹配的是成员名 (字段/属性/方法),不是方法体内容;要找「某个方法里用了谁」,还得靠实机日志或 strings。
🧮 签名解码要处理的几类语法
元数据里的签名是压缩编码的,ISignatureTypeProvider 的作用就是把它们还原成可读类型名。需要处理的几类比较典型:
| 签名形态 | 解码结果 | 对应方法 |
|---|---|---|
| 基本类型 | int/bool/string 等 |
GetPrimitiveType |
| 数组 | T[] / T[,] |
GetSZArrayType / GetArrayType |
| 泛型 | List<T> |
GetGenericInstantiation |
| 泛型参数 | !0(类型参数)/ !!0(方法参数) |
GetGenericTypeParameter / GetGenericMethodParameter |
| 引用/指针 | ref T / T* |
GetByReferenceType / GetPointerType |
正是这个 provider 把 ValueTuple2<bool, bool>这类签名还原成System.ValueTuple2<bool, bool>,移植时才看得懂新版方法的参数到底变成了什么。
⚠️ 已知局限
工具刻意保持极简(metadata-only、不解析依赖),因此有几处约定需要记住:
| 现象 | 原因 |
|---|---|
in/ref 参数都显示成 ref |
元数据读取未区分两者,需按 in 先试 |
| 嵌套类型只显示短名 | 如 Mission.Missile 会打印成 Missile,type 查询要用短名 |
可见性只分 public/private |
override 常被显示成 private,不能据此判断可访问性 |
接口/枚举用 type 查询可能为空 |
改用 find 反查更稳 |
| 看不到方法体 | 元数据无 IL,行为要靠实机日志或 strings 辅助 |
一句话记忆:它是「API 存在性与签名」的来源;行为以实机为准。
🧪 实战:定位一个被改名的字段
一次真实移植里,旧代码引用了 MissionGauntletFormationMarker 的私有字段 _focusedFormationsCache,新版运行时报 No such field。用 find 反查新版参考程序集即可确认改名:
sh
./meta.sh TaleWorlds.MountAndBlade.GauntletUI.dll find focusedFormation
# 输出里能看到新字段名(旧名已不存在)
据此把补丁里的字段名从 _focusedFormationsCache 改成新名,编译即可通过。整个过程没有启动游戏、没有加载任何依赖,只是读了一份元数据。

📌 本篇小结
- 编译期只需签名,因此元数据查看工具只读参考程序集(metadata-only)即可分析 API。
- 两种模式:
type(打印成员签名)与find(按子串反查成员)。 - 管线:定位 DLL →
PEReader/MetadataReader→ISignatureTypeProvider解码签名 → 遍历类型输出。 - 局限:
in/ref都印成ref、嵌套短名、可见性只分两类、看不到 IL。 - 用途:跨游戏版本时,先确认成员是否改名/改签名,再改代码。
到这里,从 SDK 引导、参考程序集、源码同步、编译打包到元数据工具,「跨平台 MOD 构建环境」就讲完了。后续可以继续展开 崩溃日志收集与判读、产物部署到 Windows 游戏、以及一个跨版本移植的完整复盘。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog