【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
1、【MOD开发】【骑砍2】跨平台MOD构建环境(一):总体思路与 SDK 引导
背景
这是「跨平台 MOD 构建环境」系列的第一篇,以骑砍2(Mount & Blade II: Bannerlord)为例,讲如何在 Linux 上搭建一套通用的、跨平台的 MOD 构建环境。
骑砍2 是 Windows 平台上的游戏,它的 MOD 生态也基本围绕 Windows + Visual Studio 展开:作者发布的源码工程里,路径大小写随意、直接引用游戏本体的 DLL、打包目标写死成 Windows 专属命令。一旦在没有 Windows、也没有安装游戏本体的机器上做「编译 + 打包」,这些工程几乎无法直接跑起来。
本系列要讲的,就是把这套流程统一到几条通用命令下:自动准备 .NET SDK(Software Development Kit,软件开发工具包)、用参考程序集代替游戏本体、在只读源码之外构建、再按 Bannerlord 模块布局打包。全系列按「总体思路 → 参考程序集 → 源码同步 → 编译打包 → 元数据工具」的顺序展开,本篇先给出全局设计,并讲清最底层的一步------SDK 的引导。
骑砍2
🎯 要解决的四个问题
先把痛点摆出来,后面每一篇都对应其中一块。
| 维度 | Windows 上的常见做法 | 跨平台构建环境的替代方案 |
|---|---|---|
| 编译环境 | 预装 Visual Studio / .NET SDK | 首次运行自动引导 .NET SDK 到工作区 |
| 游戏依赖 | 引用本机安装的游戏 DLL | 用 NuGet 参考程序集装配「伪游戏目录」 |
| 源码保护 | 直接在源码目录编译 | 复制到工作副本,源码只读 |
| 输出结构 | 各自 xcopy 到模块目录 |
描述符驱动,统一打包成模块目录 + zip |
这套环境不依赖 Windows、编译期也不依赖游戏本体;运行验证仍要回到 Windows 游戏,这点会在系列里反复强调。
🗂 部件与数据流
与其绑定某一份具体目录,不如先看这套环境由哪些部件组成、数据怎么流动。

三条原则贯穿始终:
- 源码只读:MOD 源码目录永不被写入,所有中间产物都在独立的工作区;
- 描述符驱动:新增一个 MOD 只需放源码 + 写一个 JSON 描述符,脚本里没有 MOD 专属硬编码;
- 产物集中:工作区可整体删除重建,源码与配置不受影响。
为便于叙述,后续统一使用一套示例命名(与实际目录无关):脚本放 bin/,描述符放 config/<mod>.json,中间物用 cache/(SDK 与包缓存)、refs/(伪游戏目录)、work/(构建副本),产物落在 out/(模块目录)与 dist/*.zip(分发包),参考程序集留档在 vendor/。
🚀 一条流水线
对外只需一个主入口脚本,它把各阶段串成流水线。核心思路如下。
sh
# build.sh:按顺序调用各阶段脚本(示例)
./sdk.sh # 1. 引导 .NET SDK
REFS=$(./refs.sh "$VER" core native sandbox) # 2. 装配参考程序集,回显路径
./sync.sh "$MOD" config/$MOD.json # 3. 同步源码副本
./compile.sh "$MOD" config/$MOD.json "$REFS" # 4. 编译
./pack.sh "$MOD" config/$MOD.json # 5. 打包
每个阶段脚本都能单独执行,主入口只是按序调用。设计上有一个容易忽略的细节:像 refs.sh 这样的脚本用 stdout 回显关键结果(如伪游戏目录路径),用 stderr 输出日志 ,这样上层才能用命令替换 $(...) 接住返回值而不被日志污染。
这五步正好对应后面几篇:本篇讲第 1 步,第二篇讲第 2 步,第三篇讲第 3 步,第四篇讲第 4、5 步,元数据查看工具单独作为第五篇。
🖥 命令行速查
对外命令围绕主入口展开,常见用法如下。
| 命令 / 选项 | 作用 |
|---|---|
build.sh --list |
列出已注册的 MOD(读 config/*.json) |
build.sh --mod <id> |
编译 + 打包指定 MOD 并出 zip |
build.sh --mod <id> --clean |
先清工作副本与产物再构建 |
--ref-version <ver> |
覆盖参考程序集版本 |
--target-framework <tfm> |
只编指定目标框架 |
--no-zip |
只出模块目录,不压缩 |
以「列出 + 构建」为例,终端输出如下(真实输出,见截图)。

🧩 免 sudo 引导 .NET SDK
需求是:不污染系统、不需要 sudo、第一次跑就自动装上。通用做法是三级查找,优先级从高到低。

对应到脚本:先看工作区缓存里有没有可执行的 dotnet,再看系统 PATH,都没有才下载官方安装脚本。
sh
# sdk.sh:三级查找(示例)
if [ -x "$CACHE_DIR/dotnet/dotnet" ]; then
DOTNET_ROOT="$CACHE_DIR/dotnet" # ① 复用本地缓存
elif have dotnet; then
DOTNET_ROOT=$(dirname "$(command -v dotnet)") # ② 复用系统
else # ③ 下载 dotnet-install.sh 安装到工作区
bash "$installer" --channel "${DOTNET_CHANNEL:-8.0}" \
--install-dir "$CACHE_DIR/dotnet"
DOTNET_ROOT="$CACHE_DIR/dotnet"
fi
安装完成后把一批环境变量导出,让后续 dotnet build 子进程直接继承:
| 环境变量 | 作用 |
|---|---|
DOTNET_ROOT |
指向 SDK 根目录,子进程据此定位运行时 |
PATH |
把 DOTNET_ROOT 前置,dotnet 命令优先命中它 |
NUGET_PACKAGES |
NuGet 包缓存改到工作区,不写用户家目录 |
DOTNET_NOLOGO/DOTNET_CLI_TELEMETRY_OPTOUT/DOTNET_SKIP_FIRST_TIME_EXPERIENCE |
关掉首启提示、遥测与首次体验,减少噪音 |
一句话记忆:缓存优先、系统兜底、都没有才下载,且一切写入工作区。
⚠️ 一个必须记住的坑:安装脚本要用 bash
官方安装脚本 dotnet-install.sh 内部用到了 set -o pipefail,而 pipefail 是 bash 特性,部分 sh(如 dash)并不支持。若直接用 sh dotnet-install.sh,会报类似 Illegal option -o pipefail。
sh
# sdk.sh:优先用 bash 执行安装脚本(示例)
if have bash; then
bash "$installer" --channel "${DOTNET_CHANNEL:-8.0}" --install-dir "$DOTNET_DIR"
else
sh "$installer" --channel "${DOTNET_CHANNEL:-8.0}" --install-dir "$DOTNET_DIR"
fi
排查表里也记这一条:遇到 dotnet-install.sh: Illegal option -o pipefail,手动执行时同样要用 bash 而不是 sh。
📌 本篇小结
- 跨平台构建环境解决「无 Windows / 无游戏本体也能编译打包」的问题,核心是 参考程序集 + 伪游戏目录 + 只读副本 + 描述符驱动。
- 主入口是一条五步流水线(SDK → 参考程序集 → 源码同步 → 编译 → 打包),每步都能单独跑。
- 引导 SDK 用三级查找实现免 sudo,并把缓存与包都锁在工作区内。
dotnet-install.sh必须用bash执行。- 全系列用一套示例命名(
bin/、config/、cache/、refs/、work/、out/),与具体目录无关,便于聚焦通用做法。
一句话记忆:先备好工具链与游戏依赖,再在只读源码之外完成构建与打包。
下一篇进入最关键的一环:为什么能用 NuGet 上的参考程序集代替游戏本体,以及「伪游戏目录」是怎么装配出来的。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog