Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of GsLethalStatsEmitter v0.5.0
GsLethalStatsEmitter.dll
Decompiled 2 months agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Net.Http; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Text; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Unity.Netcode; 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("GsLethalStatsEmitter")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.5.0.0")] [assembly: AssemblyInformationalVersion("0.5.0+9c3d0bf876106d2ebbcf034732150e2de0124ef2")] [assembly: AssemblyProduct("GsLethalStatsEmitter")] [assembly: AssemblyTitle("GsLethalStatsEmitter")] [assembly: AssemblyVersion("0.5.0.0")] [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 GsLethalStatsEmitter { [BepInPlugin("net.cproudlock.gslethalstatsemitter", "gs Lethal Company Stats", "0.5.0")] public class Plugin : BaseUnityPlugin { public const string GUID = "net.cproudlock.gslethalstatsemitter"; public const string NAME = "gs Lethal Company Stats"; public const string VERSION = "0.5.0"; internal const string PlaythroughKey = "gsPlaythroughId"; internal static ManualLogSource Log; internal static ConfigEntry<string> IngestUrl; internal static ConfigEntry<string> IngestToken; internal static ConfigEntry<bool> Enabled; internal static ConfigEntry<bool> EmitOnHostOnly; internal static ConfigEntry<int> EmitIntervalSeconds; internal static SessionState Session; private static readonly HttpClient http = new HttpClient { Timeout = TimeSpan.FromSeconds(15.0) }; private Harmony harmony; private float lastSnapshotAt; private static readonly Dictionary<ulong, string> holderByItem = new Dictionary<ulong, string>(); private static readonly Dictionary<ulong, KeyValuePair<string, DateTime>> lastGrabAt = new Dictionary<ulong, KeyValuePair<string, DateTime>>(); private static readonly HashSet<ulong> deliveredItems = new HashSet<ulong>(); private static readonly Dictionary<ulong, string> deliveredBy = new Dictionary<ulong, string>(); private static DateTime lastSaleAt = DateTime.MinValue; private static readonly Dictionary<string, DateTime> lastPurchaseAt = new Dictionary<string, DateTime>(); private void Awake() { //IL_00ba: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; IngestUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Ingest", "Url", "https://gs.proudtech.net/api/lethal/ingest", "POST endpoint that receives the session payload at game-over."); IngestToken = ((BaseUnityPlugin)this).Config.Bind<string>("Ingest", "Token", "", "Bearer token sent in the Authorization header. Set this in BepInEx/config/net.cproudlock.gslethalstatsemitter.cfg"); Enabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Ingest", "Enabled", true, "Master toggle. Set false to keep the mod loaded but skip ingest."); EmitOnHostOnly = ((BaseUnityPlugin)this).Config.Bind<bool>("Ingest", "EmitOnHostOnly", true, "Only POST when running as the lobby host (avoids duplicate sittings from co-op clients). Playthrough continuity only works from the host, since only the host owns the save file."); EmitIntervalSeconds = ((BaseUnityPlugin)this).Config.Bind<int>("Ingest", "EmitIntervalSeconds", 120, "Post a progress snapshot this often while playing, so a crash can't lose the whole sitting. The server upserts on sessionIdLocal, so snapshots replace each other. 0 disables snapshots (upload only when the sitting ends). Floored at 30s."); harmony = new Harmony("net.cproudlock.gslethalstatsemitter"); harmony.PatchAll(typeof(Plugin).Assembly); Log.LogInfo((object)("gs Lethal Company Stats v0.5.0 loaded · ingest=" + IngestUrl.Value)); } private void Update() { if (Session == null) { return; } int value = EmitIntervalSeconds.Value; if (value > 0) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (!(realtimeSinceStartup - lastSnapshotAt < (float)Math.Max(30, value))) { lastSnapshotAt = realtimeSinceStartup; Send(Session, "in_progress", blocking: false, snapshot: true); } } } internal static bool IsHost() { try { NetworkManager singleton = NetworkManager.Singleton; return (Object)(object)singleton != (Object)null && singleton.IsHost; } catch { return false; } } internal static SessionState EnsureSession(StartOfRound sor) { if (Session != null) { return Session; } string text = "host"; try { if ((Object)(object)sor != (Object)null && sor.allPlayerScripts != null) { PlayerControllerB[] allPlayerScripts = sor.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && val.isHostPlayerObject) { text = val.playerUsername; break; } } if (text == "host" && sor.allPlayerScripts.Length != 0 && (Object)(object)sor.allPlayerScripts[0] != (Object)null) { text = sor.allPlayerScripts[0].playerUsername ?? "host"; } } } catch { } ResetItemTracking(); string saveFileName; string text2 = ResolvePlaythroughId(out saveFileName); Session = new SessionState { sessionIdLocal = Guid.NewGuid().ToString(), playthroughIdLocal = text2, saveFileName = saveFileName, hostSteamId = HostSteamId(sor), startQuota = CurrentQuota(), hostName = text, seed = (((Object)(object)sor != (Object)null) ? sor.randomMapSeed : 0).ToString(CultureInfo.InvariantCulture), startedAtUtc = DateTime.UtcNow }; Log.LogInfo((object)("[gs] sitting start id=" + Session.sessionIdLocal + " host=" + text + " playthrough=" + (text2 ?? "(none)") + " save=" + (saveFileName ?? "(none)"))); return Session; } internal static string HostSteamId(StartOfRound sor) { try { StartOfRound val = sor ?? StartOfRound.Instance; if ((Object)(object)val == (Object)null || val.allPlayerScripts == null) { return null; } PlayerControllerB[] allPlayerScripts = val.allPlayerScripts; foreach (PlayerControllerB val2 in allPlayerScripts) { if ((Object)(object)val2 != (Object)null && val2.isHostPlayerObject && val2.playerSteamId != 0L) { return val2.playerSteamId.ToString(CultureInfo.InvariantCulture); } } } catch { } return null; } internal static string ResolvePlaythroughId(out string saveFileName) { saveFileName = null; try { if (!IsHost()) { return null; } GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(instance.currentSaveFileName)) { return null; } saveFileName = instance.currentSaveFileName; if (ES3.KeyExists("gsPlaythroughId", saveFileName)) { string text = ES3.Load<string>("gsPlaythroughId", saveFileName, string.Empty); if (!string.IsNullOrEmpty(text)) { return text; } } string text2 = Guid.NewGuid().ToString(); ES3.Save<string>("gsPlaythroughId", text2, saveFileName); Log.LogInfo((object)("[gs] new playthrough " + text2 + " stamped into " + saveFileName)); return text2; } catch (Exception ex) { Log.LogWarning((object)("[gs] playthrough id unavailable: " + ex.Message)); return null; } } internal static void ClearPlaythroughId(string saveFileName) { if (string.IsNullOrEmpty(saveFileName)) { return; } try { if (ES3.KeyExists("gsPlaythroughId", saveFileName)) { ES3.DeleteKey("gsPlaythroughId", saveFileName); } } catch (Exception ex) { Log.LogWarning((object)("[gs] could not clear playthrough key: " + ex.Message)); } } internal static int AbsoluteDayIndex(StartOfRound sor) { try { if (!IsHost()) { return -1; } StartOfRound val = sor ?? StartOfRound.Instance; if ((Object)(object)val == (Object)null) { return -1; } return val.gameStats.daysSpent + 1; } catch { return -1; } } internal static int CurrentQuota() { try { return ((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0; } catch { return 0; } } internal static void BeginDay(StartOfRound sor) { SessionState sessionState = EnsureSession(sor); if (sessionState == null) { return; } try { SelectableLevel val = (((Object)(object)sor != (Object)null) ? sor.currentLevel : null); string text = CleanMoonName(((Object)(object)val != (Object)null) ? val.PlanetName : null); int dayIdx = AbsoluteDayIndex(sor); if (dayIdx > 0) { sessionState.dayIndexAbsolute = true; } else { dayIdx = sessionState.days.Count + 1; } DayState dayState = sessionState.days.Find((DayState d) => d.dayIndex == dayIdx); if (dayState == null) { dayState = new DayState { dayIndex = dayIdx }; sessionState.days.Add(dayState); } dayState.moon = text; try { dayState.weather = (((Object)(object)val != (Object)null) ? ((object)Unsafe.As<LevelWeatherType, LevelWeatherType>(ref val.currentWeather)/*cast due to .constrained prefix*/).ToString() : null); } catch { } dayState.quotaBefore = CurrentQuota(); dayState.creditsBefore = CurrentCredits(); dayState.startedAtUtc = DateTime.UtcNow; dayState.endedAtUtc = null; Log.LogInfo((object)string.Format("[gs] day {0}{1} start moon={2}", dayIdx, sessionState.dayIndexAbsolute ? " (absolute)" : " (this sitting)", text)); } catch (Exception ex) { Log.LogWarning((object)("[gs] day start err: " + ex.Message)); } } internal static void ResetItemTracking() { holderByItem.Clear(); lastGrabAt.Clear(); deliveredItems.Clear(); deliveredBy.Clear(); lastPurchaseAt.Clear(); lastSaleAt = DateTime.MinValue; } internal static ulong ItemKey(GrabbableObject obj) { try { if ((Object)(object)((NetworkBehaviour)obj).NetworkObject != (Object)null) { return ((NetworkBehaviour)obj).NetworkObjectId; } } catch { } return (uint)((Object)obj).GetInstanceID(); } internal static void RecordGrab(PlayerControllerB player, GrabbableObject obj) { if (Session == null || (Object)(object)player == (Object)null || (Object)(object)obj == (Object)null) { return; } try { string playerUsername = player.playerUsername; if (string.IsNullOrEmpty(playerUsername)) { return; } ulong key = ItemKey(obj); DateTime utcNow = DateTime.UtcNow; if (lastGrabAt.TryGetValue(key, out var value) && value.Key == playerUsername && (utcNow - value.Value).TotalSeconds < 1.5) { return; } lastGrabAt[key] = new KeyValuePair<string, DateTime>(playerUsername, utcNow); if ((!obj.isInShipRoom && !obj.isInElevator) || !holderByItem.ContainsKey(key)) { holderByItem[key] = playerUsername; } PlayerSessionState playerSlot = GetPlayerSlot(playerUsername); if (playerSlot != null) { string text = SafeItemName(obj); int scrapValue = obj.scrapValue; if (!playerSlot.items.TryGetValue(text, out var value2)) { Dictionary<string, PlayerItemBag> items = playerSlot.items; PlayerItemBag obj2 = new PlayerItemBag { itemName = text }; value2 = obj2; items[text] = obj2; } value2.picks++; value2.totalValue += scrapValue; playerSlot.scrapValueCollected += scrapValue; DayState dayState = CurrentDay(); if (dayState != null) { dayState.scrapPiecesCollected++; } if ((Object)(object)obj.itemProperties != (Object)null && string.Equals(obj.itemProperties.itemName, "Apparatus", StringComparison.OrdinalIgnoreCase) && dayState != null) { dayState.apparatusPulled = true; } } } catch (Exception ex) { Log.LogWarning((object)("[gs] grab hook err: " + ex.Message)); } } internal static string SafeItemName(GrabbableObject g) { try { if ((Object)(object)g.itemProperties != (Object)null && !string.IsNullOrEmpty(g.itemProperties.itemName)) { return g.itemProperties.itemName.Replace(" ", ""); } } catch { } return ((object)g).GetType().Name; } private static bool IsAboard(GrabbableObject obj) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)obj == (Object)null) { return false; } if (obj.isInShipRoom || obj.isInElevator) { return true; } try { Collider val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.shipInnerRoomBounds : null); if ((Object)(object)val != (Object)null && (Object)(object)((Component)obj).transform != (Object)null) { Bounds bounds = val.bounds; return ((Bounds)(ref bounds)).Contains(((Component)obj).transform.position); } } catch { } return false; } internal static void RecordDelivery() { if (Session == null) { return; } try { if ((Object)(object)StartOfRound.Instance != (Object)null && StartOfRound.Instance.allPlayersDead) { Log.LogInfo((object)"[gs] crew wiped, no delivery credited"); return; } GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); if (array == null) { return; } List<GrabbableObject> list = new List<GrabbableObject>(); GrabbableObject[] array2 = array; foreach (GrabbableObject val in array2) { if ((Object)(object)val == (Object)null) { continue; } if (IsAboard(val)) { list.Add(val); } BeltBagItem val2 = (BeltBagItem)(object)((val is BeltBagItem) ? val : null); if (!((Object)(object)val2 != (Object)null) || val2.objectsInBag == null || !IsAboard(val)) { continue; } foreach (GrabbableObject item in val2.objectsInBag) { if ((Object)(object)item != (Object)null) { list.Add(item); } } } int num = 0; foreach (GrabbableObject item2 in list) { if (item2.scrapValue <= 0) { continue; } ulong num2 = ItemKey(item2); if (!deliveredItems.Add(num2) || !holderByItem.TryGetValue(num2, out var value) || string.IsNullOrEmpty(value)) { continue; } deliveredBy[num2] = value; PlayerSessionState playerSlot = GetPlayerSlot(value); if (playerSlot != null) { playerSlot.scrapDeliveredValue += item2.scrapValue; playerSlot.deliveredPending += item2.scrapValue; string text = SafeItemName(item2); if (!playerSlot.items.TryGetValue(text, out var value2)) { Dictionary<string, PlayerItemBag> items = playerSlot.items; PlayerItemBag obj = new PlayerItemBag { itemName = text }; value2 = obj; items[text] = obj; } value2.deliveredValue += item2.scrapValue; num++; } } if (num > 0) { Log.LogInfo((object)$"[gs] delivered {num} item(s) to ship"); } } catch (Exception ex) { Log.LogWarning((object)("[gs] delivery hook err: " + ex.Message)); } } internal static bool PurchaseAlreadyCounted(string signature) { DateTime utcNow = DateTime.UtcNow; if (lastPurchaseAt.TryGetValue(signature, out var value) && (utcNow - value).TotalSeconds < 1.0) { return true; } lastPurchaseAt[signature] = utcNow; return false; } internal static void RecordSale(DepositItemsDesk desk, int profit) { if (Session == null || profit <= 0) { return; } try { DateTime utcNow = DateTime.UtcNow; if ((utcNow - lastSaleAt).TotalSeconds < 3.0) { return; } lastSaleAt = utcNow; DayState dayState = CurrentDay(); int creditsAfter = CurrentCredits(); List<GrabbableObject> list = new List<GrabbableObject>(); try { if ((Object)(object)desk != (Object)null && (Object)(object)desk.deskObjectsContainer != (Object)null) { GrabbableObject[] componentsInChildren = ((Component)desk.deskObjectsContainer).GetComponentsInChildren<GrabbableObject>(); if (componentsInChildren != null) { GrabbableObject[] array = componentsInChildren; foreach (GrabbableObject val in array) { if ((Object)(object)val != (Object)null && val.scrapValue > 0) { list.Add(val); } } } } } catch { } int num = 0; foreach (GrabbableObject item in list) { num += item.scrapValue; } if (num > 0) { foreach (GrabbableObject item2 in list) { int num2 = (int)Math.Round((double)profit * (double)item2.scrapValue / (double)num); ulong num3 = ItemKey(item2); if (!deliveredBy.TryGetValue(num3, out var value) || string.IsNullOrEmpty(value)) { holderByItem.TryGetValue(num3, out value); } deliveredBy.Remove(num3); holderByItem.Remove(num3); deliveredItems.Remove(num3); lastGrabAt.Remove(num3); if (string.IsNullOrEmpty(value)) { continue; } PlayerSessionState playerSlot = GetPlayerSlot(value); if (playerSlot != null) { playerSlot.scrapSoldValue += num2; playerSlot.deliveredPending = Math.Max(0, playerSlot.deliveredPending - item2.scrapValue); string text = SafeItemName(item2); if (!playerSlot.items.TryGetValue(text, out var value2)) { Dictionary<string, PlayerItemBag> items = playerSlot.items; PlayerItemBag obj2 = new PlayerItemBag { itemName = text }; value2 = obj2; items[text] = obj2; } value2.soldValue += num2; } } } else { int num4 = 0; foreach (PlayerSessionState value3 in Session.players.Values) { num4 += value3.deliveredPending; } if (num4 > 0) { foreach (PlayerSessionState value4 in Session.players.Values) { if (value4.deliveredPending > 0) { int num5 = (int)Math.Round((double)profit * (double)value4.deliveredPending / (double)num4); value4.scrapSoldValue += num5; value4.deliveredPending = Math.Max(0, value4.deliveredPending - num5); } } } } if (dayState != null) { dayState.scrapValueCollected += profit; dayState.creditsAfter = creditsAfter; dayState.returnedToCompany = true; Log.LogInfo((object)$"[gs] sold on day {dayState.dayIndex} profit=${profit} items={list.Count}"); } } catch (Exception ex) { Log.LogWarning((object)("[gs] sell hook err: " + ex.Message)); } } internal static PlayerSessionState GetPlayerSlot(string name, bool isHost = false) { if (string.IsNullOrEmpty(name)) { name = "(unknown)"; } if (Session == null) { return null; } if (!Session.players.TryGetValue(name, out var value)) { value = new PlayerSessionState { playerName = name, isHost = isHost }; Session.players[name] = value; } else if (isHost) { value.isHost = true; } return value; } internal static DayState CurrentDay() { if (Session == null || Session.days.Count == 0) { return null; } return Session.days[Session.days.Count - 1]; } internal static string CurrentMoon() { try { return CleanMoonName((!((Object)(object)StartOfRound.Instance != (Object)null)) ? null : StartOfRound.Instance.currentLevel?.PlanetName); } catch { return null; } } internal static string CleanMoonName(string raw) { if (string.IsNullOrEmpty(raw)) { return raw; } int i; for (i = 0; i < raw.Length && raw[i] >= '0' && raw[i] <= '9'; i++) { } if (i == 0 || i >= raw.Length) { return raw; } for (; i < raw.Length && raw[i] == ' '; i++) { } return raw.Substring(i); } internal static int CurrentCredits() { try { Terminal val = Object.FindObjectOfType<Terminal>(); return ((Object)(object)val != (Object)null) ? val.groupCredits : 0; } catch { return 0; } } internal static void EmitAndReset(string outcome, bool blocking = false, bool playthroughEnded = false) { SessionState session = Session; if (session != null) { Session = null; session.playthroughEnded = playthroughEnded; Send(session, outcome, blocking, snapshot: false); if (playthroughEnded) { ClearPlaythroughId(session.saveFileName); } } } internal static void Send(SessionState s, string outcome, bool blocking, bool snapshot) { if (s == null) { return; } try { if (EmitOnHostOnly.Value && !IsHost()) { if (!snapshot) { Log.LogInfo((object)("[gs] not host, skip POST for sitting " + s.sessionIdLocal)); } return; } s.outcome = outcome; s.isSnapshot = snapshot; if (!snapshot) { s.endedAtUtc = DateTime.UtcNow; } ComputeAggregates(s); string json = SessionJson.Serialize(s); Log.LogInfo((object)string.Format("[gs] {0} sitting id={1} playthrough={2} outcome={3} days={4} players={5} deaths={6} bytes={7} blocking={8}", snapshot ? "snapshot" : "emit", s.sessionIdLocal, s.playthroughIdLocal ?? "(none)", outcome, s.daysSurvived, s.players.Count, s.deaths.Count, json.Length, blocking)); if (blocking) { PostBlocking(json); return; } Task.Run(() => PostJson(json)); } catch (Exception arg) { Log.LogError((object)$"[gs] emit error: {arg}"); } } private static void PostBlocking(string json) { if (!Enabled.Value) { Log.LogInfo((object)"[gs] disabled, skip POST"); return; } if (string.IsNullOrEmpty(IngestToken.Value)) { Log.LogWarning((object)"[gs] no token configured, skipping"); return; } try { using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(6.0)); HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, IngestUrl.Value) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + IngestToken.Value); HttpResponseMessage result = http.SendAsync(httpRequestMessage, cancellationTokenSource.Token).GetAwaiter().GetResult(); Log.LogInfo((object)$"[gs] POST(blocking) {(int)result.StatusCode}"); } catch (Exception ex) { Log.LogWarning((object)("[gs] POST(blocking) error: " + ex.Message)); } } private static void ComputeAggregates(SessionState s) { s.daysSurvived = s.days.Count; int num = 0; int num2 = 0; s.firstDayIndex = 0; s.lastDayIndex = 0; foreach (DayState day in s.days) { num += day.scrapValueCollected; if (day.scrapValueCollected > num2) { num2 = day.scrapValueCollected; } if (s.firstDayIndex == 0 || day.dayIndex < s.firstDayIndex) { s.firstDayIndex = day.dayIndex; } if (day.dayIndex > s.lastDayIndex) { s.lastDayIndex = day.dayIndex; } } s.totalScrapValue = num; s.peakScrapValue = num2; try { s.finalQuota = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0); } catch { } if (IsHost()) { s.quotasMetKnown = true; } if (s.quotasMetKnown) { try { s.quotasMet = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.timesFulfilledQuota : 0); } catch { } } s.finalCredits = CurrentCredits(); s.quotaMargin = s.finalCredits - s.finalQuota; foreach (PlayerSessionState value in s.players.Values) { value.metersTraveled = (int)Math.Round(value.metersTraveledFloat); if (!string.IsNullOrEmpty(s.hostName) && value.playerName == s.hostName) { value.isHost = true; } } } private static async Task PostJson(string json) { if (!Enabled.Value) { Log.LogInfo((object)"[gs] disabled, skip POST"); return; } if (string.IsNullOrEmpty(IngestToken.Value)) { Log.LogWarning((object)"[gs] no token configured, skipping"); return; } try { HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, IngestUrl.Value) { Content = new StringContent(json, Encoding.UTF8, "application/json") }; httpRequestMessage.Headers.TryAddWithoutValidation("Authorization", "Bearer " + IngestToken.Value); HttpResponseMessage res = await http.SendAsync(httpRequestMessage).ConfigureAwait(continueOnCapturedContext: false); string text = await res.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false); if (!res.IsSuccessStatusCode) { Log.LogWarning((object)$"[gs] POST {res.StatusCode}: {text}"); } else { Log.LogInfo((object)("[gs] POST ok: " + text)); } } catch (Exception ex) { Log.LogWarning((object)("[gs] POST error: " + ex.Message)); } } } internal class SessionState { public string sessionIdLocal; public string playthroughIdLocal; public string saveFileName; public string hostSteamId; public bool playthroughEnded; public bool isSnapshot; public int firstDayIndex; public int lastDayIndex; public bool dayIndexAbsolute; public int startQuota; public bool quotasMetKnown; public string hostName; public string seed; public DateTime startedAtUtc; public DateTime? endedAtUtc; public int daysSurvived; public int quotasMet; public int finalQuota; public int finalCredits; public int quotaMargin; public int peakScrapValue; public int totalScrapValue; public string outcome; public List<DayState> days = new List<DayState>(); public Dictionary<string, PlayerSessionState> players = new Dictionary<string, PlayerSessionState>(); public List<DeathEvent> deaths = new List<DeathEvent>(); public string lastTerminalUser; } internal class DayState { public int dayIndex; public string moon; public string weather; public int quotaBefore; public int creditsBefore; public int creditsAfter; public int scrapValueCollected; public int scrapPiecesCollected; public int scrapValueLeftBehind; public bool apparatusPulled; public int deaths; public bool returnedToCompany; public DateTime startedAtUtc; public DateTime? endedAtUtc; public int totalScrapValueOnMoon; } internal class PlayerSessionState { public string playerName; public ulong steamId; public bool isHost; public int deaths; public int scrapValueCollected; public int scrapDeliveredValue; public int scrapSoldValue; public int deliveredPending; public int daysSurvived; public int mostCreditsHeld; public int metersTraveled; public float metersTraveledFloat; public Dictionary<string, PlayerItemBag> items = new Dictionary<string, PlayerItemBag>(); public Dictionary<string, PurchaseBag> purchases = new Dictionary<string, PurchaseBag>(); public Dictionary<string, int> mobKills = new Dictionary<string, int>(); public Dictionary<string, float> distanceByWeather = new Dictionary<string, float>(); public string lastDamagedByEnemy; public DateTime lastDamagedAt; } internal class PurchaseBag { public string itemName; public int count; public int totalSpent; public bool isUnlockable; } internal class PlayerItemBag { public string itemName; public int picks; public int totalValue; public int deliveredValue; public int soldValue; } internal class DeathEvent { public int dayIndex; public string playerName; public string causeOfDeath; public string killer; public string moon; public float posX; public float posY; public float posZ; public DateTime tsUtc; } internal static class SessionJson { public static string Serialize(SessionState s) { StringBuilder stringBuilder = new StringBuilder(4096); stringBuilder.Append('{'); Kv(stringBuilder, "schemaVersion", 2); Comma(stringBuilder); Kv(stringBuilder, "sessionIdLocal", s.sessionIdLocal); Comma(stringBuilder); Kv(stringBuilder, "playthroughIdLocal", s.playthroughIdLocal); Comma(stringBuilder); Kv(stringBuilder, "saveFileName", s.saveFileName); Comma(stringBuilder); Kv(stringBuilder, "hostSteamId", s.hostSteamId); Comma(stringBuilder); Kv(stringBuilder, "playthroughEnded", s.playthroughEnded); Comma(stringBuilder); Kv(stringBuilder, "isSnapshot", s.isSnapshot); Comma(stringBuilder); Kv(stringBuilder, "firstDayIndex", s.firstDayIndex); Comma(stringBuilder); Kv(stringBuilder, "lastDayIndex", s.lastDayIndex); Comma(stringBuilder); Kv(stringBuilder, "dayIndexAbsolute", s.dayIndexAbsolute); Comma(stringBuilder); Kv(stringBuilder, "startQuota", s.startQuota); Comma(stringBuilder); Kv(stringBuilder, "quotasMetKnown", s.quotasMetKnown); Comma(stringBuilder); Kv(stringBuilder, "hostName", s.hostName); Comma(stringBuilder); Kv(stringBuilder, "seed", s.seed); Comma(stringBuilder); Kv(stringBuilder, "startedAtUtc", Iso(s.startedAtUtc)); Comma(stringBuilder); Kv(stringBuilder, "endedAtUtc", s.endedAtUtc.HasValue ? Iso(s.endedAtUtc.Value) : null); Comma(stringBuilder); Kv(stringBuilder, "daysSurvived", s.daysSurvived); Comma(stringBuilder); Kv(stringBuilder, "quotasMet", s.quotasMet); Comma(stringBuilder); Kv(stringBuilder, "finalQuota", s.finalQuota); Comma(stringBuilder); Kv(stringBuilder, "finalCredits", s.finalCredits); Comma(stringBuilder); Kv(stringBuilder, "peakScrapValue", s.peakScrapValue); Comma(stringBuilder); Kv(stringBuilder, "totalScrapValue", s.totalScrapValue); Comma(stringBuilder); Kv(stringBuilder, "quotaMargin", s.quotaMargin); Comma(stringBuilder); Kv(stringBuilder, "outcome", s.outcome); Comma(stringBuilder); stringBuilder.Append("\"days\":["); bool flag = true; foreach (DayState day in s.days) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('{'); Kv(stringBuilder, "dayIndex", day.dayIndex); Comma(stringBuilder); Kv(stringBuilder, "moon", day.moon); Comma(stringBuilder); Kv(stringBuilder, "weather", day.weather); Comma(stringBuilder); Kv(stringBuilder, "quotaBefore", day.quotaBefore); Comma(stringBuilder); Kv(stringBuilder, "creditsBefore", day.creditsBefore); Comma(stringBuilder); Kv(stringBuilder, "creditsAfter", day.creditsAfter); Comma(stringBuilder); Kv(stringBuilder, "scrapValueCollected", day.scrapValueCollected); Comma(stringBuilder); Kv(stringBuilder, "scrapPiecesCollected", day.scrapPiecesCollected); Comma(stringBuilder); Kv(stringBuilder, "scrapValueLeftBehind", day.scrapValueLeftBehind); Comma(stringBuilder); Kv(stringBuilder, "apparatusPulled", day.apparatusPulled); Comma(stringBuilder); Kv(stringBuilder, "deaths", day.deaths); Comma(stringBuilder); Kv(stringBuilder, "returnedToCompany", day.returnedToCompany); Comma(stringBuilder); Kv(stringBuilder, "startedAtUtc", Iso(day.startedAtUtc)); Comma(stringBuilder); Kv(stringBuilder, "endedAtUtc", day.endedAtUtc.HasValue ? Iso(day.endedAtUtc.Value) : null); stringBuilder.Append('}'); } stringBuilder.Append("],"); stringBuilder.Append("\"players\":["); flag = true; foreach (PlayerSessionState value2 in s.players.Values) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('{'); Kv(stringBuilder, "name", value2.playerName); Comma(stringBuilder); Kv(stringBuilder, "isHost", value2.isHost); Comma(stringBuilder); Kv(stringBuilder, "deaths", value2.deaths); Comma(stringBuilder); Kv(stringBuilder, "scrapValueCollected", value2.scrapValueCollected); Comma(stringBuilder); Kv(stringBuilder, "scrapDeliveredValue", value2.scrapDeliveredValue); Comma(stringBuilder); Kv(stringBuilder, "scrapSoldValue", value2.scrapSoldValue); Comma(stringBuilder); Kv(stringBuilder, "daysSurvived", value2.daysSurvived); Comma(stringBuilder); Kv(stringBuilder, "mostCreditsHeld", value2.mostCreditsHeld); Comma(stringBuilder); Kv(stringBuilder, "metersTraveled", value2.metersTraveled); Comma(stringBuilder); stringBuilder.Append("\"items\":["); bool flag2 = true; foreach (PlayerItemBag value3 in value2.items.Values) { if (!flag2) { stringBuilder.Append(','); } flag2 = false; stringBuilder.Append('{'); Kv(stringBuilder, "itemName", value3.itemName); Comma(stringBuilder); Kv(stringBuilder, "picks", value3.picks); Comma(stringBuilder); Kv(stringBuilder, "totalValue", value3.totalValue); Comma(stringBuilder); Kv(stringBuilder, "deliveredValue", value3.deliveredValue); Comma(stringBuilder); Kv(stringBuilder, "soldValue", value3.soldValue); stringBuilder.Append('}'); } stringBuilder.Append("],"); stringBuilder.Append("\"purchases\":["); bool flag3 = true; foreach (PurchaseBag value4 in value2.purchases.Values) { if (!flag3) { stringBuilder.Append(','); } flag3 = false; stringBuilder.Append('{'); Kv(stringBuilder, "itemName", value4.itemName); Comma(stringBuilder); Kv(stringBuilder, "count", value4.count); Comma(stringBuilder); Kv(stringBuilder, "totalSpent", value4.totalSpent); Comma(stringBuilder); Kv(stringBuilder, "isUnlockable", value4.isUnlockable); stringBuilder.Append('}'); } stringBuilder.Append("],"); stringBuilder.Append("\"mobKills\":["); bool flag4 = true; foreach (KeyValuePair<string, int> mobKill in value2.mobKills) { if (!flag4) { stringBuilder.Append(','); } flag4 = false; stringBuilder.Append('{'); Kv(stringBuilder, "enemyType", mobKill.Key); Comma(stringBuilder); Kv(stringBuilder, "kills", mobKill.Value); stringBuilder.Append('}'); } stringBuilder.Append("],"); stringBuilder.Append("\"distanceByWeather\":["); bool flag5 = true; foreach (KeyValuePair<string, float> item in value2.distanceByWeather) { if (!flag5) { stringBuilder.Append(','); } flag5 = false; stringBuilder.Append('{'); Kv(stringBuilder, "weather", item.Key); Comma(stringBuilder); Kv(stringBuilder, "meters", (int)Math.Round(item.Value)); stringBuilder.Append('}'); } stringBuilder.Append(']'); stringBuilder.Append('}'); } stringBuilder.Append("],"); stringBuilder.Append("\"mods\":["); flag = true; try { Dictionary<string, PluginInfo> pluginInfos = Chainloader.PluginInfos; if (pluginInfos != null) { foreach (KeyValuePair<string, PluginInfo> item2 in pluginInfos) { PluginInfo value = item2.Value; if (value == null || value.Metadata == null) { continue; } string text = ""; string v = ""; try { string directoryName = Path.GetDirectoryName(value.Location ?? ""); if (!string.IsNullOrEmpty(directoryName)) { text = Path.GetFileName(directoryName); int num = text.IndexOf('-'); if (num > 0) { v = text.Substring(0, num); } } } catch { } if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('{'); Kv(stringBuilder, "guid", value.Metadata.GUID ?? ""); Comma(stringBuilder); Kv(stringBuilder, "name", value.Metadata.Name ?? ""); Comma(stringBuilder); Kv(stringBuilder, "version", value.Metadata.Version?.ToString() ?? ""); Comma(stringBuilder); Kv(stringBuilder, "author", v); Comma(stringBuilder); Kv(stringBuilder, "folder", text); stringBuilder.Append('}'); } } } catch { } stringBuilder.Append("],"); stringBuilder.Append("\"deaths\":["); flag = true; foreach (DeathEvent death in s.deaths) { if (!flag) { stringBuilder.Append(','); } flag = false; stringBuilder.Append('{'); Kv(stringBuilder, "dayIndex", death.dayIndex); Comma(stringBuilder); Kv(stringBuilder, "playerName", death.playerName); Comma(stringBuilder); Kv(stringBuilder, "causeOfDeath", death.causeOfDeath); Comma(stringBuilder); Kv(stringBuilder, "killer", death.killer); Comma(stringBuilder); Kv(stringBuilder, "moon", death.moon); Comma(stringBuilder); Kv(stringBuilder, "posX", death.posX); Comma(stringBuilder); Kv(stringBuilder, "posY", death.posY); Comma(stringBuilder); Kv(stringBuilder, "posZ", death.posZ); Comma(stringBuilder); Kv(stringBuilder, "tsUtc", Iso(death.tsUtc)); stringBuilder.Append('}'); } stringBuilder.Append(']'); stringBuilder.Append('}'); return stringBuilder.ToString(); } private static void Kv(StringBuilder sb, string k, string v) { sb.Append('"').Append(k).Append("\":"); WriteString(sb, v); } private static void Kv(StringBuilder sb, string k, int v) { sb.Append('"').Append(k).Append("\":") .Append(v.ToString(CultureInfo.InvariantCulture)); } private static void Kv(StringBuilder sb, string k, float v) { sb.Append('"').Append(k).Append("\":") .Append(v.ToString("R", CultureInfo.InvariantCulture)); } private static void Kv(StringBuilder sb, string k, bool v) { sb.Append('"').Append(k).Append("\":") .Append(v ? "true" : "false"); } private static void Comma(StringBuilder sb) { sb.Append(','); } private static void WriteString(StringBuilder sb, string v) { if (v == null) { sb.Append("null"); return; } sb.Append('"'); foreach (char c in v) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { sb.AppendFormat(CultureInfo.InvariantCulture, "\\u{0:x4}", (int)c); } else { sb.Append(c); } } sb.Append('"'); } private static string Iso(DateTime dt) { return dt.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ", CultureInfo.InvariantCulture); } } [HarmonyPatch(typeof(TimeOfDay), "SyncNewProfitQuotaClientRpc")] internal static class P_TimeOfDay_SyncNewProfitQuota { private static void Postfix(int newProfitQuota, int overtimeBonus, int fulfilledQuota) { if (Plugin.Session != null) { Plugin.Session.quotasMetKnown = true; Plugin.Session.quotasMet = fulfilledQuota; } } } [HarmonyPatch(typeof(StartOfRound), "ArriveAtLevel")] internal static class P_StartOfRound_ArriveAtLevel { private static void Postfix(StartOfRound __instance) { Plugin.EnsureSession(__instance); } } [HarmonyPatch(typeof(StartOfRound), "openingDoorsSequence")] internal static class P_StartOfRound_OpeningDoorsSequence { private static void Postfix(StartOfRound __instance) { Plugin.BeginDay(__instance); } } [HarmonyPatch(typeof(StartOfRound), "ShipHasLeft")] internal static class P_StartOfRound_ShipHasLeft { private static void Postfix() { Plugin.RecordDelivery(); DayState dayState = Plugin.CurrentDay(); if (dayState != null) { dayState.endedAtUtc = DateTime.UtcNow; dayState.creditsAfter = Plugin.CurrentCredits(); if (dayState.totalScrapValueOnMoon > 0) { dayState.scrapValueLeftBehind = Math.Max(0, dayState.totalScrapValueOnMoon - dayState.scrapValueCollected); } Plugin.Log.LogInfo((object)$"[gs] day {dayState.dayIndex} end credits={dayState.creditsAfter} left=${dayState.scrapValueLeftBehind}"); } } } [HarmonyPatch] internal static class P_SellAndDisplayItemProfits { private static MethodBase TargetMethod() { string[] array = new string[3] { "DepositItemsDesk", "Terminal", "StartOfRound" }; for (int i = 0; i < array.Length; i++) { Type type = AccessTools.TypeByName(array[i]); if (!(type == null)) { MethodInfo methodInfo = AccessTools.Method(type, "SellAndDisplayItemProfits", (Type[])null, (Type[])null); if (methodInfo != null) { return methodInfo; } } } return null; } private static bool Prepare() { return TargetMethod() != null; } private static void Prefix(int profit) { Plugin.RecordSale(Object.FindObjectOfType<DepositItemsDesk>(), profit); } } [HarmonyPatch(typeof(GrabbableObject), "GrabItemOnClient")] internal static class P_GrabbableObject_GrabItemOnClient { private static void Postfix(GrabbableObject __instance) { if (Plugin.Session != null && !((Object)(object)__instance == (Object)null)) { Plugin.RecordGrab(__instance.playerHeldBy, __instance); } } } [HarmonyPatch(typeof(PlayerControllerB), "GrabObjectClientRpc")] internal static class P_PlayerControllerB_GrabObjectClientRpc { private static void Postfix(PlayerControllerB __instance, bool grabValidated, NetworkObjectReference grabbedObject) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null || !grabValidated) { return; } try { NetworkObject val = default(NetworkObject); if (((NetworkObjectReference)(ref grabbedObject)).TryGet(ref val, (NetworkManager)null) && !((Object)(object)val == (Object)null)) { GrabbableObject componentInChildren = ((Component)val).GetComponentInChildren<GrabbableObject>(); if (!((Object)(object)componentInChildren == (Object)null)) { Plugin.RecordGrab(__instance, componentInChildren); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[gs] grab rpc hook err: " + ex.Message)); } } } [HarmonyPatch(typeof(EnemyAI), "OnCollideWithPlayer")] internal static class P_EnemyAI_OnCollideWithPlayer { private static void Postfix(EnemyAI __instance, Collider other) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null || (Object)(object)other == (Object)null) { return; } try { PlayerControllerB component = ((Component)other).GetComponent<PlayerControllerB>(); if (!((Object)(object)component == (Object)null)) { PlayerSessionState playerSlot = Plugin.GetPlayerSlot(component.playerUsername); if (playerSlot != null) { playerSlot.lastDamagedByEnemy = ((object)__instance).GetType().Name; playerSlot.lastDamagedAt = DateTime.UtcNow; } } } catch { } } } [HarmonyPatch(typeof(PlayerControllerB), "KillPlayerClientRpc")] internal static class P_PlayerControllerB_KillPlayerClientRpc { private static readonly Dictionary<int, DateTime> lastDeathAt = new Dictionary<int, DateTime>(); private unsafe static void Postfix(int playerId, int causeOfDeath) { //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_00f2: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Expected I4, but got Unknown //IL_0169: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) //IL_01e6: Unknown result type (might be due to invalid IL or missing references) //IL_01f3: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Session == null) { return; } try { DateTime utcNow = DateTime.UtcNow; if (lastDeathAt.TryGetValue(playerId, out var value) && (utcNow - value).TotalSeconds < 1.0) { return; } lastDeathAt[playerId] = utcNow; StartOfRound instance = StartOfRound.Instance; PlayerControllerB val = (((Object)(object)instance != (Object)null && instance.allPlayerScripts != null && playerId >= 0 && playerId < instance.allPlayerScripts.Length) ? instance.allPlayerScripts[playerId] : null); string text = (((Object)(object)val != (Object)null) ? val.playerUsername : "(unknown)"); CauseOfDeath val2 = (CauseOfDeath)causeOfDeath; PlayerSessionState playerSlot = Plugin.GetPlayerSlot(text); if (playerSlot != null) { playerSlot.deaths++; } string text2; if (playerSlot != null && !string.IsNullOrEmpty(playerSlot.lastDamagedByEnemy) && (utcNow - playerSlot.lastDamagedAt).TotalSeconds < 10.0) { text2 = playerSlot.lastDamagedByEnemy; } else { switch (val2 - 2) { case 0: case 3: case 6: case 7: case 9: case 11: case 14: text2 = "environment"; break; case 5: text2 = "self"; break; default: text2 = "unknown"; break; } } Vector3 val3 = (((Object)(object)val != (Object)null && (Object)(object)((Component)val).transform != (Object)null) ? ((Component)val).transform.position : Vector3.zero); Plugin.Session.deaths.Add(new DeathEvent { dayIndex = (Plugin.CurrentDay()?.dayIndex ?? Math.Max(1, Plugin.Session.days.Count)), playerName = text, causeOfDeath = ((object)(*(CauseOfDeath*)(&val2))/*cast due to .constrained prefix*/).ToString(), killer = text2, moon = Plugin.CurrentMoon(), posX = val3.x, posY = val3.y, posZ = val3.z, tsUtc = utcNow }); DayState dayState = Plugin.CurrentDay(); if (dayState != null) { dayState.deaths++; } Plugin.Log.LogInfo((object)$"[gs] death {text} cause={val2} killer={text2}"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[gs] death hook err: " + ex.Message)); } } } [HarmonyPatch(typeof(PlayerControllerB), "Update")] internal static class P_PlayerControllerB_Update_Distance { private static readonly Dictionary<string, Vector3> lastPos = new Dictionary<string, Vector3>(); private const float TeleportThreshold = 25f; private static void Postfix(PlayerControllerB __instance) { //IL_010e: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) if (Plugin.Session == null || (Object)(object)__instance == (Object)null) { return; } try { if (!((Component)__instance).gameObject.activeInHierarchy || (Object)(object)((Component)__instance).transform == (Object)null) { return; } string playerUsername = __instance.playerUsername; if (string.IsNullOrEmpty(playerUsername)) { return; } Vector3 position = ((Component)__instance).transform.position; if (lastPos.TryGetValue(playerUsername, out var value)) { float num = Vector3.Distance(value, position); if (num > 0.01f && num < 25f) { PlayerSessionState playerSlot = Plugin.GetPlayerSlot(playerUsername); if (playerSlot != null) { playerSlot.metersTraveledFloat += num; string key = "None"; try { if ((Object)(object)TimeOfDay.Instance != (Object)null && (Object)(object)TimeOfDay.Instance.currentLevel != (Object)null) { key = ((object)Unsafe.As<LevelWeatherType, LevelWeatherType>(ref TimeOfDay.Instance.currentLevelWeather)/*cast due to .constrained prefix*/).ToString(); } } catch { } playerSlot.distanceByWeather.TryGetValue(key, out var value2); playerSlot.distanceByWeather[key] = value2 + num; } } } lastPos[playerUsername] = position; } catch { } } } [HarmonyPatch(typeof(StartOfRound), "FirePlayersAfterDeadlineClientRpc")] internal static class P_StartOfRound_FirePlayers { private static void Postfix() { Plugin.EmitAndReset("quota_failed", blocking: false, playthroughEnded: true); } } [HarmonyPatch(typeof(StartOfRound), "EndOfGameClientRpc")] internal static class P_StartOfRound_EndOfGame { private static void Postfix(StartOfRound __instance, int bodiesInsured, int daysPlayersSurvived, int connectedPlayersOnServer, int scrapCollectedOnServer) { if (Plugin.Session == null) { return; } try { if (connectedPlayersOnServer <= 0) { Plugin.EmitAndReset("all_dead"); } } catch { } } } [HarmonyPatch(typeof(StartOfRound), "ResetShip")] internal static class P_StartOfRound_ResetShip { private static void Postfix() { Plugin.EmitAndReset("abandoned"); } } [HarmonyPatch(typeof(GameNetworkManager), "Disconnect")] internal static class P_GameNetworkManager_Disconnect { private static void Prefix() { Plugin.EmitAndReset("disconnected", blocking: true); } } [HarmonyPatch(typeof(GameNetworkManager), "OnApplicationQuit")] internal static class P_GameNetworkManager_OnApplicationQuit { private static void Prefix() { Plugin.EmitAndReset("quit", blocking: true); } } [HarmonyPatch(typeof(Terminal), "BeginUsingTerminal")] internal static class P_Terminal_BeginUsingTerminal { private static void Postfix() { if (Plugin.Session == null) { return; } try { PlayerControllerB val = GameNetworkManager.Instance?.localPlayerController; if ((Object)(object)val != (Object)null) { Plugin.Session.lastTerminalUser = val.playerUsername; } } catch { } } } [HarmonyPatch(typeof(Terminal), "BuyItemsServerRpc")] internal static class P_Terminal_BuyItemsServerRpc { private static void Postfix(Terminal __instance, int[] boughtItems, int newGroupCredits, int numItemsInShip) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null || boughtItems == null) { return; } try { if (Plugin.PurchaseAlreadyCounted(string.Format("items:{0}:{1}:{2}", string.Join(",", boughtItems), newGroupCredits, numItemsInShip))) { return; } PlayerSessionState playerSlot = Plugin.GetPlayerSlot(Plugin.Session.lastTerminalUser ?? Plugin.Session.hostName ?? "(unknown)"); if (playerSlot == null) { return; } Item[] buyableItemsList = __instance.buyableItemsList; if (buyableItemsList == null) { return; } foreach (int num in boughtItems) { if (num < 0 || num >= buyableItemsList.Length) { continue; } Item val = buyableItemsList[num]; if (!((Object)(object)val == (Object)null)) { string text = (val.itemName ?? "Unknown").Replace(" ", ""); int creditsWorth = val.creditsWorth; if (!playerSlot.purchases.TryGetValue(text, out var value)) { Dictionary<string, PurchaseBag> purchases = playerSlot.purchases; PurchaseBag obj = new PurchaseBag { itemName = text, isUnlockable = false }; value = obj; purchases[text] = obj; } value.count++; value.totalSpent += creditsWorth; } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[gs] purchase hook err: " + ex.Message)); } } } [HarmonyPatch(typeof(StartOfRound), "BuyShipUnlockableServerRpc")] internal static class P_StartOfRound_BuyShipUnlockableServerRpc { private static void Postfix(StartOfRound __instance, int unlockableID, int newGroupCreditsAmount) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null) { return; } try { if (Plugin.PurchaseAlreadyCounted($"unlockable:{unlockableID}:{newGroupCreditsAmount}")) { return; } PlayerSessionState playerSlot = Plugin.GetPlayerSlot(Plugin.Session.lastTerminalUser ?? Plugin.Session.hostName ?? "(unknown)"); if (playerSlot == null) { return; } string text = "ShipUnlockable_" + unlockableID; try { UnlockablesList unlockablesList = __instance.unlockablesList; if ((Object)(object)unlockablesList != (Object)null && unlockablesList.unlockables != null && unlockableID >= 0 && unlockableID < unlockablesList.unlockables.Count) { UnlockableItem val = unlockablesList.unlockables[unlockableID]; if (val != null && !string.IsNullOrEmpty(val.unlockableName)) { text = val.unlockableName.Replace(" ", ""); } } } catch { } if (!playerSlot.purchases.TryGetValue(text, out var value)) { Dictionary<string, PurchaseBag> purchases = playerSlot.purchases; string key = text; PurchaseBag obj2 = new PurchaseBag { itemName = text, isUnlockable = true }; value = obj2; purchases[key] = obj2; } value.count++; value.isUnlockable = true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("[gs] unlockable hook err: " + ex.Message)); } } } [HarmonyPatch(typeof(EnemyAI), "HitEnemy")] internal static class P_EnemyAI_HitEnemy { internal static readonly Dictionary<int, string> LastHitBy = new Dictionary<int, string>(); private static void Postfix(EnemyAI __instance, int force, PlayerControllerB playerWhoHit, bool playHitSFX, int hitID) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null || (Object)(object)playerWhoHit == (Object)null) { return; } try { LastHitBy[((Object)__instance).GetInstanceID()] = playerWhoHit.playerUsername; } catch { } } } [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] internal static class P_EnemyAI_KillEnemy { private static void Postfix(EnemyAI __instance, bool destroy) { if (Plugin.Session == null || (Object)(object)__instance == (Object)null) { return; } try { int instanceID = ((Object)__instance).GetInstanceID(); if (P_EnemyAI_HitEnemy.LastHitBy.TryGetValue(instanceID, out var value)) { P_EnemyAI_HitEnemy.LastHitBy.Remove(instanceID); PlayerSessionState playerSlot = Plugin.GetPlayerSlot(value); if (playerSlot != null) { string name = ((object)__instance).GetType().Name; playerSlot.mobKills.TryGetValue(name, out var value2); playerSlot.mobKills[name] = value2 + 1; Plugin.Log.LogInfo((object)("[gs] mob kill " + value + " -> " + name)); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[gs] kill hook err: " + ex.Message)); } } } [HarmonyPatch(typeof(RoundManager), "SpawnScrapInLevel")] internal static class P_RoundManager_SpawnScrapInLevel { private static void Postfix(RoundManager __instance) { DayState dayState = Plugin.CurrentDay(); if (dayState == null || (Object)(object)__instance == (Object)null) { return; } try { dayState.totalScrapValueOnMoon = (int)__instance.totalScrapValueInLevel; } catch { } } } }