using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading; using BD2.GameNames; using BepInEx.Logging; using Google.Protobuf; using HarmonyLib; using static BD2.GameNames.Game; namespace Bd2CaptureEnvironment; internal static class PacketCapture { private static readonly object CorrelationLock = new(); private static readonly Dictionary> PendingRequests = new(StringComparer.Ordinal); private static ManualLogSource Log; private static long Sequence; private sealed class PendingRequest { public long Id; public string Type; } internal static void Install(Harmony harmony, ManualLogSource log) { Log = log; Type manager = typeof(BDNetwork.NetworkManager); MethodInfo send = manager?.GetMethods(BindingFlags.Instance | BindingFlags.Public) .SingleOrDefault(method => method.IsGameMethod("Send") && method.ReturnType == typeof(void) && method.GetParameters().Length == 6 && typeof(IMessage).IsAssignableFrom(method.GetParameters()[0].ParameterType)); if (send != null) { harmony.Patch(send, prefix: new HarmonyMethod( typeof(PacketCapture), nameof(GameSendPrefix))); Log?.LogInfo("Plaintext request capture installed"); } else { Log?.LogWarning("NetworkManager.Send plaintext hook not found"); } Type responseData = typeof(BDNetwork.ResponseData); MethodInfo responseCheck = manager?.GetMethods( BindingFlags.Instance | BindingFlags.NonPublic) .SingleOrDefault(method => { ParameterInfo[] parameters = method.GetParameters(); return method.IsGameMethod("ResponseCheck") && method.ReturnType == typeof(bool) && parameters.Length == 4 && parameters[0].ParameterType == typeof(string) && parameters[2].ParameterType == responseData && parameters[3].ParameterType == typeof(byte[]); }); if (responseCheck != null) { harmony.Patch(responseCheck, prefix: new HarmonyMethod( typeof(PacketCapture), nameof(ResponseCheckPrefix))); Log?.LogInfo("Path-correlated plaintext response capture installed"); } else { Log?.LogWarning("NetworkManager.ResponseCheck plaintext hook not found"); } } private static void GameSendPrefix(IMessage __0) { try { if (__0 == null) return; byte[] body = __0.ToByteArray(); string type = __0.GetType().FullName ?? __0.GetType().Name; string path = RequestPath(__0.GetType().Name); long id = Interlocked.Increment(ref Sequence); EnqueueRequest(path, new PendingRequest { Id = id, Type = type }); CaptureWriter.QueueCapture(id, "request", path, type, body); } catch (Exception ex) { Log?.LogWarning("plaintext request capture failed: " + ex.Message); } } private static void ResponseCheckPrefix(string __0, byte[] __3) { try { string path = NormalizePath(__0); PendingRequest request = DequeueRequest(path); long id = request?.Id ?? Interlocked.Increment(ref Sequence); string type = ResponseType(request?.Type, path); CaptureWriter.QueueCapture(id, "response", path, type, __3); } catch (Exception ex) { Log?.LogWarning("plaintext response capture failed: " + ex.Message); } } private static string RequestPath(string typeName) { const string suffix = "Request"; if (typeName != null && typeName.EndsWith(suffix, StringComparison.Ordinal)) typeName = typeName[..^suffix.Length]; return NormalizePath(typeName); } private static string NormalizePath(string path) { if (string.IsNullOrWhiteSpace(path)) return "/unknown"; return path[0] == '/' ? path : "/" + path; } private static string ResponseType(string requestType, string path) { const string suffix = "Request"; if (!string.IsNullOrEmpty(requestType) && requestType.EndsWith(suffix, StringComparison.Ordinal)) return requestType[..^suffix.Length] + "Response"; return "Proto.Net." + path.TrimStart('/') + "Response"; } private static void EnqueueRequest(string path, PendingRequest request) { lock (CorrelationLock) { if (!PendingRequests.TryGetValue(path, out Queue requests)) { requests = new Queue(); PendingRequests.Add(path, requests); } requests.Enqueue(request); } } private static PendingRequest DequeueRequest(string path) { lock (CorrelationLock) { if (PendingRequests.TryGetValue(path, out Queue requests) && requests.Count != 0) { PendingRequest request = requests.Dequeue(); if (requests.Count == 0) PendingRequests.Remove(path); return request; } } return null; } }