Decompiled source of LCStatsCapture v1.0.0

BepInEx\plugins\LCStatsCapture\LCStatsCapture.dll

Decompiled 2 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 BepInEx;
using BepInEx.Logging;
using DunGen;
using DunGen.Graph;
using GameNetcodeStuff;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Newtonsoft.Json;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: AssemblyCompany("LCStatsCapture")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyDescription("Lethal Company end-of-day stats capture mod")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("LCStatsCapture")]
[assembly: AssemblyTitle("LCStatsCapture")]
[assembly: AssemblyVersion("1.0.0.0")]
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;
		}
	}
}
namespace LCStatsCapture
{
	internal static class SessionState
	{
		internal static string SessionId = NewId();

		internal static int DayNumber = 0;

		internal static bool RoundActive = false;

		internal static readonly List<DeathRecord> DeathsThisRound = new List<DeathRecord>();

		internal static readonly HashSet<string> DiedThisRound = new HashSet<string>();

		internal static int InitialShipScrapValue = 0;

		internal static int TotalMoonScrapValue = 0;

		internal static int TotalMoonScrapItemCount = 0;

		internal static string NewId()
		{
			return Guid.NewGuid().ToString("N").Substring(0, 8);
		}

		internal static void ResetForRound()
		{
			DeathsThisRound.Clear();
			DiedThisRound.Clear();
			RoundActive = true;
			InitialShipScrapValue = 0;
			TotalMoonScrapValue = 0;
			TotalMoonScrapItemCount = 0;
		}
	}
	[HarmonyPatch(typeof(StartOfRound), "Start")]
	internal static class Patch_StartOfRound_Start
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			SessionState.SessionId = SessionState.NewId();
			SessionState.DayNumber = 0;
			SessionState.RoundActive = false;
			ContentDiscovery.Load();
			ContentDiscovery.Scan();
			Plugin.Log.LogInfo((object)("[StatsCapture] New session started — ID: " + SessionState.SessionId));
		}
	}
	[HarmonyPatch(typeof(RoundManager), "FinishGeneratingNewLevelClientRpc")]
	internal static class Patch_RoundManager_FinishGenerating
	{
		[HarmonyPostfix]
		private static void Postfix()
		{
			SessionState.ResetForRound();
			ContentDiscovery.Scan();
			try
			{
				GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>();
				int num = 0;
				int num2 = 0;
				int num3 = 0;
				GrabbableObject[] array2 = array;
				foreach (GrabbableObject val in array2)
				{
					if (!((Object)(object)val?.itemProperties == (Object)null) && val.itemProperties.isScrap)
					{
						if (val.isInShipRoom)
						{
							num += val.scrapValue;
							continue;
						}
						num2 += val.scrapValue;
						num3++;
					}
				}
				SessionState.InitialShipScrapValue = num;
				SessionState.TotalMoonScrapValue = num2;
				SessionState.TotalMoonScrapItemCount = num3;
				Plugin.Log.LogInfo((object)("[StatsCapture] Level generated — " + $"ship carry-in: {num} cr | " + $"moon pool: {num3} items / {num2} cr | " + "round tracking active."));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[StatsCapture] Could not snapshot scrap state: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(PlayerControllerB), "KillPlayer")]
	internal static class Patch_PlayerControllerB_KillPlayer
	{
		[HarmonyPostfix]
		private static void Postfix(PlayerControllerB __instance, Vector3 bodyVelocity, bool spawnBody, CauseOfDeath causeOfDeath, int deathAnimation)
		{
			//IL_008b: Unknown result type (might be due to invalid IL or missing references)
			if (SessionState.RoundActive)
			{
				string playerUsername = __instance.playerUsername;
				if (!SessionState.DiedThisRound.Contains(playerUsername))
				{
					SessionState.DiedThisRound.Add(playerUsername);
					float num = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.normalizedTimeOfDay : 0f);
					SessionState.DeathsThisRound.Add(new DeathRecord
					{
						PlayerName = playerUsername,
						CauseOfDeath = ((object)(CauseOfDeath)(ref causeOfDeath)).ToString(),
						TimeNormalized = (float)Math.Round(num, 3)
					});
					Plugin.Log.LogInfo((object)$"[StatsCapture] Death — {playerUsername} ({causeOfDeath}) at t={num:F2}");
				}
			}
		}
	}
	[HarmonyPatch(typeof(RoundManager), "DespawnPropsAtEndOfRound")]
	internal static class Patch_RoundManager_DespawnProps
	{
		[HarmonyPrefix]
		private static void Prefix()
		{
			if (SessionState.RoundActive)
			{
				SessionState.RoundActive = false;
				SessionState.DayNumber++;
				SnapshotWriter.Capture();
			}
		}
	}
	internal static class SnapshotWriter
	{
		private static readonly string OutputDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LCStatsCapture");

		internal static void Capture()
		{
			try
			{
				StartOfRound instance = StartOfRound.Instance;
				RoundManager instance2 = RoundManager.Instance;
				if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null)
				{
					Plugin.Log.LogWarning((object)"[StatsCapture] Capture skipped — game instances not ready.");
					return;
				}
				SelectableLevel currentLevel = instance2.currentLevel;
				List<PlayerControllerB> list = instance.allPlayerScripts.Where((PlayerControllerB p) => (Object)(object)p != (Object)null && p.isPlayerControlled).ToList();
				List<PlayerControllerB> list2 = list.Where((PlayerControllerB p) => !p.isPlayerDead).ToList();
				foreach (PlayerControllerB item in list)
				{
					if (item.isPlayerDead && !SessionState.DiedThisRound.Contains(item.playerUsername))
					{
						SessionState.DeathsThisRound.Add(new DeathRecord
						{
							PlayerName = item.playerUsername,
							CauseOfDeath = "Unknown",
							TimeNormalized = 1f
						});
					}
				}
				List<CollectedScrapRecord> list3 = new List<CollectedScrapRecord>();
				GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>();
				foreach (GrabbableObject val in array)
				{
					if (!((Object)(object)val?.itemProperties == (Object)null) && val.itemProperties.isScrap && val.isInShipRoom)
					{
						float weightLbs = (float)Math.Round((val.itemProperties.weight - 1f) * 105f, 1);
						list3.Add(new CollectedScrapRecord
						{
							Name = val.itemProperties.itemName,
							Value = val.scrapValue,
							WeightLbs = weightLbs,
							TwoHanded = val.itemProperties.twoHanded,
							IsConductive = val.itemProperties.isConductiveMetal
						});
					}
				}
				ContentDiscovery.Scan();
				int num = list3.Sum((CollectedScrapRecord s) => s.Value);
				int initialShipScrapValue = SessionState.InitialShipScrapValue;
				int num2 = num - initialShipScrapValue;
				string interiorType = "Unknown";
				try
				{
					DungeonFlow val2 = instance2.dungeonGenerator?.Generator?.DungeonFlow;
					if ((Object)(object)val2 != (Object)null)
					{
						interiorType = ((Object)val2).name;
					}
				}
				catch
				{
				}
				DaySnapshot daySnapshot = new DaySnapshot
				{
					SessionId = SessionState.SessionId,
					DayNumber = SessionState.DayNumber,
					CapturedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"),
					Seed = instance.randomMapSeed,
					MoonName = (currentLevel?.PlanetName ?? "Unknown"),
					Weather = (((currentLevel != null) ? ((object)(LevelWeatherType)(ref currentLevel.currentWeather)).ToString() : null) ?? "None"),
					InteriorType = interiorType,
					PlayerCount = list.Count,
					SurvivorCount = list2.Count,
					SurvivorNames = list2.Select((PlayerControllerB p) => p.playerUsername).ToList(),
					Deaths = new List<DeathRecord>(SessionState.DeathsThisRound),
					TotalMoonScrapItemCount = SessionState.TotalMoonScrapItemCount,
					TotalMoonScrapValue = SessionState.TotalMoonScrapValue,
					InitialShipScrapValue = initialShipScrapValue,
					FinalShipScrapValue = num,
					CollectedScrapValue = num2,
					TotalScrapItemCount = list3.Count,
					TotalScrapValueCollected = num,
					TotalScrapWeightCollectedLbs = (float)Math.Round(list3.Sum((CollectedScrapRecord s) => s.WeightLbs), 1),
					ScrapCollected = list3,
					KnownContent = ContentDiscovery.GetSnapshot()
				};
				if (!Directory.Exists(OutputDir))
				{
					Directory.CreateDirectory(OutputDir);
				}
				string text = string.Concat(daySnapshot.MoonName.Split(Path.GetInvalidFileNameChars()));
				string text2 = DateTime.Now.ToString("yyyyMMdd_HHmmss");
				string text3 = $"{daySnapshot.SessionId}_day{daySnapshot.DayNumber:D2}_{text}_{text2}.json";
				File.WriteAllText(Path.Combine(OutputDir, text3), JsonConvert.SerializeObject((object)daySnapshot, (Formatting)1));
				string text4 = ((SessionState.TotalMoonScrapValue > 0) ? $"{(double)num2 / (double)SessionState.TotalMoonScrapValue * 100.0:F1}%" : "N/A");
				Plugin.Log.LogInfo((object)($"[StatsCapture] Day {daySnapshot.DayNumber} | {daySnapshot.MoonName} ({daySnapshot.Weather}) | " + $"{daySnapshot.SurvivorCount}/{daySnapshot.PlayerCount} survived | " + $"moon pool: {SessionState.TotalMoonScrapItemCount} items / {SessionState.TotalMoonScrapValue} cr | " + $"ship: {initialShipScrapValue} → {num} cr (+{num2} cr, {text4} of moon) | " + "→ " + text3));
			}
			catch (Exception arg)
			{
				Plugin.Log.LogError((object)$"[StatsCapture] Snapshot capture failed:\n{arg}");
			}
		}
	}
	public class DaySnapshot
	{
		public string SessionId { get; set; }

		public int DayNumber { get; set; }

		public string CapturedAt { get; set; }

		public int Seed { get; set; }

		public string MoonName { get; set; }

		public string Weather { get; set; }

		public string InteriorType { get; set; }

		public int PlayerCount { get; set; }

		public int SurvivorCount { get; set; }

		public List<string> SurvivorNames { get; set; } = new List<string>();


		public List<DeathRecord> Deaths { get; set; } = new List<DeathRecord>();


		public int TotalMoonScrapItemCount { get; set; }

		public int TotalMoonScrapValue { get; set; }

		public int InitialShipScrapValue { get; set; }

		public int FinalShipScrapValue { get; set; }

		public int CollectedScrapValue { get; set; }

		public int TotalScrapValueCollected { get; set; }

		public float TotalScrapWeightCollectedLbs { get; set; }

		public int TotalScrapItemCount { get; set; }

		public List<CollectedScrapRecord> ScrapCollected { get; set; } = new List<CollectedScrapRecord>();


		public ContentRegistry KnownContent { get; set; }
	}
	public class DeathRecord
	{
		public string PlayerName { get; set; }

		public string CauseOfDeath { get; set; }

		public float TimeNormalized { get; set; }
	}
	public class CollectedScrapRecord
	{
		public string Name { get; set; }

		public int Value { get; set; }

		public float WeightLbs { get; set; }

		public bool TwoHanded { get; set; }

		public bool IsConductive { get; set; }
	}
	public class ContentRegistry
	{
		public List<string> Moons { get; set; } = new List<string>();


		public List<string> WeatherTypes { get; set; } = new List<string>();


		public List<string> EnemyTypes { get; set; } = new List<string>();


		public List<string> ScrapTypes { get; set; } = new List<string>();


		public List<string> InteriorFlows { get; set; } = new List<string>();


		public string LastUpdated { get; set; }
	}
	[BepInPlugin("com.yourname.lcstatscapture", "LC Stats Capture", "1.0.0")]
	public class Plugin : BaseUnityPlugin
	{
		internal static ManualLogSource Log;

		internal static Plugin Instance;

		private void Awake()
		{
			//IL_001b: Unknown result type (might be due to invalid IL or missing references)
			Instance = this;
			Log = ((BaseUnityPlugin)this).Logger;
			ContentDiscovery.Load();
			new Harmony("com.yourname.lcstatscapture").PatchAll();
			Log.LogInfo((object)"LC Stats Capture v1.0.0 loaded.");
		}
	}
	internal static class PluginInfo
	{
		internal const string PLUGIN_GUID = "com.yourname.lcstatscapture";

		internal const string PLUGIN_NAME = "LC Stats Capture";

		internal const string PLUGIN_VERSION = "1.0.0";
	}
	internal static class ContentDiscovery
	{
		private static ContentRegistry _registry = new ContentRegistry();

		private static readonly string RegistryPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "LCStatsCapture", "content_registry.json");

		internal static void Load()
		{
			try
			{
				if (File.Exists(RegistryPath))
				{
					_registry = JsonConvert.DeserializeObject<ContentRegistry>(File.ReadAllText(RegistryPath)) ?? new ContentRegistry();
					Plugin.Log.LogInfo((object)("[StatsCapture] Registry loaded — " + $"{_registry.Moons.Count} moons, " + $"{_registry.EnemyTypes.Count} enemies, " + $"{_registry.ScrapTypes.Count} scrap types, " + $"{_registry.InteriorFlows.Count} interiors"));
				}
				else
				{
					Plugin.Log.LogInfo((object)"[StatsCapture] No registry file yet — will create on first scan.");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[StatsCapture] Registry load error: " + ex.Message + ". Starting fresh."));
				_registry = new ContentRegistry();
			}
		}

		internal static void Scan()
		{
			//IL_026f: Unknown result type (might be due to invalid IL or missing references)
			//IL_0275: Invalid comparison between Unknown and I4
			try
			{
				bool flag = false;
				if (StartOfRound.Instance?.levels != null)
				{
					SelectableLevel[] levels = StartOfRound.Instance.levels;
					foreach (SelectableLevel val in levels)
					{
						if ((Object)(object)val == (Object)null)
						{
							continue;
						}
						flag |= TryAdd(_registry.Moons, val.PlanetName);
						if (val.randomWeathers != null)
						{
							RandomWeatherWithVariables[] randomWeathers = val.randomWeathers;
							foreach (RandomWeatherWithVariables val2 in randomWeathers)
							{
								flag |= TryAdd(_registry.WeatherTypes, ((object)(LevelWeatherType)(ref val2.weatherType)).ToString());
							}
						}
						if (val.Enemies != null)
						{
							foreach (SpawnableEnemyWithRarity enemy in val.Enemies)
							{
								if ((Object)(object)enemy?.enemyType != (Object)null)
								{
									flag |= TryAdd(_registry.EnemyTypes, enemy.enemyType.enemyName);
								}
							}
						}
						if (val.OutsideEnemies != null)
						{
							foreach (SpawnableEnemyWithRarity outsideEnemy in val.OutsideEnemies)
							{
								if ((Object)(object)outsideEnemy?.enemyType != (Object)null)
								{
									flag |= TryAdd(_registry.EnemyTypes, outsideEnemy.enemyType.enemyName);
								}
							}
						}
						if (val.DaytimeEnemies == null)
						{
							continue;
						}
						foreach (SpawnableEnemyWithRarity daytimeEnemy in val.DaytimeEnemies)
						{
							if ((Object)(object)daytimeEnemy?.enemyType != (Object)null)
							{
								flag |= TryAdd(_registry.EnemyTypes, daytimeEnemy.enemyType.enemyName);
							}
						}
					}
				}
				DungeonGenerator val3 = RoundManager.Instance?.dungeonGenerator?.Generator;
				if ((Object)(object)val3?.DungeonFlow != (Object)null)
				{
					flag |= TryAdd(_registry.InteriorFlows, ((Object)val3.DungeonFlow).name);
				}
				SelectableLevel val4 = RoundManager.Instance?.currentLevel;
				if ((Object)(object)val4 != (Object)null && (int)val4.currentWeather != -1)
				{
					flag |= TryAdd(_registry.WeatherTypes, ((object)(LevelWeatherType)(ref val4.currentWeather)).ToString());
				}
				GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>();
				foreach (GrabbableObject val5 in array)
				{
					if (!((Object)(object)val5?.itemProperties == (Object)null) && val5.itemProperties.isScrap)
					{
						flag |= TryAdd(_registry.ScrapTypes, val5.itemProperties.itemName);
					}
				}
				if (flag)
				{
					Save();
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[StatsCapture] Scan error: " + ex.Message));
			}
		}

		internal static ContentRegistry GetSnapshot()
		{
			return new ContentRegistry
			{
				Moons = new List<string>(_registry.Moons),
				WeatherTypes = new List<string>(_registry.WeatherTypes),
				EnemyTypes = new List<string>(_registry.EnemyTypes),
				ScrapTypes = new List<string>(_registry.ScrapTypes),
				InteriorFlows = new List<string>(_registry.InteriorFlows),
				LastUpdated = _registry.LastUpdated
			};
		}

		private static bool TryAdd(List<string> list, string value)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return false;
			}
			if (list.Contains(value))
			{
				return false;
			}
			list.Add(value);
			return true;
		}

		private static void Save()
		{
			try
			{
				_registry.LastUpdated = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
				string directoryName = Path.GetDirectoryName(RegistryPath);
				if (!Directory.Exists(directoryName))
				{
					Directory.CreateDirectory(directoryName);
				}
				File.WriteAllText(RegistryPath, JsonConvert.SerializeObject((object)_registry, (Formatting)1));
				Plugin.Log.LogInfo((object)("[StatsCapture] Registry updated — " + $"{_registry.Moons.Count} moons, " + $"{_registry.WeatherTypes.Count} weathers, " + $"{_registry.EnemyTypes.Count} enemies, " + $"{_registry.ScrapTypes.Count} scrap types, " + $"{_registry.InteriorFlows.Count} interiors"));
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[StatsCapture] Registry save error: " + ex.Message));
			}
		}
	}
}