【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
2、【MOD开发】【骑砍2】跨平台MOD构建环境(二):参考程序集与伪游戏目录
背景
上篇【MOD开发】【骑砍2】跨平台MOD构建环境(一):总体思路与 SDK 引导给出了全局设计:整套环境解决「无 Windows、无游戏本体也能编译打包」的问题,主入口build.sh是一条五步流水线(sdk.sh→refs.sh→sync.sh→compile.sh→pack.sh),并讲清了第一步——用三级查找(本地缓存 → 系统PATH→ 官方脚本下载)免 sudo 引导 .NET SDK,以及dotnet-install.sh必须用bash执行(用到pipefail)这个坑。同时约定:源码只读、描述符驱动、产物集中,缓存与包都锁在工作区内。
SDK 就位后,紧接着的问题就是:编译 MOD 需要引用游戏程序集,而机器上根本没有安装游戏。本篇就讲流水线的第二步refs.sh——用 NuGet 上的参考程序集装配出一个「伪游戏目录」,让工程里对游戏 DLL 的引用能够解析。
骑砍2
🎯 为什么可以不用游戏本体
编译期只需要知道「有哪些类型、签名是什么」,不需要方法体。因此可以用参考程序集(reference assembly,只含元数据、不含 IL(Intermediate Language,中间语言)程序体的 DLL)来替代真实的游戏 DLL。
参考程序集来自 NuGet 包bannerlord.referenceassemblies.<suffix>,按游戏版本发布。工程里常见的引用写法是:
<!-- MOD 工程里典型引用:用 $(GamePath) 指向游戏根目录 --><ReferenceInclude="TaleWorlds.MountAndBlade"><HintPath>$(GamePath)bin\Win64_Shipping_Client\TaleWorlds.MountAndBlade.dll</HintPath></Reference>要做的是把参考程序集按与真实安装一致的目录布局铺开,再把这个根目录当作GamePath传给dotnet build(注入细节见第四篇)。这样无需改动任何 csproj,也同时满足子模块工程。
🧩 后缀与目录的映射
游戏本身的 DLL 分散在多个模块目录下,NuGet 也按模块拆成多个子包。装配脚本用一张映射表,把「包后缀」还原成「相对GamePath的真实目录」。
对应实现就是一段case:
# refs.sh:包后缀 -> 真实游戏目录(示例)map_ref_dir(){case"$1"incore)echo"bin/Win64_Shipping_Client";;native)echo"Modules/Native/bin/Win64_Shipping_Client";;sandbox)echo"Modules/SandBox/bin/Win64_Shipping_Client";;navaldlc)echo"Modules/NavalDLC/bin/Win64_Shipping_Client";;*)echo"bin/Win64_Shipping_Client";;esac}core对应游戏根目录下的bin/,其余模块各落在自己的Modules/<模块>/bin/下——这正好与真实安装目录一一对应。一个典型的下载命令形如:
# 只装配引擎与 SandBox(后缀即子包名)./refs.sh1.4.8.119303 core native sandbox🧱 装配伪游戏目录
装配采用留档 → 缓存 → 在线三级优先级,避免每次构建都访问网络。
核心分支如下(节选):
# refs.sh:三级优先级(示例)if[-f"$VENDOR_DIR/$VER/.complete"];then# ① 留档:完全离线echo"$VENDOR_DIR/$VER";return0fiif[-f"$REFS_DIR/$VER/.complete"];then# ② 缓存:本地复用echo"$REFS_DIR/$VER";return0fi# ③ 在线:下载 nupkg 并解包到 map_ref_dir 指定的目录装配函数把「伪游戏目录路径」通过stdout打印,上层用命令替换REFS=$(...)接住;所以日志函数都强制写到stderr,否则路径会被日志污染。
下载解包时还有个兜底——core是编译必需的核心包,一旦拉取失败就直接终止;其余后缀不可用则只告警跳过:
# refs.sh:core 缺失直接失败,其余可跳过(示例)if!_dir=$(_fetch_nupkg"$_pkg""$_ver");thenif["$_suffix"="core"];thendie"核心参考程序集下载失败:$_pkg$_ver"fiwarn"参考程序集不可用,跳过:$_pkg$_ver"continuefi🗄 装配后长什么样:产物与缓存标记
一次在线装配完成后,GamePath指向的目录会长成这样(以core + native为例):
refs/1.4.8.119303/ ├── bin/Win64_Shipping_Client/ # core 包解出的引擎 DLL ├── Modules/Native/bin/Win64_Shipping_Client/ # native 包 └── refs.lock.json # 包 + DLL 的 SHA-256用两个标记文件避免重复劳动:解包目录下的.done表示「该 nupkg 已解包」,参考程序集根目录下的.complete表示「该版本已装配完成」。下载的 nupkg 本身缓存在工作区,即使断网,只要.done与 nupkg 还在,就能重新装配而不必再访问网络。
🔒 refs.lock.json 与 beta 暂态
Beta 版本的参考程序集是暂态的:上游可能把1.5.3.122374-beta替换成稳定版1.5.3.122374,届时原包可能被撤下。为避免「撤下即无法复现」,把装配好的结果留档到vendor/<版本>/,并生成refs.lock.json记录包 id + 每个 nupkg 与每个 DLL 的 SHA-256(Secure Hash Algorithm 256-bit,安全散列算法)。
{"referenceVersion":"1.5.3.122374-beta","packages":[{"id":"bannerlord.referenceassemblies.core","version":"1.5.3.122374-beta"}],"assemblies":[{"path":"bin/Win64_Shipping_Client/TaleWorlds.Core.dll","sha256":"..."}]}版本库层面给留档开例外(其它二进制一律忽略,唯独留档目录下的参考程序集可提交),从而支持离线复现。上游替换成稳定版后的迁移动作如下:
| 步骤 | 操作 |
|---|---|
| 1 | 更新版本注册表中该版本的referenceVersion,通道改为release |
| 2 | 更新各 MOD 描述符的game.referenceVersion |
| 3 | 删除旧留档目录,重新构建以生成新留档 |
| 4 | 在版本记录里追加一行,保留历史 |
一句话记忆:留档优先、缓存复用、在线兜底;版本钉死 + 校验和,才敢说「可复现」。
⚠️ 常见坑
| 现象 | 原因 / 处理 |
|---|---|
| 参考程序集下载 404 | 该后缀/版本在 NuGet 不存在;换版本或从referencePackages移除该项 |
只配了部分后缀(如没有core) | core缺失会直接终止,因为它是编译必需的核心包 |
| 换机器后仍想离线构建 | 确认留档目录的.complete存在(留档已提交) |
📌 本篇小结
- 编译期只需类型与签名,因此用参考程序集(无 IL)替代游戏 DLL 即可。
- 装配脚本用
map_ref_dir把包后缀还原成真实目录,铺出「伪游戏目录」。 - 三级优先级
留档 → 缓存 → 在线,配合refs.lock.json的 SHA-256 校验和实现离线可复现。 core缺失即失败,其余后缀不可用可跳过。
下一篇进入流水线第三步:把只读源码同步成构建副本、克隆子模块、并修掉 Windows 工程里的路径大小写问题。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【MOD开发】【骑砍2】跨平台MOD构建环境(三):只读源码同步、子模块与路径大小写