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 ValheimSagaStats v0.3.2
plugins/ValheimSagaStats.dll
Decompiled 2 weeks agousing System; 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.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; 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: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("ValheimSagaStats")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0")] [assembly: AssemblyProduct("ValheimSagaStats")] [assembly: AssemblyTitle("ValheimSagaStats")] [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] [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 ValheimSagaStats { internal sealed class PlayerStatsSnapshot { public string Id = ""; public string Name = "Jugador"; public long DamageDealt; public long DamageTaken; public long PlaySeconds; public int Kills; public int Deaths; public int Parries; public double DistanceMeters; public float LastSeen; public bool Online; } internal sealed class MultiplayerSync { private const string RpcReport = "VSS_030_Report"; private const string RpcRoster = "VSS_030_Roster"; private const float SendInterval = 2f; private const float OnlineTimeout = 15f; private float _sendTimer; private readonly Dictionary<string, PlayerStatsSnapshot> _players = new Dictionary<string, PlayerStatsSnapshot>(); private readonly string _savePath; private bool _registered; internal IEnumerable<PlayerStatsSnapshot> Players => (from p in _players.Values orderby p.Online descending, p.DamageDealt descending select p).ToArray(); internal MultiplayerSync(string configDir) { string text = Path.Combine(configDir, "ValheimSagaStats"); Directory.CreateDirectory(text); _savePath = Path.Combine(text, "multiplayer_players.json"); Load(); } internal void Tick(StatsData local, string playerName) { try { if ((Object)(object)ZNet.instance == (Object)null || ZRoutedRpc.instance == null || local == null) { return; } RegisterRpcs(); _sendTimer += Time.unscaledDeltaTime; string text = ZNet.GetUID().ToString(); if (!_players.ContainsKey(text)) { _players[text] = new PlayerStatsSnapshot { Id = text, Name = playerName }; } PlayerStatsSnapshot playerStatsSnapshot = FromLocal(text, playerName, local); playerStatsSnapshot.LastSeen = Time.unscaledTime; playerStatsSnapshot.Online = true; _players[text] = playerStatsSnapshot; if (_sendTimer >= 2f) { _sendTimer = 0f; if (ZNet.instance.IsServer()) { Save(); BroadcastRoster(); } else { ZPackage val = MakeSnapshotPackage(playerStatsSnapshot); MethodInfo methodInfo = AccessTools.Method(typeof(ZRoutedRpc), "GetServerPeerID", (Type[])null, (Type[])null); if (methodInfo != null) { object obj = methodInfo.Invoke(ZRoutedRpc.instance, null); if (obj is long) { ZRoutedRpc.instance.InvokeRoutedRPC((long)obj, "VSS_030_Report", new object[1] { val }); } } else { Plugin.DebugLog("No se pudo resolver ZRoutedRpc.GetServerPeerID en esta versión de Valheim."); } } } foreach (PlayerStatsSnapshot value in _players.Values) { value.Online = value.Id == text || Time.unscaledTime - value.LastSeen < 15f; } } catch (Exception ex) { Plugin.DebugLog("Multiplayer tick: " + ex.Message); } } private void RegisterRpcs() { if (!_registered) { ZRoutedRpc.instance.Register<ZPackage>("VSS_030_Report", (Action<long, ZPackage>)RPC_Report); ZRoutedRpc.instance.Register<ZPackage>("VSS_030_Roster", (Action<long, ZPackage>)RPC_Roster); _registered = true; Plugin.DebugLog("RPC multijugador registrados."); } } private void RPC_Report(long sender, ZPackage pkg) { try { if (!((Object)(object)ZNet.instance == (Object)null) && ZNet.instance.IsServer() && pkg != null) { PlayerStatsSnapshot playerStatsSnapshot = ReadSnapshot(pkg); if (playerStatsSnapshot != null) { playerStatsSnapshot.Id = sender.ToString(); playerStatsSnapshot.LastSeen = Time.unscaledTime; playerStatsSnapshot.Online = true; _players[playerStatsSnapshot.Id] = playerStatsSnapshot; Save(); } } } catch (Exception ex) { Plugin.DebugLog("RPC_Report: " + ex.Message); } } private void RPC_Roster(long sender, ZPackage pkg) { try { if (pkg == null) { return; } int num = pkg.ReadInt(); if (num < 0 || num > 128) { return; } Dictionary<string, PlayerStatsSnapshot> dictionary = new Dictionary<string, PlayerStatsSnapshot>(); for (int i = 0; i < num; i++) { PlayerStatsSnapshot playerStatsSnapshot = ReadSnapshot(pkg); if (playerStatsSnapshot == null) { return; } playerStatsSnapshot.LastSeen = Time.unscaledTime; playerStatsSnapshot.Online = true; dictionary[playerStatsSnapshot.Id] = playerStatsSnapshot; } foreach (KeyValuePair<string, PlayerStatsSnapshot> item in dictionary) { _players[item.Key] = item.Value; } } catch (Exception ex) { Plugin.DebugLog("RPC_Roster: " + ex.Message); } } private void BroadcastRoster() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Expected O, but got Unknown List<PlayerStatsSnapshot> list = _players.Values.Where((PlayerStatsSnapshot p) => p.Online).ToList(); ZPackage val = new ZPackage(); val.Write(list.Count); foreach (PlayerStatsSnapshot item in list) { WriteSnapshot(val, item); } ZRoutedRpc.instance.InvokeRoutedRPC(0L, "VSS_030_Roster", new object[1] { val }); } private static ZPackage MakeSnapshotPackage(PlayerStatsSnapshot s) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown //IL_000d: Expected O, but got Unknown ZPackage val = new ZPackage(); WriteSnapshot(val, s); return val; } private static void WriteSnapshot(ZPackage pkg, PlayerStatsSnapshot s) { pkg.Write(s.Id ?? ""); pkg.Write(s.Name ?? "Jugador"); pkg.Write(s.DamageDealt); pkg.Write(s.DamageTaken); pkg.Write(s.Kills); pkg.Write(s.Deaths); pkg.Write(s.Parries); pkg.Write((float)s.DistanceMeters); pkg.Write(s.PlaySeconds); } private static PlayerStatsSnapshot ReadSnapshot(ZPackage pkg) { return new PlayerStatsSnapshot { Id = pkg.ReadString(), Name = pkg.ReadString(), DamageDealt = pkg.ReadLong(), DamageTaken = pkg.ReadLong(), Kills = pkg.ReadInt(), Deaths = pkg.ReadInt(), Parries = pkg.ReadInt(), DistanceMeters = pkg.ReadSingle(), PlaySeconds = pkg.ReadLong() }; } private static PlayerStatsSnapshot FromLocal(string id, string name, StatsData d) { return new PlayerStatsSnapshot { Id = id, Name = (string.IsNullOrWhiteSpace(name) ? "Jugador" : name), DamageDealt = d.DamageDealt, DamageTaken = d.DamageTaken, Kills = d.Kills, Deaths = d.Deaths, Parries = d.Parries, DistanceMeters = d.DistanceMeters, PlaySeconds = d.PlaySeconds, Online = true }; } private void Load() { try { if (!File.Exists(_savePath)) { return; } foreach (Match item in Regex.Matches(File.ReadAllText(_savePath), "\\{\\s*\\\"Id\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"\\s*,\\s*\\\"Name\\\"\\s*:\\s*\\\"([^\\\"]*)\\\"\\s*,\\s*\\\"DamageDealt\\\"\\s*:\\s*(\\d+)\\s*,\\s*\\\"DamageTaken\\\"\\s*:\\s*(\\d+)\\s*,\\s*\\\"Kills\\\"\\s*:\\s*(\\d+)\\s*,\\s*\\\"Deaths\\\"\\s*:\\s*(\\d+)\\s*,\\s*\\\"Parries\\\"\\s*:\\s*(\\d+)\\s*,\\s*\\\"DistanceMeters\\\"\\s*:\\s*([0-9.]+)\\s*,\\s*\\\"PlaySeconds\\\"\\s*:\\s*(\\d+)\\s*\\}", RegexOptions.Singleline)) { double.TryParse(item.Groups[8].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result); PlayerStatsSnapshot playerStatsSnapshot = new PlayerStatsSnapshot { Id = item.Groups[1].Value, Name = item.Groups[2].Value, DamageDealt = long.Parse(item.Groups[3].Value), DamageTaken = long.Parse(item.Groups[4].Value), Kills = int.Parse(item.Groups[5].Value), Deaths = int.Parse(item.Groups[6].Value), Parries = int.Parse(item.Groups[7].Value), DistanceMeters = result, PlaySeconds = long.Parse(item.Groups[9].Value), Online = false, LastSeen = -10000f }; _players[playerStatsSnapshot.Id] = playerStatsSnapshot; } } catch (Exception ex) { Plugin.DebugLog("Roster load: " + ex.Message); } } internal void Save() { try { if ((Object)(object)ZNet.instance != (Object)null && !ZNet.instance.IsServer()) { return; } List<string> list = new List<string> { "[" }; int num = 0; foreach (PlayerStatsSnapshot value in _players.Values) { if (num++ > 0) { list.Add(","); } list.Add("{\"Id\":\"" + Esc(value.Id) + "\",\"Name\":\"" + Esc(value.Name) + "\",\"DamageDealt\":" + value.DamageDealt + ",\"DamageTaken\":" + value.DamageTaken + ",\"Kills\":" + value.Kills + ",\"Deaths\":" + value.Deaths + ",\"Parries\":" + value.Parries + ",\"DistanceMeters\":" + value.DistanceMeters.ToString("R", CultureInfo.InvariantCulture) + ",\"PlaySeconds\":" + value.PlaySeconds + "}"); } list.Add("]"); string text = _savePath + ".tmp"; File.WriteAllText(text, string.Join("", list)); if (File.Exists(_savePath)) { File.Delete(_savePath); } File.Move(text, _savePath); } catch (Exception ex) { Plugin.DebugLog("Roster save: " + ex.Message); } } private static string Esc(string s) { return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ") .Replace("\n", " "); } } [BepInPlugin("com.pedro.valheimsagastats", "Valheim Saga Stats", "0.3.0")] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.pedro.valheimsagastats"; public const string PluginName = "Valheim Saga Stats"; public const string PluginVersion = "0.3.0"; internal static Plugin Instance; internal static StatsStore Store; private Harmony _harmony; private bool _showPanel; private Rect _windowRect = new Rect(70f, 55f, 650f, 610f); private Vector2 _scroll; private ConfigEntry<KeyboardShortcut> _toggleKey; private Vector3 _lastPosition; private bool _hasPosition; private float _saveTimer; private float _playTimer; private MultiplayerSync _multiplayer; internal static void DebugLog(string message) { if ((Object)(object)Instance != (Object)null) { ((BaseUnityPlugin)Instance).Logger.LogDebug((object)message); } } private void Awake() { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Expected O, but got Unknown Instance = this; _toggleKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("General", "TogglePanelKey", new KeyboardShortcut((KeyCode)287, Array.Empty<KeyCode>()), "Tecla para abrir/cerrar el panel."); Store = new StatsStore(Path.Combine(Paths.ConfigPath, "ValheimSagaStats")); Store.Load(); _multiplayer = new MultiplayerSync(Paths.ConfigPath); _harmony = new Harmony("com.pedro.valheimsagastats"); try { _harmony.PatchAll(typeof(Plugin).Assembly); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Valheim Saga Stats 0.3.0 cargado; se instalaron los parches de estadísticas."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("No se pudieron instalar todos los parches: " + ex)); } } private void Update() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = _toggleKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { _showPanel = !_showPanel; } try { object localPlayer = GameReflection.LocalPlayer; if (localPlayer != null) { Component val = (Component)((localPlayer is Component) ? localPlayer : null); if ((Object)(object)val != (Object)null) { Vector3 position = val.transform.position; if (_hasPosition) { Store.Data.DistanceMeters += Vector3.Distance(_lastPosition, position); } _lastPosition = position; _hasPosition = true; _playTimer += Time.unscaledDeltaTime; if (_playTimer >= 1f) { Store.Data.PlaySeconds++; Store.Data.SessionSeconds++; _playTimer -= 1f; } } } else { _hasPosition = false; } if (localPlayer != null && _multiplayer != null) { _multiplayer.Tick(Store.Data, GetLocalPlayerName(localPlayer)); } _saveTimer += Time.unscaledDeltaTime; if (_saveTimer >= 30f) { Store.Save(); _saveTimer = 0f; } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogDebug((object)("Actualización de métricas: " + ex.Message)); } } private void OnGUI() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Expected O, but got Unknown //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (_showPanel) { _windowRect = GUI.Window(749031, _windowRect, new WindowFunction(DrawWindow), "VALHEIM SAGA STATS · v0.3.0"); } } private void DrawWindow(int id) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Expected O, but got Unknown //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_0085: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: 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_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Expected O, but got Unknown //IL_03df: Unknown result type (might be due to invalid IL or missing references) GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = 21, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; GUIStyle val2 = new GUIStyle(GUI.skin.label) { fontSize = 11, alignment = (TextAnchor)4 }; GUILayout.BeginVertical(Array.Empty<GUILayoutOption>()); GUILayout.Space(5f); GUILayout.Label("CRÓNICAS DEL GUERRERO", val, Array.Empty<GUILayoutOption>()); GUILayout.Label("Marcador multijugador · sincronización ~2 s · F6 para ocultar", val2, Array.Empty<GUILayoutOption>()); GUILayout.Space(10f); _scroll = GUILayout.BeginScrollView(_scroll, Array.Empty<GUILayoutOption>()); GUILayout.Label("JUGADORES Y ESTADÍSTICAS ACUMULADAS", new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = (FontStyle)1 }, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>()); GUILayout.Label("Estado / jugador", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(150f) }); GUILayout.Label("Daño +", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(75f) }); GUILayout.Label("Daño -", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(75f) }); GUILayout.Label("Bajas", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(50f) }); GUILayout.Label("Muertes", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(55f) }); GUILayout.Label("Distancia", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); GUILayout.EndHorizontal(); bool flag = false; if (_multiplayer != null) { foreach (PlayerStatsSnapshot player in _multiplayer.Players) { flag = true; GUILayout.BeginHorizontal(GUIStyle.op_Implicit("box"), Array.Empty<GUILayoutOption>()); GUILayout.Label((player.Online ? "● " : "○ ") + player.Name, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(150f) }); GUILayout.Label(player.DamageDealt.ToString("N0"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(75f) }); GUILayout.Label(player.DamageTaken.ToString("N0"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(75f) }); GUILayout.Label(player.Kills.ToString("N0"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(50f) }); GUILayout.Label(player.Deaths.ToString("N0"), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(55f) }); GUILayout.Label(FormatDistance(player.DistanceMeters), (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(80f) }); GUILayout.EndHorizontal(); } } if (!flag) { GUILayout.Label("Esperando estadísticas de jugadores con el mod instalado...", Array.Empty<GUILayoutOption>()); } GUILayout.Space(8f); GUILayout.Label("Los datos se acumulan entre sesiones. El anfitrión conserva el historial compartido.", val2, Array.Empty<GUILayoutOption>()); GUILayout.Label("Para sincronizar: todos deben tener la misma versión del mod y entrar al mundo con Start modded.", val2, Array.Empty<GUILayoutOption>()); GUILayout.EndScrollView(); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Guardar ahora", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { Store.Save(); if (_multiplayer != null) { _multiplayer.Save(); } } if (GUILayout.Button("Reiniciar estadísticas", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { Store.Reset(); } if (GUILayout.Button("Cerrar", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(30f) })) { _showPanel = false; } GUILayout.EndHorizontal(); GUILayout.EndVertical(); GUI.DragWindow(new Rect(0f, 0f, 10000f, 28f)); } private void Card(string label, string value) { //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0073: 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_008b: Expected O, but got Unknown GUILayout.BeginVertical(GUIStyle.op_Implicit("box"), (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.MinHeight(65f), GUILayout.ExpandWidth(true) }); GUILayout.Label(label, new GUIStyle(GUI.skin.label) { fontSize = 10, alignment = (TextAnchor)4, wordWrap = true }, Array.Empty<GUILayoutOption>()); GUILayout.Label(value, new GUIStyle(GUI.skin.label) { fontSize = 19, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }, Array.Empty<GUILayoutOption>()); GUILayout.EndVertical(); } private string GetLocalPlayerName(object player) { try { object obj = GameReflection.Call(player, "GetPlayerName"); if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString())) { return obj.ToString(); } obj = GameReflection.Read(player, "m_playerName"); if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString())) { return obj.ToString(); } obj = GameReflection.Read(player, "m_name"); if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString())) { return obj.ToString(); } } catch { } return "Jugador local"; } private static string FormatDistance(double m) { if (!(m >= 1000.0)) { return m.ToString("N0") + " m"; } return (m / 1000.0).ToString("N2") + " km"; } private static string FormatTime(long sec) { TimeSpan timeSpan = TimeSpan.FromSeconds(Math.Max(0L, sec)); return ((int)timeSpan.TotalHours).ToString("D2") + ":" + timeSpan.Minutes.ToString("D2") + ":" + timeSpan.Seconds.ToString("D2"); } private void OnDestroy() { if (Store != null) { Store.Save(); } if (_multiplayer != null) { _multiplayer.Save(); } if (_harmony != null) { _harmony.UnpatchSelf(); } Instance = null; } } internal static class GameReflection { internal static object LocalPlayer { get { Type type = AccessTools.TypeByName("Player"); if (type == null) { return null; } FieldInfo fieldInfo = AccessTools.Field(type, "m_localPlayer"); if (!(fieldInfo == null)) { return fieldInfo.GetValue(null); } return null; } } internal static object Read(object obj, string name) { if (obj == null) { return null; } Type type = obj.GetType(); while (type != null) { FieldInfo field = type.GetField(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field != null) { return field.GetValue(obj); } PropertyInfo property = type.GetProperty(name, BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property != null && property.GetIndexParameters().Length == 0) { try { return property.GetValue(obj, null); } catch { } } type = type.BaseType; } return null; } internal static object Call(object obj, string name) { if (obj == null) { return null; } try { MethodInfo methodInfo = AccessTools.Method(obj.GetType(), name, Type.EmptyTypes, (Type[])null); return (methodInfo == null) ? null : methodInfo.Invoke(obj, null); } catch { return null; } } internal static double DamageAmount(object hit) { object obj = Call(hit, "GetTotalDamage"); if (obj != null) { try { return Math.Max(0.0, Convert.ToDouble(obj)); } catch { } } object obj3 = Read(hit, "m_damage"); if (obj3 == null) { return 0.0; } double num = 0.0; FieldInfo[] fields = obj3.GetType().GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo fieldInfo in fields) { if (fieldInfo.FieldType == typeof(float) || fieldInfo.FieldType == typeof(double) || fieldInfo.FieldType == typeof(int)) { try { num += Convert.ToDouble(fieldInfo.GetValue(obj3)); } catch { } } } return Math.Max(0.0, num); } internal static bool IsPlayerAttacker(object hit) { object localPlayer = LocalPlayer; if (localPlayer == null || hit == null) { return false; } object obj = Call(hit, "GetAttacker"); if (obj != null) { if (obj == localPlayer) { return true; } object obj2 = Call(obj, "GetZDOID"); object obj3 = Call(localPlayer, "GetZDOID"); if (obj2 != null && obj3 != null && (obj2.Equals(obj3) || obj2.ToString() == obj3.ToString())) { return true; } } object obj4 = Read(hit, "m_attacker"); object obj5 = Call(localPlayer, "GetZDOID"); if (obj4 != null && obj5 != null && (obj4.Equals(obj5) || obj4.ToString() == obj5.ToString())) { return true; } return false; } internal static string CreatureName(object character) { object obj = Read(character, "m_name"); if (obj != null && !string.IsNullOrWhiteSpace(obj.ToString())) { return obj.ToString().Trim(); } if (character != null) { return character.GetType().Name; } return "Desconocido"; } } [HarmonyPatch] internal static class DamagePatch { private static readonly Dictionary<int, bool> LastHitByLocalPlayer = new Dictionary<int, bool>(); private static IEnumerable<MethodBase> TargetMethods() { Type type = AccessTools.TypeByName("Character"); if (type == null) { yield break; } foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type)) { if (declaredMethod.Name == "Damage" && declaredMethod.GetParameters().Length != 0) { yield return declaredMethod; } } } private static void Prefix(object __instance, object[] __args) { try { if (Plugin.Store == null || __instance == null) { return; } object obj = null; foreach (object obj2 in __args) { if (obj2 != null && obj2.GetType().Name == "HitData") { obj = obj2; break; } } if (obj == null) { return; } double num = GameReflection.DamageAmount(obj); if (num <= 0.0) { return; } object localPlayer = GameReflection.LocalPlayer; if (localPlayer != null) { if (__instance == localPlayer) { Plugin.Store.Data.DamageTaken += (long)Math.Round(num); return; } bool value = GameReflection.IsPlayerAttacker(obj); int key = (((Object)((__instance is Object) ? __instance : null) != (Object)null) ? ((Object)((__instance is Object) ? __instance : null)).GetInstanceID() : __instance.GetHashCode()); LastHitByLocalPlayer[key] = value; } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.DebugLog("DamagePatch: " + ex.Message); } } } internal static void MarkLastHitFromPlayer(int id) { LastHitByLocalPlayer[id] = true; } internal static bool WasLastHitFromPlayer(object character) { if (character == null) { return false; } int key = (((Object)((character is Object) ? character : null) != (Object)null) ? ((Object)((character is Object) ? character : null)).GetInstanceID() : character.GetHashCode()); bool value; return LastHitByLocalPlayer.TryGetValue(key, out value) && value; } } [HarmonyPatch] internal static class AppliedDamagePatch { private static IEnumerable<MethodBase> TargetMethods() { Type type = AccessTools.TypeByName("Character"); if (type == null) { yield break; } foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type)) { if (declaredMethod.Name == "ApplyDamage" && declaredMethod.GetParameters().Length != 0) { yield return declaredMethod; } } } private static void Prefix(object __instance, object[] __args) { try { if (Plugin.Store == null || __instance == null) { return; } object obj = null; foreach (object obj2 in __args) { if (obj2 != null && obj2.GetType().Name == "HitData") { obj = obj2; break; } } if (obj == null) { return; } object localPlayer = GameReflection.LocalPlayer; if (localPlayer != null && __instance != localPlayer && GameReflection.IsPlayerAttacker(obj)) { double num = GameReflection.DamageAmount(obj); if (num > 0.0) { Plugin.Store.Data.DamageDealt += (long)Math.Round(num); } DamagePatch.MarkLastHitFromPlayer(((Object)((__instance is Object) ? __instance : null) != (Object)null) ? ((Object)((__instance is Object) ? __instance : null)).GetInstanceID() : __instance.GetHashCode()); } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.DebugLog("AppliedDamagePatch: " + ex.Message); } } } } [HarmonyPatch] internal static class DeathPatch { private static IEnumerable<MethodBase> TargetMethods() { Type type = AccessTools.TypeByName("Character"); if (type == null) { yield break; } foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type)) { if (declaredMethod.Name == "OnDeath") { yield return declaredMethod; } } } private static void Postfix(object __instance) { try { if (Plugin.Store != null && __instance != null) { if (__instance == GameReflection.LocalPlayer) { Plugin.Store.Data.Deaths++; } else if (DamagePatch.WasLastHitFromPlayer(__instance)) { Plugin.Store.Data.Kills++; string key = GameReflection.CreatureName(__instance); Plugin.Store.Data.KillsByCreature.TryGetValue(key, out var value); Plugin.Store.Data.KillsByCreature[key] = value + 1; } } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.DebugLog("DeathPatch: " + ex.Message); } } } } [HarmonyPatch] internal static class BlockPatch { private static IEnumerable<MethodBase> TargetMethods() { Type type = AccessTools.TypeByName("Humanoid"); if (type == null) { yield break; } foreach (MethodInfo declaredMethod in AccessTools.GetDeclaredMethods(type)) { if ((declaredMethod.Name == "BlockAttack" || declaredMethod.Name == "BlockAttackNoStamina") && declaredMethod.GetParameters().Length != 0 && declaredMethod.ReturnType == typeof(bool)) { yield return declaredMethod; } } } private static void Postfix(object __instance, object __result) { try { if (Plugin.Store != null && __instance == GameReflection.LocalPlayer && __result is bool && (bool)__result) { Plugin.Store.Data.Parries++; } } catch (Exception ex) { if ((Object)(object)Plugin.Instance != (Object)null) { Plugin.DebugLog("BlockPatch: " + ex.Message); } } } } [Serializable] public class StatsData { public long DamageDealt; public long DamageTaken; public int Kills; public int Deaths; public int Parries; public double DistanceMeters; public long PlaySeconds; public long SessionSeconds; public Dictionary<string, int> KillsByCreature = new Dictionary<string, int>(); } public class StatsStore { private readonly string _directory; private readonly string _filePath; public StatsData Data { get; private set; } = new StatsData(); public StatsStore(string directory) { _directory = directory; _filePath = Path.Combine(directory, "stats.json"); } public void Load() { try { if (!Directory.Exists(_directory)) { Directory.CreateDirectory(_directory); } if (!File.Exists(_filePath)) { return; } string text = File.ReadAllText(_filePath); Data = new StatsData { DamageDealt = ReadLong(text, "DamageDealt"), DamageTaken = ReadLong(text, "DamageTaken"), Kills = (int)ReadLong(text, "Kills"), Deaths = (int)ReadLong(text, "Deaths"), Parries = (int)ReadLong(text, "Parries"), DistanceMeters = ReadDouble(text, "DistanceMeters"), PlaySeconds = ReadLong(text, "PlaySeconds"), SessionSeconds = 0L }; Match match = Regex.Match(text, "\\\"KillsByCreature\\\"\\s*:\\s*\\{([^}]*)\\}", RegexOptions.Singleline); if (!match.Success) { return; } foreach (Match item in Regex.Matches(match.Groups[1].Value, "\\\"((?:\\\\.|[^\\\"])*)\\\"\\s*:\\s*(\\d+)")) { string key = item.Groups[1].Value.Replace("\\\"", "\"").Replace("\\\\", "\\"); if (int.TryParse(item.Groups[2].Value, out var result)) { Data.KillsByCreature[key] = result; } } } catch (Exception ex) { Debug.LogWarning((object)("[Valheim Saga Stats] Load failed: " + ex.Message)); } } private static long ReadLong(string j, string k) { Match match = Regex.Match(j, "\\\"" + Regex.Escape(k) + "\\\"\\s*:\\s*(-?\\d+)"); if (!match.Success || !long.TryParse(match.Groups[1].Value, out var result)) { return 0L; } return result; } private static double ReadDouble(string j, string k) { Match match = Regex.Match(j, "\\\"" + Regex.Escape(k) + "\\\"\\s*:\\s*(-?\\d+(?:\\.\\d+)?)"); if (!match.Success || !double.TryParse(match.Groups[1].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result)) { return 0.0; } return result; } public void Save() { try { if (!Directory.Exists(_directory)) { Directory.CreateDirectory(_directory); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("{"); stringBuilder.AppendLine(" \"DamageDealt\": " + Data.DamageDealt + ","); stringBuilder.AppendLine(" \"DamageTaken\": " + Data.DamageTaken + ","); stringBuilder.AppendLine(" \"Kills\": " + Data.Kills + ","); stringBuilder.AppendLine(" \"Deaths\": " + Data.Deaths + ","); stringBuilder.AppendLine(" \"Parries\": " + Data.Parries + ","); stringBuilder.AppendLine(" \"DistanceMeters\": " + Data.DistanceMeters.ToString("R", CultureInfo.InvariantCulture) + ","); stringBuilder.AppendLine(" \"PlaySeconds\": " + Data.PlaySeconds + ","); stringBuilder.AppendLine(" \"KillsByCreature\": {"); int num = 0; foreach (KeyValuePair<string, int> item in Data.KillsByCreature) { stringBuilder.Append(" \"").Append(Escape(item.Key)).Append("\": ") .Append(item.Value); if (++num < Data.KillsByCreature.Count) { stringBuilder.Append(','); } stringBuilder.AppendLine(); } stringBuilder.AppendLine(" }"); stringBuilder.AppendLine("}"); string text = _filePath + ".tmp"; File.WriteAllText(text, stringBuilder.ToString()); if (File.Exists(_filePath)) { File.Delete(_filePath); } File.Move(text, _filePath); } catch (Exception ex) { Debug.LogWarning((object)("[Valheim Saga Stats] Save failed: " + ex.Message)); } } private static string Escape(string s) { return (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\r", " ") .Replace("\n", " "); } public void Reset() { Data = new StatsData(); Save(); } } }