Decompiled source of GsLethalStatsEmitter v0.1.6

GsLethalStatsEmitter.dll

Decompiled 10 hours ago
using 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.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.1.6.0")]
[assembly: AssemblyInformationalVersion("0.1.6+fced9be4a94b811c8f1c3907a535efface3a854e")]
[assembly: AssemblyProduct("GsLethalStatsEmitter")]
[assembly: AssemblyTitle("GsLethalStatsEmitter")]
[assembly: AssemblyVersion("0.1.6.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	[AttributeUsage(AttributeTargets.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.1.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.1.0";

		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 SessionState Session;

		private static readonly HttpClient http = new HttpClient
		{
			Timeout = TimeSpan.FromSeconds(15.0)
		};

		private Harmony harmony;

		private void Awake()
		{
			//IL_0099: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a3: 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 sessions from co-op clients).");
			harmony = new Harmony("net.cproudlock.gslethalstatsemitter");
			harmony.PatchAll(typeof(Plugin).Assembly);
			Log.LogInfo((object)("gs Lethal Company Stats v0.1.0 loaded · ingest=" + IngestUrl.Value));
		}

		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
			{
			}
			Session = new SessionState
			{
				sessionIdLocal = Guid.NewGuid().ToString(),
				hostName = text,
				seed = (((Object)(object)sor != (Object)null) ? sor.randomMapSeed : 0).ToString(CultureInfo.InvariantCulture),
				startedAtUtc = DateTime.UtcNow
			};
			Log.LogInfo((object)("[gs] session start id=" + Session.sessionIdLocal + " host=" + text));
			return Session;
		}

		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)
		{
			SessionState session = Session;
			if (session == null)
			{
				return;
			}
			Session = null;
			try
			{
				if (EmitOnHostOnly.Value && !IsHost())
				{
					Log.LogInfo((object)("[gs] not host, skip POST for session " + session.sessionIdLocal));
					return;
				}
				session.outcome = outcome;
				session.endedAtUtc = DateTime.UtcNow;
				ComputeAggregates(session);
				string json = SessionJson.Serialize(session);
				Log.LogInfo((object)$"[gs] emit session id={session.sessionIdLocal} outcome={outcome} days={session.daysSurvived} players={session.players.Count} deaths={session.deaths.Count} bytes={json.Length}");
				Task.Run(() => PostJson(json));
			}
			catch (Exception arg)
			{
				Log.LogError((object)$"[gs] emit error: {arg}");
			}
		}

		private static void ComputeAggregates(SessionState s)
		{
			s.daysSurvived = s.days.Count;
			int num = 0;
			int num2 = 0;
			foreach (DayState day in s.days)
			{
				num += day.scrapValueCollected;
				if (day.scrapValueCollected > num2)
				{
					num2 = day.scrapValueCollected;
				}
			}
			s.totalScrapValue = num;
			s.peakScrapValue = num2;
			try
			{
				s.finalQuota = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0);
			}
			catch
			{
			}
			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);
			}
		}

		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 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 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 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, "sessionIdLocal", s.sessionIdLocal);
			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, "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, "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(StartOfRound), "ArriveAtLevel")]
	internal static class P_StartOfRound_ArriveAtLevel
	{
		private static void Postfix(StartOfRound __instance)
		{
			SessionState sessionState = Plugin.EnsureSession(__instance);
			string text = Plugin.CleanMoonName(((Object)(object)__instance.currentLevel != (Object)null) ? __instance.currentLevel.PlanetName : null);
			int num = sessionState.days.Count + 1;
			sessionState.days.Add(new DayState
			{
				dayIndex = num,
				moon = text,
				weather = SafeWeather(__instance.currentLevel),
				quotaBefore = (((Object)(object)TimeOfDay.Instance != (Object)null) ? TimeOfDay.Instance.profitQuota : 0),
				creditsBefore = Plugin.CurrentCredits(),
				startedAtUtc = DateTime.UtcNow
			});
			Plugin.Log.LogInfo((object)$"[gs] day {num} start moon={text}");
		}

		private static string SafeWeather(SelectableLevel lvl)
		{
			try
			{
				return ((Object)(object)lvl != (Object)null) ? ((object)(LevelWeatherType)(ref lvl.currentWeather)).ToString() : null;
			}
			catch
			{
				return null;
			}
		}
	}
	[HarmonyPatch(typeof(StartOfRound), "ShipHasLeft")]
	internal static class P_StartOfRound_ShipHasLeft
	{
		private static void Postfix()
		{
			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_DepositItemsDesk_SellItemsOnServer
	{
		private static MethodBase TargetMethod()
		{
			Type type = AccessTools.TypeByName("DepositItemsDesk");
			if (type == null)
			{
				return null;
			}
			return AccessTools.Method(type, "SellItemsOnServer", (Type[])null, (Type[])null);
		}

		private static void Postfix()
		{
			try
			{
				DayState dayState = Plugin.CurrentDay();
				if (dayState != null)
				{
					int num = Plugin.CurrentCredits();
					int num2 = num - dayState.creditsBefore;
					if (num2 > 0)
					{
						dayState.scrapValueCollected += num2;
					}
					dayState.creditsAfter = num;
					dayState.returnedToCompany = true;
					Plugin.Log.LogInfo((object)$"[gs] sold on day {dayState.dayIndex} delta=${num2}");
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[gs] sell hook err: " + ex.Message));
			}
		}
	}
	[HarmonyPatch(typeof(GrabbableObject), "GrabItemOnClient")]
	internal static class P_GrabbableObject_GrabItemOnClient
	{
		private static void Postfix(GrabbableObject __instance)
		{
			if (Plugin.Session == null)
			{
				return;
			}
			try
			{
				PlayerControllerB playerHeldBy = __instance.playerHeldBy;
				if ((Object)(object)playerHeldBy == (Object)null)
				{
					return;
				}
				PlayerSessionState playerSlot = Plugin.GetPlayerSlot(playerHeldBy.playerUsername);
				if (playerSlot != null)
				{
					string text = SafeItemName(__instance);
					int scrapValue = __instance.scrapValue;
					if (!playerSlot.items.TryGetValue(text, out var value))
					{
						Dictionary<string, PlayerItemBag> items = playerSlot.items;
						PlayerItemBag obj = new PlayerItemBag
						{
							itemName = text
						};
						value = obj;
						items[text] = obj;
					}
					value.picks++;
					value.totalValue += scrapValue;
					playerSlot.scrapValueCollected += scrapValue;
					DayState dayState = Plugin.CurrentDay();
					if (dayState != null)
					{
						dayState.scrapPiecesCollected++;
					}
				}
			}
			catch (Exception ex)
			{
				Plugin.Log.LogWarning((object)("[gs] grab hook err: " + ex.Message));
			}
		}

		private 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;
		}
	}
	[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), "KillPlayer")]
	internal static class P_PlayerControllerB_KillPlayer
	{
		private static void Postfix(PlayerControllerB __instance, Vector3 bodyVelocity, bool spawnBody, CauseOfDeath causeOfDeath, int deathAnimation, Vector3 positionOffset, bool setOverrideDropItems)
		{
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0075: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b7: Expected I4, but got Unknown
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_00dd: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ef: Unknown result type (might be due to invalid IL or missing references)
			//IL_0165: Unknown result type (might be due to invalid IL or missing references)
			//IL_0171: Unknown result type (might be due to invalid IL or missing references)
			//IL_017d: Unknown result type (might be due to invalid IL or missing references)
			//IL_01bf: Unknown result type (might be due to invalid IL or missing references)
			if (Plugin.Session == null || (Object)(object)__instance == (Object)null)
			{
				return;
			}
			try
			{
				PlayerSessionState playerSlot = Plugin.GetPlayerSlot(__instance.playerUsername);
				if (playerSlot != null)
				{
					playerSlot.deaths++;
				}
				string text = "environment";
				if (playerSlot != null && !string.IsNullOrEmpty(playerSlot.lastDamagedByEnemy) && (DateTime.UtcNow - playerSlot.lastDamagedAt).TotalSeconds < 10.0)
				{
					text = playerSlot.lastDamagedByEnemy;
				}
				else
				{
					switch (causeOfDeath - 2)
					{
					case 0:
					case 3:
					case 6:
					case 7:
					case 9:
					case 11:
					case 14:
						text = "environment";
						break;
					case 5:
						text = "self";
						break;
					default:
						text = "unknown";
						break;
					}
				}
				Vector3 val = (((Object)(object)((Component)__instance).transform != (Object)null) ? ((Component)__instance).transform.position : Vector3.zero);
				Plugin.Session.deaths.Add(new DeathEvent
				{
					dayIndex = (Plugin.CurrentDay()?.dayIndex ?? Math.Max(1, Plugin.Session.days.Count)),
					playerName = (__instance.playerUsername ?? "(unknown)"),
					causeOfDeath = ((object)(CauseOfDeath)(ref causeOfDeath)).ToString(),
					killer = text,
					moon = Plugin.CurrentMoon(),
					posX = val.x,
					posY = val.y,
					posZ = val.z,
					tsUtc = DateTime.UtcNow
				});
				DayState dayState = Plugin.CurrentDay();
				if (dayState != null)
				{
					dayState.deaths++;
				}
				Plugin.Log.LogInfo((object)$"[gs] death {__instance.playerUsername} cause={causeOfDeath} killer={text}");
			}
			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)(LevelWeatherType)(ref TimeOfDay.Instance.currentLevelWeather)).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");
		}
	}
	[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(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
			{
				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
			{
				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
			{
			}
		}
	}
	[HarmonyPatch(typeof(GrabbableObject), "GrabItemOnClient")]
	internal static class P_GrabbableObject_GrabItemOnClient_Apparatus
	{
		private static void Postfix(GrabbableObject __instance)
		{
			if (Plugin.Session == null || (Object)(object)__instance == (Object)null)
			{
				return;
			}
			try
			{
				if (!((Object)(object)__instance.itemProperties == (Object)null) && string.Equals(__instance.itemProperties.itemName, "Apparatus", StringComparison.OrdinalIgnoreCase))
				{
					DayState dayState = Plugin.CurrentDay();
					if (dayState != null)
					{
						dayState.apparatusPulled = true;
					}
				}
			}
			catch
			{
			}
		}
	}
}