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 DeathsThisRound = new List(); internal static readonly HashSet DiedThisRound = new HashSet(); 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(); 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 list = instance.allPlayerScripts.Where((PlayerControllerB p) => (Object)(object)p != (Object)null && p.isPlayerControlled).ToList(); List 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 list3 = new List(); GrabbableObject[] array = Object.FindObjectsOfType(); 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(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 SurvivorNames { get; set; } = new List(); public List Deaths { get; set; } = new List(); 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 ScrapCollected { get; set; } = new List(); 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 Moons { get; set; } = new List(); public List WeatherTypes { get; set; } = new List(); public List EnemyTypes { get; set; } = new List(); public List ScrapTypes { get; set; } = new List(); public List InteriorFlows { get; set; } = new List(); 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(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(); 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(_registry.Moons), WeatherTypes = new List(_registry.WeatherTypes), EnemyTypes = new List(_registry.EnemyTypes), ScrapTypes = new List(_registry.ScrapTypes), InteriorFlows = new List(_registry.InteriorFlows), LastUpdated = _registry.LastUpdated }; } private static bool TryAdd(List 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)); } } } }