using System.Text.RegularExpressions; namespace CapySR.ProtoGen; // the dump's translation table isn't injective: 11 real names are claimed by two classes each. // one keeps the name, the rest fall back to their obf name. picking wrong means answering on the // wrong cmdid, so score it against independent signals instead of guessing. public sealed class DuplicateResolver { // deliberate calls, keyed by name. add one when the client proves which cmdid is real. private static readonly Dictionary Overrides = new(StringComparer.Ordinal) { // 321 sits in CmdAvatarType next to DressRelicAvatarCsReq(364); rival 4515 is CmdPlayerReturnType ["DressRelicAvatarScRsp"] = 321, // UNVERIFIED: 716 { retcode=6 } vs 719 { unk=7, retcode=9 }, both CmdLineupType. // we only ever answer retcode 0, which proto3 omits, so both encode empty. flip if joining hangs. ["JoinLineupScRsp"] = 716, }; private static readonly Regex ServiceEnumPattern = new(@"^enum\s+Cmd(?\w+)Type\s*\{", RegexOptions.Compiled); private static readonly Regex EnumMemberPattern = new(@"=\s*(?\d+)\s*;", RegexOptions.Compiled); private readonly Dictionary> _services = []; private readonly HashSet<(ushort Id, string Name)> _packetIds = []; public DuplicateResolver(List declarations, string packetIdsPath) { foreach (var declaration in declarations.Where(d => d.Kind == DeclarationKind.Enum)) { var match = ServiceEnumPattern.Match(declaration.Body[0]); if (!match.Success) { continue; } var service = match.Groups["service"].Value; foreach (var line in declaration.Body.Skip(1)) { var member = EnumMemberPattern.Match(line); if (member.Success && ushort.TryParse(member.Groups["id"].Value, out var id) && id != 0) { if (!_services.TryGetValue(id, out var list)) { _services[id] = list = []; } list.Add(service); } } } foreach (var entry in PacketIdFile.Read(packetIdsPath)) { _packetIds.Add(entry); } } public ProtoDeclaration PickWinner(List candidates) => candidates.OrderByDescending(Score).ThenBy(c => c.CmdId ?? ushort.MaxValue).First(); public string Explain(ProtoDeclaration declaration) { var services = declaration.CmdId is { } id && _services.TryGetValue(id, out var list) ? string.Join('/', list) : "-"; return $"CmdID={declaration.CmdId?.ToString() ?? "?"} Type={declaration.PacketType ?? "?"} " + $"service={services} score={Score(declaration)}"; } private int Score(ProtoDeclaration declaration) { if (declaration.CmdId is not { } id) { return 0; } var score = 0; if (Overrides.TryGetValue(declaration.Name, out var chosen) && chosen == id) { score += 8; } // a message in service "Avatar" should say Avatar in its name if (_services.TryGetValue(id, out var services) && services.Any(s => declaration.Name.Contains(s, StringComparison.Ordinal))) { score += 4; } if (SuffixMatchesType(declaration)) { score += 2; } if (_packetIds.Contains((id, declaration.Name))) { score += 1; } return score; } private static bool SuffixMatchesType(ProtoDeclaration declaration) { var expected = declaration.Name switch { var n when n.EndsWith("CsReq", StringComparison.Ordinal) => "Req", var n when n.EndsWith("ScRsp", StringComparison.Ordinal) => "Rsp", var n when n.EndsWith("Notify", StringComparison.Ordinal) => "Notify", _ => null, }; return expected is not null && declaration.PacketType == expected; } } // "34 = PlayerLoginCsReq // obf: ..." public static class PacketIdFile { public static IEnumerable<(ushort Id, string Name)> Read(string path) { if (!File.Exists(path)) { yield break; } 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 (name.Length > 0) { yield return (id, name); } } } }