259 lines
19 KiB
Markdown
259 lines
19 KiB
Markdown
# BD2.GameSdk
|
||
|
||
给 Brown Dust II 插件作者使用的 C# 开发包。通过标准 NuGet `PackageReference` 导入,自动完成 **生成可读引用程序集和全量源码导航 → 编译插件 → reobf → 验证运行 DLL**。源码可以使用可读类型和成员,字符串反射由配套的 `BD2.GameNames` 处理。转到定义可查看完整反编译方法体。
|
||
|
||
## 包与版本
|
||
|
||
| NuGet 包 | 内容 | 使用方式 |
|
||
| --- | --- | --- |
|
||
| `BD2.GameSdk` | .NET 8 构建工具、`build/*.props/targets`、压缩名字表 | 插件项目直接引用,`PrivateAssets="all"` |
|
||
| `BD2.GameNames` | `lib/netstandard2.0/BD2.GameNames.dll`、XML API 文档、同一份内嵌表 | SDK 固定依赖对应版本,自动引入;DLL 随插件部署 |
|
||
|
||
当前包版本为 `0.2.1-game.2.35.10`:`0.2.1` 是开发工具/API 的语义版本,`game.2.35.10` 指定游戏版本。采用 SemVer 的 prerelease 段,安装时明确指定版本。升级游戏后更新包并重新构建插件;相同游戏版本下的不同官方 DLL 也会因指纹不同而被拒绝。
|
||
|
||
包不包含游戏 DLL 或可读壳。维护者用官方映射生成名字表,提交为 `plugins/GameNames/Mappings/names.json.gz`,SDK 和运行时从同一文件嵌入,插件作者只需要对应游戏客户端和 BepInEx,无需官方映射、Python、仓库 `versions.json` 或本仓库源码。
|
||
|
||
包当前由维护者提供 `.nupkg` 或 NuGet 源,**尚未发布到 nuget.org**。包内包含仓库 LICENSE,许可条款沿用项目现有授权,不声明为开源许可。
|
||
|
||
## 导入到自己的插件项目
|
||
|
||
前提:安装 **.NET 8 SDK**,准备与包对应的游戏目录,并在该目录安装 BepInEx。推荐 SDK-style `.csproj` 与 `netstandard2.1`;运行时库本身兼容 `.NET Standard 2.0`。旧式 `packages.config` 不受支持。
|
||
|
||
将维护者提供的两个 `.nupkg` 放在一个目录,例如 `D:\NuGet\BD2`,添加为本地 NuGet 源:
|
||
|
||
```powershell
|
||
dotnet nuget add source 'D:\NuGet\BD2' --name BD2
|
||
dotnet add MyPlugin.csproj package BD2.GameSdk --version 0.2.1-game.2.35.10
|
||
```
|
||
|
||
Visual Studio / Rider 也可在 NuGet 包管理界面添加该源,打开“包含预发布版本”,安装指定版本。若维护者提供远程 NuGet 源,把上面的目录换成该源 URL。
|
||
|
||
最小项目文件:
|
||
|
||
```xml
|
||
<Project Sdk="Microsoft.NET.Sdk">
|
||
<PropertyGroup>
|
||
<TargetFramework>netstandard2.1</TargetFramework>
|
||
<LangVersion>latest</LangVersion>
|
||
<AssemblyName>MyPlugin</AssemblyName>
|
||
<BD2GameVersion>2.35.10</BD2GameVersion>
|
||
</PropertyGroup>
|
||
<ItemGroup>
|
||
<PackageReference Include="BD2.GameSdk"
|
||
Version="0.2.1-game.2.35.10"
|
||
PrivateAssets="all" />
|
||
</ItemGroup>
|
||
</Project>
|
||
```
|
||
|
||
`PrivateAssets="all"` 让构建步骤只用于当前插件项目;它不会阻止运行时 DLL 复制到输出目录。SDK 自动引入 `BD2.GameNames`,以及游戏目录中的 `BepInEx`、`0Harmony`、`UnityEngine`、`UnityEngine.CoreModule` 引用,无需手写 `<Import>`。**不要再引用真实的 `Assembly-CSharp.dll`**,游戏 API 编译引用由 SDK 提供。
|
||
|
||
在项目旁创建仅保存在本机的 `Directory.Build.props`,并加入自己的 `.gitignore`:
|
||
|
||
```xml
|
||
<Project>
|
||
<PropertyGroup>
|
||
<GameDir>E:\Games\BrownDustII</GameDir>
|
||
</PropertyGroup>
|
||
</Project>
|
||
```
|
||
|
||
或者构建时传入目录:
|
||
|
||
```powershell
|
||
dotnet build MyPlugin.csproj -c Release '-p:GameDir=E:\Games\BrownDustII'
|
||
```
|
||
|
||
IDE 项目加载/设计时构建会准备可读引用;CLI 首次构建同样会自动生成。使用 SDK 不需要运行额外的生成命令。完整可复制示例在 [samples/ExamplePlugin](samples/ExamplePlugin),包含 `.csproj`、插件源码和本机配置示例。
|
||
|
||
## 完整源码导航
|
||
|
||
导入包、设置 `GameDir` 后,先完成一次构建。SDK 使用 ILSpy 从匹配版本的真实游戏 DLL 生成**全部类型的可读 C# 源码**,保留方法体、私有成员、嵌套类型和编译器生成实现;同步生成 Portable PDB,将源码内嵌到 PDB。名字仍来自同一份内嵌表,没有另一份源码映射。
|
||
|
||
在支持外部源码导航的 IDE 中,对 `NetworkManager.Send` 等可读 API 使用“转到定义”(Visual Studio 的 F12),即可打开完整方法体。重载、泛型、参数、属性、字段、事件和嵌套类型使用真实元数据及 PDB 定位,不靠搜索同名字符串。
|
||
|
||
- **Visual Studio 2022**:在“工具 → 选项 → 文本编辑器 → C# → 高级”中启用“导航到 Source Link 和嵌入源”(不同语言/版本的名称可能略有差异)。如果仍打开旧的签名视图,关闭原外部源码页并重新加载项目后再 F12。只阅读源码不需要关闭“仅我的代码”调试设置。
|
||
- **Rider**:启用外部代码的源文件/PDB 或反编译导航。可读引用保留完整 IL,IDE 也可直接反编译出方法体;本项目的自动测试验证了 Visual Studio 的 Roslyn PDB 导航引擎,尚未手动验证 Rider 各版本界面。
|
||
- 项目中还会链接一个 **`Game Sources`** 文件夹,支持直接浏览和文本搜索。首次生成后若文件夹未出现,重新加载项目使生成的 MSBuild 导入生效。这些 `.cs` 是 `None` 项,**不参与插件编译,也不复制到部署目录**。
|
||
|
||
普通视图恢复 `async/await`、迭代器和 lambda,便于阅读;`generated` 视图补全被这些语法隐藏的状态机、访问器等,`metadata` 视图补全其他隐藏声明。无法作为 C# 标识符的 CLR 生成名在源码中显示为 `__generated_...`,`navigation.json` 和名字表仍保留真实元数据身份。
|
||
|
||
首次全量生成会花费数分钟,日志持续报告已处理的类型数。本机 2.35.10 的一次验证生成了 **24,856 个源码文件、654,265 个声明、319,350 个带方法体的符号**。生成结果按名字表、生成器及其依赖、全部游戏 Managed DLL 的指纹缓存,同一台机器上的插件项目共用;并行构建会等待同一个缓存生成完成。后续构建不再执行全量反编译。
|
||
|
||
所有依赖 SDK 的插件必须在自己的 `.csproj` 明确声明目标**游戏版本**:
|
||
|
||
```xml
|
||
<BD2GameVersion>2.35.10</BD2GameVersion>
|
||
```
|
||
|
||
这个值与 SDK/API 工具版本(如 `0.2.1`)不同。构建核对声明值、SDK 内嵌表、真实游戏 DLL 指纹,以及仓库 `versions.json`(仓库插件)。缺少声明或版本不匹配会报错;不会自动选用其他版本。NuGet 项目仍需安装带对应游戏版本的包。
|
||
|
||
共享目录按 `<缓存根>/<游戏版本>/<内容指纹>/` 存放。同版本的三个插件、Debug/Release、NuGet 项目在输入一致时直接引用同一份产物;内容指纹用于隔离同一游戏版本内不同 SDK 实现或 Managed DLL,避免覆盖正在使用的旧引用。
|
||
|
||
仓库默认缓存根为 `.build/game-sdk`,与源码位于同一盘。第三方 NuGet 项目默认使用 `%LOCALAPPDATA%\BD2\GameSdk\navigation`;可在本机 `Directory.Build.props` 设置 `BD2GameSdkCache`,或设置环境变量 `BD2_GAME_SDK_CACHE`。删除插件 `obj` 不影响共享缓存。缺失的共享源码可在下次准备时从 PDB 恢复;删除共享缓存才会触发重新生成。
|
||
|
||
共享目录包含如下产物,**每个插件 obj 不再复制这些大文件**:
|
||
|
||
| 路径 | 用途 |
|
||
| --- | --- |
|
||
| `ref/Assembly-CSharp.Readable.dll` + XML | 编译器引用,带引用程序集标记,禁止执行 |
|
||
| `lib/Assembly-CSharp.Readable.dll` + `.pdb` | IDE 对应实现和内嵌源码符号 |
|
||
| `lib/sources` | 完整可读源码,供项目浏览与搜索 |
|
||
| `names.json` + `.gz` | 同一名字表的缓存导出,供 reobf 使用 |
|
||
| `navigation.json` | 全量 token 索引;schema 2 路径相对于 `SourceRoot` |
|
||
| `GameSourceNavigation.props` | 共享源码的 MSBuild 文件列表 |
|
||
|
||
每个插件 `obj/<配置>/<框架>/game-sdk`(NuGet 为 `bd2-game-sdk`)只保留小型 `GameSdkIdentity.g.cs`、`shared-sdk.txt`、导航导入 `.props` 和锁文件。首次构建通过生成的指针设置引用路径,后续 IDE 加载直接导入共享配置;`Game Sources` 仍可浏览搜索。旧布局的大文件副本在成功准备后自动清理。
|
||
|
||
`lib` 中的程序集仅供开发导航,**不要部署或执行**。这里展示的是当前 DLL 的反编译源码,局部变量名和语法可能与原始工程不同。导航 PDB 对应可读程序集,不能用于真实混淆游戏 DLL 的逐行调试。所有开发产物都不会复制到游戏部署目录。
|
||
|
||
## 插件代码
|
||
|
||
```csharp
|
||
using System;
|
||
using BD2.GameNames;
|
||
using BepInEx;
|
||
using HarmonyLib;
|
||
|
||
[BepInPlugin("example.my-plugin", "My Plugin", "1.0.0")]
|
||
public sealed class MyPlugin : BaseUnityPlugin
|
||
{
|
||
private void Awake()
|
||
{
|
||
try
|
||
{
|
||
Game.Validate(typeof(MyPlugin).Assembly,
|
||
message => Logger.LogInfo(message));
|
||
|
||
var target = Game.Method<IntroUI>(ui => ui.SendMaintenanceInfo(false));
|
||
new Harmony("example.my-plugin").Patch(target,
|
||
prefix: new HarmonyMethod(typeof(MyPlugin), nameof(BeforeMaintenance)));
|
||
}
|
||
catch (Exception exception)
|
||
{
|
||
Logger.LogError("My Plugin initialization failed: " + exception);
|
||
}
|
||
}
|
||
|
||
private static void BeforeMaintenance() { }
|
||
}
|
||
```
|
||
|
||
表达式只取得 `MethodInfo`,不会执行游戏调用;签名由 C# 编译器检查。已知游戏类型优先使用 `typeof`,公开成员优先使用强类型表达式或 `nameof`,可以获得 IDE 补全和编译检查。实际公开调用和字段访问也能直接使用可读名字。私有成员保持原访问性,通过已知类型和映射反射 API 查询:
|
||
|
||
```csharp
|
||
using System.Reflection;
|
||
|
||
var intro = typeof(IntroUI);
|
||
var enter = intro.GetGameMethod("Enter",
|
||
BindingFlags.Instance | BindingFlags.NonPublic,
|
||
null, Type.EmptyTypes, null);
|
||
|
||
var field = typeof(IntroUI).GetGameField("_maintenanceTimeoutCts",
|
||
BindingFlags.Instance | BindingFlags.NonPublic);
|
||
|
||
// nameof 的结果仍是可读字符串,必须传给 GetGameMethod 等运行时 API。
|
||
var maintenance = typeof(IntroUI).GetGameMethod(nameof(IntroUI.SendMaintenanceInfo),
|
||
BindingFlags.Instance | BindingFlags.Public, null, new[] { typeof(bool) }, null);
|
||
|
||
// 对实际协程可读名调用 MemberName,再交给 Unity 的字符串 API。
|
||
owner.StartCoroutine(Game.MemberName(owner.GetType(), "ReadableCoroutineName"));
|
||
```
|
||
|
||
`GetGameMethod/GetGameField/GetGameProperty/GetGameEvent` 采用 .NET 反射约定:查不到返回 null,重载歧义抛出 `AmbiguousMatchException`,null 名字参数抛出 `ArgumentNullException`。`MemberName` 的成员种类使用 `GameMemberKind` 枚举。未知映射保留字面名;普通 UI 文案、`nameof` 字符串和协程常量都不会被 reobf 自动替换。
|
||
|
||
`Game.FindType` 保留给运行时才知道类型名称的查询。只做字符串反射的项目可单独安装 `BD2.GameNames`,启动时用 `Game.ValidateGame(...)`。这种方式没有可读游戏类型引用,也没有编译指纹;使用 SDK 的项目必须用 `Game.Validate(typeof(MyPlugin).Assembly, ...)`。
|
||
|
||
## 构建与部署
|
||
|
||
构建成功后,将 `bin/Release/netstandard2.1/MyPlugin.dll` 和相邻的 **`BD2.GameNames.dll`** 安装到游戏 `BepInEx/plugins`。客户端只安装一份共享库;多个插件应使用同一游戏表。不要部署 `obj`、可读壳、源码、导航 PDB、`GameSdk.dll`、`Mono.Cecil.dll`、`ICSharpCode.Decompiler.dll` 或其他构建工具。
|
||
|
||
最终插件 DLL 已回映射,`obj` 保留可读编译产物和 PDB,最终目录删除改写前的旧 PDB。SDK 包是构建依赖;玩家电脑不需要 .NET 8 SDK 或 NuGet。这里支持 Unity 的 Mono/Managed 客户端,要求存在真实 `Assembly-CSharp.dll`;IL2CPP/AOT 客户端不受支持。
|
||
|
||
| MSBuild 属性 | 用途 |
|
||
| --- | --- |
|
||
| `BD2GameVersion` | 每个插件 csproj 必填的目标游戏版本,例如 `2.35.10`;不从 SDK 包版本或仓库版本自动推断 |
|
||
| `GameDir` | 游戏根目录,本机安装路径(另须在插件 csproj 声明 BD2GameVersion) |
|
||
| `BD2ManagedDir` | 自定义 Managed 路径,默认 `GameDir/BrownDust II_Data/Managed` |
|
||
| `BD2BepInExDir` | 自定义 BepInEx 路径,默认 `GameDir/BepInEx` |
|
||
| `BD2GameSdkCache` | 共享导航缓存目录,可在 `Directory.Build.props` 配置;默认取 `BD2_GAME_SDK_CACHE` 环境变量或用户缓存目录 |
|
||
| `BD2AddBepInExReferences=false` | 使用其他宿主或自己提供 BepInEx/Harmony 引用时关闭自动引用 |
|
||
| `BD2AddUnityReferences=false` | 自己提供 Unity 引用时关闭自动引用 |
|
||
| `BD2GameSdkEnabled=false` | 暂时关闭壳生成与 reobf;只适用于不依赖可读游戏 API 的项目 |
|
||
|
||
额外 Unity 模块或第三方游戏依赖仍以普通 `<Reference>` 添加,并设 `<Private>false</Private>`。强签名插件需要重新签名流程,当前 reobf 明确拒绝。CI 用相同的 PackageReference 和 `GameDir` 参数构建,直接分发 Build 的 DLL 产物;不要把原始 `obj` DLL 放进发布包。
|
||
|
||
## 维护者打包
|
||
|
||
在本仓库运行:
|
||
|
||
```powershell
|
||
.\plugins\GameSdk\Pack.ps1 `
|
||
-GameDir '<当前客户端目录>'
|
||
```
|
||
|
||
默认生成 `.build/nuget/BD2.GameSdk.<版本>.nupkg` 和 `BD2.GameNames.<版本>.nupkg`。工具版本来自 `plugins/PackageMetadata.props`,游戏版本来自根目录 `versions.json`,组合成 `工具版本-game.游戏版本`。可传 `-PackageVersion` 和 `-OutputDirectory`;发布后的同一包版本必须保持内容不可变,有任何变更都递增工具版本。
|
||
|
||
SDK 精确依赖同版本运行时包;两个包中的压缩表都直接来自同一生成结果。工具通过 `dotnet publish` 打包自带 Mono.Cecil,不把 Cecil 作为插件的 NuGet 依赖。包内携带 README、LICENSE、仓库地址和作者信息。本脚本只生成本地包,不上传到任何 NuGet 服务。
|
||
|
||
工具包也包含 Mono.Cecil、ILSpy 和相关 .NET 组件的 MIT 授权原文 `THIRD-PARTY-NOTICES.txt`;第三方组件保持其自身许可。打包后运行 `VerifyPackages.ps1 -GameDir '<客户端目录>'`,它在仓库外的全新目录和 NuGet 缓存中构建示例插件,检查运行时复制、禁止部署的文件、回映射引用、完整内嵌表、源码/PDB 覆盖和重复构建。
|
||
|
||
打包工程在普通 IDE 加载/源码构建时直接引用 `GameNames.csproj`,无需先生成包,也不会到 nuget.org 查找未发布的 `BD2.GameNames`。`Pack.ps1` 用 `BD2Packaging=true` 切换到同版本的精确 NuGet 依赖,并把打包用 `obj/bin` 放在独立 staging 目录,避免 IDE restore 与 pack 互相覆盖依赖资产。不要直接对打包 `.csproj` 执行 `dotnet pack`,使用脚本才能保证两个包版本配套。
|
||
|
||
仓库中的 `samples/ExamplePlugin/NuGet.Config` 指向 `.build/nuget`,示例保持真正的 NuGet 消费方式;先运行 `Pack.ps1` 再构建示例。复制示例到其他目录时按前文配置自己的包源,不要照搬仓库相对路径。
|
||
|
||
## 仓库源码构建与内嵌表
|
||
|
||
仓库的三个插件与 NuGet 消费项目采用同样的内嵌表流程。开发启动命令保持不变:
|
||
|
||
公共配置见 `plugins/Directory.Build.props`,本机游戏安装位置见不提交的 `plugins/Directory.Build.local.props`。可从 `Directory.Build.local.props.example` 复制并设置 `BD2LocalGameDir`、`BD2CaptureGameDir`、可选的 `BD2GameSdkCache`。配置和目标框架确定后,由 `Directory.Build.targets` 计算中间目录并导入 SDK/版本构建步骤。
|
||
|
||
仓库插件通过普通 `ProjectReference` 使用共享运行时;IDE 设计时加载不会嵌套构建三份运行时。SDK 准备过程按输出目录加锁,导航 `.props` 只在内容改变时原子替换,重复设计时构建不会因重写该文件触发连续项目加载。设计时构建不执行 reobf。
|
||
|
||
```powershell
|
||
# 从 go 目录运行;游戏目录来自 go/config.json
|
||
go run .\cmd\bd2client --dev run
|
||
```
|
||
|
||
直接构建源码插件也只需要游戏目录:
|
||
|
||
```powershell
|
||
dotnet build plugins/LocalIdentity/LocalIdentity.csproj -c Release '-p:GameDir=<客户端目录>'
|
||
```
|
||
|
||
构建工具把表和可读引用生成到按游戏版本分组的共享缓存,插件 `obj` 只保存该目录的指针,再使用共享表做回映射。`GameSdk` 和 `BD2.GameNames` 的唯一名字数据源是仓库内的 `GameNames/Mappings/names.json.gz`。两份程序集内嵌的是同一份压缩字节,不维护第二份映射;共享目录里的表只是可以删除重建的缓存。NuGet 包同样不再包含单独的 `tools/data` 表文件。
|
||
|
||
表包含 `game_version`、完整类型名、成员声明类型/签名/metadata token、参数映射、真实 DLL 的 MVID/SHA-256 和官方映射 SHA-256。构建时先验证 DLL 指纹,仓库构建还核对 `versions.json`;不匹配会要求更新 SDK,不会尝试使用其他版本或猜名字。插件启动时核对表指纹、游戏 DLL 和少量已知条目。
|
||
|
||
## 更新游戏版本的名字表
|
||
|
||
只有维护者更新 SDK 名字数据时需要官方 `.obfuscate`。先更新仓库 `versions.json`,然后执行:
|
||
|
||
```powershell
|
||
.\plugins\GameSdk\UpdateNames.ps1 `
|
||
-GameDir '<新版本客户端目录>' `
|
||
-GameMapping '<ObfuscationTranslation_新版本.obfuscate>'
|
||
```
|
||
|
||
脚本从官方映射和真实元数据生成表,完成全量壳转换验证后才替换 `plugins/GameNames/Mappings/names.json.gz`。提交这份表与对应版本变更;重新构建插件并发布新版本配套包。更新过程中产生的明文表和壳留在 `.build/names-update`,不作为源码提交。
|
||
|
||
`tools/python/deobfuscate_client_source.py` 继续生成阅读镜像;程序集映射使用这里的元数据名字表,保留作用域、泛型位置和重载签名。官方映射冲突或可读签名冲突会报错。
|
||
|
||
工具也提供独立命令:
|
||
|
||
```powershell
|
||
dotnet build plugins/GameSdk/GameSdk.csproj -c Release
|
||
$tool = 'plugins/GameSdk/bin/Release/net8.0/GameSdk.dll'
|
||
dotnet $tool prepare-embedded '<Assembly-CSharp.dll>' '<插件 obj 指针目录>' --game-version 2.35.10
|
||
dotnet $tool export-names '<导出的 names.json>'
|
||
dotnet $tool reobf '<names.json>' '<可读插件.dll>' '<运行插件.dll>' '<Assembly-CSharp.dll>' '<BepInEx/core>'
|
||
dotnet $tool verify '<names.json>' '<运行插件.dll>' '<Assembly-CSharp.dll>'
|
||
dotnet $tool verify-runtime '<names.json>' '<BD2.GameNames.dll>'
|
||
dotnet $tool verify-navigation '<生成的 SDK 目录>'
|
||
dotnet $tool self-test
|
||
```
|
||
|
||
手动编译通过 `shared-sdk.txt` 找到共享目录,引用其中 `ref/Assembly-CSharp.Readable.dll`,并编译指针目录中的 `GameSdkIdentity.g.cs`;共享目录相邻 `lib` 供 IDE 查找。`verify-navigation` 全量检查 PE/PDB 身份、内嵌/本地源码校验和、类型文档和全部方法体的符号。
|
||
|
||
`self-test` 生成合成游戏 DLL,验证重载、泛型、继承、嵌套/编译器生成类型、私有成员、事件、参数、表达式、字符串不变和版本拒绝。`.build/game-sdk-tests` 仅保存每次自测的临时产物,不参与 SDK 构建、源码导航或客户端启动,用完可以删除;下次自测会重新生成。`VerifyPackages.ps1` 验证仓库外 NuGet 项目及完整源码/PDB 覆盖。实际 Unity/Harmony 行为需在游戏启动后检查日志。
|