Decompiled source of ULTRASTATS beta v0.0.10
ULTRASTATS.dll
Decompiled 6 months 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.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using PluginConfig.API; using PluginConfig.API.Decorators; using PluginConfig.API.Fields; using TMPro; using UnityEngine; 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+72b29ade396e589014832ce2aeba7e99834ed95a")] [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__DisplayClass5_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 enqueued: level={finalCapture.LevelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}"; } } [CompilerGenerated] private sealed class <QueueWhenReadyRoutine>d__5 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public object inst; private <>c__DisplayClass5_0 <>8__1; private float <waited>5__2; private PendingRunManager.CampaignRunCapture? <completeCapture>5__3; private Scratch <scratch>5__4; private Component <component>5__5; private PendingRunManager.CampaignRunCapture <capture>5__6; private string <incompleteReason>5__7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <QueueWhenReadyRoutine>d__5(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <completeCapture>5__3 = null; <scratch>5__4 = null; <component>5__5 = null; <capture>5__6 = default(PendingRunManager.CampaignRunCapture); <incompleteReason>5__7 = null; <>1__state = -2; } private bool MoveNext() { //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass5_0(); <waited>5__2 = 0f; <completeCapture>5__3 = null; <>8__1.bestFailureReason = "campaign endscreen data never became complete"; break; case 1: <>1__state = -1; <waited>5__2 += 0.05f; <capture>5__6 = default(PendingRunManager.CampaignRunCapture); <incompleteReason>5__7 = null; break; } if (<waited>5__2 < 3f) { if (TryBuildCapture(inst, out <capture>5__6, out <incompleteReason>5__7)) { if (HasCompleteEndscreenData(<capture>5__6)) { <completeCapture>5__3 = <capture>5__6; goto IL_0106; } <>8__1.bestFailureReason = <incompleteReason>5__7; } <>2__current = (object)new WaitForSecondsRealtime(0.05f); <>1__state = 1; return true; } goto IL_0106; IL_0106: <scratch>5__4 = GetScratch(inst); <scratch>5__4.CaptureScheduled = false; if (<scratch>5__4.Logged) { UltraStatsLog.Debug("Campaign queue routine finished without queueing a run because it was already logged."); return false; } if (!<completeCapture>5__3.HasValue) { UltraStatsLog.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__4.Logged = true; PendingRunManager.EnqueueCapture(<>8__1.finalCapture); ref Component reference = ref <component>5__5; object obj = inst; reference = (Component)((obj is Component) ? obj : null); if (<component>5__5 != null) { PendingRunManager.AttachDiscardWatcher(<component>5__5); } UltraStatsLog.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 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; UltraStatsLog.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__5))] private static IEnumerator QueueWhenReadyRoutine(object inst) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <QueueWhenReadyRoutine>d__5(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 (capture.TimeMs <= 0) { return false; } if (capture.Points <= 0) { return false; } if (string.IsNullOrWhiteSpace(capture.TotalRankRaw) || capture.TotalRankRaw.Length != 1) { return false; } if (string.IsNullOrWhiteSpace(capture.TimeRankRaw) || string.IsNullOrWhiteSpace(capture.KillsRankRaw) || string.IsNullOrWhiteSpace(capture.StyleRankRaw)) { return false; } return true; } 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"; } return "unknown incomplete state"; } } [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__DisplayClass5_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 enqueued: pack={packKey}, level={levelId}, time={finalCapture.TimeMs}, points={finalCapture.Points}"; } } [CompilerGenerated] private sealed class <QueueWhenReadyRoutine>d__5 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public object inst; private <>c__DisplayClass5_0 <>8__1; private float <waited>5__2; private PendingRunManager.CustomRunCapture? <completeCapture>5__3; private Scratch <scratch>5__4; private Component <component>5__5; private PendingRunManager.CustomRunCapture <capture>5__6; private string <resolvedLevelId>5__7; private string <resolvedPackKey>5__8; private string <incompleteReason>5__9; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <QueueWhenReadyRoutine>d__5(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <>8__1 = null; <completeCapture>5__3 = null; <scratch>5__4 = null; <component>5__5 = null; <capture>5__6 = default(PendingRunManager.CustomRunCapture); <resolvedLevelId>5__7 = null; <resolvedPackKey>5__8 = null; <incompleteReason>5__9 = null; <>1__state = -2; } private bool MoveNext() { //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_010a: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>8__1 = new <>c__DisplayClass5_0(); <waited>5__2 = 0f; <completeCapture>5__3 = null; <>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__6 = default(PendingRunManager.CustomRunCapture); <resolvedLevelId>5__7 = null; <resolvedPackKey>5__8 = null; <incompleteReason>5__9 = null; break; } if (<waited>5__2 < 3f) { if (TryBuildCapture(inst, out <capture>5__6, out <resolvedLevelId>5__7, out <resolvedPackKey>5__8, out <incompleteReason>5__9)) { if (HasCompleteEndscreenData(<capture>5__6)) { <completeCapture>5__3 = <capture>5__6; <>8__1.levelId = <resolvedLevelId>5__7; <>8__1.packKey = <resolvedPackKey>5__8; goto IL_0162; } <>8__1.bestFailureReason = <incompleteReason>5__9; } <>2__current = (object)new WaitForSecondsRealtime(0.05f); <>1__state = 1; return true; } goto IL_0162; IL_0162: <scratch>5__4 = GetScratch(inst); <scratch>5__4.CaptureScheduled = false; if (<scratch>5__4.Logged) { UltraStatsLog.Debug("Custom queue routine finished without queueing a run because it was already logged."); return false; } if (!<completeCapture>5__3.HasValue) { UltraStatsLog.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__4.Logged = true; PendingRunManager.EnqueueCapture(<>8__1.finalCapture); ref Component reference = ref <component>5__5; object obj = inst; reference = (Component)((obj is Component) ? obj : null); if (<component>5__5 != null) { PendingRunManager.AttachDiscardWatcher(<component>5__5); } UltraStatsLog.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 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; UltraStatsLog.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__5))] private static IEnumerator QueueWhenReadyRoutine(object inst) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <QueueWhenReadyRoutine>d__5(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 (capture.TimeMs <= 0) { return false; } if (capture.Points <= 0) { return false; } if (string.IsNullOrWhiteSpace(capture.TotalRankRaw) || capture.TotalRankRaw.Length != 1) { return false; } if (string.IsNullOrWhiteSpace(capture.TimeRankRaw) || string.IsNullOrWhiteSpace(capture.KillsRankRaw) || string.IsNullOrWhiteSpace(capture.StyleRankRaw)) { return false; } return true; } 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"; } return "unknown incomplete state"; } } [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 <flags>5__5; 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(); <flags>5__5 = (((Object)(object)rank != (Object)null) ? ResolveCyberFlagsCached(rank) : 0); <>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__5); if (QueuedRankIds.Contains(rankId)) { _captureInProgress = false; return false; } QueuedRankIds.Add(rankId); PendingRunManager.EnqueueCapture(<>8__4.line); PendingRunManager.AttachDiscardWatcher((Component?)(object)rank); UltraStatsLog.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[6] { "cheatsUsed", "cheats", "usedCheats", "cheatUsed", "cheatMode", "cheatsEnabled" }; private static readonly string[] MajorPrefKeys = new string[4] { "major_assists", "majorAssist", "major assists", "majorAssists" }; private static readonly string[] CheatPrefKeys = new string[5] { "cheats", "cheat", "usedCheats", "cheatsEnabled", "keepCheatsEnabled" }; 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; ((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 ResolveCyberFlagsCached(FinalCyberRank rank) { if (_cachedFlagsValid) { return _cachedFlags; } _cachedFlags = ResolveCyberFlags(rank); _cachedFlagsValid = true; return _cachedFlags; } private static int ResolveCyberFlags(FinalCyberRank rank) { bool major = false; bool cheats = false; TryResolveFlagsFromObject(rank, ref major, ref cheats); for (int i = 0; i < SingletonTypeNames.Length; i++) { if (major && cheats) { break; } object obj = UltraStatsReflection.TryGetSingletonInstance(SingletonTypeNames[i]); if (obj != null) { TryResolveFlagsFromObject(obj, ref major, ref 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"); } 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); } } } } [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) { UltraStatsLog.Debug("Custom ctx failed: not currently in a custom level."); return false; } Type type = AccessTools.TypeByName("AngryLevelLoader.Managers.AngrySceneManager"); if (type == null) { UltraStatsLog.Debug("Custom ctx failed: AngrySceneManager type not found."); return false; } object currentLevelData = TryGetStaticMember(type, "currentLevelData"); object currentBundleContainer = TryGetStaticMember(type, "currentBundleContainer"); UltraStatsLog.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"); } UltraStatsLog.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(); UltraStatsLog.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++) { UltraStatsLog.Queue(newBatch.QueueLines[i]); } UltraStatsLog.Debug(() => $"Queued batch {newBatch.BatchToken} for {newBatch.OutputPath}"); } } 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); } } UltraStatsLog.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 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.CustomOutputPath(c.Difficulty, c.PackFolderKey, c.LevelFileStem); string[] queueLines = new string[3] { $"| CUSTOM | Difficulty = {c.Difficulty} | Pack = {c.PackFolderKey} | 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? 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); UltraStatsLog.QueueCleared(); UltraStatsLog.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); UltraStatsLog.QueueAppended(campLine.ID, batch.OutputPath); } else if (batch.RunRecord is CgLine cgLine) { cgLine.ID = LoggerShared.NextIdForFile(batch.OutputPath); LoggerShared.AppendJsonLine(batch.OutputPath, cgLine); UltraStatsLog.QueueAppended(cgLine.ID, batch.OutputPath); } batch.State = PendingBatchState.Saved; UltraStatsLog.Debug(() => "Pending batch committed. reason = " + reason2); } catch (Exception ex) { UltraStatsLog.Error("Failed to append run to " + batch.OutputPath, ex); } finally { UltraStatsLog.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 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) { UltraStatsLog.Error("Worker step failed.", ex); } } if (token.IsCancellationRequested) { break; } WorkerSignal.WaitOne(250); } Action result2; while (WorkerQueue.TryDequeue(out result2)) { try { result2(); } catch (Exception ex2) { UltraStatsLog.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; UltraStatsLog.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.10")] public class Plugin : BaseUnityPlugin { public const string ModGuid = "atom.ultrastats"; public const string ModName = "ULTRASTATS"; public const string ModVer = "0.0.10"; 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; 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 CustomLevelLoggingEnabled { get { BoolField enableCustomLevelLoggingField = EnableCustomLevelLoggingField; return enableCustomLevelLoggingField == null || enableCustomLevelLoggingField.value; } } 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)); } } 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_0104: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; SetupConfig(); if (!LoggerShared.AngryAvailable) { UltraStatsLog.Warn("Angry Level Loader not found. Custom level logging is disabled."); } PendingRunManager.Initialize(); MainMenuButtonInjector.Init(); UltraStatsLog.ModLoaded(); UltraStatsLog.Debug(() => "Data folder parent = " + DataFolderParentPath); UltraStatsLog.Debug(() => "Data folder = " + DataFolderPath); UltraStatsLog.Debug(() => $"Campaign logging enabled = {CampaignLoggingEnabled}"); UltraStatsLog.Debug(() => $"Cybergrind logging enabled = {CybergrindLoggingEnabled}"); UltraStatsLog.Debug(() => $"Custom level logging enabled = {CustomLevelLoggingEnabled}"); _harmony = new Harmony("atom.ultrastats"); _harmony.PatchAll(); UltraStatsLog.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_015d: Unknown result type (might be due to invalid IL or missing references) //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Expected O, but got Unknown //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Expected O, but got Unknown //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01c1: Expected O, but got Unknown //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Expected O, but got Unknown //IL_01e9: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: 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); if (!LoggerShared.AngryAvailable) { UltraStatsLog.Warn("Angry Level Loader compatibility check failed. Custom logging may not work."); } 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 void OnDestroy() { MainMenuButtonInjector.Shutdown(); PendingRunManager.FlushPendingIfAny("Plugin.OnDestroy"); PendingRunManager.Shutdown(); Harmony? harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal static class MainMenuButtonInjector { [CompilerGenerated] private sealed class <InjectIntoMenuScene>d__8 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public Scene scene; private int <i>5__1; private GameObject <canvasRoot>5__2; private Transform <mainMenu>5__3; private Transform <leftSide>5__4; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <InjectIntoMenuScene>d__8(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { <canvasRoot>5__2 = null; <mainMenu>5__3 = null; <leftSide>5__4 = null; <>1__state = -2; } private bool MoveNext() { //IL_0053: Unknown result type (might be due to invalid IL or missing references) switch (<>1__state) { default: return false; case 0: <>1__state = -1; _injecting = true; <i>5__1 = 0; break; case 1: <>1__state = -1; <canvasRoot>5__2 = null; <i>5__1++; break; } if (<i>5__1 < 600) { if (!((Scene)(ref scene)).isLoaded) { _injecting = false; return false; } <canvasRoot>5__2 = FindRootCanvasObject(scene); if ((Object)(object)<canvasRoot>5__2 != (Object)null) { UltraStatsLog.Debug("Found root canvas object: " + ((Object)<canvasRoot>5__2).name); <mainMenu>5__3 = <canvasRoot>5__2.transform.Find("Main Menu (1)"); Transform obj = <mainMenu>5__3; <leftSide>5__4 = ((obj != null) ? obj.Find("LeftSide") : null); UltraStatsLog.Debug($"mainMenu found = {(Object)(object)<mainMenu>5__3 != (Object)null}, leftSide found = {(Object)(object)<leftSide>5__4 != (Object)null}"); if ((Object)(object)<mainMenu>5__3 != (Object)null && (Object)(object)<leftSide>5__4 != (Object)null) { Inject(<leftSide>5__4, <mainMenu>5__3); _injecting = false; return false; } <mainMenu>5__3 = null; <leftSide>5__4 = null; } <>2__current = null; <>1__state = 1; return true; } UltraStatsLog.Warn("ULTRASTATS could not find Canvas/Main Menu (1)/LeftSide."); _injecting = 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 const string MenuSceneName = "b3e7f2f8052488a45b35549efb98d902"; private const string ButtonObjectName = "ULTRASTATS_MenuButton"; private const string PanelObjectName = "ULTRASTATS_MenuPanel"; private static bool _subscribed; private static bool _injecting; public static void Init() { if (!_subscribed) { _subscribed = true; SceneManager.activeSceneChanged += OnActiveSceneChanged; UltraStatsLog.Debug("MainMenuButtonInjector.Init called"); } } public static void Shutdown() { if (_subscribed) { _subscribed = false; SceneManager.activeSceneChanged -= OnActiveSceneChanged; } } private static void OnActiveSceneChanged(Scene oldScene, Scene newScene) { //IL_0076: Unknown result type (might be due to invalid IL or missing references) UltraStatsLog.Debug("Scene changed: '" + ((Scene)(ref oldScene)).name + "' -> '" + ((Scene)(ref newScene)).name + "'"); if (!((Object)(object)Plugin.Instance == (Object)null) && !(((Scene)(ref newScene)).name != "b3e7f2f8052488a45b35549efb98d902") && !_injecting) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(InjectIntoMenuScene(newScene)); } } [IteratorStateMachine(typeof(<InjectIntoMenuScene>d__8))] private static IEnumerator InjectIntoMenuScene(Scene scene) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new <InjectIntoMenuScene>d__8(0) { scene = scene }; } private static Canvas? FindCanvasInScene(Scene scene) { GameObject[] rootGameObjects = ((Scene)(ref scene)).GetRootGameObjects(); for (int i = 0; i < rootGameObjects.Length; i++) { Canvas val = rootGameObjects[i].GetComponentsInChildren<Canvas>(true).FirstOrDefault(); if ((Object)(object)val != (Object)null) { return val; } } return null; } private static GameObject? FindRootCanvasObject(Scene scene) { GameObject[] rootGameObjects = ((Scene)(ref scene)).GetRootGameObjects(); foreach (GameObject val in rootGameObjects) { if (((Object)val).name == "Canvas") { return val; } } return null; } private static void CreateButton(Transform canvasTransform) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Expected O, but got Unknown //IL_013b: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Expected O, but got Unknown //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0188: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_01e5: Unknown result type (might be due to invalid IL or missing references) Transform canvasTransform2 = canvasTransform; GameObject val = new GameObject("ULTRASTATS_MenuButton", new Type[3] { typeof(RectTransform), typeof(Image), typeof(Button) }); val.transform.SetParent(canvasTransform2, false); RectTransform component = val.GetComponent<RectTransform>(); component.anchorMin = new Vector2(0f, 0.5f); component.anchorMax = new Vector2(0f, 0.5f); component.pivot = new Vector2(0f, 0.5f); component.sizeDelta = new Vector2(420f, 72f); component.anchoredPosition = new Vector2(100f, -260f); Image component2 = val.GetComponent<Image>(); ((Graphic)component2).color = new Color(0f, 0f, 0f, 0.95f); Button component3 = val.GetComponent<Button>(); ((UnityEvent)component3.onClick).AddListener((UnityAction)delegate { TogglePanel(canvasTransform2); }); GameObject val2 = new GameObject("Label", new Type[2] { typeof(RectTransform), typeof(TextMeshProUGUI) }); val2.transform.SetParent(val.transform, false); RectTransform component4 = val2.GetComponent<RectTransform>(); component4.anchorMin = Vector2.zero; component4.anchorMax = Vector2.one; component4.offsetMin = Vector2.zero; component4.offsetMax = Vector2.zero; TextMeshProUGUI component5 = val2.GetComponent<TextMeshProUGUI>(); ApplyLabelStyle(canvasTransform2, component5); ((TMP_Text)component5).text = "ULTRASTATS"; ((TMP_Text)component5).alignment = (TextAlignmentOptions)514; Outline val3 = val.AddComponent<Outline>(); ((Shadow)val3).effectColor = Color.white; ((Shadow)val3).effectDistance = new Vector2(2f, -2f); } private static void TogglePanel(Transform mainMenu) { Transform val = mainMenu.Find("ULTRASTATS_MenuPanel"); if ((Object)(object)val != (Object)null) { ((Component)val).gameObject.SetActive(!((Component)val).gameObject.activeSelf); } else { BuildPanel(mainMenu); } } private static void BuildPanel(Transform mainMenu) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Expected O, but got Unknown //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_016e: Unknown result type (might be due to invalid IL or missing references) //IL_0185: Unknown result type (might be due to invalid IL or missing references) //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01b3: Unknown result type (might be due to invalid IL or missing references) //IL_021c: Unknown result type (might be due to invalid IL or missing references) //IL_0223: Expected O, but got Unknown //IL_024c: Unknown result type (might be due to invalid IL or missing references) //IL_0263: Unknown result type (might be due to invalid IL or missing references) //IL_027a: Unknown result type (might be due to invalid IL or missing references) //IL_0291: Unknown result type (might be due to invalid IL or missing references) //IL_02a7: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("ULTRASTATS_MenuPanel", new Type[3] { typeof(RectTransform), typeof(Image), typeof(Outline) }); val.transform.SetParent(mainMenu, false); RectTransform component = val.GetComponent<RectTransform>(); component.anchorMin = new Vector2(0.5f, 0.5f); component.anchorMax = new Vector2(0.5f, 0.5f); component.pivot = new Vector2(0.5f, 0.5f); component.sizeDelta = new Vector2(860f, 420f); component.anchoredPosition = Vector2.zero; Image component2 = val.GetComponent<Image>(); ((Graphic)component2).color = new Color(0f, 0f, 0f, 0.975f); Outline component3 = val.GetComponent<Outline>(); ((Shadow)component3).effectColor = Color.white; ((Shadow)component3).effectDistance = new Vector2(2f, -2f); GameObject val2 = new GameObject("Title", new Type[2] { typeof(RectTransform), typeof(TextMeshProUGUI) }); val2.transform.SetParent(val.transform, false); RectTransform component4 = val2.GetComponent<RectTransform>(); component4.anchorMin = new Vector2(0f, 1f); component4.anchorMax = new Vector2(1f, 1f); component4.pivot = new Vector2(0.5f, 1f); component4.offsetMin = new Vector2(36f, -90f); component4.offsetMax = new Vector2(-36f, -24f); TextMeshProUGUI component5 = val2.GetComponent<TextMeshProUGUI>(); ApplyPanelTextStyle(mainMenu, component5); ((TMP_Text)component5).fontSize = 30f; ((TMP_Text)component5).alignment = (TextAlignmentOptions)257; ((TMP_Text)component5).text = "ULTRASTATS BETA"; GameObject val3 = new GameObject("BodyText", new Type[2] { typeof(RectTransform), typeof(TextMeshProUGUI) }); val3.transform.SetParent(val.transform, false); RectTransform component6 = val3.GetComponent<RectTransform>(); component6.anchorMin = new Vector2(0f, 0f); component6.anchorMax = new Vector2(1f, 1f); component6.offsetMin = new Vector2(36f, 36f); component6.offsetMax = new Vector2(-36f, -100f); component.sizeDelta = new Vector2(1000f, 700f); TextMeshProUGUI component7 = val3.GetComponent<TextMeshProUGUI>(); ApplyPanelTextStyle(mainMenu, component7); ((TMP_Text)component7).fontSize = 18f; ((TMP_Text)component7).alignment = (TextAlignmentOptions)257; ((TMP_Text)component7).enableWordWrapping = true; ((TMP_Text)component7).lineSpacing = 8f; ((TMP_Text)component7).text = "Thank you for playtesting ULTRASTATS, ULTRAKILL’s only all in one stat tracking, performance analysis, and data visualization mod.\n\nThis is an ambitious project, when ULTRASTATS is finished I genuinely believe it could be the most valuable tool in an ULTRAKILL player’s arsenal, being able to determine your strong suits, your weaknesses. What weapons you over or under use. Etc. I want to be able to improve the skills of everyone, from the best players to beginners, and share the joy of getting better at something.\n\nOf course working on a project of this size takes time, It will probably take several months until ULTRASTATS is in a finished form. Though I have been having so much fun that I’m confident I’ll keep working on it after it is finished.\n\nCurrently the mod supports recording run data from the cybergrind, campaign levels, and even custom Angry levels (having Angry level loader is recommended, but there should be no issues if you don’t have it installed.)\n\nMuch more data should be able to be recorded in the future, but I spent extra time perfecting recording, processing and storing run data because essentially it is the backbone of all other data types. All further data will have a short code with it that can be used to locate the run that data originated from.\n\nMany of the choices I made were to lower the filesize of a run: Cybergrind runs are only 64B, Campaign and custom level runs are 107B.\n\nI am trying to make ULTRASTATS the best tool it can be for everyone. Any criticism, suggestions, ideas, whatever, you may have about ULTRASTATS feel free to shoot me a discord dm: atomsmasher_1586\n\nP.S. this button will bring up data vis and graphing tools when I add those features."; } private static void ApplyLabelStyle(Transform canvasTransform, TextMeshProUGUI target) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI val = ((IEnumerable<TextMeshProUGUI>)((Component)canvasTransform).GetComponentsInChildren<TextMeshProUGUI>(true)).FirstOrDefault((Func<TextMeshProUGUI, bool>)delegate(TextMeshProUGUI t) { string text = (((TMP_Text)t).text ?? "").Trim().ToUpperInvariant(); int result; switch (text) { default: result = ((text == "QUIT") ? 1 : 0); break; case "PLAY": case "OPTIONS": case "MUSEUM": result = 1; break; } return (byte)result != 0; }); if ((Object)(object)val != (Object)null) { ((TMP_Text)target).font = ((TMP_Text)val).font; ((TMP_Text)target).fontSharedMaterial = ((TMP_Text)val).fontSharedMaterial; ((Graphic)target).color = ((Graphic)val).color; ((TMP_Text)target).fontSize = ((TMP_Text)val).fontSize; ((TMP_Text)target).enableWordWrapping = false; } else { ((TMP_Text)target).fontSize = 32f; ((Graphic)target).color = Color.white; } } private static void ApplyButtonLabelStyle(Transform root, TextMeshProUGUI target) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI val = ((IEnumerable<TextMeshProUGUI>)((Component)root).GetComponentsInChildren<TextMeshProUGUI>(true)).FirstOrDefault((Func<TextMeshProUGUI, bool>)delegate(TextMeshProUGUI t) { string text = (((TMP_Text)t).text ?? "").Trim().ToUpperInvariant(); int result; switch (text) { default: result = ((text == "QUIT") ? 1 : 0); break; case "PLAY": case "OPTIONS": case "MUSEUM": result = 1; break; } return (byte)result != 0; }); if ((Object)(object)val != (Object)null) { ((TMP_Text)target).font = ((TMP_Text)val).font; } ((Graphic)target).color = Color.white; ((TMP_Text)target).enableWordWrapping = false; } private static void ApplyPanelTextStyle(Transform root, TextMeshProUGUI target) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) TextMeshProUGUI val = ((IEnumerable<TextMeshProUGUI>)((Component)root).GetComponentsInChildren<TextMeshProUGUI>(true)).FirstOrDefault((Func<TextMeshProUGUI, bool>)delegate(TextMeshProUGUI t) { string text = (((TMP_Text)t).text ?? "").Trim().ToUpperInvariant(); int result; switch (text) { default: result = ((text == "QUIT") ? 1 : 0); break; case "PLAY": case "OPTIONS": case "MUSEUM": result = 1; break; } return (byte)result != 0; }); if ((Object)(object)val != (Object)null) { ((TMP_Text)target).font = ((TMP_Text)val).font; } ((Graphic)target).color = Color.white; ((TMP_Text)target).enableWordWrapping = true; } private static void Inject(Transform leftSide, Transform mainMenu) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Expected O, but got Unknown //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0124: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0171: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Expected O, but got Unknown //IL_01a1: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Expected O, but got Unknown //IL_01d1: Unknown result type (might be due to invalid IL or missing references) //IL_01e8: Unknown result type (might be du