From c4f77881ed6890db508367e44cf725afd33c842a Mon Sep 17 00:00:00 2001 From: Flechazo <2558755403@qq.com> Date: Sat, 3 Oct 2026 23:35:18 +0800 Subject: [PATCH] fix(all): show client startup progress and stabilize SDK cache --- README.md | 7 +- go/cmd/bd2client/dev_command.go | 12 +- go/env.ps1 | 3 + .../GameSdk.NavigationTests.csproj | 13 -- plugins/GameSdk.NavigationTests/Program.cs | 116 ------------------ plugins/GameSdk/GameSdk.csproj | 3 + plugins/GameSdk/README.md | 10 +- 7 files changed, 21 insertions(+), 143 deletions(-) create mode 100644 go/env.ps1 delete mode 100644 plugins/GameSdk.NavigationTests/GameSdk.NavigationTests.csproj delete mode 100644 plugins/GameSdk.NavigationTests/Program.cs diff --git a/README.md b/README.md index ee5173e..915a611 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ ## 开发构建 +外部 PowerShell 终端从 `go` 目录执行 `. .\env.ps1`,将当前终端的 Go 缓存设为 `go/.cache/go-build`,不修改用户级 Go 配置。VS Code 可在工作区 `terminal.integrated.env.windows` 和 `go.toolsEnvVars` 中配置同一路径,让本项目的新终端自动生效。 + ```powershell # Go Push-Location .\go -$env:GOCACHE = (Join-Path $PWD '.cache\go-build') go test ./... go build .\cmd\bd2server go build .\cmd\bd2client @@ -19,17 +20,17 @@ Pop-Location 开发时在两个终端中分别从 `go` 目录运行: ```powershell -$env:GOCACHE = (Join-Path $PWD '.cache\go-build') go run .\cmd\bd2server --dev run ``` ```powershell -$env:GOCACHE = (Join-Path $PWD '.cache\go-build') go run .\cmd\bd2client --dev run ``` 服务端开发入口会自动使用仓库根目录的版本、认证、资源、数据和存档配置;客户端开发入口会直接使用仓库版本清单和插件产物,并从本机开发配置读取游戏目录,随后用 SDK 内嵌的当前版本名字表增量构建两个客户端插件,无需提供官方映射文件。首次开发前创建该配置: +客户端窗口在插件准备完成后打开。首次生成完整游戏源码或 SDK/游戏更新后可能等待数分钟,终端会实时显示构建和反编译进度;后续启动复用共享缓存。 + ```powershell Copy-Item .\go\config.example.json .\go\config.json ``` diff --git a/go/cmd/bd2client/dev_command.go b/go/cmd/bd2client/dev_command.go index 0a739b8..32f275d 100644 --- a/go/cmd/bd2client/dev_command.go +++ b/go/cmd/bd2client/dev_command.go @@ -3,6 +3,7 @@ package main import ( + "bytes" "encoding/json" "errors" "fmt" @@ -177,6 +178,7 @@ func buildDevelopmentPlugins(root, gameDir string) error { filepath.Join(root, "plugins", "LoginUI", "LoginUI.csproj"), } for _, project := range projects { + fmt.Fprintf(os.Stderr, "bd2client: preparing development plugin %s; first-time game source generation may take several minutes\n", filepath.Base(project)) command := exec.Command( "dotnet", "build", project, "-c", "Release", "-p:GameDir="+filepath.Clean(gameDir), @@ -185,9 +187,15 @@ func buildDevelopmentPlugins(root, gameDir string) error { "--nologo", ) command.Dir = root - output, err := command.CombinedOutput() + // SDK preparation can decompile the entire game before the window opens. + // Stream progress immediately, while retaining diagnostics for failures. + var output bytes.Buffer + writer := io.MultiWriter(os.Stderr, &output) + command.Stdout = writer + command.Stderr = writer + err := command.Run() if err != nil { - message := strings.TrimSpace(string(output)) + message := strings.TrimSpace(output.String()) if message == "" { message = err.Error() } diff --git a/go/env.ps1 b/go/env.ps1 new file mode 100644 index 0000000..f194ee1 --- /dev/null +++ b/go/env.ps1 @@ -0,0 +1,3 @@ +# Dot-source in an external PowerShell terminal: . .\env.ps1 +# Applies only to this terminal and child processes; no user-level Go settings. +$env:GOCACHE = Join-Path $PSScriptRoot '.cache\go-build' diff --git a/plugins/GameSdk.NavigationTests/GameSdk.NavigationTests.csproj b/plugins/GameSdk.NavigationTests/GameSdk.NavigationTests.csproj deleted file mode 100644 index ccd6560..0000000 --- a/plugins/GameSdk.NavigationTests/GameSdk.NavigationTests.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - Exe - net8.0 - enable - enable - false - - - - - - diff --git a/plugins/GameSdk.NavigationTests/Program.cs b/plugins/GameSdk.NavigationTests/Program.cs deleted file mode 100644 index c48ec77..0000000 --- a/plugins/GameSdk.NavigationTests/Program.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.Host.Mef; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -if (args.Length == 0) - throw new ArgumentException("Usage: GameSdk.NavigationTests [game-managed-directory] [--embedded-only]"); - -var hiddenSources = new List<(string Original, string Hidden)>(); -try -{ - // Missing local source proves that navigation uses the embedded documents. - if (args.Contains("--embedded-only")) - { - using var manifest = JsonDocument.Parse(File.ReadAllText(Path.Combine(args[0], "navigation.json"))); - string root = manifest.RootElement.GetProperty("SourceRoot").GetString()!; - string fixtures = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, ".build", "game-sdk-tests")) + Path.DirectorySeparatorChar; - if (!Path.GetFullPath(root).StartsWith(fixtures, StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException("Embedded-only tests may only move generated synthetic fixture sources."); - foreach (string source in Directory.GetFiles(root, "*.cs", SearchOption.AllDirectories)) - { - string hidden = source + ".embedded-test"; - File.Move(source, hidden); - hiddenSources.Add((source, hidden)); - } - } - - // Reflection accesses Visual Studio's internal Roslyn service. A mock would - // miss the reference-assembly lookup and embedded-document loader failures. - var features = Assembly.Load("Microsoft.CodeAnalysis.Features"); - var host = MefHostServices.Create(MefHostServices.DefaultAssemblies - .Add(features).Add(Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features"))); - var exportProvider = host.GetType().GetInterfaces().Single(t => t.Name == "IMefHostExportProvider"); - var getExports = exportProvider.GetMethods().Single(m => m.Name == "GetExports" && m.GetGenericArguments().Length == 1); - var contract = features.GetType("Microsoft.CodeAnalysis.MetadataAsSource.IMetadataAsSourceFileService")!; - var exports = (System.Collections.IEnumerable)getExports.MakeGenericMethod(contract).Invoke(host, null)!; - object export = exports.Cast().First(); - object service = export.GetType().GetProperty("Value")!.GetValue(export)!; - - using var workspace = new AdhocWorkspace(host); - string reference = Path.Combine(Path.GetFullPath(args[0]), "ref", "Assembly-CSharp.Readable.dll"); - IEnumerable referencePaths = args.Length > 1 && !args[1].StartsWith("--") - ? Directory.GetFiles(Path.GetFullPath(args[1]), "*.dll").Where(p => Path.GetFileName(p) != "Assembly-CSharp.dll") - : ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!).Split(Path.PathSeparator); - var references = referencePaths.Select(p => MetadataReference.CreateFromFile(p)) - .Append(MetadataReference.CreateFromFile(reference)); - var project = workspace.AddProject("Consumer", LanguageNames.CSharp) - .WithMetadataReferences(references) - .WithCompilationOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - if (!workspace.TryApplyChanges(project.Solution)) throw new InvalidOperationException("Cannot create navigation consumer project."); - project = workspace.CurrentSolution.GetProject(project.Id)!; - var compilation = (await project.GetCompilationAsync())!; - bool fixture = compilation.GetTypeByMetadataName("Readable.Agent") is not null; - var type = compilation.GetTypeByMetadataName(fixture ? "Readable.Agent" : "BDNetwork.NetworkManager") - ?? throw new InvalidOperationException("Navigation test type missing."); - - var optionsType = features.GetType("Microsoft.CodeAnalysis.MetadataAsSource.MetadataAsSourceOptions")!; - object options = optionsType.GetMethod("GetDefault", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)! - .Invoke(null, new object[] { project.Services })!; - optionsType.GetProperty("NavigateToSourceLinkAndEmbeddedSources")!.SetValue(options, true); - optionsType.GetProperty("NavigateToDecompiledSources")!.SetValue(options, false); - - var symbols = new List { type }; - if (fixture) - { - var ping = type.GetMembers("Ping").OfType().Single(m => m.Parameters[0].Type.SpecialType == SpecialType.System_Int32); - symbols.AddRange(new ISymbol[] { - ping, type.GetMembers("Ping").OfType().Single(m => m.Parameters[0].Type.SpecialType == SpecialType.System_String), - ping.Parameters[0], type.GetMembers("Echo").Single(), type.GetMembers("Value").Single(), type.GetMembers("Changed").Single() - }); - var box = compilation.GetTypeByMetadataName("Readable.Box`1")!; - symbols.AddRange(new ISymbol[] { box, box.GetMembers("Echo").Single(), box.GetTypeMembers("Nested").Single() }); - } - else - { - symbols.AddRange(new ISymbol[] { - type.GetMembers("Send").First(), type.GetMembers("Refresh").Single(), type.GetMembers("BatchCount").Single(), - type.GetMembers("UpdateNowFunc").Single(), type.GetMembers("OnRequestComplete").Single(), type.GetTypeMembers("BatchObject").Single() - }); - } - - foreach (var symbol in symbols) - { - var task = (Task)contract.GetMethod("GetGeneratedFileAsync")! - .Invoke(service, new object[] { workspace, project, symbol, false, options, CancellationToken.None })!; - await task; - object result = task.GetType().GetProperty("Result")!.GetValue(task)!; - string path = (string)result.GetType().GetProperty("FilePath")!.GetValue(result)!; - string source = File.ReadAllText(path); - if (!path.Contains("PdbSourceDocumentMetadataAsSourceFileProvider", StringComparison.Ordinal)) - throw new InvalidDataException("Navigation fell back from PDB source: " + path); - if (!source.Contains(fixture ? "return value;" : "Path.Combine", StringComparison.Ordinal)) - throw new InvalidDataException("Navigation did not load complete method bodies: " + path); - var location = (Location)result.GetType().GetProperty("IdentifierLocation")!.GetValue(result)!; - if (!location.IsInSource) throw new InvalidDataException("Navigation has no source declaration."); - string identifier = source.Substring(location.SourceSpan.Start, location.SourceSpan.Length); - if (identifier != symbol.Name) - throw new InvalidDataException($"Navigation selected '{identifier}' instead of '{symbol.Name}'."); - if (fixture && symbol is IMethodSymbol { Name: "Ping" } method) - { - var syntax = CSharpSyntaxTree.ParseText(source).GetRoot().FindToken(location.SourceSpan.Start) - .Parent!.AncestorsAndSelf().OfType().First(); - string expected = method.Parameters[0].Type.SpecialType == SpecialType.System_Int32 ? "int" : "string"; - if (syntax.ParameterList.Parameters[0].Type!.ToString() != expected) - throw new InvalidDataException("Navigation selected the wrong Ping overload."); - } - Console.WriteLine($"{symbol.ToDisplayString()} => {path}"); - } - Console.WriteLine($"Visual Studio Roslyn PDB-source navigation: {symbols.Count} declarations resolved to exact identifiers and complete source; embedded-only={hiddenSources.Count > 0}."); -} -finally -{ - foreach (var source in hiddenSources) File.Move(source.Hidden, source.Original); -} diff --git a/plugins/GameSdk/GameSdk.csproj b/plugins/GameSdk/GameSdk.csproj index 52a856e..e12d7dd 100644 --- a/plugins/GameSdk/GameSdk.csproj +++ b/plugins/GameSdk/GameSdk.csproj @@ -6,6 +6,9 @@ enable disable false + + false diff --git a/plugins/GameSdk/README.md b/plugins/GameSdk/README.md index 39516f5..9337ddc 100644 --- a/plugins/GameSdk/README.md +++ b/plugins/GameSdk/README.md @@ -246,12 +246,4 @@ dotnet $tool self-test 手动编译须引用 `ref/Assembly-CSharp.Readable.dll` 并编译同目录 `GameSdkIdentity.g.cs`,保留相邻 `lib` 以供 IDE 查找。`verify-navigation` 全量检查 PE/PDB 身份、内嵌/本地源码校验和、类型文档和全部方法体的符号。 -`self-test` 生成合成游戏 DLL,验证重载、泛型、继承、嵌套/编译器生成类型、私有成员、事件、参数、表达式、字符串不变和版本拒绝;它会打印合成 SDK 目录。使用实际 Roslyn 引擎验证该目录的导航: - -```powershell -dotnet run --project plugins/GameSdk.NavigationTests -- '<合成 SDK 目录>' --embedded-only -# 或验证完整游戏导航: -dotnet run --project plugins/GameSdk.NavigationTests -- '<插件 obj 中的 SDK 目录>' '<游戏 Managed 目录>' -``` - -嵌入源码测试临时移走合成 fixture 的 `.cs`,结束时恢复;它限制在 `.build/game-sdk-tests` 下运行。测试断言类型、重载、泛型、参数、字段、事件和嵌套类型跳到准确的源码标识符,并拒绝签名/反编译回退。`VerifyPackages.ps1` 验证仓库外 NuGet 项目。实际 Unity/Harmony 行为需在游戏启动后检查日志。 +`self-test` 生成合成游戏 DLL,验证重载、泛型、继承、嵌套/编译器生成类型、私有成员、事件、参数、表达式、字符串不变和版本拒绝。`.build/game-sdk-tests` 仅保存每次自测的临时产物,不参与 SDK 构建、源码导航或客户端启动,用完可以删除;下次自测会重新生成。`VerifyPackages.ps1` 验证仓库外 NuGet 项目及完整源码/PDB 覆盖。实际 Unity/Harmony 行为需在游戏启动后检查日志。