Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of REPOrt v0.1.1
plugins/Kai-REPOrt/REPOrt.dll
Decompiled 9 months agousing System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("")] [assembly: AssemblyCompany("Kai")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("REPOrt")] [assembly: AssemblyTitle("REPOrt")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [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 REPOrt { public static class DeliveryTracker { private class StationSnapshot { public string StationName; public int HaulGoal; public int HaulTotal; public DateTime Time; public List<ExtractedItem> ExtractedItems { get; set; } } private class StationRuntime { public string StationName; public int HaulGoal; public int HaulTotal; public DateTime LastUpdate; public StationRuntime(string name) { StationName = name; } } [HarmonyPatch(typeof(PhysGrabObjectImpactDetector), "DestroyObject")] private class Patch_DestroyObject { private static void Prefix(PhysGrabObjectImpactDetector __instance, bool effects) { if ((Object)(object)__instance?.valuableObject != (Object)null) { int id = ((Object)__instance.valuableObject).GetInstanceID(); int num = _haulList.RemoveAll((ExtractedItem i) => i.InstanceId == id); REPOrt.VLog($"[REPOrt] ForceRemove (Destroy): {((Object)__instance.valuableObject).name}, ID={id}, Removed={num}"); } } } [HarmonyPatch(typeof(ValuableObject), "AddToDollarHaulList")] private class Patch_AddToDollarHaulList { private static void Postfix(ValuableObject __instance) { ExtractedItem extractedItem = new ExtractedItem { Name = ((Object)__instance).name, Value = (int)__instance.dollarValueCurrent, InstanceId = ((Object)__instance).GetInstanceID() }; _haulList.Add(extractedItem); REPOrt.VLog($"[REPOrt] HaulList Add: {extractedItem.Name}, Value={extractedItem.Value}"); } } [HarmonyPatch(typeof(ValuableObject), "RemoveFromDollarHaulList")] private class Patch_RemoveFromDollarHaulList { private static void Postfix(ValuableObject __instance) { int id = ((Object)__instance).GetInstanceID(); int num = _haulList.RemoveAll((ExtractedItem i) => i.InstanceId == id); REPOrt.VLog($"[REPOrt] HaulList Remove: {((Object)__instance).name}, ID={id}, Removed={num}"); } } private static int _stationCounter = 0; private static readonly Dictionary<string, StationSnapshot> _confirmed = new Dictionary<string, StationSnapshot>(); private static readonly Dictionary<string, StationRuntime> _runtimes = new Dictionary<string, StationRuntime>(); private static readonly List<ExtractedItem> _haulList = new List<ExtractedItem>(); public static IReadOnlyDictionary<string, StationDeliveryInfo> Confirmed => _confirmed.ToDictionary<KeyValuePair<string, StationSnapshot>, string, StationDeliveryInfo>((KeyValuePair<string, StationSnapshot> kv) => kv.Key, (KeyValuePair<string, StationSnapshot> kv) => new StationDeliveryInfo { Value = 0, Time = kv.Value.Time.ToString("o"), ExtractedItems = new List<ExtractedItem>(), HaulGoal = kv.Value.HaulGoal, HaulTotal = kv.Value.HaulTotal, UnknownSigned = 0 }); public static int GetDeliveredValue() { int num = 0; foreach (KeyValuePair<string, StationSnapshot> item in _confirmed) { num += item.Value.HaulTotal; } return num; } public static void MarkComplete(string stationName, int goal, int haulTotal) { string text = $"{stationName}_{++_stationCounter}"; List<ExtractedItem> list = _haulList.ToList(); int num = list.Sum((ExtractedItem i) => i.Value); int num2 = num - haulTotal; StationSnapshot value = new StationSnapshot { StationName = text, HaulGoal = goal, HaulTotal = haulTotal, Time = DateTime.UtcNow, ExtractedItems = list }; _confirmed[text] = value; REPOrt.VLog("[REPOrt] Snapshot frozen (Complete): " + text + ", " + $"Goal={goal}, Total={haulTotal}, Value={num}, Unknown={num2}, Items={list.Count}"); _haulList.Clear(); } public static Dictionary<string, StationDeliveryInfo> Flush() { Dictionary<string, StationDeliveryInfo> dictionary = new Dictionary<string, StationDeliveryInfo>(); foreach (KeyValuePair<string, StationSnapshot> item in _confirmed) { StationSnapshot value = item.Value; dictionary[item.Key] = new StationDeliveryInfo { Value = (item.Value.ExtractedItems?.Sum((ExtractedItem i) => i.Value) ?? 0), Time = item.Value.Time.ToString("o"), ExtractedItems = (item.Value.ExtractedItems ?? new List<ExtractedItem>()), HaulGoal = item.Value.HaulGoal, HaulTotal = item.Value.HaulTotal, UnknownSigned = (item.Value.ExtractedItems?.Sum((ExtractedItem i) => i.Value) ?? 0) - item.Value.HaulTotal }; } REPOrt.VLog("[REPOrt] Delivery dump: " + JsonConvert.SerializeObject((object)dictionary, (Formatting)1)); Reset(); return dictionary; } public static void Reset() { _confirmed.Clear(); _runtimes.Clear(); _stationCounter = 0; REPOrt.VLog("[REPOrt] DeliveryTracker Reset()"); } public static void Observe(ExtractionPoint ep, string source) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected I4, but got Unknown //IL_00c6: Unknown result type (might be due to invalid IL or missing references) string name = ((Object)ep).name; int value = Traverse.Create((object)ep).Field<int>("haulGoal").Value; int value2 = Traverse.Create((object)ep).Field<int>("haulCurrent").Value; int value3 = Traverse.Create((object)ep).Field<int>("haulPrevious").Value; State value4 = Traverse.Create((object)ep).Field<State>("currentState").Value; REPOrt.VLog($"[REPOrt] Station={name}, Goal={value}, Current={value2}, Previous={value3}, Source={source}, State={value4}"); switch (value4 - 4) { case 0: case 2: case 4: REPOrt.VLog($"[REPOrt] Snapshot candidate: {name} (state={value4})"); break; case 1: if (_confirmed.Remove(name)) { REPOrt.VLog("[REPOrt] Snapshot discarded (Cancel): " + name); } break; case 3: break; } } } [HarmonyPatch(typeof(ExtractionPoint), "StateSet")] internal class Patch_ExtractionPoint_StateSet { private static void Prefix(ExtractionPoint __instance, State newState) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) int value = Traverse.Create((object)__instance).Field<int>("haulCurrent").Value; int value2 = Traverse.Create((object)__instance).Field<int>("haulPrevious").Value; State value3 = Traverse.Create((object)__instance).Field<State>("currentState").Value; REPOrt.VLog($"[REPOrt] (Prefix) {((Object)__instance).name} {value3} -> {newState}, " + $"Current={value}, Previous={value2}"); } private static void Postfix(ExtractionPoint __instance, State newState) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 DeliveryTracker.Observe(__instance, "StateSet"); if ((int)newState == 7) { int value = Traverse.Create((object)__instance).Field<int>("haulGoal").Value; int value2 = Traverse.Create((object)__instance).Field<int>("haulCurrent").Value; DeliveryTracker.MarkComplete(((Object)__instance).name, value, value2); } } } [HarmonyPatch(typeof(ExtractionPoint), "StateSetRPC")] internal class Patch_ExtractionPoint_StateSetRPC { private static void Postfix(ExtractionPoint __instance, State state) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Invalid comparison between Unknown and I4 DeliveryTracker.Observe(__instance, "StateSetRPC"); if ((int)state == 7) { int value = Traverse.Create((object)__instance).Field<int>("haulGoal").Value; int value2 = Traverse.Create((object)__instance).Field<int>("haulCurrent").Value; } } } public static class DestroyedTracker { private static HashSet<int> destroyedIDs = new HashSet<int>(); private static Dictionary<int, int> degradedMap = new Dictionary<int, int>(); public static int DestroyedLost = 0; public static void Reset() { DestroyedLost = 0; destroyedIDs.Clear(); degradedMap.Clear(); } public static void RecordBreak(ValuableObject obj, float valueLost) { if ((Object)(object)obj == (Object)null) { return; } int instanceID = ((Object)obj).GetInstanceID(); int num = Mathf.RoundToInt(valueLost); if (num > 0) { if (!degradedMap.ContainsKey(instanceID)) { degradedMap[instanceID] = 0; } degradedMap[instanceID] += num; REPOrt.Logger.LogInfo((object)$"[REPOrt] Break: {((Object)obj).name} degraded +{num}, totalDegraded={degradedMap[instanceID]}"); } } public static void RecordDestroy(ValuableObject obj) { if (!((Object)(object)obj == (Object)null)) { int instanceID = ((Object)obj).GetInstanceID(); if (!destroyedIDs.Contains(instanceID)) { destroyedIDs.Add(instanceID); int num = Mathf.RoundToInt(obj.dollarValueOriginal); int num2 = (degradedMap.ContainsKey(instanceID) ? degradedMap[instanceID] : 0); int num3 = Mathf.Max(0, num - num2); DestroyedLost += num2 + num3; REPOrt.Logger.LogInfo((object)$"[REPOrt] Destroy: {((Object)obj).name} lost {num2}+{num3}={num2 + num3}, total={DestroyedLost}"); degradedMap.Remove(instanceID); } } } public static int CalcLost() { int num = DestroyedLost; foreach (KeyValuePair<int, int> item in degradedMap) { num += item.Value; } REPOrt.Logger.LogInfo((object)$"[REPOrt] CalcLost: TotalLost={num}"); return num; } } [HarmonyPatch(typeof(PhysGrabObjectImpactDetector), "Break")] internal class Patch_PhysGrabObjectImpactDetector_Break { private static void Prefix(PhysGrabObjectImpactDetector __instance, float valueLost, Vector3 _contactPoint, int breakLevel, bool _forceBreak) { if ((Object)(object)__instance?.valuableObject != (Object)null) { DestroyedTracker.RecordBreak(__instance.valuableObject, valueLost); } } } [HarmonyPatch(typeof(PhysGrabObjectImpactDetector), "DestroyObject")] internal class Patch_PhysGrabObjectImpactDetector_DestroyObject { private static void Prefix(PhysGrabObjectImpactDetector __instance, bool effects) { if ((Object)(object)__instance?.valuableObject != (Object)null) { DestroyedTracker.RecordDestroy(__instance.valuableObject); } } } [HarmonyPatch(typeof(EnemyHealth), "Death")] internal class Patch_EnemyHealth_Death { [HarmonyPostfix] private static void Post_Death(EnemyHealth __instance) { EnemyKillTracker.RegisterKillFromHealth(__instance, "Death"); } } [HarmonyPatch(typeof(EnemyHealth), "DeathRPC")] internal class Patch_EnemyHealth_DeathRPC { [HarmonyPostfix] private static void Post_DeathRPC(EnemyHealth __instance) { EnemyKillTracker.RegisterKillFromHealth(__instance, "DeathRPC"); } } public static class EnemyKillTracker { public static readonly List<EnemyKillRecord> KillRecords = new List<EnemyKillRecord>(); private static readonly Dictionary<int, DateTime> LastKilled = new Dictionary<int, DateTime>(); private static readonly TimeSpan DuplicateThreshold = TimeSpan.FromSeconds(2.0); private static readonly TimeSpan EarlyDeathThreshold = TimeSpan.FromSeconds(10.0); public static DateTime StageStartTime { get; set; } = DateTime.MinValue; public static void Reset() { KillRecords.Clear(); LastKilled.Clear(); } public static void Reset(DateTime stageStart) { KillRecords.Clear(); LastKilled.Clear(); StageStartTime = stageStart; } public static void RegisterKillFromHealth(EnemyHealth health, string source) { if (!((Object)(object)health == (Object)null) && !((Object)(object)health.enemy == (Object)null)) { Enemy enemy = health.enemy; DateTime utcNow = DateTime.UtcNow; int instanceID = ((Object)((Component)enemy).gameObject).GetInstanceID(); if (StageStartTime != DateTime.MinValue && utcNow - StageStartTime < EarlyDeathThreshold) { REPOrt.Logger.LogDebug((object)$"[REPOrt] Ignored early kill (<10s): {((Object)enemy).name}, ID={((Object)((Component)enemy).gameObject).GetInstanceID()}"); return; } EnemySpawnRecord value; string type = ((!EnemySpawnTracker.SpawnedRecords.TryGetValue(instanceID, out value)) ? (((Object)(object)enemy.EnemyParent != (Object)null) ? ((Object)enemy.EnemyParent).name.Replace("(Clone)", "") : ((Object)((Component)enemy).gameObject).name.Replace("(Clone)", "")) : value.Type); EnemyKillRecord enemyKillRecord = new EnemyKillRecord { InstanceId = instanceID, Type = type, Time = DateTime.UtcNow.ToString("o"), KilledBy = "Unknown" }; RegisterKill(enemyKillRecord); REPOrt.Logger.LogInfo((object)$"[REPOrt] Enemy killed ({source}): {enemyKillRecord.Type}, ID={enemyKillRecord.InstanceId}"); } } public static List<EnemyKillRecord> ToList() { return KillRecords.Select((EnemyKillRecord r) => new EnemyKillRecord { InstanceId = r.InstanceId, Type = r.Type, Time = r.Time, KilledBy = r.KilledBy }).ToList(); } public static void RegisterKill(EnemyKillRecord rec) { if (LastKilled.TryGetValue(rec.InstanceId, out var value) && DateTime.UtcNow - value < DuplicateThreshold) { REPOrt.Logger.LogDebug((object)$"[REPOrt] Duplicate kill ignored: {rec.Type}, ID={rec.InstanceId}"); return; } KillRecords.Add(rec); LastKilled[rec.InstanceId] = DateTime.UtcNow; REPOrt.Logger.LogDebug((object)$"[REPOrt] Saved EnemyKill: {rec.Type}, ID={rec.InstanceId}, Time={rec.Time}"); } } [HarmonyPatch(typeof(Enemy))] internal class Patch_Enemy_Spawn { [HarmonyPostfix] [HarmonyPatch("Spawn")] private static void Post_Spawn(Enemy __instance) { if ((Object)(object)__instance != (Object)null) { EnemySpawnTracker.RegisterSpawn(__instance); } } } [HarmonyPatch(typeof(EnemyDirector))] internal class Patch_EnemyDirector_PickEnemies { [HarmonyPostfix] [HarmonyPatch("PickEnemies")] private static void Post_PickEnemies(List<EnemySetup> _enemiesList) { if (_enemiesList != null && _enemiesList.Count > 0) { EnemySpawnTracker.RegisterExpected(_enemiesList); REPOrt.Logger.LogInfo((object)("[REPOrt] Registered expected enemies: " + string.Join(", ", _enemiesList.Select((EnemySetup s) => ((s != null) ? ((Object)s).name : null) ?? "null")))); } } } [HarmonyPatch(typeof(RunManager))] internal class Patch_RunManager_SetRunLevel { [HarmonyPostfix] [HarmonyPatch("SetRunLevel")] private static void Post_SetRunLevel() { EnemySpawnTracker.Reset(); REPOrt.Logger.LogInfo((object)"[REPOrt] EnemySpawnTracker reset at stage start"); } } public static class EnemySpawnTracker { public static readonly Dictionary<int, EnemySpawnRecord> SpawnedRecords = new Dictionary<int, EnemySpawnRecord>(); public static readonly List<string> ExpectedNames = new List<string>(); public static void Reset() { SpawnedRecords.Clear(); ExpectedNames.Clear(); } public static void RegisterExpected(List<EnemySetup> setups) { foreach (EnemySetup setup in setups) { if ((Object)(object)setup == (Object)null) { continue; } if (((Object)setup).name.StartsWith("Enemy Group")) { foreach (GameObject spawnObject in setup.spawnObjects) { if ((Object)(object)spawnObject != (Object)null) { ExpectedNames.Add(((Object)spawnObject).name.Replace("(Clone)", "")); } } } else { ExpectedNames.Add(((Object)setup).name); } } } public static void RegisterSpawn(Enemy enemy) { int instanceID = ((Object)((Component)enemy).gameObject).GetInstanceID(); string text = (((Object)(object)enemy.EnemyParent != (Object)null) ? ((Object)enemy.EnemyParent).name.Replace("(Clone)", "") : ((Object)((Component)enemy).gameObject).name.Replace("(Clone)", "")); EnemySpawnRecord value = new EnemySpawnRecord { InstanceId = instanceID, Type = text }; if (!SpawnedRecords.ContainsKey(instanceID)) { SpawnedRecords[instanceID] = value; REPOrt.Logger.LogInfo((object)$"[REPOrt] Saved EnemySpawn: {text}, ID={instanceID}"); } else { REPOrt.Logger.LogDebug((object)$"[REPOrt] Duplicate spawn ignored: {text}, ID={instanceID}"); } } public static List<EnemySpawnRecord> ToList() { return SpawnedRecords.Values.ToList(); } } public static class ModDetector { public static bool IsModded { get; private set; } public static List<string> DetectedMods { get; private set; } = new List<string>(); public static void ScanMods() { try { DetectedMods.Clear(); foreach (KeyValuePair<string, PluginInfo> pluginInfo in Chainloader.PluginInfos) { PluginInfo value = pluginInfo.Value; if (!(value.Metadata.GUID == "Kai.REPOrt")) { DetectedMods.Add(value.Metadata.Name); } } string path = Path.Combine(Paths.BepInExRootPath, "plugins"); if (Directory.Exists(path)) { string[] files = Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories); foreach (string path2 in files) { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path2); if (!fileNameWithoutExtension.Equals("Kai-REPOrt", StringComparison.OrdinalIgnoreCase) && !fileNameWithoutExtension.Equals("REPOrt", StringComparison.OrdinalIgnoreCase) && !DetectedMods.Contains(fileNameWithoutExtension)) { DetectedMods.Add(fileNameWithoutExtension); } } string[] directories = Directory.GetDirectories(path); foreach (string path3 in directories) { string fileName = Path.GetFileName(path3); if (!fileName.Equals("Kai-REPOrt", StringComparison.OrdinalIgnoreCase) && !fileName.Equals("REPOrt", StringComparison.OrdinalIgnoreCase) && !DetectedMods.Contains(fileName)) { DetectedMods.Add(fileName); } } } IsModded = DetectedMods.Count > 0; REPOrt.Logger.LogInfo((object)$"[REPOrt] Mod scan complete. IsModded={IsModded}, Count={DetectedMods.Count}"); } catch (Exception arg) { REPOrt.Logger.LogError((object)$"[REPOrt] Mod scan failed: {arg}"); } } } [HarmonyPatch(typeof(LevelGenerator), "Generate")] public static class Patch_LevelGenerator { [HarmonyPostfix] public static void OnLevelGenerated(LevelGenerator __instance) { try { Level levelCurrent = RunManager.instance.levelCurrent; if ((Object)(object)levelCurrent == (Object)null) { REPOrt.Logger.LogWarning((object)"[REPOrt] LevelGenerator.Generate called, but levelCurrent is null"); return; } string name = ((Object)levelCurrent).name; string mapName; string text = StageUtil.DetectStageType(name, out mapName); if (text == null) { REPOrt.Logger.LogInfo((object)("[REPOrt] Ignored scene: " + name)); return; } REPOrt.Logger.LogInfo((object)("[REPOrt] Level started: " + name + " -> StageType=" + text + ", MapName=" + mapName)); StageRecord rec = new StageRecord { StageName = text, LevelNum = 0, StartedAt = DateTime.UtcNow.ToString("o"), EndedAt = DateTime.UtcNow.ToString("o"), MapName = mapName }; REPOrtSave.AddStageRecord(text, rec); if (text == "GameOver") { REPOrtSave.FinalizeSession(); } } catch (Exception arg) { REPOrt.Logger.LogError((object)$"[REPOrt] Patch_LevelGenerator error: {arg}"); } } } [HarmonyPatch(typeof(SceneManager))] public static class Patch_SceneManager { [HarmonyPostfix] [HarmonyPatch(typeof(SceneManager), "Internal_ActiveSceneChanged")] public static void ActiveSceneChanged(Scene previousActiveScene, Scene newActiveScene) { string name = ((Scene)(ref newActiveScene)).name; string mapName; string text = StageUtil.DetectStageType(name, out mapName); if (text == null) { REPOrt.Logger.LogInfo((object)("[REPOrt] Ignored scene: " + name)); return; } if (!REPOrtSave.HasSession) { string runSessionId = $"REPO_SAVE_{DateTime.Now:yyyy_MM_dd_HH_mm_ss}"; REPOrtSave.StartOrAttachSession(runSessionId); } REPOrt.Logger.LogInfo((object)("[REPOrt] Level started: " + name + " -> StageType=" + text + ", MapName=" + mapName)); StageRecord rec = new StageRecord { StageName = text, LevelNum = 0, StartedAt = DateTime.UtcNow.ToString("o"), EndedAt = DateTime.UtcNow.ToString("o"), MapName = mapName }; REPOrtSave.AddStageRecord(text, rec); if (text == "GameOver") { REPOrtSave.FinalizeSession(); } } } [HarmonyPatch(typeof(StatsManager), "SaveGame")] internal class StatsManager_SaveGame_Patch { private static void Prefix(string fileName) { try { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); REPOrtSave.StartOrAttachSession(fileNameWithoutExtension); REPOrt.Logger.LogInfo((object)("[REPOrt] SaveGame hooked, session id = " + fileNameWithoutExtension)); } catch (Exception arg) { REPOrt.Logger.LogError((object)$"[REPOrt] Error in SaveGame patch: {arg}"); } } } [HarmonyPatch(typeof(StatsManager), "LoadGame")] internal class StatsManager_LoadGame_Patch { private static void Prefix(string fileName) { try { string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); REPOrtSave.StartOrAttachSession(fileNameWithoutExtension); REPOrtSave.UpdateModInfo(); REPOrt.Logger.LogInfo((object)("[REPOrt] LoadGame hooked, session id = " + fileNameWithoutExtension)); } catch (Exception arg) { REPOrt.Logger.LogError((object)$"[REPOrt] Error in LoadGame patch: {arg}"); } } } public static class PlayerTracker { private static readonly string[] UpgradeKeys = new string[11] { "playerUpgradeHealth", "playerUpgradeStamina", "playerUpgradeExtraJump", "playerUpgradeLaunch", "playerUpgradeMapPlayerCount", "playerUpgradeSpeed", "playerUpgradeStrength", "playerUpgradeThrow", "playerUpgradeRange", "playerUpgradeCrouchRest", "playerUpgradeTumbleWings" }; public static void RecordPlayers(StageRecord record) { StatsManager instance = StatsManager.instance; if ((Object)(object)instance == (Object)null) { return; } foreach (KeyValuePair<string, string> playerName in instance.playerNames) { string key = playerName.Key; string value = playerName.Value; Dictionary<string, int> dictionary = instance.FetchPlayerUpgrades(key); Dictionary<string, int> dictionary2 = new Dictionary<string, int>(); if (dictionary != null) { foreach (KeyValuePair<string, int> item2 in dictionary) { string text = (item2.Key.StartsWith("playerUpgrade") ? item2.Key.Substring("playerUpgrade".Length) : item2.Key); dictionary2[text] = item2.Value; REPOrt.Logger.LogInfo((object)$"[REPOrt] Upgrade {value}: {text} = {item2.Value}"); } } else { REPOrt.Logger.LogInfo((object)("[REPOrt] No upgrades for " + value)); } PlayerSnapshot item = new PlayerSnapshot { Name = value, Upgrades = dictionary2 }; record.Players.Add(item); } } } [BepInPlugin("Kai.REPOrt", "REPOrt", "1.15.1")] public class REPOrt : BaseUnityPlugin { internal static bool VerboseDeliveryLog; internal static bool DeliveryLiteMode; private static string _lastConfirmedStageType; private static string _lastMapName; internal static REPOrt Instance { get; private set; } internal static ManualLogSource Logger => Instance._logger; private ManualLogSource _logger => ((BaseUnityPlugin)this).Logger; internal Harmony? Harmony { get; set; } internal static void VLog(string msg) { if (VerboseDeliveryLog) { Logger.LogInfo((object)msg); } } private void Awake() { Instance = this; ((Component)this).gameObject.transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; Patch(); SceneManager.activeSceneChanged += OnActiveSceneChanged; ModDetector.ScanMods(); REPOrtSave.CleanupOrphanSessions(); Logger.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} has loaded!"); } private void OnEnable() { SceneManager.activeSceneChanged += OnActiveSceneChanged; } private void OnDisable() { SceneManager.activeSceneChanged -= OnActiveSceneChanged; } private void OnActiveSceneChanged(Scene prev, Scene next) { string mapName; string text = StageUtil.DetectStageType(((Scene)(ref next)).name, out mapName); if (text == null) { Logger.LogInfo((object)("[REPOrt] Ignored scene: " + ((Scene)(ref next)).name)); return; } Logger.LogInfo((object)("[REPOrt] Level started: " + ((Scene)(ref next)).name + " -> StageType=" + text + ", MapName=" + mapName)); if (!string.IsNullOrEmpty(_lastConfirmedStageType)) { Logger.LogInfo((object)("[REPOrt] Closing previous stage: " + _lastConfirmedStageType + " -> " + text)); REPOrtSave.MarkPreviousStage(_lastConfirmedStageType, text); } if (!REPOrtSave.HasSession) { string runSessionId = $"REPO_SAVE_{DateTime.Now:yyyy_MM_dd_HH_mm_ss}"; REPOrtSave.StartOrAttachSession(runSessionId); } else { Logger.LogInfo((object)"[REPOrt] Continuing existing session"); } StageRecord rec = new StageRecord { StageName = text, StartedAt = DateTime.UtcNow.ToString("o"), EndedAt = DateTime.UtcNow.ToString("o"), MapName = mapName }; REPOrtSave.AddStageRecord(text, rec); if (string.Equals(text, "GameOver", StringComparison.OrdinalIgnoreCase)) { REPOrtSave.FinalizeSession(); _lastConfirmedStageType = null; } else { _lastConfirmedStageType = text; _lastMapName = mapName; } if (string.Equals(text, "Main", StringComparison.OrdinalIgnoreCase)) { EnemyKillTracker.Reset(DateTime.UtcNow); EnemySpawnTracker.Reset(); DestroyedTracker.Reset(); DeliveryTracker.Reset(); Logger.LogInfo((object)"[REPOrt] EnemyKillTracker reset at Main start"); } Dictionary<string, StationDeliveryInfo> dictionary = DeliveryTracker.Flush(); Logger.LogInfo((object)("[REPOrt] Delivery dump: " + JsonConvert.SerializeObject((object)dictionary, (Formatting)1))); } internal void Patch() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_0025: Expected O, but got Unknown if (Harmony == null) { Harmony val = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); Harmony val2 = val; Harmony = val; } Harmony.PatchAll(); } internal void Unpatch() { Harmony? harmony = Harmony; if (harmony != null) { harmony.UnpatchSelf(); } } private void Update() { } } public static class REPOrtSave { private static RunSession _current; private static string _dir; private static string _currentPath; public static bool HasSession => _current != null; public static void StartOrAttachSession(string runSessionId) { _dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..\\LocalLow\\semiwork\\Repo\\REPOrtGallery", runSessionId); Directory.CreateDirectory(_dir); _currentPath = Path.Combine(_dir, "session_current.json"); if (File.Exists(_currentPath)) { string text = File.ReadAllText(_currentPath); _current = JsonConvert.DeserializeObject<RunSession>(text); REPOrt.Logger.LogInfo((object)("[REPOrt] Session " + runSessionId + " attached")); return; } _current = new RunSession { RunSessionId = runSessionId, StartedAt = DateTime.UtcNow.ToString("o"), LastUpdatedAt = DateTime.UtcNow.ToString("o") }; UpdateModInfo(); Save(); REPOrt.Logger.LogInfo((object)("[REPOrt] Session " + runSessionId + " started")); } public static void AddStageRecord(string stageName, StageRecord rec) { if (!HasSession) { string text = $"REPO_SAVE_{DateTime.Now:yyyy_MM_dd_HH_mm_ss}"; REPOrt.Logger.LogInfo((object)("[REPOrt] Starting new session " + text)); StartOrAttachSession(text); } else { REPOrt.Logger.LogInfo((object)"[REPOrt] Continuing existing session"); } if (_current == null) { return; } string prev = _current.LastStageType?.Trim(); if (!string.IsNullOrEmpty(prev)) { bool? cleared = null; switch (prev.ToLowerInvariant()) { case "main": if (stageName.Equals("Shop", StringComparison.OrdinalIgnoreCase)) { cleared = true; } else if (stageName.Equals("GameOver", StringComparison.OrdinalIgnoreCase)) { cleared = false; } break; case "shop": if (stageName.Equals("Lobby", StringComparison.OrdinalIgnoreCase)) { cleared = true; } break; case "lobby": if (stageName.Equals("Main", StringComparison.OrdinalIgnoreCase)) { cleared = true; } break; } if (cleared.HasValue) { int num = _current.Stages.FindLastIndex((StageRecord s) => s != null && s.StageName != null && s.StageName.Trim().Equals(prev, StringComparison.OrdinalIgnoreCase) && !s.Cleared.HasValue); REPOrt.Logger.LogInfo((object)$"[REPOrt] Auto-close prev from JSON: prev={prev}, next={stageName}, idx={num}"); if (num >= 0) { StageRecord stageRecord = _current.Stages[num]; stageRecord.Cleared = cleared; stageRecord.EndedAt = DateTime.UtcNow.ToString("o"); DateTime dateTime = DateTime.Parse(stageRecord.StartedAt); DateTime dateTime2 = DateTime.Parse(stageRecord.EndedAt); stageRecord.ElapsedTimeSec = (dateTime2 - dateTime).TotalSeconds; if (stageRecord.StageName.Equals("Main", StringComparison.OrdinalIgnoreCase)) { stageRecord.DestroyedValue = DestroyedTracker.CalcLost(); REPOrt.Logger.LogInfo((object)$"[REPOrt] Stage {stageRecord.StageName} DestroyedValue={stageRecord.DestroyedValue}"); stageRecord.DeliveredValue = DeliveryTracker.GetDeliveredValue(); stageRecord.DeliveryPerStation = DeliveryTracker.Flush(); stageRecord.EnemiesSpawned = EnemySpawnTracker.ToList(); stageRecord.EnemiesKilled = EnemyKillTracker.ToList(); PlayerTracker.RecordPlayers(stageRecord); StatsManager instance = StatsManager.instance; stageRecord.Balance = ((instance != null) ? instance.GetRunStatCurrency() : 0); REPOrt.Logger.LogInfo((object)("[REPOrt] Stage " + stageRecord.StageName + " DeliveryPerStation=" + JsonConvert.SerializeObject((object)stageRecord.DeliveryPerStation, (Formatting)1))); DestroyedTracker.Reset(); DeliveryTracker.Reset(); EnemySpawnTracker.Reset(); EnemyKillTracker.Reset(); } if (stageRecord.StageName.Equals("Shop", StringComparison.OrdinalIgnoreCase)) { try { StatsManager instance2 = StatsManager.instance; stageRecord.Balance = ((instance2 != null) ? instance2.GetRunStatCurrency() : 0); DeliveryTracker.Reset(); REPOrt.Logger.LogInfo((object)$"[REPOrt] Money updated ({stageRecord.StageName} end) -> {_current.Money}"); } catch (Exception arg) { REPOrt.Logger.LogWarning((object)$"[REPOrt] Money fetch failed: {arg}"); } } if (stageRecord.StageName.Equals("Lobby", StringComparison.OrdinalIgnoreCase)) { PlayerTracker.RecordPlayers(stageRecord); } _current.Stages[num] = stageRecord; Save(); } } } StatsManager instance3 = StatsManager.instance; int finalLevel = (rec.LevelNum = (1 + ((instance3 != null) ? new int?(instance3.GetRunStatLevel()) : null)) ?? ((RunManager.instance?.levelsCompleted ?? 0) + 1)); StageRecord stageRecord2 = rec; if (stageRecord2.StartedAt == null) { string text3 = (stageRecord2.StartedAt = DateTime.UtcNow.ToString("o")); } stageRecord2 = rec; if (stageRecord2.EndedAt == null) { string text3 = (stageRecord2.EndedAt = DateTime.UtcNow.ToString("o")); } DateTime dateTime3 = DateTime.Parse(rec.StartedAt); DateTime dateTime4 = DateTime.Parse(rec.EndedAt); rec.ElapsedTimeSec = (dateTime4 - dateTime3).TotalSeconds; stageRecord2 = rec; if (!stageRecord2.Cleared.HasValue) { stageRecord2.Cleared = null; } REPOrt.Logger.LogInfo((object)("[REPOrt] Adding stage: " + JsonConvert.SerializeObject((object)rec, (Formatting)1))); _current.Stages.Add(rec); if (stageName.Equals("Main", StringComparison.OrdinalIgnoreCase)) { DestroyedTracker.Reset(); REPOrt.Logger.LogInfo((object)"[REPOrt] DestroyedTracker reset at Main start"); } _current.LastStageType = stageName; _current.PlayerName = StatsManager.instance.playerNames.Values.Distinct().ToList(); RunSession current = _current; StatsManager instance4 = StatsManager.instance; current.Money = ((instance4 != null) ? instance4.GetRunStatCurrency() : 0); RunSession current2 = _current; StatsManager instance5 = StatsManager.instance; current2.TotalHaul = ((instance5 != null) ? instance5.GetRunStatTotalHaul() : 0); _current.FinalLevel = finalLevel; if ((Object)(object)RunManager.instance != (Object)null) { int moonLevel = RunManager.instance.moonLevel; _current.CurrentMoon = RunManager.instance.MoonGetName(moonLevel); } else { _current.CurrentMoon = "Unknown"; } _current.LastUpdatedAt = DateTime.UtcNow.ToString("o"); Save(); REPOrt.Logger.LogInfo((object)("[REPOrt] Stage " + stageName + " appended -> " + _currentPath)); REPOrt.Logger.LogInfo((object)("[REPOrt] Stage " + stageName + " appended")); } internal static void MarkLastUnresolvedStage(string nextStage) { if (_current != null) { int num = _current.Stages.FindLastIndex((StageRecord s) => !s.Cleared.HasValue); if (num >= 0) { StageRecord stageRecord = _current.Stages[num]; MarkPreviousStage(stageRecord.StageName, nextStage); } } } internal static void MarkPreviousStage(string prevStage, string nextStage) { string prevStage2 = prevStage; if (_current == null || string.IsNullOrWhiteSpace(prevStage2)) { return; } int num = _current.Stages.FindLastIndex((StageRecord s) => s != null && s.StageName != null && s.StageName.Equals(prevStage2, StringComparison.OrdinalIgnoreCase) && !s.Cleared.HasValue); if (num < 0) { return; } StageRecord stageRecord = _current.Stages[num]; bool? cleared = null; switch (prevStage2.ToLowerInvariant()) { case "main": if (nextStage.Equals("Shop", StringComparison.OrdinalIgnoreCase)) { cleared = true; } else if (nextStage.Equals("GameOver", StringComparison.OrdinalIgnoreCase)) { cleared = false; } break; case "shop": if (nextStage.Equals("Lobby", StringComparison.OrdinalIgnoreCase)) { cleared = true; } break; case "lobby": if (nextStage.Equals("Main", StringComparison.OrdinalIgnoreCase)) { cleared = true; } break; } if (cleared.HasValue) { stageRecord.Cleared = cleared; stageRecord.EndedAt = DateTime.UtcNow.ToString("o"); DateTime dateTime = DateTime.Parse(stageRecord.StartedAt); DateTime dateTime2 = DateTime.Parse(stageRecord.EndedAt); stageRecord.ElapsedTimeSec = (dateTime2 - dateTime).TotalSeconds; _current.Stages[num] = stageRecord; Save(); REPOrt.Logger.LogInfo((object)("[REPOrt] Stage closed: " + JsonConvert.SerializeObject((object)stageRecord, (Formatting)1))); } } public static void FinalizeSession(string reason = "GameOver") { if (_current != null) { _current.FinalizedAt = DateTime.UtcNow.ToString("o"); _current.FinalizeReason = reason; string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "..\\LocalLow\\semiwork\\Repo\\REPOrtGallery"); string text = Path.Combine(path, "Finalized"); Directory.CreateDirectory(text); string path2 = Path.Combine(text, $"session_final_{DateTime.Now:yyyy_MM_dd_HH_mm_ss}.json"); string contents = JsonConvert.SerializeObject((object)_current, (Formatting)1); File.WriteAllText(path2, contents); REPOrt.Logger.LogInfo((object)("[REPOrt] finalized -> " + Path.GetFileName(path2) + " (moved to Finalized/)")); if (File.Exists(_currentPath)) { File.Delete(_currentPath); REPOrt.Logger.LogInfo((object)"[REPOrt] session_current.json deleted after finalization"); } } } private static void Save() { string contents = JsonConvert.SerializeObject((object)_current, (Formatting)1); File.WriteAllText(_currentPath, contents); REPOrt.Logger.LogInfo((object)"[REPOrt] session_current.json saved"); } public static StageRecord GetCurrentStage() { return _current?.Stages?.LastOrDefault(); } public static void CleanupOrphanSessions() { try { string text = Path.Combine(Application.persistentDataPath, "REPOrtGallery"); if (!Directory.Exists(text)) { return; } string[] directories = Directory.GetDirectories(text, "REPO_SAVE_*"); string[] array = directories; foreach (string text2 in array) { string path = Path.Combine(text2, "session_current.json"); if (!File.Exists(path)) { continue; } string text3 = File.ReadAllText(path); if (text3.Contains("\"Cycles\": []")) { string text4 = Path.Combine(text, "Quarantine"); Directory.CreateDirectory(text4); string fileName = Path.GetFileName(text2); string text5 = Path.Combine(text4, fileName); if (Directory.Exists(text5)) { Directory.Delete(text5, recursive: true); } Directory.Move(text2, text5); REPOrt.Logger.LogInfo((object)("[REPOrt] Orphan session moved to Quarantine: " + fileName)); } } } catch (Exception arg) { REPOrt.Logger.LogError((object)$"[REPOrt] Cleanup failed: {arg}"); } } public static void UpdateModInfo() { if (_current != null) { bool isModded = ModDetector.IsModded; List<string> detectedMods = new List<string>(ModDetector.DetectedMods); if (isModded) { _current.IsModded = true; } _current.DetectedMods = detectedMods; ModSnapshot snapshot = new ModSnapshot { Timestamp = DateTime.UtcNow.ToString("o"), IsModded = isModded, DetectedMods = detectedMods }; if (!_current.ModHistory.Any((ModSnapshot h) => h.IsModded == snapshot.IsModded && h.DetectedMods.SequenceEqual(snapshot.DetectedMods))) { _current.ModHistory.Add(snapshot); } _current.LastUpdatedAt = DateTime.UtcNow.ToString("o"); Save(); REPOrt.Logger.LogInfo((object)$"[REPOrt] Mod info updated (IsModded={_current.IsModded}, HistoryCount={_current.ModHistory.Count})"); } } } [Serializable] public class RunSession { public string RunSessionId { get; set; } public List<StageRecord> Stages { get; set; } = new List<StageRecord>(); public string StartedAt { get; set; } public string LastUpdatedAt { get; set; } public string FinalizedAt { get; set; } public string FinalizeReason { get; set; } public bool IsModded { get; set; } public List<string> DetectedMods { get; set; } = new List<string>(); public List<ModSnapshot> ModHistory { get; set; } = new List<ModSnapshot>(); public string LastStageType { get; set; } public List<string> PlayerName { get; set; } public double TotalPlayTimeSec { get; set; } public int FinalLevel { get; set; } public int TotalDeaths { get; set; } public int Money { get; set; } public int TotalHaul { get; set; } public string CurrentMoon { get; set; } } [Serializable] public class StageRecord { public string StageName { get; set; } public int LevelNum { get; set; } public string StartedAt { get; set; } public string EndedAt { get; set; } public string MapName { get; set; } public bool? Cleared { get; set; } public double ElapsedTimeSec { get; set; } public int DeliveredValue { get; set; } public int DestroyedValue { get; set; } public Dictionary<string, StationDeliveryInfo> DeliveryPerStation { get; set; } = new Dictionary<string, StationDeliveryInfo>(); public List<EnemySpawnRecord> EnemiesSpawned { get; set; } = new List<EnemySpawnRecord>(); public List<EnemyKillRecord> EnemiesKilled { get; set; } = new List<EnemyKillRecord>(); public List<PlayerSnapshot> Players { get; set; } = new List<PlayerSnapshot>(); public int Balance { get; set; } } public class StationDeliveryInfo { public int Value { get; set; } public string Time { get; set; } public List<ExtractedItem> ExtractedItems { get; set; } = new List<ExtractedItem>(); public int HaulGoal { get; set; } public int HaulTotal { get; set; } public int UnknownSigned { get; set; } public int UnknownPlus => Math.Max(0, UnknownSigned); public int UnknownMinus => Math.Max(0, -UnknownSigned); } public sealed class EnemySpawnRecord { public int InstanceId { get; set; } public string Type { get; set; } } public class EnemyKillRecord { public int InstanceId { get; set; } public string Type { get; set; } = ""; public string Time { get; set; } = ""; public string KilledBy { get; set; } = "Unknown"; } public class ExtractedItem { public string Name { get; set; } public int Value { get; set; } public int InstanceId { get; set; } } public class PlayerSnapshot { public string Name { get; set; } public Dictionary<string, int> Upgrades { get; set; } = new Dictionary<string, int>(); } [Serializable] public class ModSnapshot { public string Timestamp { get; set; } public bool IsModded { get; set; } public List<string> DetectedMods { get; set; } = new List<string>(); } public static class StageUtil { private static readonly HashSet<string> KnownMaps = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Museum", "Manor", "Arctic", "Wizard" }; public static string DetectStageType(string sceneName, out string mapName) { mapName = null; if (string.IsNullOrWhiteSpace(sceneName)) { return null; } string text = sceneName.Trim(); if (text.IndexOf("Main Menu", StringComparison.OrdinalIgnoreCase) >= 0) { return null; } if (text.IndexOf("Shop", StringComparison.OrdinalIgnoreCase) >= 0) { return "Shop"; } if (text.IndexOf("Lobby", StringComparison.OrdinalIgnoreCase) >= 0) { return "Lobby"; } if (text.IndexOf("Arena", StringComparison.OrdinalIgnoreCase) >= 0 || text.Equals("GameOver", StringComparison.OrdinalIgnoreCase)) { return "GameOver"; } if (text.StartsWith("Level - ", StringComparison.OrdinalIgnoreCase)) { string text2 = text.Substring("Level - ".Length).Trim(); if (KnownMaps.Contains(text2)) { mapName = text2; return "Main"; } if (text2.Equals("Shop", StringComparison.OrdinalIgnoreCase)) { return "Shop"; } if (text2.Equals("Lobby", StringComparison.OrdinalIgnoreCase)) { return "Lobby"; } if (text2.Equals("GameOver", StringComparison.OrdinalIgnoreCase) || text2.Equals("Arena", StringComparison.OrdinalIgnoreCase)) { return "GameOver"; } return null; } if (KnownMaps.Contains(text)) { mapName = text; return "Main"; } return null; } } }