using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using LiveStatsMod.Overlay; using LiveStatsMod.Stats; using Microsoft.CodeAnalysis; using Steamworks.Data; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("LiveStats")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("LiveStats")] [assembly: AssemblyTitle("LiveStats")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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 LiveStatsMod { internal class HeartbeatDriver : MonoBehaviour { public ConfigEntry IntervalSeconds; private float _timer; private void Awake() { Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } private void OnDestroy() { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"HeartbeatDriver was destroyed - live stats will stop updating until relaunch."); } } private void Update() { _timer += Time.unscaledDeltaTime; if (_timer >= IntervalSeconds.Value) { _timer = 0f; StatsBroadcaster.Refresh(); } } } [BepInPlugin("alterego.livestats", "LiveStats", "1.0.5")] public class Plugin : BaseUnityPlugin { public const string Guid = "alterego.livestats"; public const string Name = "LiveStats"; public const string Version = "1.0.5"; internal static ManualLogSource Log; internal static ConfigEntry HeartbeatSeconds; internal static ConfigEntry OverlayEnabled; internal static ConfigEntry ShowStreamSchedule; private Harmony _harmony; private void Awake() { //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); HeartbeatSeconds = ((BaseUnityPlugin)this).Config.Bind("Overlay", "HeartbeatSeconds", 1f, "How often (seconds) to refresh live stats even without a discrete game event, so timers/quota countdowns stay live."); OverlayEnabled = ((BaseUnityPlugin)this).Config.Bind("Overlay", "Enabled", true, "Show the in-game HUD overlay (day, moon, weather, quota, average scrap per day, scrap on ship)."); ShowStreamSchedule = ((BaseUnityPlugin)this).Config.Bind("PauseMenu", "ShowStreamSchedule", false, "Show a small \"STREAM SCHEDULE\" box in the pause menu's Stats tab. Off by default since the schedule text baked into the mod belongs to its original author - only turn this on if that's you."); _harmony = new Harmony("alterego.livestats"); _harmony.PatchAll(); PlayerLifetimeStats.ResetAllHighestQuotasOnce(); PlayerLifetimeStats.ResetArchNemesisTallyOnce(); Log.LogInfo((object)"LiveStats v1.0.5 loaded."); } internal static void SpawnHeartbeat() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) new GameObject("LiveStats_Heartbeat").AddComponent().IntervalSeconds = HeartbeatSeconds; } internal static void SpawnOverlay() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) new GameObject("LiveStats_Overlay").AddComponent(); } } } namespace LiveStatsMod.Stats { internal static class EnemyAttackTracker { private static readonly TimeSpan FreshWindow = TimeSpan.FromSeconds(15.0); private static readonly ConcurrentDictionary LastAttacker = new ConcurrentDictionary(); public static void Record(PlayerControllerB player, EnemyAI enemy) { if (!((Object)(object)player == (Object)null) && !((Object)(object)enemy == (Object)null) && !((Object)(object)enemy.enemyType == (Object)null)) { LastAttacker[(int)player.playerClientId] = (enemy.enemyType.enemyName, DateTime.UtcNow); } } public static bool TryGetAttacker(int playerClientId, out string enemyName) { if (LastAttacker.TryGetValue(playerClientId, out (string, DateTime) value) && DateTime.UtcNow - value.Item2 <= FreshWindow) { (enemyName, _) = value; return true; } enemyName = null; return false; } public static void ClearAll() { LastAttacker.Clear(); } } internal static class LiveStats { public static bool InSaveFile; public static int DayNumber = 1; public static int Seed; public static string MoonName = ""; public static string Weather = ""; public static float TimeUntilDeadline; public static int DaysUntilDeadline; public static int ProfitQuota; public static int QuotaFulfilled; public static int ScrapValueOnShip; public static int AverageScrapSum; public static int AverageScrapDayCount; public static int QuotaNumber; public static int LastObservedProfitQuota = -1; public static int ScrapValueCollected; public static int LastDayScrapCollected; public static bool IsLanded; public static DateTime? MoonArrivedAt; public static bool WasInSaveFile; public static string OnScreenMessage = ""; public static double CurrentRunMoonSeconds; public static bool CurrentRunSegmentActive; public static DateTime? CurrentRunSegmentStartedAt; public static double CurrentRunDowntimeSeconds; public static bool DowntimeActive; public static DateTime? DowntimeSegmentStartedAt; public static string PersistedValuesLoadedForSave; public static readonly ConcurrentDictionary Players = new ConcurrentDictionary(); private const string CrewDeathsKey = "LCLiveStatsTracker_CrewDeaths"; private static readonly Dictionary PersistedCrewDeathsByUsername = new Dictionary(); public static int AverageScrapPerDay() { if (AverageScrapDayCount != 0) { return (int)Math.Round((double)AverageScrapSum / (double)AverageScrapDayCount); } return 0; } public static void LoadPersistedAverage(string saveFileName) { AverageScrapSum = ES3.Load("LCLiveStatsTracker_AvgScrapSum", saveFileName, 0); AverageScrapDayCount = ES3.Load("LCLiveStatsTracker_AvgScrapDayCount", saveFileName, 0); } public static void LoadPersistedRunMoonSeconds(string saveFileName) { CurrentRunMoonSeconds = ES3.Load("LCLiveStatsTracker_RunMoonSeconds", saveFileName, 0.0); } public static void LoadPersistedDowntime(string saveFileName) { CurrentRunDowntimeSeconds = ES3.Load("LCLiveStatsTracker_RunDowntimeSeconds", saveFileName, 0.0); } public static void LoadPersistedCrewDeaths(string saveFileName) { PersistedCrewDeathsByUsername.Clear(); foreach (KeyValuePair item in ES3.Load>("LCLiveStatsTracker_CrewDeaths", saveFileName, new Dictionary())) { PersistedCrewDeathsByUsername[item.Key] = item.Value; } } public static int GetPersistedDeathCount(string username) { if (string.IsNullOrEmpty(username) || !PersistedCrewDeathsByUsername.TryGetValue(username, out var value)) { return 0; } return value; } public static void ResetRunScopedStatsInMemory() { AverageScrapSum = 0; AverageScrapDayCount = 0; CurrentRunMoonSeconds = 0.0; CurrentRunDowntimeSeconds = 0.0; PersistedCrewDeathsByUsername.Clear(); } public static void PersistCrewDeath(string username, int deathCount, string saveFileName) { PersistedCrewDeathsByUsername[username] = deathCount; if (!string.IsNullOrEmpty(saveFileName)) { Dictionary dictionary = ES3.Load>("LCLiveStatsTracker_CrewDeaths", saveFileName, new Dictionary()); dictionary[username] = deathCount; ES3.Save>("LCLiveStatsTracker_CrewDeaths", dictionary, saveFileName); } } public static void ResetCrewDeaths(string saveFileName) { foreach (PlayerStats value in Players.Values) { value.DeathCount = 0; } PersistedCrewDeathsByUsername.Clear(); if (!string.IsNullOrEmpty(saveFileName)) { ES3.Save>("LCLiveStatsTracker_CrewDeaths", new Dictionary(), saveFileName); } } public static void ResetForNewSession() { DayNumber = 1; Seed = 0; MoonName = ""; Weather = ""; TimeUntilDeadline = 0f; DaysUntilDeadline = 0; ProfitQuota = 0; QuotaFulfilled = 0; ScrapValueOnShip = 0; QuotaNumber = 0; LastObservedProfitQuota = -1; ScrapValueCollected = 0; LastDayScrapCollected = 0; IsLanded = false; MoonArrivedAt = null; CurrentRunSegmentActive = false; CurrentRunSegmentStartedAt = null; DowntimeActive = false; DowntimeSegmentStartedAt = null; PersistedValuesLoadedForSave = null; Players.Clear(); } } internal class PlayerStats { public string Name; public bool Alive = true; public bool Disconnected; public string CauseOfDeath; public int DeathCount; public int JumpsThisRound; } internal static class LobbyPrivacy { public const string PublicDataKey = "LiveStats_Public"; public static bool IsCurrentLobbyPublic() { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) Lobby? val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.currentLobby : ((Lobby?)null)); if (!val.HasValue) { return false; } Lobby value = val.Value; return ((Lobby)(ref value)).GetData("LiveStats_Public") == "1"; } } internal static class PlayerLifetimeStats { private const string GlobalFile = "LCLiveStatsTracker_GlobalPlayerStats"; private const string KnownPlayersKey = "LCLiveStatsTracker_KnownPlayers"; private const string HighestQuotaResetDoneKey = "LCLiveStatsTracker_HighestQuotaResetDone"; private const string ArchNemesisResetDoneKey = "LCLiveStatsTracker_ArchNemesisResetDone"; private static readonly string[] AllSuffixes = new string[15] { "DaysSurvived", "CurrentStreak", "MaxStreak", "TotalSteps", "TotalScrapCollected", "ScrapDayCount", "TotalJumps", "TotalDamageTaken", "HighestQuota", "MoonVisits", "TotalMoonTimeSeconds", "TotalDeaths", "DeathCauses", "DeathMoons", "EnemyKills" }; private static string Key(string username, string suffix) { return "LCLiveStatsTracker_P_" + username + "_" + suffix; } private static void RememberPlayer(string username) { List list = ES3.Load>("LCLiveStatsTracker_KnownPlayers", "LCLiveStatsTracker_GlobalPlayerStats", new List()); if (!list.Contains(username)) { list.Add(username); ES3.Save>("LCLiveStatsTracker_KnownPlayers", list, "LCLiveStatsTracker_GlobalPlayerStats"); } } public static void RecordDayEnd(string username, bool survived, int stepsThisRound, int scrapThisRound, int jumpsThisRound, int damageTakenThisRound, int currentQuota, string moonName, bool countsForMoon) { if (string.IsNullOrEmpty(username)) { return; } RememberPlayer(username); if (survived) { ES3.Save(Key(username, "DaysSurvived"), ES3.Load(Key(username, "DaysSurvived"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + 1, "LCLiveStatsTracker_GlobalPlayerStats"); int num = ES3.Load(Key(username, "CurrentStreak"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + 1; ES3.Save(Key(username, "CurrentStreak"), num, "LCLiveStatsTracker_GlobalPlayerStats"); if (num > ES3.Load(Key(username, "MaxStreak"), "LCLiveStatsTracker_GlobalPlayerStats", 0)) { ES3.Save(Key(username, "MaxStreak"), num, "LCLiveStatsTracker_GlobalPlayerStats"); } } else { ES3.Save(Key(username, "CurrentStreak"), 0, "LCLiveStatsTracker_GlobalPlayerStats"); } if (stepsThisRound > 0) { ES3.Save(Key(username, "TotalSteps"), ES3.Load(Key(username, "TotalSteps"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + stepsThisRound, "LCLiveStatsTracker_GlobalPlayerStats"); } if (scrapThisRound > 0) { ES3.Save(Key(username, "TotalScrapCollected"), ES3.Load(Key(username, "TotalScrapCollected"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + scrapThisRound, "LCLiveStatsTracker_GlobalPlayerStats"); } if (countsForMoon) { ES3.Save(Key(username, "ScrapDayCount"), ES3.Load(Key(username, "ScrapDayCount"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + 1, "LCLiveStatsTracker_GlobalPlayerStats"); } if (jumpsThisRound > 0) { ES3.Save(Key(username, "TotalJumps"), ES3.Load(Key(username, "TotalJumps"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + jumpsThisRound, "LCLiveStatsTracker_GlobalPlayerStats"); } if (damageTakenThisRound > 0) { ES3.Save(Key(username, "TotalDamageTaken"), ES3.Load(Key(username, "TotalDamageTaken"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + damageTakenThisRound, "LCLiveStatsTracker_GlobalPlayerStats"); } if (!LobbyPrivacy.IsCurrentLobbyPublic() && currentQuota > ES3.Load(Key(username, "HighestQuota"), "LCLiveStatsTracker_GlobalPlayerStats", 0)) { ES3.Save(Key(username, "HighestQuota"), currentQuota, "LCLiveStatsTracker_GlobalPlayerStats"); } if (countsForMoon && !string.IsNullOrEmpty(moonName)) { Dictionary dictionary = ES3.Load>(Key(username, "MoonVisits"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); dictionary[moonName] = ((!dictionary.TryGetValue(moonName, out var value)) ? 1 : (value + 1)); ES3.Save>(Key(username, "MoonVisits"), dictionary, "LCLiveStatsTracker_GlobalPlayerStats"); } } public static void RecordMoonTime(string username, double elapsedSeconds) { if (!string.IsNullOrEmpty(username) && !(elapsedSeconds <= 0.0)) { RememberPlayer(username); double num = ES3.Load(Key(username, "TotalMoonTimeSeconds"), "LCLiveStatsTracker_GlobalPlayerStats", 0.0) + elapsedSeconds; ES3.Save(Key(username, "TotalMoonTimeSeconds"), num, "LCLiveStatsTracker_GlobalPlayerStats"); } } public static void RecordDeath(string username, string cause, string moonName, string killedBy) { if (!string.IsNullOrEmpty(username)) { RememberPlayer(username); ES3.Save(Key(username, "TotalDeaths"), ES3.Load(Key(username, "TotalDeaths"), "LCLiveStatsTracker_GlobalPlayerStats", 0) + 1, "LCLiveStatsTracker_GlobalPlayerStats"); Dictionary dictionary = ES3.Load>(Key(username, "DeathCauses"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); dictionary[cause] = ((!dictionary.TryGetValue(cause, out var value)) ? 1 : (value + 1)); ES3.Save>(Key(username, "DeathCauses"), dictionary, "LCLiveStatsTracker_GlobalPlayerStats"); if (!string.IsNullOrEmpty(moonName)) { Dictionary dictionary2 = ES3.Load>(Key(username, "DeathMoons"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); dictionary2[moonName] = ((!dictionary2.TryGetValue(moonName, out var value2)) ? 1 : (value2 + 1)); ES3.Save>(Key(username, "DeathMoons"), dictionary2, "LCLiveStatsTracker_GlobalPlayerStats"); } if (!string.IsNullOrEmpty(killedBy)) { Dictionary dictionary3 = ES3.Load>(Key(username, "EnemyKills"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); dictionary3[killedBy] = ((!dictionary3.TryGetValue(killedBy, out var value3)) ? 1 : (value3 + 1)); ES3.Save>(Key(username, "EnemyKills"), dictionary3, "LCLiveStatsTracker_GlobalPlayerStats"); } } } public static List GetKnownPlayers() { return ES3.Load>("LCLiveStatsTracker_KnownPlayers", "LCLiveStatsTracker_GlobalPlayerStats", new List()); } public static bool RemovePlayer(string username) { List knownPlayers = GetKnownPlayers(); if (knownPlayers.RemoveAll((string n) => string.Equals(n, username, StringComparison.OrdinalIgnoreCase)) == 0) { return false; } string[] allSuffixes = AllSuffixes; foreach (string suffix in allSuffixes) { ES3.DeleteKey(Key(username, suffix), "LCLiveStatsTracker_GlobalPlayerStats"); } ES3.Save>("LCLiveStatsTracker_KnownPlayers", knownPlayers, "LCLiveStatsTracker_GlobalPlayerStats"); return true; } public static void ResetAllHighestQuotasOnce() { if (ES3.Load("LCLiveStatsTracker_HighestQuotaResetDone", "LCLiveStatsTracker_GlobalPlayerStats", false)) { return; } foreach (string knownPlayer in GetKnownPlayers()) { ES3.Save(Key(knownPlayer, "HighestQuota"), 0, "LCLiveStatsTracker_GlobalPlayerStats"); } ES3.Save("LCLiveStatsTracker_HighestQuotaResetDone", true, "LCLiveStatsTracker_GlobalPlayerStats"); } public static void ResetArchNemesisTallyOnce() { if (ES3.Load("LCLiveStatsTracker_ArchNemesisResetDone", "LCLiveStatsTracker_GlobalPlayerStats", false)) { return; } foreach (string knownPlayer in GetKnownPlayers()) { ES3.DeleteKey(Key(knownPlayer, "EnemyKills"), "LCLiveStatsTracker_GlobalPlayerStats"); } ES3.Save("LCLiveStatsTracker_ArchNemesisResetDone", true, "LCLiveStatsTracker_GlobalPlayerStats"); } public static string BuildProfileText(string username) { int num = ES3.Load(Key(username, "DaysSurvived"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num2 = ES3.Load(Key(username, "MaxStreak"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num3 = ES3.Load(Key(username, "HighestQuota"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num4 = ES3.Load(Key(username, "TotalDeaths"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num5 = ES3.Load(Key(username, "TotalScrapCollected"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num6 = ES3.Load(Key(username, "ScrapDayCount"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num7 = ES3.Load(Key(username, "TotalSteps"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num8 = ES3.Load(Key(username, "TotalJumps"), "LCLiveStatsTracker_GlobalPlayerStats", 0); int num9 = ES3.Load(Key(username, "TotalDamageTaken"), "LCLiveStatsTracker_GlobalPlayerStats", 0); double totalSeconds = ES3.Load(Key(username, "TotalMoonTimeSeconds"), "LCLiveStatsTracker_GlobalPlayerStats", 0.0); Dictionary dictionary = ES3.Load>(Key(username, "MoonVisits"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); Dictionary dictionary2 = ES3.Load>(Key(username, "DeathMoons"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); Dictionary dictionary3 = ES3.Load>(Key(username, "EnemyKills"), "LCLiveStatsTracker_GlobalPlayerStats", new Dictionary()); int num10 = ((num6 > 0) ? (num5 / num6) : 0); string text = ((dictionary.Count > 0) ? dictionary.OrderByDescending((KeyValuePair kv) => kv.Value).First().Key : "-"); string text2 = ((dictionary2.Count > 0) ? dictionary2.OrderByDescending((KeyValuePair kv) => kv.Value).First().Key : "-"); string text3 = ((dictionary3.Count > 0) ? dictionary3.OrderByDescending((KeyValuePair kv) => kv.Value).First().Key : "-"); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("PROFILE: " + username); stringBuilder.AppendLine(); stringBuilder.AppendLine($"Days Survived: {num}"); stringBuilder.AppendLine($"Best Streak: {num2}"); stringBuilder.AppendLine($"Highest Quota: ${num3}"); stringBuilder.AppendLine($"Total Deaths: {num4}"); stringBuilder.AppendLine($"Total Scrap Collected: ${num5}"); stringBuilder.AppendLine($"Average Scrap/Day: ${num10}"); stringBuilder.AppendLine("Favorite Moon: " + text); stringBuilder.AppendLine("Most Dangerous Moon: " + text2); stringBuilder.AppendLine("Total Time Spent On A Moon: " + FormatDuration(totalSeconds)); stringBuilder.AppendLine($"Total Steps Taken: {num7}"); stringBuilder.AppendLine($"Total Jumps: {num8}"); stringBuilder.AppendLine($"Total Damage Taken: {num9}"); stringBuilder.AppendLine("Arch Nemesis: " + text3); return stringBuilder.ToString(); } private static string FormatDuration(double totalSeconds) { TimeSpan timeSpan = TimeSpan.FromSeconds(totalSeconds); if (!(timeSpan.TotalHours >= 1.0)) { return $"{timeSpan.Minutes}m {timeSpan.Seconds}s"; } return $"{(int)timeSpan.TotalHours}h {timeSpan.Minutes}m"; } } internal static class StatsBroadcaster { private const string ClientSessionMarker = "__client_session__"; public static string GetPersistenceSaveFileName() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null || !singleton.IsHost) { return null; } if (!((Object)(object)GameNetworkManager.Instance != (Object)null)) { return null; } return GameNetworkManager.Instance.currentSaveFileName; } public static void Refresh() { try { UpdateLiveStats(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed to update live stats: {arg}"); } } private static void UpdateLiveStats() { StartOfRound instance = StartOfRound.Instance; TimeOfDay instance2 = TimeOfDay.Instance; bool flag = (Object)(object)NetworkManager.Singleton != (Object)null && NetworkManager.Singleton.IsListening; if (flag && !LiveStats.WasInSaveFile) { LiveStats.ResetForNewSession(); } LiveStats.WasInSaveFile = flag; LiveStats.InSaveFile = flag; if (flag) { if (NetworkManager.Singleton.IsHost) { string text = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.currentSaveFileName : null); if (!string.IsNullOrEmpty(text) && LiveStats.PersistedValuesLoadedForSave != text) { LiveStats.LoadPersistedAverage(text); LiveStats.LoadPersistedRunMoonSeconds(text); LiveStats.LoadPersistedDowntime(text); LiveStats.LoadPersistedCrewDeaths(text); LiveStats.PersistedValuesLoadedForSave = text; } } else if (LiveStats.PersistedValuesLoadedForSave != "__client_session__") { LiveStats.ResetRunScopedStatsInMemory(); LiveStats.PersistedValuesLoadedForSave = "__client_session__"; } } if (flag && (Object)(object)instance != (Object)null) { LiveStats.Seed = instance.randomMapSeed; LiveStats.MoonName = (((Object)(object)instance.currentLevel != (Object)null) ? instance.currentLevel.PlanetName : ""); LiveStats.DayNumber = ((instance.gameStats != null) ? instance.gameStats.daysSpent : 0) + 1; if (instance.allPlayerScripts != null) { PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB p in allPlayerScripts) { if (!((Object)(object)p == (Object)null) && (LiveStats.Players.ContainsKey(p.playerClientId) || p.isPlayerControlled || p.isPlayerDead || p.disconnectedMidGame)) { PlayerStats orAdd = LiveStats.Players.GetOrAdd(p.playerClientId, (ulong _) => new PlayerStats { Name = p.playerUsername, DeathCount = LiveStats.GetPersistedDeathCount(p.playerUsername) }); orAdd.Name = p.playerUsername; orAdd.Alive = !p.isPlayerDead; bool flag2 = instance.ClientPlayerList != null && instance.ClientPlayerList.ContainsKey(p.actualClientId); orAdd.Disconnected = p.disconnectedMidGame || !flag2; } } } } if (flag && (Object)(object)instance2 != (Object)null) { LiveStats.Weather = ((object)Unsafe.As(ref instance2.currentLevelWeather)/*cast due to .constrained prefix*/).ToString(); LiveStats.ProfitQuota = instance2.profitQuota; LiveStats.QuotaFulfilled = instance2.quotaFulfilled; LiveStats.TimeUntilDeadline = instance2.timeUntilDeadline; LiveStats.DaysUntilDeadline = instance2.daysUntilDeadline; if (LiveStats.LastObservedProfitQuota == -1) { LiveStats.QuotaNumber = instance2.timesFulfilledQuota + 1; } else if (instance2.profitQuota != LiveStats.LastObservedProfitQuota) { LiveStats.QuotaNumber++; } LiveStats.LastObservedProfitQuota = instance2.profitQuota; bool currentDayTimeStarted = instance2.currentDayTimeStarted; if (currentDayTimeStarted && !LiveStats.IsLanded) { LiveStats.MoonArrivedAt = DateTime.UtcNow; } else if (!currentDayTimeStarted && LiveStats.IsLanded && LiveStats.MoonArrivedAt.HasValue) { double totalSeconds = (DateTime.UtcNow - LiveStats.MoonArrivedAt.Value).TotalSeconds; if ((Object)(object)instance != (Object)null && instance.allPlayerScripts != null) { PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && !val.disconnectedMidGame && (val.isPlayerControlled || val.isPlayerDead)) { PlayerLifetimeStats.RecordMoonTime(val.playerUsername, totalSeconds); } } } } LiveStats.IsLanded = currentDayTimeStarted; bool flag3 = !currentDayTimeStarted && (Object)(object)instance != (Object)null && (Object)(object)instance.currentLevel != (Object)null && !instance.currentLevel.spawnEnemiesAndScrap && instance.shipDoorsEnabled; bool flag4 = currentDayTimeStarted || flag3; if (flag4 && !LiveStats.CurrentRunSegmentActive) { LiveStats.CurrentRunSegmentStartedAt = DateTime.UtcNow; } else if (!flag4 && LiveStats.CurrentRunSegmentActive && LiveStats.CurrentRunSegmentStartedAt.HasValue) { double totalSeconds2 = (DateTime.UtcNow - LiveStats.CurrentRunSegmentStartedAt.Value).TotalSeconds; LiveStats.CurrentRunMoonSeconds += totalSeconds2; string persistenceSaveFileName = GetPersistenceSaveFileName(); if (!string.IsNullOrEmpty(persistenceSaveFileName)) { ES3.Save("LCLiveStatsTracker_RunMoonSeconds", LiveStats.CurrentRunMoonSeconds, persistenceSaveFileName); } } LiveStats.CurrentRunSegmentActive = flag4; bool flag5 = !currentDayTimeStarted && !flag3; if (flag5 && !LiveStats.DowntimeActive) { LiveStats.DowntimeSegmentStartedAt = DateTime.UtcNow; } else if (!flag5 && LiveStats.DowntimeActive && LiveStats.DowntimeSegmentStartedAt.HasValue) { double totalSeconds3 = (DateTime.UtcNow - LiveStats.DowntimeSegmentStartedAt.Value).TotalSeconds; LiveStats.CurrentRunDowntimeSeconds += totalSeconds3; string persistenceSaveFileName2 = GetPersistenceSaveFileName(); if (!string.IsNullOrEmpty(persistenceSaveFileName2)) { ES3.Save("LCLiveStatsTracker_RunDowntimeSeconds", LiveStats.CurrentRunDowntimeSeconds, persistenceSaveFileName2); } } LiveStats.DowntimeActive = flag5; } else { LiveStats.IsLanded = false; } if (flag && (Object)(object)instance != (Object)null) { LiveStats.ScrapValueOnShip = instance.GetValueOfAllScrap(true, false); LiveStats.ScrapValueCollected = ((instance.gameStats != null) ? instance.gameStats.scrapValueCollected : 0); } } } internal static class StatsSummaryText { private static readonly Regex PhantomPlayerName = new Regex("^Player #\\d+$", RegexOptions.IgnoreCase); public static string Build(bool richText) { string text = (richText ? "" : ""); string text2 = (richText ? "" : ""); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(text + "CURRENT RUN: " + FormatElapsed() + text2); stringBuilder.AppendLine("Downtime: " + FormatDowntime()); stringBuilder.AppendLine(); stringBuilder.AppendLine($"Day: {LiveStats.DayNumber}"); stringBuilder.AppendLine("Moon: " + ((!LiveStats.IsLanded) ? "In Orbit" : (string.IsNullOrEmpty(LiveStats.MoonName) ? "-" : LiveStats.MoonName))); stringBuilder.AppendLine("Weather: " + (LiveStats.IsLanded ? LiveStats.Weather : "-")); stringBuilder.AppendLine($"Quota {LiveStats.QuotaNumber}: ${LiveStats.ProfitQuota}"); stringBuilder.AppendLine($"Scrap On Ship: ${LiveStats.ScrapValueOnShip}"); stringBuilder.AppendLine($"Total Scrap Sold: ${LiveStats.ScrapValueCollected}"); stringBuilder.AppendLine($"Average Scrap/Day: ${LiveStats.AverageScrapPerDay()}"); stringBuilder.AppendLine(); stringBuilder.AppendLine(text + "CREW" + text2); bool flag = false; foreach (KeyValuePair player in LiveStats.Players) { if (!PhantomPlayerName.IsMatch((player.Value.Name ?? "").Trim())) { flag = true; PlayerStats value = player.Value; stringBuilder.AppendLine($"{value.Name}: {value.DeathCount} deaths"); } } if (!flag) { stringBuilder.AppendLine("-"); } return stringBuilder.ToString(); } private static string FormatElapsed() { double num = LiveStats.CurrentRunMoonSeconds; if (LiveStats.CurrentRunSegmentActive && LiveStats.CurrentRunSegmentStartedAt.HasValue) { num += (DateTime.UtcNow - LiveStats.CurrentRunSegmentStartedAt.Value).TotalSeconds; } return FormatDuration(num); } private static string FormatDowntime() { double num = LiveStats.CurrentRunDowntimeSeconds; if (LiveStats.DowntimeActive && LiveStats.DowntimeSegmentStartedAt.HasValue) { num += (DateTime.UtcNow - LiveStats.DowntimeSegmentStartedAt.Value).TotalSeconds; } return FormatDuration(num); } private static string FormatDuration(double totalSeconds) { TimeSpan timeSpan = TimeSpan.FromSeconds(totalSeconds); if (!(timeSpan.TotalHours >= 1.0)) { return $"{timeSpan.Minutes:00}:{timeSpan.Seconds:00}"; } return $"{(int)timeSpan.TotalHours}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}"; } } } namespace LiveStatsMod.Patches { internal static class EnemyAttackHelpers { public static void FromCollision(EnemyAI instance, Collider other) { EnemyAttackTracker.Record(instance.MeetsStandardPlayerCollisionConditions(other, false, false), instance); } public static void FromPlayerId(EnemyAI instance, int playerId) { StartOfRound instance2 = StartOfRound.Instance; if (!((Object)(object)instance2 == (Object)null) && instance2.allPlayerScripts != null && playerId >= 0 && playerId < instance2.allPlayerScripts.Length) { EnemyAttackTracker.Record(instance2.allPlayerScripts[playerId], instance); } } public static void FromLocalPlayer(EnemyAI instance) { EnemyAttackTracker.Record(((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null, instance); } } [HarmonyPatch(typeof(BaboonBirdAI), "OnCollideWithPlayer")] internal static class BaboonBirdAttackPatch { private static void Prefix(BaboonBirdAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(BlobAI), "OnCollideWithPlayer")] internal static class BlobAttackPatch { private static void Prefix(BlobAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(BushWolfEnemy), "OnCollideWithPlayer")] internal static class BushWolfAttackPatch { private static void Prefix(BushWolfEnemy __instance) { EnemyAttackTracker.Record(((EnemyAI)__instance).targetPlayer, (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(ButlerBeesEnemyAI), "OnCollideWithPlayer")] internal static class ButlerBeesAttackPatch { private static void Prefix(ButlerBeesEnemyAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(ButlerEnemyAI), "OnCollideWithPlayer")] internal static class ButlerAttackPatch { private static void Prefix(ButlerEnemyAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(CadaverBloomAI))] internal static class CadaverBloomAttackPatch { [HarmonyPatch("BurstForth")] [HarmonyPrefix] private static void BurstForthPrefix(CadaverBloomAI __instance, PlayerControllerB player) { EnemyAttackTracker.Record(player, (EnemyAI)(object)__instance); } [HarmonyPatch("OnCollideWithPlayer")] [HarmonyPrefix] private static void CollidePrefix(CadaverBloomAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(CaveDwellerAI), "KillPlayerAnimationClientRpc")] internal static class CaveDwellerAttackPatch { private static void Prefix(CaveDwellerAI __instance, int playerObjectId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerObjectId); } } [HarmonyPatch(typeof(CentipedeAI), "DamagePlayerOnIntervals")] internal static class CentipedeAttackPatch { private static void Prefix(CentipedeAI __instance) { EnemyAttackTracker.Record(__instance.clingingToPlayer, (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(ClaySurgeonAI), "OnCollideWithPlayer")] internal static class ClaySurgeonAttackPatch { private static void Prefix(ClaySurgeonAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(CrawlerAI), "OnCollideWithPlayer")] internal static class CrawlerAttackPatch { private static void Prefix(CrawlerAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(DressGirlAI), "OnCollideWithPlayer")] internal static class DressGirlAttackPatch { private static void Prefix(DressGirlAI __instance) { EnemyAttackTracker.Record(__instance.hauntingPlayer, (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(FlowermanAI), "KillPlayerAnimationClientRpc")] internal static class FlowermanAttackPatch { private static void Prefix(FlowermanAI __instance, int playerObjectId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerObjectId); } } [HarmonyPatch(typeof(ForestGiantAI))] internal static class ForestGiantAttackPatch { [HarmonyPatch("AnimationEventA")] [HarmonyPrefix] private static void AnimationEventAPrefix(ForestGiantAI __instance) { EnemyAttackHelpers.FromLocalPlayer((EnemyAI)(object)__instance); } [HarmonyPatch("BeginEatPlayer")] [HarmonyPrefix] private static void BeginEatPlayerPrefix(ForestGiantAI __instance, PlayerControllerB playerBeingEaten) { EnemyAttackTracker.Record(playerBeingEaten, (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(GiantKiwiAI), "AnimationEventB")] internal static class GiantKiwiAttackPatch { private static void Prefix(GiantKiwiAI __instance) { EnemyAttackHelpers.FromLocalPlayer((EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(HoarderBugAI), "OnCollideWithPlayer")] internal static class HoarderBugAttackPatch { private static void Prefix(HoarderBugAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(JesterAI), "KillPlayerClientRpc")] internal static class JesterAttackPatch { private static void Prefix(JesterAI __instance, int playerId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerId); } } [HarmonyPatch(typeof(LassoManAI), "OnCollideWithPlayer")] internal static class LassoManAttackPatch { private static void Prefix(LassoManAI __instance, Collider other) { EnemyAttackTracker.Record(((Component)other).gameObject.GetComponent(), (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(MaskedPlayerEnemy), "KillPlayerAnimationClientRpc")] internal static class MaskedAttackPatch { private static void Prefix(MaskedPlayerEnemy __instance, int playerObjectId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerObjectId); } } [HarmonyPatch(typeof(MouthDogAI), "KillPlayerClientRpc")] internal static class MouthDogAttackPatch { private static void Prefix(MouthDogAI __instance, int playerId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerId); } } [HarmonyPatch(typeof(NutcrackerEnemyAI), "LegKickPlayer")] internal static class NutcrackerAttackPatch { private static void Prefix(NutcrackerEnemyAI __instance, int playerId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerId); } } [HarmonyPatch(typeof(PufferAI), "OnCollideWithPlayer")] internal static class PufferAttackPatch { private static void Prefix(PufferAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(PumaAI), "OnCollideWithPlayer")] internal static class PumaAttackPatch { private static void Prefix(PumaAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(RadMechAI))] internal static class RadMechAttackPatch { [HarmonyPatch("BeginTorchPlayer")] [HarmonyPrefix] private static void BeginTorchPlayerPrefix(RadMechAI __instance, PlayerControllerB playerBeingTorched) { EnemyAttackTracker.Record(playerBeingTorched, (EnemyAI)(object)__instance); } [HarmonyPatch("Stomp")] [HarmonyPrefix] private static void StompPrefix(RadMechAI __instance) { EnemyAttackHelpers.FromLocalPlayer((EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(RedLocustBees))] internal static class RedLocustBeesAttackPatch { [HarmonyPatch("BeeKillPlayerOnLocalClient")] [HarmonyPrefix] private static void BeeKillPrefix(RedLocustBees __instance, int playerId) { EnemyAttackHelpers.FromPlayerId((EnemyAI)(object)__instance, playerId); } [HarmonyPatch("OnCollideWithPlayer")] [HarmonyPrefix] private static void CollidePrefix(RedLocustBees __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(SandSpiderAI), "OnCollideWithPlayer")] internal static class SandSpiderAttackPatch { private static void Prefix(SandSpiderAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(SandWormAI), "EatPlayer")] internal static class SandWormAttackPatch { private static void Prefix(SandWormAI __instance, PlayerControllerB playerScript) { EnemyAttackTracker.Record(playerScript, (EnemyAI)(object)__instance); } } [HarmonyPatch(typeof(SpringManAI), "OnCollideWithPlayer")] internal static class SpringManAttackPatch { private static void Prefix(SpringManAI __instance, Collider other) { EnemyAttackHelpers.FromCollision((EnemyAI)(object)__instance, other); } } [HarmonyPatch(typeof(GameNetworkManager), "SteamMatchmaking_OnLobbyCreated")] internal static class LobbyPrivacyPatch { private static void Postfix(GameNetworkManager __instance, Lobby lobby) { bool flag = __instance.lobbyHostSettings != null && __instance.lobbyHostSettings.isLobbyPublic; ((Lobby)(ref lobby)).SetData("LiveStats_Public", flag ? "1" : "0"); } } [HarmonyPatch(typeof(MenuManager), "Start")] internal static class MenuManagerStartPatch { private static bool _spawned; private static void Postfix(MenuManager __instance) { if (!_spawned && !__instance.isInitScene) { _spawned = true; Plugin.SpawnHeartbeat(); Plugin.SpawnOverlay(); } } } [HarmonyPatch(typeof(QuickMenuManager), "Start")] internal static class QuickMenuStatsTabPatch { private static void Postfix(QuickMenuManager __instance) { try { PauseMenuStatsPanel.Install(__instance); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed to install pause menu Stats tab: {arg}"); } } } [HarmonyPatch(typeof(QuickMenuManager), "CloseQuickMenuPanels")] internal static class QuickMenuCloseStatsTabPatch { private static void Postfix() { try { PauseMenuStatsPanel.Close(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Failed to close pause menu Stats tab: {arg}"); } } } [HarmonyPatch(typeof(TimeOfDay), "OnDayChanged")] internal static class DayChangedPatch { private static void Postfix() { StatsBroadcaster.Refresh(); EnemyAttackTracker.ClearAll(); } } [HarmonyPatch(typeof(StartOfRound), "FirePlayersAfterDeadlineClientRpc")] internal static class PlayersFiredPatch { private static void Postfix() { StatsBroadcaster.Refresh(); } } [HarmonyPatch(typeof(StartOfRound), "ResetShip")] internal static class ShipResetPatch { private static void Postfix() { LiveStats.QuotaNumber = 0; LiveStats.LastObservedProfitQuota = -1; LiveStats.CurrentRunMoonSeconds = 0.0; LiveStats.CurrentRunSegmentActive = false; LiveStats.CurrentRunSegmentStartedAt = null; LiveStats.CurrentRunDowntimeSeconds = 0.0; LiveStats.DowntimeActive = false; LiveStats.DowntimeSegmentStartedAt = null; string persistenceSaveFileName = StatsBroadcaster.GetPersistenceSaveFileName(); if (!string.IsNullOrEmpty(persistenceSaveFileName)) { ES3.Save("LCLiveStatsTracker_RunMoonSeconds", 0.0, persistenceSaveFileName); ES3.Save("LCLiveStatsTracker_RunDowntimeSeconds", 0.0, persistenceSaveFileName); } LiveStats.ResetCrewDeaths(persistenceSaveFileName); foreach (PlayerStats value in LiveStats.Players.Values) { value.JumpsThisRound = 0; } } } [HarmonyPatch(typeof(StartOfRound), "Awake")] internal static class JumpTrackingPatch { private static void Postfix(StartOfRound __instance) { ((UnityEvent)(object)__instance.PlayerJumpEvent).AddListener((UnityAction)OnPlayerJump); } private static void OnPlayerJump(PlayerControllerB player) { if (!((Object)(object)player == (Object)null)) { LiveStats.Players.GetOrAdd(player.playerClientId, (ulong _) => new PlayerStats { Name = player.playerUsername, DeathCount = LiveStats.GetPersistedDeathCount(player.playerUsername) }).JumpsThisRound++; } } } [HarmonyPatch(typeof(StartOfRound), "EndOfGame")] internal static class EndOfDayScrapPatch { private static void Prefix(int scrapCollected) { LiveStats.LastDayScrapCollected = scrapCollected; StartOfRound instance = StartOfRound.Instance; string persistenceSaveFileName = StatsBroadcaster.GetPersistenceSaveFileName(); SelectableLevel val = (((Object)(object)instance != (Object)null) ? instance.currentLevel : null); bool flag = (Object)(object)val != (Object)null && val.spawnEnemiesAndScrap; if (flag) { LiveStats.AverageScrapSum += scrapCollected; LiveStats.AverageScrapDayCount++; if (!string.IsNullOrEmpty(persistenceSaveFileName)) { ES3.Save("LCLiveStatsTracker_AvgScrapSum", LiveStats.AverageScrapSum, persistenceSaveFileName); ES3.Save("LCLiveStatsTracker_AvgScrapDayCount", LiveStats.AverageScrapDayCount, persistenceSaveFileName); } } if (!((Object)(object)instance != (Object)null) || instance.gameStats == null || instance.allPlayerScripts == null) { return; } string moonName = (((Object)(object)val != (Object)null) ? val.PlanetName : null); int currentQuota = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0); PlayerStats[] allPlayerStats = instance.gameStats.allPlayerStats; for (int i = 0; i < instance.allPlayerScripts.Length && i < allPlayerStats.Length; i++) { PlayerControllerB val2 = instance.allPlayerScripts[i]; if (!((Object)(object)val2 == (Object)null) && (val2.isPlayerControlled || val2.isPlayerDead || val2.disconnectedMidGame)) { LiveStats.Players.TryGetValue(val2.playerClientId, out var value); int jumpsThisRound = value?.JumpsThisRound ?? 0; if (value != null) { value.JumpsThisRound = 0; } PlayerLifetimeStats.RecordDayEnd(val2.playerUsername, !val2.isPlayerDead, allPlayerStats[i].stepsTaken, allPlayerStats[i].profitable, jumpsThisRound, allPlayerStats[i].damageTaken, currentQuota, moonName, flag); } } } } [HarmonyPatch(typeof(HUDManager))] internal static class ControlTipSuppressPatch { private static bool ShouldSuppress() { if (Plugin.OverlayEnabled.Value) { return LiveStats.InSaveFile; } return false; } [HarmonyPatch("ChangeControlTip")] [HarmonyPrefix] private static bool ChangeControlTipPrefix() { return !ShouldSuppress(); } [HarmonyPatch("ChangeControlTipMultiple")] [HarmonyPrefix] private static bool ChangeControlTipMultiplePrefix() { return !ShouldSuppress(); } } [HarmonyPatch(typeof(GameNetworkManager), "ResetSavedGameValues")] internal static class ResetSavedGameValuesPatch { private static void Postfix(GameNetworkManager __instance) { LiveStats.AverageScrapSum = 0; LiveStats.AverageScrapDayCount = 0; LiveStats.CurrentRunMoonSeconds = 0.0; LiveStats.CurrentRunDowntimeSeconds = 0.0; ES3.Save("LCLiveStatsTracker_AvgScrapSum", 0, __instance.currentSaveFileName); ES3.Save("LCLiveStatsTracker_AvgScrapDayCount", 0, __instance.currentSaveFileName); ES3.Save("LCLiveStatsTracker_RunMoonSeconds", 0.0, __instance.currentSaveFileName); ES3.Save("LCLiveStatsTracker_RunDowntimeSeconds", 0.0, __instance.currentSaveFileName); LiveStats.ResetCrewDeaths(__instance.currentSaveFileName); } } [HarmonyPatch(typeof(PlayerControllerB), "KillPlayerClientRpc")] internal static class PlayerDeathPatch { private static void Postfix(int playerId, int causeOfDeath) { //IL_0042: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || playerId < 0 || playerId >= instance.allPlayerScripts.Length) { return; } PlayerControllerB player = instance.allPlayerScripts[playerId]; if (player.isPlayerDead) { string text = ((object)(CauseOfDeath)causeOfDeath/*cast due to .constrained prefix*/).ToString(); PlayerStats orAdd = LiveStats.Players.GetOrAdd(player.playerClientId, (ulong _) => new PlayerStats { Name = player.playerUsername, DeathCount = LiveStats.GetPersistedDeathCount(player.playerUsername) }); if (orAdd.Alive) { orAdd.Alive = false; orAdd.CauseOfDeath = text; orAdd.DeathCount++; LiveStats.PersistCrewDeath(player.playerUsername, orAdd.DeathCount, StatsBroadcaster.GetPersistenceSaveFileName()); string moonName = (((Object)(object)instance.currentLevel != (Object)null) ? instance.currentLevel.PlanetName : null); EnemyAttackTracker.TryGetAttacker(playerId, out var enemyName); PlayerLifetimeStats.RecordDeath(player.playerUsername, text, moonName, enemyName); StatsBroadcaster.Refresh(); } } } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class TerminalMessageCommandPatch { private const string ClearCommand = "message clear"; private const string AddPrefix = "message add "; private const string MessagePrefix = "message "; private static bool Prefix(Terminal __instance, ref TerminalNode __result) { string text = __instance.screenText.text.Substring(__instance.screenText.text.Length - __instance.textAdded); text = text.Trim().TrimEnd('.', '!', '?'); if (string.Equals(text, "message clear", StringComparison.OrdinalIgnoreCase)) { LiveStats.OnScreenMessage = ""; __result = MakeNode("Message cleared."); return false; } if (text.Length > "message add ".Length && text.Substring(0, "message add ".Length).Equals("message add ", StringComparison.OrdinalIgnoreCase)) { string text2 = text.Substring("message add ".Length).Trim(); if (string.IsNullOrEmpty(text2)) { return true; } LiveStats.OnScreenMessage = (string.IsNullOrEmpty(LiveStats.OnScreenMessage) ? text2 : (LiveStats.OnScreenMessage + " " + text2)); __result = MakeNode("Message updated: \"" + LiveStats.OnScreenMessage + "\""); return false; } if (text.Length > "message ".Length && text.Substring(0, "message ".Length).Equals("message ", StringComparison.OrdinalIgnoreCase)) { string text3 = text.Substring("message ".Length).Trim(); if (string.IsNullOrEmpty(text3)) { return true; } LiveStats.OnScreenMessage = text3; __result = MakeNode("Message set: \"" + text3 + "\""); return false; } return true; } private static TerminalNode MakeNode(string text) { TerminalNode obj = ScriptableObject.CreateInstance(); obj.displayText = text + "\n"; obj.clearPreviousText = true; return obj; } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class TerminalSellCommandPatch { private static bool Prefix(Terminal __instance, ref TerminalNode __result) { string[] array = __instance.screenText.text.Substring(__instance.screenText.text.Length - __instance.textAdded).Trim().Split(new char[1] { ' ' }); if (array.Length != 2 || array[0].ToLowerInvariant() != "sell") { return true; } if (!int.TryParse(array[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return true; } TimeOfDay instance = TimeOfDay.Instance; if ((Object)(object)instance == (Object)null) { __result = MakeNode("No active quota to calculate against.\n"); return false; } int groupCredits = __instance.groupCredits; if (groupCredits >= result) { __result = MakeNode($"You already have {groupCredits} credits, which meets your {result} credit goal.\n"); return false; } int num = RequiredSaleAmount(groupCredits, instance.quotaFulfilled, instance.profitQuota, instance.daysUntilDeadline, result); __result = MakeNode($"Sell ${num} worth of scrap to reach {result} credits\n(including the overtime bonus for meeting quota).\n"); return false; } private static int RequiredSaleAmount(int currentCredits, int quotaFulfilled, int profitQuota, int daysUntilDeadline, int target) { int num = (5 * (target - currentCredits) - quotaFulfilled + profitQuota - 75 * daysUntilDeadline) / 6; int i; for (i = Mathf.Max(0, num - 2); currentCredits + i + OvertimeBonus(quotaFulfilled + i, profitQuota, daysUntilDeadline) < target; i++) { } return i; } private static int OvertimeBonus(int quotaFulfilledAfterSale, int profitQuota, int daysUntilDeadline) { return (quotaFulfilledAfterSale - profitQuota) / 5 + 15 * daysUntilDeadline; } private static TerminalNode MakeNode(string text) { TerminalNode obj = ScriptableObject.CreateInstance(); obj.displayText = text; obj.clearPreviousText = true; return obj; } } [HarmonyPatch(typeof(Terminal), "ParsePlayerSentence")] internal static class TerminalStatsCommandPatch { private static bool Prefix(Terminal __instance, ref TerminalNode __result) { if (string.Equals(__instance.screenText.text.Substring(__instance.screenText.text.Length - __instance.textAdded).Trim().TrimEnd('.', '!', '?'), "stats", StringComparison.OrdinalIgnoreCase)) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); string text = (((Object)(object)val != (Object)null) ? val.playerUsername : null); __result = MakeNode((!string.IsNullOrEmpty(text)) ? (PlayerLifetimeStats.BuildProfileText(text) + "\n") : "Unable to determine your player name.\n"); return false; } return true; } private static TerminalNode MakeNode(string text) { TerminalNode obj = ScriptableObject.CreateInstance(); obj.displayText = text; obj.clearPreviousText = true; return obj; } } } namespace LiveStatsMod.Overlay { internal class OverlayDriver : MonoBehaviour { private static readonly Regex PhantomPlayerName = new Regex("^Player #\\d+$", RegexOptions.IgnoreCase); private TextMeshProUGUI _safetyLine; private void Awake() { Object.DontDestroyOnLoad((Object)(object)((Component)this).gameObject); } private void Update() { try { Tick(); } catch (Exception arg) { Plugin.Log.LogWarning((object)$"Overlay update failed: {arg}"); } } private void Tick() { HandleToggleKeybind(); if (!Plugin.OverlayEnabled.Value || !LiveStats.InSaveFile) { return; } HUDManager instance = HUDManager.Instance; if ((Object)(object)instance == (Object)null || instance.controlTipLines == null) { return; } PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if ((Object)(object)val != (Object)null && (Object)(object)val.quickMenuManager != (Object)null && val.quickMenuManager.isMenuOpen) { ClearLines(instance); return; } string text = "Moon: In Orbit"; if (LiveStats.IsLanded) { text = "Moon: " + (string.IsNullOrEmpty(LiveStats.MoonName) ? "-" : LiveStats.MoonName); string weatherGlyph = GetWeatherGlyph(GetLineFont(instance, 1), LiveStats.Weather); if (!string.IsNullOrEmpty(weatherGlyph)) { text = text + " " + weatherGlyph; } } string text2 = PartySizeLabel(); string text3 = (string.IsNullOrEmpty(text2) ? $"Day: {LiveStats.DayNumber}" : $"Day: {LiveStats.DayNumber} | {text2}"); SetLine(instance, 0, text3); SetLine(instance, 1, text); SetLine(instance, 2, $"Quota {LiveStats.QuotaNumber}: ${LiveStats.ProfitQuota}"); SetLine(instance, 3, $"Avg Loot/Day: ${LiveStats.AverageScrapPerDay()}"); UpdateSafetyLine(instance); } private void UpdateSafetyLine(HUDManager hud) { EnsureSafetyLine(hud); if (!((Object)(object)_safetyLine == (Object)null)) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); ShotgunItem val2 = (ShotgunItem)(((Object)(object)val != (Object)null) ? /*isinst with value type is only supported in some contexts*/: null); if ((Object)(object)val2 == (Object)null) { ((Component)_safetyLine).gameObject.SetActive(false); return; } ((Component)_safetyLine).gameObject.SetActive(true); ((TMP_Text)_safetyLine).enableWordWrapping = false; ((TMP_Text)_safetyLine).text = (val2.safetyOn ? "Turn safety off: [Q]" : "Turn safety on: [Q]"); } } private void EnsureSafetyLine(HUDManager hud) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: 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_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_safetyLine != (Object)null || hud.controlTipLines == null || hud.controlTipLines.Length < 4) { return; } TextMeshProUGUI val = hud.controlTipLines[3]; TextMeshProUGUI val2 = hud.controlTipLines[2]; if (!((Object)(object)val == (Object)null) && !((Object)(object)val2 == (Object)null)) { RectTransform component = ((Component)val).GetComponent(); RectTransform component2 = ((Component)val2).GetComponent(); if (!((Object)(object)component == (Object)null) && !((Object)(object)component2 == (Object)null)) { GameObject val3 = Object.Instantiate(((Component)val).gameObject, ((TMP_Text)val).transform.parent); ((Object)val3).name = "LiveStats_SafetyLine"; RectTransform component3 = val3.GetComponent(); Vector2 val4 = component.anchoredPosition - component2.anchoredPosition; component3.anchoredPosition = component.anchoredPosition + val4; _safetyLine = val3.GetComponent(); ((TMP_Text)_safetyLine).text = ""; val3.SetActive(false); } } } private void ClearLines(HUDManager hud) { for (int i = 0; i < hud.controlTipLines.Length; i++) { SetLine(hud, i, ""); } if ((Object)(object)_safetyLine != (Object)null) { ((Component)_safetyLine).gameObject.SetActive(false); } } private static void HandleToggleKeybind() { Keyboard current = Keyboard.current; if (current != null && ((ButtonControl)current.oKey).wasPressedThisFrame) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance != (Object)null) ? GameNetworkManager.Instance.localPlayerController : null); if (!((Object)(object)val == (Object)null) && !val.inTerminalMenu && !val.isTypingChat && (!((Object)(object)val.quickMenuManager != (Object)null) || !val.quickMenuManager.isMenuOpen)) { Plugin.OverlayEnabled.Value = !Plugin.OverlayEnabled.Value; } } } private static string PartySizeLabel() { return LiveStats.Players.Values.Count((PlayerStats p) => !p.Disconnected && !PhantomPlayerName.IsMatch((p.Name ?? "").Trim())) switch { 0 => "", 1 => "Solo", 2 => "Duos", 3 => "Trios", _ => "Squads", }; } private static TMP_FontAsset GetLineFont(HUDManager hud, int index) { if (index >= hud.controlTipLines.Length || (Object)(object)hud.controlTipLines[index] == (Object)null) { return null; } return ((TMP_Text)hud.controlTipLines[index]).font; } private static string GetWeatherGlyph(TMP_FontAsset font, string weather) { if ((Object)(object)font == (Object)null) { return ""; } char c; switch (weather) { case "None": c = '☀'; break; case "DustClouds": c = '☁'; break; case "Rainy": c = '☂'; break; case "Stormy": c = '⚡'; break; case "Foggy": c = '☁'; break; case "Flooded": c = '≈'; break; case "Eclipsed": c = '☾'; break; default: return ""; } if (!font.HasCharacter(c, true, true)) { return ""; } return c.ToString(); } private static void SetLine(HUDManager hud, int index, string text) { if (index < hud.controlTipLines.Length && !((Object)(object)hud.controlTipLines[index] == (Object)null)) { TextMeshProUGUI obj = hud.controlTipLines[index]; ((TMP_Text)obj).enableWordWrapping = false; ((TMP_Text)obj).text = text; } } } internal static class PauseMenuStatsPanel { private class StatsPanelRefresher : MonoBehaviour { private void Update() { RefreshStatsText(); } } private const string ButtonObjectName = "LiveStats_StatsButton"; private const string PanelObjectName = "LiveStats_StatsPanel"; private const string ScheduleText = "STREAM SCHEDULE\n\nMonday @ 5pm EST\nTuesday @ 5pm EST\nWednesday @ 5pm EST\nThursday @ 5pm EST\nFriday @ 5pm EST\nSaturday & Sunday - Event Streams\n"; private static GameObject _panel; private static TextMeshProUGUI _statsText; private static TextMeshProUGUI _messageTitleText; public static void Close() { if ((Object)(object)_panel != (Object)null) { _panel.SetActive(false); } } public static void Install(QuickMenuManager quickMenu) { if (!((Object)(object)quickMenu.mainButtonsPanel == (Object)null) && !((Object)(object)quickMenu.leaveGameConfirmPanel == (Object)null) && !((Object)(object)quickMenu.mainButtonsPanel.transform.Find("LiveStats_StatsButton") != (Object)null)) { BuildPanel(quickMenu); BuildButton(quickMenu); } } private static void BuildPanel(QuickMenuManager quickMenu) { //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) _panel = Object.Instantiate(quickMenu.leaveGameConfirmPanel, quickMenu.leaveGameConfirmPanel.transform.parent); ((Object)_panel).name = "LiveStats_StatsPanel"; _panel.SetActive(false); Button[] componentsInChildren = _panel.GetComponentsInChildren