mirror of
https://github.com/JogmansonReal/Radicade.git
synced 2026-10-27 16:09:07 -07:00
Radicade Public Code
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
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<string, Entry> Seen = new Dictionary<string, Entry>();
|
||||
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<string>();
|
||||
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 = "<message unreadable>"; }
|
||||
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 "<no stack>";
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user