2026-09-15 18:13:46 +07:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Text.Json;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ananta.SDK.Logging;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Process-wide runtime logs shared by the SDK and server.
|
|
|
|
|
|
/// Run-All.ps1 supplies one run id so C# and the Node proxy append to the same console files.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static class RuntimeLogs
|
|
|
|
|
|
{
|
|
|
|
|
|
private static readonly object FileLock = new();
|
|
|
|
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
|
|
|
|
{
|
|
|
|
|
|
IncludeFields = true,
|
|
|
|
|
|
WriteIndented = false,
|
|
|
|
|
|
MaxDepth = 8,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
private static bool _initialized;
|
|
|
|
|
|
private static bool _consoleEnabled;
|
|
|
|
|
|
private static bool _packetEnabled;
|
|
|
|
|
|
private static bool _includeHex;
|
|
|
|
|
|
private static bool _includeDecoded;
|
|
|
|
|
|
private static int _maxBodyBytes;
|
|
|
|
|
|
private static string? _consoleLatest;
|
|
|
|
|
|
private static string? _consoleArchive;
|
|
|
|
|
|
private static string? _packetLatest;
|
|
|
|
|
|
private static string? _packetArchive;
|
2026-10-29 02:44:24 +07:00
|
|
|
|
|
|
|
|
|
|
// ⛑️⛑️ 长驻 writer —— 见 AppendText 的注释。原来每一行日志都要
|
|
|
|
|
|
// 「CreateDirectory + new FileStream(Append) + new StreamWriter + 写 + 释放」×2 份文件,
|
|
|
|
|
|
// 而 FileMode.Append 每次打开都要重新定位到 EOF ⇒ 单行的代价随日志文件变大而变大。
|
|
|
|
|
|
// 实测:SyncPlayerInfo 那 200 KB 的 hexdump 有 12 541 行,在 09-30 16:50(日志还小)
|
|
|
|
|
|
// 要 2.8 s,到 10-01 22:08(日志 20 MB)要 6.8~14.1 s —— 而这段时间是**同步**跑在
|
|
|
|
|
|
// LoginGame handler 里的(RpcContext.NotifyAsync<T> 写完包立刻 RuntimeLogs.Decoded),
|
|
|
|
|
|
// 收包循环串行 ⇒ 一帧都不读 ⇒ 客户端 TimeScaleAnomaly 判定掉线 ⇒ 重连 ⇒ 又打一份
|
|
|
|
|
|
// 200 KB 的 hexdump ⇒ 日志更大 ⇒ 更慢。正反馈,永不收敛。
|
|
|
|
|
|
private static StreamWriter? _consoleLatestWriter;
|
|
|
|
|
|
private static StreamWriter? _consoleArchiveWriter;
|
|
|
|
|
|
private static StreamWriter? _packetLatestWriter;
|
|
|
|
|
|
private static StreamWriter? _packetArchiveWriter;
|
|
|
|
|
|
|
2026-09-15 18:13:46 +07:00
|
|
|
|
private static TextWriter? _originalOut;
|
|
|
|
|
|
private static TextWriter? _originalError;
|
|
|
|
|
|
private static bool _bootstrapConsoleHidden;
|
|
|
|
|
|
|
|
|
|
|
|
public static void Initialize(
|
|
|
|
|
|
string logDirectory,
|
|
|
|
|
|
string? runId,
|
|
|
|
|
|
bool consoleEnabled,
|
|
|
|
|
|
bool packetEnabled,
|
|
|
|
|
|
bool includeHex,
|
|
|
|
|
|
bool includeDecoded,
|
|
|
|
|
|
int maxBodyBytes)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_initialized) return;
|
|
|
|
|
|
|
|
|
|
|
|
Directory.CreateDirectory(logDirectory);
|
|
|
|
|
|
runId = string.IsNullOrWhiteSpace(runId)
|
|
|
|
|
|
? DateTime.Now.ToString("yyyyMMdd-HHmmss")
|
|
|
|
|
|
: runId;
|
|
|
|
|
|
|
|
|
|
|
|
_consoleEnabled = consoleEnabled;
|
|
|
|
|
|
_packetEnabled = packetEnabled;
|
|
|
|
|
|
_includeHex = includeHex;
|
|
|
|
|
|
_includeDecoded = includeDecoded;
|
|
|
|
|
|
_maxBodyBytes = Math.Max(0, maxBodyBytes);
|
|
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
|
_consoleLatest = Environment.GetEnvironmentVariable("Ananta_CONSOLE_LOG_LATEST") ?? Environment.GetEnvironmentVariable("DRMK_CONSOLE_LOG_LATEST")
|
2026-09-15 18:13:46 +07:00
|
|
|
|
?? Path.Combine(logDirectory, "console-latest.log");
|
2026-10-29 02:44:24 +07:00
|
|
|
|
_consoleArchive = Environment.GetEnvironmentVariable("Ananta_CONSOLE_LOG_ARCHIVE") ?? Environment.GetEnvironmentVariable("DRMK_CONSOLE_LOG_ARCHIVE")
|
2026-09-15 18:13:46 +07:00
|
|
|
|
?? Path.Combine(logDirectory, $"console-{runId}.log");
|
2026-10-29 02:44:24 +07:00
|
|
|
|
_packetLatest = Environment.GetEnvironmentVariable("DRMK_PACKET_LOG_LATEST")
|
2026-09-15 18:13:46 +07:00
|
|
|
|
?? Path.Combine(logDirectory, "packets-latest.log");
|
2026-10-29 02:44:24 +07:00
|
|
|
|
_packetArchive = Environment.GetEnvironmentVariable("DRMK_PACKET_LOG_ARCHIVE")
|
2026-09-15 18:13:46 +07:00
|
|
|
|
?? Path.Combine(logDirectory, $"packets-{runId}.log");
|
|
|
|
|
|
|
|
|
|
|
|
// When launched without Run-All, create a fresh latest/archive pair here.
|
2026-10-29 02:44:24 +07:00
|
|
|
|
if (Environment.GetEnvironmentVariable("DRMK_LOGS_PREPARED") != "1")
|
2026-09-15 18:13:46 +07:00
|
|
|
|
{
|
|
|
|
|
|
if (_consoleEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
PrepareFile(_consoleLatest);
|
|
|
|
|
|
PrepareFile(_consoleArchive);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_packetEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
PrepareFile(_packetLatest);
|
|
|
|
|
|
PrepareFile(_packetArchive);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
|
// 文件已经 PrepareFile 过(或由 Run-All 预先清空),这里一次性打开并常驻。
|
|
|
|
|
|
// ⛑ latest 与 archive 指向同一个文件时只开一个 writer —— 两个 Append 流写同一个文件
|
|
|
|
|
|
// 会各自维护文件指针、互相覆盖(旧代码是每次重写,所以没这个问题)。
|
|
|
|
|
|
_consoleLatestWriter = _consoleEnabled ? OpenWriter(_consoleLatest) : null;
|
|
|
|
|
|
_consoleArchiveWriter = _consoleEnabled && !SamePath(_consoleLatest, _consoleArchive)
|
|
|
|
|
|
? OpenWriter(_consoleArchive) : null;
|
|
|
|
|
|
_packetLatestWriter = _packetEnabled ? OpenWriter(_packetLatest) : null;
|
|
|
|
|
|
_packetArchiveWriter = _packetEnabled && !SamePath(_packetLatest, _packetArchive)
|
|
|
|
|
|
? OpenWriter(_packetArchive) : null;
|
|
|
|
|
|
|
2026-09-15 18:13:46 +07:00
|
|
|
|
_initialized = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (_consoleEnabled)
|
|
|
|
|
|
{
|
|
|
|
|
|
_originalOut = Console.Out;
|
|
|
|
|
|
_originalError = Console.Error;
|
2026-10-29 02:44:24 +07:00
|
|
|
|
_bootstrapConsoleHidden = Environment.GetEnvironmentVariable("DRMK_HIDE_BOOT_LOG") == "1";
|
2026-09-15 18:13:46 +07:00
|
|
|
|
if (_bootstrapConsoleHidden)
|
|
|
|
|
|
{
|
|
|
|
|
|
// Run-All already cleared the launcher window. Keep startup diagnostics in the
|
|
|
|
|
|
// normal console log files without repainting the interactive console.
|
|
|
|
|
|
Console.SetOut(new MirrorOnlyTextWriter(_originalOut.Encoding, AppendConsole));
|
|
|
|
|
|
Console.SetError(new MirrorOnlyTextWriter(_originalError.Encoding, AppendConsole));
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
Console.SetOut(new TeeTextWriter(_originalOut, AppendConsole));
|
|
|
|
|
|
Console.SetError(new TeeTextWriter(_originalError, AppendConsole));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Restore normal runtime console output after boot diagnostics have completed.</summary>
|
|
|
|
|
|
public static void EndBootstrapQuietMode()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_initialized || !_consoleEnabled || !_bootstrapConsoleHidden || _originalOut is null || _originalError is null)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
Console.SetOut(new TeeTextWriter(_originalOut, AppendConsole));
|
|
|
|
|
|
Console.SetError(new TeeTextWriter(_originalError, AppendConsole));
|
|
|
|
|
|
_bootstrapConsoleHidden = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void Incoming(string scope, RpcPacketKindView kind, uint methodId, int invokeId, ReadOnlySpan<byte> body)
|
|
|
|
|
|
=> WritePacket(scope, "C2S", kind.ToString().ToUpperInvariant(), methodId, invokeId, null, body);
|
|
|
|
|
|
|
|
|
|
|
|
public static void OutgoingNotify(string scope, uint methodId, ReadOnlySpan<byte> body)
|
|
|
|
|
|
=> WritePacket(scope, "S2C", "NOTIFY", methodId, 0, null, body);
|
|
|
|
|
|
|
|
|
|
|
|
public static void OutgoingReturn(string scope, uint methodId, int invokeId, int error, ReadOnlySpan<byte> body)
|
|
|
|
|
|
=> WritePacket(scope, "S2C", "RETURN", methodId, invokeId, error, body);
|
|
|
|
|
|
|
|
|
|
|
|
public static void RawFrame(string scope, string direction, byte mode, ReadOnlySpan<byte> payload, string note)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_initialized || !_packetEnabled) return;
|
|
|
|
|
|
AppendPacket($"{Timestamp()} [{scope}] {direction} FRAME mode=0x{mode:X2} note={note} payload={payload.Length}b");
|
|
|
|
|
|
if (!_includeHex || payload.Length == 0) return;
|
|
|
|
|
|
var count = _maxBodyBytes == 0 ? payload.Length : Math.Min(payload.Length, _maxBodyBytes);
|
|
|
|
|
|
foreach (var line in HexDump(payload[..count]))
|
|
|
|
|
|
AppendPacket($" {line}");
|
|
|
|
|
|
if (count < payload.Length)
|
|
|
|
|
|
AppendPacket($" ... truncated {payload.Length - count} bytes (logging.packets.maxBodyBytes={_maxBodyBytes})");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static void Decoded(string scope, string direction, uint methodId, Type contractType, object? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_initialized || !_packetEnabled || !_includeDecoded) return;
|
|
|
|
|
|
string json;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
json = JsonSerializer.Serialize(value, contractType, JsonOptions);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
json = $"<decode-log-failed {ex.GetType().Name}: {ex.Message}>";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
AppendPacket($"{Timestamp()} [{scope}] {direction} DECODED {RpcMethodNames.Display(methodId)} type={contractType.Name} {json}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void WritePacket(
|
|
|
|
|
|
string scope,
|
|
|
|
|
|
string direction,
|
|
|
|
|
|
string kind,
|
|
|
|
|
|
uint methodId,
|
|
|
|
|
|
int invokeId,
|
|
|
|
|
|
int? error,
|
|
|
|
|
|
ReadOnlySpan<byte> body)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_initialized || !_packetEnabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
var invoke = invokeId != 0 ? $" invoke={invokeId}" : string.Empty;
|
|
|
|
|
|
var err = error.HasValue ? $" error={error.Value}" : string.Empty;
|
|
|
|
|
|
AppendPacket($"{Timestamp()} [{scope}] {direction} {kind} {RpcMethodNames.Display(methodId)}{invoke}{err} body={body.Length}b");
|
|
|
|
|
|
|
|
|
|
|
|
if (!_includeHex || body.Length == 0) return;
|
|
|
|
|
|
var count = _maxBodyBytes == 0 ? body.Length : Math.Min(body.Length, _maxBodyBytes);
|
|
|
|
|
|
foreach (var line in HexDump(body[..count]))
|
|
|
|
|
|
AppendPacket($" {line}");
|
|
|
|
|
|
if (count < body.Length)
|
|
|
|
|
|
AppendPacket($" ... truncated {body.Length - count} bytes (logging.packets.maxBodyBytes={_maxBodyBytes})");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static List<string> HexDump(ReadOnlySpan<byte> body)
|
|
|
|
|
|
{
|
|
|
|
|
|
const int width = 16;
|
|
|
|
|
|
var lines = new List<string>((body.Length + width - 1) / width);
|
|
|
|
|
|
for (var offset = 0; offset < body.Length; offset += width)
|
|
|
|
|
|
{
|
|
|
|
|
|
var len = Math.Min(width, body.Length - offset);
|
|
|
|
|
|
var slice = body.Slice(offset, len);
|
|
|
|
|
|
var hex = new StringBuilder(width * 3);
|
|
|
|
|
|
var ascii = new StringBuilder(width);
|
|
|
|
|
|
for (var i = 0; i < width; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (i < len)
|
|
|
|
|
|
{
|
|
|
|
|
|
var b = slice[i];
|
|
|
|
|
|
hex.Append(b.ToString("X2")).Append(' ');
|
|
|
|
|
|
ascii.Append(b is >= 32 and <= 126 ? (char)b : '.');
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
hex.Append(" ");
|
|
|
|
|
|
ascii.Append(' ');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lines.Add($"{offset:X8} {hex}|{ascii}|");
|
|
|
|
|
|
}
|
|
|
|
|
|
return lines;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string Timestamp() => DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
|
|
|
|
|
|
|
|
|
|
|
private static void AppendConsole(string text)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_initialized || !_consoleEnabled || string.IsNullOrEmpty(text)) return;
|
2026-10-29 02:44:24 +07:00
|
|
|
|
AppendToPair(_consoleLatestWriter, _consoleArchiveWriter, text);
|
2026-09-15 18:13:46 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void AppendPacket(string line)
|
2026-10-29 02:44:24 +07:00
|
|
|
|
=> AppendToPair(_packetLatestWriter, _packetArchiveWriter, line + Environment.NewLine);
|
2026-09-15 18:13:46 +07:00
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
|
private static void AppendToPair(StreamWriter? latest, StreamWriter? archive, string text)
|
2026-09-15 18:13:46 +07:00
|
|
|
|
{
|
|
|
|
|
|
lock (FileLock)
|
|
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
|
latest?.Write(text);
|
|
|
|
|
|
if (!ReferenceEquals(latest, archive)) archive?.Write(text);
|
2026-09-15 18:13:46 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-29 02:44:24 +07:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 一次性打开日志文件的长驻 writer。
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ⛑️ 这里<b>不能</b>回到「每行 new FileStream(FileMode.Append)」的写法:Append 模式每次
|
|
|
|
|
|
/// 打开都要重新定位到 EOF,单行代价随文件体积线性上涨,20 MB 的 packets 日志上实测
|
|
|
|
|
|
/// ~0.5 ms/行 ×2 份文件 —— 一个 200 KB 包的 hexdump(12 541 行)就是 10 秒级同步阻塞,
|
|
|
|
|
|
/// 而它发生在 RPC handler 里(见上面 writer 字段的注释)。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static StreamWriter? OpenWriter(string? path)
|
2026-09-15 18:13:46 +07:00
|
|
|
|
{
|
2026-10-29 02:44:24 +07:00
|
|
|
|
if (string.IsNullOrWhiteSpace(path)) return null;
|
2026-09-15 18:13:46 +07:00
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
2026-10-29 02:44:24 +07:00
|
|
|
|
var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
|
|
|
|
|
|
// AutoFlush=true:玩家/我自己是边跑边看 console-latest.log 的,不能为了省一次
|
|
|
|
|
|
// syscall 把尾巴攒在缓冲区里。真正的开销在「打开/关闭」而不在 flush。
|
|
|
|
|
|
return new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = true };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool SamePath(string? a, string? b)
|
|
|
|
|
|
=> !string.IsNullOrWhiteSpace(a) && !string.IsNullOrWhiteSpace(b)
|
|
|
|
|
|
&& string.Equals(Path.GetFullPath(a), Path.GetFullPath(b), StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>把长驻 writer 全部 flush(进程正常退出时调用一次,可选)。</summary>
|
|
|
|
|
|
public static void FlushAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (FileLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
_consoleLatestWriter?.Flush();
|
|
|
|
|
|
_consoleArchiveWriter?.Flush();
|
|
|
|
|
|
_packetLatestWriter?.Flush();
|
|
|
|
|
|
_packetArchiveWriter?.Flush();
|
|
|
|
|
|
}
|
2026-09-15 18:13:46 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void PrepareFile(string? path)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))!);
|
|
|
|
|
|
File.WriteAllText(path, string.Empty, new UTF8Encoding(false));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class MirrorOnlyTextWriter(Encoding encoding, Action<string> mirror) : TextWriter
|
|
|
|
|
|
{
|
|
|
|
|
|
public override Encoding Encoding => encoding;
|
|
|
|
|
|
|
|
|
|
|
|
public override void Write(char value) => mirror(value.ToString());
|
|
|
|
|
|
|
|
|
|
|
|
public override void Write(string? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!string.IsNullOrEmpty(value)) mirror(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void WriteLine(string? value)
|
|
|
|
|
|
=> mirror((value ?? string.Empty) + Environment.NewLine);
|
|
|
|
|
|
|
|
|
|
|
|
public override void WriteLine() => mirror(Environment.NewLine);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class TeeTextWriter(TextWriter original, Action<string> mirror) : TextWriter
|
|
|
|
|
|
{
|
|
|
|
|
|
public override Encoding Encoding => original.Encoding;
|
|
|
|
|
|
|
|
|
|
|
|
public override void Write(char value)
|
|
|
|
|
|
{
|
|
|
|
|
|
original.Write(value);
|
|
|
|
|
|
mirror(value.ToString());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void Write(string? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
original.Write(value);
|
|
|
|
|
|
if (!string.IsNullOrEmpty(value)) mirror(value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void WriteLine(string? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
original.WriteLine(value);
|
|
|
|
|
|
mirror((value ?? string.Empty) + Environment.NewLine);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override void WriteLine()
|
|
|
|
|
|
{
|
|
|
|
|
|
original.WriteLine();
|
|
|
|
|
|
mirror(Environment.NewLine);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Small SDK-only view so the logging layer does not depend on RPC implementation types.</summary>
|
|
|
|
|
|
public enum RpcPacketKindView
|
|
|
|
|
|
{
|
|
|
|
|
|
Invoke,
|
|
|
|
|
|
Notify,
|
|
|
|
|
|
Return,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Global names populated by RpcRouter.Name; outgoing packets can therefore log symbolic MethodId names too.</summary>
|
|
|
|
|
|
public static class RpcMethodNames
|
|
|
|
|
|
{
|
|
|
|
|
|
private static readonly object Lock = new();
|
|
|
|
|
|
private static readonly Dictionary<uint, string> Names = new();
|
|
|
|
|
|
|
|
|
|
|
|
public static void Register(uint methodId, string name)
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (Lock)
|
|
|
|
|
|
Names.TryAdd(methodId, name);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public static string Display(uint methodId)
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (Lock)
|
|
|
|
|
|
return Names.TryGetValue(methodId, out var name)
|
|
|
|
|
|
? $"{name} [0x{methodId:X8}]"
|
|
|
|
|
|
: $"0x{methodId:X8}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|