using 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 _players = new Dictionary(); private readonly string _savePath; private bool _registered; internal IEnumerable 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("VSS_030_Report", (Action)RPC_Report); ZRoutedRpc.instance.Register("VSS_030_Roster", (Action)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 dictionary = new Dictionary(); 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 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 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 list = new List { "[" }; 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 _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("General", "TogglePanelKey", new KeyboardShortcut((KeyCode)287, Array.Empty()), "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()); GUILayout.Space(5f); GUILayout.Label("CRÓNICAS DEL GUERRERO", val, Array.Empty()); GUILayout.Label("Marcador multijugador · sincronización ~2 s · F6 para ocultar", val2, Array.Empty()); GUILayout.Space(10f); _scroll = GUILayout.BeginScrollView(_scroll, Array.Empty()); GUILayout.Label("JUGADORES Y ESTADÍSTICAS ACUMULADAS", new GUIStyle(GUI.skin.label) { fontSize = 14, fontStyle = (FontStyle)1 }, Array.Empty()); GUILayout.BeginHorizontal(GUIStyle.op_Implicit("box"), Array.Empty()); 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()); 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()); } GUILayout.Space(8f); GUILayout.Label("Los datos se acumulan entre sesiones. El anfitrión conserva el historial compartido.", val2, Array.Empty()); GUILayout.Label("Para sincronizar: todos deben tener la misma versión del mod y entrar al mundo con Start modded.", val2, Array.Empty()); GUILayout.EndScrollView(); GUILayout.BeginHorizontal(Array.Empty()); 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()); GUILayout.Label(value, new GUIStyle(GUI.skin.label) { fontSize = 19, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }, Array.Empty()); 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 LastHitByLocalPlayer = new Dictionary(); private static IEnumerable 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 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 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 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 KillsByCreature = new Dictionary(); } 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 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(); } } }