Decompiled source of ValheimHubCapture v0.1.2

ValheimHub.Capture.dll

Decompiled a month ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using Microsoft.CodeAnalysis;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")]
[assembly: AssemblyCompany("ValheimHub.Capture")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("0.1.2.0")]
[assembly: AssemblyInformationalVersion("0.1.2+1c66d289b3d8ddaf6fa475158f50f3039b11389d")]
[assembly: AssemblyProduct("ValheimHub.Capture")]
[assembly: AssemblyTitle("ValheimHub.Capture")]
[assembly: AssemblyVersion("0.1.2.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 ValheimHub
{
	[BepInPlugin("app.valheimhub.capture", "Valheim Hub Capture", "0.1.2")]
	public sealed class HubPlugin : BaseUnityPlugin
	{
		internal static HubPlugin Instance;

		internal QueueEngine Engine = null;

		internal HubClient Client = null;

		private ConfigEntry<string> _key = null;

		private ConfigEntry<string> _baseUrl = null;

		private ConfigEntry<string> _status = null;

		private string _storedKey = "";

		private bool _flushing;

		private float _nextFlush;

		private float _nextBossPoll;

		private bool _bossSnapshot;

		private bool _bossPollLogged;

		private readonly HashSet<string> _seenBosses = new HashSet<string>();

		private object _tracked;

		private string _trackedId = "";

		private bool _wasDead;

		private float _nextPlaySave;

		private void Awake()
		{
			//IL_0085: Unknown result type (might be due to invalid IL or missing references)
			//IL_008b: Expected O, but got Unknown
			//IL_014c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0156: Expected O, but got Unknown
			//IL_0169: Unknown result type (might be due to invalid IL or missing references)
			//IL_0173: Expected O, but got Unknown
			Instance = this;
			_key = ((BaseUnityPlugin)this).Config.Bind<string>("Hub", "IngestKey", "", "Paste the key from https://admin.valheimhub.app.");
			_baseUrl = ((BaseUnityPlugin)this).Config.Bind<string>("Hub", "BaseUrl", "https://valheimhub.app", "Leave this.");
			_status = ((BaseUnityPlugin)this).Config.Bind<string>("Hub", "KeyStatus", "Not set", "Do not edit this.");
			StateStore val = new StateStore(Path.Combine(Paths.ConfigPath, "valheimhub.state.json"));
			HubState val2 = val.Load();
			string text = (_key.Value ?? "").Trim();
			if (text.Length > 0)
			{
				val2.IngestKey = text;
				val.Save(val2);
				_key.Value = "";
			}
			_storedKey = (val2.IngestKey ?? "").Trim();
			_status.Value = ((_storedKey.Length == 0) ? "Not set" : ("Saved, ends in " + _storedKey.Substring(Math.Max(0, _storedKey.Length - 4))));
			Engine = new QueueEngine(val, (Func<string, Task>)delegate(string message)
			{
				((BaseUnityPlugin)this).Logger.LogInfo((object)message);
				return Task.CompletedTask;
			});
			Client = new HubClient(_baseUrl.Value, _storedKey, (HttpClient)null);
			if (string.IsNullOrWhiteSpace(_storedKey))
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)"Ingest key is empty. Events will stay queued until it is set.");
			}
			_nextPlaySave = Time.unscaledTime + 30f;
			((BaseUnityPlugin)this).Logger.LogInfo((object)"Valheim Hub capture loaded (client only, no game patches).");
			((BaseUnityPlugin)this).Logger.LogInfo((object)("Lookups resolved: " + GameHooks.ResolvedNames()));
		}

		private void OnApplicationQuit()
		{
			try
			{
				Engine.CommitAllPlayed();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Play time save failed: " + ex.GetType().Name));
			}
		}

		private void Update()
		{
			try
			{
				WatchPlayer();
			}
			catch (Exception ex)
			{
				((BaseUnityPlugin)this).Logger.LogWarning((object)("Player watch failed: " + ex.GetType().Name));
			}
			try
			{
				PollBosses();
			}
			catch (Exception ex2)
			{
				if (!_bossPollLogged)
				{
					_bossPollLogged = true;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Boss poll failed: " + ex2.GetType().Name));
				}
			}
			if (_flushing || Time.unscaledTime < _nextFlush || string.IsNullOrWhiteSpace(_storedKey))
			{
				return;
			}
			_flushing = true;
			_nextFlush = Time.unscaledTime + 1.5f;
			QueueEngine engine = Engine;
			HubClient client = Client;
			Task.Run(async delegate
			{
				try
				{
					await engine.FlushOnce(client, DateTime.UtcNow);
				}
				catch (Exception ex3)
				{
					Exception ex4 = ex3;
					((BaseUnityPlugin)this).Logger.LogWarning((object)("Flush failed: " + ex4.GetType().Name));
				}
				finally
				{
					_flushing = false;
				}
			});
		}

		private void WatchPlayer()
		{
			object obj = GameHooks.LocalPlayer();
			if (obj == null)
			{
				if (_tracked != null)
				{
					Engine.CommitAllPlayed();
				}
				_tracked = null;
				_trackedId = "";
				_wasDead = false;
				_bossSnapshot = false;
				_seenBosses.Clear();
			}
			else if (obj != _tracked)
			{
				string text = GameHooks.CharacterId(obj);
				if (!string.IsNullOrWhiteSpace(text))
				{
					if (_tracked != null)
					{
						Engine.CommitAllPlayed();
					}
					_tracked = obj;
					_trackedId = text;
					_wasDead = GameHooks.IsDead(obj);
					_bossSnapshot = false;
					_seenBosses.Clear();
					_nextPlaySave = Time.unscaledTime + 30f;
					if (!_wasDead)
					{
						OnLocalSpawn();
					}
				}
			}
			else
			{
				bool flag = GameHooks.IsDead(obj);
				if (!_wasDead)
				{
					TickPlayed();
				}
				if (flag && !_wasDead)
				{
					OnLocalDeath(obj);
				}
				_wasDead = flag;
			}
		}

		private void TickPlayed()
		{
			if (!string.IsNullOrWhiteSpace(_trackedId))
			{
				Engine.AddPlayedTime(_trackedId, (double)Time.unscaledDeltaTime);
				if (!(Time.unscaledTime < _nextPlaySave))
				{
					_nextPlaySave = Time.unscaledTime + 30f;
					Engine.CommitPlayed(_trackedId);
				}
			}
		}

		internal void OnLocalSpawn()
		{
			string text = Clock.IsoZ(DateTime.UtcNow);
			string text2 = null;
			string text3 = null;
			try
			{
				text2 = GameHooks.WorldSeed();
			}
			catch (Exception)
			{
				text2 = null;
			}
			try
			{
				text3 = GameHooks.GameVersion();
			}
			catch (Exception)
			{
				text3 = null;
			}
			Engine.NoteRunStart(_trackedId, text, text2, text3, ((BaseUnityPlugin)this).Info.Metadata.Version.ToString());
			TrySnapshotBosses();
		}

		internal void OnLocalDeath(object player)
		{
			string text = Clock.IsoZ(DateTime.UtcNow);
			DeathFacts deathFacts = GameHooks.ReadDeath(player);
			Engine.NoteDeath(_trackedId, text, deathFacts.CauseRaw, GameHooks.GameVersion(), ((BaseUnityPlugin)this).Info.Metadata.Version.ToString(), deathFacts.CauseDisplay, deathFacts.CauseCategory, deathFacts.Biome);
		}

		private void TrySnapshotBosses()
		{
			try
			{
				bool unresolved;
				HashSet<string> hashSet = GameHooks.ReadBossKeys(out unresolved);
				if (unresolved || hashSet == null)
				{
					return;
				}
				_seenBosses.Clear();
				foreach (string item in hashSet)
				{
					_seenBosses.Add(item);
				}
				_bossSnapshot = true;
			}
			catch (Exception)
			{
			}
		}

		private void PollBosses()
		{
			if (_tracked == null || Time.unscaledTime < _nextBossPoll)
			{
				return;
			}
			_nextBossPoll = Time.unscaledTime + 3f;
			bool unresolved;
			HashSet<string> hashSet = GameHooks.ReadBossKeys(out unresolved);
			if (unresolved)
			{
				if (!_bossPollLogged)
				{
					_bossPollLogged = true;
					((BaseUnityPlugin)this).Logger.LogInfo((object)"ZoneSystem lookup missed. Boss kills are off.");
				}
			}
			else
			{
				if (hashSet == null)
				{
					return;
				}
				if (!_bossSnapshot)
				{
					foreach (string item in hashSet)
					{
						_seenBosses.Add(item);
					}
					_bossSnapshot = true;
				}
				else
				{
					if (string.IsNullOrWhiteSpace(_trackedId))
					{
						return;
					}
					string text = Clock.IsoZ(DateTime.UtcNow);
					string text2 = GameHooks.GameVersion();
					string text3 = ((BaseUnityPlugin)this).Info.Metadata.Version.ToString();
					string[] bossKeys = CaptureMaps.BossKeys;
					foreach (string text4 in bossKeys)
					{
						if (hashSet.Contains(text4) && _seenBosses.Add(text4))
						{
							string text5 = CaptureMaps.BossName(text4);
							if (text5 != null)
							{
								Engine.NoteBossKill(_trackedId, text, text5, text2, text3);
							}
						}
					}
				}
			}
		}
	}
	internal static class GameHooks
	{
		private static Type? _playerType;

		private static FieldInfo? _localPlayer;

		private static MethodInfo? _isDead;

		private static bool _lookedUp;

		public static object LocalPlayer()
		{
			EnsureLookups();
			return _localPlayer?.GetValue(null);
		}

		public static bool IsDead(object player)
		{
			EnsureLookups();
			if (_isDead == null)
			{
				return false;
			}
			try
			{
				object obj = _isDead.Invoke(player, ArgsFor(_isDead));
				bool flag = default(bool);
				int num;
				if (obj is bool)
				{
					flag = (bool)obj;
					num = 1;
				}
				else
				{
					num = 0;
				}
				return (byte)((uint)num & (flag ? 1u : 0u)) != 0;
			}
			catch (Exception)
			{
				return false;
			}
		}

		public static string CharacterId(object player)
		{
			try
			{
				MethodInfo methodInfo = FindMethod(player.GetType(), "GetPlayerName");
				if (methodInfo == null)
				{
					return "";
				}
				string text = methodInfo.Invoke(player, ArgsFor(methodInfo)) as string;
				return string.IsNullOrWhiteSpace(text) ? "" : text.Trim();
			}
			catch (Exception)
			{
				return "";
			}
		}

		public static DeathFacts ReadDeath(object player)
		{
			string text = null;
			string text2 = null;
			try
			{
				text = AttackerToken(player);
			}
			catch (Exception)
			{
				text = null;
			}
			try
			{
				text2 = HitTypeName(player);
			}
			catch (Exception)
			{
				text2 = null;
			}
			string text3 = null;
			try
			{
				text3 = CaptureMaps.Category(text2, text);
			}
			catch (Exception)
			{
				text3 = null;
			}
			string text4 = null;
			try
			{
				text4 = BiomeName(player);
			}
			catch (Exception)
			{
				text4 = null;
			}
			string causeRaw = "unknown";
			string causeDisplay = null;
			try
			{
				string text5 = ((!string.IsNullOrWhiteSpace(text)) ? text : text2);
				if (!string.IsNullOrWhiteSpace(text5) && text5 != "None" && text5 != "Undefined")
				{
					causeRaw = ReadableToken(text5);
					causeDisplay = DisplayCause(text5);
				}
			}
			catch (Exception)
			{
				causeRaw = "unknown";
				causeDisplay = null;
			}
			return new DeathFacts(causeRaw, causeDisplay, text3, text4);
		}

		public static string? WorldSeed()
		{
			try
			{
				Type type = FindType("ZNet");
				if (type == null)
				{
					return null;
				}
				BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
				object obj = type.GetProperty("instance", bindingAttr)?.GetValue(null) ?? type.GetField("instance", bindingAttr)?.GetValue(null);
				if (obj == null)
				{
					return null;
				}
				MethodInfo methodInfo = FindMethod(obj.GetType(), "GetWorld");
				if (methodInfo == null)
				{
					return null;
				}
				object obj2 = methodInfo.Invoke(obj, ArgsFor(methodInfo));
				if (obj2 == null)
				{
					return null;
				}
				string text = (FindField(obj2.GetType(), "m_seedName")?.GetValue(obj2) as string) ?? (obj2.GetType().GetProperty("m_seedName", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)?.GetValue(obj2) as string);
				if (string.IsNullOrWhiteSpace(text))
				{
					return null;
				}
				return text.Trim();
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static string? GameVersion()
		{
			try
			{
				Type type = FindType("Version");
				if (type == null)
				{
					return null;
				}
				BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
				object obj = type.GetProperty("CurrentVersion", bindingAttr)?.GetValue(null) ?? type.GetField("CurrentVersion", bindingAttr)?.GetValue(null);
				string text = obj?.ToString();
				if (string.IsNullOrWhiteSpace(text) || text == obj.GetType().Name || !ContainsDigit(text))
				{
					text = VersionString(type);
				}
				if (string.IsNullOrWhiteSpace(text))
				{
					return null;
				}
				return (text.Length <= 32) ? text : text.Substring(0, 32);
			}
			catch (Exception)
			{
				return null;
			}
		}

		public static HashSet<string>? ReadBossKeys(out bool unresolved)
		{
			unresolved = false;
			try
			{
				Type type = FindType("ZoneSystem");
				if (type == null)
				{
					unresolved = true;
					return null;
				}
				BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
				object obj = type.GetProperty("instance", bindingAttr)?.GetValue(null) ?? type.GetField("instance", bindingAttr)?.GetValue(null);
				if (obj == null)
				{
					return null;
				}
				HashSet<string> hashSet = new HashSet<string>();
				MethodInfo methodInfo = FindMethod(obj.GetType(), "GetGlobalKey", typeof(string));
				if (methodInfo != null)
				{
					string[] bossKeys = CaptureMaps.BossKeys;
					bool flag = default(bool);
					foreach (string text in bossKeys)
					{
						object obj2 = methodInfo.Invoke(obj, ArgsFor(methodInfo, text));
						int num;
						if (obj2 is bool)
						{
							flag = (bool)obj2;
							num = 1;
						}
						else
						{
							num = 0;
						}
						if (((uint)num & (flag ? 1u : 0u)) != 0)
						{
							hashSet.Add(text);
						}
					}
					return hashSet;
				}
				if (FindField(obj.GetType(), "m_globalKeys")?.GetValue(obj) is IEnumerable enumerable)
				{
					foreach (object item in enumerable)
					{
						if (item is string text2 && CaptureMaps.BossName(text2) != null)
						{
							hashSet.Add(text2);
						}
					}
					return hashSet;
				}
				unresolved = true;
				return null;
			}
			catch (Exception)
			{
				unresolved = true;
				return null;
			}
		}

		public static string ResolvedNames()
		{
			try
			{
				List<string> list = new List<string>();
				Type type = FindType("Player");
				if (type != null)
				{
					if (FindField(type, "m_localPlayer") != null)
					{
						list.Add("Player.m_localPlayer");
					}
					if (FindMethod(type, "IsDead") != null)
					{
						list.Add("Player.IsDead");
					}
					if (FindField(type, "m_lastHit") != null)
					{
						list.Add("Player.m_lastHit");
					}
					if (FindMethod(type, "GetCurrentBiome") != null)
					{
						list.Add("Player.GetCurrentBiome");
					}
					if (FindMethod(type, "GetPlayerName") != null)
					{
						list.Add("Player.GetPlayerName");
					}
				}
				Type type2 = FindType("Version");
				if (type2 != null)
				{
					BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
					if (type2.GetProperty("CurrentVersion", bindingAttr) != null || type2.GetField("CurrentVersion", bindingAttr) != null)
					{
						list.Add("Version.CurrentVersion");
					}
					if (FindMethod(type2, "GetVersionString") != null)
					{
						list.Add("Version.GetVersionString");
					}
				}
				Type type3 = FindType("ZoneSystem");
				if (type3 != null)
				{
					list.Add("ZoneSystem");
					if (FindMethod(type3, "GetGlobalKey") != null)
					{
						list.Add("ZoneSystem.GetGlobalKey");
					}
				}
				return (list.Count == 0) ? "none" : string.Join(", ", list);
			}
			catch (Exception)
			{
				return "none";
			}
		}

		private static string? AttackerToken(object player)
		{
			object obj = FindField(player.GetType(), "m_lastHit")?.GetValue(player);
			if (obj == null)
			{
				return null;
			}
			MethodInfo methodInfo = FindMethod(obj.GetType(), "GetAttacker");
			if (methodInfo == null)
			{
				return null;
			}
			object obj2 = methodInfo.Invoke(obj, ArgsFor(methodInfo));
			if (obj2 == null)
			{
				return null;
			}
			return FindField(obj2.GetType(), "m_name")?.GetValue(obj2) as string;
		}

		private static string? HitTypeName(object player)
		{
			object obj = FindField(player.GetType(), "m_lastHit")?.GetValue(player);
			if (obj == null)
			{
				return null;
			}
			return (FindField(obj.GetType(), "m_hitType")?.GetValue(obj))?.ToString();
		}

		private static string? BiomeName(object player)
		{
			MethodInfo methodInfo = FindMethod(player.GetType(), "GetCurrentBiome");
			if (methodInfo == null)
			{
				return null;
			}
			return CaptureMaps.Biome(methodInfo.Invoke(player, ArgsFor(methodInfo))?.ToString());
		}

		private static bool ContainsDigit(string text)
		{
			foreach (char c in text)
			{
				if (c >= '0' && c <= '9')
				{
					return true;
				}
			}
			return false;
		}

		private static string? VersionString(Type version)
		{
			MethodInfo methodInfo = FindMethod(version, "GetVersionString");
			if (methodInfo == null)
			{
				return null;
			}
			return methodInfo.Invoke(null, ArgsFor(methodInfo)) as string;
		}

		private static string DisplayCause(string raw)
		{
			string text = Localize(raw.Trim());
			if (string.IsNullOrWhiteSpace(text) || text.TrimStart(Array.Empty<char>()).StartsWith("$", StringComparison.Ordinal))
			{
				return ReadableToken(raw);
			}
			return text.Trim();
		}

		private static string Localize(string raw)
		{
			try
			{
				Type type = FindType("Localization");
				if (type == null)
				{
					return raw;
				}
				BindingFlags bindingAttr = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
				object obj = type.GetProperty("instance", bindingAttr)?.GetValue(null) ?? type.GetField("instance", bindingAttr)?.GetValue(null);
				if (obj == null)
				{
					return raw;
				}
				MethodInfo methodInfo = FindMethod(obj.GetType(), "Localize", typeof(string));
				if (methodInfo == null)
				{
					return raw;
				}
				string text = methodInfo.Invoke(obj, ArgsFor(methodInfo, raw)) as string;
				return string.IsNullOrWhiteSpace(text) ? raw : text;
			}
			catch (Exception)
			{
				return raw;
			}
		}

		private static string ReadableToken(string raw)
		{
			string text = raw.Trim();
			if (text.StartsWith("$enemy_", StringComparison.Ordinal))
			{
				text = text.Substring("$enemy_".Length);
			}
			else if (text.StartsWith("$", StringComparison.Ordinal))
			{
				text = text.Substring(1);
			}
			text = text.Replace('_', ' ').Trim();
			if (text.Length == 0)
			{
				return "unknown";
			}
			string[] array = text.Split(new char[1] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
			for (int i = 0; i < array.Length; i++)
			{
				string text2 = array[i];
				array[i] = ((text2.Length == 1) ? char.ToUpperInvariant(text2[0]).ToString() : (char.ToUpperInvariant(text2[0]) + text2.Substring(1)));
			}
			return string.Join(" ", array);
		}

		private static void EnsureLookups()
		{
			if (!_lookedUp)
			{
				_lookedUp = true;
				_playerType = FindType("Player");
				_localPlayer = ((_playerType == null) ? null : FindField(_playerType, "m_localPlayer"));
				_isDead = ((_playerType == null) ? null : FindMethod(_playerType, "IsDead"));
			}
		}

		private static Type? FindType(string name)
		{
			Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
			foreach (Assembly assembly in assemblies)
			{
				Type type = assembly.GetType(name);
				if (type != null)
				{
					return type;
				}
			}
			return null;
		}

		private static MethodInfo? FindMethod(Type type, string name, Type? requiredParameter = null)
		{
			MethodInfo methodInfo = null;
			Type type2 = type;
			while (type2 != null && type2 != typeof(object))
			{
				BindingFlags bindingAttr = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
				MethodInfo[] methods = type2.GetMethods(bindingAttr);
				foreach (MethodInfo methodInfo2 in methods)
				{
					if (!(methodInfo2.Name != name) && (!(requiredParameter != null) || HasParameter(methodInfo2, requiredParameter)) && (methodInfo == null || methodInfo2.GetParameters().Length < methodInfo.GetParameters().Length))
					{
						methodInfo = methodInfo2;
					}
				}
				type2 = type2.BaseType;
			}
			return methodInfo;
		}

		private static bool HasParameter(MethodInfo method, Type parameterType)
		{
			ParameterInfo[] parameters = method.GetParameters();
			foreach (ParameterInfo parameterInfo in parameters)
			{
				if (parameterInfo.ParameterType == parameterType)
				{
					return true;
				}
			}
			return false;
		}

		private static FieldInfo? FindField(Type type, string name)
		{
			BindingFlags bindingAttr = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
			Type type2 = type;
			while (type2 != null && type2 != typeof(object))
			{
				FieldInfo field = type2.GetField(name, bindingAttr);
				if (field != null)
				{
					return field;
				}
				type2 = type2.BaseType;
			}
			return null;
		}

		private static object[] ArgsFor(MethodInfo method, string? text = null)
		{
			ParameterInfo[] parameters = method.GetParameters();
			object[] array = new object[parameters.Length];
			bool flag = false;
			for (int i = 0; i < parameters.Length; i++)
			{
				ParameterInfo parameterInfo = parameters[i];
				if (!flag && text != null && parameterInfo.ParameterType == typeof(string))
				{
					array[i] = text;
					flag = true;
				}
				else if (parameterInfo.ParameterType == typeof(bool))
				{
					array[i] = false;
				}
				else if (parameterInfo.HasDefaultValue && parameterInfo.DefaultValue != DBNull.Value)
				{
					array[i] = parameterInfo.DefaultValue;
				}
				else
				{
					array[i] = (parameterInfo.ParameterType.IsValueType ? Activator.CreateInstance(parameterInfo.ParameterType) : null);
				}
			}
			return array;
		}
	}
	internal sealed class DeathFacts
	{
		public string CauseRaw { get; }

		public string? CauseDisplay { get; }

		public string? CauseCategory { get; }

		public string? Biome { get; }

		public DeathFacts(string causeRaw, string? causeDisplay, string? causeCategory, string? biome)
		{
			CauseRaw = causeRaw;
			CauseDisplay = causeDisplay;
			CauseCategory = causeCategory;
			Biome = biome;
		}
	}
}

ValheimHub.Core.dll

Decompiled a month ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")]
[assembly: AssemblyCompany("ValheimHub.Core")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+1c66d289b3d8ddaf6fa475158f50f3039b11389d")]
[assembly: AssemblyProduct("ValheimHub.Core")]
[assembly: AssemblyTitle("ValheimHub.Core")]
[assembly: AssemblyVersion("1.0.0.0")]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class IsReadOnlyAttribute : Attribute
	{
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[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 ValheimHub
{
	public static class CaptureMaps
	{
		public static readonly string[] BossKeys = new string[8] { "defeated_eikthyr", "defeated_gdking", "defeated_bonemass", "defeated_dragon", "defeated_goblinking", "defeated_queen", "defeated_fader", "defeated_frozenking" };

		public static string? BossName(string key)
		{
			return key switch
			{
				"defeated_eikthyr" => "Eikthyr", 
				"defeated_gdking" => "The Elder", 
				"defeated_bonemass" => "Bonemass", 
				"defeated_dragon" => "Moder", 
				"defeated_goblinking" => "Yagluth", 
				"defeated_queen" => "The Queen", 
				"defeated_fader" => "Fader", 
				"defeated_frozenking" => "Kall Fimbulbringer", 
				_ => null, 
			};
		}

		public static string? Biome(string? enumName)
		{
			return enumName switch
			{
				"Meadows" => "Meadows", 
				"Swamp" => "Swamp", 
				"Mountain" => "Mountains", 
				"BlackForest" => "Black Forest", 
				"Plains" => "Plains", 
				"AshLands" => "Ashlands", 
				"DeepNorth" => "Deep North", 
				"Ocean" => "Ocean", 
				"Mistlands" => "Mistlands", 
				_ => null, 
			};
		}

		public static string? Category(string? hitType, string? attackerToken)
		{
			switch (hitType)
			{
			case "EnemyHit":
			case "PlayerHit":
			case "Turret":
				return Family(attackerToken);
			case "Fall":
			case "Drowning":
			case "Burning":
			case "Freezing":
			case "Poisoned":
			case "Water":
			case "Smoke":
			case "EdgeOfWorld":
			case "Impact":
			case "Cart":
			case "Tree":
			case "Structural":
			case "Boat":
			case "Stalagtite":
			case "Catapult":
			case "CinderFire":
			case "AshlandsOcean":
				return "Environment";
			default:
				return null;
			}
		}

		public static string? Family(string? token)
		{
			if (token == null || token.Trim().Length == 0)
			{
				return null;
			}
			string text = token.Trim();
			if (text.StartsWith("$enemy_", StringComparison.Ordinal))
			{
				text = text.Substring("$enemy_".Length);
			}
			else if (text.StartsWith("$", StringComparison.Ordinal))
			{
				text = text.Substring(1);
			}
			if (text.Length == 0)
			{
				return null;
			}
			if (IsBoss(text))
			{
				return "Boss";
			}
			if (Starts(text, "draugr"))
			{
				return "Draugr";
			}
			if (Starts(text, "skeleton"))
			{
				return "Skeleton";
			}
			if (Starts(text, "greydwarf"))
			{
				return "Greydwarf";
			}
			if (Starts(text, "troll"))
			{
				return "Troll";
			}
			if (Starts(text, "goblin"))
			{
				return "Fuling";
			}
			if (Starts(text, "seeker"))
			{
				return "Seeker";
			}
			if (IsAnimal(text))
			{
				return "Animal";
			}
			return null;
		}

		private static bool IsBoss(string name)
		{
			return Starts(name, "eikthyr") || Starts(name, "gdking") || Starts(name, "bonemass") || Starts(name, "dragon") || Starts(name, "goblinking") || Starts(name, "seekerqueen") || Starts(name, "queen") || Starts(name, "fader") || Starts(name, "frozenking");
		}

		private static bool IsAnimal(string name)
		{
			return Starts(name, "boar") || Starts(name, "deer") || Starts(name, "wolf") || Starts(name, "neck") || Starts(name, "leech") || Starts(name, "lox") || Starts(name, "hare") || Starts(name, "bat") || Starts(name, "serpent");
		}

		private static bool Starts(string name, string prefix)
		{
			return name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
		}
	}
	public sealed class HubClient
	{
		private readonly HttpClient _http;

		private readonly string _baseUrl;

		private readonly string _ingestKey;

		public HubClient(string baseUrl, string ingestKey, HttpClient? http = null)
		{
			_baseUrl = baseUrl.TrimEnd(new char[1] { '/' });
			_ingestKey = ingestKey;
			_http = http ?? new HttpClient();
		}

		public async Task<string?> VerifySlug()
		{
			HttpReply reply = await Send(HttpMethod.Get, "/api/v1/ingest/verify", null, authorized: true);
			if (reply.NetworkError || reply.Status != 200)
			{
				return null;
			}
			return ReadString(reply.Body, "streamer_slug");
		}

		public async Task<HttpReply> PostRunStart(QueuedEvent item)
		{
			JsonWriter payload = new JsonWriter();
			payload.String("idempotency_id", item.Id);
			payload.String("started_at", item.StartedAt);
			payload.String("seed", item.Seed);
			payload.String("source", "mod");
			return await Send(HttpMethod.Post, "/api/v1/ingest/run_start", payload.Object(), authorized: true);
		}

		public async Task<HttpReply> PostDeath(QueuedEvent item)
		{
			JsonWriter payload = new JsonWriter();
			payload.String("idempotency_id", item.Id);
			payload.Number("run_id", item.RunId);
			payload.String("occurred_at", item.OccurredAt);
			payload.Number("duration_seconds", item.DurationSeconds);
			payload.Bool("duration_approx", item.DurationApprox);
			payload.String("cause_raw", item.CauseRaw);
			payload.String("cause_display", item.CauseDisplay);
			payload.String("cause_category", item.CauseCategory);
			payload.String("biome", item.Biome);
			payload.String("source", "mod");
			payload.String("game_version", item.GameVersion);
			payload.String("mod_version", item.ModVersion);
			return await Send(HttpMethod.Post, "/api/v1/ingest/death", payload.Object(), authorized: true);
		}

		public async Task<HttpReply> PostBossKill(QueuedEvent item)
		{
			JsonWriter payload = new JsonWriter();
			payload.String("idempotency_id", item.Id);
			payload.Number("run_id", item.RunId);
			payload.String("boss_name", item.BossName);
			payload.String("occurred_at", item.OccurredAt);
			payload.Number("offset_seconds", item.OffsetSeconds);
			payload.String("source", "mod");
			payload.String("game_version", item.GameVersion);
			payload.String("mod_version", item.ModVersion);
			return await Send(HttpMethod.Post, "/api/v1/ingest/boss_kill", payload.Object(), authorized: true);
		}

		public async Task<string?> FetchStartedAt(string slug, int runId)
		{
			HttpReply reply = await Send(HttpMethod.Get, "/api/v1/streamers/" + Uri.EscapeDataString(slug) + "/runs?outcome=active", null, authorized: false);
			if (reply.NetworkError || reply.Status != 200)
			{
				return null;
			}
			List<JsonValue> runs = JsonValue.Parse(reply.Body)?.Get("runs")?.Array;
			if (runs == null)
			{
				return null;
			}
			foreach (JsonValue run in runs)
			{
				if (run.Int("id") != runId)
				{
					continue;
				}
				return run.Str("started_at");
			}
			return null;
		}

		private async Task<HttpReply> Send(HttpMethod method, string path, string? payload, bool authorized)
		{
			try
			{
				using HttpRequestMessage request = new HttpRequestMessage(method, _baseUrl + path);
				if (authorized)
				{
					request.Headers.TryAddWithoutValidation("Authorization", "Bearer " + _ingestKey);
				}
				if (payload != null)
				{
					request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
				}
				using HttpResponseMessage response = await _http.SendAsync(request);
				string body = await response.Content.ReadAsStringAsync();
				return new HttpReply
				{
					Status = (int)response.StatusCode,
					Body = body
				};
			}
			catch (Exception ex) when (ex is HttpRequestException || ex is TaskCanceledException)
			{
				return new HttpReply
				{
					NetworkError = true,
					Body = ex.GetType().Name
				};
			}
		}

		public static int? ReadInt(string json, string name)
		{
			return JsonValue.Parse(json)?.Int(name);
		}

		public static string? ReadString(string json, string name)
		{
			return JsonValue.Parse(json)?.Str(name);
		}
	}
	public struct HttpReply
	{
		public int Status { get; set; }

		public string Body { get; set; }

		public bool NetworkError { get; set; }
	}
	internal sealed class JsonValue
	{
		private sealed class Parser
		{
			private readonly string _text;

			private int _index;

			public Parser(string text)
			{
				_text = text ?? "";
			}

			public JsonValue ReadValue()
			{
				JsonValue result = Read();
				Skip();
				return result;
			}

			private JsonValue Read()
			{
				Skip();
				if (_index >= _text.Length)
				{
					throw new FormatException();
				}
				switch (_text[_index])
				{
				case '{':
					return ReadObject();
				case '[':
					return ReadArray();
				case '"':
					return new JsonValue
					{
						String = ReadString()
					};
				case 't':
					Expect("true");
					return new JsonValue
					{
						Bool = true
					};
				case 'f':
					Expect("false");
					return new JsonValue
					{
						Bool = false
					};
				case 'n':
					Expect("null");
					return new JsonValue
					{
						IsNull = true
					};
				default:
					return ReadNumber();
				}
			}

			private JsonValue ReadObject()
			{
				Expect("{");
				Dictionary<string, JsonValue> dictionary = new Dictionary<string, JsonValue>();
				Skip();
				if (Take('}'))
				{
					return new JsonValue
					{
						Object = dictionary
					};
				}
				while (true)
				{
					Skip();
					string key = ReadString();
					Skip();
					Expect(":");
					dictionary[key] = Read();
					Skip();
					if (Take('}'))
					{
						break;
					}
					Expect(",");
				}
				return new JsonValue
				{
					Object = dictionary
				};
			}

			private JsonValue ReadArray()
			{
				Expect("[");
				List<JsonValue> list = new List<JsonValue>();
				Skip();
				if (Take(']'))
				{
					return new JsonValue
					{
						Array = list
					};
				}
				while (true)
				{
					list.Add(Read());
					Skip();
					if (Take(']'))
					{
						break;
					}
					Expect(",");
				}
				return new JsonValue
				{
					Array = list
				};
			}

			private JsonValue ReadNumber()
			{
				int index = _index;
				if (Take('-'))
				{
				}
				if (_index >= _text.Length || !char.IsDigit(_text[_index]))
				{
					throw new FormatException();
				}
				while (_index < _text.Length && char.IsDigit(_text[_index]))
				{
					_index++;
				}
				if (Take('.'))
				{
					while (_index < _text.Length && char.IsDigit(_text[_index]))
					{
						_index++;
					}
				}
				if (_index < _text.Length && (_text[_index] == 'e' || _text[_index] == 'E'))
				{
					_index++;
					if (_index < _text.Length && (_text[_index] == '+' || _text[_index] == '-'))
					{
						_index++;
					}
					while (_index < _text.Length && char.IsDigit(_text[_index]))
					{
						_index++;
					}
				}
				string s = _text.Substring(index, _index - index);
				if (!long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
				{
					return new JsonValue
					{
						Number = 0L
					};
				}
				return new JsonValue
				{
					Number = result
				};
			}

			private string ReadString()
			{
				Expect("\"");
				StringBuilder stringBuilder = new StringBuilder();
				while (_index < _text.Length)
				{
					char c = _text[_index++];
					switch (c)
					{
					case '"':
						return stringBuilder.ToString();
					default:
						stringBuilder.Append(c);
						break;
					case '\\':
					{
						if (_index >= _text.Length)
						{
							throw new FormatException();
						}
						char c2 = _text[_index++];
						if (c2 == 'u')
						{
							if (_index + 4 > _text.Length)
							{
								throw new FormatException();
							}
							string s = _text.Substring(_index, 4);
							_index += 4;
							stringBuilder.Append((char)int.Parse(s, NumberStyles.HexNumber, CultureInfo.InvariantCulture));
							break;
						}
						StringBuilder stringBuilder2 = stringBuilder;
						if (1 == 0)
						{
						}
						char value = c2 switch
						{
							'"' => '"', 
							'\\' => '\\', 
							'/' => '/', 
							'b' => '\b', 
							'f' => '\f', 
							'n' => '\n', 
							'r' => '\r', 
							't' => '\t', 
							_ => throw new FormatException(), 
						};
						if (1 == 0)
						{
						}
						stringBuilder2.Append(value);
						break;
					}
					}
				}
				throw new FormatException();
			}

			private void Skip()
			{
				while (_index < _text.Length && char.IsWhiteSpace(_text[_index]))
				{
					_index++;
				}
			}

			private void Expect(string token)
			{
				Skip();
				if (!_text.Substring(_index).StartsWith(token, StringComparison.Ordinal))
				{
					throw new FormatException();
				}
				_index += token.Length;
			}

			private bool Take(char token)
			{
				Skip();
				if (_index >= _text.Length || _text[_index] != token)
				{
					return false;
				}
				_index++;
				return true;
			}
		}

		public string? String { get; set; }

		public long? Number { get; set; }

		public bool? Bool { get; set; }

		public bool IsNull { get; set; }

		public Dictionary<string, JsonValue>? Object { get; set; }

		public List<JsonValue>? Array { get; set; }

		public static JsonValue? Parse(string json)
		{
			try
			{
				return new Parser(json).ReadValue();
			}
			catch (FormatException)
			{
				return null;
			}
		}

		public JsonValue? Get(string name)
		{
			if (Object != null && Object.TryGetValue(name, out JsonValue value))
			{
				return value;
			}
			return null;
		}

		public int? Int(string name)
		{
			JsonValue jsonValue = Get(name);
			if (jsonValue == null || !jsonValue.Number.HasValue)
			{
				return null;
			}
			return (int)jsonValue.Number.Value;
		}

		public string? Str(string name)
		{
			JsonValue jsonValue = Get(name);
			if (jsonValue == null || jsonValue.IsNull)
			{
				return null;
			}
			return jsonValue.String;
		}
	}
	internal sealed class JsonWriter
	{
		private readonly List<string> _fields = new List<string>();

		public void String(string name, string? value)
		{
			if (value != null)
			{
				Field(name, Quote(value));
			}
		}

		public void Number(string name, int? value)
		{
			if (value.HasValue)
			{
				Field(name, value.Value.ToString(CultureInfo.InvariantCulture));
			}
		}

		public void Bool(string name, bool value)
		{
			Field(name, value ? "true" : "false");
		}

		public void Raw(string name, string json)
		{
			Field(name, json);
		}

		public string Object()
		{
			return "{" + string.Join(",", _fields) + "}";
		}

		public static string Quote(string value)
		{
			StringBuilder stringBuilder = new StringBuilder(value.Length + 2);
			stringBuilder.Append('"');
			foreach (char c in value)
			{
				switch (c)
				{
				case '"':
					stringBuilder.Append("\\\"");
					continue;
				case '\\':
					stringBuilder.Append("\\\\");
					continue;
				case '\b':
					stringBuilder.Append("\\b");
					continue;
				case '\f':
					stringBuilder.Append("\\f");
					continue;
				case '\n':
					stringBuilder.Append("\\n");
					continue;
				case '\r':
					stringBuilder.Append("\\r");
					continue;
				case '\t':
					stringBuilder.Append("\\t");
					continue;
				}
				if (c < ' ')
				{
					StringBuilder stringBuilder2 = stringBuilder.Append("\\u");
					int num = c;
					stringBuilder2.Append(num.ToString("x4", CultureInfo.InvariantCulture));
				}
				else
				{
					stringBuilder.Append(c);
				}
			}
			stringBuilder.Append('"');
			return stringBuilder.ToString();
		}

		private void Field(string name, string jsonValue)
		{
			_fields.Add(Quote(name) + ":" + jsonValue);
		}
	}
	public sealed class HubState
	{
		public string? Slug { get; set; }

		public string? IngestKey { get; set; }

		public List<CharacterRun> Characters { get; set; } = new List<CharacterRun>();

		public List<QueuedEvent> Queue { get; set; } = new List<QueuedEvent>();
	}
	public sealed class CharacterRun
	{
		public string Id { get; set; } = "";

		public int RunId { get; set; }

		public string StartedAt { get; set; } = "";

		public bool DurationApprox { get; set; }

		public int? PlayedSeconds { get; set; }
	}
	public sealed class QueuedEvent
	{
		public string Id { get; set; } = "";

		public string Kind { get; set; } = "";

		public string CharacterId { get; set; } = "";

		public string OccurredAt { get; set; } = "";

		public string? StartedAt { get; set; }

		public int? DurationSeconds { get; set; }

		public bool DurationApprox { get; set; }

		public int? RunId { get; set; }

		public string? CauseRaw { get; set; }

		public string? CauseDisplay { get; set; }

		public string? CauseCategory { get; set; }

		public string? Biome { get; set; }

		public string? BossName { get; set; }

		public int? OffsetSeconds { get; set; }

		public string? Seed { get; set; }

		public string? GameVersion { get; set; }

		public string? ModVersion { get; set; }

		public bool StaleRetried { get; set; }

		public string Status { get; set; } = "pending";

		public string? ParkedBody { get; set; }

		public string? NextAttemptUtc { get; set; }

		public int Attempts { get; set; }
	}
	public enum SendKind
	{
		Success,
		Adopt,
		Retryable,
		Terminal,
		StaleRun
	}
	public sealed class SendResult
	{
		public SendKind Kind { get; set; }

		public int Status { get; set; }

		public string Body { get; set; } = "";

		public int? RunId { get; set; }
	}
	public static class Clock
	{
		public static string IsoZ(DateTime utc)
		{
			return utc.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'");
		}

		public static int WallSeconds(string startedAt, string occurredAt)
		{
			if (!DateTime.TryParse(startedAt, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result))
			{
				return 0;
			}
			if (!DateTime.TryParse(occurredAt, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result2))
			{
				return 0;
			}
			int num = (int)Math.Floor((result2 - result).TotalSeconds);
			return (num >= 0) ? num : 0;
		}
	}
	public static class OutcomeClassifier
	{
		public static SendResult Classify(QueuedEvent item, HttpReply reply)
		{
			if (reply.NetworkError || reply.Status == 0)
			{
				return new SendResult
				{
					Kind = SendKind.Retryable,
					Body = reply.Body
				};
			}
			if (reply.Status >= 200 && reply.Status < 300)
			{
				return new SendResult
				{
					Kind = SendKind.Success,
					Status = reply.Status,
					Body = reply.Body,
					RunId = HubClient.ReadInt(reply.Body, "run_id")
				};
			}
			if (reply.Status == 429 || reply.Status >= 500)
			{
				return new SendResult
				{
					Kind = SendKind.Retryable,
					Status = reply.Status,
					Body = reply.Body
				};
			}
			if (item.Kind == "boss_kill")
			{
				if (reply.Status == 400 || reply.Status == 401 || reply.Status == 404 || reply.Status == 409)
				{
					return new SendResult
					{
						Kind = SendKind.Terminal,
						Status = reply.Status,
						Body = reply.Body
					};
				}
				return new SendResult
				{
					Kind = SendKind.Retryable,
					Status = reply.Status,
					Body = reply.Body
				};
			}
			if (item.Kind == "run_start" && reply.Status == 409 && reply.Body.IndexOf("Active run already exists", StringComparison.Ordinal) >= 0)
			{
				return new SendResult
				{
					Kind = SendKind.Adopt,
					Status = reply.Status,
					Body = reply.Body,
					RunId = HubClient.ReadInt(reply.Body, "run_id")
				};
			}
			if (item.Kind == "death" && item.RunId.HasValue && !item.StaleRetried && (IsRunMissing(reply) || IsRunInactive(reply)))
			{
				return new SendResult
				{
					Kind = SendKind.StaleRun,
					Status = reply.Status,
					Body = reply.Body
				};
			}
			if (reply.Status == 400 || reply.Status == 401 || reply.Status == 404 || reply.Status == 409)
			{
				return new SendResult
				{
					Kind = SendKind.Terminal,
					Status = reply.Status,
					Body = reply.Body
				};
			}
			return new SendResult
			{
				Kind = SendKind.Retryable,
				Status = reply.Status,
				Body = reply.Body
			};
		}

		private static bool IsRunMissing(HttpReply reply)
		{
			return reply.Status == 404 && reply.Body.IndexOf("Run not found", StringComparison.Ordinal) >= 0;
		}

		private static bool IsRunInactive(HttpReply reply)
		{
			return reply.Status == 409 && reply.Body.IndexOf("Run is not active", StringComparison.Ordinal) >= 0;
		}
	}
	public sealed class QueueEngine
	{
		private const double MaxFrameSeconds = 5.0;

		private readonly object _gate = new object();

		private readonly StateStore _store;

		private readonly Func<string, Task> _log;

		private readonly Dictionary<string, double> _played = new Dictionary<string, double>();

		public HubState State { get; }

		public QueueEngine(StateStore store, Func<string, Task>? log = null)
		{
			_store = store;
			_log = log ?? ((Func<string, Task>)((string _) => Task.CompletedTask));
			State = store.Load();
			foreach (CharacterRun character in State.Characters)
			{
				if (character.PlayedSeconds.HasValue)
				{
					_played[character.Id] = character.PlayedSeconds.Value;
				}
			}
		}

		public void NoteRunStart(string characterId, string startedAt, string? seed, string? gameVersion, string modVersion)
		{
			if (string.IsNullOrWhiteSpace(characterId))
			{
				return;
			}
			lock (_gate)
			{
				if (!State.Characters.Any((CharacterRun c) => c.Id == characterId) && !State.Queue.Any((QueuedEvent q) => q.Status == "pending" && q.Kind == "run_start" && q.CharacterId == characterId))
				{
					State.Characters.Add(new CharacterRun
					{
						Id = characterId,
						RunId = 0,
						StartedAt = startedAt,
						PlayedSeconds = 0
					});
					_played[characterId] = 0.0;
					State.Queue.Add(new QueuedEvent
					{
						Id = Guid.NewGuid().ToString(),
						Kind = "run_start",
						CharacterId = characterId,
						OccurredAt = startedAt,
						StartedAt = startedAt,
						Seed = TrimOptional(seed, 64),
						GameVersion = gameVersion,
						ModVersion = modVersion
					});
					_store.Save(State);
				}
			}
		}

		public void NoteDeath(string characterId, string occurredAt, string causeRaw, string? gameVersion, string modVersion, string? causeDisplay = null, string? causeCategory = null, string? biome = null)
		{
			if (string.IsNullOrWhiteSpace(characterId))
			{
				return;
			}
			lock (_gate)
			{
				CharacterRun characterRun = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == characterId);
				FloorPlayed(characterId);
				int? durationSeconds = null;
				bool durationApprox = false;
				if (characterRun != null && characterRun.PlayedSeconds.HasValue)
				{
					durationSeconds = characterRun.PlayedSeconds;
				}
				else if (characterRun != null && !string.IsNullOrEmpty(characterRun.StartedAt))
				{
					durationSeconds = Clock.WallSeconds(characterRun.StartedAt, occurredAt);
					durationApprox = characterRun.DurationApprox;
				}
				State.Queue.Add(new QueuedEvent
				{
					Id = Guid.NewGuid().ToString(),
					Kind = "death",
					CharacterId = characterId,
					OccurredAt = occurredAt,
					DurationSeconds = durationSeconds,
					DurationApprox = durationApprox,
					RunId = ((characterRun != null && characterRun.RunId != 0) ? new int?(characterRun.RunId) : ((int?)null)),
					CauseRaw = Trim(causeRaw, 120),
					CauseDisplay = TrimOptional(causeDisplay, 120),
					CauseCategory = TrimOptional(causeCategory, 64),
					Biome = TrimOptional(biome, 64),
					GameVersion = gameVersion,
					ModVersion = modVersion
				});
				_store.Save(State);
			}
		}

		public void NoteBossKill(string characterId, string occurredAt, string bossName, string? gameVersion, string modVersion)
		{
			if (string.IsNullOrWhiteSpace(characterId))
			{
				return;
			}
			string name = TrimOptional(bossName, 64);
			if (name == null)
			{
				return;
			}
			lock (_gate)
			{
				if (!State.Queue.Any((QueuedEvent q) => q.Status == "pending" && q.Kind == "boss_kill" && q.CharacterId == characterId && q.BossName == name))
				{
					FloorPlayed(characterId);
					CharacterRun characterRun = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == characterId);
					State.Queue.Add(new QueuedEvent
					{
						Id = Guid.NewGuid().ToString(),
						Kind = "boss_kill",
						CharacterId = characterId,
						OccurredAt = occurredAt,
						BossName = name,
						OffsetSeconds = characterRun?.PlayedSeconds,
						GameVersion = gameVersion,
						ModVersion = modVersion
					});
					_store.Save(State);
				}
			}
		}

		public async Task FlushOnce(HubClient client, DateTime utcNow)
		{
			string bossHold = null;
			QueuedEvent head;
			lock (_gate)
			{
				head = State.Queue.FirstOrDefault((QueuedEvent q) => q.Status == "pending");
				if (head == null || (head.NextAttemptUtc != null && DateTime.TryParse(head.NextAttemptUtc, null, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var next) && next > utcNow))
				{
					return;
				}
				if (head.Kind == "boss_kill")
				{
					bossHold = PrepareBossKill(head);
				}
			}
			switch (bossHold)
			{
			case "skip-approx":
				await _log("boss_kill skipped, start time approximate");
				return;
			case "skip":
				await _log("boss_kill skipped, no run id");
				return;
			case "wait":
				return;
			}
			HttpReply httpReply;
			if (head.Kind == "run_start")
			{
				httpReply = await client.PostRunStart(head);
			}
			else
			{
				HttpReply httpReply2 = ((!(head.Kind == "boss_kill")) ? (await client.PostDeath(head)) : (await client.PostBossKill(head)));
				httpReply = httpReply2;
			}
			HttpReply reply = httpReply;
			SendResult result = OutcomeClassifier.Classify(head, reply);
			await Apply(client, head, result, utcNow);
			lock (_gate)
			{
				_store.Save(State);
			}
		}

		private async Task Apply(HubClient client, QueuedEvent item, SendResult result, DateTime utcNow)
		{
			switch (result.Kind)
			{
			case SendKind.Success:
				lock (_gate)
				{
					if (item.Kind == "run_start" && result.RunId.HasValue)
					{
						Remember(item.CharacterId, result.RunId.Value, item.StartedAt ?? item.OccurredAt, approx: false);
					}
					if (item.Kind == "death")
					{
						Forget(item.CharacterId);
					}
					State.Queue.Remove(item);
				}
				await _log(item.Kind + " accepted");
				break;
			case SendKind.Adopt:
				await Adopt(client, item, result, utcNow);
				break;
			case SendKind.StaleRun:
			{
				lock (_gate)
				{
					item.StaleRetried = true;
					item.RunId = null;
					Forget(item.CharacterId);
				}
				SendResult secondResult = OutcomeClassifier.Classify(item, await client.PostDeath(item));
				if (secondResult.Kind == SendKind.StaleRun)
				{
					secondResult.Kind = SendKind.Terminal;
				}
				await Apply(client, item, secondResult, utcNow);
				break;
			}
			case SendKind.Retryable:
				lock (_gate)
				{
					item.Attempts++;
					int delay = Math.Min(60, 2 << Math.Min(item.Attempts, 5));
					item.NextAttemptUtc = Clock.IsoZ(utcNow.AddSeconds(delay));
				}
				await _log(item.Kind + " retry " + result.Status);
				break;
			default:
				lock (_gate)
				{
					item.Status = "parked";
					item.ParkedBody = result.Body;
				}
				await _log(item.Kind + " parked " + result.Status);
				break;
			}
		}

		public void AddPlayedTime(string characterId, double deltaSeconds)
		{
			if (string.IsNullOrWhiteSpace(characterId) || deltaSeconds <= 0.0 || double.IsNaN(deltaSeconds) || double.IsInfinity(deltaSeconds))
			{
				return;
			}
			double num = Math.Min(deltaSeconds, 5.0);
			lock (_gate)
			{
				CharacterRun characterRun = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == characterId);
				if (characterRun != null && characterRun.PlayedSeconds.HasValue)
				{
					if (!_played.TryGetValue(characterId, out var value))
					{
						value = characterRun.PlayedSeconds.Value;
					}
					_played[characterId] = value + num;
				}
			}
		}

		public void CommitPlayed(string characterId)
		{
			if (string.IsNullOrWhiteSpace(characterId))
			{
				return;
			}
			lock (_gate)
			{
				FloorPlayed(characterId);
				_store.Save(State);
			}
		}

		public void CommitAllPlayed()
		{
			lock (_gate)
			{
				foreach (string item in _played.Keys.ToList())
				{
					FloorPlayed(item);
				}
				_store.Save(State);
			}
		}

		private string? PrepareBossKill(QueuedEvent item)
		{
			FloorPlayed(item.CharacterId);
			CharacterRun characterRun = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == item.CharacterId);
			if (characterRun != null && characterRun.RunId != 0 && characterRun.DurationApprox)
			{
				State.Queue.Remove(item);
				_store.Save(State);
				return "skip-approx";
			}
			if (characterRun != null && characterRun.RunId != 0)
			{
				item.RunId = characterRun.RunId;
				if (!item.OffsetSeconds.HasValue)
				{
					item.OffsetSeconds = characterRun.PlayedSeconds ?? Clock.WallSeconds(characterRun.StartedAt, item.OccurredAt);
				}
				return null;
			}
			QueuedEvent queuedEvent = State.Queue.FirstOrDefault((QueuedEvent q) => q.Status == "pending" && q.Kind == "run_start" && q.CharacterId == item.CharacterId);
			if (queuedEvent != null && queuedEvent != item)
			{
				State.Queue.Remove(item);
				int num = State.Queue.IndexOf(queuedEvent);
				State.Queue.Insert(num + 1, item);
				_store.Save(State);
				return "wait";
			}
			State.Queue.Remove(item);
			_store.Save(State);
			return "skip";
		}

		private async Task Adopt(HubClient client, QueuedEvent item, SendResult result, DateTime utcNow)
		{
			if (!result.RunId.HasValue)
			{
				lock (_gate)
				{
					item.Status = "parked";
					item.ParkedBody = result.Body;
				}
				await _log("run_start parked, adopt response had no run_id");
				return;
			}
			string startedAt = Clock.IsoZ(utcNow);
			bool approx = true;
			string slug = State.Slug;
			if (string.IsNullOrEmpty(slug))
			{
				slug = await client.VerifySlug();
			}
			State.Slug = slug;
			string knownSlug = default(string);
			int num;
			if (slug != null)
			{
				knownSlug = slug;
				num = ((knownSlug.Length > 0) ? 1 : 0);
			}
			else
			{
				num = 0;
			}
			if (num != 0)
			{
				string fetched = await client.FetchStartedAt(knownSlug, result.RunId.Value);
				string found = default(string);
				int num2;
				if (fetched != null)
				{
					found = fetched;
					num2 = 1;
				}
				else
				{
					num2 = 0;
				}
				if (num2 != 0)
				{
					startedAt = found;
					approx = false;
				}
			}
			lock (_gate)
			{
				CharacterRun run = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == item.CharacterId);
				if (run != null)
				{
					run.PlayedSeconds = null;
				}
				_played.Remove(item.CharacterId);
				Remember(item.CharacterId, result.RunId.Value, startedAt, approx);
				State.Queue.Remove(item);
			}
			await _log(approx ? "run adopted, start time approximate" : "run adopted");
		}

		private void Remember(string characterId, int runId, string startedAt, bool approx)
		{
			FloorPlayed(characterId);
			int? playedSeconds = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == characterId)?.PlayedSeconds;
			State.Characters.RemoveAll((CharacterRun c) => c.Id == characterId);
			State.Characters.Add(new CharacterRun
			{
				Id = characterId,
				RunId = runId,
				StartedAt = startedAt,
				DurationApprox = approx,
				PlayedSeconds = playedSeconds
			});
		}

		private void Forget(string characterId)
		{
			_played.Remove(characterId);
			State.Characters.RemoveAll((CharacterRun c) => c.Id == characterId);
		}

		private void FloorPlayed(string characterId)
		{
			CharacterRun characterRun = State.Characters.FirstOrDefault((CharacterRun c) => c.Id == characterId);
			double value;
			if (characterRun == null || !characterRun.PlayedSeconds.HasValue)
			{
				_played.Remove(characterId);
			}
			else if (_played.TryGetValue(characterId, out value))
			{
				double num = Math.Floor(value);
				if (num < 0.0)
				{
					num = 0.0;
				}
				if (num > 2147483647.0)
				{
					num = 2147483647.0;
				}
				characterRun.PlayedSeconds = (int)num;
			}
		}

		private static string? TrimOptional(string? value, int max)
		{
			if (string.IsNullOrWhiteSpace(value))
			{
				return null;
			}
			string text = value.Trim();
			return (text.Length <= max) ? text : text.Substring(0, max);
		}

		private static string Trim(string value, int max)
		{
			string text = (value ?? "").Trim();
			if (text.Length == 0)
			{
				return "unknown";
			}
			return (text.Length <= max) ? text : text.Substring(0, max);
		}
	}
	public sealed class StateStore
	{
		public string Path { get; }

		public StateStore(string path)
		{
			Path = path;
		}

		public HubState Load()
		{
			try
			{
				if (!File.Exists(Path))
				{
					return new HubState();
				}
				JsonValue jsonValue = JsonValue.Parse(File.ReadAllText(Path));
				if (jsonValue?.Object == null)
				{
					return new HubState();
				}
				return new HubState
				{
					Slug = jsonValue.Str("Slug"),
					IngestKey = jsonValue.Str("IngestKey"),
					Characters = ReadCharacters(jsonValue.Get("Characters")),
					Queue = ReadQueue(jsonValue.Get("Queue"))
				};
			}
			catch (Exception)
			{
				return new HubState();
			}
		}

		public void Save(HubState state)
		{
			string directoryName = System.IO.Path.GetDirectoryName(Path);
			if (!string.IsNullOrEmpty(directoryName))
			{
				Directory.CreateDirectory(directoryName);
			}
			string contents = WriteState(state);
			string text = Path + ".tmp";
			File.WriteAllText(text, contents);
			if (File.Exists(Path))
			{
				File.Replace(text, Path, null);
			}
			else
			{
				File.Move(text, Path);
			}
		}

		private static List<CharacterRun> ReadCharacters(JsonValue? value)
		{
			List<CharacterRun> list = new List<CharacterRun>();
			if (value?.Array == null)
			{
				return list;
			}
			foreach (JsonValue item in value.Array)
			{
				list.Add(new CharacterRun
				{
					Id = (item.Str("Id") ?? ""),
					RunId = item.Int("RunId").GetValueOrDefault(),
					StartedAt = (item.Str("StartedAt") ?? ""),
					DurationApprox = ((item.Get("DurationApprox")?.Bool == true) ?? false),
					PlayedSeconds = item.Int("PlayedSeconds")
				});
			}
			return list;
		}

		private static List<QueuedEvent> ReadQueue(JsonValue? value)
		{
			List<QueuedEvent> list = new List<QueuedEvent>();
			if (value?.Array == null)
			{
				return list;
			}
			foreach (JsonValue item in value.Array)
			{
				list.Add(new QueuedEvent
				{
					Id = (item.Str("Id") ?? ""),
					Kind = (item.Str("Kind") ?? ""),
					CharacterId = (item.Str("CharacterId") ?? ""),
					OccurredAt = (item.Str("OccurredAt") ?? ""),
					StartedAt = item.Str("StartedAt"),
					DurationSeconds = item.Int("DurationSeconds"),
					DurationApprox = ((item.Get("DurationApprox")?.Bool == true) ?? false),
					RunId = item.Int("RunId"),
					CauseRaw = item.Str("CauseRaw"),
					CauseDisplay = item.Str("CauseDisplay"),
					CauseCategory = item.Str("CauseCategory"),
					Biome = item.Str("Biome"),
					BossName = item.Str("BossName"),
					OffsetSeconds = item.Int("OffsetSeconds"),
					Seed = item.Str("Seed"),
					GameVersion = item.Str("GameVersion"),
					ModVersion = item.Str("ModVersion"),
					StaleRetried = ((item.Get("StaleRetried")?.Bool == true) ?? false),
					Status = (item.Str("Status") ?? "pending"),
					ParkedBody = item.Str("ParkedBody"),
					NextAttemptUtc = item.Str("NextAttemptUtc"),
					Attempts = item.Int("Attempts").GetValueOrDefault()
				});
			}
			return list;
		}

		private static string WriteState(HubState state)
		{
			List<string> list = new List<string>();
			foreach (CharacterRun character in state.Characters)
			{
				JsonWriter jsonWriter = new JsonWriter();
				jsonWriter.String("Id", character.Id);
				jsonWriter.Number("RunId", character.RunId);
				jsonWriter.String("StartedAt", character.StartedAt);
				jsonWriter.Bool("DurationApprox", character.DurationApprox);
				jsonWriter.Number("PlayedSeconds", character.PlayedSeconds);
				list.Add(jsonWriter.Object());
			}
			List<string> list2 = new List<string>();
			foreach (QueuedEvent item in state.Queue)
			{
				JsonWriter jsonWriter2 = new JsonWriter();
				jsonWriter2.String("Id", item.Id);
				jsonWriter2.String("Kind", item.Kind);
				jsonWriter2.String("CharacterId", item.CharacterId);
				jsonWriter2.String("OccurredAt", item.OccurredAt);
				jsonWriter2.String("StartedAt", item.StartedAt);
				jsonWriter2.Number("DurationSeconds", item.DurationSeconds);
				jsonWriter2.Bool("DurationApprox", item.DurationApprox);
				jsonWriter2.Number("RunId", item.RunId);
				jsonWriter2.String("CauseRaw", item.CauseRaw);
				jsonWriter2.String("CauseDisplay", item.CauseDisplay);
				jsonWriter2.String("CauseCategory", item.CauseCategory);
				jsonWriter2.String("Biome", item.Biome);
				jsonWriter2.String("BossName", item.BossName);
				jsonWriter2.Number("OffsetSeconds", item.OffsetSeconds);
				jsonWriter2.String("Seed", item.Seed);
				jsonWriter2.String("GameVersion", item.GameVersion);
				jsonWriter2.String("ModVersion", item.ModVersion);
				jsonWriter2.Bool("StaleRetried", item.StaleRetried);
				jsonWriter2.String("Status", item.Status);
				jsonWriter2.String("ParkedBody", item.ParkedBody);
				jsonWriter2.String("NextAttemptUtc", item.NextAttemptUtc);
				jsonWriter2.Number("Attempts", item.Attempts);
				list2.Add(jsonWriter2.Object());
			}
			JsonWriter jsonWriter3 = new JsonWriter();
			jsonWriter3.String("Slug", state.Slug);
			jsonWriter3.String("IngestKey", state.IngestKey);
			jsonWriter3.Raw("Characters", "[" + string.Join(",", list) + "]");
			jsonWriter3.Raw("Queue", "[" + string.Join(",", list2) + "]");
			return jsonWriter3.Object();
		}
	}
}