Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of ValheimHubCapture v0.1.2
ValheimHub.Capture.dll
Decompiled a month agousing 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 agousing 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(); } } }