diff --git a/plugins/CaptureEnvironment/CaptureEnvironment.csproj b/plugins/CaptureEnvironment/CaptureEnvironment.csproj
index e2fcd7a..b0fb0df 100644
--- a/plugins/CaptureEnvironment/CaptureEnvironment.csproj
+++ b/plugins/CaptureEnvironment/CaptureEnvironment.csproj
@@ -6,6 +6,7 @@
BD2CaptureEnvironment
Bd2CaptureEnvironment
capture_environment
+ 2.35.10
diff --git a/plugins/Directory.Build.props b/plugins/Directory.Build.props
index f787693..205933d 100644
--- a/plugins/Directory.Build.props
+++ b/plugins/Directory.Build.props
@@ -17,5 +17,6 @@
$(BD2PluginsDir)GameSdk/bin/$(BD2SdkConfiguration)/net8.0/GameSdk.dll
game-sdk
$(BD2_GAME_SDK_CACHE)
+ $(BD2RepositoryDir).build/game-sdk
diff --git a/plugins/GameSdk.targets b/plugins/GameSdk.targets
index cb271d4..bf8fad3 100644
--- a/plugins/GameSdk.targets
+++ b/plugins/GameSdk.targets
@@ -3,19 +3,28 @@
- $(BD2SdkDir)\ref\Assembly-CSharp.Readable.dll
+ $(BD2SharedSdkDir)/ref/Assembly-CSharp.Readable.dll
false
+
-
+
+
+
+
+
+ $(BD2SharedSdkDir)/ref/Assembly-CSharp.Readable.dll
+ false
+
+
-
+
diff --git a/plugins/GameSdk/Pack.ps1 b/plugins/GameSdk/Pack.ps1
index 3716f44..4334f1a 100644
--- a/plugins/GameSdk/Pack.ps1
+++ b/plugins/GameSdk/Pack.ps1
@@ -37,8 +37,13 @@ $toolVersion = $PackageVersion.Split('-')[0]
Invoke-Dotnet @('publish', (Join-Path $PSScriptRoot 'GameSdk.csproj'), '-c', 'Release', '--nologo', '-o', $tool, "-p:Version=$toolVersion", '-p:UseAppHost=false')
$assembly = Join-Path $GameDir 'BrownDust II_Data\Managed\Assembly-CSharp.dll'
$toolDll = Join-Path $tool 'GameSdk.dll'
-Invoke-Dotnet @($toolDll, 'prepare-embedded', $assembly, $sdk, $VersionConfig)
-$table = Join-Path $sdk 'names.json'
+$previousSdkCache = $env:BD2_GAME_SDK_CACHE
+try {
+ if (-not $env:BD2_GAME_SDK_CACHE) { $env:BD2_GAME_SDK_CACHE = Join-Path $repository '.build/game-sdk' }
+ Invoke-Dotnet @($toolDll, 'prepare-embedded', $assembly, $sdk, $VersionConfig)
+} finally { $env:BD2_GAME_SDK_CACHE = $previousSdkCache }
+$sharedSdk = [IO.File]::ReadAllText((Join-Path $sdk 'shared-sdk.txt')).Trim()
+$table = Join-Path $sharedSdk 'names.json'
$runtimeProject = Join-Path $PSScriptRoot '..\GameNames\GameNames.csproj'
Invoke-Dotnet @('pack', $runtimeProject, '-c', 'Release', '--nologo', '-o', $OutputDirectory,
"-p:Version=$PackageVersion", "-p:GameNamesTable=$table", "-p:BaseIntermediateOutputPath=$runtimeObj/", "-p:OutputPath=$runtimeBin/")
diff --git a/plugins/GameSdk/Package/build/BD2.GameSdk.targets b/plugins/GameSdk/Package/build/BD2.GameSdk.targets
index 1e28619..7ebc5c5 100644
--- a/plugins/GameSdk/Package/build/BD2.GameSdk.targets
+++ b/plugins/GameSdk/Package/build/BD2.GameSdk.targets
@@ -10,7 +10,7 @@
- $(BD2SdkDir)/ref/Assembly-CSharp.Readable.dll
+ $(BD2SharedSdkDir)/ref/Assembly-CSharp.Readable.dll
false
$(BD2BepInExDir)/core/BepInEx.dllfalse
@@ -21,14 +21,23 @@
+
-
+
+
+
+
+
+ $(BD2SharedSdkDir)/ref/Assembly-CSharp.Readable.dll
+ false
+
+
-
+
diff --git a/plugins/GameSdk/Program.cs b/plugins/GameSdk/Program.cs
index 8441d4f..f6a136f 100644
--- a/plugins/GameSdk/Program.cs
+++ b/plugins/GameSdk/Program.cs
@@ -19,7 +19,18 @@ internal static class Program
switch (args.FirstOrDefault())
{
case "prepare" when args.Length == 5: Prepare(args[1], args[2], args[3], args[4]); break;
- case "prepare-embedded" when args.Length is 3 or 4: PrepareEmbedded(args[1], args[2], args.Length == 4 ? args[3] : null); break;
+ case "prepare-embedded" when args.Length is >= 3 and <= 6:
+ var prepareOptions = args.Skip(3).ToList();
+ string expectedGameVersion = null;
+ int versionOption = prepareOptions.IndexOf("--game-version");
+ if (versionOption >= 0)
+ {
+ if (versionOption + 1 >= prepareOptions.Count) throw new ArgumentException("--game-version requires a version");
+ expectedGameVersion = prepareOptions[versionOption + 1];
+ prepareOptions.RemoveRange(versionOption, 2);
+ }
+ if (prepareOptions.Count > 1) throw new ArgumentException("Invalid prepare-embedded options");
+ PrepareEmbedded(args[1], args[2], prepareOptions.FirstOrDefault(), expectedGameVersion); break;
case "export-names" when args.Length == 2: ExportNames(args[1]); break;
case "names" when args.Length == 5: GenerateNames(args[1], args[2], args[3], args[4]); break;
case "shell" when args.Length == 4: GenerateShell(args[1], args[2], args[3]); break;
@@ -64,85 +75,88 @@ internal static class Program
gzip.CopyTo(destination);
}
- internal static void PrepareEmbedded(string assembly, string output, string versions = null)
+ internal static string ResolveSdkDirectory(string directory)
{
- byte[] compressed = EmbeddedNames();
+ string pointer = Path.Combine(directory, "shared-sdk.txt");
+ return File.Exists(pointer) ? File.ReadAllText(pointer).Trim() : Path.GetFullPath(directory);
+ }
+
+ internal static void PrepareEmbedded(string assembly, string output, string versions = null, string expectedGameVersion = null) =>
+ PrepareShared(EmbeddedNames(), assembly, output, versions, expectedGameVersion);
+
+ internal static void PreparePackage(string compressedTable, string assembly, string output) =>
+ PrepareShared(File.ReadAllBytes(compressedTable), assembly, output, null, null);
+
+ private static void PrepareShared(byte[] compressed, string assembly, string output, string versions, string expectedGameVersion)
+ {
+ output = Path.GetFullPath(output);
Directory.CreateDirectory(output);
- // IDE loads and command-line builds may prepare the same obj directory.
- // Serialize the whole transaction, including the exported table and props.
using var outputLock = AcquireCacheLock(Path.Combine(output, "prepare.lock"), "Waiting for this project's game SDK preparation to finish...");
- string tablePath = Path.Combine(output, "names.json"), compressedPath = tablePath + ".gz";
- // Keep the cache copy exact; the only authoritative source is the embedded resource.
- if (!File.Exists(compressedPath) || !File.ReadAllBytes(compressedPath).SequenceEqual(compressed))
- File.WriteAllBytes(compressedPath, compressed);
- PreparePackage(compressedPath, assembly, output);
+ NameTable table;
+ using (var gzip = new GZipStream(new MemoryStream(compressed), CompressionMode.Decompress))
+ table = JsonSerializer.Deserialize(gzip, Json);
+ string gameVersion = table.game_version;
+ if (string.IsNullOrWhiteSpace(gameVersion) || gameVersion.Any(c => !(char.IsLetterOrDigit(c) || c is '.' or '-' or '_')))
+ throw new InvalidDataException("Invalid game_version in names table");
+ if (expectedGameVersion != null && expectedGameVersion != gameVersion)
+ throw new InvalidDataException($"Plugin requires game {expectedGameVersion}, but SDK names target game {gameVersion}. Install the matching SDK package.");
+ if (Hash(assembly) != table.assembly_sha256)
+ throw new InvalidDataException("Game DLL does not match SDK names for game " + gameVersion);
if (versions != null)
{
using var config = JsonDocument.Parse(File.ReadAllText(versions));
string expected = config.RootElement.GetProperty("game_version").GetString();
- string actual = ReadTable(tablePath).game_version;
- if (expected != actual) throw new InvalidDataException($"Repository game_version {expected} does not match SDK embedded names {actual}; update the SDK names table for this version");
+ if (expected != gameVersion) throw new InvalidDataException($"Repository game_version {expected} does not match SDK embedded names {gameVersion}");
}
- }
- internal static void PreparePackage(string compressedTable, string assembly, string output)
- {
- Directory.CreateDirectory(output);
- string tablePath = Path.Combine(output, "names.json"), shellPath = Path.Combine(output, ShellName + ".dll");
- string identityPath = Path.Combine(output, "GameSdkIdentity.g.cs"), readyPath = Path.Combine(output, "package-ready.txt");
- string pdbPath = Path.Combine(output, "lib", ShellName + ".pdb"), navigationPath = Path.Combine(output, "navigation.json");
- string inputs = Hash(compressedTable) + "|" + GeneratorStamp() + "|" + DependencyStamp(assembly);
- string[] required = { tablePath, shellPath, identityPath, pdbPath, navigationPath,
- Path.Combine(output, "ref", ShellName + ".dll"), Path.Combine(output, "ref", ShellName + ".xml"),
- Path.Combine(output, "lib", ShellName + ".dll"), Path.Combine(output, "lib", ShellName + ".xml") };
- string Outputs() => string.Join("|", required.Select(Hash));
- if (File.Exists(readyPath) && required.All(File.Exists) &&
- File.ReadAllText(readyPath) == inputs + "|" + Outputs())
- {
- WriteNavigationItems(output);
- return;
- }
- File.Delete(readyPath);
- using (var gzip = new GZipStream(File.OpenRead(compressedTable), CompressionMode.Decompress))
- using (var destination = File.Create(tablePath)) gzip.CopyTo(destination);
- GenerateCachedShell(tablePath, assembly, shellPath);
- WriteIdentity(tablePath, identityPath);
- WriteNavigationItems(output);
- File.WriteAllText(readyPath, inputs + "|" + Outputs());
- }
-
- private static void GenerateCachedShell(string tablePath, string assembly, string destination)
- {
string cacheRoot = Environment.GetEnvironmentVariable("BD2_GAME_SDK_CACHE");
if (string.IsNullOrEmpty(cacheRoot)) cacheRoot = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "BD2", "GameSdk", "navigation");
- // Changes to any managed dependency can affect decompilation and semantic references.
- string dependencyDirectory = Path.GetDirectoryName(Path.GetFullPath(assembly));
- string key = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(Hash(tablePath) + "|" + GeneratorStamp() + "|" + DependencyStamp(assembly)))).ToLowerInvariant();
- string cache = Path.Combine(Path.GetFullPath(cacheRoot), key);
- Directory.CreateDirectory(cache);
- string cachedShell = Path.Combine(cache, ShellName + ".dll"), ready = Path.Combine(cache, "ready.txt");
- using (AcquireCacheLock(Path.Combine(cache, "generation.lock")))
+ string tableHash = Convert.ToHexString(SHA256.HashData(compressed)).ToLowerInvariant();
+ string inputs = tableHash + "|" + GeneratorStamp() + "|" + DependencyStamp(assembly);
+ // Game version is the public grouping; the fingerprint prevents incompatible
+ // tools or binaries within one game version from overwriting active references.
+ string key = Convert.ToHexString(SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(inputs))).ToLowerInvariant();
+ string shared = Path.Combine(Path.GetFullPath(cacheRoot), gameVersion, key);
+ Directory.CreateDirectory(shared);
+ using (AcquireCacheLock(Path.Combine(shared, "generation.lock")))
{
- string[] relativeFiles = { ShellName + ".dll", ShellName + ".xml", "navigation.json", "ref/" + ShellName + ".dll", "ref/" + ShellName + ".xml",
- "lib/" + ShellName + ".dll", "lib/" + ShellName + ".pdb", "lib/" + ShellName + ".xml", "lib/navigation.json" };
- string[] files = relativeFiles.Select(p => Path.Combine(cache, p)).ToArray();
- bool complete = File.Exists(ready) && files.All(File.Exists) && File.ReadAllText(ready) == string.Join("|", files.Select(Hash));
- if (!complete)
+ string tablePath = Path.Combine(shared, "names.json"), ready = Path.Combine(shared, "ready.txt");
+ string[] relativeFiles = { "names.json", "names.json.gz", ShellName + ".dll", ShellName + ".xml", "navigation.json",
+ "ref/" + ShellName + ".dll", "ref/" + ShellName + ".xml", "lib/" + ShellName + ".dll", "lib/" + ShellName + ".pdb", "lib/" + ShellName + ".xml", "lib/navigation.json" };
+ string[] files = relativeFiles.Select(p => Path.Combine(shared, p)).ToArray();
+ string StampFiles() => inputs + "|" + string.Join("|", files.Select(Hash));
+ if (!File.Exists(ready) || !files.All(File.Exists) || File.ReadAllText(ready) != StampFiles())
{
File.Delete(ready);
- GenerateShell(tablePath, assembly, cachedShell);
- File.WriteAllText(ready, string.Join("|", files.Select(Hash)));
+ File.WriteAllBytes(tablePath + ".gz", compressed);
+ using (var gzip = new GZipStream(new MemoryStream(compressed), CompressionMode.Decompress))
+ using (var destination = File.Create(tablePath)) gzip.CopyTo(destination);
+ GenerateShell(tablePath, assembly, Path.Combine(shared, ShellName + ".dll"));
+ File.WriteAllText(ready, StampFiles());
}
- string destinationDirectory = Path.GetDirectoryName(Path.GetFullPath(destination));
- foreach (string relative in relativeFiles)
- {
- if (relative == ShellName + ".xml" || relative == "lib/navigation.json") continue;
- string target = Path.Combine(destinationDirectory, relative);
- Directory.CreateDirectory(Path.GetDirectoryName(target));
- File.Copy(Path.Combine(cache, relative), target, overwrite: true);
- }
- File.Delete(Path.ChangeExtension(destination, ".xml"));
- File.Delete(Path.Combine(destinationDirectory, "lib", "navigation.json"));
+ WriteNavigationItems(shared);
+ WriteGeneratedFile(Path.Combine(output, "GameSdkIdentity.g.cs"), System.Text.Encoding.UTF8.GetBytes(
+ "// Generated from the shared names table.\n[assembly: System.Reflection.AssemblyMetadataAttribute(\"BD2.GameNames\", " + JsonSerializer.Serialize(Stamp(table)) + ")]\n"));
+ WriteGeneratedFile(Path.Combine(output, "shared-sdk.txt"), System.Text.Encoding.UTF8.GetBytes(shared));
+ var properties = new System.Xml.Linq.XElement("PropertyGroup",
+ new System.Xml.Linq.XElement("BD2SharedSdkDir", shared));
+ var import = new System.Xml.Linq.XElement("Import",
+ new System.Xml.Linq.XAttribute("Project", Path.Combine(shared, "GameSourceNavigation.props")),
+ new System.Xml.Linq.XAttribute("Condition", "Exists('" + Path.Combine(shared, "GameSourceNavigation.props") + "')"));
+ using var props = new MemoryStream();
+ new System.Xml.Linq.XDocument(new System.Xml.Linq.XElement("Project", properties, import)).Save(props);
+ WriteGeneratedFile(Path.Combine(output, "GameSourceNavigation.props"), props.ToArray());
}
+ // Remove only obsolete generated copies in this SDK obj directory after
+ // publishing a complete shared reference. Never touch hand-written files.
+ foreach (string name in new[] { "names.json", "names.json.gz", ShellName + ".dll", ShellName + ".xml", "navigation.json", "package-ready.txt", "ready.txt" })
+ File.Delete(Path.Combine(output, name));
+ foreach (string folder in new[] { "ref", "lib", "runtime-obj", "runtime-bin" })
+ {
+ string target = Path.GetFullPath(Path.Combine(output, folder));
+ if (Path.GetDirectoryName(target) != output) throw new InvalidDataException("Invalid generated cleanup path");
+ if (Directory.Exists(target)) Directory.Delete(target, recursive: true);
+ }
+ Console.WriteLine($"Shared game SDK {gameVersion}: {shared}");
}
private static FileStream AcquireCacheLock(string path, string message = "Waiting for the shared game source-navigation cache to finish generating...")
diff --git a/plugins/GameSdk/README.md b/plugins/GameSdk/README.md
index 9337ddc..571fda7 100644
--- a/plugins/GameSdk/README.md
+++ b/plugins/GameSdk/README.md
@@ -36,6 +36,7 @@ Visual Studio / Rider 也可在 NuGet 包管理界面添加该源,打开“包
netstandard2.1
latest
MyPlugin
+ 2.35.10
2.35.10
```
-保持这个路径稳定;更改路径、工具或游戏 DLL 后首次构建会重新准备缓存。删除 `obj` 不影响共享源码缓存;缺失的缓存源码可在下次准备时从 PDB 恢复。可删除共享缓存来强制重新生成,它不存放手写源码。
+这个值与 SDK/API 工具版本(如 `0.2.1`)不同。构建核对声明值、SDK 内嵌表、真实游戏 DLL 指纹,以及仓库 `versions.json`(仓库插件)。缺少声明或版本不匹配会报错;不会自动选用其他版本。NuGet 项目仍需安装带对应游戏版本的包。
-插件 `obj/<配置>/<框架>/bd2-game-sdk` 中的布局遵循 .NET 的引用程序集查找约定(仓库插件使用 `game-sdk` 子目录):
+共享目录按 `<缓存根>/<游戏版本>/<内容指纹>/` 存放。同版本的三个插件、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` | 编译器引用,带引用程序集标记,禁止执行 |
-| `lib/Assembly-CSharp.Readable.dll` + `.pdb` | IDE 对应实现和导航符号;源码内嵌,不依赖联网下载 |
-| `GameSourceNavigation.props` | 把共享源码链接到项目浏览器 |
-| `navigation.json` | 元数据 token 到文件/行列的全量索引、覆盖统计与 DLL/PDB 指纹;schema 2 的文件路径相对于 `SourceRoot` |
+| `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 文件列表 |
-`lib` 中的程序集仅供开发导航,**不要部署或执行**。这里展示的是从当前 DLL 重建的源码,局部变量名和语法可能与开发商原始工程不同。导航 PDB 对应可读程序集;它不能用来在真实混淆游戏 DLL 上逐行调试。
+每个插件 `obj/<配置>/<框架>/game-sdk`(NuGet 为 `bd2-game-sdk`)只保留小型 `GameSdkIdentity.g.cs`、`shared-sdk.txt`、导航导入 `.props` 和锁文件。首次构建通过生成的指针设置引用路径,后续 IDE 加载直接导入共享配置;`Game Sources` 仍可浏览搜索。旧布局的大文件副本在成功准备后自动清理。
+
+`lib` 中的程序集仅供开发导航,**不要部署或执行**。这里展示的是当前 DLL 的反编译源码,局部变量名和语法可能与原始工程不同。导航 PDB 对应可读程序集,不能用于真实混淆游戏 DLL 的逐行调试。所有开发产物都不会复制到游戏部署目录。
## 插件代码
@@ -164,7 +172,8 @@ owner.StartCoroutine(Game.MemberName(owner.GetType(), "ReadableCoroutineName"));
| MSBuild 属性 | 用途 |
| --- | --- |
-| `GameDir` | 游戏根目录,常规项目唯一必填配置 |
+| `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` 环境变量或用户缓存目录 |
@@ -212,7 +221,7 @@ go run .\cmd\bd2client --dev run
dotnet build plugins/LocalIdentity/LocalIdentity.csproj -c Release '-p:GameDir=<客户端目录>'
```
-构建工具从自身资源导出表到 `obj` 缓存,用本机真实 DLL 的元数据生成壳,再做回映射。`GameSdk` 和 `BD2.GameNames` 的唯一名字数据源是仓库内的 `GameNames/Mappings/names.json.gz`。两份程序集内嵌的是同一份压缩字节,不维护第二份映射;`obj` 里的表只是可以删除重建的缓存。NuGet 包同样不再包含单独的 `tools/data` 表文件。
+构建工具把表和可读引用生成到按游戏版本分组的共享缓存,插件 `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 和少量已知条目。
@@ -235,7 +244,7 @@ dotnet build plugins/LocalIdentity/LocalIdentity.csproj -c Release '-p:GameDir=<
```powershell
dotnet build plugins/GameSdk/GameSdk.csproj -c Release
$tool = 'plugins/GameSdk/bin/Release/net8.0/GameSdk.dll'
-dotnet $tool prepare-embedded '' '<输出目录>'
+dotnet $tool prepare-embedded '' '<插件 obj 指针目录>' --game-version 2.35.10
dotnet $tool export-names '<导出的 names.json>'
dotnet $tool reobf '' '<可读插件.dll>' '<运行插件.dll>' '' ''
dotnet $tool verify '' '<运行插件.dll>' ''
@@ -244,6 +253,6 @@ dotnet $tool verify-navigation '<生成的 SDK 目录>'
dotnet $tool self-test
```
-手动编译须引用 `ref/Assembly-CSharp.Readable.dll` 并编译同目录 `GameSdkIdentity.g.cs`,保留相邻 `lib` 以供 IDE 查找。`verify-navigation` 全量检查 PE/PDB 身份、内嵌/本地源码校验和、类型文档和全部方法体的符号。
+手动编译通过 `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 行为需在游戏启动后检查日志。
diff --git a/plugins/GameSdk/SourceNavigation.cs b/plugins/GameSdk/SourceNavigation.cs
index aeedc13..1ba4350 100644
--- a/plugins/GameSdk/SourceNavigation.cs
+++ b/plugins/GameSdk/SourceNavigation.cs
@@ -146,6 +146,7 @@ internal static class SourceNavigation
internal static void Verify(string sdkDirectory)
{
+ sdkDirectory = Program.ResolveSdkDirectory(sdkDirectory);
string library = Path.Combine(sdkDirectory, "lib", Program.ShellName + ".dll"), pdbPath = Path.ChangeExtension(library, ".pdb");
var manifest = JsonSerializer.Deserialize(File.ReadAllText(Path.Combine(sdkDirectory, "navigation.json")));
if (Program.Hash(library) != manifest.AssemblySha256 || Program.Hash(pdbPath) != manifest.PdbSha256)
diff --git a/plugins/GameSdk/VerifyPackages.ps1 b/plugins/GameSdk/VerifyPackages.ps1
index 9712508..12948a9 100644
--- a/plugins/GameSdk/VerifyPackages.ps1
+++ b/plugins/GameSdk/VerifyPackages.ps1
@@ -14,7 +14,8 @@ if (-not $PackageVersion) {
$PackageDirectory = [IO.Path]::GetFullPath($PackageDirectory)
# A fresh external directory and package cache prevent repository imports and stale
# same-version development packages from making an invalid package appear to work.
-$workspace = Join-Path ([IO.Path]::GetTempPath()) ('BD2 NuGet Test ' + [guid]::NewGuid().ToString('N'))
+$verificationRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../.build/nuget-tests'))
+$workspace = Join-Path $verificationRoot ([guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $workspace | Out-Null
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'samples\ExamplePlugin\ExamplePlugin.csproj'), (Join-Path $PSScriptRoot 'samples\ExamplePlugin\Plugin.cs') -Destination $workspace
$project = Join-Path $workspace 'ExamplePlugin.csproj'
@@ -27,7 +28,8 @@ function Invoke-Dotnet([string[]]$Arguments) {
& dotnet @Arguments
if ($LASTEXITCODE -ne 0) { throw "dotnet failed with exit code $LASTEXITCODE" }
}
-Invoke-Dotnet @('build', $project, '-c', 'Release', '--nologo', "-p:GameDir=$GameDir")
+$sharedCacheRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../.build/game-sdk'))
+Invoke-Dotnet @('build', $project, '-c', 'Release', '--nologo', "-p:GameDir=$GameDir", "-p:BD2GameSdkCache=$sharedCacheRoot")
$output = Join-Path $workspace 'bin\Release\netstandard2.1'
$runtime = Join-Path $output 'BD2.GameNames.dll'
foreach ($required in @('BD2.GameNames.dll', 'ExamplePlugin.dll')) {
@@ -38,16 +40,43 @@ foreach ($forbidden in @('Assembly-CSharp.Readable.dll', 'Assembly-CSharp.Readab
}
$tool = Join-Path $workspace "packages\bd2.gamesdk\$PackageVersion\tools\net8.0\GameSdk.dll"
$sdk = Join-Path $workspace 'obj\Release\netstandard2.1\bd2-game-sdk'
-$table = Join-Path $sdk 'names.json'
+$sharedSdk = [IO.File]::ReadAllText((Join-Path $sdk 'shared-sdk.txt')).Trim()
+$table = Join-Path $sharedSdk 'names.json'
+if ((Get-ChildItem -LiteralPath $sdk -Recurse -File | Measure-Object Length -Sum).Sum -gt 65536) {
+ throw 'SDK obj still contains large copied artifacts instead of shared references.'
+}
+[xml]$exampleProject = Get-Content -LiteralPath $project -Raw
+if ([IO.Path]::GetFileName([IO.Path]::GetDirectoryName($sharedSdk)) -ne $exampleProject.Project.PropertyGroup.BD2GameVersion) {
+ throw 'Shared SDK directory is not grouped by the plugin-declared game version.'
+}
+$sharedReadyTimestamp = (Get-Item -LiteralPath (Join-Path $sharedSdk 'ready.txt')).LastWriteTimeUtc
Invoke-Dotnet @($tool, 'verify-runtime', $table, $runtime)
Invoke-Dotnet @($tool, 'verify-navigation', $sdk)
Invoke-Dotnet @($tool, 'verify', $table, (Join-Path $output 'ExamplePlugin.dll'), (Join-Path $GameDir 'BrownDust II_Data\Managed\Assembly-CSharp.dll'))
# Repeated builds must always transform the readable obj DLL, never reprocess bin.
$navigationProps = Join-Path $sdk 'GameSourceNavigation.props'
$navigationTimestamp = (Get-Item -LiteralPath $navigationProps).LastWriteTimeUtc
-Invoke-Dotnet @('build', $project, '-c', 'Release', '--no-restore', '--nologo', "-p:GameDir=$GameDir")
+Invoke-Dotnet @('build', $project, '-c', 'Release', '--no-restore', '--nologo', "-p:GameDir=$GameDir", "-p:BD2GameSdkCache=$sharedCacheRoot")
if ((Get-Item -LiteralPath $navigationProps).LastWriteTimeUtc -ne $navigationTimestamp) {
throw 'Unchanged navigation props were rewritten; this triggers repeated IDE reloads.'
}
+if ((Get-Item -LiteralPath (Join-Path $sharedSdk 'ready.txt')).LastWriteTimeUtc -ne $sharedReadyTimestamp) {
+ throw 'Repeated build unexpectedly regenerated the shared SDK.'
+}
Invoke-Dotnet @($tool, 'verify', $table, (Join-Path $output 'ExamplePlugin.dll'), (Join-Path $GameDir 'BrownDust II_Data\Managed\Assembly-CSharp.dll'))
+foreach ($invalidVersion in @('', '0.0.0')) {
+ $rejectedBuild = & dotnet build $project -c Release --no-restore --nologo "-p:GameDir=$GameDir" "-p:BD2GameSdkCache=$sharedCacheRoot" "-p:BD2GameVersion=$invalidVersion" 2>&1 | Out-String
+ if ($LASTEXITCODE -eq 0 -or ($invalidVersion -eq '' -and -not $rejectedBuild.Contains('BD2GameVersion')) -or
+ ($invalidVersion -ne '' -and -not $rejectedBuild.Contains('Plugin requires game 0.0.0'))) {
+ throw "Missing/mismatched game version was not rejected: $invalidVersion"
+ }
+}
Write-Host "Verified external PackageReference consumer, complete source navigation, shared runtime and incremental rebuild: $workspace"
+# Successful probes are disposable. Retain a failed probe for diagnostics, but
+# do not accumulate package caches after successful verification.
+$resolvedProbe = [IO.Path]::GetFullPath($workspace)
+if ([IO.Path]::GetDirectoryName($resolvedProbe) -ne $verificationRoot -or
+ [IO.Path]::GetFileName($resolvedProbe) -notmatch '^[a-f0-9]{32}$') {
+ throw 'Invalid verification cleanup path.'
+}
+Remove-Item -LiteralPath $resolvedProbe -Recurse -Force
diff --git a/plugins/GameSdk/samples/ExamplePlugin/ExamplePlugin.csproj b/plugins/GameSdk/samples/ExamplePlugin/ExamplePlugin.csproj
index 2479114..19f230e 100644
--- a/plugins/GameSdk/samples/ExamplePlugin/ExamplePlugin.csproj
+++ b/plugins/GameSdk/samples/ExamplePlugin/ExamplePlugin.csproj
@@ -3,6 +3,7 @@
netstandard2.1
latest
ExamplePlugin
+ 2.35.10
diff --git a/plugins/LocalIdentity/LocalIdentity.csproj b/plugins/LocalIdentity/LocalIdentity.csproj
index 410367c..b42df11 100644
--- a/plugins/LocalIdentity/LocalIdentity.csproj
+++ b/plugins/LocalIdentity/LocalIdentity.csproj
@@ -6,6 +6,7 @@
BD2LocalIdentity
Bd2LocalIdentity
local_identity
+ 2.35.10
diff --git a/plugins/LoginUI/LoginUI.csproj b/plugins/LoginUI/LoginUI.csproj
index 6e77449..a9a6103 100644
--- a/plugins/LoginUI/LoginUI.csproj
+++ b/plugins/LoginUI/LoginUI.csproj
@@ -6,6 +6,7 @@
BD2LoginUI
Bd2LoginUI
login_ui
+ 2.35.10
diff --git a/plugins/README.md b/plugins/README.md
index 2eb6c8e..6f3f063 100644
--- a/plugins/README.md
+++ b/plugins/README.md
@@ -11,7 +11,7 @@
SDK 和共享运行时已内嵌当前游戏版本的同一份名字表。源码构建只需客户端目录,无需提供官方映射。`.NET 8 SDK` 构建工具;插件为 `netstandard2.1`,运行时为 `netstandard2.0`。构建自动生成可读引用、完整源码和内嵌源码 PDB,编译插件、执行 reobf 并验证真实 DLL 的引用。首次生成耗时数分钟,三个插件和 Debug/Release 共用本机缓存;IDE 转到定义可以查看方法体,`Game Sources` 文件夹提供全量浏览与搜索。IDE 设置见 [完整源码导航](GameSdk/README.md#完整源码导航)。
-仓库公共路径和默认值集中在 `plugins/Directory.Build.props`;配置/框架相关的输出路径与构建步骤由 `Directory.Build.targets` 自动导入。三个插件 `.csproj` 只保留插件属性和需要的程序集引用。首次开发复制 `Directory.Build.local.props.example` 为 `Directory.Build.local.props`,填写 `BD2LocalGameDir` 和可选的 `BD2CaptureGameDir`;本机文件被 Git 忽略。配置后 IDE 与命令行共用这些路径,不需要在三个子目录分别放一份 `Directory.Build.props`。命令行 `-p:GameDir=...` 仍可覆盖本机设置。
+仓库公共路径和默认值集中在 `plugins/Directory.Build.props`;配置/框架相关的输出路径与构建步骤由 `Directory.Build.targets` 自动导入。三个插件 `.csproj` 保留插件属性、明确的 `2.35.10` 和需要的程序集引用。目标游戏版本必须由每个插件声明,缺失或与 SDK 表不符时构建报错。首次开发复制 `Directory.Build.local.props.example` 为 `Directory.Build.local.props`,填写 `BD2LocalGameDir` 和可选的 `BD2CaptureGameDir`;本机文件被 Git 忽略。配置后 IDE 与命令行共用这些路径,不需要在三个子目录分别放一份 `Directory.Build.props`。命令行 `-p:GameDir=...` 仍可覆盖本机设置。
`BD2GameSdkCache` 可放在本机 props 中指定共享源码缓存。特殊目录布局可以设置 `BD2ManagedDir`、`BD2BepInExDir`;`BD2SdkDir` 可覆盖中间产物目录。新增子目录 `Directory.Build.props` 会阻断 MSBuild 自动查找父文件,确有需要时应显式导入公共 `plugins/Directory.Build.props`。
@@ -21,7 +21,7 @@ dotnet build .\plugins\LoginUI\LoginUI.csproj -c Release -p:GameDir="<本地服
dotnet build .\plugins\CaptureEnvironment\CaptureEnvironment.csproj -c Release -p:GameDir="<原版客户端目录>"
```
-`bin` 中的插件是回映射后的运行产物,部署时同时复制相邻的 `BD2.GameNames.dll`。壳和明文表缓存在 `obj`,不部署。客户端开发入口 `go run .\cmd\bd2client --dev run` 自动使用内嵌表;发布和原版抓包启动脚本也携带共享库。
+`bin` 中的插件是回映射后的运行产物,部署时同时复制相邻的 `BD2.GameNames.dll`。可读程序集、PDB、源码和名字表统一保存在 `.build/game-sdk/<游戏版本>/<内容指纹>`,各插件 `obj` 只留小型指针和生成配置,不部署。客户端开发入口 `go run .\cmd\bd2client --dev run` 自动使用内嵌表;发布和原版抓包启动脚本也携带共享库。
唯一名字数据源为 `GameNames/Mappings/names.json.gz`。只有维护者更新游戏版本的表时才需要官方 `.obfuscate`,执行 `GameSdk/UpdateNames.ps1`,验证后提交新表,再发布新版本 SDK。