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.Security.Cryptography; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using FishNet; using FishNet.Broadcast; using FishNet.Connection; using FishNet.Managing.Client; using FishNet.Managing.Server; using FishNet.Serializing; using FishNet.Transporting; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyVersion("0.0.0.0")] namespace HowToFishStats; internal enum DeduplicationResult { NewEvent, Duplicate, PromoteToExact } internal sealed class CrossSourceDeduplicator { private sealed class RecentObservation { internal float Time; internal Confidence Confidence; internal readonly HashSet Sources = new HashSet(StringComparer.Ordinal); } private readonly float _windowSeconds; private readonly Dictionary> _recent = new Dictionary>(StringComparer.Ordinal); internal CrossSourceDeduplicator(float windowSeconds) { _windowSeconds = windowSeconds; } internal DeduplicationResult Observe(string signature, string source, Confidence confidence, float now) { if (!_recent.TryGetValue(signature, out var value)) { value = new List(); _recent.Add(signature, value); } value.RemoveAll((RecentObservation observation) => now - observation.Time > _windowSeconds); foreach (RecentObservation item in value) { if (!item.Sources.Contains(source) && CanRepresentSameEvent(item.Sources, source)) { item.Sources.Add(source); if (item.Confidence == Confidence.Inferred && confidence == Confidence.Exact) { item.Confidence = confidence; return DeduplicationResult.PromoteToExact; } return DeduplicationResult.Duplicate; } } RecentObservation recentObservation = new RecentObservation { Time = now, Confidence = confidence }; recentObservation.Sources.Add(source); value.Add(recentObservation); if (_recent.Count > 1024) { List list = new List(); foreach (KeyValuePair> item2 in _recent) { item2.Value.RemoveAll((RecentObservation observation) => now - observation.Time > 10f); if (item2.Value.Count == 0) { list.Add(item2.Key); } } foreach (string item3 in list) { _recent.Remove(item3); } } return DeduplicationResult.NewEvent; } private static bool CanRepresentSameEvent(IEnumerable existingSources, string incomingSource) { string text = Channel(incomingSource); foreach (string existingSource in existingSources) { string text2 = Channel(existingSource); if ((text2 == "server-hit" && text == "observer-hit") || (text2 == "observer-hit" && text == "server-hit") || (text2 == "money-authority" && text == "money-observer") || (text2 == "money-observer" && text == "money-authority") || (text2 == "bait" && text == "rod-state") || (text2 == "rod-state" && text == "bait")) { return true; } } return false; } private static string Channel(string source) { switch (source) { case "Server.HitCreature": case "Server.HitPlayer": return "server-hit"; case "Creature.ObserverHit": case "Creature.ObserverExplosionHit": case "PlayerVitals.ObserverHit": return "observer-hit"; case "MoneyManager.AddMoney": case "MoneyManager.RemoveMoney": case "MoneyManager.SellItem": return "money-authority"; case "money_correlation": return "money-observer"; case "Bait.AddItemOnBait": return "bait"; case "Item.OnAttachedRodChange": return "rod-state"; default: return source ?? string.Empty; } } } internal sealed class PendingFishingCatch { internal object Player; internal object Item; internal bool Authoritative; } internal sealed class FishingCatchTracker { private readonly Dictionary _pending = new Dictionary(StringComparer.Ordinal); internal void Begin(string rodKey, object player, object item, bool authoritative) { if (!string.IsNullOrEmpty(rodKey)) { _pending[rodKey] = new PendingFishingCatch { Player = player, Item = item, Authoritative = authoritative }; } } internal bool TryConfirm(string rodKey, out PendingFishingCatch pending) { pending = null; if (string.IsNullOrEmpty(rodKey) || !_pending.TryGetValue(rodKey, out pending)) { return false; } _pending.Remove(rodKey); return true; } internal bool Cancel(string rodKey) { if (!string.IsNullOrEmpty(rodKey)) { return _pending.Remove(rodKey); } return false; } } internal static class FishingLandingRule { internal const float RequiredSecondsAboveWater = 0.2f; internal static bool IsReached(float currentSeconds, float fixedDeltaSeconds) { return currentSeconds + fixedDeltaSeconds >= 0.2f; } } internal sealed class GameIntrospection { private readonly Type _playerType = AccessTools.TypeByName("Player"); internal bool HasPlayerType => _playerType != null; internal IList FindPlayers() { List list = new List(); if (_playerType == null) { return list; } try { Object[] array = Object.FindObjectsOfType(_playerType); foreach (Object val in array) { if (val != (Object)null) { list.Add(val); } } } catch (Exception ex) { Plugin.LogWarning("Player discovery failed: " + ex.Message); } return list; } internal PlayerIdentity GetPlayerIdentity(object player) { if (IsNull(player)) { return null; } ulong num = ToUInt64(GetMember(player, "SteamID")); string text = Convert.ToString(GetMember(player, "SteamName"), CultureInfo.InvariantCulture); string objectId = GetObjectId(player); if (string.IsNullOrWhiteSpace(text)) { text = ((num != 0L) ? ("Steam " + num.ToString(CultureInfo.InvariantCulture)) : ("Player " + objectId)); } return new PlayerIdentity { Key = ((num != 0L) ? ("steam:" + num.ToString(CultureInfo.InvariantCulture)) : ("network:" + objectId)), SteamId = num, Name = text }; } internal object GetPlayerFromVitals(object playerVitals) { return GetMember(playerVitals, "_player") ?? GetMember(playerVitals, "Player"); } internal object GetPlayerFromDying(object playerDying) { return GetMember(playerDying, "_player") ?? GetMember(playerDying, "Player"); } internal object GetFishingRodFromBait(object bait) { return GetMember(bait, "FishingRod") ?? GetMember(bait, "_fishingRod"); } internal object GetPlayerFromBait(object bait) { return GetPlayerFromRod(GetFishingRodFromBait(bait)); } internal object GetPlayerFromRod(object rod) { return GetMember(rod, "Holder") ?? GetMember(rod, "SyncedHolder") ?? GetMember(rod, "LastHolder") ?? GetMember(rod, "LastPlayer"); } internal object GetAttachedRod(object item) { return GetMember(item, "AttachedRod") ?? GetMember(item, "_rodAttachedTo"); } internal object GetItemFromBait(object bait) { return GetMember(bait, "ServerItemOnBait") ?? GetMember(bait, "ItemOnBait"); } internal object GetItemFromRod(object rod) { return GetItemFromBait(GetMember(rod, "Bait") ?? GetMember(rod, "_bait")); } internal bool IsBaitAtLandingThreshold(object bait) { object member = GetMember(bait, "_timeAboveWater"); float currentSeconds; try { currentSeconds = ((member == null) ? 0f : Convert.ToSingle(member, CultureInfo.InvariantCulture)); } catch { return false; } return FishingLandingRule.IsReached(currentSeconds, Time.fixedDeltaTime); } internal bool IsSameObject(object first, object second) { if (IsNull(first) || IsNull(second)) { return false; } if (first != second) { return string.Equals(GetObjectId(first), GetObjectId(second), StringComparison.Ordinal); } return true; } internal bool IsFishItem(object item) { if (IsNull(item)) { return false; } return !IsNull(GetMember(item, "Fish")); } internal int GetItemWorth(object item) { object member = GetMember(item, "TotalWorth"); try { return (member != null) ? Convert.ToInt32(member, CultureInfo.InvariantCulture) : 0; } catch { return 0; } } internal object GetItemLastHolder(object item) { return GetMember(item, "LastHolder") ?? GetMember(item, "LastPlayer") ?? GetMember(item, "Holder"); } internal object GetDeadPlayerHolder(object deadPlayer) { return GetMember(deadPlayer, "Holder") ?? GetMember(deadPlayer, "_holder") ?? GetMember(deadPlayer, "SyncedHolder"); } internal bool IsPlayerDead(object player) { if (IsNull(player)) { return false; } object target = GetMember(player, "Vitals") ?? GetMember(player, "_vitals"); object obj = GetMember(target, "Health") ?? GetMember(target, "_health") ?? GetMember(target, "_syncedHealth"); try { return obj != null && Convert.ToInt32(obj, CultureInfo.InvariantCulture) <= 0; } catch { return false; } } internal bool IsPlayerAlive(object player) { if (IsNull(player)) { return false; } object target = GetMember(player, "Vitals") ?? GetMember(player, "_vitals"); object obj = GetMember(target, "Health") ?? GetMember(target, "_health") ?? GetMember(target, "_syncedHealth"); try { return obj != null && Convert.ToInt32(obj, CultureInfo.InvariantCulture) > 0; } catch { return false; } } internal string GetItemName(object item) { if (IsNull(item)) { return string.Empty; } try { MethodInfo methodInfo = AccessTools.Method(item.GetType(), "GetName", Type.EmptyTypes, (Type[])null); if (methodInfo != null) { string text = Convert.ToString(methodInfo.Invoke(item, null), CultureInfo.InvariantCulture); if (!string.IsNullOrWhiteSpace(text)) { return text; } } } catch { } Object val = (Object)((item is Object) ? item : null); if (val != (Object)null && !string.IsNullOrWhiteSpace(val.name)) { return val.name; } return item.GetType().Name; } internal string GetObjectId(object value) { if (IsNull(value)) { return string.Empty; } object member = GetMember(value, "ObjectId"); if (member != null) { return "net:" + Convert.ToString(member, CultureInfo.InvariantCulture); } Object val = (Object)((value is Object) ? value : null); if (val != (Object)null) { return "unity:" + val.GetInstanceID().ToString(CultureInfo.InvariantCulture); } return "runtime:" + value.GetHashCode().ToString(CultureInfo.InvariantCulture); } internal bool IsServerObject(object value) { object obj = GetMember(value, "IsServerStarted") ?? GetMember(value, "IsServerInitialized"); if (obj is bool) { return (bool)obj; } Type type = AccessTools.TypeByName("FishNet.InstanceFinder"); object obj2 = GetStaticMember(type, "IsServerStarted") ?? GetStaticMember(type, "IsServer"); if (obj2 is bool) { return (bool)obj2; } return false; } internal bool IsOwnedByLocalClient(object player) { object member = GetMember(GetMember(player, "Owner"), "IsLocalClient"); if (member is bool) { return (bool)member; } return false; } internal static bool IsNull(object value) { if (value == null) { return true; } Object val = (Object)((value is Object) ? value : null); if (val != null) { return val == (Object)null; } return false; } private static object GetMember(object target, string name) { return ReflectionMemberAccessor.GetValue(target, name); } private static object GetStaticMember(Type type, string name) { return ReflectionMemberAccessor.GetStaticValue(type, name); } private static ulong ToUInt64(object value) { if (value == null) { return 0uL; } try { return Convert.ToUInt64(value, CultureInfo.InvariantCulture); } catch { return 0uL; } } } internal sealed class HudPanelState { internal MetricKind Metric; internal float X; internal float Y; internal float Width; internal float Height; } internal sealed class HudPanelCollection { internal const int MaximumPanels = 5; internal const float CompactDefaultWidthPixels = 320f; internal const float CompactDefaultHeightPixels = 158f; internal const float CompactDefaultMarginPixels = 12f; private const float ScreenEdge = 0.002f; private const float MinimumPanelSize = 0.07f; private readonly List _panels = new List(); internal IList Panels => _panels; internal int SelectedIndex { get; private set; } internal bool NeedsCompactDefault { get; private set; } internal HudPanelCollection(string serialized) { Parse(serialized); if (_panels.Count == 0) { _panels.Add(DefaultPanel()); NeedsCompactDefault = true; return; } NeedsCompactDefault = IsLegacyDefault(_panels) || IsBrokenStartupDefault(_panels); if (_panels.Count > 1) { PackAttached(); } } internal bool ApplyCompactDefault(float screenWidth, float screenHeight) { if (!NeedsCompactDefault || _panels.Count != 1) { return false; } if (screenWidth < 320f || screenHeight < 158f) { return false; } HudPanelState hudPanelState = _panels[0]; hudPanelState.Width = Math.Max(0.07f, Math.Min(0.8f, 320f / screenWidth)); hudPanelState.Height = Math.Max(0.07f, Math.Min(0.8f, 158f / screenHeight)); hudPanelState.X = Math.Max(0.002f, 1f - hudPanelState.Width - 12f / screenWidth); hudPanelState.Y = Math.Max(0.002f, 12f / screenHeight); NeedsCompactDefault = false; return true; } internal bool Select(int index) { if (index < 0 || index >= _panels.Count) { return false; } SelectedIndex = index; return true; } internal bool SetMetric(int index, MetricKind metric) { if (!Select(index)) { return false; } _panels[index].Metric = MetricCatalog.NormalizeVisible(metric); return true; } internal bool MoveAll(float deltaX, float deltaY) { if (_panels.Count == 0) { return false; } float num = _panels.Min((HudPanelState panel) => panel.X); float num2 = _panels.Min((HudPanelState panel) => panel.Y); float num3 = _panels.Max((HudPanelState panel) => panel.X + panel.Width); float num4 = _panels.Max((HudPanelState panel) => panel.Y + panel.Height); deltaX = Math.Max(0.002f - num, Math.Min(0.998f - num3, deltaX)); deltaY = Math.Max(0.002f - num2, Math.Min(0.998f - num4, deltaY)); if (Math.Abs(deltaX) < 1E-06f && Math.Abs(deltaY) < 1E-06f) { return false; } foreach (HudPanelState panel in _panels) { panel.X += deltaX; panel.Y += deltaY; } return true; } internal bool ResizeAll(float requestedWidth, float requestedHeight) { if (_panels.Count == 0) { return false; } HudPanelState hudPanelState = _panels[0]; float val = 0.998f - hudPanelState.X; float val2 = (0.998f - hudPanelState.Y) / (float)_panels.Count; float width = Math.Max(0.07f, Math.Min(Math.Min(0.8f, val), requestedWidth)); float height = Math.Max(0.07f, Math.Min(Math.Min(0.8f, val2), requestedHeight)); foreach (HudPanelState panel in _panels) { panel.Width = width; panel.Height = height; } PackAttached(); return true; } internal bool AddAfterSelected() { return AddAfter(SelectedIndex); } internal bool AddAfter(int index) { if (_panels.Count >= 5 || index < 0 || index >= _panels.Count) { return false; } HudPanelState hudPanelState = _panels[index]; HudPanelState item = new HudPanelState { Metric = MetricCatalog.Cycle(hudPanelState.Metric, 1), X = hudPanelState.X, Y = hudPanelState.Y + hudPanelState.Height, Width = hudPanelState.Width, Height = hudPanelState.Height }; _panels.Insert(index + 1, item); SelectedIndex = index + 1; FitHeightAndPackAttached(); return true; } internal bool RemoveSelected() { return RemoveNewest(); } internal bool RemoveNewest() { if (_panels.Count <= 1) { return false; } _panels.RemoveAt(_panels.Count - 1); SelectedIndex = Math.Min(SelectedIndex, _panels.Count - 1); PackAttached(); return true; } internal bool Remove(int index) { if (_panels.Count <= 1 || index < 0 || index >= _panels.Count) { return false; } _panels.RemoveAt(index); SelectedIndex = Math.Min(index, _panels.Count - 1); PackAttached(); return true; } internal string Serialize() { return string.Join(";", _panels.Select(delegate(HudPanelState panel) { string[] array = new string[5]; int metric = (int)panel.Metric; array[0] = metric.ToString(CultureInfo.InvariantCulture); array[1] = panel.X.ToString("0.#####", CultureInfo.InvariantCulture); array[2] = panel.Y.ToString("0.#####", CultureInfo.InvariantCulture); array[3] = panel.Width.ToString("0.#####", CultureInfo.InvariantCulture); array[4] = panel.Height.ToString("0.#####", CultureInfo.InvariantCulture); return string.Join(",", array); }).ToArray()); } private void Parse(string serialized) { if (string.IsNullOrWhiteSpace(serialized)) { return; } string[] array = serialized.Split(new char[1] { ';' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split(new char[1] { ',' }); if ((array2.Length == 5 || array2.Length == 7) && int.TryParse(array2[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) && float.TryParse(array2[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var result2) && float.TryParse(array2[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var result3) && float.TryParse(array2[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var result4) && float.TryParse(array2[4], NumberStyles.Float, CultureInfo.InvariantCulture, out var result5) && MetricCatalog.IsDefined(result)) { MetricKind metric = MetricCatalog.NormalizeVisible((MetricKind)result); _panels.Add(new HudPanelState { Metric = metric, X = Clamp01(result2), Y = Clamp01(result3), Width = Math.Max(0.07f, Math.Min(0.8f, result4)), Height = Math.Max(0.07f, Math.Min(0.8f, result5)) }); if (_panels.Count == 5) { break; } } } } private static HudPanelState DefaultPanel() { return new HudPanelState { Metric = MetricKind.CreatureDamage, X = 0.765f, Y = 0.02f, Width = 0.22f, Height = 0.3f }; } private void FitHeightAndPackAttached() { if (_panels.Count == 0) { return; } float val = (0.998f - _panels[0].Y) / (float)_panels.Count; float height = Math.Max(0.07f, Math.Min(_panels[0].Height, val)); foreach (HudPanelState panel in _panels) { panel.Height = height; } PackAttached(); } private void PackAttached() { if (_panels.Count != 0) { float x = _panels[0].X; float num = _panels.Sum((HudPanelState panel) => panel.Height); float num2 = Math.Max(0.002f, Math.Min(_panels[0].Y, 0.998f - num)); for (int num3 = 0; num3 < _panels.Count; num3++) { _panels[num3].X = x; _panels[num3].Y = num2; num2 += _panels[num3].Height; } } } private static bool IsLegacyDefault(IList panels) { if (panels.Count != 1) { return false; } HudPanelState hudPanelState = panels[0]; if (hudPanelState.Metric == MetricKind.CreatureDamage && Approximately(hudPanelState.X, 0.765f) && Approximately(hudPanelState.Y, 0.02f) && Approximately(hudPanelState.Width, 0.22f)) { return Approximately(hudPanelState.Height, 0.3f); } return false; } private static bool IsBrokenStartupDefault(IList panels) { if (panels.Count != 1) { return false; } HudPanelState hudPanelState = panels[0]; if (hudPanelState.Metric == MetricKind.CreatureDamage && Approximately(hudPanelState.X, 0.002f) && Approximately(hudPanelState.Y, 1f) && Approximately(hudPanelState.Width, 0.8f)) { return Approximately(hudPanelState.Height, 0.8f); } return false; } private static bool Approximately(float first, float second) { return Math.Abs(first - second) < 1E-05f; } private static float Clamp01(float value) { return Math.Max(0f, Math.Min(1f, value)); } } internal static class MetricCatalog { internal static readonly MetricKind[] Visible = new MetricKind[8] { MetricKind.CreatureDamage, MetricKind.PlayerDamage, MetricKind.MoneyEarned, MetricKind.MoneySpent, MetricKind.FishCaught, MetricKind.TeammatesRevived, MetricKind.PlayerKills, MetricKind.PlayerDeaths }; internal static MetricKind NormalizeVisible(MetricKind metric) { if (IndexOf(metric) < 0) { return MetricKind.PlayerDeaths; } return metric; } internal static MetricKind Cycle(MetricKind current, int delta) { int num = IndexOf(NormalizeVisible(current)); int num2 = Visible.Length; return Visible[(num + delta % num2 + num2) % num2]; } internal static bool IsDefined(int value) { return Enum.IsDefined(typeof(MetricKind), value); } private static int IndexOf(MetricKind metric) { for (int i = 0; i < Visible.Length; i++) { if (Visible[i] == metric) { return i; } } return -1; } } internal enum MetricKind { CreatureDamage, PlayerDamage, MoneyEarned, MoneySpent, FishCaught, TimesRevived, TeammatesRevived, PlayerKills, PlayerDeaths } internal enum Confidence { Exact, Inferred, Corrected, Unknown } internal sealed class PlayerIdentity { internal string Key; internal ulong SteamId; internal string Name; } internal sealed class MetricValue { internal long Exact; internal long Inferred; internal long Total => Exact + Inferred; } internal sealed class PlayerStats { internal string Key; internal ulong SteamId; internal string Name; internal bool IsPresent; internal DateTime LastSeenUtc; internal readonly Dictionary Metrics = new Dictionary(); internal PlayerStats() { foreach (MetricKind value in Enum.GetValues(typeof(MetricKind))) { Metrics[value] = new MetricValue(); } } } internal sealed class PlayerMetricRow { internal string Key; internal ulong SteamId; internal string Name; internal bool IsPresent; internal long Exact; internal long Inferred; internal long Total => Exact + Inferred; } internal sealed class PatchRegistrar { private readonly Harmony _harmony; private readonly List _missing = new List(); private int _patched; internal PatchRegistrar(Harmony harmony) { _harmony = harmony; } internal string ApplyAll() { PatchPrefix("Creature", "RpcLogic___ObserverHit___", "CreatureObserverHit", required: true); PatchPrefix("Creature", "RpcLogic___ObserverExplosionHit___", "CreatureObserverExplosionHit", required: true); PatchPrefix("PlayerVitals", "RpcLogic___ObserverHit___", "PlayerObserverHit", required: true); PatchPrefix("Server", "RpcLogic___HitCreature___", "ServerHitCreature", required: false); PatchNamePrefixWithState("Server", "RpcLogic___HitPlayer___", "ServerHitPlayerPrefix", "ServerHitPlayerPostfix", required: false); PatchExact("MoneyManager", "AddMoney", 2, "AddMoney", required: false); PatchExact("MoneyManager", "RemoveMoney", 2, "RemoveMoney", required: false); PatchExact("MoneyManager", "SellItem", 1, "SellItem", required: true); PatchExact("MoneyManager", "OnChangeMoney", 3, "MoneyChanged", required: false); PatchPrefix("MoneyManager", "RpcLogic___ObserverMoneySound___", "MoneyCue", required: false); PatchExact("CreatureManager", "HookItem", 2, "CreatureManagerHookItem", required: true); PatchExact("Item", "InitializeBait", 1, "ItemInitializeBait", required: false); PatchExactPrefix("Bait", "IncreaseAboveWaterTime", 0, "BaitIncreaseAboveWaterTime", required: true); PatchExactPrefix("Item", "PickUp", 3, "ItemPickUp", required: false); PatchExactPrefix("FishingRod", "ReleaseItem", 1, "FishingRodReleaseItem", required: true); PatchNamePrefixAsPrefix("Server", "RpcLogic___ResurrectPlayer___", "ServerResurrectPlayer", required: true); PatchExact("PlayerDying", "ServerDie", 1, "PlayerDied", required: true); if (_missing.Count != 0) { return _patched + " collectors active; missing: " + string.Join(", ", _missing.ToArray()); } return "all " + _patched + " collectors active"; } private void PatchPrefix(string typeName, string methodPrefix, string callback, bool required) { Type type = AccessTools.TypeByName(typeName); MethodInfo[] array = ((type == null) ? new MethodInfo[0] : (from method in type.GetMethods(AccessTools.all) where method.Name.StartsWith(methodPrefix, StringComparison.Ordinal) select method).ToArray()); if (array.Length == 0) { Missing(typeName + "." + methodPrefix, required); return; } MethodInfo[] array2 = array; foreach (MethodInfo target in array2) { Patch(target, callback); } } private void PatchExact(string typeName, string methodName, int parameterCount, string callback, bool required) { Type type = AccessTools.TypeByName(typeName); MethodInfo methodInfo = ((type == null) ? null : type.GetMethods(AccessTools.all).FirstOrDefault((MethodInfo candidate) => candidate.Name == methodName && candidate.GetParameters().Length == parameterCount)); if (methodInfo == null) { Missing(typeName + "." + methodName, required); } else { Patch(methodInfo, callback); } } private void PatchExactPrefix(string typeName, string methodName, int parameterCount, string callback, bool required) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Expected O, but got Unknown Type type = AccessTools.TypeByName(typeName); MethodInfo methodInfo = ((type == null) ? null : type.GetMethods(AccessTools.all).FirstOrDefault((MethodInfo candidate) => candidate.Name == methodName && candidate.GetParameters().Length == parameterCount)); if (methodInfo == null) { Missing(typeName + "." + methodName, required); return; } try { MethodInfo methodInfo2 = AccessTools.Method(typeof(PatchCallbacks), callback, (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched++; Plugin.LogDebug("Patched " + methodInfo.DeclaringType.FullName + "." + methodInfo.Name + " (prefix)"); } catch (Exception ex) { _missing.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name); Plugin.LogWarning("Patch failed for " + methodInfo.Name + ": " + ex.Message); } } private void PatchNamePrefixAsPrefix(string typeName, string methodPrefix, string callback, bool required) { //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Expected O, but got Unknown Type type = AccessTools.TypeByName(typeName); MethodInfo[] array = ((type == null) ? new MethodInfo[0] : (from method in type.GetMethods(AccessTools.all) where method.Name.StartsWith(methodPrefix, StringComparison.Ordinal) select method).ToArray()); if (array.Length == 0) { Missing(typeName + "." + methodPrefix, required); return; } MethodInfo[] array2 = array; foreach (MethodInfo methodInfo in array2) { try { MethodInfo methodInfo2 = AccessTools.Method(typeof(PatchCallbacks), callback, (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched++; Plugin.LogDebug("Patched " + methodInfo.DeclaringType.FullName + "." + methodInfo.Name + " (prefix)"); } catch (Exception ex) { _missing.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name); Plugin.LogWarning("Patch failed for " + methodInfo.Name + ": " + ex.Message); } } } private void PatchNamePrefixWithState(string typeName, string methodPrefix, string prefixCallback, string postfixCallback, bool required) { //IL_00a8: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Expected O, but got Unknown //IL_00bc: Expected O, but got Unknown Type type = AccessTools.TypeByName(typeName); MethodInfo[] array = ((type == null) ? new MethodInfo[0] : (from method in type.GetMethods(AccessTools.all) where method.Name.StartsWith(methodPrefix, StringComparison.Ordinal) select method).ToArray()); if (array.Length == 0) { Missing(typeName + "." + methodPrefix, required); return; } MethodInfo[] array2 = array; foreach (MethodInfo methodInfo in array2) { try { MethodInfo methodInfo2 = AccessTools.Method(typeof(PatchCallbacks), prefixCallback, (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(PatchCallbacks), postfixCallback, (Type[])null, (Type[])null); _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo2), new HarmonyMethod(methodInfo3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched++; Plugin.LogDebug("Patched " + methodInfo.DeclaringType.FullName + "." + methodInfo.Name + " (state transition)"); } catch (Exception ex) { _missing.Add(methodInfo.DeclaringType.FullName + "." + methodInfo.Name); Plugin.LogWarning("Patch failed for " + methodInfo.Name + ": " + ex.Message); } } } private void Patch(MethodInfo target, string callback) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(typeof(PatchCallbacks), callback, (Type[])null, (Type[])null); _harmony.Patch((MethodBase)target, (HarmonyMethod)null, new HarmonyMethod(methodInfo), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); _patched++; Plugin.LogDebug("Patched " + target.DeclaringType.FullName + "." + target.Name); } catch (Exception ex) { _missing.Add(target.DeclaringType.FullName + "." + target.Name); Plugin.LogWarning("Patch failed for " + target.Name + ": " + ex.Message); } } private void Missing(string name, bool required) { _missing.Add(name + (required ? " (required)" : string.Empty)); Plugin.LogWarning("Collector entry not found: " + name); } } internal static class PatchCallbacks { internal static void CreatureObserverHit(object __instance, object[] __args) { Safe(delegate { Plugin.Current.Ledger.RecordCreatureDamage(Arg(__args, 0), __instance, IntArg(__args, 3), "Creature.ObserverHit", Confidence.Exact); }); } internal static void CreatureObserverExplosionHit(object __instance, object[] __args) { Safe(delegate { Plugin.Current.Ledger.RecordCreatureDamage(Arg(__args, 0), __instance, IntArg(__args, 2), "Creature.ObserverExplosionHit", Confidence.Exact); }); } internal static void PlayerObserverHit(object __instance, object[] __args) { Safe(delegate { object playerFromVitals = Plugin.Current.Game.GetPlayerFromVitals(__instance); Plugin.Current.Ledger.RecordPlayerDamage(Arg(__args, 0), playerFromVitals, IntArg(__args, 3), "PlayerVitals.ObserverHit", Confidence.Exact, Convert.ToString(Arg(__args, 4))); }); } internal static void ServerHitCreature(object __instance, object[] __args) { Safe(delegate { Plugin.Current.Ledger.RecordCreatureDamage(Arg(__args, 1), Arg(__args, 0), IntArg(__args, 2), "Server.HitCreature", Confidence.Exact); }); } internal static void ServerHitPlayerPrefix(object[] __args, ref PlayerHitState __state) { try { object obj = Arg(__args, 0); __state = new PlayerHitState { Victim = obj, Attacker = Arg(__args, 5), WasAlive = ((Object)(object)Plugin.Current != (Object)null && Plugin.Current.Game != null && Plugin.Current.Game.IsPlayerAlive(obj)) }; } catch (Exception ex) { __state = null; Plugin.LogWarning("Player kill transition prefix failed: " + ex.Message); } } internal static void ServerHitPlayerPostfix(object __instance, object[] __args, PlayerHitState __state) { Safe(delegate { Plugin.Current.Ledger.RecordPlayerDamage(Arg(__args, 5), Arg(__args, 0), IntArg(__args, 1), "Server.HitPlayer", Confidence.Exact, Convert.ToString(Arg(__args, 4))); bool hasAttacker = __state != null && !GameIntrospection.IsNull(__state.Attacker); bool isDeadAfter = __state != null && Plugin.Current.Game.IsPlayerDead(__state.Victim); if (__state != null && PlayerKillTransitionRule.IsPlayerKill(__state.WasAlive, isDeadAfter, hasAttacker)) { Plugin.Current.Ledger.RecordPlayerKill(__state.Attacker, __state.Victim, "Server.HitPlayerLethalTransition"); } }); } internal static void AddMoney(object __instance, object[] __args) { Safe(delegate { if (Plugin.Current.Game.IsServerObject(__instance)) { Plugin.Current.Ledger.RecordAuthoritativeMoney(Arg(__args, 1), Math.Abs(IntArg(__args, 0)), increase: true, "MoneyManager.AddMoney"); } }); } internal static void RemoveMoney(object __instance, object[] __args) { Safe(delegate { if (Plugin.Current.Game.IsServerObject(__instance)) { Plugin.Current.Ledger.RecordAuthoritativeMoney(Arg(__args, 1), Math.Abs(IntArg(__args, 0)), increase: false, "MoneyManager.RemoveMoney"); } }); } internal static void SellItem(object __instance, object[] __args) { Safe(delegate { object item = Arg(__args, 0); if (Plugin.Current.Game.IsServerObject(__instance)) { Plugin.Current.Ledger.RecordAuthoritativeMoney(Plugin.Current.Game.GetItemLastHolder(item), Math.Abs(Plugin.Current.Game.GetItemWorth(item)), increase: true, "MoneyManager.SellItem"); } }); } internal static void MoneyChanged(object __instance, object[] __args) { Safe(delegate { Plugin.Current.Ledger.ObserveBalanceChange(IntArg(__args, 0), IntArg(__args, 1), BoolArg(__args, 2)); }); } internal static void MoneyCue(object __instance, object[] __args) { Safe(delegate { Plugin.Current.Ledger.ObserveMoneyCue(BoolArg(__args, 0), Arg(__args, 1)); }); } internal static void CreatureManagerHookItem(object __instance, object[] __args) { Safe(delegate { object bait = Arg(__args, 1); if (Plugin.Current.Game.IsServerObject(__instance)) { object fishingRodFromBait = Plugin.Current.Game.GetFishingRodFromBait(bait); object item = Plugin.Current.Game.GetItemFromBait(bait) ?? Arg(__args, 0); Plugin.Current.Ledger.BeginFishingCatch(Plugin.Current.Game.GetPlayerFromRod(fishingRodFromBait), item, fishingRodFromBait, authoritative: true); } }); } internal static void ItemInitializeBait(object __instance, object[] __args) { Safe(delegate { object rod = Arg(__args, 0); object playerFromRod = Plugin.Current.Game.GetPlayerFromRod(rod); if (!Plugin.Current.Game.IsServerObject(__instance) && Plugin.Current.Game.IsOwnedByLocalClient(playerFromRod)) { Plugin.Current.Ledger.BeginFishingCatch(playerFromRod, __instance, rod, authoritative: false); } }); } internal static void BaitIncreaseAboveWaterTime(object __instance) { Safe(delegate { if (Plugin.Current.Game.IsBaitAtLandingThreshold(__instance)) { object fishingRodFromBait = Plugin.Current.Game.GetFishingRodFromBait(__instance); Plugin.Current.Ledger.ConfirmFishingCatch(fishingRodFromBait, Plugin.Current.Game.GetItemFromBait(__instance), "Bait.AboveWaterLanding"); } }); } internal static void ItemPickUp(object __instance, object[] __args) { Safe(delegate { object attachedRod = Plugin.Current.Game.GetAttachedRod(__instance); if (!GameIntrospection.IsNull(attachedRod)) { Plugin.Current.Ledger.ConfirmFishingCatch(attachedRod, __instance, "Item.PickUpWhileHooked"); } }); } internal static void FishingRodReleaseItem(object __instance, object[] __args) { Safe(delegate { object first = Arg(__args, 0); object itemFromRod = Plugin.Current.Game.GetItemFromRod(__instance); if (Plugin.Current.Game.IsSameObject(first, itemFromRod)) { Plugin.Current.Ledger.CancelFishingCatch(__instance); } }); } internal static void ServerResurrectPlayer(object __instance, object[] __args) { Safe(delegate { if (Plugin.Current.Game.IsServerObject(__instance)) { Plugin.Current.Ledger.RecordResurrection(Arg(__args, 0), Arg(__args, 1), "Server.ResurrectPlayer"); } }); } internal static void PlayerDied(object __instance) { Safe(delegate { if (Plugin.Current.Game.IsServerObject(__instance)) { Plugin.Current.Ledger.RecordDeath(Plugin.Current.Game.GetPlayerFromDying(__instance), "PlayerDying.ServerDie"); } }); } private static object Arg(object[] args, int index) { if (args == null || index < 0 || index >= args.Length) { return null; } return args[index]; } private static int IntArg(object[] args, int index) { object obj = Arg(args, index); try { return (obj != null) ? Convert.ToInt32(obj) : 0; } catch { return 0; } } private static bool BoolArg(object[] args, int index) { object obj = Arg(args, index); if (obj is bool) { return (bool)obj; } return false; } private static void Safe(Action action) { try { if ((Object)(object)Plugin.Current != (Object)null && Plugin.Current.Ledger != null) { action(); } } catch (Exception ex) { Plugin.LogWarning("Collector callback failed: " + ex.Message); } } } internal sealed class PlayerHitState { internal object Victim; internal object Attacker; internal bool WasAlive; } internal sealed class PlayerColorAssigner { private readonly Dictionary _assignments = new Dictionary(StringComparer.Ordinal); private int _nextIndex; internal int GetIndex(string playerKey) { playerKey = playerKey ?? string.Empty; if (_assignments.TryGetValue(playerKey, out var value)) { return value; } int num = _nextIndex++; _assignments[playerKey] = num; return num; } } internal static class PlayerKillTransitionRule { internal static bool IsPlayerKill(bool wasAlive, bool isDeadAfter, bool hasAttacker) { return wasAlive && isDeadAfter && hasAttacker; } } [BepInPlugin("com.corntaxi.howtofish.checkmystats", "CheckMyStats", "1.0.0")] [BepInProcess("How to Fish.exe")] public sealed class Plugin : BaseUnityPlugin { internal const string PluginGuid = "com.corntaxi.howtofish.checkmystats"; internal const string PluginName = "CheckMyStats"; internal const string PluginVersion = "1.0.0"; private const string ExpectedAssemblyHash = "FA8C6F47874E69FE07B9C978F35CC05372DF2BDD3535DE5F5FAC355F999A5762"; private Harmony _harmony; private StatsHud _hud; private ConfigEntry _toggleHud; private ConfigEntry _toggleMouseEdit; private ConfigEntry _addPanel; private ConfigEntry _removePanel; private ConfigEntry _hudEnabled; private ConfigEntry _language; private ConfigEntry _hudScale; private ConfigEntry _panelLayout; private ConfigFile _settings; private float _nextDiscovery; private bool _shutdown; private StatsSyncService _sync; internal static Plugin Current { get; private set; } internal static ManualLogSource LogSource { get; private set; } internal GameIntrospection Game { get; private set; } internal StatsLedger Ledger { get; private set; } internal string CompatibilitySummary { get; private set; } internal bool IsFullyCompatible { get; private set; } private void Awake() { //IL_008e: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Expected O, but got Unknown Current = this; LogSource = ((BaseUnityPlugin)this).Logger; BindConfiguration(); string sessionId = DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture) + "-" + Guid.NewGuid().ToString("N").Substring(0, 8); Game = new GameIntrospection(); Ledger = new StatsLedger(Game, sessionId); _sync = new StatsSyncService(Ledger); string text = VerifyGameAssembly(); _harmony = new Harmony("com.corntaxi.howtofish.checkmystats"); string text2 = new PatchRegistrar(_harmony).ApplyAll(); CompatibilitySummary = text + "; " + text2; IsFullyCompatible = text.StartsWith("verified", StringComparison.OrdinalIgnoreCase) && text2.StartsWith("all", StringComparison.OrdinalIgnoreCase); _hud = new StatsHud(Ledger, UseChinese, () => _hudScale.Value, _panelLayout.Value, SaveHudLayout); ((BaseUnityPlugin)this).Logger.LogInfo((object)("CheckMyStats 1.0.0 loaded. " + CompatibilitySummary)); } private void BindConfiguration() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Expected O, but got Unknown //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_0065: 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_00c3: Unknown result type (might be due to invalid IL or missing references) _settings = new ConfigFile(Path.Combine(Paths.ConfigPath, "CheckMyStats.cfg"), true); _toggleMouseEdit = _settings.Bind("Controls", "MouseEditKey", new KeyboardShortcut((KeyCode)287, Array.Empty()), "Toggle mouse editing, dragging, and resizing."); _toggleHud = _settings.Bind("Controls", "ToggleHudKey", new KeyboardShortcut((KeyCode)288, Array.Empty()), "Show or hide all HUD panels."); _addPanel = _settings.Bind("Controls", "AddPanelKey", new KeyboardShortcut((KeyCode)270, Array.Empty()), "Add a HUD panel. Shift+= is always also supported."); _removePanel = _settings.Bind("Controls", "RemovePanelKey", new KeyboardShortcut((KeyCode)269, Array.Empty()), "Remove the newest HUD panel. Main keyboard minus is also supported."); _hudEnabled = _settings.Bind("HUD", "Enabled", true, "Show the multiplayer statistics HUD."); _hudScale = _settings.Bind("HUD", "Scale", 1f, "HUD content scale from 0.75 to 2.0."); _language = _settings.Bind("HUD", "Language", "Auto", "Auto, English, or Chinese."); _panelLayout = _settings.Bind("HUD", "PanelLayout", string.Empty, "Saved metric, position, and size for up to five panels."); _settings.Save(); } 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_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) KeyboardShortcut value = _addPanel.Value; if (((KeyboardShortcut)(ref value)).IsDown() || MainKeyboardPlusDown()) { _hud.AddPanel(); } value = _removePanel.Value; if (((KeyboardShortcut)(ref value)).IsDown() || Input.GetKeyDown((KeyCode)45)) { _hud.RemoveSelectedPanel(); } value = _toggleMouseEdit.Value; if (((KeyboardShortcut)(ref value)).IsDown() && _hudEnabled.Value) { _hud.SetEditMode(!_hud.EditMode); } value = _toggleHud.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { _hudEnabled.Value = !_hudEnabled.Value; if (!_hudEnabled.Value) { _hud.SetEditMode(enabled: false); } } Ledger.Tick(); if (_sync != null) { _sync.Tick(); } if (Time.unscaledTime >= _nextDiscovery) { _nextDiscovery = Time.unscaledTime + 1f; Ledger.RefreshPlayers(Game.FindPlayers()); } } private void LateUpdate() { if (_hud != null) { _hud.MaintainCursor(); } } private void OnGUI() { if (_hudEnabled != null && _hudEnabled.Value && _hud != null) { _hud.Draw(IsFullyCompatible, CompatibilitySummary); } } private void OnApplicationQuit() { Shutdown(); } private void OnDestroy() { Shutdown(); } private void Shutdown() { if (!_shutdown) { _shutdown = true; if (_hud != null) { _hud.Shutdown(); } if (_sync != null) { _sync.Shutdown(); } if (Ledger != null) { Ledger.FlushPending(); } if (_harmony != null) { _harmony.UnpatchSelf(); } } } private bool MainKeyboardPlusDown() { if (Input.GetKeyDown((KeyCode)61)) { if (!Input.GetKey((KeyCode)304)) { return Input.GetKey((KeyCode)303); } return true; } return false; } private void SaveHudLayout(string serialized) { if (_panelLayout != null && !string.Equals(_panelLayout.Value, serialized, StringComparison.Ordinal)) { _panelLayout.Value = serialized; if (_settings != null) { _settings.Save(); } } } private string VerifyGameAssembly() { try { using FileStream inputStream = File.OpenRead(Path.Combine(Paths.GameRootPath, "How to Fish_Data", "Managed", "Assembly-CSharp.dll")); using SHA256 sHA = SHA256.Create(); string text = BitConverter.ToString(sHA.ComputeHash(inputStream)).Replace("-", string.Empty); return string.Equals(text, "FA8C6F47874E69FE07B9C978F35CC05372DF2BDD3535DE5F5FAC355F999A5762", StringComparison.OrdinalIgnoreCase) ? ("verified Assembly-CSharp " + text.Substring(0, 12)) : ("unverified Assembly-CSharp " + text.Substring(0, 12)); } catch (Exception ex) { return "assembly verification failed: " + ex.Message; } } private bool UseChinese() { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Invalid comparison between Unknown and I4 //IL_0069: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Invalid comparison between Unknown and I4 string text = ((_language == null) ? "Auto" : _language.Value) ?? "Auto"; if (text.Equals("Chinese", StringComparison.OrdinalIgnoreCase) || text.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) { return true; } if (text.Equals("English", StringComparison.OrdinalIgnoreCase) || text.StartsWith("en", StringComparison.OrdinalIgnoreCase)) { return false; } if ((int)Application.systemLanguage != 40) { return (int)Application.systemLanguage == 41; } return true; } internal static void LogWarning(string message) { if (LogSource != null) { LogSource.LogWarning((object)message); } } internal static void LogInfo(string message) { if (LogSource != null) { LogSource.LogInfo((object)message); } } internal static void LogDebug(string message) { if (LogSource != null) { LogSource.LogDebug((object)message); } } } internal static class ReflectionMemberAccessor { private const BindingFlags InstanceFlags = BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; private const BindingFlags StaticFlags = BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; private static readonly Dictionary Cache = new Dictionary(StringComparer.Ordinal); private static readonly object CacheLock = new object(); internal static object GetValue(object target, string name) { if (target != null) { return Read(target.GetType(), target, name, isStatic: false, unwrapSyncVar: true); } return null; } internal static object GetStaticValue(Type type, string name) { if (!(type == null)) { return Read(type, null, name, isStatic: true, unwrapSyncVar: false); } return null; } private static object Read(Type type, object target, string name, bool isStatic, bool unwrapSyncVar) { try { MemberInfo memberInfo = Find(type, name, isStatic); PropertyInfo propertyInfo = memberInfo as PropertyInfo; object obj; if (propertyInfo != null) { obj = propertyInfo.GetValue(target, null); } else { FieldInfo fieldInfo = memberInfo as FieldInfo; obj = ((fieldInfo == null) ? null : fieldInfo.GetValue(target)); } return unwrapSyncVar ? UnwrapSyncVar(obj) : obj; } catch { return null; } } private static MemberInfo Find(Type type, string name, bool isStatic) { string key = type.AssemblyQualifiedName + "|" + (isStatic ? "S|" : "I|") + name; lock (CacheLock) { if (Cache.TryGetValue(key, out var value)) { return value; } } BindingFlags bindingAttr = (isStatic ? (BindingFlags.DeclaredOnly | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) : (BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)); MemberInfo memberInfo = null; Type type2 = type; while (type2 != null && memberInfo == null) { PropertyInfo property = type2.GetProperty(name, bindingAttr); memberInfo = ((!(property != null) || property.GetIndexParameters().Length != 0) ? ((MemberInfo)type2.GetField(name, bindingAttr)) : ((MemberInfo)property)); type2 = type2.BaseType; } lock (CacheLock) { Cache[key] = memberInfo; return memberInfo; } } private static object UnwrapSyncVar(object value) { if (value == null) { return null; } Type type = value.GetType(); if (!type.IsGenericType || !type.Name.StartsWith("SyncVar`", StringComparison.Ordinal) || !string.Equals(type.Namespace, "FishNet.Object.Synchronizing", StringComparison.Ordinal)) { return value; } return Read(type, value, "Value", isStatic: false, unwrapSyncVar: false); } } internal sealed class StatsHud { private readonly StatsLedger _ledger; private readonly Func _useChinese; private readonly Func _scale; private readonly Action _saveLayout; private readonly HudPanelCollection _panels; private readonly PlayerColorAssigner _playerColors = new PlayerColorAssigner(); private GUIStyle _panel; private GUIStyle _selectedPanel; private GUIStyle _title; private GUIStyle _label; private GUIStyle _value; private GUIStyle _small; private Texture2D _background; private Texture2D _selectedBackground; private int _dragging = -1; private int _resizing = -1; private int _metricMenuPanel = -1; private Vector2 _metricMenuPosition; private CursorLockMode _previousCursorLock; private bool _previousCursorVisible; private bool _cursorCaptured; internal bool EditMode { get; private set; } internal StatsHud(StatsLedger ledger, Func useChinese, Func scale, string layout, Action saveLayout) { _ledger = ledger; _useChinese = useChinese; _scale = scale; _saveLayout = saveLayout; _panels = new HudPanelCollection(layout); if (_panels.ApplyCompactDefault(Screen.width, Screen.height) || _panels.Panels.Count > 1) { SaveLayout(); } } internal void AddPanel() { if (_panels.AddAfterSelected()) { SaveLayout(); } } internal void RemoveSelectedPanel() { if (_panels.RemoveSelected()) { SaveLayout(); } } internal void SetEditMode(bool enabled) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) if (EditMode != enabled) { EditMode = enabled; _dragging = -1; _resizing = -1; _metricMenuPanel = -1; if (enabled) { _previousCursorLock = Cursor.lockState; _previousCursorVisible = Cursor.visible; _cursorCaptured = true; MaintainCursor(); } else { RestoreCursor(); SaveLayout(); } } } internal void MaintainCursor() { if (EditMode) { Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; } } internal void Shutdown() { SetEditMode(enabled: false); } internal void Draw(bool compatible, string compatibilitySummary) { //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_008d: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) if (_panels.ApplyCompactDefault(Screen.width, Screen.height)) { SaveLayout(); } EnsureStyles(); float scale = Mathf.Clamp(_scale(), 0.75f, 2f); UpdateFontSizes(scale); HandleActiveManipulation(); int addAfter = -1; int remove = -1; int count = _panels.Panels.Count; for (int i = 0; i < count; i++) { HudPanelState hudPanelState = _panels.Panels[i]; Rect rect = ToPixels(hudPanelState); ClampRect(ref rect); FromPixels(hudPanelState, rect); DrawPanel(i, hudPanelState, rect, scale, compatible, compatibilitySummary, ref addAfter, ref remove); } if (remove >= 0 && _panels.Remove(remove)) { SaveLayout(); } if (addAfter >= 0 && _panels.AddAfter(addAfter)) { SaveLayout(); } DrawMetricMenu(scale); } private void DrawPanel(int index, HudPanelState state, Rect rect, float scale, bool compatible, string compatibilitySummary, ref int addAfter, ref int remove) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0117: 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_0156: Unknown result type (might be due to invalid IL or missing references) //IL_019f: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_02a2: Unknown result type (might be due to invalid IL or missing references) //IL_0382: Unknown result type (might be due to invalid IL or missing references) //IL_0324: Unknown result type (might be due to invalid IL or missing references) //IL_03ab: Unknown result type (might be due to invalid IL or missing references) //IL_03ae: Unknown result type (might be due to invalid IL or missing references) //IL_03b3: Unknown result type (might be due to invalid IL or missing references) //IL_03b5: Unknown result type (might be due to invalid IL or missing references) //IL_03c9: Unknown result type (might be due to invalid IL or missing references) //IL_03ca: Unknown result type (might be due to invalid IL or missing references) //IL_03cc: Unknown result type (might be due to invalid IL or missing references) if (EditMode && index == _panels.SelectedIndex) { GUI.Box(new Rect(((Rect)(ref rect)).x - 2f, ((Rect)(ref rect)).y - 2f, ((Rect)(ref rect)).width + 4f, ((Rect)(ref rect)).height + 4f), GUIContent.none, _selectedPanel); } GUI.Box(rect, GUIContent.none, _panel); float num = 12f * scale; float num2 = 30f * scale; float num3 = 27f * scale; Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref rect)).x + num, ((Rect)(ref rect)).y + 8f * scale, ((Rect)(ref rect)).width - num * 2f, num2); Rect val2 = default(Rect); ((Rect)(ref val2))..ctor(((Rect)(ref val)).xMax - num3 * 2f - 3f * scale, ((Rect)(ref val)).y, num3, ((Rect)(ref val)).height); Rect val3 = default(Rect); ((Rect)(ref val3))..ctor(((Rect)(ref val2)).xMax + 3f * scale, ((Rect)(ref val)).y, num3, ((Rect)(ref val)).height); Rect val4 = (Rect)(EditMode ? new Rect(((Rect)(ref val)).x, ((Rect)(ref val)).y, Math.Max(40f, ((Rect)(ref val2)).x - ((Rect)(ref val)).x - 5f * scale), ((Rect)(ref val)).height) : val); if (EditMode) { if (GUI.Button(val2, "+")) { addAfter = index; } if (GUI.Button(val3, "−")) { remove = index; } GUI.Label(val4, MetricName(state.Metric), _title); } else { GUI.Label(val, MetricName(state.Metric), _title); } IList rows = _ledger.GetRows(state.Metric); float num4 = 27f * scale; float num5 = ((Rect)(ref val)).yMax + 4f * scale; if (!compatible) { Rect val5 = default(Rect); ((Rect)(ref val5))..ctor(((Rect)(ref rect)).x + num, ((Rect)(ref val)).yMax, ((Rect)(ref rect)).width - num * 2f, 20f * scale); GUI.Label(val5, WarningText(compatibilitySummary), WarningStyle()); num5 = ((Rect)(ref val5)).yMax + 4f * scale; } float num6 = (EditMode ? (22f * scale) : (8f * scale)); int num7 = Math.Max(0, Mathf.FloorToInt((((Rect)(ref rect)).yMax - num6 - num5) / num4)); if (rows.Count == 0 && num7 > 0) { GUI.Label(new Rect(((Rect)(ref rect)).x + num, num5, ((Rect)(ref rect)).width - num * 2f, num4), _useChinese() ? "等待玩家对象……" : "Waiting for player objects…", _label); } else { long max = ((rows.Count == 0) ? 1 : Math.Max(1L, rows[0].Total)); for (int i = 0; i < rows.Count && i < num7; i++) { DrawRow(rows[i], max, new Rect(((Rect)(ref rect)).x + num, num5 + (float)i * num4, ((Rect)(ref rect)).width - num * 2f, num4), scale); } } if (EditMode) { GUI.Label(new Rect(((Rect)(ref rect)).x + num, ((Rect)(ref rect)).yMax - 20f * scale, ((Rect)(ref rect)).width - num * 2f, 17f * scale), _useChinese() ? "右键标题选指标|拖动/缩放全部框" : "Right-click title | move/resize all", _small); Rect val6 = ResizeHandle(rect, scale); GUI.Label(val6, "◢", _value); HandlePointerDown(index, rect, val4, val6); } } private void DrawRow(PlayerMetricRow row, long max, Rect rowRect, float scale) { //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_0102: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0175: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_018e: Unknown result type (might be due to invalid IL or missing references) float num = ((Rect)(ref rowRect)).width * 0.4f; float num2 = 76f * scale; Rect val = default(Rect); ((Rect)(ref val))..ctor(((Rect)(ref rowRect)).x, ((Rect)(ref rowRect)).y, num, ((Rect)(ref rowRect)).height); Rect val2 = default(Rect); ((Rect)(ref val2))..ctor(((Rect)(ref rowRect)).xMax - num2, ((Rect)(ref rowRect)).y, num2, ((Rect)(ref rowRect)).height); Rect val3 = default(Rect); ((Rect)(ref val3))..ctor(((Rect)(ref val)).xMax + 5f * scale, ((Rect)(ref rowRect)).y + 8f * scale, Math.Max(10f, ((Rect)(ref val2)).x - ((Rect)(ref val)).xMax - 10f * scale), 11f * scale); string text = (row.IsPresent ? string.Empty : (_useChinese() ? "(已离开)" : " (left)")); GUI.Label(val, Trim(row.Name, 18) + text, _label); GUI.color = new Color(0.14f, 0.18f, 0.24f, 0.95f); GUI.DrawTexture(val3, (Texture)(object)Texture2D.whiteTexture); GUI.color = Color.HSVToRGB(Mathf.Repeat((float)_playerColors.GetIndex(row.Key) * 0.618034f, 1f), 0.68f, 1f); GUI.DrawTexture(new Rect(((Rect)(ref val3)).x, ((Rect)(ref val3)).y, ((Rect)(ref val3)).width * ((float)row.Total / (float)max), ((Rect)(ref val3)).height), (Texture)(object)Texture2D.whiteTexture); GUI.color = Color.white; GUI.Label(val2, row.Total.ToString("N0", CultureInfo.InvariantCulture), _value); } private void HandlePointerDown(int index, Rect panelRect, Rect titleRect, Rect resizeHandle) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0049: 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_0095: Unknown result type (might be due to invalid IL or missing references) Event current = Event.current; if ((int)current.type != 0) { return; } Vector2 mousePosition = current.mousePosition; if (!((Rect)(ref panelRect)).Contains(mousePosition)) { return; } _panels.Select(index); if (current.button == 1 && ((Rect)(ref titleRect)).Contains(mousePosition)) { _metricMenuPanel = index; _metricMenuPosition = mousePosition; _dragging = -1; _resizing = -1; current.Use(); } else if (current.button == 0) { if (((Rect)(ref resizeHandle)).Contains(mousePosition)) { _resizing = index; _dragging = -1; _metricMenuPanel = -1; current.Use(); } else if (((Rect)(ref titleRect)).Contains(mousePosition)) { _dragging = index; _resizing = -1; _metricMenuPanel = -1; current.Use(); } } } private void HandleActiveManipulation() { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Invalid comparison between Unknown and I4 //IL_011f: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Invalid comparison between Unknown and I4 //IL_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) if (!EditMode) { return; } Event current = Event.current; if ((int)current.type == 3 && (_dragging >= 0 || _resizing >= 0)) { int num = ((_dragging >= 0) ? _dragging : _resizing); if (num < _panels.Panels.Count) { HudPanelState hudPanelState = _panels.Panels[num]; if (_dragging >= 0) { _panels.MoveAll(current.delta.x / Math.Max(1f, Screen.width), current.delta.y / Math.Max(1f, Screen.height)); } else { float requestedWidth = hudPanelState.Width + current.delta.x / Math.Max(1f, Screen.width); float requestedHeight = hudPanelState.Height + current.delta.y / Math.Max(1f, Screen.height); _panels.ResizeAll(requestedWidth, requestedHeight); } current.Use(); } } else if ((int)current.rawType == 1 && (_dragging >= 0 || _resizing >= 0)) { _dragging = -1; _resizing = -1; SaveLayout(); current.Use(); } } private void DrawMetricMenu(float scale) { //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_016b: Unknown result type (might be due to invalid IL or missing references) //IL_0176: Unknown result type (might be due to invalid IL or missing references) if (!EditMode || _metricMenuPanel < 0 || _metricMenuPanel >= _panels.Panels.Count) { return; } float num = 190f * scale; float num2 = 30f * scale; float num3 = num2 * (float)MetricCatalog.Visible.Length + 8f * scale; Rect val = default(Rect); ((Rect)(ref val))..ctor(Mathf.Clamp(_metricMenuPosition.x, 4f, Math.Max(4f, (float)Screen.width - num - 4f)), Mathf.Clamp(_metricMenuPosition.y, 4f, Math.Max(4f, (float)Screen.height - num3 - 4f)), num, num3); GUI.Box(val, GUIContent.none, _panel); int num4 = 0; MetricKind[] visible = MetricCatalog.Visible; foreach (MetricKind metric in visible) { if (GUI.Button(new Rect(((Rect)(ref val)).x + 4f * scale, ((Rect)(ref val)).y + 4f * scale + (float)num4 * num2, ((Rect)(ref val)).width - 8f * scale, num2 - 2f * scale), MetricName(metric))) { _panels.SetMetric(_metricMenuPanel, metric); _metricMenuPanel = -1; SaveLayout(); } num4++; } Event current = Event.current; if ((int)current.type == 0 && !((Rect)(ref val)).Contains(current.mousePosition)) { _metricMenuPanel = -1; current.Use(); } } private static Rect ResizeHandle(Rect rect, float scale) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) float num = 22f * scale; return new Rect(((Rect)(ref rect)).xMax - num, ((Rect)(ref rect)).yMax - num, num, num); } private static Rect ToPixels(HudPanelState panel) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) return new Rect(panel.X * (float)Screen.width, panel.Y * (float)Screen.height, panel.Width * (float)Screen.width, panel.Height * (float)Screen.height); } private static void FromPixels(HudPanelState panel, Rect rect) { panel.X = ((Rect)(ref rect)).x / Math.Max(1f, Screen.width); panel.Y = ((Rect)(ref rect)).y / Math.Max(1f, Screen.height); panel.Width = ((Rect)(ref rect)).width / Math.Max(1f, Screen.width); panel.Height = ((Rect)(ref rect)).height / Math.Max(1f, Screen.height); } private static void ClampRect(ref Rect rect) { float num = Math.Min(300f, (float)Screen.width * 0.6f); float num2 = Math.Min(150f, (float)Screen.height * 0.5f); ((Rect)(ref rect)).width = Mathf.Clamp(((Rect)(ref rect)).width, num, Math.Max(num, (float)Screen.width - 8f)); ((Rect)(ref rect)).height = Mathf.Clamp(((Rect)(ref rect)).height, num2, Math.Max(num2, (float)Screen.height - 8f)); ((Rect)(ref rect)).x = Mathf.Clamp(((Rect)(ref rect)).x, 4f, Math.Max(4f, (float)Screen.width - ((Rect)(ref rect)).width - 4f)); ((Rect)(ref rect)).y = Mathf.Clamp(((Rect)(ref rect)).y, 4f, Math.Max(4f, (float)Screen.height - ((Rect)(ref rect)).height - 4f)); } private string MetricName(MetricKind metric) { if (!_useChinese()) { return metric switch { MetricKind.CreatureDamage => "Creature damage", MetricKind.PlayerDamage => "Friendly fire", MetricKind.MoneyEarned => "Money earned", MetricKind.MoneySpent => "Money spent", MetricKind.TeammatesRevived => "Teammates revived", MetricKind.PlayerKills => "Teammates killed", MetricKind.PlayerDeaths => "Deaths", _ => "Fishing catches", }; } return metric switch { MetricKind.CreatureDamage => "对鱼造成伤害", MetricKind.PlayerDamage => "友军伤害", MetricKind.MoneyEarned => "赚钱数量", MetricKind.MoneySpent => "花钱数量", MetricKind.TeammatesRevived => "复活队友次数", MetricKind.PlayerKills => "击杀次数", MetricKind.PlayerDeaths => "死亡次数", _ => "钓鱼数量", }; } private string WarningText(string detail) { return (_useChinese() ? "兼容性降级:部分采集器缺失" : "Compatibility degraded: collectors missing") + (string.IsNullOrEmpty(detail) ? string.Empty : (" — " + Trim(detail, 46))); } private GUIStyle WarningStyle() { //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_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown GUIStyle val = new GUIStyle(_small); val.normal.textColor = new Color(1f, 0.55f, 0.35f, 1f); return val; } private void EnsureStyles() { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Expected O, but got Unknown //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Expected O, but got Unknown //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_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Expected O, but got Unknown //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0105: Expected O, but got Unknown //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Expected O, but got Unknown //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Expected O, but got Unknown //IL_0174: Unknown result type (might be due to invalid IL or missing references) if (_panel == null) { _background = SolidTexture(new Color(0.035f, 0.055f, 0.09f, 0.58f)); _selectedBackground = SolidTexture(new Color(0.95f, 0.68f, 0.18f, 0.82f)); GUIStyle val = new GUIStyle(GUI.skin.box); val.normal.background = _background; _panel = val; GUIStyle val2 = new GUIStyle(GUI.skin.box); val2.normal.background = _selectedBackground; _selectedPanel = val2; _title = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)4, fontStyle = (FontStyle)1 }; _title.normal.textColor = new Color(0.86f, 0.98f, 1f, 1f); _label = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)3 }; _label.normal.textColor = Color.white; _value = new GUIStyle(_label) { alignment = (TextAnchor)5, fontStyle = (FontStyle)1 }; _small = new GUIStyle(GUI.skin.label) { alignment = (TextAnchor)4 }; _small.normal.textColor = new Color(0.65f, 0.74f, 0.82f, 1f); } } private void UpdateFontSizes(float scale) { _title.fontSize = Mathf.RoundToInt(17f * scale); _label.fontSize = Mathf.RoundToInt(13f * scale); _value.fontSize = Mathf.RoundToInt(13f * scale); _small.fontSize = Mathf.RoundToInt(11f * scale); } private static Texture2D SolidTexture(Color color) { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Expected O, but got Unknown Texture2D val = new Texture2D(1, 1, (TextureFormat)4, false); val.SetPixel(0, 0, color); val.Apply(); return val; } private void SaveLayout() { if (_saveLayout != null) { _saveLayout(_panels.Serialize()); } } private void RestoreCursor() { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (_cursorCaptured) { Cursor.lockState = _previousCursorLock; Cursor.visible = _previousCursorVisible; _cursorCaptured = false; } } private static string Trim(string value, int max) { if (string.IsNullOrEmpty(value) || value.Length <= max) { return value ?? string.Empty; } return value.Substring(0, max - 1) + "…"; } } internal sealed class StatsLedger { private sealed class MoneyCue { internal float Time; internal bool Increase; internal object Player; } private sealed class MoneyDelta { internal float Time; internal int Amount; } private const float DuplicateWindowSeconds = 0.45f; private const float CorrelationWindowSeconds = 1.5f; private readonly GameIntrospection _game; private readonly Dictionary _players = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _serverPlayers = new Dictionary(StringComparer.Ordinal); private readonly CrossSourceDeduplicator _deduplicator = new CrossSourceDeduplicator(0.45f); private readonly FishingCatchTracker _catchTracker = new FishingCatchTracker(); private readonly List _moneyCues = new List(); private readonly List _moneyDeltas = new List(); internal string SessionId { get; private set; } internal DateTime StartedUtc { get; private set; } internal long UnassignedMoneyEarned { get; private set; } internal long UnassignedMoneySpent { get; private set; } internal long Revision { get; private set; } internal bool UsingServerSnapshot { get; private set; } internal StatsLedger(GameIntrospection game, string sessionId) { _game = game; SessionId = sessionId; StartedUtc = DateTime.UtcNow; } internal void RefreshPlayers(IList livePlayers) { HashSet hashSet = new HashSet(StringComparer.Ordinal); foreach (object livePlayer in livePlayers) { PlayerIdentity playerIdentity = _game.GetPlayerIdentity(livePlayer); if (playerIdentity != null) { PlayerStats playerStats = EnsurePlayer(playerIdentity); playerStats.IsPresent = true; playerStats.LastSeenUtc = DateTime.UtcNow; hashSet.Add(playerStats.Key); } } foreach (PlayerStats value in _players.Values) { if (!hashSet.Contains(value.Key)) { value.IsPresent = false; } } Revision++; } internal void RecordCreatureDamage(object player, object creature, int damage, string source, Confidence confidence) { if (damage > 0) { Record("creature_damage", MetricKind.CreatureDamage, player, damage, creature, source, confidence, string.Empty); } } internal void RecordPlayerDamage(object attacker, object target, int damage, string source, Confidence confidence, string damageType) { if (damage > 0) { Record("player_damage", MetricKind.PlayerDamage, attacker, damage, target, source, confidence, damageType ?? string.Empty); } } internal void RecordAuthoritativeMoney(object player, int amount, bool increase, string source) { if (amount > 0) { Record(increase ? "money_earned" : "money_spent", increase ? MetricKind.MoneyEarned : MetricKind.MoneySpent, player, amount, null, source, Confidence.Exact, string.Empty); } } internal void ObserveMoneyCue(bool increase, object player) { float now = Time.realtimeSinceStartup; MoneyDelta moneyDelta = (from d in _moneyDeltas where Math.Sign(d.Amount) == (increase ? 1 : (-1)) && Math.Abs(now - d.Time) <= 1.5f orderby d.Time descending select d).FirstOrDefault(); if (moneyDelta != null) { _moneyDeltas.Remove(moneyDelta); RecordAuthoritativeOrInferredMoney(player, Math.Abs(moneyDelta.Amount), increase, "money_correlation"); return; } _moneyCues.Add(new MoneyCue { Time = now, Increase = increase, Player = player }); } internal void ObserveBalanceChange(int previous, int next, bool asServer) { int num = next - previous; if (!(num == 0 || asServer)) { float now = Time.realtimeSinceStartup; bool increase = num > 0; List list = _moneyCues.Where((MoneyCue c) => c.Increase == increase && Math.Abs(now - c.Time) <= 1.5f).ToList(); if (list.Count == 1) { _moneyCues.Remove(list[0]); RecordAuthoritativeOrInferredMoney(list[0].Player, Math.Abs(num), increase, "money_correlation"); } else { _moneyDeltas.Add(new MoneyDelta { Time = now, Amount = num }); } } } internal void BeginFishingCatch(object player, object item, object rod, bool authoritative) { _catchTracker.Begin(_game.GetObjectId(rod), player, item, authoritative); } internal void ConfirmFishingCatch(object rod, object landedItem, string source) { if (_catchTracker.TryConfirm(_game.GetObjectId(rod), out var pending)) { object obj = (GameIntrospection.IsNull(landedItem) ? pending.Item : landedItem); Record("fish_caught", MetricKind.FishCaught, pending.Player, 1L, obj, source, (!pending.Authoritative) ? Confidence.Inferred : Confidence.Exact, _game.GetItemName(obj)); } } internal void CancelFishingCatch(object rod) { _catchTracker.Cancel(_game.GetObjectId(rod)); } internal void RecordResurrection(object victim, object deadPlayer, string source) { if (!GameIntrospection.IsNull(victim) && !GameIntrospection.IsNull(deadPlayer) && _game.IsPlayerDead(victim)) { object deadPlayerHolder = _game.GetDeadPlayerHolder(deadPlayer); if (!GameIntrospection.IsNull(deadPlayerHolder) && !_game.IsSameObject(deadPlayerHolder, victim)) { Record("teammate_revived", MetricKind.TeammatesRevived, deadPlayerHolder, 1L, victim, source, Confidence.Exact, string.Empty); } } } internal void RecordPlayerKill(object attacker, object victim, string source) { if (!GameIntrospection.IsNull(attacker) && !GameIntrospection.IsNull(victim) && !_game.IsSameObject(attacker, victim)) { Record("player_kill", MetricKind.PlayerKills, attacker, 1L, victim, source, Confidence.Exact, string.Empty); } } internal void RecordDeath(object player, string source) { if (!GameIntrospection.IsNull(player)) { Record("player_death", MetricKind.PlayerDeaths, player, 1L, null, source, Confidence.Exact, string.Empty); } } internal void Tick() { float now = Time.realtimeSinceStartup; for (int num = _moneyDeltas.Count - 1; num >= 0; num--) { if (!(now - _moneyDeltas[num].Time <= 1.5f)) { MoneyDelta moneyDelta = _moneyDeltas[num]; _moneyDeltas.RemoveAt(num); RecordUnknownMoney(moneyDelta.Amount); } } _moneyCues.RemoveAll((MoneyCue c) => now - c.Time > 1.5f); } internal IList GetRows(MetricKind metric) { return (from p in ((IDictionary)(UsingServerSnapshot ? _serverPlayers : _players)).Values select new PlayerMetricRow { Key = p.Key, SteamId = p.SteamId, Name = p.Name, IsPresent = p.IsPresent, Exact = p.Metrics[metric].Exact, Inferred = p.Metrics[metric].Inferred } into row orderby row.Total descending, (row.SteamId != 0L) ? row.SteamId : ulong.MaxValue select row).ThenBy((PlayerMetricRow row) => row.Key, StringComparer.Ordinal).ToList(); } internal IList GetPlayers() { return ((IDictionary)(UsingServerSnapshot ? _serverPlayers : _players)).Values.OrderBy((PlayerStats p) => (p.SteamId != 0L) ? p.SteamId : ulong.MaxValue).ThenBy((PlayerStats p) => p.Key, StringComparer.Ordinal).ToList(); } internal StatsSyncSnapshot CreateSyncSnapshot() { StatsSyncSnapshot statsSyncSnapshot = new StatsSyncSnapshot { SessionId = SessionId }; int length = Enum.GetValues(typeof(MetricKind)).Length; foreach (PlayerStats item in _players.Values.OrderBy((PlayerStats p) => p.Key, StringComparer.Ordinal)) { StatsSyncPlayer statsSyncPlayer = new StatsSyncPlayer { Key = item.Key, SteamId = item.SteamId, Name = item.Name, IsPresent = item.IsPresent, Exact = new long[length], Inferred = new long[length] }; foreach (MetricKind value in Enum.GetValues(typeof(MetricKind))) { statsSyncPlayer.Exact[(int)value] = item.Metrics[value].Exact; statsSyncPlayer.Inferred[(int)value] = item.Metrics[value].Inferred; } statsSyncSnapshot.Players.Add(statsSyncPlayer); } return statsSyncSnapshot; } internal void ApplyServerSnapshot(StatsSyncSnapshot snapshot) { if (snapshot == null) { return; } _serverPlayers.Clear(); int length = Enum.GetValues(typeof(MetricKind)).Length; foreach (StatsSyncPlayer player in snapshot.Players) { if (player != null && !string.IsNullOrEmpty(player.Key)) { PlayerStats playerStats = new PlayerStats { Key = player.Key, SteamId = player.SteamId, Name = player.Name, IsPresent = player.IsPresent, LastSeenUtc = DateTime.UtcNow }; for (int i = 0; i < length; i++) { MetricKind key = (MetricKind)i; playerStats.Metrics[key].Exact = ((player.Exact != null && i < player.Exact.Length) ? player.Exact[i] : 0); playerStats.Metrics[key].Inferred = ((player.Inferred != null && i < player.Inferred.Length) ? player.Inferred[i] : 0); } _serverPlayers[playerStats.Key] = playerStats; } } UsingServerSnapshot = true; } internal void ClearServerSnapshot() { if (UsingServerSnapshot || _serverPlayers.Count != 0) { UsingServerSnapshot = false; _serverPlayers.Clear(); } } internal void FlushPending() { MoneyDelta[] array = _moneyDeltas.ToArray(); foreach (MoneyDelta moneyDelta in array) { RecordUnknownMoney(moneyDelta.Amount); } _moneyDeltas.Clear(); _moneyCues.Clear(); } private void RecordAuthoritativeOrInferredMoney(object player, int amount, bool increase, string source) { Record(increase ? "money_earned" : "money_spent", increase ? MetricKind.MoneyEarned : MetricKind.MoneySpent, player, amount, null, source, Confidence.Inferred, string.Empty); } private void RecordUnknownMoney(int delta) { if (delta > 0) { UnassignedMoneyEarned += delta; } else { UnassignedMoneySpent += Math.Abs((long)delta); } Record((delta > 0) ? "money_delta_unassigned_earned" : "money_delta_unassigned_spent", null, null, Math.Abs((long)delta), null, "balance_delta", Confidence.Unknown, "Original game traffic did not uniquely identify a player."); } private void Record(string eventType, MetricKind? metric, object player, long value, object target, string source, Confidence confidence, string detail) { PlayerIdentity playerIdentity = _game.GetPlayerIdentity(player); string text = ((playerIdentity == null) ? string.Empty : playerIdentity.Key); string objectId = _game.GetObjectId(target); float realtimeSinceStartup = Time.realtimeSinceStartup; string signature = eventType + "|" + text + "|" + objectId + "|" + value.ToString(CultureInfo.InvariantCulture); PlayerStats playerStats = ((playerIdentity == null) ? null : EnsurePlayer(playerIdentity)); switch (_deduplicator.Observe(signature, source, confidence, realtimeSinceStartup)) { case DeduplicationResult.PromoteToExact: if (playerStats != null && metric.HasValue) { MetricValue metricValue2 = playerStats.Metrics[metric.Value]; metricValue2.Inferred = Math.Max(0L, metricValue2.Inferred - value); metricValue2.Exact += value; Revision++; } break; case DeduplicationResult.NewEvent: if (metric.HasValue && playerStats != null) { MetricValue metricValue = playerStats.Metrics[metric.Value]; switch (confidence) { case Confidence.Exact: case Confidence.Corrected: metricValue.Exact += value; break; case Confidence.Inferred: metricValue.Inferred += value; break; } } Revision++; break; } } private PlayerStats EnsurePlayer(PlayerIdentity identity) { if (!_players.TryGetValue(identity.Key, out var value)) { value = new PlayerStats { Key = identity.Key, SteamId = identity.SteamId, Name = identity.Name }; _players.Add(identity.Key, value); } else { value.SteamId = identity.SteamId; if (!string.IsNullOrWhiteSpace(identity.Name)) { value.Name = identity.Name; } } return value; } } internal sealed class StatsSyncSnapshot { internal string SessionId; internal readonly List Players = new List(); } internal sealed class StatsSyncPlayer { internal string Key; internal ulong SteamId; internal string Name; internal bool IsPresent; internal long[] Exact; internal long[] Inferred; } internal static class StatsSyncPayloadCodec { internal const byte ProtocolVersion = 1; internal static string Serialize(StatsSyncSnapshot snapshot) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append(((byte)1).ToString(CultureInfo.InvariantCulture)).Append('\n').Append(Encode((snapshot == null) ? string.Empty : snapshot.SessionId)) .Append('\n'); if (snapshot == null) { return stringBuilder.ToString(); } int length = Enum.GetValues(typeof(MetricKind)).Length; foreach (StatsSyncPlayer player in snapshot.Players) { stringBuilder.Append(Encode(player.Key)).Append('|').Append(player.SteamId.ToString(CultureInfo.InvariantCulture)) .Append('|') .Append(Encode(player.Name)) .Append('|') .Append(player.IsPresent ? '1' : '0'); for (int i = 0; i < length; i++) { stringBuilder.Append('|').Append(ValueAt(player.Exact, i).ToString(CultureInfo.InvariantCulture)).Append('|') .Append(ValueAt(player.Inferred, i).ToString(CultureInfo.InvariantCulture)); } stringBuilder.Append('\n'); } return stringBuilder.ToString(); } internal static bool TryDeserialize(string payload, out StatsSyncSnapshot snapshot) { snapshot = null; if (string.IsNullOrEmpty(payload)) { return false; } string[] array = payload.Replace("\r", string.Empty).Split(new char[1] { '\n' }); if (array.Length < 2 || !byte.TryParse(array[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) || result != 1) { return false; } if (!TryDecode(array[1], out var decoded)) { return false; } StatsSyncSnapshot statsSyncSnapshot = new StatsSyncSnapshot { SessionId = decoded }; int length = Enum.GetValues(typeof(MetricKind)).Length; int num = 4 + length * 2; for (int i = 2; i < array.Length; i++) { if (string.IsNullOrEmpty(array[i])) { continue; } string[] array2 = array[i].Split(new char[1] { '|' }); if (array2.Length != num || !TryDecode(array2[0], out var decoded2) || !ulong.TryParse(array2[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2) || !TryDecode(array2[2], out var decoded3) || (array2[3] != "0" && array2[3] != "1")) { return false; } StatsSyncPlayer statsSyncPlayer = new StatsSyncPlayer { Key = decoded2, SteamId = result2, Name = decoded3, IsPresent = (array2[3] == "1"), Exact = new long[length], Inferred = new long[length] }; for (int j = 0; j < length; j++) { if (!long.TryParse(array2[4 + j * 2], NumberStyles.Integer, CultureInfo.InvariantCulture, out statsSyncPlayer.Exact[j]) || !long.TryParse(array2[5 + j * 2], NumberStyles.Integer, CultureInfo.InvariantCulture, out statsSyncPlayer.Inferred[j])) { return false; } } statsSyncSnapshot.Players.Add(statsSyncPlayer); } snapshot = statsSyncSnapshot; return true; } private static long ValueAt(long[] values, int index) { if (values == null || index < 0 || index >= values.Length) { return 0L; } return values[index]; } private static string Encode(string value) { return Convert.ToBase64String(Encoding.UTF8.GetBytes(value ?? string.Empty)); } private static bool TryDecode(string value, out string decoded) { decoded = string.Empty; try { decoded = Encoding.UTF8.GetString(Convert.FromBase64String(value ?? string.Empty)); return true; } catch { return false; } } } public struct StatsHandshakeBroadcast : IBroadcast { public byte Protocol; } public struct StatsSnapshotBroadcast : IBroadcast { public string Payload; } internal sealed class StatsSyncService { private readonly StatsLedger _ledger; private readonly Dictionary _moddedClients = new Dictionary(); private ServerManager _server; private ClientManager _client; private bool _handshakeSent; private bool _snapshotReceived; private float _nextSnapshot; internal StatsSyncService(StatsLedger ledger) { _ledger = ledger; RegisterSerializers(); } internal void Tick() { EnsureManagers(); if ((Object)(object)_client == (Object)null || !_client.Started) { _handshakeSent = false; _snapshotReceived = false; _ledger.ClearServerSnapshot(); } else if (!_handshakeSent && !InstanceFinder.IsServerStarted && _client.Connection != (NetworkConnection)null && _client.Connection.IsAuthenticated) { _client.Broadcast(new StatsHandshakeBroadcast { Protocol = 1 }, (Channel)0); _handshakeSent = true; Plugin.LogDebug("Stats sync handshake sent to host."); } if ((Object)(object)_server != (Object)null && _server.Started && Time.unscaledTime >= _nextSnapshot) { _nextSnapshot = Time.unscaledTime + 1f; BroadcastSnapshot(); } } internal void Shutdown() { if ((Object)(object)_server != (Object)null) { _server.UnregisterBroadcast((Action)OnHandshake); } if ((Object)(object)_client != (Object)null) { _client.UnregisterBroadcast((Action)OnSnapshot); _client.OnAuthenticated -= OnAuthenticated; _client.OnClientConnectionState -= OnClientConnectionState; } _moddedClients.Clear(); _ledger.ClearServerSnapshot(); } private static void RegisterSerializers() { GenericWriter.SetWrite((Action)delegate(Writer writer, StatsHandshakeBroadcast value) { writer.WriteUInt8Unpacked(value.Protocol); }); GenericReader.SetRead((Func)((Reader reader) => new StatsHandshakeBroadcast { Protocol = reader.ReadUInt8Unpacked() })); GenericWriter.SetWrite((Action)delegate(Writer writer, StatsSnapshotBroadcast value) { writer.WriteString(value.Payload ?? string.Empty); }); GenericReader.SetRead((Func)((Reader reader) => new StatsSnapshotBroadcast { Payload = reader.ReadStringAllocated() })); } private void EnsureManagers() { ServerManager serverManager = InstanceFinder.ServerManager; if (serverManager != _server) { if ((Object)(object)_server != (Object)null) { _server.UnregisterBroadcast((Action)OnHandshake); } _server = serverManager; _moddedClients.Clear(); if ((Object)(object)_server != (Object)null) { _server.RegisterBroadcast((Action)OnHandshake, false); } } ClientManager clientManager = InstanceFinder.ClientManager; if (clientManager != _client) { if ((Object)(object)_client != (Object)null) { _client.UnregisterBroadcast((Action)OnSnapshot); _client.OnAuthenticated -= OnAuthenticated; _client.OnClientConnectionState -= OnClientConnectionState; } _client = clientManager; _handshakeSent = false; if ((Object)(object)_client != (Object)null) { _client.RegisterBroadcast((Action)OnSnapshot); _client.OnAuthenticated += OnAuthenticated; _client.OnClientConnectionState += OnClientConnectionState; } } } private void OnAuthenticated() { _handshakeSent = false; } private void OnClientConnectionState(ClientConnectionStateArgs args) { if ((Object)(object)_client == (Object)null || !_client.Started) { _handshakeSent = false; _ledger.ClearServerSnapshot(); } } private void OnHandshake(NetworkConnection connection, StatsHandshakeBroadcast message, Channel channel) { if (message.Protocol == 1 && !(connection == (NetworkConnection)null) && connection.IsValid) { bool num = !_moddedClients.ContainsKey(connection.ClientId); _moddedClients[connection.ClientId] = connection; SendSnapshot(connection); if (num) { Plugin.LogInfo("Stats sync client connected: " + connection.ClientId + "."); } } } private void OnSnapshot(StatsSnapshotBroadcast message, Channel channel) { if (InstanceFinder.IsServerStarted) { return; } if (!StatsSyncPayloadCodec.TryDeserialize(message.Payload, out var snapshot)) { Plugin.LogWarning("Ignored an invalid multiplayer stats snapshot."); return; } _ledger.ApplyServerSnapshot(snapshot); if (!_snapshotReceived) { _snapshotReceived = true; Plugin.LogInfo("Authoritative host stats sync active."); } } private void BroadcastSnapshot() { foreach (int item in new List(_moddedClients.Keys)) { NetworkConnection val = _moddedClients[item]; if (val == (NetworkConnection)null || !val.IsValid || !val.IsActive) { _moddedClients.Remove(item); } else { SendSnapshot(val); } } } private void SendSnapshot(NetworkConnection connection) { if (!((Object)(object)_server == (Object)null) && _server.Started && !(connection == (NetworkConnection)null)) { string payload = StatsSyncPayloadCodec.Serialize(_ledger.CreateSyncSnapshot()); _server.Broadcast(connection, new StatsSnapshotBroadcast { Payload = payload }, true, (Channel)0); } } }