Files
2026-10-16 23:33:13 -07:00

904 lines
31 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using HarmonyLib;
internal static class RealtimeDiagnostics
{
private static readonly object Gate = new object();
private static readonly Dictionary<string, int> Targets = new Dictionary<string, int>();
private static readonly Dictionary<string, int> NotificationIds = new Dictionary<string, int>();
private static readonly HashSet<string> KnownNotifications = new HashSet<string>();
private static readonly HashSet<string> HighFrequencyIds = new HashSet<string>(StringComparer.Ordinal)
{
"PresenceUpdate",
"AccountUpdate",
"SelfAccountUpdate",
"SubscriptionUpdatePresence",
"SubscriptionUpdateProfile",
"SubscriptionUpdateGameSession",
"SubscriptionUpdateRoom",
"SubscriptionUpdateRoomPlaylist"
};
private static int Frames;
private static int Failures;
private const int VerboseFrames = 40;
private const int PreviewChars = 400;
internal static void StartHandlerKeyProbe(Harmony harmony)
{
if (Interlocked.Exchange(ref HandlerKeyProbeStarted, 1) != 0) return;
var worker = new Thread(() =>
{
Type dispatcher = null;
bool hooksInstalled = false;
bool tablesDumped = false;
for (int attempt = 1; attempt <= HandlerKeyProbeAttempts; attempt++)
{
try
{
if (dispatcher == null) dispatcher = FindNotificationDispatcher();
if (!hooksInstalled && dispatcher != null)
hooksInstalled = InstallRegistrationHooks(harmony, dispatcher);
if (!tablesDumped && attempt <= TableDumpAttempts)
tablesDumped = DumpDispatcherKeys(attempt, dispatcher);
if (hooksInstalled && (tablesDumped || attempt >= TableDumpAttempts)) return;
}
catch (Exception ex)
{
if (attempt == 1)
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] handler-key probe failed ({ex.GetType().Name}: {ex.Message}); retrying.");
}
Thread.Sleep(HandlerKeyProbeIntervalMs);
}
if (!hooksInstalled)
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] no handler-registration method was located after {HandlerKeyProbeAttempts} attempts " +
"- notification ids cannot be checked against the client's own registrations in this build.");
})
{ IsBackground = true, Name = "RadicadeNotifyKeyProbe" };
worker.Start();
}
private const int HandlerKeyProbeAttempts = 60;
private const int HandlerKeyProbeIntervalMs = 2000;
private const int TableDumpAttempts = 3;
private static int HandlerKeyProbeStarted;
private static readonly HashSet<string> RegisteredHandlerKeys = new HashSet<string>(StringComparer.Ordinal);
private static bool ChatKeyReported;
private static bool InstallRegistrationHooks(Harmony harmony, Type dispatcher)
{
var hooked = 0;
var callbacks = 0;
var generics = 0;
foreach (var method in AccessTools.GetDeclaredMethods(dispatcher))
{
try
{
if (method.ReturnType != typeof(void)) continue;
var parameters = method.GetParameters();
if (parameters.Length != 1 && parameters.Length != 2) continue;
var keyType = parameters[0].ParameterType;
if (keyType != typeof(string) && !keyType.IsEnum) continue;
if (parameters.Length == 2)
{
if (!IsCallback(parameters[1].ParameterType)) continue;
callbacks++;
if (method.IsGenericMethodDefinition) { generics++; continue; }
}
harmony.Patch(method,
prefix: new HarmonyMethod(AccessTools.Method(typeof(RealtimeDiagnostics), nameof(RegistrationPrefix))));
hooked++;
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] could not hook {dispatcher.Name}.{method.Name} for key logging ({ex.GetType().Name}: {ex.Message}).");
}
}
if (hooked == 0) return false;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] watching {hooked} dispatcher method(s) on {dispatcher.FullName} " +
$"({callbacks} registration, {generics} generic overload(s) skipped but funnel through the core, rest channels): " +
"every notification id this client accepts will be printed as it registers.");
return true;
}
private static bool IsCallback(Type type)
{
try
{
if (typeof(Delegate).IsAssignableFrom(type)) return true;
for (var current = type; current != null; current = current.BaseType)
{
var name = current.Name;
if (name == "Delegate" || name == "MulticastDelegate") return true;
}
}
catch { }
return false;
}
private static void RegistrationPrefix(object[] __args, MethodBase __originalMethod)
{
try
{
if (__args == null || __args.Length == 0) return;
var key = DescribeKey(__args[0]);
if (key == null) return;
if (__args.Length == 1)
{
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] the client called {__originalMethod?.Name}({key}) - " +
"a realtime channel/lookup name it expects the server to publish on.");
return;
}
bool first;
lock (Gate) first = RegisteredHandlerKeys.Add(key);
if (!first) return;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] the client registered a notification handler under {key} " +
$"({__originalMethod?.DeclaringType?.Name}.{__originalMethod?.Name}) - " +
$"a notification whose Id EQUALS {key} is handled, anything else is dropped.");
if (key.IndexOf("Progression", StringComparison.Ordinal) >= 0
|| key.IndexOf("AppVersion", StringComparison.Ordinal) >= 0
|| key.IndexOf("Announcement", StringComparison.Ordinal) >= 0)
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] {key} binds a payload of type {DescribeCallback(__args.Length > 1 ? __args[1] : null)}");
ReportChatKeyMatch();
}
catch { }
}
private static string DescribeCallback(object? callback)
{
if (callback == null) return "(no callback)";
try
{
var type = callback.GetType();
var candidates = new List<Type>();
if (type.IsGenericType) candidates.AddRange(type.GetGenericArguments());
for (var current = type; current != null; current = current.BaseType)
{
MethodInfo? invoke = null;
try { invoke = current.GetMethod("Invoke", BindingFlags.Public | BindingFlags.Instance); }
catch { }
if (invoke == null) continue;
foreach (var parameter in invoke.GetParameters()) candidates.Add(parameter.ParameterType);
}
var payloads = candidates
.Where(t => t != null && t != typeof(void))
.Distinct()
.ToList();
if (payloads.Count == 0) return $"{type.FullName} (no payload readable)";
return string.Join(" | ", payloads.Select(DescribePayloadType));
}
catch (Exception ex)
{
return $"(unreadable: {ex.GetType().Name})";
}
}
private static string DescribePayloadType(Type type)
{
try
{
if (type.IsPrimitive || type == typeof(string)) return type.Name;
var members = new List<string>();
foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Instance))
members.Add(field.Name);
foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
members.Add(property.Name);
members = members.Distinct().OrderBy(m => m, StringComparer.Ordinal).ToList();
if (members.Count == 0) return type.FullName ?? type.Name;
return $"{type.FullName} {{{string.Join(", ", members)}}}";
}
catch
{
return type.FullName ?? type.Name;
}
}
private static string DescribeKey(object value)
{
if (value is string text) return Quote(text);
if (value == null) return null;
try
{
var type = value.GetType();
return type.IsEnum ? Quote($"{type.Name}.{value}") : null;
}
catch
{
return null;
}
}
private static void ReportChatKeyMatch()
{
lock (Gate)
{
if (ChatKeyReported) return;
var numeric = RegisteredHandlerKeys.Contains("\"90\"");
var named = RegisteredHandlerKeys.FirstOrDefault(
key => key.IndexOf("ChatMessage", StringComparison.Ordinal) >= 0);
if (!numeric && named == null) return;
ChatKeyReported = true;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] chat push key check: this client registered " +
$"{(numeric ? "a handler for the chat enum's number (\"90\")" : "NO handler for the chat enum's number (\"90\")")}" +
$"{(named == null ? string.Empty : $", and {named} by name")}. " +
"Only an exact match is delivered, so the server's notification Id has to be the " +
"spelling that was registered - for chat that is the name.");
}
}
private const int MaxTables = 25;
private const int MaxKeysPerTable = 60;
private static bool DumpDispatcherKeys(int attempt, Type dispatcher)
{
var tables = new List<string>();
if (dispatcher != null) CollectStringKeyedTables(dispatcher, tables);
if (tables.Count == 0)
{
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
var name = assembly.GetName().Name ?? string.Empty;
if (!name.StartsWith("RecNet", StringComparison.Ordinal)) continue;
Type[] types;
try { types = assembly.GetTypes(); }
catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); }
catch { continue; }
foreach (var type in types)
{
try { CollectStringKeyedTables(type, tables); } catch { }
if (tables.Count >= MaxTables) break;
}
if (tables.Count >= MaxTables) break;
}
}
if (tables.Count == 0) return false;
RadicadeEacBypassPlugin.LogSource.LogInfo(
dispatcher != null
? $"[Realtime] the client's notification dispatcher ({dispatcher.FullName}) holds these string keys (attempt {attempt}). " +
"A notification Id must EQUAL one of them to be handled - this is the contract:"
: $"[Realtime] could not pin the notification dispatcher, so every string-keyed static table in RecNet.* is listed " +
$"instead (attempt {attempt}). The handler keys are among them; a notification Id must EQUAL one to be handled:");
foreach (var table in tables)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {table}");
var flat = " " + string.Join(" ", tables) + " ";
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] chat push key check: the client registered {Registered(flat, "\"90\"")} for \"90\" and " +
$"{Registered(flat, "ChatMessageReceived")} for \"ChatMessageReceived\". " +
"Whichever is registered is the string the server's chat notification Id must equal.");
return true;
}
private static string Registered(string flatKeys, string key)
=> flatKeys.Contains(" " + key, StringComparison.Ordinal) ? "a handler" : "NO handler";
private static void CollectStringKeyedTables(Type type, List<string> tables)
{
foreach (var field in type.GetFields(
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly))
{
if (tables.Count >= MaxTables) return;
Type fieldType;
try { fieldType = field.FieldType; } catch { continue; }
if (!IsStringKeyed(fieldType)) continue;
object value;
try { value = field.GetValue(null); }
catch { continue; }
AddTable(tables, $"{type.Name}.{field.Name}", value);
}
}
private static bool IsStringKeyed(Type type)
{
try
{
return type.IsGenericType && type.GetGenericArguments()[0] == typeof(string);
}
catch
{
return false;
}
}
private static void AddTable(List<string> tables, string label, object value)
{
if (value == null) return;
var keys = new List<string>();
try
{
if (value is System.Collections.IDictionary map)
{
foreach (var key in map.Keys) keys.Add(Quote(key?.ToString()));
}
else if (value is System.Collections.IEnumerable items)
{
foreach (var item in items) keys.Add(KeyOf(item));
}
else return;
}
catch
{
return;
}
if (keys.Count == 0) return;
keys.Sort(StringComparer.Ordinal);
var shown = keys.Count <= MaxKeysPerTable
? string.Join(", ", keys)
: string.Join(", ", keys.Take(MaxKeysPerTable)) + $", … (+{keys.Count - MaxKeysPerTable} more)";
tables.Add($"{label}: {shown}");
}
private static string KeyOf(object item)
{
if (item == null) return "(null)";
try
{
var type = item.GetType();
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
var key = type.GetProperty("Key");
if (key != null) return Quote(key.GetValue(item)?.ToString());
}
}
catch { }
return Quote(item.ToString());
}
private static string Quote(string text) => text == null ? "(null)" : $"\"{text}\"";
private static Type FindNotificationDispatcher()
{
var byName = FindType("LMBGGGGNBOB");
if (byName != null && DeclaresNotificationEnum(byName)) return byName;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
Type[] types;
try { types = assembly.GetTypes(); }
catch (ReflectionTypeLoadException ex) { types = ex.Types.Where(t => t != null).ToArray(); }
catch { continue; }
foreach (var type in types)
{
try
{
if (!type.IsEnum || !IsNotificationEnum(type)) continue;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] notification enum located: {type.FullName} " +
$"(message = {Convert.ToInt64(Enum.Parse(type, "MessageReceived"), System.Globalization.CultureInfo.InvariantCulture)}, " +
$"kick = {Convert.ToInt64(Enum.Parse(type, "ModerationKick"), System.Globalization.CultureInfo.InvariantCulture)})");
if (type.DeclaringType != null) return type.DeclaringType;
continue;
}
catch { }
}
}
return null;
}
private static bool DeclaresNotificationEnum(Type type)
{
try
{
foreach (var nested in type.GetNestedTypes(BindingFlags.Public | BindingFlags.NonPublic))
if (nested.IsEnum && IsNotificationEnum(nested)) return true;
}
catch { }
return false;
}
private static bool IsNotificationEnum(Type type)
{
var members = Enum.GetNames(type);
return Array.IndexOf(members, "MessageReceived") >= 0
&& Array.IndexOf(members, "ModerationKick") >= 0
&& Array.IndexOf(members, "PresenceHeartbeatResponse") >= 0;
}
internal static System.DateTime? ServerRequestedExitUtc;
private const int ExitRequestWindowSeconds = 120;
internal static bool ServerRequestedExitRecently()
=> ServerRequestedExitUtc.HasValue
&& (System.DateTime.UtcNow - ServerRequestedExitUtc.Value).TotalSeconds < ExitRequestWindowSeconds;
private static void NoteServerRequestedExit(string id)
{
if (id != "20" && id != "6") return;
if (ServerRequestedExitRecently()) return;
ServerRequestedExitUtc = System.DateTime.UtcNow;
RadicadeEacBypassPlugin.LogSource.LogInfo(
$"[Realtime] Id={id} arrived - the server asked this client to leave, so the AppExit guard will let the exit through.");
}
internal static void ApplyExitWatch(Harmony harmony)
{
try
{
var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport");
var target = transport == null ? null : AccessTools.DeclaredMethod(transport, "OnMessage");
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning(
"[Realtime] could not watch for the server's quit/logout frames - a moderator's Quit Game may be suppressed as a room-load exit.");
return;
}
harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), nameof(ExitWatchPrefix)));
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] exit watch failed to install: {ex.GetType().Name}: {ex.Message}");
}
}
private static void ExitWatchPrefix(string __1)
{
var text = __1;
if (string.IsNullOrEmpty(text)) return;
if (text.IndexOf("\"Notification\"", StringComparison.Ordinal) < 0) return;
if (text.IndexOf("\"Id\":\"20\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":20", StringComparison.Ordinal) >= 0)
{
NoteServerRequestedExit("20");
return;
}
if (text.IndexOf("\"Id\":\"6\"", StringComparison.Ordinal) >= 0 || text.IndexOf("\"Id\":6", StringComparison.Ordinal) >= 0)
NoteServerRequestedExit("6");
}
internal static bool Verbose = true;
internal static void Apply(Harmony harmony, bool verbose)
{
Verbose = verbose;
var transport = FindType("BestHTTP.SignalRCore.Transports.WebSocketTransport");
if (transport == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.Transports.WebSocketTransport not found - notify frame logging stays inactive.");
}
else
{
Hook(harmony, transport, "OnOpen", nameof(OnOpenPrefix), "socket open");
Hook(harmony, transport, "HandleHandshakeResponse", nameof(HandshakePrefix), "SignalR handshake answer");
Hook(harmony, transport, "OnMessage", nameof(OnMessagePrefix), "inbound frames");
Hook(harmony, transport, "OnError", nameof(OnErrorPrefix), "transport errors");
Hook(harmony, transport, "OnClosed", nameof(OnClosedPrefix), "socket close with code/reason");
}
var hub = FindType("BestHTTP.SignalRCore.HubConnection");
if (hub == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning("[Realtime] BestHTTP.SignalRCore.HubConnection not found - hub-pump guard stays inactive.");
}
else
{
HookFinalizer(harmony, hub, "OnMessages", nameof(OnMessagesFinalizer));
}
RadicadeEacBypassPlugin.LogSource.LogInfo(Verbose
? "[Realtime] notify-hub tap armed: every inbound frame is read and logged."
: "[Realtime] notify-hub tap armed for moderation only - a voice chat restriction is applied from the frame it arrives in; frames are not logged (Diagnostics=false).");
if (Verbose) StartHandlerKeyProbe(harmony);
}
private static Type FindType(string name)
{
var direct = AccessTools.TypeByName(name);
if (direct != null) return direct;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
try
{
var found = assembly.GetType(name, false);
if (found != null) return found;
}
catch { }
}
return null;
}
private static void Hook(Harmony harmony, Type type, string method, string prefix, string what)
{
try
{
var target = AccessTools.DeclaredMethod(type, method);
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - {what} will not be logged.");
return;
}
harmony.Patch(target, prefix: new HarmonyMethod(typeof(RealtimeDiagnostics), prefix));
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hooked {type.Name}.{method} ({what})");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not hook {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}");
}
}
private static void HookFinalizer(Harmony harmony, Type type, string method, string finalizer)
{
try
{
var target = AccessTools.DeclaredMethod(type, method);
if (target == null)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] {type.Name}.{method} not found - the hub-pump guard stays inactive.");
return;
}
harmony.Patch(target, finalizer: new HarmonyMethod(typeof(RealtimeDiagnostics), finalizer));
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] guarded {type.Name}.{method} (a frame the client cannot parse is logged, not thrown)");
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] could not guard {type.Name}.{method}: {ex.GetType().Name}: {ex.Message}");
}
}
private static void OnOpenPrefix()
{
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] socket open - the notify hub WebSocket upgraded and is ready.");
}
private static void HandshakePrefix(string __0)
{
var text = __0 ?? string.Empty;
if (text.IndexOf("error", StringComparison.OrdinalIgnoreCase) >= 0)
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] hub handshake answered with an error: {Trim(text, 300)}");
else if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] hub handshake accepted: {Trim(text, 120)}");
}
private static void OnMessagePrefix(string __1) => ObserveFrame(__1);
private static void OnErrorPrefix(string __1) =>
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] transport error: {Trim(__1, 300)}");
private static void OnClosedPrefix(ushort __1, string __2) =>
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] socket closed code={__1} reason={Trim(__2, 200)}");
private static Exception OnMessagesFinalizer(Exception __exception)
{
if (__exception == null) return null;
lock (Gate) Failures++;
RadicadeEacBypassPlugin.LogSource.LogWarning(
$"[Realtime] the client failed to handle a hub frame ({__exception.GetType().Name}: {__exception.Message}) - " +
$"frame swallowed so the notify socket and the rest of the batch survive (failures={Failures}). " +
"A parse error here means a notification payload the client's own parser rejects; the IdToken/summary lines " +
"above name which notification it was.");
return null;
}
private static void ObserveFrame(string data)
{
if (string.IsNullOrEmpty(data))
{
if (Verbose)
RadicadeEacBypassPlugin.LogSource.LogInfo("[Realtime] empty frame received (keep-alive).");
return;
}
foreach (var record in data.Split('\u001e'))
{
if (record.Length == 0) continue;
if (!Verbose && record.IndexOf(VoiceChatRestrictionPatches.NotificationId, StringComparison.Ordinal) < 0)
continue;
int index;
lock (Gate) index = ++Frames;
DescribeRecord(index, record);
}
}
private static void DescribeRecord(int index, string record)
{
try
{
using var document = JsonDocument.Parse(record);
var root = document.RootElement;
var type = root.TryGetProperty("type", out var typeElement) ? typeElement.GetRawText() : "?";
var target = root.TryGetProperty("target", out var targetElement) ? targetElement.GetString() : null;
if (string.Equals(target, "Notification", StringComparison.Ordinal))
{
DescribeNotification(index, root, record);
return;
}
Count(Targets, target ?? ("type " + type));
if (!Verbose) return;
if (index <= VerboseFrames)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] frame #{index} target={target ?? "-"} type={type} len={record.Length}: {Trim(record, PreviewChars)}");
else
Summarize(index);
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] frame #{index} is not readable as a hub record ({ex.GetType().Name}: {ex.Message}): {Trim(record, PreviewChars)}");
}
}
private static void DescribeNotification(int index, JsonElement root, string record)
{
string envelope = null;
if (root.TryGetProperty("arguments", out var arguments)
&& arguments.ValueKind == JsonValueKind.Array
&& arguments.GetArrayLength() > 0)
{
var first = arguments[0];
envelope = first.ValueKind == JsonValueKind.String ? first.GetString() : first.GetRawText();
}
string id = null;
string idToken = "(no Id)";
string msg = "(no Msg)";
if (!string.IsNullOrEmpty(envelope))
{
try
{
using var inner = JsonDocument.Parse(envelope);
var payload = inner.RootElement;
if (TryGetProperty(payload, "Id", out var idElement))
{
idToken = idElement.ValueKind.ToString();
id = idElement.ValueKind == JsonValueKind.String ? idElement.GetString() : idElement.GetRawText();
}
if (TryGetProperty(payload, "Msg", out var msgElement))
msg = Trim(msgElement.GetRawText(), 200);
}
catch (Exception ex)
{
RadicadeEacBypassPlugin.LogSource.LogWarning($"[Realtime] Notification #{index} envelope did not parse at all ({ex.GetType().Name}: {ex.Message}): {Trim(envelope, PreviewChars)}");
envelope = null;
}
}
if (!string.IsNullOrEmpty(id)) NoteServerRequestedExit(id);
if (!string.IsNullOrEmpty(id) && !string.IsNullOrEmpty(envelope))
VoiceChatRestrictionPatches.Observe(id, envelope);
Count(Targets, "Notification");
Count(NotificationIds, id ?? "(no Id)");
bool firstOfItsKind = false;
if (!string.IsNullOrEmpty(id))
lock (Gate) firstOfItsKind = KnownNotifications.Add(id);
if (!Verbose) return;
bool worthFullDetail = string.IsNullOrEmpty(id) || !HighFrequencyIds.Contains(id);
if (worthFullDetail || firstOfItsKind || index <= VerboseFrames)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] Notification #{index} Id={id ?? "(none)"} IdToken={idToken} Msg={msg}");
else
Summarize(index);
if (worthFullDetail || firstOfItsKind)
RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {id ?? "envelope"} envelope: {Trim(envelope ?? record, PreviewChars)}");
}
private static void Summarize(int index)
{
if (index % 25 != 0) return;
string targets;
string ids;
int failures;
lock (Gate)
{
targets = Join(Targets);
ids = Join(NotificationIds);
failures = Failures;
} RadicadeEacBypassPlugin.LogSource.LogInfo($"[Realtime] {index} frames so far - targets: {targets}; notifications: {ids}; handler failures: {failures}");
}
private static bool TryGetProperty(JsonElement element, string name, out JsonElement value)
{
if (element.ValueKind == JsonValueKind.Object)
{
if (element.TryGetProperty(name, out value)) return true;
var lower = char.ToLowerInvariant(name[0]) + name.Substring(1);
if (element.TryGetProperty(lower, out value)) return true;
}
value = default;
return false;
}
private static void Count(Dictionary<string, int> counts, string key)
{
lock (Gate)
{
counts.TryGetValue(key, out var current);
counts[key] = current + 1;
}
}
private static string Join(Dictionary<string, int> counts)
{
var parts = new List<string>();
foreach (var pair in counts) parts.Add($"{pair.Key} x{pair.Value}");
return parts.Count == 0 ? "none" : string.Join(", ", parts);
}
private static string Trim(string text, int limit)
{
if (string.IsNullOrEmpty(text)) return string.Empty;
var flat = text.Replace("\r", " ").Replace("\n", " ");
return flat.Length <= limit ? flat : flat.Substring(0, limit) + "...";
}
}