using HarmonyLib; using System; using System.Collections.Generic; using UnityEngine; internal static class LogFloodGate { internal const double WindowSeconds = 30.0; internal static bool Enabled => !_disabled; private static readonly bool _disabled = IsDisabled(); private static readonly Dictionary Seen = new Dictionary(); private static long _totalSuppressed; private static bool _selfTestPending; private static int _selfTestFired; private sealed class Entry { public double WindowStart; public int Suppressed; public int ReportedAt; } private static bool IsDisabled() { try { var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE"); if (string.IsNullOrWhiteSpace(raw)) return false; return raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no"; } catch { return false; } } internal static void Apply(Harmony harmony) { try { var target = AccessTools.Method(typeof(ODGGOALOLEG), "LogException", new[] { typeof(Il2CppSystem.Exception), typeof(UnityEngine.Object) }); if (target == null) { Warn("[LogGate] ODGGOALOLEG.LogException(Il2CppSystem.Exception, UnityEngine.Object) not found - " + "not armed; the per-frame exception flood stays as-is."); return; } var prefix = AccessTools.Method(typeof(LogFloodGate), nameof(Prefix)); if (prefix == null) { Warn("[LogGate] patch method Prefix not found - not armed."); return; } harmony.Patch(target, prefix: new HarmonyMethod(prefix)); } catch (Exception e) { Warn($"[LogGate] failed to arm ({e.GetType().Name}: {e.Message}); the exception flood stays as-is."); } } [HarmonyPrefix] internal static bool Prefix(Il2CppSystem.Exception __0) { if (!Enabled) return true; try { if (_selfTestPending && System.Threading.Interlocked.Exchange(ref _selfTestFired, 1) == 0) { _selfTestPending = false; RunSelfTest(); } var signature = Signature(__0); if (signature == null) return true; var now = Time.realtimeSinceStartup; lock (Seen) { if (Seen.Count > 512) Evict(now); if (!Seen.TryGetValue(signature, out var entry)) { Seen[signature] = new Entry { WindowStart = now }; return true; } if (now - entry.WindowStart < WindowSeconds) { entry.Suppressed++; _totalSuppressed++; if (entry.Suppressed == 1) Warn($"[LogGate] repeats of this exception are now suppressed for {WindowSeconds:0}s at a time: {signature}"); else if (entry.Suppressed - entry.ReportedAt >= 500) { entry.ReportedAt = entry.Suppressed; Warn($"[LogGate] {entry.Suppressed} identical repeats of this exception suppressed so far " + $"(session total {_totalSuppressed}): {signature}"); } return false; } var dropped = entry.Suppressed; entry.WindowStart = now; entry.Suppressed = 0; entry.ReportedAt = 0; if (dropped > 0) Warn($"[LogGate] {dropped} identical repeats were collapsed in the previous {WindowSeconds:0}s window " + $"(session total {_totalSuppressed}); logging this occurrence in full: {signature}"); return true; } } catch { return true; } } private static void Evict(double now) { var stale = new List(); foreach (var pair in Seen) if (now - pair.Value.WindowStart > 600.0) stale.Add(pair.Key); foreach (var key in stale) Seen.Remove(key); } private static string Signature(Il2CppSystem.Exception exception) { string type, message, stack; try { type = exception.GetType().Name; } catch { type = "Exception"; } try { message = exception.Message; } catch { message = ""; } try { stack = exception.StackTrace; } catch { stack = null; } if (string.IsNullOrEmpty(message) && string.IsNullOrEmpty(stack)) return null; return Trim(type + ": " + message, 150) + " @ " + FirstFrame(stack); } private static string FirstFrame(string stack) { if (string.IsNullOrEmpty(stack)) return ""; var start = stack.IndexOf(" at ", StringComparison.Ordinal); if (start < 0) { var newline = stack.IndexOf('\n'); return Trim(newline < 0 ? stack : stack.Substring(0, newline), 120); } var end = stack.IndexOf('\n', start); return Trim(end < 0 ? stack.Substring(start) : stack.Substring(start, end - start), 120); } private static string Trim(string value, int max) { if (string.IsNullOrEmpty(value)) return string.Empty; value = value.Replace('\r', ' ').Replace('\n', ' ').Trim(); return value.Length <= max ? value : value.Substring(0, max) + "..."; } private static void Warn(string message) { try { RadicadeEacBypassPlugin.LogSource.LogWarning(message); } catch { } } internal static void RequestSelfTestIfAsked() { try { var raw = Environment.GetEnvironmentVariable("RADICADE_LOG_GATE_SELFTEST"); if (string.IsNullOrWhiteSpace(raw)) return; if (raw.Trim().ToLowerInvariant() is "0" or "off" or "false" or "no") return; _selfTestPending = true; RadicadeEacBypassPlugin.LogSource.LogInfo( "[LogGate] self-test armed: the next exception through the game handler fires five identical ones to prove the gate (RADICADE_LOG_GATE_SELFTEST)."); } catch (Exception e) { Warn($"[LogGate] self-test could not be armed: {e.GetType().Name}: {e.Message}"); } } private static void RunSelfTest() { try { var before = _totalSuppressed; var exception = new Il2CppSystem.Exception("radicade log gate self-test"); const int shots = 5; for (var i = 0; i < shots; i++) UnityEngine.Debug.LogException(exception); var collapsed = _totalSuppressed - before; Warn($"[LogGate] self-test: fired {shots} identical exceptions through the live game handler; {collapsed} were collapsed, " + $"{shots - collapsed} reached the log " + $"({(collapsed == shots - 1 ? "PASS" : "FAIL")} - the first of a burst always logs in full, the rest are suppressed)."); } catch (Exception e) { Warn($"[LogGate] self-test failed to run: {e.GetType().Name}: {e.Message}"); } } }