Decompiled source of REPOrt v0.1.1

plugins/Kai-REPOrt/REPOrt.dll

Decompiled 4 days ago
using 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;
		}
	}
}