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。