Decompiled source of ULTRASTATS v0.0.15
plugins/ULTRASTATS/ULTRASTATS.dll
Decompiled a week ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.CodeDom.Compiler; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; using System.Threading; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using PluginConfig.API; using PluginConfig.API.Decorators; using PluginConfig.API.Fields; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.Events; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ULTRASTATS")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+a1324cb7708b4d411a2c1aca3eee74cee5e60d2d")] [assembly: AssemblyProduct("ULTRASTATS")] [assembly: AssemblyTitle("ULTRASTATS")] [assembly: AssemblyVersion("1.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace ULTRASTATS { [DataContract] public sealed class CampLine { [DataMember(Name = "S", Order = 0)] public int S; [DataMember(Name = "ID", Order = 1)] public long ID; [DataMember(Name = "T", Order = 2)] public long T; [DataMember(Name = "t", Order = 3)] public int t; [DataMember(Name = "k", Order = 4)] public int k; [DataMember(Name = "s", Order = 5)] public int s; [DataMember(Name = "r", Order = 6)] public int r; [DataMember(Name = "p", Order = 7)] public int p; [DataMember(Name = "rs", Order = 8)] public string rs = ""; [DataMember(Name = "rt", Order = 9)] public string rt = ""; [DataMember(Name = "td", Order = 10)] public bool td; [DataMember(Name = "F", Order = 11, EmitDefaultValue = false)] public int F; [DataMember(Name = "c", Order = 12)] public bool c; } internal static class CampaignLevelStatsLogger { private sealed class Scratch { public bool Logged; public bool CaptureScheduled; public int Restarts; public bool TookDamage = true; public bool MajorAssistsUsed; public bool CheatsUsed; } [CompilerGenerated] private sealed class <>c__DisplayClass6_0 { public string bestFailureReason; public PendingRunManager.CampaignRunCapture finalCapture; internal string <QueueWhenReadyRoutine>b__0() { return "Campaign queue routine timed out. Skipping save because endscreen data stayed incomplete: " + bestFailureReason; } internal string <QueueWhenReadyRoutine>b__1() { return $"Campaign snapshot salvaged after incomplete endscreen data: level={finalCapture.LevelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}, rs='{finalCapture.TimeRankRaw}{finalCapture.KillsRankRaw}{finalCapture.StyleRankRaw}', rt='{finalCapture.TotalRankRaw}'"; } internal string <QueueWhenReadyRoutine>b__2() { return $"Campaign snapshot enqueued: level={finalCapture.LevelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}"; } } [CompilerGenerated] private sealed class <QueueWhenReadyRoutine>d__6 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public object inst; private <>c__DisplayClass6_0 <>8__1; private float <waited>5__2; private PendingRunManager.CampaignRunCapture? <completeCapture>5__3; private PendingRunManager.CampaignRunCapture? <bestPartialCapture>5__4; private int <bestPartialScore>5__5; private Scratch <scratch>5__6; private bool <salvaged>5__7; private Component <component>5__8; private PendingRunManager.CampaignRunCapture <capture>5__9; private string <incompleteReason>5__10; private int <partialScore>5__11; private PendingRunManager.CampaignRunCapture <fallbackCapture>5__12; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <QueueWhenReadyRoutine>d__6(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <completeCapture>5__3 = null; <bestPartialCapture>5__4 = null; <scratch>5__6 = null; <component>5__8 = null; <capture>5__9 = default(PendingRunManager.CampaignRunCapture); <incompleteReason>5__10 = null; <fallbackCapture>5__12 = default(PendingRunManager.CampaignRunCapture); <>1__state = -2; } private bool MoveNext() { //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass6_0(); <waited>5__2 = 0f; <completeCapture>5__3 = null; <bestPartialCapture>5__4 = null; <bestPartialScore>5__5 = int.MinValue; <>8__1.bestFailureReason = "campaign endscreen data never became complete"; break; case 1: <>1__state = -1; <waited>5__2 += 0.05f; <capture>5__9 = default(PendingRunManager.CampaignRunCapture); <incompleteReason>5__10 = null; break; } if (<waited>5__2 < 3f) { if (TryBuildCapture(inst, out <capture>5__9, out <incompleteReason>5__10)) { if (HasCompleteEndscreenData(<capture>5__9)) { <completeCapture>5__3 = <capture>5__9; goto IL_018d; } if (HasMinimumCoreEndscreenData(<capture>5__9)) { <partialScore>5__11 = ScorePartialCapture(<capture>5__9); if (!<bestPartialCapture>5__4.HasValue || <partialScore>5__11 >= <bestPartialScore>5__5) { <bestPartialCapture>5__4 = <capture>5__9; <bestPartialScore>5__5 = <partialScore>5__11; } } <>8__1.bestFailureReason = <incompleteReason>5__10; } <>2__current = (object)new WaitForSecondsRealtime(0.05f); <>1__state = 1; return true; } goto IL_018d; IL_018d: <scratch>5__6 = GetScratch(inst); <scratch>5__6.CaptureScheduled = false; if (<scratch>5__6.Logged) { BepInExLogs_US.Debug("Campaign queue routine finished without queueing a run because it was already logged."); return false; } <salvaged>5__7 = false; if (!<completeCapture>5__3.HasValue && <bestPartialCapture>5__4.HasValue) { <fallbackCapture>5__12 = SalvageCapture(<bestPartialCapture>5__4.Value); if (HasCompleteEndscreenData(<fallbackCapture>5__12)) { <completeCapture>5__3 = <fallbackCapture>5__12; <salvaged>5__7 = true; } <fallbackCapture>5__12 = default(PendingRunManager.CampaignRunCapture); } if (!<completeCapture>5__3.HasValue) { BepInExLogs_US.Debug(() => "Campaign queue routine timed out. Skipping save because endscreen data stayed incomplete: " + <>8__1.bestFailureReason); return false; } <>8__1.finalCapture = <completeCapture>5__3.Value; <scratch>5__6.Logged = true; PendingRunManager.EnqueueCapture(<>8__1.finalCapture); ref Component reference = ref <component>5__8; object obj = inst; reference = (Component)((obj is Component) ? obj : null); if (<component>5__8 != null) { PendingRunManager.AttachDiscardWatcher(<component>5__8); } if (<salvaged>5__7) { BepInExLogs_US.Debug(() => $"Campaign snapshot salvaged after incomplete endscreen data: level={<>8__1.finalCapture.LevelId}, time={<>8__1.finalCapture.TimeMs}, points={<>8__1.finalCapture.Points}, rs='{<>8__1.finalCapture.TimeRankRaw}{<>8__1.finalCapture.KillsRankRaw}{<>8__1.finalCapture.StyleRankRaw}', rt='{<>8__1.finalCapture.TotalRankRaw}'"); } else { BepInExLogs_US.Debug(() => $"Campaign snapshot enqueued: level={<>8__1.finalCapture.LevelId}, time={<>8__1.finalCapture.TimeMs}, points={<>8__1.finalCapture.Points}"); } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string UnknownRankPlaceholder = "_"; private static readonly ConditionalWeakTable<object, Scratch> ScratchTable = new ConditionalWeakTable<object, Scratch>(); private static Scratch GetScratch(object inst) { return ScratchTable.GetOrCreateValue(inst); } public static void NoteInfo(object inst, int restarts, bool damage, bool majorUsed, bool cheatsUsed) { Scratch scratch = GetScratch(inst); scratch.Restarts = restarts; scratch.TookDamage = damage; scratch.MajorAssistsUsed = majorUsed; scratch.CheatsUsed = cheatsUsed; BepInExLogs_US.Debug(() => $"Campaign NoteInfo: restarts={restarts}, damage={damage}, major={majorUsed}, cheats={cheatsUsed}"); } public static void ScheduleQueue(object inst) { if (!Plugin.CampaignLoggingEnabled || (Object)(object)Plugin.Instance == (Object)null) { return; } Scratch scratch = GetScratch(inst); if (!scratch.Logged && !scratch.CaptureScheduled) { LevelContext currentLevelContext = LoggerShared.GetCurrentLevelContext(); if (currentLevelContext.Source == LevelSource.Campaign && !currentLevelContext.IsCustom) { scratch.CaptureScheduled = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(QueueWhenReadyRoutine(inst)); } } } [IteratorStateMachine(typeof(<QueueWhenReadyRoutine>d__6))] private static IEnumerator QueueWhenReadyRoutine(object inst) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <QueueWhenReadyRoutine>d__6(0) { inst = inst }; } private static bool TryBuildCapture(object inst, out PendingRunManager.CampaignRunCapture capture, out string incompleteReason) { incompleteReason = "unknown"; capture = default(PendingRunManager.CampaignRunCapture); LevelContext currentLevelContext = LoggerShared.GetCurrentLevelContext(); if (currentLevelContext.Source != LevelSource.Campaign || currentLevelContext.IsCustom) { incompleteReason = "not in a normal campaign level"; return false; } Scratch scratch = GetScratch(inst); float num = UltraStatsReflection.TryGetFloat(inst, "savedTime"); int val = UltraStatsReflection.TryGetInt(inst, "savedKills"); int val2 = UltraStatsReflection.TryGetInt(inst, "savedStyle"); int val3 = UltraStatsReflection.TryGetInt(inst, "totalPoints"); ChallengeManager challengeManager = GetChallengeManager(); bool challengeComplete = (Object)(object)challengeManager != (Object)null && challengeManager.challengeDone; int restarts = scratch.Restarts; bool tookDamage = scratch.TookDamage; bool majorAssistsUsed = scratch.MajorAssistsUsed; StatsManager statsManager = GetStatsManager(); if ((Object)(object)statsManager != (Object)null) { restarts = statsManager.restarts; tookDamage = statsManager.tookDamage; majorAssistsUsed = statsManager.majorUsed; } bool cheatsUsed = scratch.CheatsUsed; string timeRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "timeRank"))); string killsRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "killsRank"))); string styleRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "styleRank"))); string totalRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "totalRank"))); capture = new PendingRunManager.CampaignRunCapture(LoggerShared.Difficulty(), LoggerShared.GetSaveSlotId(), currentLevelContext.LevelId, (num >= 0f) ? Mathf.RoundToInt(num * 1000f) : 0, Math.Max(0, val), Math.Max(0, val2), Math.Max(0, restarts), Math.Max(0, val3), timeRankRaw, killsRankRaw, styleRankRaw, totalRankRaw, tookDamage, challengeComplete, majorAssistsUsed, cheatsUsed); incompleteReason = DescribeIncompleteData(capture); return true; } private static StatsManager? GetStatsManager() { try { return MonoSingleton<StatsManager>.Instance; } catch { return null; } } private static ChallengeManager? GetChallengeManager() { try { return MonoSingleton<ChallengeManager>.Instance; } catch { return null; } } private static bool HasCompleteEndscreenData(PendingRunManager.CampaignRunCapture capture) { if (!HasMinimumCoreEndscreenData(capture)) { return false; } if (!HasPersistableRank(capture.TotalRankRaw)) { return false; } if (!HasPersistableRank(capture.TimeRankRaw) || !HasPersistableRank(capture.KillsRankRaw) || !HasPersistableRank(capture.StyleRankRaw)) { return false; } return true; } private static bool HasMinimumCoreEndscreenData(PendingRunManager.CampaignRunCapture capture) { if (capture.TimeMs <= 0) { return false; } if (capture.Points <= 0) { return false; } return true; } private static int ScorePartialCapture(PendingRunManager.CampaignRunCapture capture) { int num = 0; if (capture.TimeMs > 0) { num += 8; } if (capture.Points > 0) { num += 8; } if (HasPersistableRank(capture.TimeRankRaw)) { num++; } if (HasPersistableRank(capture.KillsRankRaw)) { num++; } if (HasPersistableRank(capture.StyleRankRaw)) { num++; } if (HasPersistableRank(capture.TotalRankRaw)) { num++; } return num; } private static PendingRunManager.CampaignRunCapture SalvageCapture(PendingRunManager.CampaignRunCapture capture) { return new PendingRunManager.CampaignRunCapture(capture.Difficulty, capture.SaveSlot, capture.LevelId, capture.TimeMs, capture.Kills, capture.Style, capture.Restarts, capture.Points, NormalizeRankForPersistence(capture.TimeRankRaw), NormalizeRankForPersistence(capture.KillsRankRaw), NormalizeRankForPersistence(capture.StyleRankRaw), NormalizeRankForPersistence(capture.TotalRankRaw), capture.TookDamage, capture.ChallengeComplete, capture.MajorAssistsUsed, capture.CheatsUsed); } private static bool HasPersistableRank(string raw) { raw = (raw ?? string.Empty).Trim().ToUpperInvariant(); return raw.Length == 1; } private static string NormalizeRankForPersistence(string raw) { raw = NormalizeRankLetter(raw); return (raw.Length == 1) ? raw : "_"; } private static string NormalizeRankLetter(string raw) { raw = (raw ?? "").Trim().ToUpperInvariant(); return (raw.Length == 1) ? raw : ""; } private static string DescribeIncompleteData(PendingRunManager.CampaignRunCapture capture) { if (capture.TimeMs <= 0) { return "time not populated yet"; } if (capture.Points <= 0) { return "total points not populated yet"; } if (string.IsNullOrWhiteSpace(capture.TotalRankRaw)) { return "overall rank is empty"; } if (capture.TotalRankRaw.Length != 1) { return $"overall rank length was {capture.TotalRankRaw.Length} instead of 1"; } if (string.IsNullOrWhiteSpace(capture.TimeRankRaw)) { return "time rank is empty"; } if (string.IsNullOrWhiteSpace(capture.KillsRankRaw)) { return "kills rank is empty"; } if (string.IsNullOrWhiteSpace(capture.StyleRankRaw)) { return "style rank is empty"; } return "campaign snapshot is complete"; } } [HarmonyPatch] internal static class FinalRank_SetInfo_Patch { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("FinalRank"); return AccessTools.Method(type, "SetInfo", new Type[4] { typeof(int), typeof(bool), typeof(bool), typeof(bool) }, (Type[])null); } private static void Prefix(object __instance, int restarts, bool damage, bool majorUsed, bool cheatsUsed) { if (Plugin.CampaignLoggingEnabled) { CampaignLevelStatsLogger.NoteInfo(__instance, restarts, damage, majorUsed, cheatsUsed); } } private static void Postfix(object __instance) { if (Plugin.CampaignLoggingEnabled) { CampaignLevelStatsLogger.ScheduleQueue(__instance); } } } internal static class CustomLevelStatsLogger { private sealed class Scratch { public bool Logged; public bool CaptureScheduled; public int Restarts; public bool TookDamage = true; public bool MajorAssistsUsed; public bool CheatsUsed; } [CompilerGenerated] private sealed class <>c__DisplayClass6_0 { public string bestFailureReason; public string packKey; public string levelId; public PendingRunManager.CustomRunCapture finalCapture; internal string <QueueWhenReadyRoutine>b__0() { return "Custom queue routine timed out. Skipping save because endscreen data stayed incomplete: " + bestFailureReason; } internal string <QueueWhenReadyRoutine>b__1() { return $"Custom snapshot salvaged after incomplete endscreen data: pack={packKey}, level={levelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}, rs='{finalCapture.TimeRankRaw}{finalCapture.KillsRankRaw}{finalCapture.StyleRankRaw}', rt='{finalCapture.TotalRankRaw}'"; } internal string <QueueWhenReadyRoutine>b__2() { return $"Custom snapshot enqueued: pack={packKey}, level={levelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}"; } } [CompilerGenerated] private sealed class <QueueWhenReadyRoutine>d__6 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public object inst; private <>c__DisplayClass6_0 <>8__1; private float <waited>5__2; private PendingRunManager.CustomRunCapture? <completeCapture>5__3; private PendingRunManager.CustomRunCapture? <bestPartialCapture>5__4; private int <bestPartialScore>5__5; private Scratch <scratch>5__6; private bool <salvaged>5__7; private Component <component>5__8; private PendingRunManager.CustomRunCapture <capture>5__9; private string <resolvedLevelId>5__10; private string <resolvedPackKey>5__11; private string <incompleteReason>5__12; private int <partialScore>5__13; private PendingRunManager.CustomRunCapture <fallbackCapture>5__14; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <QueueWhenReadyRoutine>d__6(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <completeCapture>5__3 = null; <bestPartialCapture>5__4 = null; <scratch>5__6 = null; <component>5__8 = null; <capture>5__9 = default(PendingRunManager.CustomRunCapture); <resolvedLevelId>5__10 = null; <resolvedPackKey>5__11 = null; <incompleteReason>5__12 = null; <fallbackCapture>5__14 = default(PendingRunManager.CustomRunCapture); <>1__state = -2; } private bool MoveNext() { //IL_01a7: Unknown result type (might be due to invalid IL or missing references) //IL_01b1: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass6_0(); <waited>5__2 = 0f; <completeCapture>5__3 = null; <bestPartialCapture>5__4 = null; <bestPartialScore>5__5 = int.MinValue; <>8__1.levelId = ""; <>8__1.packKey = ""; <>8__1.bestFailureReason = "custom endscreen data never became complete"; break; case 1: <>1__state = -1; <waited>5__2 += 0.05f; <capture>5__9 = default(PendingRunManager.CustomRunCapture); <resolvedLevelId>5__10 = null; <resolvedPackKey>5__11 = null; <incompleteReason>5__12 = null; break; } if (<waited>5__2 < 3f) { if (TryBuildCapture(inst, out <capture>5__9, out <resolvedLevelId>5__10, out <resolvedPackKey>5__11, out <incompleteReason>5__12)) { if (HasCompleteEndscreenData(<capture>5__9)) { <completeCapture>5__3 = <capture>5__9; <>8__1.levelId = <resolvedLevelId>5__10; <>8__1.packKey = <resolvedPackKey>5__11; goto IL_020b; } if (HasMinimumCoreEndscreenData(<capture>5__9)) { <partialScore>5__13 = ScorePartialCapture(<capture>5__9); if (!<bestPartialCapture>5__4.HasValue || <partialScore>5__13 >= <bestPartialScore>5__5) { <bestPartialCapture>5__4 = <capture>5__9; <bestPartialScore>5__5 = <partialScore>5__13; <>8__1.levelId = <resolvedLevelId>5__10; <>8__1.packKey = <resolvedPackKey>5__11; } } <>8__1.bestFailureReason = <incompleteReason>5__12; } <>2__current = (object)new WaitForSecondsRealtime(0.05f); <>1__state = 1; return true; } goto IL_020b; IL_020b: <scratch>5__6 = GetScratch(inst); <scratch>5__6.CaptureScheduled = false; if (<scratch>5__6.Logged) { BepInExLogs_US.Debug("Custom queue routine finished without queueing a run because it was already logged."); return false; } <salvaged>5__7 = false; if (!<completeCapture>5__3.HasValue && <bestPartialCapture>5__4.HasValue) { <fallbackCapture>5__14 = SalvageCapture(<bestPartialCapture>5__4.Value); if (HasCompleteEndscreenData(<fallbackCapture>5__14)) { <completeCapture>5__3 = <fallbackCapture>5__14; <salvaged>5__7 = true; } <fallbackCapture>5__14 = default(PendingRunManager.CustomRunCapture); } if (!<completeCapture>5__3.HasValue) { BepInExLogs_US.Debug(() => "Custom queue routine timed out. Skipping save because endscreen data stayed incomplete: " + <>8__1.bestFailureReason); return false; } <>8__1.finalCapture = <completeCapture>5__3.Value; <scratch>5__6.Logged = true; PendingRunManager.EnqueueCapture(<>8__1.finalCapture); ref Component reference = ref <component>5__8; object obj = inst; reference = (Component)((obj is Component) ? obj : null); if (<component>5__8 != null) { PendingRunManager.AttachDiscardWatcher(<component>5__8); } if (<salvaged>5__7) { BepInExLogs_US.Debug(() => $"Custom snapshot salvaged after incomplete endscreen data: pack={<>8__1.packKey}, level={<>8__1.levelId}, time={<>8__1.finalCapture.TimeMs}, points={<>8__1.finalCapture.Points}, rs='{<>8__1.finalCapture.TimeRankRaw}{<>8__1.finalCapture.KillsRankRaw}{<>8__1.finalCapture.StyleRankRaw}', rt='{<>8__1.finalCapture.TotalRankRaw}'"); } else { BepInExLogs_US.Debug(() => $"Custom snapshot enqueued: pack={<>8__1.packKey}, level={<>8__1.levelId}, time={<>8__1.finalCapture.TimeMs}, points={<>8__1.finalCapture.Points}"); } return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private const string UnknownRankPlaceholder = "_"; private static readonly ConditionalWeakTable<object, Scratch> ScratchTable = new ConditionalWeakTable<object, Scratch>(); private static Scratch GetScratch(object inst) { return ScratchTable.GetOrCreateValue(inst); } public static void NoteInfo(object inst, int restarts, bool damage, bool majorUsed, bool cheatsUsed) { Scratch scratch = GetScratch(inst); scratch.Restarts = restarts; scratch.TookDamage = damage; scratch.MajorAssistsUsed = majorUsed; scratch.CheatsUsed = cheatsUsed; BepInExLogs_US.Debug(() => $"Custom NoteInfo: restarts={restarts}, damage={damage}, major={majorUsed}, cheats={cheatsUsed}"); } public static void ScheduleQueue(object inst) { if (!Plugin.CustomLevelLoggingEnabled || (Object)(object)Plugin.Instance == (Object)null) { return; } Scratch scratch = GetScratch(inst); if (!scratch.Logged && !scratch.CaptureScheduled) { LevelContext currentLevelContext = LoggerShared.GetCurrentLevelContext(); if (currentLevelContext.Source == LevelSource.Custom && currentLevelContext.IsCustom) { scratch.CaptureScheduled = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(QueueWhenReadyRoutine(inst)); } } } [IteratorStateMachine(typeof(<QueueWhenReadyRoutine>d__6))] private static IEnumerator QueueWhenReadyRoutine(object inst) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <QueueWhenReadyRoutine>d__6(0) { inst = inst }; } private static bool TryBuildCapture(object inst, out PendingRunManager.CustomRunCapture capture, out string levelId, out string packKey, out string incompleteReason) { levelId = ""; packKey = ""; incompleteReason = "unknown"; capture = default(PendingRunManager.CustomRunCapture); if (!LoggerShared.TryGetCurrentCustomLogContext(out var ctx)) { incompleteReason = "custom Angry metadata unavailable"; return false; } Scratch scratch = GetScratch(inst); float num = UltraStatsReflection.TryGetFloat(inst, "savedTime"); int val = UltraStatsReflection.TryGetInt(inst, "savedKills"); int val2 = UltraStatsReflection.TryGetInt(inst, "savedStyle"); int val3 = UltraStatsReflection.TryGetInt(inst, "totalPoints"); ChallengeManager challengeManager = GetChallengeManager(); bool challengeComplete = (Object)(object)challengeManager != (Object)null && challengeManager.challengeDone; int restarts = scratch.Restarts; bool tookDamage = scratch.TookDamage; bool majorAssistsUsed = scratch.MajorAssistsUsed; StatsManager statsManager = GetStatsManager(); if ((Object)(object)statsManager != (Object)null) { restarts = statsManager.restarts; tookDamage = statsManager.tookDamage; majorAssistsUsed = statsManager.majorUsed; } bool cheatsUsed = scratch.CheatsUsed; string timeRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "timeRank"))); string killsRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "killsRank"))); string styleRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "styleRank"))); string totalRankRaw = NormalizeRankLetter(LoggerShared.StripTags(UltraStatsReflection.TryGetTMPText(inst, "totalRank"))); levelId = ctx.LevelId; packKey = ctx.PackFolderKey; capture = new PendingRunManager.CustomRunCapture(LoggerShared.Difficulty(), LoggerShared.GetSaveSlotId(), ctx.PackFolderKey, ctx.LevelFileStem, ctx.LevelId, (num >= 0f) ? Mathf.RoundToInt(num * 1000f) : 0, Math.Max(0, val), Math.Max(0, val2), Math.Max(0, restarts), Math.Max(0, val3), timeRankRaw, killsRankRaw, styleRankRaw, totalRankRaw, tookDamage, challengeComplete, majorAssistsUsed, cheatsUsed); incompleteReason = DescribeIncompleteData(capture); return true; } private static StatsManager? GetStatsManager() { try { return MonoSingleton<StatsManager>.Instance; } catch { return null; } } private static ChallengeManager? GetChallengeManager() { try { return MonoSingleton<ChallengeManager>.Instance; } catch { return null; } } private static bool HasCompleteEndscreenData(PendingRunManager.CustomRunCapture capture) { if (!HasMinimumCoreEndscreenData(capture)) { return false; } if (!HasPersistableRank(capture.TotalRankRaw)) { return false; } if (!HasPersistableRank(capture.TimeRankRaw) || !HasPersistableRank(capture.KillsRankRaw) || !HasPersistableRank(capture.StyleRankRaw)) { return false; } return true; } private static bool HasMinimumCoreEndscreenData(PendingRunManager.CustomRunCapture capture) { if (capture.TimeMs <= 0) { return false; } if (capture.Points <= 0) { return false; } return true; } private static int ScorePartialCapture(PendingRunManager.CustomRunCapture capture) { int num = 0; if (capture.TimeMs > 0) { num += 8; } if (capture.Points > 0) { num += 8; } if (HasPersistableRank(capture.TimeRankRaw)) { num++; } if (HasPersistableRank(capture.KillsRankRaw)) { num++; } if (HasPersistableRank(capture.StyleRankRaw)) { num++; } if (HasPersistableRank(capture.TotalRankRaw)) { num++; } return num; } private static PendingRunManager.CustomRunCapture SalvageCapture(PendingRunManager.CustomRunCapture capture) { return new PendingRunManager.CustomRunCapture(capture.Difficulty, capture.SaveSlot, capture.PackFolderKey, capture.LevelFileStem, capture.LevelId, capture.TimeMs, capture.Kills, capture.Style, capture.Restarts, capture.Points, NormalizeRankForPersistence(capture.TimeRankRaw), NormalizeRankForPersistence(capture.KillsRankRaw), NormalizeRankForPersistence(capture.StyleRankRaw), NormalizeRankForPersistence(capture.TotalRankRaw), capture.TookDamage, capture.ChallengeComplete, capture.MajorAssistsUsed, capture.CheatsUsed); } private static bool HasPersistableRank(string raw) { raw = (raw ?? string.Empty).Trim().ToUpperInvariant(); return raw.Length == 1; } private static string NormalizeRankForPersistence(string raw) { raw = NormalizeRankLetter(raw); return (raw.Length == 1) ? raw : "_"; } private static string NormalizeRankLetter(string raw) { raw = (raw ?? "").Trim().ToUpperInvariant(); return (raw.Length == 1) ? raw : ""; } private static string DescribeIncompleteData(PendingRunManager.CustomRunCapture capture) { if (capture.TimeMs <= 0) { return "time not populated yet"; } if (capture.Points <= 0) { return "total points not populated yet"; } if (string.IsNullOrWhiteSpace(capture.TotalRankRaw)) { return "overall rank is empty"; } if (capture.TotalRankRaw.Length != 1) { return $"overall rank length was {capture.TotalRankRaw.Length} instead of 1"; } if (string.IsNullOrWhiteSpace(capture.TimeRankRaw)) { return "time rank is empty"; } if (string.IsNullOrWhiteSpace(capture.KillsRankRaw)) { return "kills rank is empty"; } if (string.IsNullOrWhiteSpace(capture.StyleRankRaw)) { return "style rank is empty"; } return "custom snapshot is complete"; } } [HarmonyPatch] internal static class FinalRank_SetInfo_Custom_Patch { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("FinalRank"); return AccessTools.Method(type, "SetInfo", new Type[4] { typeof(int), typeof(bool), typeof(bool), typeof(bool) }, (Type[])null); } private static void Prefix(object __instance, int restarts, bool damage, bool majorUsed, bool cheatsUsed) { if (Plugin.CustomLevelLoggingEnabled) { CustomLevelStatsLogger.NoteInfo(__instance, restarts, damage, majorUsed, cheatsUsed); } } private static void Postfix(object __instance) { if (Plugin.CustomLevelLoggingEnabled) { CustomLevelStatsLogger.ScheduleQueue(__instance); } } } [DataContract] public sealed class CgLine { [DataMember(Name = "S", Order = 0)] public int S; [DataMember(Name = "ID", Order = 1)] public long ID; [DataMember(Name = "T", Order = 2)] public long T; [DataMember(Name = "t", Order = 3)] public int t; [DataMember(Name = "k", Order = 4)] public int k; [DataMember(Name = "s", Order = 5)] public int s; [DataMember(Name = "w", Order = 6)] public int w; [DataMember(Name = "F", Order = 7, EmitDefaultValue = false)] public int F; } internal static class CybergrindDeathStatsLogger { [CompilerGenerated] private sealed class <>c__DisplayClass15_0 { public PendingRunManager.CybergrindRunCapture line; internal string <CaptureRoutine>b__0() { return $"Cybergrind snapshot enqueued: wave={line.WaveHundredths}, time={line.TimeMs}, kills={line.Kills}, style={line.Style}, flags={line.Flags}"; } } [CompilerGenerated] private sealed class <CaptureRoutine>d__15 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public FinalCyberRank rank; public int rankId; private float <waited>5__1; private PendingRunManager.CybergrindRunCapture? <best>5__2; private PendingRunManager.CybergrindRunCapture <rec>5__3; private <>c__DisplayClass15_0 <>8__4; private int <lateFlags>5__5; private int <flags>5__6; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <CaptureRoutine>d__15(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__4 = null; <>1__state = -2; } private bool MoveNext() { //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <waited>5__1 = 0f; <best>5__2 = null; break; case 1: <>1__state = -1; <waited>5__1 += 0.05f; break; } if (<waited>5__1 < 1f && (Object)(object)rank != (Object)null) { <rec>5__3 = CaptureCoreNoFlags(rank); if (!<best>5__2.HasValue || ScoreCore(<rec>5__3) > ScoreCore(<best>5__2.Value)) { <best>5__2 = <rec>5__3; } <>2__current = (object)new WaitForSecondsRealtime(0.05f); <>1__state = 1; return true; } if (<best>5__2.HasValue) { <>8__4 = new <>c__DisplayClass15_0(); <lateFlags>5__5 = (((Object)(object)rank != (Object)null) ? ResolveCyberFlags(rank) : 0); <flags>5__6 = (_cachedFlagsValid ? (_cachedFlags | <lateFlags>5__5) : <lateFlags>5__5); <>8__4.line = new PendingRunManager.CybergrindRunCapture(LoggerShared.Difficulty(), LoggerShared.GetSaveSlotId(), <best>5__2.Value.TimeMs, <best>5__2.Value.Kills, <best>5__2.Value.Style, <best>5__2.Value.WaveHundredths, <flags>5__6); if (QueuedRankIds.Contains(rankId)) { _captureInProgress = false; return false; } QueuedRankIds.Add(rankId); PendingRunManager.EnqueueCapture(<>8__4.line); PendingRunManager.AttachDiscardWatcher((Component?)(object)rank); BepInExLogs_US.Debug(() => $"Cybergrind snapshot enqueued: wave={<>8__4.line.WaveHundredths}, time={<>8__4.line.TimeMs}, kills={<>8__4.line.Kills}, style={<>8__4.line.Style}, flags={<>8__4.line.Flags}"); <>8__4 = null; } _captureInProgress = false; return false; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private static bool _captureInProgress; private static bool _cachedFlagsValid; private static int _cachedFlags; private static readonly HashSet<int> QueuedRankIds = new HashSet<int>(); private static readonly string[] WaveNames = new string[4] { "savedWaves", "SavedWaves", "preciseWaves", "PreciseWaves" }; private static readonly string[] TimeNames = new string[4] { "savedTime", "SavedTime", "time", "Time" }; private static readonly string[] KillNames = new string[8] { "savedKills", "SavedKills", "kills", "Kills", "killCount", "KillCount", "totalKills", "TotalKills" }; private static readonly string[] StyleNames = new string[8] { "savedStyle", "SavedStyle", "style", "Style", "stylePoints", "StylePoints", "savedStylePoints", "SavedStylePoints" }; private static readonly string[] SingletonTypeNames = new string[10] { "AssistController", "AssistManager", "CheatController", "CheatsController", "CheatManager", "CheatsManager", "OptionsManager", "PrefsManager", "StatsManager", "GameProgressSaver" }; private static readonly string[] MajorFlagNames = new string[5] { "majorAssistsUsed", "majorUsed", "majorAssists", "majorAssist", "usedMajorAssists" }; private static readonly string[] CheatFlagNames = new string[10] { "cheatsUsed", "cheats", "usedCheats", "cheatUsed", "cheatMode", "cheatsEnabled", "hasCheats", "usingCheats", "cheatActive", "cheatActivated" }; private static readonly string[] MajorPrefKeys = new string[4] { "major_assists", "majorAssist", "major assists", "majorAssists" }; private static readonly string[] CheatPrefKeys = new string[12] { "cheats", "cheat", "usedCheats", "cheatsUsed", "cheatsEnabled", "cheatEnabled", "keepCheatsEnabled", "hasCheats", "usingCheats", "cheatMode", "cheat_mode", "cheats_enabled" }; public static void ResetSceneState() { _captureInProgress = false; _cachedFlagsValid = false; _cachedFlags = 0; QueuedRankIds.Clear(); } public static void ScheduleCapture(FinalCyberRank rank) { if (Plugin.CybergrindLoggingEnabled && !((Object)(object)Plugin.Instance == (Object)null) && !((Object)(object)rank == (Object)null)) { int instanceID = ((Object)rank).GetInstanceID(); if (!QueuedRankIds.Contains(instanceID) && !_captureInProgress) { _captureInProgress = true; _cachedFlags = ResolveCyberFlags(rank); _cachedFlagsValid = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(CaptureRoutine(rank, instanceID)); } } } [IteratorStateMachine(typeof(<CaptureRoutine>d__15))] private static IEnumerator CaptureRoutine(FinalCyberRank rank, int rankId) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <CaptureRoutine>d__15(0) { rank = rank, rankId = rankId }; } private static int ScoreCore(PendingRunManager.CybergrindRunCapture record) { int num = 0; if (record.WaveHundredths > 0) { num += 3; } if (record.TimeMs > 0) { num += 2; } if (record.Kills > 0) { num += 2; } else if (record.Kills == 0) { num++; } if (record.Style > 0) { num += 2; } else if (record.Style == 0) { num++; } return num; } private static PendingRunManager.CybergrindRunCapture CaptureCoreNoFlags(FinalCyberRank rank) { int waveHundredths = 0; int timeMs = 0; float num = UltraStatsReflection.TryGetFloat(rank, WaveNames); if (num >= 0f) { waveHundredths = Mathf.RoundToInt(num * 100f); } float num2 = UltraStatsReflection.TryGetFloat(rank, TimeNames); if (num2 >= 0f) { timeMs = Mathf.RoundToInt(num2 * 1000f); } return new PendingRunManager.CybergrindRunCapture(0, 0, timeMs, Math.Max(0, UltraStatsReflection.TryGetInt(rank, KillNames)), Math.Max(0, UltraStatsReflection.TryGetInt(rank, StyleNames)), waveHundredths, 0); } private static int ResolveCyberFlags(FinalCyberRank rank) { bool major = false; bool cheats = false; try { StatsManager instance = MonoSingleton<StatsManager>.Instance; if ((Object)(object)instance != (Object)null) { major |= instance.majorUsed; } } catch { } try { AssistController instance2 = MonoSingleton<AssistController>.Instance; if ((Object)(object)instance2 != (Object)null) { major |= instance2.majorEnabled; cheats |= instance2.cheatsEnabled; } } catch { } TryResolveFlagsFromObject(rank, ref major, ref cheats); for (int i = 0; i < SingletonTypeNames.Length; i++) { if (major && cheats) { break; } object obj3 = UltraStatsReflection.TryGetSingletonInstance(SingletonTypeNames[i]); if (obj3 != null) { TryResolveFlagsFromObject(obj3, ref major, ref cheats); } } BepInExLogs_US.Debug(() => $"Cybergrind ResolveCyberFlags => major={major}, cheats={cheats}, packed={LoggerShared.BuildFlags(major, cheats)}"); return LoggerShared.BuildFlags(major, cheats); } private static void TryResolveFlagsFromObject(object obj, ref bool major, ref bool cheats) { if (obj == null) { return; } if (!major) { major = UltraStatsReflection.TryGetBoolExact(obj, MajorFlagNames) || UltraStatsReflection.TryGetBoolLoose(obj, "major", "assist"); } if (!cheats) { cheats = UltraStatsReflection.TryGetBoolExact(obj, CheatFlagNames) || UltraStatsReflection.TryGetBoolLoose(obj, "cheat") || TryGetCheatLikeState(obj); } string name = obj.GetType().Name; if (name.IndexOf("pref", StringComparison.OrdinalIgnoreCase) >= 0 || name.IndexOf("option", StringComparison.OrdinalIgnoreCase) >= 0) { if (!major) { major = UltraStatsReflection.TryInvokeBoolGetter(obj, MajorPrefKeys); } if (!cheats) { cheats = UltraStatsReflection.TryInvokeBoolGetter(obj, CheatPrefKeys) || TryInvokeIntGetterNonZero(obj, CheatPrefKeys); } } } private static bool TryGetCheatLikeState(object obj) { try { Type type = obj.GetType(); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.Name.IndexOf("cheat", StringComparison.OrdinalIgnoreCase) >= 0) { object value = null; try { value = fieldInfo.GetValue(obj); } catch { } if (ValueLooksEnabled(value)) { return true; } } } PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.GetIndexParameters().Length == 0 && propertyInfo.Name.IndexOf("cheat", StringComparison.OrdinalIgnoreCase) >= 0) { object value2 = null; try { value2 = propertyInfo.GetValue(obj, null); } catch { } if (ValueLooksEnabled(value2)) { return true; } } } } catch { } return false; } private static bool ValueLooksEnabled(object value) { if (value == null) { return false; } try { if (value is bool result) { return result; } if (value is int num) { return num != 0; } if (value is uint num2) { return num2 != 0; } if (value is long num3) { return num3 != 0; } if (value is float num4) { return Mathf.Abs(num4) > float.Epsilon; } if (value is double value2) { return Math.Abs(value2) > double.Epsilon; } if (value is string text) { string text2 = text.Trim(); return !string.IsNullOrWhiteSpace(text2) && !text2.Equals("false", StringComparison.OrdinalIgnoreCase) && !text2.Equals("none", StringComparison.OrdinalIgnoreCase) && !text2.Equals("0", StringComparison.OrdinalIgnoreCase); } if (value is IEnumerable enumerable) { { IEnumerator enumerator = enumerable.GetEnumerator(); try { return enumerator.MoveNext(); } finally { IDisposable disposable = enumerator as IDisposable; if (disposable != null) { disposable.Dispose(); } } } } } catch { } return false; } private static bool TryInvokeIntGetterNonZero(object obj, params string[] keys) { try { Type type = obj.GetType(); MethodInfo method = type.GetMethod("GetInt", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2] { typeof(string), typeof(int) }, null); MethodInfo method2 = type.GetMethod("GetInt", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); foreach (string text in keys) { if (method != null && method.Invoke(obj, new object[2] { text, 0 }) is int num && num != 0) { return true; } if (method2 != null && method2.Invoke(obj, new object[1] { text }) is int num2 && num2 != 0) { return true; } } } catch { } return false; } } [HarmonyPatch(typeof(FinalCyberRank))] internal static class FinalCyberRank_GameOver_Patch { [HarmonyPostfix] [HarmonyPatch("GameOver")] private static void Postfix(FinalCyberRank __instance) { if (!(SceneHelper.CurrentScene != "Endless")) { CybergrindDeathStatsLogger.ScheduleCapture(__instance); } } } internal enum LevelSource { Unknown, Campaign, Cybergrind, Custom } internal readonly struct LevelContext { public LevelSource Source { get; } public string SceneName { get; } public string LevelId { get; } public bool IsCustom { get; } public LevelContext(LevelSource source, string sceneName, string levelId, bool isCustom) { Source = source; SceneName = sceneName ?? ""; LevelId = (string.IsNullOrWhiteSpace(levelId) ? "unknown" : levelId); IsCustom = isCustom; } } internal readonly struct CustomLevelLogContext { public string PackFolderKey { get; } public string LevelFileStem { get; } public string LevelId { get; } public CustomLevelLogContext(string packFolderKey, string levelFileStem, string levelId) { PackFolderKey = (string.IsNullOrWhiteSpace(packFolderKey) ? "unknown_bundle" : packFolderKey); LevelFileStem = (string.IsNullOrWhiteSpace(levelFileStem) ? "unknown" : levelFileStem); LevelId = (string.IsNullOrWhiteSpace(levelId) ? "unknown" : levelId); } } internal static class LoggerShared { private static readonly object FileLock = new object(); private static readonly object IdLock = new object(); private static readonly object SerializerLock = new object(); private static readonly Dictionary<Type, DataContractJsonSerializer> SerializerCache = new Dictionary<Type, DataContractJsonSerializer>(); private static readonly Regex SlotRegex = new Regex("^Slot(?<n>\\d+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex LayerRegex = new Regex("^(?<layer>\\d+)-", RegexOptions.Compiled); private static readonly Regex PrimeRegex = new Regex("^P-\\d+", RegexOptions.IgnoreCase | RegexOptions.Compiled); private static readonly Regex LevelRegex = new Regex("(?<id>\\d+-\\d+[A-Za-z]?)", RegexOptions.Compiled); private static readonly Regex TagRegex = new Regex("<.*?>", RegexOptions.Compiled); private static readonly Regex WhitespaceRegex = new Regex("\\s+", RegexOptions.Compiled); private static readonly Regex HumanLevelCodeRegex = new Regex("^(?:\\d+-\\d+[A-Za-z]?|P-\\d+|[A-Z]{1,4}-\\d+[A-Za-z]?)$", RegexOptions.Compiled); public static readonly object WriteLock = new object(); public static string RootDir => Plugin.DataFolderPath; public static bool AngryAvailable => AccessTools.TypeByName("AngryLevelLoader.Managers.AngrySceneManager") != null; public static long UnixSecondsNow() { return DateTimeOffset.UtcNow.ToUnixTimeSeconds(); } public static int Difficulty() { try { return MonoSingleton<PrefsManager>.Instance.GetInt("difficulty", 0); } catch { return 0; } } public static string DifficultyDir(int difficulty) { return Path.Combine(RootDir, $"Difficulty_{difficulty}"); } public static int GetSaveSlotId() { try { string fileName = Path.GetFileName(GameProgressSaver.SavePath.TrimEnd('\\', '/')); Match match = SlotRegex.Match(fileName); if (match.Success && int.TryParse(match.Groups["n"].Value, out var result)) { return result; } return 0; } catch { return 0; } } public static string LayerFolder(string levelId) { Match match = LayerRegex.Match(levelId ?? ""); if (match.Success && int.TryParse(match.Groups["layer"].Value, out var result) && result >= 0) { return result.ToString(); } if (PrimeRegex.IsMatch(levelId ?? "")) { return "P"; } return "misc"; } public static string SanitizeFileName(string s) { if (string.IsNullOrWhiteSpace(s)) { return "unknown"; } char[] invalidFileNameChars = Path.GetInvalidFileNameChars(); foreach (char oldChar in invalidFileNameChars) { s = s.Replace(oldChar, '_'); } s = Regex.Replace(s, "\\s*_\\s*", "_"); return s.Trim(); } public static string CampaignOutputPath(int difficulty, string levelId) { string arg = SanitizeFileName(levelId); return Path.Combine(DifficultyDir(difficulty), LayerFolder(levelId), $"{arg}_{difficulty}.jsonl"); } public static string CybergrindOutputPath(int difficulty) { return Path.Combine(DifficultyDir(difficulty), $"cybergrind_{difficulty}.jsonl"); } public static string CustomOutputPath(int difficulty, string packFolderKey, string levelFileStem) { string path = SanitizeFileName(packFolderKey); string arg = SanitizeFileName(levelFileStem); return Path.Combine(DifficultyDir(difficulty), "custom", path, $"{arg}_{difficulty}.jsonl"); } public static string StripTags(string s) { if (string.IsNullOrEmpty(s)) { return ""; } return TagRegex.Replace(s, "").Trim(); } public static int BuildFlags(bool majorUsed, bool cheatsUsed) { int num = 0; if (majorUsed) { num |= 1; } if (cheatsUsed) { num |= 2; } return num; } public static LevelContext GetCurrentLevelContext() { string text = ""; try { text = SceneHelper.CurrentScene ?? ""; } catch { } string text2 = text.Trim(); bool flag = false; try { flag = SceneHelper.IsPlayingCustom; } catch { } if (string.Equals(text2, "Endless", StringComparison.OrdinalIgnoreCase) || string.Equals(text2, "Cybergrind", StringComparison.OrdinalIgnoreCase)) { return new LevelContext(LevelSource.Cybergrind, text2, "cybergrind", flag); } string levelId = ResolveCampaignLikeLevelId(text2); return new LevelContext((!flag) ? LevelSource.Campaign : LevelSource.Custom, text2, levelId, flag); } private static string ResolveCampaignLikeLevelId(string scene) { scene = (scene ?? "").Trim(); if (scene.StartsWith("Level ", StringComparison.OrdinalIgnoreCase)) { scene = scene.Substring("Level ".Length).Trim(); } if (string.Equals(scene, "Bootstrap", StringComparison.OrdinalIgnoreCase)) { return "0-1"; } Match match = LevelRegex.Match(scene); if (match.Success) { return match.Groups["id"].Value; } return SanitizeFileName((scene.Length > 0) ? scene : "unknown"); } public static long NextIdForFile(string jsonlPath) { lock (IdLock) { string path = jsonlPath + ".id"; Directory.CreateDirectory(Path.GetDirectoryName(path)); long result = 0L; if (File.Exists(path)) { long.TryParse(File.ReadAllText(path).Trim(), out result); } result++; File.WriteAllText(path, result.ToString()); return result; } } public static void AppendJsonLine<T>(string path, T value) { Directory.CreateDirectory(Path.GetDirectoryName(path)); string @string; lock (SerializerLock) { using MemoryStream memoryStream = new MemoryStream(); GetSerializer(typeof(T)).WriteObject(memoryStream, value); @string = Encoding.UTF8.GetString(memoryStream.ToArray()); } lock (FileLock) { using FileStream stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read); using StreamWriter streamWriter = new StreamWriter(stream, Encoding.UTF8); streamWriter.WriteLine(@string); } } private static DataContractJsonSerializer GetSerializer(Type type) { if (SerializerCache.TryGetValue(type, out DataContractJsonSerializer value)) { return value; } value = new DataContractJsonSerializer(type); SerializerCache[type] = value; return value; } public static string BoolText(bool value) { return value ? "true" : "false"; } public static string FormatWaveHundredths(int waveHundredths) { return ((float)waveHundredths / 100f).ToString("0.00", CultureInfo.InvariantCulture); } public static bool TryGetCurrentCustomLogContext(out CustomLevelLogContext ctx) { ctx = default(CustomLevelLogContext); LevelContext currentLevelContext = GetCurrentLevelContext(); if (currentLevelContext.Source != LevelSource.Custom || !currentLevelContext.IsCustom) { BepInExLogs_US.Debug("Custom ctx failed: not currently in a custom level."); return false; } Type type = AccessTools.TypeByName("AngryLevelLoader.Managers.AngrySceneManager"); if (type == null) { BepInExLogs_US.Debug("Custom ctx failed: AngrySceneManager type not found."); return false; } object currentLevelData = TryGetStaticMember(type, "currentLevelData"); object currentBundleContainer = TryGetStaticMember(type, "currentBundleContainer"); BepInExLogs_US.Debug(() => $"currentLevelData null = {currentLevelData == null}, currentBundleContainer null = {currentBundleContainer == null}"); string levelId = ReadStringMember(currentLevelData, "uniqueIdentifier"); string levelName = ReadStringMember(currentLevelData, "levelName"); string scenePath = ReadStringMember(currentLevelData, "scenePath"); object value = null; if (currentBundleContainer != null) { UltraStatsReflection.TryGetMember(currentBundleContainer, "bundleData", out value); } string packFolderKey = ReadStringMember(value, "bundleDataPath"); if (string.IsNullOrWhiteSpace(packFolderKey)) { string text = ReadStringMember(currentBundleContainer, "pathToTempFolder"); string fileName = Path.GetFileName((text ?? "").TrimEnd('\\', '/')); if (!string.IsNullOrWhiteSpace(fileName)) { packFolderKey = fileName; } } if (string.IsNullOrWhiteSpace(packFolderKey)) { packFolderKey = ReadStringMember(value, "bundleGuid"); } BepInExLogs_US.Debug(() => "Custom ctx values: levelId='" + levelId + "', levelName='" + levelName + "', scenePath='" + scenePath + "', packFolderKey='" + packFolderKey + "'"); if (string.IsNullOrWhiteSpace(levelId)) { levelId = currentLevelContext.LevelId; } string levelFileStem = BuildCustomLevelFileStem(levelId, levelName, scenePath); ctx = new CustomLevelLogContext(packFolderKey, levelFileStem, levelId); return true; } public static string BuildCustomLevelFileStem(string levelId, string levelName, string scenePath = "") { string text = NormalizeFileStemPart(levelId); string text2 = NormalizeFileStemPart(levelName); if (string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(scenePath)) { text2 = NormalizeFileStemPart(Path.GetFileNameWithoutExtension(scenePath)); } if (!string.IsNullOrWhiteSpace(text2)) { if (!string.IsNullOrWhiteSpace(text) && HumanLevelCodeRegex.IsMatch(text)) { if (string.Equals(text, text2, StringComparison.OrdinalIgnoreCase) || text2.StartsWith(text + "_", StringComparison.OrdinalIgnoreCase) || text2.StartsWith(text + " ", StringComparison.OrdinalIgnoreCase)) { return SanitizeFileName(text2); } return SanitizeFileName(text + "_" + text2); } return SanitizeFileName(text2); } if (!string.IsNullOrWhiteSpace(text)) { return SanitizeFileName(text); } return "unknown"; } private static string NormalizeFileStemPart(string s) { s = StripTags(s ?? ""); s = s.Replace('\r', ' ').Replace('\n', ' ').Replace('\t', ' '); s = WhitespaceRegex.Replace(s, " ").Trim(); return s; } private static object? TryGetStaticMember(Type type, params string[] names) { for (int i = 0; i < names.Length; i++) { PropertyInfo property = type.GetProperty(names[i], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetIndexParameters().Length == 0) { object value = property.GetValue(null, null); if (value != null) { return value; } } FieldInfo field = type.GetField(names[i], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { object value2 = field.GetValue(null); if (value2 != null) { return value2; } } } return null; } private static string ReadStringMember(object? obj, params string[] names) { if (obj == null) { return ""; } if (obj is string text) { return (text ?? "").Trim(); } for (int i = 0; i < names.Length; i++) { if (UltraStatsReflection.TryGetMember(obj, names[i], out object value) && value != null) { try { return Convert.ToString(value)?.Trim() ?? ""; } catch { } } } return ""; } } internal static class UltraStatsReflection { public static bool TryGetMember(object obj, string name, out object? value) { value = null; try { Type type = obj.GetType(); FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { value = field.GetValue(obj); return true; } PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetIndexParameters().Length == 0) { value = property.GetValue(obj, null); return true; } } catch { } return false; } public static int TryGetInt(object obj, params string[] names) { for (int i = 0; i < names.Length; i++) { if (!TryGetMember(obj, names[i], out object value) || value == null) { continue; } try { if (value is int result) { return result; } if (value is float num) { return Mathf.RoundToInt(num); } if (value is double) { double a = (double)value; if (true) { return (int)Math.Round(a); } } } catch { } } return -1; } public static float TryGetFloat(object obj, params string[] names) { for (int i = 0; i < names.Length; i++) { if (!TryGetMember(obj, names[i], out object value) || value == null) { continue; } try { if (value is float result) { return result; } if (value is int num) { return num; } if (value is double) { double num2 = (double)value; if (true) { return (float)num2; } } } catch { } } return -1f; } public static bool TryGetBoolExact(object obj, params string[] names) { for (int i = 0; i < names.Length; i++) { if (!TryGetMember(obj, names[i], out object value) || value == null) { continue; } try { if (value is bool result) { return result; } if (value is int) { int num = (int)value; if (true) { return num != 0; } } } catch { } } return false; } public static bool TryGetBoolLoose(object obj, params string[] tokens) { try { Type type = obj.GetType(); FieldInfo[] fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); bool flag2 = default(bool); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType != typeof(bool) && fieldInfo.FieldType != typeof(int)) { continue; } bool flag = true; for (int j = 0; j < tokens.Length; j++) { if (fieldInfo.Name.IndexOf(tokens[j], StringComparison.OrdinalIgnoreCase) < 0) { flag = false; break; } } if (flag) { object obj2 = null; try { obj2 = fieldInfo.GetValue(obj); } catch { } int num; if (obj2 is bool) { flag2 = (bool)obj2; num = 1; } else { num = 0; } if (((uint)num & (flag2 ? 1u : 0u)) != 0) { return true; } if (obj2 is int num2 && num2 != 0) { return true; } } } PropertyInfo[] properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); bool flag4 = default(bool); foreach (PropertyInfo propertyInfo in properties) { if (propertyInfo.GetIndexParameters().Length != 0 || (propertyInfo.PropertyType != typeof(bool) && propertyInfo.PropertyType != typeof(int))) { continue; } bool flag3 = true; for (int l = 0; l < tokens.Length; l++) { if (propertyInfo.Name.IndexOf(tokens[l], StringComparison.OrdinalIgnoreCase) < 0) { flag3 = false; break; } } if (flag3) { object obj4 = null; try { obj4 = propertyInfo.GetValue(obj, null); } catch { } int num3; if (obj4 is bool) { flag4 = (bool)obj4; num3 = 1; } else { num3 = 0; } if (((uint)num3 & (flag4 ? 1u : 0u)) != 0) { return true; } if (obj4 is int num4 && num4 != 0) { return true; } } } } catch { } return false; } public static bool TryInvokeBoolGetter(object obj, params string[] keys) { try { Type type = obj.GetType(); MethodInfo method = type.GetMethod("GetBool", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2] { typeof(string), typeof(bool) }, null); MethodInfo method2 = type.GetMethod("GetBool", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); MethodInfo method3 = type.GetMethod("GetInt", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[2] { typeof(string), typeof(int) }, null); MethodInfo method4 = type.GetMethod("GetInt", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[1] { typeof(string) }, null); bool flag = default(bool); bool flag2 = default(bool); foreach (string text in keys) { if (method != null) { object obj2 = method.Invoke(obj, new object[2] { text, false }); int num; if (obj2 is bool) { flag = (bool)obj2; num = 1; } else { num = 0; } if (((uint)num & (flag ? 1u : 0u)) != 0) { return true; } } if (method2 != null) { object obj3 = method2.Invoke(obj, new object[1] { text }); int num2; if (obj3 is bool) { flag2 = (bool)obj3; num2 = 1; } else { num2 = 0; } if (((uint)num2 & (flag2 ? 1u : 0u)) != 0) { return true; } } if (method3 != null && method3.Invoke(obj, new object[2] { text, 0 }) is int num3 && num3 != 0) { return true; } if (method4 != null && method4.Invoke(obj, new object[1] { text }) is int num4 && num4 != 0) { return true; } } } catch { } return false; } public static string TryGetTMPText(object inst, string fieldName) { if (!TryGetMember(inst, fieldName, out object value) || value == null) { return ""; } try { Type type = value.GetType(); MethodInfo method = type.GetMethod("GetParsedText", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method != null) { return (method.Invoke(value, null) as string) ?? ""; } return (type.GetProperty("text", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(value, null) as string) ?? ""; } catch { return ""; } } public static object? TryGetSingletonInstance(string typeName) { try { Type type = null; Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); for (int i = 0; i < assemblies.Length && type == null; i++) { try { type = assemblies[i].GetType(typeName, throwOnError: false); if (type != null) { break; } Type[] types = assemblies[i].GetTypes(); for (int j = 0; j < types.Length; j++) { if (types[j].Name == typeName) { type = types[j]; break; } } continue; } catch { continue; } } if (type == null) { return null; } string[] array = new string[6] { "Instance", "instance", "Current", "current", "Inst", "inst" }; for (int k = 0; k < array.Length; k++) { PropertyInfo property = type.GetProperty(array[k], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetIndexParameters().Length == 0) { object value = property.GetValue(null, null); if (value != null) { return value; } } FieldInfo field = type.GetField(array[k], BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { object value2 = field.GetValue(null); if (value2 != null) { return value2; } } } } catch { } return null; } } internal static class PendingRunManager { internal interface IRunCaptureSnapshot { } internal readonly struct CampaignRunCapture : IRunCaptureSnapshot { public int Difficulty { get; } public int SaveSlot { get; } public string LevelId { get; } public int TimeMs { get; } public int Kills { get; } public int Style { get; } public int Restarts { get; } public int Points { get; } public string TimeRankRaw { get; } public string KillsRankRaw { get; } public string StyleRankRaw { get; } public string TotalRankRaw { get; } public bool TookDamage { get; } public bool ChallengeComplete { get; } public bool MajorAssistsUsed { get; } public bool CheatsUsed { get; } public CampaignRunCapture(int difficulty, int saveSlot, string levelId, int timeMs, int kills, int style, int restarts, int points, string timeRankRaw, string killsRankRaw, string styleRankRaw, string totalRankRaw, bool tookDamage, bool challengeComplete, bool majorAssistsUsed, bool cheatsUsed) { Difficulty = difficulty; SaveSlot = saveSlot; LevelId = levelId ?? "unknown"; TimeMs = timeMs; Kills = kills; Style = style; Restarts = restarts; Points = points; TimeRankRaw = timeRankRaw ?? ""; KillsRankRaw = killsRankRaw ?? ""; StyleRankRaw = styleRankRaw ?? ""; TotalRankRaw = totalRankRaw ?? ""; TookDamage = tookDamage; ChallengeComplete = challengeComplete; MajorAssistsUsed = majorAssistsUsed; CheatsUsed = cheatsUsed; } } internal readonly struct CustomRunCapture : IRunCaptureSnapshot { public int Difficulty { get; } public int SaveSlot { get; } public string PackFolderKey { get; } public string LevelFileStem { get; } public string LevelId { get; } public int TimeMs { get; } public int Kills { get; } public int Style { get; } public int Restarts { get; } public int Points { get; } public string TimeRankRaw { get; } public string KillsRankRaw { get; } public string StyleRankRaw { get; } public string TotalRankRaw { get; } public bool TookDamage { get; } public bool ChallengeComplete { get; } public bool MajorAssistsUsed { get; } public bool CheatsUsed { get; } public CustomRunCapture(int difficulty, int saveSlot, string packFolderKey, string levelFileStem, string levelId, int timeMs, int kills, int style, int restarts, int points, string timeRankRaw, string killsRankRaw, string styleRankRaw, string totalRankRaw, bool tookDamage, bool challengeComplete, bool majorAssistsUsed, bool cheatsUsed) { Difficulty = difficulty; SaveSlot = saveSlot; PackFolderKey = packFolderKey ?? "unknown_bundle"; LevelFileStem = levelFileStem ?? "unknown"; LevelId = levelId ?? "unknown"; TimeMs = timeMs; Kills = kills; Style = style; Restarts = restarts; Points = points; TimeRankRaw = timeRankRaw ?? ""; KillsRankRaw = killsRankRaw ?? ""; StyleRankRaw = styleRankRaw ?? ""; TotalRankRaw = totalRankRaw ?? ""; TookDamage = tookDamage; ChallengeComplete = challengeComplete; MajorAssistsUsed = majorAssistsUsed; CheatsUsed = cheatsUsed; } } internal readonly struct CybergrindRunCapture : IRunCaptureSnapshot { public int Difficulty { get; } public int SaveSlot { get; } public int TimeMs { get; } public int Kills { get; } public int Style { get; } public int WaveHundredths { get; } public int Flags { get; } public CybergrindRunCapture(int difficulty, int saveSlot, int timeMs, int kills, int style, int waveHundredths, int flags) { Difficulty = difficulty; SaveSlot = saveSlot; TimeMs = timeMs; Kills = kills; Style = style; WaveHundredths = waveHundredths; Flags = flags; } } private enum PendingBatchState { Pending, Discarded, Saved } private sealed class PendingBatch { public Guid BatchToken { get; set; } public string OutputPath { get; set; } = ""; public object RunRecord { get; set; } = null; public Type RunRecordType { get; set; } = null; public List<object> DataLogRecords { get; } = new List<object>(); public string[] QueueLines { get; set; } = Array.Empty<string>(); public PendingBatchState State { get; set; } = PendingBatchState.Pending; } private static readonly object WorkerLock = new object(); private static readonly ConcurrentQueue<Action> WorkerQueue = new ConcurrentQueue<Action>(); private static readonly AutoResetEvent WorkerSignal = new AutoResetEvent(initialState: false); private static readonly object PendingStateLock = new object(); private static CancellationTokenSource? _workerCts; private static Thread? _workerThread; private static PendingBatch? _pendingBatch; private static bool _hasPending; public static bool HasPending { get { lock (PendingStateLock) { return _hasPending; } } } public static void Initialize() { lock (WorkerLock) { if (_workerThread == null) { _workerCts = new CancellationTokenSource(); _workerThread = new Thread((ThreadStart)delegate { WorkerLoop(_workerCts.Token); }) { Name = "ULTRASTATS.Worker", IsBackground = true }; _workerThread.Start(); BepInExLogs_US.Debug("ULTRASTATS worker thread started."); } } } public static void EnqueueCapture(IRunCaptureSnapshot snapshot) { IRunCaptureSnapshot snapshot2 = snapshot; Initialize(); WorkerQueue.Enqueue(delegate { HandleCapturedRun(snapshot2); }); WorkerSignal.Set(); } public static bool TryDiscardPending() { if (!HasPending) { return false; } Initialize(); WorkerQueue.Enqueue(DiscardPendingCore); WorkerSignal.Set(); return true; } public static void FlushPendingIfAny(string reason) { string reason2 = reason; Initialize(); WorkerQueue.Enqueue(delegate { FlushPendingCore(reason2); }); WorkerSignal.Set(); } public static void AttachDiscardWatcher(Component? owner) { if (!((Object)(object)owner == (Object)null) && (Object)(object)owner.GetComponent<PendingRunDiscardWatcher>() == (Object)null) { owner.gameObject.AddComponent<PendingRunDiscardWatcher>(); } } private static void SetHasPending(bool value) { lock (PendingStateLock) { _hasPending = value; } } private static void HandleCapturedRun(IRunCaptureSnapshot snapshot) { PendingBatch newBatch = BuildPendingBatch(snapshot); if (newBatch != null) { if (_pendingBatch != null && _pendingBatch.State == PendingBatchState.Pending) { SaveBatch(_pendingBatch, "superseded"); } _pendingBatch = newBatch; SetHasPending(value: true); for (int i = 0; i < newBatch.QueueLines.Length; i++) { BepInExLogs_US.Queue(newBatch.QueueLines[i]); } BepInExLogs_US.Debug(() => $"Queued batch {newBatch.BatchToken} for {newBatch.OutputPath}"); if (!Plugin.EndscreenDiscardEnabled) { SaveBatch(_pendingBatch, "immediate-save"); _pendingBatch = null; SetHasPending(value: false); } } } private static PendingBatch? BuildPendingBatch(IRunCaptureSnapshot snapshot) { IRunCaptureSnapshot snapshot2 = snapshot; if (snapshot2 is CampaignRunCapture) { CampaignRunCapture c = (CampaignRunCapture)(object)snapshot2; if (true) { return BuildCampaignBatch(c); } } if (snapshot2 is CustomRunCapture) { CustomRunCapture c2 = (CustomRunCapture)(object)snapshot2; if (true) { return BuildCustomBatch(c2); } } if (snapshot2 is CybergrindRunCapture) { CybergrindRunCapture c3 = (CybergrindRunCapture)(object)snapshot2; if (true) { return BuildCybergrindBatch(c3); } } BepInExLogs_US.Debug(() => "Unknown snapshot type ignored: " + snapshot2.GetType().FullName); return null; } private static PendingBatch? BuildCampaignBatch(CampaignRunCapture c) { string text = NormalizeRankLetter(LoggerShared.StripTags(c.TimeRankRaw)); string text2 = NormalizeRankLetter(LoggerShared.StripTags(c.KillsRankRaw)); string text3 = NormalizeRankLetter(LoggerShared.StripTags(c.StyleRankRaw)); string rt = NormalizeRankLetter(LoggerShared.StripTags(c.TotalRankRaw)); CampLine campLine = new CampLine { S = Math.Max(0, c.SaveSlot), T = LoggerShared.UnixSecondsNow(), t = Math.Max(0, c.TimeMs), k = Math.Max(0, c.Kills), s = Math.Max(0, c.Style), r = Math.Max(0, c.Restarts), p = Math.Max(0, c.Points), rs = text + text2 + text3, rt = rt, td = c.TookDamage, F = LoggerShared.BuildFlags(c.MajorAssistsUsed, c.CheatsUsed), c = c.ChallengeComplete }; if (!HasCompleteEndscreenData(campLine)) { return null; } string outputPath = LoggerShared.CampaignOutputPath(c.Difficulty, c.LevelId); string[] queueLines = new string[3] { $"| CAMPAIGN | Difficulty = {c.Difficulty} | Level = {c.LevelId} | Savefile = {campLine.S} | Time of Death = {campLine.T} |", $"| TimeElapsed = {campLine.t}ms | Kills = {campLine.k} | Style = {campLine.s} | Restarts = {campLine.r} | P Earned = {campLine.p} |", "| Individual Ranks = " + campLine.rs + " | Overall Rank = " + campLine.rt + " | Took Damage = " + LoggerShared.BoolText(campLine.td) + " | Challenge = " + LoggerShared.BoolText(campLine.c) + " |" }; PendingBatch pendingBatch = new PendingBatch(); pendingBatch.BatchToken = Guid.NewGuid(); pendingBatch.OutputPath = outputPath; pendingBatch.RunRecord = campLine; pendingBatch.RunRecordType = typeof(CampLine); pendingBatch.QueueLines = queueLines; return pendingBatch; } private static PendingBatch? BuildCustomBatch(CustomRunCapture c) { string text = NormalizeRankLetter(LoggerShared.StripTags(c.TimeRankRaw)); string text2 = NormalizeRankLetter(LoggerShared.StripTags(c.KillsRankRaw)); string text3 = NormalizeRankLetter(LoggerShared.StripTags(c.StyleRankRaw)); string rt = NormalizeRankLetter(LoggerShared.StripTags(c.TotalRankRaw)); CampLine line = new CampLine { S = Math.Max(0, c.SaveSlot), T = LoggerShared.UnixSecondsNow(), t = Math.Max(0, c.TimeMs), k = Math.Max(0, c.Kills), s = Math.Max(0, c.Style), r = Math.Max(0, c.Restarts), p = Math.Max(0, c.Points), rs = text + text2 + text3, rt = rt, td = c.TookDamage, F = LoggerShared.BuildFlags(c.MajorAssistsUsed, c.CheatsUsed), c = c.ChallengeComplete }; if (!HasCompleteCustomEndscreenData(line)) { BepInExLogs_US.Debug(() => $"Custom batch ignored because endscreen data stayed incomplete: time={line.t}, points={line.p}, rs='{line.rs}', rt='{line.rt}'"); return null; } string outputPath = LoggerShared.CustomOutputPath(c.Difficulty, c.PackFolderKey, c.LevelFileStem); string[] queueLines = new string[3] { $"| CUSTOM | Difficulty = {c.Difficulty} | Pack = {c.PackFolderKey} | Level = {c.LevelId} | Savefile = {line.S} |", $"| Time of Death = {line.T} | TimeElapsed = {line.t}ms | Kills = {line.k} | Style = {line.s} | Restarts = {line.r} | P Earned = {line.p} |", "| Individual Ranks = " + line.rs + " | Overall Rank = " + line.rt + " | Took Damage = " + LoggerShared.BoolText(line.td) + " | Challenge = " + LoggerShared.BoolText(line.c) + " |" }; PendingBatch pendingBatch = new PendingBatch(); pendingBatch.BatchToken = Guid.NewGuid(); pendingBatch.OutputPath = outputPath; pendingBatch.RunRecord = line; pendingBatch.RunRecordType = typeof(CampLine); pendingBatch.QueueLines = queueLines; return pendingBatch; } private static PendingBatch? BuildCybergrindBatch(CybergrindRunCapture c) { CgLine cgLine = new CgLine { S = Math.Max(0, c.SaveSlot), T = LoggerShared.UnixSecondsNow(), t = Math.Max(0, c.TimeMs), k = Math.Max(0, c.Kills), s = Math.Max(0, c.Style), w = Math.Max(0, c.WaveHundredths), F = c.Flags }; if (cgLine.t <= 0 && cgLine.w <= 0) { return null; } string outputPath = LoggerShared.CybergrindOutputPath(c.Difficulty); string[] queueLines = new string[2] { $"| CYBERGRIND | Difficulty = {c.Difficulty} | Savefile = {cgLine.S} | Time of Death = {cgLine.T} Unix Time |", $"| Wave = {LoggerShared.FormatWaveHundredths(cgLine.w)} | TimeElapsed = {cgLine.t}ms | Kills = {cgLine.k} | Style = {cgLine.s} |" }; PendingBatch pendingBatch = new PendingBatch(); pendingBatch.BatchToken = Guid.NewGuid(); pendingBatch.OutputPath = outputPath; pendingBatch.RunRecord = cgLine; pendingBatch.RunRecordType = typeof(CgLine); pendingBatch.QueueLines = queueLines; return pendingBatch; } private static void DiscardPendingCore() { if (_pendingBatch != null && _pendingBatch.State == PendingBatchState.Pending) { _pendingBatch.State = PendingBatchState.Discarded; _pendingBatch = null; SetHasPending(value: false); BepInExLogs_US.QueueCleared(); BepInExLogs_US.Debug("Pending batch discarded."); } } private static void FlushPendingCore(string reason) { if (_pendingBatch != null) { SaveBatch(_pendingBatch, reason); _pendingBatch = null; SetHasPending(value: false); } } private static void SaveBatch(PendingBatch batch, string reason) { string reason2 = reason; if (batch.State != 0) { return; } try { if (batch.RunRecord is CampLine campLine) { campLine.ID = LoggerShared.NextIdForFile(batch.OutputPath); LoggerShared.AppendJsonLine(batch.OutputPath, campLine); BepInExLogs_US.QueueAppended(campLine.ID, batch.OutputPath); } else if (batch.RunRecord is CgLine cgLine) { cgLine.ID = LoggerShared.NextIdForFile(batch.OutputPath); LoggerShared.AppendJsonLine(batch.OutputPath, cgLine); BepInExLogs_US.QueueAppended(cgLine.ID, batch.OutputPath); } batch.State = PendingBatchState.Saved; BepInExLogs_US.Debug(() => "Pending batch committed. reason = " + reason2); } catch (Exception ex) { BepInExLogs_US.Error("Failed to append run to " + batch.OutputPath, ex); } finally { BepInExLogs_US.QueueCleared(); } } private static bool HasCompleteEndscreenData(CampLine line) { if (line.t <= 0 || line.p <= 0) { return false; } if (string.IsNullOrWhiteSpace(line.rt) || line.rt.Length != 1) { return false; } if (string.IsNullOrWhiteSpace(line.rs) || line.rs.Length != 3) { return false; } return true; } private static bool HasCompleteCustomEndscreenData(CampLine line) { if (line.t <= 0 || line.p <= 0) { return false; } if (string.IsNullOrWhiteSpace(line.rt) || line.rt.Length != 1) { return false; } if (string.IsNullOrWhiteSpace(line.rs) || line.rs.Length != 3) { return false; } return true; } private static string NormalizeRankLetter(string raw) { raw = (raw ?? "").Trim().ToUpperInvariant(); return (raw.Length == 1) ? raw : ""; } private static void WorkerLoop(CancellationToken token) { while (true) { bool flag = true; Action result; while (WorkerQueue.TryDequeue(out result)) { try { result(); } catch (Exception ex) { BepInExLogs_US.Error("Worker step failed.", ex); } } if (token.IsCancellationRequested) { break; } WorkerSignal.WaitOne(250); } Action result2; while (WorkerQueue.TryDequeue(out result2)) { try { result2(); } catch (Exception ex2) { BepInExLogs_US.Error("Worker drain step failed.", ex2); } } if (_pendingBatch != null) { SaveBatch(_pendingBatch, "shutdown-drain"); _pendingBatch = null; SetHasPending(value: false); } } public static void Shutdown() { CancellationTokenSource workerCts; Thread workerThread; lock (WorkerLock) { workerCts = _workerCts; workerThread = _workerThread; _workerCts = null; _workerThread = null; } if (workerCts == null) { return; } try { workerCts.Cancel(); WorkerSignal.Set(); workerThread?.Join(1000); } catch (Exception ex2) { Exception ex3 = ex2; Exception ex = ex3; BepInExLogs_US.Debug(() => "Worker shutdown wait failed: " + ex.Message); } finally { workerCts.Dispose(); } } public static void QueuePending(string outputPath, string[] queueLines, Action commitAction) { throw new NotSupportedException("QueuePending is replaced by capture snapshots and worker-owned PendingBatch."); } } internal sealed class PendingRunDiscardWatcher : MonoBehaviour { private void Update() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Invalid comparison between Unknown and I4 //IL_004b: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.EndscreenDiscardEnabled) { return; } if (!PendingRunManager.HasPending) { Object.Destroy((Object)(object)this); } else if (((Component)this).gameObject.activeInHierarchy) { KeyCode endscreenDiscardKey = Plugin.EndscreenDiscardKey; if ((int)endscreenDiscardKey != 0 && Input.GetKeyDown(endscreenDiscardKey) && PendingRunManager.TryDiscardPending()) { Object.Destroy((Object)(object)this); } } } } [HarmonyPatch(typeof(SceneHelper))] internal static class SceneHelper_LoadScene_Patch { [HarmonyPrefix] [HarmonyPatch("LoadScene")] private static void Prefix() { PendingRunManager.FlushPendingIfAny("LoadScene"); CybergrindDeathStatsLogger.ResetSceneState(); } } [HarmonyPatch(typeof(SceneHelper))] internal static class SceneHelper_RestartScene_Patch { [HarmonyPrefix] [HarmonyPatch("RestartScene")] private static void Prefix() { PendingRunManager.FlushPendingIfAny("RestartScene"); CybergrindDeathStatsLogger.ResetSceneState(); } } [BepInPlugin("atom.ultrastats", "ULTRASTATS", "0.0.15")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { internal enum MainMenuButtonCornerOption { BottomRight, BottomLeft, TopRight, TopLeft } internal enum DefaultMainMenuTabOption { Info, Stats, Plots } internal enum StatsDefaultDifficultyOption { Harmless, Lenient, Standard, Violent, Brutal, UltrakillMustDie } internal enum StatsIdSortOrderOption { Ascending, Descending } public const string ModGuid = "atom.ultrastats"; public const string ModName = "ULTRASTATS"; public const string ModVer = "0.0.15"; internal static ManualLogSource Log; internal static StringField DataFolderParentPathField; internal static BoolField EnableCybergrindLoggingField; internal static BoolField EnableCampaignLoggingField; internal static BoolField EnableCustomLevelLoggingField; internal static BoolField EnableEndscreenDiscardField; internal static KeyCodeField DiscardPendingRunKeyField; internal static BoolField EnableDebugLoggingField; internal static EnumField<MainMenuButtonCornerOption> MainMenuButtonCornerField; internal static EnumField<DefaultMainMenuTabOption> DefaultMainMenuTabField; internal static EnumField<StatsDefaultDifficultyOption> DefaultStatsDifficultyField; internal static EnumField<StatsIdSortOrderOption> StatsIdSortOrderField; private Harmony? _harmony; private PluginConfigurator? _config; internal static Plugin? Instance { get; private set; } internal static bool DebugLoggingEnabled { get { BoolField enableDebugLoggingField = EnableDebugLoggingField; return enableDebugLoggingField != null && enableDebugLoggingField.value; } } internal static string DefaultDataFolderParentPath { get { string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); return Path.Combine(folderPath, "AtomSmasher1586"); } } internal static string DataFolderParentPath { get { StringField dataFolderParentPathField = DataFolderParentPathField; return NormalizeDataFolderParentPath((dataFolderParentPathField != null) ? dataFolderParentPathField.value : null); } } internal static string DataFolderPath => Path.Combine(DataFolderParentPath, "ULTRASTATS"); internal static bool CybergrindLoggingEnabled { get { BoolField enableCybergrindLoggingField = EnableCybergrindLoggingField; return enableCybergrindLoggingField == null || enableCybergrindLoggingField.value; } } internal static bool AngryLevelLoaderInstalled => LoggerShared.AngryAvailable; internal static bool CustomLevelLoggingEnabled { get { int result; if (AngryLevelLoaderInstalled) { BoolField enableCustomLevelLoggingField = EnableCustomLevelLoggingField; result = ((enableCustomLevelLoggingField == null || enableCustomLevelLoggingField.value) ? 1 : 0); } else { result = 0; } return (byte)result != 0; } } internal static bool CampaignLoggingEnabled { get { BoolField enableCampaignLoggingField = EnableCampaignLoggingField; return enableCampaignLoggingField == null || enableCampaignLoggingField.value; } } internal static bool EndscreenDiscardEnabled { get { BoolField enableEndscreenDiscardField = EnableEndscreenDiscardField; return enableEndscreenDiscardField != null && enableEndscreenDiscardField.value; } } internal static KeyCode EndscreenDiscardKey { get { //IL_000d: Unknown result type (might be due to invalid IL or missing references) KeyCodeField discardPendingRunKeyField = DiscardPendingRunKeyField; return (KeyCode)((discardPendingRunKeyField == null) ? 127 : ((int)discardPendingRunKeyField.value)); } } internal static MainMenuButtonCornerOption MainMenuButtonCorner => MainMenuButtonCornerField?.value ?? MainMenuButtonCornerOption.BottomRight; internal static DefaultMainMenuTabOption DefaultMainMenuTab => DefaultMainMenuTabField?.value ?? DefaultMainMenuTabOption.Info; internal static int DefaultStatsDifficultyNumber { get { StatsDefaultDifficultyOption? statsDefaultDifficultyOption = DefaultStatsDifficultyField?.value; if (1 == 0) { } int result = statsDefaultDifficultyOption switch { StatsDefaultDifficultyOption.Harmless => 0, StatsDefaultDifficultyOption.Lenient => 1, StatsDefaultDifficultyOption.Standard => 2, StatsDefaultDifficultyOption.Violent => 3, StatsDefaultDifficultyOption.Brutal => 4, StatsDefaultDifficultyOption.UltrakillMustDie => 5, _ => 2, }; if (1 == 0) { } return result; } } internal static StatsIdSortOrderOption StatsIdSortOrder => StatsIdSortOrderField?.value ?? StatsIdSortOrderOption.Ascending; private static string NormalizeDataFolderParentPath(string? rawPath) { if (string.IsNullOrWhiteSpace(rawPath)) { return DefaultDataFolderParentPath; } try { string path = Environment.ExpandEnvironmentVariables(rawPath.Trim()); string fullPath = Path.GetFullPath(path); string path2 = fullPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string fileName = Path.GetFileName(path2); if (fileName.Equals("ULTRASTATS", StringComparison.OrdinalIgnoreCase)) { string directoryName = Path.GetDirectoryName(path2); if (!string.IsNullOrWhiteSpace(directoryName)) { return directoryName; } } return fullPath; } catch { return DefaultDataFolderParentPath; } } private static ConfigHeader AddSectionHeader(ConfigPanel panel, string title, Color color, int size = 26) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Expected O, but got Unknown //IL_000b: Unknown result type (might be due to invalid IL or missing references) ConfigHeader val = new ConfigHeader(panel, title, size); val.textColor = color; return val; } private void Awake() { //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; SetupConfig(); PendingRunManager.Initialize(); MainMenuButton_US.Init(); BepInExLogs_US.ModLoaded(); BepInExLogs_US.Debug(() => "Data folder parent = " + DataFolderParentPath); BepInExLogs_US.Debug(() => "Data folder = " + DataFolderPath); BepInExLogs_US.Debug(() => $"Campaign logging enabled = {CampaignLoggingEnabled}"); BepInExLogs_US.Debug(() => $"Cybergrind logging enabled = {CybergrindLoggingEnabled}"); BepInExLogs_US.Debug(() => $"Angry Level Loader installed = {AngryLevelLoaderInstalled}"); BepInExLogs_US.Debug(() => $"Custom level logging enabled = {CustomLevelLoggingEnabled}"); BepInExLogs_US.Debug(() => $"Main menu button corner = {MainMenuButtonCorner}"); BepInExLogs_US.Debug(() => $"Main menu default tab = {DefaultMainMenuTab}"); BepInExLogs_US.Debug(() => $"Stats default difficulty = {DefaultStatsDifficultyNumber}"); BepInExLogs_US.Debug(() => $"Stats ID sort order = {StatsIdSortOrder}"); BepInExLogs_US.Debug(() => Chainloader.PluginInfos.ContainsKey("com.eternalUnion.angryLevelLoader") ? "Angry Level Loader is loaded." : "Angry Level Loader is not loaded."); if (!AngryLevelLoaderInstalled) { BepInExLogs_US.Warn("Angry Level Loader not found. Custom level logging is disabled."); } Application.quitting += OnAppQuitting; SceneManager.activeSceneChanged += OnActiveSceneChanged; _harmony = new Harmony("atom.ultrastats"); _harmony.PatchAll(); BepInExLogs_US.Debug("Harmony patches applied."); } private void SetupConfig() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Expected O, but got Unknown //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Expected O, but got Unknown //IL_0108: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Expected O, but got Unknown //IL_0128: Unknown result type (might be due to invalid IL or missing references) //IL_0132: Expected O, but got Unknown //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_032e: Unknown result type (might be due to invalid IL or missing references) //IL_034d: Unknown result type (might be due to invalid IL or missing references) //IL_0357: Expected O, but got Unknown //IL_036d: Unknown result type (might be due to invalid IL or missing references) //IL_0377: Expected O, but got Unknown //IL_0388: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Expected O, but got Unknown //IL_03a4: Unknown result type (might be due to invalid IL or missing references) //IL_03ae: Expected O, but got Unknown //IL_03ba: Unknown result type (might be due to invalid IL or missing references) //IL_03c4: Expected O, but got Unknown _config = PluginConfigurator.Create("ULTRASTATS", "atom.ultrastats"); Color color = Color32.op_Implicit(new Color32((byte)170, (byte)225, byte.MaxValue, byte.MaxValue)); Color color2 = Color32.op_Implicit(new Color32((byte)125, (byte)165, (byte)185, byte.MaxValue)); AddSectionHeader(_config.rootPanel, "Data Storage", color); DataFolderParentPathField = new StringField(_config.rootPanel, "ULTRASTATS parent folder", "data_folder_parent_path", DefaultDataFolderParentPath); DataFolderParentPathField.allowEmptyValues = false; DataFolderParentPathField.value = NormalizeDataFolderParentPath(DataFolderParentPathField.value); AddSectionHeader(_config.rootPanel, "Data Saving", color2); EnableCybergrindLoggingField = new BoolField(_config.rootPanel, "Enable Cybergrind logging", "enable_cybergrind_logging", true); EnableCampaignLoggingField = new BoolField(_config.rootPanel, "Enable campaign logging", "enable_campaign_logging", true); EnableCustomLevelLoggingField = new BoolField(_config.rootPanel, "Enable custom level logging", "enable_custom_level_logging", true); ((ConfigField)EnableCustomLevelLoggingField).interactable = AngryLevelLoaderInstalled; if (!AngryLevelLoaderInstalled) { EnableCustomLevelLoggingField.value = false; } AddSectionHeader(_config.rootPanel, "Preferences", color2); MainMenuButtonCornerField = new EnumField<MainMenuButtonCornerOption>(_config.rootPanel, "Main menu button corner (Restart)", "main_menu_button_corner", MainMenuButtonCornerOption.BottomRight); MainMenuButtonCornerField.SetEnumDisplayName(MainMenuButtonCornerOption.BottomRight, "Bottom Right"); MainMenuButtonCornerField.SetEnumDisplayName(MainMenuButtonCornerOption.BottomLeft, "Bottom Left"); MainMenuButtonCornerField.SetEnumDisplayName(MainMenuButtonCornerOption.TopRight, "Top Right"); MainMenuButtonCornerField.SetEnumDisplayName(MainMenuButtonCornerOption.TopLeft, "Top Left"); MainMenuButtonCornerField.onValueChange += delegate { MainMenuButton_US.RefreshButtonPosition(); }; DefaultMainMenuTabField = new EnumField<DefaultMainMenuTabOption>(_config.rootPanel, "Main menu button default tab", "default_main_menu_tab", DefaultMainMenuTabOption.Info); DefaultMainMenuTabField.SetEnumDisplayName(DefaultMainMenuTabOption.Info, "Info"); DefaultMainMenuTabField.SetEnumDisplayName(DefaultMainMenuTabOption.Stats, "Stats"); DefaultMainMenuTabField.SetEnumDisplayName(DefaultMainMenuTabOption.Plots, "Plots"); DefaultStatsDifficultyField = new EnumField<StatsDefaultDifficultyOption>(_config.rootPanel, "Stats tab default difficulty", "default_stats_difficulty", StatsDefaultDifficultyOption.Standard); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.Harmless, "Harmless"); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.Lenient, "Lenient"); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.Standard, "Standard"); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.Violent, "Violent"); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.Brutal, "Brutal"); DefaultStatsDifficultyField.SetEnumDisplayName(StatsDefaultDifficultyOption.UltrakillMustDie, "UKMD"); StatsIdSortOrderField = new EnumField<StatsIdSortOrderOption>(_config.rootPanel, "Stats ID order", "stats_id_sort_order", StatsIdSortOrderOption.Ascending); StatsIdSortOrderField.SetEnumDisplayName(StatsIdSortOrderOption.Ascending, "Ascending"); StatsIdSortOrderField.SetEnumDisplayName(StatsIdSortOrderOption.Descending, "Descending"); AddSectionHeader(_config.rootPanel, "Debug", color); EnableDebugLoggingField = new BoolField(_config.rootPanel, "Enable debug logging", "enable_debug_logging", false); EnableEndscreenDiscardField = new BoolField(_config.rootPanel, "Allow discard on endscreen", "allow_discard_on_endscreen", false); ConfigDivision discardDivision = new ConfigDivision(_config.rootPanel, "discard_division"); DiscardPendingRunKeyField = new KeyCodeField((ConfigPanel)(object)discardDivision, "Discard pending run key", "discard_pending_run_key", (KeyCode)127); EnableEndscreenDiscardField.onValueChange += (BoolValueChangeEventDelegate)delegate(BoolValueChangeEvent e) { ((ConfigField)discardDivision).interactable = e.value; }; ((ConfigField)discardDivision).interactable = EnableEndscreenDiscardField.value; try { Directory.CreateDirectory(DataFolderPath); } catch (Exception arg) { Log.LogWarning((object)$"[ULTRASTATS] Could not create configured data folder, falling back to default: {arg}"); DataFolderParentPathField.value = DefaultDataFolderParentPath; Directory.CreateDirectory(DataFolderPath); } string directoryName = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); string text = Path.Combine(directoryName, "icon.png"); if (File.Exists(text)) { string absoluteUri = new Uri(text).AbsoluteUri; Log.LogInfo((object)("[ULTRASTATS] Icon uri: " + absoluteUri)); _config.SetIconWithURL(absoluteUri); } else { Log.LogWarning((object)("[ULTRASTATS] icon.png not found at: " + text)); } } private static void OnAppQuitting() { PendingRunManager.FlushPendingIfAny("Application.quitting"); } private static void OnActiveSceneChanged(Scene oldScene, Scene newScene) { PendingRunManager.FlushPendingIfAny("SceneManager.activeSceneChanged"); } private void OnDestroy() { SceneManager.activeSceneChanged -= OnActiveSceneChanged; Application.quitting -= OnAppQuitting; MainMenuButton_US.Shutdown(); PendingRunManager.FlushPendingIfAny("Plugin.OnDestroy"); PendingRunManager.Shutdown(); Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class BepInExLogs_US { private static bool DebugEnabled => Plugin.DebugLoggingEnabled; [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ModLoaded() { Plugin.Log.LogInfo((object)"[ULTRASTATS] : Mod loaded"); } [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Queue(string message) { Plugin.Log.LogInfo((object)("[Queue] : " + message)); } [MethodImpl(MethodImplOptions