using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using Microsoft.CodeAnalysis; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RegistreDuGungeonMod { public class DataStore { public const string ModVersion = "1.0.0"; public const int GUNS_TOTAL = 237; public const int ITEMS_TOTAL = 258; public const int BOSSES_TOTAL = 30; private readonly string _dir; private readonly Action _log; private readonly object _lock = new object(); private Timer _debounceTimer; public bool[] GunsFound = new bool[237]; public bool[] ItemsFound = new bool[258]; public bool[] BossesFound = new bool[30]; public List PastRuns = new List(); public RunRecord CurrentRun; private volatile bool _saving; public string LiveProgressPath => Path.Combine(_dir, "live_progress.json"); public string RunHistoryPath => Path.Combine(_dir, "run_history.json"); public DataStore(string dataDir, Action log) { _dir = dataDir; _log = log; try { Directory.CreateDirectory(_dir); } catch (Exception ex) { _log?.Invoke("[Registre] Impossible de creer le dossier de donnees : " + ex.Message); } LoadExisting(); } private static string NowIso() { return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } private void LoadExisting() { try { if (File.Exists(LiveProgressPath) && MiniJson.Decode(File.ReadAllText(LiveProgressPath)) is Dictionary decoded) { CopyBoolArray(decoded, "guns", GunsFound); CopyBoolArray(decoded, "items", ItemsFound); CopyBoolArray(decoded, "bosses", BossesFound); } } catch (Exception ex) { _log?.Invoke("[Registre] Lecture live_progress.json precedent impossible : " + ex.Message); } try { if (File.Exists(RunHistoryPath) && MiniJson.Decode(File.ReadAllText(RunHistoryPath)) is Dictionary dictionary && dictionary.TryGetValue("runs", out var value) && value is List list) { foreach (object item in list) { if (item is Dictionary d) { PastRuns.Add(RunRecord.FromDict(d)); } } } _log?.Invoke("[Registre] " + PastRuns.Count + " run(s) precedent(s) recharge(s)."); } catch (Exception ex2) { _log?.Invoke("[Registre] Lecture run_history.json precedent impossible : " + ex2.Message); } bool flag = false; foreach (RunRecord pastRun in PastRuns) { if (pastRun != null && pastRun.result == "in_progress") { pastRun.result = "abandoned"; if (string.IsNullOrEmpty(pastRun.endedAt)) { pastRun.endedAt = NowIso(); } flag = true; } } if (flag) { _log?.Invoke("[Registre] Run(s) laissee(s) \"en cours\" par une fermeture precedente : marquee(s) comme abandonnee(s)."); RequestSave(); } } private static void CopyBoolArray(Dictionary decoded, string key, bool[] target) { if (decoded.TryGetValue(key, out var value) && value is List list) { for (int i = 0; i < target.Length && i < list.Count; i++) { target[i] = ReflectionHelper.As(list[i], fallback: false); } } } public bool MarkEncountered(string cat, int idx) { bool[] array = cat switch { "bosses" => BossesFound, "items" => ItemsFound, "guns" => GunsFound, _ => null, }; if (array == null || idx < 0 || idx >= array.Length) { return false; } lock (_lock) { if (array[idx]) { return false; } array[idx] = true; } RequestSave(); return true; } public void StartRun(string characterKey, string seed) { lock (_lock) { CurrentRun = new RunRecord { id = Guid.NewGuid().ToString("N"), startedAt = NowIso(), characterKey = characterKey, seed = seed, floorsReached = 1, result = "in_progress" }; } RequestSave(); } public void EndRun(string result, string deathCause) { lock (_lock) { if (CurrentRun == null) { return; } CurrentRun.endedAt = NowIso(); CurrentRun.result = result; CurrentRun.deathCause = deathCause; try { if (DateTime.TryParse(CurrentRun.startedAt, null, DateTimeStyles.RoundtripKind, out var result2)) { CurrentRun.durationSeconds = (DateTime.UtcNow - result2).TotalSeconds; } } catch { } PastRuns.Add(CurrentRun); CurrentRun = null; } RequestSave(); } public void RequestSave() { lock (_lock) { if (_debounceTimer == null) { _debounceTimer = new Timer(delegate { SaveNow(); }, null, 500, -1); } else { _debounceTimer.Change(500, -1); } } } public void SaveNow() { if (_saving) { RequestSave(); return; } _saving = true; try { bool[] guns; bool[] items; bool[] bosses; List list; RunRecord currentRun; lock (_lock) { guns = (bool[])GunsFound.Clone(); items = (bool[])ItemsFound.Clone(); bosses = (bool[])BossesFound.Clone(); list = new List(PastRuns); currentRun = CurrentRun; } LiveProgress liveProgress = new LiveProgress { guns = guns, items = items, bosses = bosses, writtenAt = NowIso(), modVersion = "1.0.0" }; WriteAtomic(LiveProgressPath, MiniJson.Encode(liveProgress.ToDict())); List list2 = new List(); foreach (RunRecord item in list) { list2.Add(item.ToDict()); } if (currentRun != null) { list2.Add(currentRun.ToDict()); } Dictionary obj = new Dictionary { { "modVersion", "1.0.0" }, { "writtenAt", NowIso() }, { "runs", list2 } }; WriteAtomic(RunHistoryPath, MiniJson.Encode(obj)); } catch (Exception ex) { _log?.Invoke("[Registre] Erreur d'ecriture des fichiers de progression : " + ex.Message); } finally { _saving = false; } } private static void WriteAtomic(string path, string content) { string text = path + ".tmp"; File.WriteAllText(text, content); if (File.Exists(path)) { File.Replace(text, path, null); } else { File.Move(text, path); } } } [HarmonyPatch(typeof(EncounterTrackable), "HandleEncounter")] public static class EncounterPatcher { public const int NoQuality = -1000; public static Action OnGuidTracked; [HarmonyPostfix] public static void Postfix(EncounterTrackable __instance) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Expected I4, but got Unknown try { string text = (((Object)(object)__instance != (Object)null) ? __instance.EncounterGuid : null); if (!string.IsNullOrEmpty(text)) { int arg = -1000; PickupObject component = ((Component)__instance).GetComponent(); if ((Object)(object)component != (Object)null) { arg = (int)component.quality; } OnGuidTracked?.Invoke(text, arg); } } catch { } } } public struct GuidTarget { public string cat; public int idx; } public static class GuidMap { public static Dictionary Load(string path, Action log) { Dictionary dictionary = new Dictionary(); try { if (!File.Exists(path)) { log?.Invoke("[Registre] guid_map_data.json introuvable a cote du mod (" + path + ") - le suivi automatique des armes/objets/boss sera desactive."); return dictionary; } if (!(MiniJson.Decode(File.ReadAllText(path)) is Dictionary dictionary2)) { log?.Invoke("[Registre] guid_map_data.json n'a pas pu etre lu (format inattendu)."); return dictionary; } foreach (KeyValuePair item in dictionary2) { if (item.Value is Dictionary dictionary3 && dictionary3.TryGetValue("cat", out var value) && dictionary3.TryGetValue("idx", out var value2)) { dictionary[item.Key] = new GuidTarget { cat = (value as string), idx = ReflectionHelper.As(value2, -1) }; } } log?.Invoke("[Registre] guid_map_data.json charge : " + dictionary.Count + " entrees."); } catch (Exception ex) { log?.Invoke("[Registre] Erreur de lecture de guid_map_data.json : " + ex.Message); } return dictionary; } } public static class MiniJson { public static string Encode(object obj, bool pretty = true) { StringBuilder stringBuilder = new StringBuilder(); WriteValue(stringBuilder, obj, pretty, 0); return stringBuilder.ToString(); } private static void Indent(StringBuilder sb, int level) { sb.Append(' ', level * 2); } private static void WriteValue(StringBuilder sb, object value, bool pretty, int level) { if (value == null) { sb.Append("null"); } else if (value is bool flag) { sb.Append(flag ? "true" : "false"); } else if (value is string s) { WriteString(sb, s); } else if (value is int || value is long || value is short) { sb.Append(Convert.ToInt64(value).ToString(CultureInfo.InvariantCulture)); } else if (value is float || value is double || value is decimal) { double num = Convert.ToDouble(value, CultureInfo.InvariantCulture); if (double.IsNaN(num) || double.IsInfinity(num)) { sb.Append('0'); } else if (num == Math.Floor(num)) { sb.Append(((long)num).ToString(CultureInfo.InvariantCulture)); } else { sb.Append(num.ToString("R", CultureInfo.InvariantCulture)); } } else if (value is IDictionary dict) { WriteObject(sb, dict, pretty, level); } else if (value is IEnumerable arr) { WriteArray(sb, arr, pretty, level); } else { WriteString(sb, value.ToString()); } } private static void WriteObject(StringBuilder sb, IDictionary dict, bool pretty, int level) { if (dict.Count == 0) { sb.Append("{}"); return; } sb.Append('{'); if (pretty) { sb.Append('\n'); } int num = 0; foreach (DictionaryEntry item in dict) { if (pretty) { Indent(sb, level + 1); } WriteString(sb, Convert.ToString(item.Key, CultureInfo.InvariantCulture)); sb.Append(pretty ? ": " : ":"); WriteValue(sb, item.Value, pretty, level + 1); num++; if (num < dict.Count) { sb.Append(','); } if (pretty) { sb.Append('\n'); } } if (pretty) { Indent(sb, level); } sb.Append('}'); } private static void WriteArray(StringBuilder sb, IEnumerable arr, bool pretty, int level) { List list = new List(); foreach (object item in arr) { list.Add(item); } if (list.Count == 0) { sb.Append("[]"); return; } sb.Append('['); if (pretty) { sb.Append('\n'); } for (int i = 0; i < list.Count; i++) { if (pretty) { Indent(sb, level + 1); } WriteValue(sb, list[i], pretty, level + 1); if (i < list.Count - 1) { sb.Append(','); } if (pretty) { sb.Append('\n'); } } if (pretty) { Indent(sb, level); } sb.Append(']'); } private static void WriteString(StringBuilder sb, string s) { sb.Append('"'); foreach (char c in s) { switch (c) { case '"': sb.Append("\\\""); continue; case '\\': sb.Append("\\\\"); continue; case '\n': sb.Append("\\n"); continue; case '\r': sb.Append("\\r"); continue; case '\t': sb.Append("\\t"); continue; } if (c < ' ') { StringBuilder stringBuilder = sb.Append("\\u"); int num = c; stringBuilder.Append(num.ToString("x4", CultureInfo.InvariantCulture)); } else { sb.Append(c); } } sb.Append('"'); } public static object Decode(string json) { if (string.IsNullOrEmpty(json)) { return null; } int i = 0; return ParseValue(json, ref i); } private static void SkipWhitespace(string s, ref int i) { while (i < s.Length && char.IsWhiteSpace(s[i])) { i++; } } private static object ParseValue(string s, ref int i) { SkipWhitespace(s, ref i); if (i >= s.Length) { return null; } switch (s[i]) { case '{': return ParseObject(s, ref i); case '[': return ParseArray(s, ref i); case '"': return ParseString(s, ref i); case 't': i += 4; return true; case 'f': i += 5; return false; case 'n': i += 4; return null; default: return ParseNumber(s, ref i); } } private static Dictionary ParseObject(string s, ref int i) { Dictionary dictionary = new Dictionary(); i++; SkipWhitespace(s, ref i); if (i < s.Length && s[i] == '}') { i++; return dictionary; } while (i < s.Length) { SkipWhitespace(s, ref i); string key = ParseString(s, ref i); SkipWhitespace(s, ref i); i++; object value = ParseValue(s, ref i); dictionary[key] = value; SkipWhitespace(s, ref i); if (i < s.Length && s[i] == ',') { i++; continue; } if (i < s.Length && s[i] == '}') { i++; } break; } return dictionary; } private static List ParseArray(string s, ref int i) { List list = new List(); i++; SkipWhitespace(s, ref i); if (i < s.Length && s[i] == ']') { i++; return list; } while (i < s.Length) { object item = ParseValue(s, ref i); list.Add(item); SkipWhitespace(s, ref i); if (i < s.Length && s[i] == ',') { i++; continue; } if (i < s.Length && s[i] == ']') { i++; } break; } return list; } private static string ParseString(string s, ref int i) { StringBuilder stringBuilder = new StringBuilder(); i++; while (i < s.Length && s[i] != '"') { char c = s[i]; if (c == '\\' && i + 1 < s.Length) { i++; char c2 = s[i]; switch (c2) { case '"': stringBuilder.Append('"'); break; case '\\': stringBuilder.Append('\\'); break; case '/': stringBuilder.Append('/'); break; case 'n': stringBuilder.Append('\n'); break; case 'r': stringBuilder.Append('\r'); break; case 't': stringBuilder.Append('\t'); break; case 'b': stringBuilder.Append('\b'); break; case 'f': stringBuilder.Append('\f'); break; case 'u': { string value = s.Substring(i + 1, 4); stringBuilder.Append((char)Convert.ToInt32(value, 16)); i += 4; break; } default: stringBuilder.Append(c2); break; } i++; } else { stringBuilder.Append(c); i++; } } i++; return stringBuilder.ToString(); } private static object ParseNumber(string s, ref int i) { int num = i; while (i < s.Length && (char.IsDigit(s[i]) || s[i] == '-' || s[i] == '+' || s[i] == '.' || s[i] == 'e' || s[i] == 'E')) { i++; } string text = s.Substring(num, i - num); if (text.IndexOfAny(new char[3] { '.', 'e', 'E' }) >= 0) { return double.Parse(text, CultureInfo.InvariantCulture); } return long.Parse(text, CultureInfo.InvariantCulture); } } public static class RunResults { public const string InProgress = "in_progress"; public const string Victory = "victory"; public const string Death = "death"; public const string Abandoned = "abandoned"; public const string ReturnedToHub = "returned_to_hub"; } public class ItemPickupEntry { public string name; public string cat; public int idx = -1; public int floor; public string at; public int quality = -1000; public Dictionary ToDict() { return new Dictionary { { "name", name }, { "cat", cat }, { "idx", idx }, { "floor", floor }, { "at", at }, { "quality", quality } }; } public static ItemPickupEntry FromDict(Dictionary d) { ItemPickupEntry itemPickupEntry = new ItemPickupEntry(); if (d == null) { return itemPickupEntry; } itemPickupEntry.name = (d.TryGetValue("name", out var value) ? (value as string) : null); itemPickupEntry.cat = (d.TryGetValue("cat", out value) ? (value as string) : null); itemPickupEntry.idx = (d.TryGetValue("idx", out value) ? ReflectionHelper.As(value, -1) : (-1)); itemPickupEntry.floor = (d.TryGetValue("floor", out value) ? ReflectionHelper.As(value, 0) : 0); itemPickupEntry.at = (d.TryGetValue("at", out value) ? (value as string) : null); itemPickupEntry.quality = (d.TryGetValue("quality", out value) ? ReflectionHelper.As(value, -1000) : (-1000)); return itemPickupEntry; } } public class BossFightEntry { public string name; public int idx = -1; public string startedAt; public string endedAt; public double durationSeconds; public double damageDealt; public double damageTaken; public string result; public Dictionary ToDict() { return new Dictionary { { "name", name }, { "idx", idx }, { "startedAt", startedAt }, { "endedAt", endedAt }, { "durationSeconds", durationSeconds }, { "damageDealt", damageDealt }, { "damageTaken", damageTaken }, { "result", result } }; } public static BossFightEntry FromDict(Dictionary d) { BossFightEntry bossFightEntry = new BossFightEntry(); if (d == null) { return bossFightEntry; } bossFightEntry.name = (d.TryGetValue("name", out var value) ? (value as string) : null); bossFightEntry.idx = (d.TryGetValue("idx", out value) ? ReflectionHelper.As(value, -1) : (-1)); bossFightEntry.startedAt = (d.TryGetValue("startedAt", out value) ? (value as string) : null); bossFightEntry.endedAt = (d.TryGetValue("endedAt", out value) ? (value as string) : null); bossFightEntry.durationSeconds = (d.TryGetValue("durationSeconds", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); bossFightEntry.damageDealt = (d.TryGetValue("damageDealt", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); bossFightEntry.damageTaken = (d.TryGetValue("damageTaken", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); bossFightEntry.result = (d.TryGetValue("result", out value) ? (value as string) : null); return bossFightEntry; } } public class LiveStats { public float currentHealth = -1f; public float maxHealth; public float currentArmor; public int currency; public int keys; public int blanks; public int gunsHeld; public int itemsHeld; public double avgQuality = -1.0; public Dictionary ToDict() { return new Dictionary { { "currentHealth", currentHealth }, { "maxHealth", maxHealth }, { "currentArmor", currentArmor }, { "currency", currency }, { "keys", keys }, { "blanks", blanks }, { "gunsHeld", gunsHeld }, { "itemsHeld", itemsHeld }, { "avgQuality", avgQuality } }; } public static LiveStats FromDict(Dictionary d) { LiveStats liveStats = new LiveStats(); if (d == null) { return liveStats; } liveStats.currentHealth = (d.TryGetValue("currentHealth", out var value) ? ReflectionHelper.As(value, -1f) : (-1f)); liveStats.maxHealth = (d.TryGetValue("maxHealth", out value) ? ReflectionHelper.As(value, 0f) : 0f); liveStats.currentArmor = (d.TryGetValue("currentArmor", out value) ? ReflectionHelper.As(value, 0f) : 0f); liveStats.currency = (d.TryGetValue("currency", out value) ? ReflectionHelper.As(value, 0) : 0); liveStats.keys = (d.TryGetValue("keys", out value) ? ReflectionHelper.As(value, 0) : 0); liveStats.blanks = (d.TryGetValue("blanks", out value) ? ReflectionHelper.As(value, 0) : 0); liveStats.gunsHeld = (d.TryGetValue("gunsHeld", out value) ? ReflectionHelper.As(value, 0) : 0); liveStats.itemsHeld = (d.TryGetValue("itemsHeld", out value) ? ReflectionHelper.As(value, 0) : 0); liveStats.avgQuality = (d.TryGetValue("avgQuality", out value) ? ReflectionHelper.As(value, -1.0) : (-1.0)); return liveStats; } } public class RunRecord { public string id; public string startedAt; public string endedAt; public string characterKey; public string seed; public int floorsReached; public string result = "in_progress"; public string deathCause; public double durationSeconds; public int enemiesKilled; public double damageDealt; public double damageTaken; public List itemsPickedUp = new List(); public List bossFights = new List(); public LiveStats liveStats = new LiveStats(); public Dictionary ToDict() { List list = new List(); foreach (ItemPickupEntry item in itemsPickedUp) { list.Add(item.ToDict()); } List list2 = new List(); foreach (BossFightEntry bossFight in bossFights) { list2.Add(bossFight.ToDict()); } return new Dictionary { { "id", id }, { "startedAt", startedAt }, { "endedAt", endedAt }, { "characterKey", characterKey }, { "seed", seed }, { "floorsReached", floorsReached }, { "result", result }, { "deathCause", deathCause }, { "durationSeconds", durationSeconds }, { "enemiesKilled", enemiesKilled }, { "damageDealt", damageDealt }, { "damageTaken", damageTaken }, { "itemsPickedUp", list }, { "bossFights", list2 }, { "liveStats", liveStats.ToDict() } }; } public static RunRecord FromDict(Dictionary d) { RunRecord runRecord = new RunRecord(); if (d == null) { return runRecord; } runRecord.id = (d.TryGetValue("id", out var value) ? (value as string) : null); runRecord.startedAt = (d.TryGetValue("startedAt", out value) ? (value as string) : null); runRecord.endedAt = (d.TryGetValue("endedAt", out value) ? (value as string) : null); runRecord.characterKey = (d.TryGetValue("characterKey", out value) ? (value as string) : null); runRecord.seed = (d.TryGetValue("seed", out value) ? (value as string) : null); runRecord.floorsReached = (d.TryGetValue("floorsReached", out value) ? ReflectionHelper.As(value, 0) : 0); runRecord.result = (d.TryGetValue("result", out value) ? ((value as string) ?? "in_progress") : "in_progress"); runRecord.deathCause = (d.TryGetValue("deathCause", out value) ? (value as string) : null); runRecord.durationSeconds = (d.TryGetValue("durationSeconds", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); runRecord.enemiesKilled = (d.TryGetValue("enemiesKilled", out value) ? ReflectionHelper.As(value, 0) : 0); runRecord.damageDealt = (d.TryGetValue("damageDealt", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); runRecord.damageTaken = (d.TryGetValue("damageTaken", out value) ? ReflectionHelper.As(value, 0.0) : 0.0); if (d.TryGetValue("itemsPickedUp", out value) && value is List list) { foreach (object item in list) { if (item is Dictionary d2) { runRecord.itemsPickedUp.Add(ItemPickupEntry.FromDict(d2)); } } } if (d.TryGetValue("bossFights", out value) && value is List list2) { foreach (object item2 in list2) { if (item2 is Dictionary d3) { runRecord.bossFights.Add(BossFightEntry.FromDict(d3)); } } } runRecord.liveStats = (d.TryGetValue("liveStats", out value) ? LiveStats.FromDict(value as Dictionary) : new LiveStats()); return runRecord; } } public class LiveProgress { public bool[] guns; public bool[] items; public bool[] bosses; public string source = "mod-live"; public string writtenAt; public string modVersion; public Dictionary ToDict() { List list = new List(); bool[] array = guns; foreach (bool flag in array) { list.Add(flag); } List list2 = new List(); array = items; foreach (bool flag2 in array) { list2.Add(flag2); } List list3 = new List(); array = bosses; foreach (bool flag3 in array) { list3.Add(flag3); } int num = 0; array = guns; for (int i = 0; i < array.Length; i++) { if (array[i]) { num++; } } int num2 = 0; array = items; for (int i = 0; i < array.Length; i++) { if (array[i]) { num2++; } } int num3 = 0; array = bosses; for (int i = 0; i < array.Length; i++) { if (array[i]) { num3++; } } return new Dictionary { { "guns", list }, { "items", list2 }, { "bosses", list3 }, { "gunsFound", num }, { "itemsFound", num2 }, { "bossesFound", num3 }, { "source", source }, { "writtenAt", writtenAt }, { "modVersion", modVersion } }; } } public class NotificationUI : MonoBehaviour { private class Toast { public string text; public float remaining; } private const float DISPLAY_SECONDS = 3.5f; private const float FADE_SECONDS = 0.6f; private readonly Queue _queue = new Queue(); private Toast _current; private GUIStyle _boxStyle; private GUIStyle _textStyle; public void Push(string text) { _queue.Enqueue(new Toast { text = text, remaining = 3.5f }); } private void Update() { if (_current == null && _queue.Count > 0) { _current = _queue.Dequeue(); } if (_current != null) { _current.remaining -= Time.unscaledDeltaTime; if (_current.remaining <= 0f) { _current = null; } } } private void EnsureStyles() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Expected O, but got Unknown //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //IL_00b7: Unknown result type (might be due to invalid IL or missing references) if (_boxStyle == null) { _boxStyle = new GUIStyle(GUI.skin.box); Texture2D val = new Texture2D(1, 1); val.SetPixel(0, 0, new Color(0.07f, 0.06f, 0.05f, 0.92f)); val.Apply(); _boxStyle.normal.background = val; _boxStyle.border = new RectOffset(4, 4, 4, 4); _textStyle = new GUIStyle(GUI.skin.label) { fontSize = 16, alignment = (TextAnchor)3, wordWrap = true }; _textStyle.normal.textColor = new Color(0.95f, 0.85f, 0.55f); } } private void OnGUI() { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00d8: Unknown result type (might be due to invalid IL or missing references) if (_current != null) { EnsureStyles(); float num = 1f; if (_current.remaining < 0.6f) { num = _current.remaining / 0.6f; } else if (_current.remaining > 2.9f) { num = (3.5f - _current.remaining) / 0.6f; } Color color = GUI.color; GUI.color = new Color(1f, 1f, 1f, num); float num2 = 360f; float num3 = 56f; float num4 = (float)Screen.width - num2 - 24f; float num5 = 24f; GUI.Box(new Rect(num4, num5, num2, num3), string.Empty, _boxStyle); GUI.Label(new Rect(num4 + 14f, num5 + 8f, num2 - 28f, num3 - 16f), _current.text, _textStyle); GUI.color = color; } } } [BepInPlugin("user.registredugungeon.livetracker", "GR - Gungeon Register", "2.7.2")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "user.registredugungeon.livetracker"; public const string PluginName = "GR - Gungeon Register"; public const string PluginVersion = "2.7.2"; private static readonly string[] DesktopAppExeNames = new string[1] { "RegistreDuGungeon.exe" }; private Harmony _harmony; private DataStore _store; private Dictionary _guidMap; private NotificationUI _notifications; private ConfigEntry _dataDirConfig; private ConfigEntry _notificationsEnabledConfig; private ConfigEntry _autoLaunchAppConfig; private void Awake() { //IL_00dc: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Expected O, but got Unknown //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Expected O, but got Unknown //IL_012b: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected O, but got Unknown //IL_01b2: Unknown result type (might be due to invalid IL or missing references) //IL_01bc: Expected O, but got Unknown ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Registre] Demarrage du mod GR - Gungeon Register v2.7.2"); _dataDirConfig = ((BaseUnityPlugin)this).Config.Bind("General", "DataDirectory", DefaultDataDir(), "Dossier ou ecrire live_progress.json et run_history.json. Par defaut, le meme dossier deja utilise par l'application de bureau Le Registre du Gungeon."); _notificationsEnabledConfig = ((BaseUnityPlugin)this).Config.Bind("General", "NotificationsEnabled", true, "Afficher un petit popup a l'ecran lors d'une nouvelle decouverte (arme/objet/boss) ou d'un debut de combat de boss."); _autoLaunchAppConfig = ((BaseUnityPlugin)this).Config.Bind("General", "AutoLaunchApp", true, "Lancer automatiquement RegistreDuGungeon.exe (l'application de bureau) au demarrage du jeu, si cet exe est trouve juste a cote du DLL de ce mod. Mettre a false pour desactiver si tu preferes la lancer toi-meme."); try { string path = Path.Combine(Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location), "guid_map_data.json"); _guidMap = GuidMap.Load(path, delegate(string msg) { ((BaseUnityPlugin)this).Logger.LogWarning((object)msg); }); PluginGuidMap.Map = _guidMap; _store = new DataStore(_dataDirConfig.Value, delegate(string msg) { ((BaseUnityPlugin)this).Logger.LogInfo((object)msg); }); GameObject val = new GameObject("RegistreDuGungeon_Notifications"); Object.DontDestroyOnLoad((Object)(object)val); _notifications = val.AddComponent(); GameObject val2 = new GameObject("RegistreDuGungeon_RunMonitor"); Object.DontDestroyOnLoad((Object)val2); RunMonitor runMonitor = val2.AddComponent(); runMonitor.Store = _store; runMonitor.Log = delegate(string msg) { ((BaseUnityPlugin)this).Logger.LogInfo((object)msg); }; GameObject val3 = new GameObject("RegistreDuGungeon_ProgressPoller"); Object.DontDestroyOnLoad((Object)val3); ProgressPoller progressPoller = val3.AddComponent(); progressPoller.Store = _store; progressPoller.GuidMap = _guidMap; progressPoller.Log = delegate(string msg) { ((BaseUnityPlugin)this).Logger.LogInfo((object)msg); }; EncounterPatcher.OnGuidTracked = HandleGuidTracked; SessionPatches.Store = _store; SessionPatches.Notifications = (_notificationsEnabledConfig.Value ? _notifications : null); SessionPatches.Log = delegate(string msg) { ((BaseUnityPlugin)this).Logger.LogInfo((object)msg); }; _harmony = new Harmony("user.registredugungeon.livetracker"); ApplyPatchesIndividually(); _store.SaveNow(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Registre] Mod initialise. Donnees ecrites dans : " + _dataDirConfig.Value)); if (_autoLaunchAppConfig.Value) { try { LaunchDesktopAppIfNeeded(); return; } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Registre] Erreur lors du lancement automatique de l'application de bureau (non bloquant) : " + ex.Message)); return; } } } catch (TypeLoadException ex2) { ((BaseUnityPlugin)this).Logger.LogError((object)("[Registre] Erreur au demarrage du mod (TypeLoadException) : " + ex2.Message + " | Type concerne : " + ex2.TypeName)); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogError((object)("[Registre] Erreur au demarrage du mod : " + ex3)); } } private void ApplyPatchesIndividually() { Type[] array; try { array = Assembly.GetExecutingAssembly().GetTypes(); } catch (ReflectionTypeLoadException ex) { array = ex.Types.Where((Type t) => (object)t != null).ToArray(); ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Registre] Certains types du mod n'ont pas pu etre charges (ignores) : " + string.Join(", ", ex.LoaderExceptions.Select((Exception le) => le?.Message).ToArray()))); } Type[] array2 = array; foreach (Type type in array2) { bool flag; try { flag = type.GetCustomAttributes(typeof(HarmonyPatch), inherit: true).Any(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Registre] Impossible d'inspecter la classe " + type.FullName + " (probablement un point d'accroche perime pour cette version du jeu) : " + ex2.GetType().Name + " - " + ex2.Message)); continue; } if (flag) { try { _harmony.CreateClassProcessor(type).Patch(); ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Registre] Patch applique : " + type.FullName)); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Registre] Echec du patch " + type.FullName + " (fonctionnalite desactivee pour cette session) : " + ex3.Message)); } } } } private void HandleGuidTracked(string guid, int quality) { try { if (_guidMap != null && _guidMap.TryGetValue(guid, out var target)) { if (_store.MarkEncountered(target.cat, target.idx) && _notificationsEnabledConfig.Value) { string text = ((target.cat == "guns") ? "Nouvelle arme" : ((target.cat == "items") ? "Nouvel objet" : "Boss rencontre")); _notifications?.Push(text + " decouvert(e) !"); } if (_store.CurrentRun != null && (target.cat == "guns" || target.cat == "items") && !_store.CurrentRun.itemsPickedUp.Any((ItemPickupEntry it) => it.cat == target.cat && it.idx == target.idx)) { _store.CurrentRun.itemsPickedUp.Add(new ItemPickupEntry { name = guid, cat = target.cat, idx = target.idx, floor = _store.CurrentRun.floorsReached, at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"), quality = quality }); } _store.RequestSave(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[Registre] Erreur de traitement d'une decouverte : " + ex.Message)); } } private void LaunchDesktopAppIfNeeded() { string directoryName = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location); if (string.IsNullOrEmpty(directoryName)) { return; } string text = null; string[] desktopAppExeNames = DesktopAppExeNames; foreach (string path in desktopAppExeNames) { string text2 = Path.Combine(directoryName, path); if (File.Exists(text2)) { text = text2; break; } } if (text == null) { ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Registre] Application de bureau introuvable a cote du mod (dossier : " + directoryName + "), lancement automatique ignore.")); return; } if (Process.GetProcessesByName(Path.GetFileNameWithoutExtension(text)).Length != 0) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Registre] L'application de bureau tourne deja, lancement automatique ignore."); return; } ((BaseUnityPlugin)this).Logger.LogInfo((object)("[Registre] Lancement automatique de l'application de bureau : " + text)); Process.Start(new ProcessStartInfo { FileName = text, WorkingDirectory = directoryName, UseShellExecute = true }); } private void OnApplicationQuit() { try { if (_store != null && _store.CurrentRun != null) { _store.EndRun("abandoned", null); ((BaseUnityPlugin)this).Logger.LogInfo((object)"[Registre] Run encore en cours a la fermeture du jeu (Quitter vers le bureau) : marquee comme abandonnee."); } _store?.SaveNow(); } catch { } } private static string DefaultDataDir() { return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "RegistreDuGungeon"); } } public class ProgressPoller : MonoBehaviour { public DataStore Store; public Dictionary GuidMap; public Action Log; public float PollIntervalSeconds = 1f; private FieldInfo _dictField; private bool _fieldLookupAttempted; private float _timer; private void Update() { _timer += Time.unscaledDeltaTime; if (_timer < PollIntervalSeconds) { return; } _timer = 0f; try { PollOnce(); } catch (Exception ex) { Log("[Registre] Erreur lors du sondage de progression (non bloquante) : " + ex.Message); } } private void PollOnce() { if (!GameStatsManager.HasInstance) { return; } if (!_fieldLookupAttempted) { _fieldLookupAttempted = true; _dictField = typeof(GameStatsManager).GetField("m_encounteredTrackables", BindingFlags.Instance | BindingFlags.NonPublic); if ((object)_dictField == null) { Log("[Registre] Champ m_encounteredTrackables introuvable sur GameStatsManager : le rattrapage periodique de progression est desactive (les notifications instantanees continuent de fonctionner si elles marchent)."); return; } } if ((object)_dictField == null || !(_dictField.GetValue(GameStatsManager.Instance) is IDictionary dictionary)) { return; } foreach (DictionaryEntry item in dictionary) { string text = item.Key as string; if (!string.IsNullOrEmpty(text)) { object? value = item.Value; EncounteredObjectData val = (EncounteredObjectData)((value is EncounteredObjectData) ? value : null); if (val != null && val.encounterCount > 0 && GuidMap != null && GuidMap.TryGetValue(text, out var value2)) { Store.MarkEncountered(value2.cat, value2.idx); } } } } } public static class ReflectionHelper { private const BindingFlags AllInstance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags AllStatic = BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; public static bool TryGetMember(object instance, string[] candidateNames, out object value) { value = null; if (instance == null) { return false; } Type type = instance.GetType(); foreach (string name in candidateNames) { FieldInfo field = type.GetField(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)field != null) { value = field.GetValue(instance); return true; } PropertyInfo property = type.GetProperty(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)property != null && property.CanRead) { try { value = property.GetValue(instance, null); return true; } catch { } } } return false; } public static bool TryGetStaticMember(Type type, string[] candidateNames, out object value) { value = null; if ((object)type == null) { return false; } foreach (string name in candidateNames) { FieldInfo field = type.GetField(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if ((object)field != null) { value = field.GetValue(null); return true; } PropertyInfo property = type.GetProperty(name, BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if ((object)property != null && property.CanRead) { try { value = property.GetValue(null, null); return true; } catch { } } } return false; } public static bool TryCallMethod(object instance, string[] candidateNames, object[] args, out object result) { result = null; if (instance == null) { return false; } Type type = instance.GetType(); foreach (string name in candidateNames) { MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if ((object)method != null) { try { result = method.Invoke(instance, args); return true; } catch { } } } return false; } public static T As(object value, T fallback) { if (value == null) { return fallback; } try { return (T)Convert.ChangeType(value, typeof(T)); } catch { return fallback; } } public static string SafeToString(object value, string fallback = "") { if (value != null) { return value.ToString(); } return fallback; } } public class RunMonitor : MonoBehaviour { public DataStore Store; public Action Log; public float PollIntervalSeconds = 1f; private float _timer; private static readonly Dictionary QualityScore = new Dictionary { { 0, 1 }, { 1, 2 }, { 2, 3 }, { 3, 4 }, { 4, 5 }, { 5, 6 } }; private void Update() { _timer += Time.unscaledDeltaTime; if (_timer < PollIntervalSeconds) { return; } _timer = 0f; try { if (Store.CurrentRun != null && GameManager.HasInstance) { int currentFloor = GameManager.Instance.CurrentFloor; if (currentFloor > Store.CurrentRun.floorsReached) { Store.CurrentRun.floorsReached = currentFloor; } UpdateLiveStats(); Store.RequestSave(); } } catch (Exception ex) { Log("[Registre] Erreur de suivi d'etage (non bloquante) : " + ex.Message); } } private void UpdateLiveStats() { PlayerController value = Traverse.Create((object)GameManager.Instance).Field("m_player").GetValue(); if ((Object)(object)value == (Object)null) { return; } LiveStats liveStats = Store.CurrentRun.liveStats; try { HealthHaver component = ((Component)value).GetComponent(); if ((Object)(object)component != (Object)null) { Traverse val = Traverse.Create((object)component); liveStats.currentHealth = val.Field("currentHealth").GetValue(); liveStats.maxHealth = val.Field("maximumHealth").GetValue(); liveStats.currentArmor = val.Field("currentArmor").GetValue(); } } catch (Exception ex) { Log("[Registre] Erreur de sondage des PV (non bloquante) : " + ex.Message); } try { Traverse val2 = Traverse.Create((object)value); PlayerConsumables value2 = val2.Field("carriedConsumables").GetValue(); if (value2 != null) { Traverse val3 = Traverse.Create((object)value2); liveStats.currency = val3.Field("m_currency").GetValue(); liveStats.keys = val3.Field("m_keyBullets").GetValue(); } liveStats.blanks = val2.Field("m_blanks").GetValue(); } catch (Exception ex2) { Log("[Registre] Erreur de sondage des consommables (non bloquante) : " + ex2.Message); } try { List list = new List(); int gunsHeld = 0; int num = 0; Traverse val4 = Traverse.Create((object)value); GunInventory value3 = val4.Field("inventory").GetValue(); if (value3 != null) { List value4 = Traverse.Create((object)value3).Field("m_guns").GetValue>(); if (value4 != null) { gunsHeld = value4.Count; foreach (Gun item in value4) { AddQuality(list, (PickupObject)(object)item); } } } List value5 = val4.Field("activeItems").GetValue>(); if (value5 != null) { num += value5.Count; foreach (PlayerItem item2 in value5) { AddQuality(list, (PickupObject)(object)item2); } } List value6 = val4.Field("passiveItems").GetValue>(); if (value6 != null) { num += value6.Count; foreach (PassiveItem item3 in value6) { AddQuality(list, (PickupObject)(object)item3); } } liveStats.gunsHeld = gunsHeld; liveStats.itemsHeld = num; liveStats.avgQuality = ((list.Count > 0) ? list.Average() : (-1.0)); } catch (Exception ex3) { Log("[Registre] Erreur de sondage de l'inventaire (non bloquante) : " + ex3.Message); } } private static void AddQuality(List qualities, PickupObject pickup) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Expected I4, but got Unknown if (!((Object)(object)pickup == (Object)null) && QualityScore.TryGetValue((int)pickup.quality, out var value)) { qualities.Add(value); } } } public static class SessionPatches { [HarmonyPatch(typeof(GameStatsManager), "BeginNewSession")] public static class BeginSessionPatch { [HarmonyPostfix] public static void Postfix(PlayerController player) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) try { string text = (((Object)(object)player != (Object)null) ? NormalizeCharacterKey(player.characterIdentity) : "unknown"); string text2 = "unknown"; if (GameManager.HasInstance) { text2 = GameManager.Instance.CurrentRunSeed.ToString(CultureInfo.InvariantCulture); } Store.StartRun(text, text2); Log("[Registre] Nouveau run demarre (GameStatsManager.BeginNewSession) : personnage=" + text + ", seed=" + text2); } catch (Exception ex) { Log("[Registre] Erreur au demarrage de run : " + ex.Message); } } } [HarmonyPatch(typeof(GameStatsManager), "EndSession")] public static class EndSessionPatch { [HarmonyPrefix] public static void Prefix(bool recordSessionStats, bool decrementDifferentiator) { try { if (Store.CurrentRun != null) { if (GameManager.HasInstance) { Store.CurrentRun.floorsReached = Math.Max(Store.CurrentRun.floorsReached, GameManager.Instance.CurrentFloor); } if (GameStatsManager.HasInstance) { GameStatsManager instance = GameStatsManager.Instance; Store.CurrentRun.enemiesKilled = (int)instance.GetSessionStatValue((TrackedStats)22); Store.CurrentRun.durationSeconds = instance.GetSessionStatValue((TrackedStats)23); } string text = ((Store.CurrentRun.result == "in_progress") ? "returned_to_hub" : Store.CurrentRun.result); Store.EndRun(text, Store.CurrentRun.deathCause); Log("[Registre] Fin de run (GameStatsManager.EndSession) : resultat=" + text); } } catch (Exception ex) { Log("[Registre] Erreur a la fin de run : " + ex.Message); } } } [HarmonyPatch(typeof(HealthHaver), "Start")] public static class HealthHaverStartPatch { [HarmonyPostfix] public static void Postfix(HealthHaver __instance) { try { if (Store.CurrentRun != null && !((Object)(object)__instance == (Object)null) && __instance.IsBoss && !ActiveBossFights.ContainsKey(__instance)) { string text = (((Object)(object)((Component)__instance).gameObject != (Object)null) ? ((Object)((Component)__instance).gameObject).name : "Boss"); int idx = -1; EncounterTrackable component = ((Component)__instance).GetComponent(); if ((Object)(object)component != (Object)null && !string.IsNullOrEmpty(component.EncounterGuid) && PluginGuidMap.Map != null && PluginGuidMap.Map.TryGetValue(component.EncounterGuid, out var value) && value.cat == "bosses") { idx = value.idx; } BossFightEntry bossFightEntry = new BossFightEntry { name = text, idx = idx, startedAt = NowIso(), result = "unknown" }; Store.CurrentRun.bossFights.Add(bossFightEntry); ActiveBossFights[__instance] = bossFightEntry; Notifications?.Push("Combat de boss : " + text); Log("[Registre] Debut de combat de boss detecte : " + text); Store.RequestSave(); } } catch (Exception ex) { Log("[Registre] Erreur au debut de combat de boss : " + ex.Message); } } } [HarmonyPatch(typeof(HealthHaver), "ApplyDamage")] public static class HealthHaverDamagePatch { [HarmonyPostfix] public static void Postfix(HealthHaver __instance, float damage) { try { if (Store.CurrentRun == null || (Object)(object)__instance == (Object)null || damage <= 0f) { return; } if (__instance.IsBoss && ActiveBossFights.TryGetValue(__instance, out var value)) { value.damageDealt += damage; } else { if (!IsPlayerCharacterSafe(__instance)) { return; } Store.CurrentRun.damageTaken += damage; if (ActiveBossFights.Count <= 0) { return; } float num = damage / (float)ActiveBossFights.Count; { foreach (KeyValuePair activeBossFight in ActiveBossFights) { activeBossFight.Value.damageTaken += num; } return; } } } catch { } } } [HarmonyPatch(typeof(HealthHaver), "Die")] public static class HealthHaverDiePatch { [HarmonyPostfix] public static void Postfix(HealthHaver __instance) { try { if ((Object)(object)__instance == (Object)null) { return; } if (__instance.IsBoss && ActiveBossFights.TryGetValue(__instance, out var value)) { value.endedAt = NowIso(); value.result = "won"; try { if (DateTime.TryParse(value.startedAt, null, DateTimeStyles.RoundtripKind, out var result)) { value.durationSeconds = (DateTime.UtcNow - result).TotalSeconds; } } catch { } ActiveBossFights.Remove(__instance); Log("[Registre] Fin de combat de boss : " + value.name + " (" + value.durationSeconds.ToString("F0") + "s)."); if (value.idx == 17 && Store.CurrentRun != null) { CloseStaleBossFights("won"); Store.EndRun("victory", null); Log("[Registre] Liche vaincue : run cloturee en victoire."); } else { Store.RequestSave(); } } else if (IsPlayerCharacterSafe(__instance) && Store.CurrentRun != null) { CloseStaleBossFights("lost"); Store.EndRun("death", "Mort en jeu"); Log("[Registre] Mort du joueur detectee (HealthHaver.Die) : run cloture."); } } catch (Exception ex) { Log("[Registre] Erreur a la mort/fin de combat : " + ex.Message); } } } public static DataStore Store; public static NotificationUI Notifications; public static Action Log; private static readonly Dictionary ActiveBossFights = new Dictionary(); private const int LICH_BOSS_IDX = 17; private static FieldInfo _isPlayerCharacterField; private static bool _isPlayerCharacterFieldLookupAttempted; private static string NowIso() { return DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture); } private unsafe static string NormalizeCharacterKey(PlayableCharacters id) { return ((object)(*(PlayableCharacters*)(&id))/*cast due to .constrained prefix*/).ToString().ToLowerInvariant(); } private static bool IsPlayerCharacterSafe(HealthHaver instance) { if (instance == null) { return false; } if (!_isPlayerCharacterFieldLookupAttempted) { _isPlayerCharacterFieldLookupAttempted = true; _isPlayerCharacterField = typeof(HealthHaver).GetField("isPlayerCharacter", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if ((object)_isPlayerCharacterField == null) { return false; } try { return (bool)_isPlayerCharacterField.GetValue(instance); } catch { return false; } } private static void CloseStaleBossFights(string fallbackResult) { foreach (BossFightEntry value in ActiveBossFights.Values) { if (!string.IsNullOrEmpty(value.endedAt)) { continue; } value.endedAt = NowIso(); value.result = fallbackResult; try { if (DateTime.TryParse(value.startedAt, null, DateTimeStyles.RoundtripKind, out var result)) { value.durationSeconds = (DateTime.UtcNow - result).TotalSeconds; } } catch { } } ActiveBossFights.Clear(); } } public static class PluginGuidMap { public static Dictionary Map; } }