fix(all): show client startup progress and stabilize SDK cache
This commit is contained in:
@@ -6,10 +6,11 @@
|
|||||||
|
|
||||||
## 开发构建
|
## 开发构建
|
||||||
|
|
||||||
|
外部 PowerShell 终端从 `go` 目录执行 `. .\env.ps1`,将当前终端的 Go 缓存设为 `go/.cache/go-build`,不修改用户级 Go 配置。VS Code 可在工作区 `terminal.integrated.env.windows` 和 `go.toolsEnvVars` 中配置同一路径,让本项目的新终端自动生效。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Go
|
# Go
|
||||||
Push-Location .\go
|
Push-Location .\go
|
||||||
$env:GOCACHE = (Join-Path $PWD '.cache\go-build')
|
|
||||||
go test ./...
|
go test ./...
|
||||||
go build .\cmd\bd2server
|
go build .\cmd\bd2server
|
||||||
go build .\cmd\bd2client
|
go build .\cmd\bd2client
|
||||||
@@ -19,17 +20,17 @@ Pop-Location
|
|||||||
开发时在两个终端中分别从 `go` 目录运行:
|
开发时在两个终端中分别从 `go` 目录运行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:GOCACHE = (Join-Path $PWD '.cache\go-build')
|
|
||||||
go run .\cmd\bd2server --dev run
|
go run .\cmd\bd2server --dev run
|
||||||
```
|
```
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$env:GOCACHE = (Join-Path $PWD '.cache\go-build')
|
|
||||||
go run .\cmd\bd2client --dev run
|
go run .\cmd\bd2client --dev run
|
||||||
```
|
```
|
||||||
|
|
||||||
服务端开发入口会自动使用仓库根目录的版本、认证、资源、数据和存档配置;客户端开发入口会直接使用仓库版本清单和插件产物,并从本机开发配置读取游戏目录,随后用 SDK 内嵌的当前版本名字表增量构建两个客户端插件,无需提供官方映射文件。首次开发前创建该配置:
|
服务端开发入口会自动使用仓库根目录的版本、认证、资源、数据和存档配置;客户端开发入口会直接使用仓库版本清单和插件产物,并从本机开发配置读取游戏目录,随后用 SDK 内嵌的当前版本名字表增量构建两个客户端插件,无需提供官方映射文件。首次开发前创建该配置:
|
||||||
|
|
||||||
|
客户端窗口在插件准备完成后打开。首次生成完整游戏源码或 SDK/游戏更新后可能等待数分钟,终端会实时显示构建和反编译进度;后续启动复用共享缓存。
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Copy-Item .\go\config.example.json .\go\config.json
|
Copy-Item .\go\config.example.json .\go\config.json
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -177,6 +178,7 @@ func buildDevelopmentPlugins(root, gameDir string) error {
|
|||||||
filepath.Join(root, "plugins", "LoginUI", "LoginUI.csproj"),
|
filepath.Join(root, "plugins", "LoginUI", "LoginUI.csproj"),
|
||||||
}
|
}
|
||||||
for _, project := range projects {
|
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(
|
command := exec.Command(
|
||||||
"dotnet", "build", project, "-c", "Release",
|
"dotnet", "build", project, "-c", "Release",
|
||||||
"-p:GameDir="+filepath.Clean(gameDir),
|
"-p:GameDir="+filepath.Clean(gameDir),
|
||||||
@@ -185,9 +187,15 @@ func buildDevelopmentPlugins(root, gameDir string) error {
|
|||||||
"--nologo",
|
"--nologo",
|
||||||
)
|
)
|
||||||
command.Dir = root
|
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 {
|
if err != nil {
|
||||||
message := strings.TrimSpace(string(output))
|
message := strings.TrimSpace(output.String())
|
||||||
if message == "" {
|
if message == "" {
|
||||||
message = err.Error()
|
message = err.Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
|
||||||
<PropertyGroup>
|
|
||||||
<OutputType>Exe</OutputType>
|
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<IsPackable>false</IsPackable>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<!-- Exercise Visual Studio's actual metadata-as-source provider, not a mock. -->
|
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.11.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
</Project>
|
|
||||||
@@ -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 <sdk-directory> [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<object>().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<string> 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<ISymbol> { type };
|
|
||||||
if (fixture)
|
|
||||||
{
|
|
||||||
var ping = type.GetMembers("Ping").OfType<IMethodSymbol>().Single(m => m.Parameters[0].Type.SpecialType == SpecialType.System_Int32);
|
|
||||||
symbols.AddRange(new ISymbol[] {
|
|
||||||
ping, type.GetMembers("Ping").OfType<IMethodSymbol>().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<MethodDeclarationSyntax>().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);
|
|
||||||
}
|
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>disable</Nullable>
|
<Nullable>disable</Nullable>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
|
<!-- The navigation cache hashes this DLL. Unrelated repository commits
|
||||||
|
must not change it through informational-version metadata. -->
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ICSharpCode.Decompiler" Version="9.1.0.7988" />
|
<PackageReference Include="ICSharpCode.Decompiler" Version="9.1.0.7988" />
|
||||||
|
|||||||
@@ -246,12 +246,4 @@ dotnet $tool self-test
|
|||||||
|
|
||||||
手动编译须引用 `ref/Assembly-CSharp.Readable.dll` 并编译同目录 `GameSdkIdentity.g.cs`,保留相邻 `lib` 以供 IDE 查找。`verify-navigation` 全量检查 PE/PDB 身份、内嵌/本地源码校验和、类型文档和全部方法体的符号。
|
手动编译须引用 `ref/Assembly-CSharp.Readable.dll` 并编译同目录 `GameSdkIdentity.g.cs`,保留相邻 `lib` 以供 IDE 查找。`verify-navigation` 全量检查 PE/PDB 身份、内嵌/本地源码校验和、类型文档和全部方法体的符号。
|
||||||
|
|
||||||
`self-test` 生成合成游戏 DLL,验证重载、泛型、继承、嵌套/编译器生成类型、私有成员、事件、参数、表达式、字符串不变和版本拒绝;它会打印合成 SDK 目录。使用实际 Roslyn 引擎验证该目录的导航:
|
`self-test` 生成合成游戏 DLL,验证重载、泛型、继承、嵌套/编译器生成类型、私有成员、事件、参数、表达式、字符串不变和版本拒绝。`.build/game-sdk-tests` 仅保存每次自测的临时产物,不参与 SDK 构建、源码导航或客户端启动,用完可以删除;下次自测会重新生成。`VerifyPackages.ps1` 验证仓库外 NuGet 项目及完整源码/PDB 覆盖。实际 Unity/Harmony 行为需在游戏启动后检查日志。
|
||||||
|
|
||||||
```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 行为需在游戏启动后检查日志。
|
|
||||||
|
|||||||
Reference in New Issue
Block a user