feat(init): server has been rewritten
This commit is contained in:
1 parent
3647ed19c4
commit
b87874f2fe
84 files changed
+1437031
-1
No files matched your search
@@ -0,0 +1,419 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using CapySR.ProtoGen;
|
||||
|
||||
// dumped proto -> deduped proto -> protoc --csharp_out -> cmdid tables.
|
||||
// dotnet run --project tools/CapySR.ProtoGen
|
||||
|
||||
var repoRoot = FindRepoRoot();
|
||||
if (repoRoot is null)
|
||||
{
|
||||
Console.Error.WriteLine("could not locate repo root (no Directory.Build.props found walking up)");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var sourceProto = Path.Combine(repoRoot, "TurnBaseGameProto", "Raw", "StarRail.proto");
|
||||
var packetIds = Path.Combine(repoRoot, "TurnBaseGameProto", "Raw", "packetIds.txt");
|
||||
var protocolProject = Path.Combine(repoRoot, "src", "CapySR.Protocol");
|
||||
var cleanedProtoDir = Path.Combine(protocolProject, "Proto");
|
||||
var cleanedProto = Path.Combine(cleanedProtoDir, "StarRail.proto");
|
||||
var generatedDir = Path.Combine(protocolProject, "Generated");
|
||||
|
||||
if (!File.Exists(sourceProto))
|
||||
{
|
||||
Console.Error.WriteLine($"source proto not found: {sourceProto}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(cleanedProtoDir);
|
||||
Directory.CreateDirectory(generatedDir);
|
||||
|
||||
Console.WriteLine($"reading {Path.GetRelativePath(repoRoot, sourceProto)}");
|
||||
var lines = File.ReadAllLines(sourceProto);
|
||||
var declarations = ProtoParser.Parse(lines);
|
||||
|
||||
var messages = declarations.Count(d => d.Kind == DeclarationKind.Message);
|
||||
var enums = declarations.Count(d => d.Kind == DeclarationKind.Enum);
|
||||
Console.WriteLine($"parsed {declarations.Count} top-level declarations ({messages} messages, {enums} enums)");
|
||||
|
||||
// -- 2. resolve duplicate names -------------------------------------------
|
||||
|
||||
var taken = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var declaration in declarations)
|
||||
{
|
||||
taken.Add(declaration.Name);
|
||||
}
|
||||
|
||||
var resolver = new DuplicateResolver(declarations, packetIds);
|
||||
var renames = 0;
|
||||
foreach (var group in declarations.GroupBy(d => d.Name, StringComparer.Ordinal).Where(g => g.Count() > 1).ToList())
|
||||
{
|
||||
var candidates = group.ToList();
|
||||
var winner = resolver.PickWinner(candidates);
|
||||
|
||||
Console.WriteLine($" duplicate '{group.Key}':");
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var marker = ReferenceEquals(candidate, winner) ? "keep" : " ";
|
||||
Console.WriteLine($" [{marker}] line {candidate.SourceLine,5} {resolver.Explain(candidate)}");
|
||||
}
|
||||
|
||||
foreach (var loser in candidates.Where(c => !ReferenceEquals(c, winner)))
|
||||
{
|
||||
var replacement = ChooseReplacementName(loser, taken);
|
||||
Console.WriteLine($" -> renamed to '{replacement}'");
|
||||
loser.Rename(replacement);
|
||||
taken.Add(replacement);
|
||||
renames++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($"resolved {renames} duplicate name(s)");
|
||||
|
||||
// names the dump's translation got wrong, keyed by cmdid
|
||||
var knownRenames = new Dictionary<ushort, string>
|
||||
{
|
||||
// the dump has 5 lineup requests but calls 719 a second JoinLineupScRsp and has no
|
||||
// ReplaceLineupScRsp at all. pearl-sr's dump names it ReplaceLineupScRsp.
|
||||
[719] = "ReplaceLineupScRsp",
|
||||
};
|
||||
|
||||
foreach (var (cmdId, correct) in knownRenames)
|
||||
{
|
||||
var target = declarations.FirstOrDefault(d => d.CmdId == cmdId);
|
||||
|
||||
if (target is null)
|
||||
{
|
||||
Console.WriteLine($" warning: no message with CmdID {cmdId} to rename to '{correct}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (declarations.Any(d => d.Name == correct))
|
||||
{
|
||||
Console.WriteLine($" warning: '{correct}' already exists, leaving CmdID {cmdId} as '{target.Name}'");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($" rename: CmdID {cmdId} '{target.Name}' -> '{correct}'");
|
||||
target.Rename(correct);
|
||||
taken.Add(correct);
|
||||
}
|
||||
|
||||
var stillDuplicated = declarations
|
||||
.GroupBy(d => d.Name, StringComparer.Ordinal)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
if (stillDuplicated.Count > 0)
|
||||
{
|
||||
Console.Error.WriteLine($"unresolved duplicates remain: {string.Join(", ", stillDuplicated)}");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// -- 3. emit the cleaned proto --------------------------------------------
|
||||
|
||||
var sb = new StringBuilder(6 * 1024 * 1024);
|
||||
sb.AppendLine("// <auto-generated>");
|
||||
sb.AppendLine("// generated by tools/CapySR.ProtoGen - do not edit");
|
||||
sb.AppendLine("// </auto-generated>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("syntax = \"proto3\";");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("option csharp_namespace = \"CapySR.Protocol\";");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var declaration in declarations)
|
||||
{
|
||||
declaration.Write(sb);
|
||||
}
|
||||
|
||||
File.WriteAllText(cleanedProto, sb.ToString());
|
||||
Console.WriteLine($"wrote {Path.GetRelativePath(repoRoot, cleanedProto)} ({sb.Length / 1024} KiB)");
|
||||
|
||||
// -- 4. run protoc ---------------------------------------------------------
|
||||
|
||||
var protoc = ResolveProtoc(args);
|
||||
if (protoc is null)
|
||||
{
|
||||
Console.Error.WriteLine("protoc not found. Pass --protoc <path>, set PROTOC, or put it on PATH.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"running {protoc}");
|
||||
var protocResult = Run(protoc,
|
||||
[
|
||||
$"--csharp_out={generatedDir}",
|
||||
$"--proto_path={cleanedProtoDir}",
|
||||
"StarRail.proto",
|
||||
]);
|
||||
|
||||
if (protocResult.ExitCode != 0)
|
||||
{
|
||||
Console.Error.WriteLine("protoc failed:");
|
||||
Console.Error.WriteLine(protocResult.Output);
|
||||
return 1;
|
||||
}
|
||||
|
||||
var generatedCs = Path.Combine(generatedDir, "StarRail.cs");
|
||||
if (File.Exists(generatedCs))
|
||||
{
|
||||
Console.WriteLine($"wrote {Path.GetRelativePath(repoRoot, generatedCs)} ({new FileInfo(generatedCs).Length / 1024} KiB)");
|
||||
}
|
||||
|
||||
// -- 5. emit the CmdId tables ---------------------------------------------
|
||||
|
||||
var withCmdId = declarations
|
||||
.Where(d => d.Kind == DeclarationKind.Message && d.CmdId is > 0)
|
||||
.OrderBy(d => d.CmdId!.Value)
|
||||
.ToList();
|
||||
|
||||
var byId = new Dictionary<ushort, ProtoDeclaration>();
|
||||
foreach (var declaration in withCmdId)
|
||||
{
|
||||
if (!byId.TryAdd(declaration.CmdId!.Value, declaration))
|
||||
{
|
||||
Console.WriteLine(
|
||||
$" warning: CmdID {declaration.CmdId} claimed by both '{byId[declaration.CmdId!.Value].Name}' " +
|
||||
$"and '{declaration.Name}' - keeping the first");
|
||||
}
|
||||
}
|
||||
|
||||
var ordered = byId.OrderBy(kv => kv.Key).ToList();
|
||||
Console.WriteLine($"found {ordered.Count} messages carrying a CmdID");
|
||||
|
||||
CrossCheckPacketIds(packetIds, byId);
|
||||
|
||||
WriteCmdIdEnum(Path.Combine(generatedDir, "CmdId.g.cs"), ordered);
|
||||
WriteCmdIdTable(Path.Combine(generatedDir, "CmdIdTable.g.cs"), ordered);
|
||||
|
||||
Console.WriteLine("done");
|
||||
return 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static string ChooseReplacementName(ProtoDeclaration declaration, HashSet<string> taken)
|
||||
{
|
||||
if (declaration.ObfuscatedName is { Length: > 0 } obf && !taken.Contains(obf))
|
||||
{
|
||||
return obf;
|
||||
}
|
||||
|
||||
for (var i = 2; ; i++)
|
||||
{
|
||||
var candidate = $"{declaration.Name}_Dup{i}";
|
||||
if (!taken.Contains(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void CrossCheckPacketIds(string path, Dictionary<ushort, ProtoDeclaration> byId)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Console.WriteLine(" (packetIds.txt not found, skipping cross-check)");
|
||||
return;
|
||||
}
|
||||
|
||||
int agree = 0, disagree = 0, missing = 0;
|
||||
|
||||
foreach (var line in File.ReadLines(path))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed.Length == 0 || trimmed.StartsWith("//", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var eq = trimmed.IndexOf('=');
|
||||
if (eq < 0 || !ushort.TryParse(trimmed[..eq].Trim(), out var id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var rest = trimmed[(eq + 1)..];
|
||||
var comment = rest.IndexOf("//", StringComparison.Ordinal);
|
||||
var name = (comment >= 0 ? rest[..comment] : rest).Trim();
|
||||
|
||||
if (!byId.TryGetValue(id, out var declaration))
|
||||
{
|
||||
missing++;
|
||||
}
|
||||
else if (string.Equals(declaration.Name, name, StringComparison.Ordinal))
|
||||
{
|
||||
agree++;
|
||||
}
|
||||
else
|
||||
{
|
||||
disagree++;
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine($" packetIds.txt cross-check: {agree} agree, {disagree} disagree, {missing} not in proto");
|
||||
}
|
||||
|
||||
static void WriteCmdIdEnum(string path, List<KeyValuePair<ushort, ProtoDeclaration>> ordered)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated>");
|
||||
sb.AppendLine("// generated by tools/CapySR.ProtoGen - do not edit");
|
||||
sb.AppendLine("// </auto-generated>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace CapySR.Protocol;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("public enum CmdId : ushort");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" None = 0,");
|
||||
|
||||
foreach (var (id, declaration) in ordered)
|
||||
{
|
||||
sb.AppendLine($" {declaration.Name} = {id},");
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
File.WriteAllText(path, sb.ToString());
|
||||
Console.WriteLine($"wrote {Path.GetFileName(path)} ({ordered.Count} entries)");
|
||||
}
|
||||
|
||||
static void WriteCmdIdTable(string path, List<KeyValuePair<ushort, ProtoDeclaration>> ordered)
|
||||
{
|
||||
// parallel arrays; parsers get resolved from the descriptor at runtime
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated>");
|
||||
sb.AppendLine("// generated by tools/CapySR.ProtoGen - do not edit");
|
||||
sb.AppendLine("// </auto-generated>");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace CapySR.Protocol;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("public static class CmdIdTable");
|
||||
sb.AppendLine("{");
|
||||
|
||||
sb.AppendLine(" public static ReadOnlySpan<ushort> Ids =>");
|
||||
sb.AppendLine(" [");
|
||||
AppendWrapped(sb, ordered.Select(kv => kv.Key.ToString()));
|
||||
sb.AppendLine(" ];");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine(" public static readonly string[] Names =");
|
||||
sb.AppendLine(" [");
|
||||
AppendWrapped(sb, ordered.Select(kv => $"\"{kv.Value.Name}\""));
|
||||
sb.AppendLine(" ];");
|
||||
|
||||
sb.AppendLine("}");
|
||||
File.WriteAllText(path, sb.ToString());
|
||||
Console.WriteLine($"wrote {Path.GetFileName(path)}");
|
||||
}
|
||||
|
||||
static void AppendWrapped(StringBuilder sb, IEnumerable<string> values)
|
||||
{
|
||||
var line = new StringBuilder(" ");
|
||||
|
||||
foreach (var value in values)
|
||||
{
|
||||
if (line.Length + value.Length + 2 > 116)
|
||||
{
|
||||
sb.AppendLine(line.ToString());
|
||||
line.Clear().Append(" ");
|
||||
}
|
||||
|
||||
line.Append(value).Append(", ");
|
||||
}
|
||||
|
||||
if (line.ToString().Trim().Length > 0)
|
||||
{
|
||||
sb.AppendLine(line.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
static string? FindRepoRoot()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(dir.FullName, "Directory.Build.props")))
|
||||
{
|
||||
return dir.FullName;
|
||||
}
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static string? ResolveProtoc(string[] args)
|
||||
{
|
||||
var index = Array.IndexOf(args, "--protoc");
|
||||
if (index >= 0 && index + 1 < args.Length && File.Exists(args[index + 1]))
|
||||
{
|
||||
return args[index + 1];
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable("PROTOC") is { Length: > 0 } fromEnv && File.Exists(fromEnv))
|
||||
{
|
||||
return fromEnv;
|
||||
}
|
||||
|
||||
var exe = OperatingSystem.IsWindows() ? "protoc.exe" : "protoc";
|
||||
|
||||
foreach (var pathDir in (Environment.GetEnvironmentVariable("PATH") ?? string.Empty)
|
||||
.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
try
|
||||
{
|
||||
var candidate = Path.Combine(pathDir.Trim('"'), exe);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
// protoc-*/bin/protoc.exe at a drive root, how it usually gets unzipped
|
||||
foreach (var drive in DriveInfo.GetDrives().Where(d => d.IsReady))
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var dir in Directory.EnumerateDirectories(drive.RootDirectory.FullName, "protoc-*"))
|
||||
{
|
||||
var candidate = Path.Combine(dir, "bin", exe);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is UnauthorizedAccessException or IOException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static (int ExitCode, string Output) Run(string fileName, string[] arguments)
|
||||
{
|
||||
var info = new ProcessStartInfo(fileName)
|
||||
{
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
info.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
using var process = Process.Start(info)!;
|
||||
var stdout = process.StandardOutput.ReadToEnd();
|
||||
var stderr = process.StandardError.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
return (process.ExitCode, stdout + stderr);
|
||||
}
|
||||
Reference in new issue
Block a user