Decompiled source of StatsCore v0.1.0

StatsCore.dll

Decompiled 2 days ago
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using BepInEx;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
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: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: AssemblyCompany("Vippy")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("0.1.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0+42bba24d34b2cdd6427e1871ebe56b1a610ba930")]
[assembly: AssemblyProduct("StatsCore")]
[assembly: AssemblyTitle("StatsCore")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("0.1.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableAttribute : Attribute
	{
		public readonly byte[] NullableFlags;

		public NullableAttribute(byte P_0)
		{
			NullableFlags = new byte[1] { P_0 };
		}

		public NullableAttribute(byte[] P_0)
		{
			NullableFlags = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)]
	internal sealed class NullableContextAttribute : Attribute
	{
		public readonly byte Flag;

		public NullableContextAttribute(byte P_0)
		{
			Flag = P_0;
		}
	}
	[CompilerGenerated]
	[Embedded]
	[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
	internal sealed class RefSafetyRulesAttribute : Attribute
	{
		public readonly int Version;

		public RefSafetyRulesAttribute(int P_0)
		{
			Version = P_0;
		}
	}
}
namespace StatsCore
{
	public static class GameStats
	{
		public const string K_Deaths = "statscore.deaths";

		public const string K_DeathsByEnemy = "statscore.deathsByEnemy";

		public const string K_Knockdowns = "statscore.knockdowns";

		public const string K_Revives = "statscore.revives";

		public const string K_SpewerFaces = "statscore.spewerFaces";

		public const string K_CrownWins = "statscore.crownWins";

		public const string K_MapVisits = "statscore.mapVisits";

		public const string K_DamageTaken = "statscore.damageTaken";

		public const string K_DamageTakenByEnemy = "statscore.damageTakenByEnemy";

		public const string K_HitsTaken = "statscore.hitsTaken";

		public const string K_HealthHealed = "statscore.healthHealed";

		public const string K_DistanceTraveled = "statscore.distanceTraveled";

		public const string K_DistanceWalking = "statscore.distanceWalking";

		public const string K_DistanceSprinting = "statscore.distanceSprinting";

		public const string K_ObjectsGrabbed = "statscore.objectsGrabbed";

		public const string K_EnemiesKilled = "statscore.enemiesKilled";

		public const string K_FriendlyFireDealt = "statscore.friendlyFireDealt";

		public const string K_TeammatesKilled = "statscore.teammatesKilled";

		public const string K_DamageFromTeammates = "statscore.damageFromTeammates";

		public const string K_RunLevel = "statscore.runLevel";

		public const string K_RunCurrency = "statscore.runCurrency";

		public const string K_RunTotalHaul = "statscore.runTotalHaul";

		private static bool _registered;

		private static float _distAccum;

		private static float _walkAccum;

		private static float _runAccum;

		public static Stat Deaths { get; private set; }

		public static Stat DeathsByEnemy { get; private set; }

		public static Stat Knockdowns { get; private set; }

		public static Stat Revives { get; private set; }

		public static Stat SpewerFaces { get; private set; }

		public static Stat CrownWins { get; private set; }

		public static Stat MapVisits { get; private set; }

		public static Stat DamageTaken { get; private set; }

		public static Stat DamageTakenByEnemy { get; private set; }

		public static Stat HitsTaken { get; private set; }

		public static Stat HealthHealed { get; private set; }

		public static Stat DistanceTraveled { get; private set; }

		public static Stat DistanceWalking { get; private set; }

		public static Stat DistanceSprinting { get; private set; }

		public static Stat ObjectsGrabbed { get; private set; }

		public static Stat EnemiesKilled { get; private set; }

		public static Stat FriendlyFireDealt { get; private set; }

		public static Stat TeammatesKilled { get; private set; }

		public static Stat DamageFromTeammates { get; private set; }

		public static Stat RunLevel { get; private set; }

		public static Stat RunCurrency { get; private set; }

		public static Stat RunTotalHaul { get; private set; }

		internal static void Register()
		{
			if (!_registered)
			{
				_registered = true;
				DefinePack();
				WireMisfortune();
				WireRecovery();
				WireGlory();
				WireMischief();
				WireRunMirrors();
			}
		}

		private static void DefinePack()
		{
			Deaths = Career("statscore.deaths", "Deaths", "Times you've gone down for good.", "Misfortune").TrackDaily();
			DeathsByEnemy = Career("statscore.deathsByEnemy", "Deaths by Enemy", "Your deaths, broken down by what killed you (keyed by enemy name).", "Misfortune");
			Knockdowns = Career("statscore.knockdowns", "Knockdowns", "Times something put you on the floor.", "Misfortune").TrackDaily();
			SpewerFaces = Career("statscore.spewerFaces", "Spewer Faces", "Times a spewer latched onto your face.", "Misfortune");
			DamageTaken = Career("statscore.damageTaken", "Damage Taken", "Total damage you've soaked.", "Misfortune", StatFormat.Raw).TrackDaily();
			DamageTakenByEnemy = Career("statscore.damageTakenByEnemy", "Damage by Enemy", "Damage dealt to you, broken down by what dealt it (keyed by enemy name). How hard each thing has clapped you.", "Misfortune", StatFormat.Raw);
			HitsTaken = Career("statscore.hitsTaken", "Hits Taken", "Number of separate times you've been hurt.", "Misfortune").TrackDaily();
			Revives = Career("statscore.revives", "Revives", "Times you were brought back.", "Recovery").TrackDaily();
			HealthHealed = Career("statscore.healthHealed", "Health Healed", "Total health restored to you.", "Recovery", StatFormat.Raw).TrackDaily();
			CrownWins = Career("statscore.crownWins", "Crown Wins", "King-of-the-losers victories.", "Glory");
			EnemiesKilled = Career("statscore.enemiesKilled", "Enemies Killed", "Enemies that died to your handiwork (best effort - the game rarely names a killer).", "Glory").TrackDaily();
			MapVisits = Career("statscore.mapVisits", "Map Visits", "How many times you've dropped into each level (keyed by level name).", "Expeditions");
			DistanceTraveled = Career("statscore.distanceTraveled", "Distance Traveled", "Total ground you've covered, in metres.", "Mobility", StatFormat.Distance).TrackDaily();
			DistanceWalking = Career("statscore.distanceWalking", "Distance Walking", "Metres covered at a walk.", "Mobility", StatFormat.Distance).TrackDaily();
			DistanceSprinting = Career("statscore.distanceSprinting", "Distance Sprinting", "Metres covered at a sprint.", "Mobility", StatFormat.Distance).TrackDaily();
			ObjectsGrabbed = Career("statscore.objectsGrabbed", "Objects Grabbed", "Things you've latched onto with the grab beam.", "Mobility").TrackDaily();
			FriendlyFireDealt = Career("statscore.friendlyFireDealt", "Friendly Fire", "Damage you've dealt to your own crew.", "Mischief", StatFormat.Raw).TrackDaily();
			TeammatesKilled = Career("statscore.teammatesKilled", "Teammates Killed", "Crewmates you've personally finished off.", "Mischief");
			DamageFromTeammates = Career("statscore.damageFromTeammates", "Betrayal Taken", "Damage your own crew has dealt to you.", "Mischief", StatFormat.Raw).TrackDaily();
			RunLevel = Stats.Define("statscore.runLevel", StatScope.Team, StatLifetime.Run).Describe("Run Level", "The expedition level the crew is on.", "Expedition", StatFormat.Raw);
			RunCurrency = Stats.Define("statscore.runCurrency", StatScope.Team, StatLifetime.Run).Describe("Run Currency", "Money the crew is holding this run.", "Expedition", StatFormat.Money);
			RunTotalHaul = Stats.Define("statscore.runTotalHaul", StatScope.Team, StatLifetime.Run).Describe("Total Haul", "Total value the crew has hauled this run.", "Expedition", StatFormat.Money);
		}

		private static void WireMisfortune()
		{
			Trackers.Hook(Deaths, typeof(PlayerAvatar), "PlayerDeathRPC", delegate(Stat s, object? inst, object[] args)
			{
				PlayerAvatar val = (PlayerAvatar)((inst is PlayerAvatar) ? inst : null);
				if (val != null && IsLocal(val))
				{
					s.Add(val.steamID, 1);
					int enemyIndex = ((args.Length != 0 && args[0] is int num) ? num : (-1));
					DeathsByEnemy.Add(EnemyName(enemyIndex), 1);
				}
			});
			Trackers.Hook(Knockdowns, typeof(PlayerTumble), "TumbleSetRPC", delegate(Stat s, object? inst, object[] args)
			{
				if (args.Length >= 2 && (bool)args[0] && !(bool)args[1])
				{
					PlayerAvatar val = ((PlayerTumble)(((inst is PlayerTumble) ? inst : null)?)).playerAvatar;
					if ((Object)(object)val != (Object)null && IsLocal(val))
					{
						s.Add(val.steamID, 1);
					}
				}
			});
			Trackers.Hook(SpewerFaces, typeof(EnemySlowMouth), "UpdateStateRPC", delegate(Stat s, object? inst, object[] args)
			{
				//IL_0009: Unknown result type (might be due to invalid IL or missing references)
				//IL_0010: Invalid comparison between Unknown and I4
				if (args.Length >= 1 && (int)(State)args[0] == 9)
				{
					PlayerAvatar val = ((EnemySlowMouth)(((inst is EnemySlowMouth) ? inst : null)?)).playerTarget;
					if ((Object)(object)val != (Object)null && IsLocal(val))
					{
						s.Add(val.steamID, 1);
					}
				}
			});
			Trackers.Hook(DamageTaken, typeof(PlayerHealth), "Hurt", delegate(Stat s, object? inst, object[] args)
			{
				PlayerHealth val = (PlayerHealth)((inst is PlayerHealth) ? inst : null);
				if (val != null)
				{
					PlayerAvatar playerAvatar = val.playerAvatar;
					if (!((Object)(object)playerAvatar == (Object)null) && IsLocal(playerAvatar))
					{
						int num = ((args.Length != 0 && args[0] is int num2) ? num2 : 0);
						bool flag = default(bool);
						int num3;
						if (args.Length > 3)
						{
							object obj = args[3];
							if (obj is bool)
							{
								flag = (bool)obj;
								num3 = 1;
							}
							else
							{
								num3 = 0;
							}
						}
						else
						{
							num3 = 0;
						}
						bool flag2 = (byte)((uint)num3 & (flag ? 1u : 0u)) != 0;
						if (!(num <= 0 || flag2))
						{
							s.Add(playerAvatar.steamID, num);
							HitsTaken.Add(playerAvatar.steamID, 1);
							int enemyIndex = ((args.Length > 2 && args[2] is int num4) ? num4 : (-1));
							DamageTakenByEnemy.Add(EnemyName(enemyIndex), num);
						}
					}
				}
			}, new Type[4]
			{
				typeof(int),
				typeof(bool),
				typeof(int),
				typeof(bool)
			});
		}

		private static void WireRecovery()
		{
			Trackers.Hook(Revives, typeof(PlayerAvatar), "ReviveRPC", delegate(Stat s, object? inst, object[] args)
			{
				PlayerAvatar val = (PlayerAvatar)((inst is PlayerAvatar) ? inst : null);
				if (val != null && IsLocal(val))
				{
					s.Add(val.steamID, 1);
				}
			});
			Trackers.Hook(HealthHealed, typeof(PlayerHealth), "Heal", delegate(Stat s, object? inst, object[] args)
			{
				PlayerHealth val = (PlayerHealth)((inst is PlayerHealth) ? inst : null);
				if (val != null)
				{
					PlayerAvatar playerAvatar = val.playerAvatar;
					if (!((Object)(object)playerAvatar == (Object)null) && IsLocal(playerAvatar))
					{
						int num = ((args.Length != 0 && args[0] is int num2) ? num2 : 0);
						if (num > 0)
						{
							s.Add(playerAvatar.steamID, num);
						}
					}
				}
			}, new Type[2]
			{
				typeof(int),
				typeof(bool)
			});
		}

		private static void WireGlory()
		{
			Trackers.Hook(CrownWins, typeof(Arena), "CrownGrabRPC", delegate(Stat s, object? inst, object[] args)
			{
				PlayerAvatar val = ((Arena)(((inst is Arena) ? inst : null)?)).winnerPlayer;
				if ((Object)(object)val != (Object)null && IsLocal(val))
				{
					s.Add(val.steamID, 1);
				}
			});
			Trackers.Hook(CrownWins, typeof(ArenaRace), "SetWinnerRPC", delegate(Stat s, object? inst, object[] args)
			{
				PlayerAvatar val = ((ArenaRace)(((inst is ArenaRace) ? inst : null)?)).winnerPlayer;
				if ((Object)(object)val != (Object)null && IsLocal(val))
				{
					s.Add(val.steamID, 1);
				}
			});
			Trackers.Hook(EnemiesKilled, typeof(EnemyHealth), "DeathRPC", delegate(Stat s, object? inst, object[] args)
			{
				PlayerAvatar val = ((EnemyHealth)(((inst is EnemyHealth) ? inst : null)?)).onObjectHurtPlayer;
				if ((Object)(object)val != (Object)null && IsLocal(val))
				{
					s.Add(val.steamID, 1);
				}
			});
			Trackers.Hook(ObjectsGrabbed, typeof(PhysGrabObject), "GrabStarted", delegate(Stat s, object? inst, object[] args)
			{
				PhysGrabber val = (PhysGrabber)((args.Length != 0) ? /*isinst with value type is only supported in some contexts*/: null);
				PlayerAvatar val2 = (((Object)(object)val != (Object)null) ? val.playerAvatar : null);
				if ((Object)(object)val2 != (Object)null && IsLocal(val2))
				{
					s.Add(val2.steamID, 1);
				}
			}, new Type[1] { typeof(PhysGrabber) });
		}

		private static void WireMischief()
		{
			Trackers.Hook(FriendlyFireDealt, typeof(PunManager), "PlayerDamagingPlayerRPC", delegate(Stat s, object? inst, object[] args)
			{
				if (args.Length >= 5)
				{
					int num = ((args[0] is int num2) ? num2 : 0);
					int num3 = ((args[1] is int num4) ? num4 : 0);
					int num5 = ((args[2] is int num6) ? num6 : 0);
					object obj = args[4];
					bool flag = default(bool);
					int num7;
					if (obj is bool)
					{
						flag = (bool)obj;
						num7 = 1;
					}
					else
					{
						num7 = 0;
					}
					bool flag2 = (byte)((uint)num7 & (flag ? 1u : 0u)) != 0;
					PlayerAvatar val = SemiFunc.PlayerAvatarGetFromPhotonID(num);
					if ((Object)(object)val != (Object)null && IsLocal(val))
					{
						if (num5 > 0)
						{
							s.Add(val.steamID, num5);
						}
						if (flag2)
						{
							TeammatesKilled.Add(val.steamID, 1);
						}
					}
					PlayerAvatar val2 = SemiFunc.PlayerAvatarGetFromPhotonID(num3);
					if ((Object)(object)val2 != (Object)null && IsLocal(val2) && num5 > 0)
					{
						DamageFromTeammates.Add(val2.steamID, num5);
					}
				}
			});
		}

		private static void WireRunMirrors()
		{
			Trackers.Mirror(RunLevel, () => ((Object)(object)StatsManager.instance != (Object)null) ? StatsManager.instance.GetRunStatLevel() : 0);
			Trackers.Mirror(RunCurrency, () => ((Object)(object)StatsManager.instance != (Object)null) ? StatsManager.instance.GetRunStatCurrency() : 0, SampleWhen.Tick);
			Trackers.Mirror(RunTotalHaul, () => ((Object)(object)StatsManager.instance != (Object)null) ? StatsManager.instance.GetRunStatTotalHaul() : 0, SampleWhen.Tick);
		}

		internal static void SampleLocalMovement(float dt)
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_004c: Unknown result type (might be due to invalid IL or missing references)
			if (!_registered || dt <= 0f || !SemiFunc.RunIsLevel())
			{
				return;
			}
			PlayerController instance = PlayerController.instance;
			PlayerAvatar instance2 = PlayerAvatar.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)instance2 == (Object)null)
			{
				return;
			}
			string steamID = instance2.steamID;
			if (string.IsNullOrEmpty(steamID))
			{
				return;
			}
			Vector3 velocity = instance.Velocity;
			velocity.y = 0f;
			float num = ((Vector3)(ref velocity)).magnitude * dt;
			if (!(num <= 0.01f) && !(num >= 5f))
			{
				_distAccum += num;
				if (instance.sprinting)
				{
					_runAccum += num;
				}
				else
				{
					_walkAccum += num;
				}
				CommitMetres(DistanceTraveled, steamID, ref _distAccum);
				CommitMetres(DistanceSprinting, steamID, ref _runAccum);
				CommitMetres(DistanceWalking, steamID, ref _walkAccum);
			}
		}

		private static void CommitMetres(Stat stat, string id, ref float accum)
		{
			if (!(accum < 1f))
			{
				int num = (int)accum;
				accum -= num;
				stat.Add(id, num);
			}
		}

		private static Stat Career(string key, string name, string description, string category, StatFormat format = StatFormat.Count)
		{
			return Stats.Define(key, StatScope.PerPlayer, StatLifetime.Career).Describe(name, description, category, format);
		}

		private static bool IsLocal(PlayerAvatar p)
		{
			if ((Object)(object)p != (Object)null && (Object)(object)PlayerAvatar.instance != (Object)null)
			{
				return p.steamID == PlayerAvatar.instance.steamID;
			}
			return false;
		}

		private static string EnemyName(int enemyIndex)
		{
			if (enemyIndex < 0)
			{
				return "Environment";
			}
			Enemy val = SemiFunc.EnemyGetFromIndex(enemyIndex);
			string text = (((Object)(object)val != (Object)null && (Object)(object)val.EnemyParent != (Object)null) ? val.EnemyParent.enemyName : null);
			if (!string.IsNullOrEmpty(text))
			{
				return text;
			}
			return "Unknown";
		}

		internal static void OnLevelChanged()
		{
			if (!_registered || !SemiFunc.RunIsLevel())
			{
				return;
			}
			Level val = (((Object)(object)RunManager.instance != (Object)null) ? RunManager.instance.levelCurrent : null);
			if (!((Object)(object)val == (Object)null))
			{
				string text = ((!string.IsNullOrEmpty(val.NarrativeName)) ? val.NarrativeName : ((Object)val).name);
				if (!string.IsNullOrEmpty(text))
				{
					MapVisits.Add(text, 1);
				}
			}
		}
	}
	public sealed class Stat
	{
		public const string TeamKey = "_team";

		private readonly Dictionary<string, int> _values = new Dictionary<string, int>();

		private readonly Dictionary<string, long> _firstUtc = new Dictionary<string, long>();

		private readonly Dictionary<string, long> _lastUtc = new Dictionary<string, long>();

		private readonly Dictionary<string, Dictionary<int, int>> _daily = new Dictionary<string, Dictionary<int, int>>();

		private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

		public string Key { get; }

		public StatScope Scope { get; }

		public StatLifetime Lifetime { get; }

		public string DisplayName { get; private set; }

		public string Description { get; private set; } = "";

		public string Category { get; private set; } = "";

		public StatFormat Format { get; private set; }

		public bool TracksDaily { get; private set; }

		public bool IsClientAuthoritative { get; private set; }

		public IReadOnlyDictionary<string, int> Entries => _values;

		internal IEnumerable<KeyValuePair<string, int>> Raw => _values;

		internal IEnumerable<KeyValuePair<string, Dictionary<int, int>>> RawDaily => _daily;

		public event Action<string, int>? Changed;

		internal Stat(string key, StatScope scope, StatLifetime lifetime)
		{
			Key = key;
			Scope = scope;
			Lifetime = lifetime;
			DisplayName = key;
		}

		public Stat Describe(string? displayName = null, string? description = null, string? category = null, StatFormat? format = null)
		{
			if (displayName != null)
			{
				DisplayName = displayName;
			}
			if (description != null)
			{
				Description = description;
			}
			if (category != null)
			{
				Category = category;
			}
			if (format.HasValue)
			{
				Format = format.Value;
			}
			return this;
		}

		public Stat TrackDaily()
		{
			if (Lifetime != StatLifetime.Career)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] TrackDaily() on non-career '" + Key + "' ignored - daily history needs persistence (use StatLifetime.Career)"));
				return this;
			}
			if (!TracksDaily)
			{
				TracksDaily = true;
				Persistence.HydrateDaily(this);
			}
			return this;
		}

		public Stat ClientAuthoritative()
		{
			if (Lifetime == StatLifetime.Career)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] ClientAuthoritative() on career '" + Key + "' ignored - career stats are already local; use Stats.RequestFromClients to pull them"));
			}
			else
			{
				IsClientAuthoritative = true;
			}
			return this;
		}

		public int Get(string steamID)
		{
			if (!_values.TryGetValue(Norm(steamID), out var value))
			{
				return 0;
			}
			return value;
		}

		public int GetTeam()
		{
			return Get("_team");
		}

		public int Sum()
		{
			int num = 0;
			foreach (KeyValuePair<string, int> value in _values)
			{
				if (value.Key != "_team")
				{
					num += value.Value;
				}
			}
			return num;
		}

		public List<KeyValuePair<string, int>> Leaderboard()
		{
			return (from kv in _values
				where kv.Key != "_team"
				orderby kv.Value descending
				select kv).ToList();
		}

		public long FirstUtc(string steamID)
		{
			if (!_firstUtc.TryGetValue(Norm(steamID), out var value))
			{
				return 0L;
			}
			return value;
		}

		public long LastUtc(string steamID)
		{
			if (!_lastUtc.TryGetValue(Norm(steamID), out var value))
			{
				return 0L;
			}
			return value;
		}

		public DateTime? FirstRecorded(string steamID)
		{
			long num = FirstUtc(steamID);
			if (num != 0L)
			{
				return DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime;
			}
			return null;
		}

		public DateTime? LastUpdated(string steamID)
		{
			long num = LastUtc(steamID);
			if (num != 0L)
			{
				return DateTimeOffset.FromUnixTimeSeconds(num).UtcDateTime;
			}
			return null;
		}

		public int GetToday(string steamID)
		{
			return Bucket(Norm(steamID), DayEpochNow());
		}

		public int GetOnDay(string steamID, DateTime dayUtc)
		{
			return Bucket(Norm(steamID), DayEpoch(dayUtc));
		}

		public int GetRange(string steamID, DateTime fromUtc, DateTime toUtc)
		{
			if (!_daily.TryGetValue(Norm(steamID), out Dictionary<int, int> value))
			{
				return 0;
			}
			int num = DayEpoch(fromUtc);
			int num2 = DayEpoch(toUtc);
			if (num > num2)
			{
				int num3 = num2;
				num2 = num;
				num = num3;
			}
			int num4 = 0;
			foreach (KeyValuePair<int, int> item in value)
			{
				if (item.Key >= num && item.Key <= num2)
				{
					num4 += item.Value;
				}
			}
			return num4;
		}

		public int GetLastDays(string steamID, int days)
		{
			if (days < 1 || !_daily.TryGetValue(Norm(steamID), out Dictionary<int, int> value))
			{
				return 0;
			}
			int num = DayEpochNow();
			int num2 = num - (days - 1);
			int num3 = 0;
			foreach (KeyValuePair<int, int> item in value)
			{
				if (item.Key >= num2 && item.Key <= num)
				{
					num3 += item.Value;
				}
			}
			return num3;
		}

		public List<KeyValuePair<DateTime, int>> DailyHistory(string steamID)
		{
			List<KeyValuePair<DateTime, int>> list = new List<KeyValuePair<DateTime, int>>();
			if (_daily.TryGetValue(Norm(steamID), out Dictionary<int, int> value))
			{
				foreach (KeyValuePair<int, int> item in value.OrderBy((KeyValuePair<int, int> k) => k.Key))
				{
					list.Add(new KeyValuePair<DateTime, int>(DateOfEpoch(item.Key), item.Value));
				}
			}
			return list;
		}

		private int Bucket(string id, int dayEpoch)
		{
			if (!_daily.TryGetValue(id, out Dictionary<int, int> value) || !value.TryGetValue(dayEpoch, out var value2))
			{
				return 0;
			}
			return value2;
		}

		private static int DayEpochNow()
		{
			return (int)(DateTimeOffset.UtcNow.ToUnixTimeSeconds() / 86400);
		}

		private static int DayEpoch(DateTime day)
		{
			return (int)(((DateTimeOffset)new DateTime(day.Year, day.Month, day.Day, 0, 0, 0, DateTimeKind.Utc)).ToUnixTimeSeconds() / 86400);
		}

		private static DateTime DateOfEpoch(int dayEpoch)
		{
			return UnixEpoch.AddDays(dayEpoch);
		}

		public void Add(string steamID, int delta)
		{
			Write(Norm(steamID), Get(steamID) + delta);
		}

		public void Set(string steamID, int value)
		{
			Write(Norm(steamID), value);
		}

		public void AddTeam(int delta)
		{
			Write("_team", GetTeam() + delta);
		}

		public void SetTeam(int value)
		{
			Write("_team", value);
		}

		private void Write(string steamID, int value)
		{
			if (Lifetime == StatLifetime.Career)
			{
				ApplyLocal(steamID, value);
			}
			else if (IsClientAuthoritative)
			{
				if (!StatsNet.OwnsSlot(steamID))
				{
					StatsCorePlugin.Logger.LogDebug((object)("[StatsCore] client-authoritative write to '" + Key + "' for a slot you don't own ignored (write your own steamID only)"));
					return;
				}
				ApplyLocal(steamID, value);
				StatsNet.OnOwnerWrote(this, steamID, value);
			}
			else if (!StatsNet.CanWriteSynced)
			{
				StatsCorePlugin.Logger.LogDebug((object)("[StatsCore] write to synced '" + Key + "' ignored off-host (gate on Stats.IsHost)"));
			}
			else
			{
				ApplyLocal(steamID, value);
				StatsNet.OnHostWrote(this, steamID, value);
			}
		}

		internal void ApplyFromNetwork(string steamID, int value)
		{
			ApplyLocal(Norm(steamID), value);
		}

		public void OnReach(string steamID, int threshold, Action callback)
		{
			string id = Norm(steamID);
			if (Get(id) >= threshold)
			{
				callback();
			}
			else
			{
				Changed += Handler;
			}
			void Handler(string changedId, int newValue)
			{
				if (!(changedId != id) && newValue >= threshold)
				{
					Changed -= Handler;
					callback();
				}
			}
		}

		internal void ApplyLocal(string steamID, int value)
		{
			int value2;
			int num = (_values.TryGetValue(steamID, out value2) ? value2 : 0);
			_values[steamID] = value;
			long num2 = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
			if (!_firstUtc.ContainsKey(steamID))
			{
				_firstUtc[steamID] = num2;
			}
			_lastUtc[steamID] = num2;
			if (TracksDaily && Lifetime == StatLifetime.Career && value != num)
			{
				int key = (int)(num2 / 86400);
				if (!_daily.TryGetValue(steamID, out Dictionary<int, int> value3))
				{
					value3 = new Dictionary<int, int>();
					_daily[steamID] = value3;
				}
				value3.TryGetValue(key, out var value4);
				value3[key] = value4 + (value - num);
			}
			this.Changed?.Invoke(steamID, value);
			Stats.RaiseAnyChanged(this, steamID, value);
			if (Lifetime == StatLifetime.Career)
			{
				Persistence.MarkDirty();
			}
		}

		internal void Clear()
		{
			_values.Clear();
			_firstUtc.Clear();
			_lastUtc.Clear();
			_daily.Clear();
		}

		internal void HydrateCareer(Dictionary<string, CareerEntry> saved)
		{
			foreach (KeyValuePair<string, CareerEntry> item in saved)
			{
				_values[item.Key] = item.Value.Value;
				if (item.Value.FirstUtc != 0L)
				{
					_firstUtc[item.Key] = item.Value.FirstUtc;
				}
				if (item.Value.LastUtc != 0L)
				{
					_lastUtc[item.Key] = item.Value.LastUtc;
				}
			}
		}

		internal void HydrateDailyEntry(string steamID, Dictionary<int, int> byDay)
		{
			_daily[steamID] = byDay;
		}

		private static string Norm(string steamID)
		{
			if (!string.IsNullOrEmpty(steamID))
			{
				return steamID;
			}
			return "_team";
		}
	}
	public static class Stats
	{
		private static readonly Dictionary<string, Stat> _stats = new Dictionary<string, Stat>();

		private static readonly char[] ReservedKeyChars = new char[9] { '|', ',', ':', ';', '=', ' ', '\t', '\n', '\r' };

		public static bool IsHost => StatsNet.CanWriteSynced;

		public static IEnumerable<Stat> All => _stats.Values;

		public static event Action<Stat, string, int>? AnyChanged;

		public static Stat Define(string key, StatScope scope, StatLifetime lifetime)
		{
			if (string.IsNullOrEmpty(key) || key.IndexOfAny(ReservedKeyChars) >= 0)
			{
				throw new ArgumentException("stat key '" + key + "' is empty or contains a reserved character (| , : ; = or whitespace)", "key");
			}
			if (_stats.TryGetValue(key, out Stat value))
			{
				StatsCorePlugin.Logger.LogDebug((object)("[StatsCore] Define('" + key + "') returned the existing stat (scope/lifetime of the first definition win)"));
				return value;
			}
			Stat stat = new Stat(key, scope, lifetime);
			_stats[key] = stat;
			if (lifetime == StatLifetime.Career)
			{
				Persistence.Hydrate(stat);
			}
			return stat;
		}

		public static Stat? Get(string key)
		{
			if (!_stats.TryGetValue(key, out Stat value))
			{
				return null;
			}
			return value;
		}

		public static void RequestFromClients(string key, Action<IReadOnlyDictionary<string, int>> onComplete, float timeoutSeconds = 3f)
		{
			StatsNet.RequestFromClients(key, onComplete, timeoutSeconds);
		}

		internal static void RaiseAnyChanged(Stat stat, string steamID, int value)
		{
			Stats.AnyChanged?.Invoke(stat, steamID, value);
		}

		internal static void ResetLifetime(StatLifetime lifetime)
		{
			foreach (Stat value in _stats.Values)
			{
				if (value.Lifetime == lifetime)
				{
					value.Clear();
				}
			}
		}
	}
	public enum StatScope
	{
		PerPlayer,
		Team
	}
	public enum StatLifetime
	{
		Level,
		Run,
		Session,
		Career
	}
	internal struct CareerEntry
	{
		public int Value;

		public long FirstUtc;

		public long LastUtc;
	}
	public enum StatFormat
	{
		Count,
		Time,
		Distance,
		Money,
		Percent,
		Raw
	}
	public delegate void StatHit(Stat stat, object? instance, object[] args);
	public enum SampleWhen
	{
		LevelChange,
		RunReset,
		Tick
	}
	public static class Trackers
	{
		public sealed class Hooked
		{
			internal readonly StatHit OnHit;

			public Stat Stat { get; }

			public MethodBase? Target { get; }

			public bool Enabled { get; private set; }

			internal Hooked(Stat stat, MethodBase? target, StatHit onHit)
			{
				Stat = stat;
				Target = target;
				OnHit = onHit;
				Enabled = target != null;
			}

			public void Disable()
			{
				Enabled = false;
			}

			internal void Fire(object? instance, object[] args)
			{
				if (!Enabled)
				{
					return;
				}
				try
				{
					OnHit(Stat, instance, args);
				}
				catch (Exception ex)
				{
					StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] hook for '" + Stat.Key + "' threw: " + ex.Message));
				}
			}
		}

		private sealed class MirrorEntry
		{
			public Stat Stat;

			public SampleWhen When;

			public Func<int>? Global;

			public Func<PlayerAvatar, int>? PerPlayer;
		}

		private static readonly Harmony _harmony = new Harmony("Vippy.StatsCore.Trackers");

		private static readonly Dictionary<MethodBase, List<Hooked>> _byMethod = new Dictionary<MethodBase, List<Hooked>>();

		private static readonly List<MirrorEntry> _mirrors = new List<MirrorEntry>();

		public static Hooked Hook(Stat stat, Type gameType, string methodName, StatHit onHit, Type[]? argTypes = null)
		{
			//IL_00a5: Unknown result type (might be due to invalid IL or missing references)
			//IL_00b2: Expected O, but got Unknown
			MethodBase methodBase = AccessTools.Method(gameType, methodName, argTypes, (Type[])null);
			Hooked hooked = new Hooked(stat, methodBase, onHit);
			if (methodBase == null)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] " + gameType.Name + "." + methodName + " not found - hook for '" + stat.Key + "' disabled (game update?)."));
				return hooked;
			}
			if (!_byMethod.TryGetValue(methodBase, out List<Hooked> value))
			{
				value = new List<Hooked>();
				_byMethod[methodBase] = value;
				try
				{
					_harmony.Patch(methodBase, (HarmonyMethod)null, new HarmonyMethod(typeof(Trackers), "HookPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
				}
				catch (Exception ex)
				{
					StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't patch " + gameType.Name + "." + methodName + ": " + ex.Message));
					hooked.Disable();
					_byMethod.Remove(methodBase);
					return hooked;
				}
			}
			value.Add(hooked);
			return hooked;
		}

		private static void HookPostfix(object __instance, object[] __args, MethodBase __originalMethod)
		{
			if (_byMethod.TryGetValue(__originalMethod, out List<Hooked> value))
			{
				for (int i = 0; i < value.Count; i++)
				{
					value[i].Fire(__instance, __args);
				}
			}
		}

		public static void Mirror(Stat stat, Func<int> read, SampleWhen when = SampleWhen.LevelChange)
		{
			_mirrors.Add(new MirrorEntry
			{
				Stat = stat,
				Global = read,
				When = when
			});
		}

		public static void MirrorPlayers(Stat stat, Func<PlayerAvatar, int> read, SampleWhen when = SampleWhen.LevelChange)
		{
			_mirrors.Add(new MirrorEntry
			{
				Stat = stat,
				PerPlayer = read,
				When = when
			});
		}

		internal static void Sample(SampleWhen when)
		{
			for (int i = 0; i < _mirrors.Count; i++)
			{
				MirrorEntry mirrorEntry = _mirrors[i];
				if (mirrorEntry.When != when)
				{
					continue;
				}
				try
				{
					if (mirrorEntry.Global != null)
					{
						mirrorEntry.Stat.SetTeam(mirrorEntry.Global());
					}
					else
					{
						if (mirrorEntry.PerPlayer == null)
						{
							continue;
						}
						foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
						{
							if ((Object)(object)item != (Object)null)
							{
								mirrorEntry.Stat.Set(SemiFunc.PlayerGetSteamID(item), mirrorEntry.PerPlayer(item));
							}
						}
						continue;
					}
				}
				catch (Exception ex)
				{
					StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] mirror for '" + mirrorEntry.Stat.Key + "' threw: " + ex.Message));
				}
			}
		}

		internal static bool HasTickMirrors()
		{
			for (int i = 0; i < _mirrors.Count; i++)
			{
				if (_mirrors[i].When == SampleWhen.Tick)
				{
					return true;
				}
			}
			return false;
		}
	}
	internal static class BuildInfo
	{
		public const string Version = "0.1.0";
	}
	[BepInPlugin("Vippy.StatsCore", "StatsCore", "0.1.0")]
	public class StatsCorePlugin : BaseUnityPlugin
	{
		internal static StatsCorePlugin Instance { get; private set; }

		internal static ManualLogSource Logger => Instance.BaseLogger;

		private ManualLogSource BaseLogger => ((BaseUnityPlugin)this).Logger;

		private void Awake()
		{
			//IL_0029: 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_003a: Expected O, but got Unknown
			Instance = this;
			((Component)this).gameObject.transform.parent = null;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			GameObject val = new GameObject("StatsCore_Driver");
			val.AddComponent<StatsDriver>();
			Object.DontDestroyOnLoad((Object)val);
			GameStats.Register();
			StatsNet.Install();
			Logger.LogInfo((object)"StatsCore v0.1.0 loaded");
		}
	}
	internal static class Persistence
	{
		private static bool _loaded;

		private static readonly Dictionary<string, Dictionary<string, CareerEntry>> _saved = new Dictionary<string, Dictionary<string, CareerEntry>>();

		private static readonly Dictionary<string, Dictionary<string, Dictionary<int, int>>> _savedDaily = new Dictionary<string, Dictionary<string, Dictionary<int, int>>>();

		internal static bool Dirty { get; private set; }

		private static string FilePath => Path.Combine(Paths.ConfigPath, "StatsCore", "career.txt");

		private static string DailyFilePath => Path.Combine(Paths.ConfigPath, "StatsCore", "career-daily.txt");

		internal static void MarkDirty()
		{
			Dirty = true;
		}

		private static void EnsureLoaded()
		{
			if (_loaded)
			{
				return;
			}
			_loaded = true;
			try
			{
				if (File.Exists(FilePath))
				{
					string[] array = File.ReadAllLines(FilePath);
					foreach (string text in array)
					{
						int num = text.IndexOf('|');
						int num2 = text.LastIndexOf('=');
						if (num < 0 || num2 < num)
						{
							continue;
						}
						string key = text.Substring(0, num);
						string key2 = text.Substring(num + 1, num2 - num - 1);
						string text2 = text.Substring(num2 + 1);
						CareerEntry value = default(CareerEntry);
						string[] array2 = text2.Split(';');
						if (int.TryParse(array2[0], out value.Value))
						{
							if (array2.Length > 1)
							{
								long.TryParse(array2[1], out value.FirstUtc);
							}
							if (array2.Length > 2)
							{
								long.TryParse(array2[2], out value.LastUtc);
							}
							if (!_saved.TryGetValue(key, out Dictionary<string, CareerEntry> value2))
							{
								value2 = new Dictionary<string, CareerEntry>();
								_saved[key] = value2;
							}
							value2[key2] = value;
						}
					}
				}
			}
			catch (Exception ex)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't read career file: " + ex.Message));
			}
			try
			{
				if (!File.Exists(DailyFilePath))
				{
					return;
				}
				string[] array = File.ReadAllLines(DailyFilePath);
				foreach (string text3 in array)
				{
					int num3 = text3.IndexOf('|');
					int num4 = ((num3 < 0) ? (-1) : text3.IndexOf('|', num3 + 1));
					if (num3 < 0 || num4 < 0)
					{
						continue;
					}
					string key3 = text3.Substring(0, num3);
					string key4 = text3.Substring(num3 + 1, num4 - num3 - 1);
					Dictionary<int, int> dictionary = new Dictionary<int, int>();
					string[] array3 = text3.Substring(num4 + 1).Split(';');
					foreach (string text4 in array3)
					{
						int num5 = text4.IndexOf('=');
						if (num5 > 0 && int.TryParse(text4.Substring(0, num5), out var result) && int.TryParse(text4.Substring(num5 + 1), out var result2))
						{
							dictionary[result] = result2;
						}
					}
					if (dictionary.Count != 0)
					{
						if (!_savedDaily.TryGetValue(key3, out Dictionary<string, Dictionary<int, int>> value3))
						{
							value3 = new Dictionary<string, Dictionary<int, int>>();
							_savedDaily[key3] = value3;
						}
						value3[key4] = dictionary;
					}
				}
			}
			catch (Exception ex2)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't read career daily file: " + ex2.Message));
			}
		}

		internal static void Hydrate(Stat stat)
		{
			EnsureLoaded();
			if (_saved.TryGetValue(stat.Key, out Dictionary<string, CareerEntry> value))
			{
				stat.HydrateCareer(value);
			}
		}

		internal static void HydrateDaily(Stat stat)
		{
			EnsureLoaded();
			if (!_savedDaily.TryGetValue(stat.Key, out Dictionary<string, Dictionary<int, int>> value))
			{
				return;
			}
			foreach (KeyValuePair<string, Dictionary<int, int>> item in value)
			{
				stat.HydrateDailyEntry(item.Key, new Dictionary<int, int>(item.Value));
			}
		}

		internal static void Save()
		{
			if (!Dirty)
			{
				return;
			}
			Dirty = false;
			try
			{
				Directory.CreateDirectory(Path.GetDirectoryName(FilePath));
				StringBuilder stringBuilder = new StringBuilder();
				StringBuilder stringBuilder2 = new StringBuilder();
				foreach (Stat item in Stats.All)
				{
					if (item.Lifetime != StatLifetime.Career)
					{
						continue;
					}
					foreach (KeyValuePair<string, int> item2 in item.Raw)
					{
						stringBuilder.Append(item.Key).Append('|').Append(item2.Key)
							.Append('=')
							.Append(item2.Value)
							.Append(';')
							.Append(item.FirstUtc(item2.Key))
							.Append(';')
							.Append(item.LastUtc(item2.Key))
							.Append('\n');
					}
					if (!item.TracksDaily)
					{
						continue;
					}
					foreach (KeyValuePair<string, Dictionary<int, int>> item3 in item.RawDaily)
					{
						if (item3.Value.Count == 0)
						{
							continue;
						}
						stringBuilder2.Append(item.Key).Append('|').Append(item3.Key)
							.Append('|');
						bool flag = false;
						foreach (KeyValuePair<int, int> item4 in item3.Value)
						{
							if (flag)
							{
								stringBuilder2.Append(';');
							}
							stringBuilder2.Append(item4.Key).Append('=').Append(item4.Value);
							flag = true;
						}
						stringBuilder2.Append('\n');
					}
				}
				WriteAtomic(FilePath, stringBuilder.ToString());
				WriteAtomic(DailyFilePath, stringBuilder2.ToString());
			}
			catch (Exception ex)
			{
				StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't write career file: " + ex.Message));
			}
		}

		private static void WriteAtomic(string path, string content)
		{
			string text = path + ".tmp";
			File.WriteAllText(text, content);
			if (File.Exists(path))
			{
				File.Replace(text, path, null);
			}
			else
			{
				File.Move(text, path);
			}
		}
	}
	internal class StatsDriver : MonoBehaviour
	{
		private object? _lastLevel;

		private bool _wasLobbyMenu;

		private float _saveTimer;

		private float _tickTimer;

		private void Update()
		{
			FlushTick();
			TickSamples();
			GameStats.SampleLocalMovement(Time.deltaTime);
			RunManager instance = RunManager.instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			object levelCurrent = instance.levelCurrent;
			if (levelCurrent != _lastLevel)
			{
				_lastLevel = levelCurrent;
				Stats.ResetLifetime(StatLifetime.Level);
				bool flag = SemiFunc.RunIsLobbyMenu();
				if (flag && !_wasLobbyMenu)
				{
					Stats.ResetLifetime(StatLifetime.Run);
					Trackers.Sample(SampleWhen.RunReset);
				}
				_wasLobbyMenu = flag;
				Trackers.Sample(SampleWhen.LevelChange);
				GameStats.OnLevelChanged();
			}
		}

		private void TickSamples()
		{
			if (Trackers.HasTickMirrors())
			{
				_tickTimer += Time.unscaledDeltaTime;
				if (!(_tickTimer < 3f))
				{
					_tickTimer = 0f;
					Trackers.Sample(SampleWhen.Tick);
				}
			}
		}

		private void FlushTick()
		{
			if (Persistence.Dirty)
			{
				_saveTimer += Time.unscaledDeltaTime;
				if (!(_saveTimer < 5f))
				{
					_saveTimer = 0f;
					Persistence.Save();
				}
			}
		}

		private void OnApplicationQuit()
		{
			Persistence.Save();
		}
	}
	internal sealed class StatsNet : MonoBehaviourPunCallbacks
	{
		private sealed class PullRequest
		{
			public Dictionary<string, int> Results = new Dictionary<string, int>();

			public int Pending;

			public bool Completed;

			public Action<IReadOnlyDictionary<string, int>> Done;
		}

		private static StatsNet? _instance;

		private static readonly Harmony _harmony = new Harmony("Vippy.StatsCore.Net");

		private const int ChunkChars = 32000;

		private static int _nextRequestId;

		private readonly Dictionary<int, PullRequest> _pulls = new Dictionary<int, PullRequest>();

		internal static bool CanWriteSynced
		{
			get
			{
				if (PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode)
				{
					return PhotonNetwork.IsMasterClient;
				}
				return true;
			}
		}

		internal static void Install()
		{
			//IL_0047: Unknown result type (might be due to invalid IL or missing references)
			//IL_0054: Expected O, but got Unknown
			MethodInfo methodInfo = AccessTools.Method(typeof(PunManager), "Awake", (Type[])null, (Type[])null);
			if (methodInfo == null)
			{
				StatsCorePlugin.Logger.LogWarning((object)"[StatsNet] PunManager.Awake not found - sync disabled, stats run local-only");
			}
			else
			{
				_harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(StatsNet), "AttachPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
			}
		}

		private static void AttachPostfix(PunManager __instance)
		{
			if ((Object)(object)((Component)__instance).GetComponent<StatsNet>() == (Object)null)
			{
				((Component)__instance).gameObject.AddComponent<StatsNet>();
			}
		}

		private void Awake()
		{
			_instance = this;
		}

		internal static void OnHostWrote(Stat stat, string steamID, int value)
		{
			if (!((Object)(object)_instance == (Object)null) && PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode && PhotonNetwork.IsMasterClient)
			{
				((MonoBehaviourPun)_instance).photonView.RPC("StatsCoreSetRPC", (RpcTarget)1, new object[3] { stat.Key, steamID, value });
			}
		}

		[PunRPC]
		private void StatsCoreSetRPC(string key, string steamID, int value, PhotonMessageInfo info)
		{
			//IL_0000: 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)
			if (info.Sender != null && info.Sender == PhotonNetwork.MasterClient)
			{
				Stats.Get(key)?.ApplyFromNetwork(steamID, value);
			}
		}

		internal static bool OwnsSlot(string steamID)
		{
			if (!PhotonNetwork.InRoom || PhotonNetwork.OfflineMode)
			{
				return true;
			}
			PlayerAvatar instance = PlayerAvatar.instance;
			if ((Object)(object)instance != (Object)null)
			{
				return steamID == instance.steamID;
			}
			return false;
		}

		internal static void OnOwnerWrote(Stat stat, string steamID, int value)
		{
			if (!((Object)(object)_instance == (Object)null) && PhotonNetwork.InRoom && !PhotonNetwork.OfflineMode)
			{
				((MonoBehaviourPun)_instance).photonView.RPC("StatsCoreOwnedRPC", (RpcTarget)1, new object[3] { stat.Key, steamID, value });
			}
		}

		[PunRPC]
		private void StatsCoreOwnedRPC(string key, string steamID, int value, PhotonMessageInfo info)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			if (SenderOwns(info, steamID))
			{
				Stat stat = Stats.Get(key);
				if (stat != null && stat.IsClientAuthoritative)
				{
					stat.ApplyFromNetwork(steamID, value);
				}
			}
		}

		private static bool SenderOwns(PhotonMessageInfo info, string steamID)
		{
			//IL_0000: 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)
			if (info.Sender == null || string.IsNullOrEmpty(steamID))
			{
				return false;
			}
			PlayerAvatar val = SemiFunc.PlayerAvatarGetFromPhotonPlayer(info.Sender);
			if ((Object)(object)val != (Object)null)
			{
				return val.steamID == steamID;
			}
			return false;
		}

		internal static void RequestFromClients(string key, Action<IReadOnlyDictionary<string, int>> onComplete, float timeoutSeconds)
		{
			if (onComplete != null)
			{
				if ((Object)(object)_instance == (Object)null || !PhotonNetwork.InRoom || PhotonNetwork.OfflineMode || !PhotonNetwork.IsMasterClient)
				{
					onComplete(LocalSlot(key));
				}
				else
				{
					_instance.BeginPull(key, onComplete, timeoutSeconds);
				}
			}
		}

		private void BeginPull(string key, Action<IReadOnlyDictionary<string, int>> done, float timeoutSeconds)
		{
			int num = ++_nextRequestId;
			PullRequest pullRequest = new PullRequest
			{
				Done = done,
				Pending = ((PhotonNetwork.CurrentRoom != null) ? (PhotonNetwork.CurrentRoom.PlayerCount - 1) : 0)
			};
			foreach (KeyValuePair<string, int> item in LocalSlot(key))
			{
				pullRequest.Results[item.Key] = item.Value;
			}
			_pulls[num] = pullRequest;
			if (pullRequest.Pending <= 0)
			{
				CompletePull(num);
				return;
			}
			((MonoBehaviourPun)this).photonView.RPC("StatsCorePullRequestRPC", (RpcTarget)1, new object[2] { key, num });
			((MonoBehaviour)this).StartCoroutine(PullTimeout(num, timeoutSeconds));
		}

		[PunRPC]
		private void StatsCorePullRequestRPC(string key, int requestId, PhotonMessageInfo info)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_005a: Unknown result type (might be due to invalid IL or missing references)
			if (info.Sender != null && info.Sender == PhotonNetwork.MasterClient)
			{
				PlayerAvatar instance = PlayerAvatar.instance;
				string text = (((Object)(object)instance != (Object)null) ? instance.steamID : "");
				int num = 0;
				Stat stat = Stats.Get(key);
				if (stat != null && !string.IsNullOrEmpty(text))
				{
					num = stat.Get(text);
				}
				((MonoBehaviourPun)this).photonView.RPC("StatsCorePullResponseRPC", info.Sender, new object[4] { key, requestId, text, num });
			}
		}

		[PunRPC]
		private void StatsCorePullResponseRPC(string key, int requestId, string steamID, int value, PhotonMessageInfo info)
		{
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			if (PhotonNetwork.IsMasterClient && SenderOwns(info, steamID) && _pulls.TryGetValue(requestId, out PullRequest value2) && !value2.Completed)
			{
				value2.Results[steamID] = value;
				value2.Pending--;
				if (value2.Pending <= 0)
				{
					CompletePull(requestId);
				}
			}
		}

		private IEnumerator PullTimeout(int requestId, float seconds)
		{
			yield return (object)new WaitForSecondsRealtime(seconds);
			CompletePull(requestId);
		}

		private void CompletePull(int requestId)
		{
			if (!_pulls.TryGetValue(requestId, out PullRequest value) || value.Completed)
			{
				return;
			}
			value.Completed = true;
			_pulls.Remove(requestId);
			try
			{
				value.Done(value.Results);
			}
			catch (Exception ex)
			{
				StatsCorePlugin.Logger.LogWarning((object)$"[StatsNet] pull callback for '{requestId}' threw: {ex.Message}");
			}
		}

		private static Dictionary<string, int> LocalSlot(string key)
		{
			Dictionary<string, int> dictionary = new Dictionary<string, int>();
			Stat stat = Stats.Get(key);
			PlayerAvatar instance = PlayerAvatar.instance;
			if (stat != null && (Object)(object)instance != (Object)null && !string.IsNullOrEmpty(instance.steamID))
			{
				dictionary[instance.steamID] = stat.Get(instance.steamID);
			}
			return dictionary;
		}

		public override void OnPlayerEnteredRoom(Player newPlayer)
		{
			if (!PhotonNetwork.IsMasterClient)
			{
				return;
			}
			foreach (string item in BuildSnapshotChunks())
			{
				((MonoBehaviourPun)this).photonView.RPC("StatsCoreSnapshotRPC", newPlayer, new object[1] { item });
			}
		}

		[PunRPC]
		private void StatsCoreSnapshotRPC(string payload, PhotonMessageInfo info)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			if (info.Sender != null && info.Sender == PhotonNetwork.MasterClient)
			{
				ApplySnapshot(payload);
			}
		}

		private static IEnumerable<string> BuildSnapshotChunks()
		{
			StringBuilder sb = new StringBuilder();
			foreach (Stat item in Stats.All)
			{
				if (item.Lifetime == StatLifetime.Career)
				{
					continue;
				}
				StringBuilder entry = new StringBuilder(item.Key).Append('|');
				bool flag = false;
				foreach (KeyValuePair<string, int> item2 in item.Raw)
				{
					if (flag)
					{
						entry.Append(',');
					}
					entry.Append(item2.Key).Append('=').Append(item2.Value);
					flag = true;
				}
				if (flag)
				{
					if (sb.Length > 0 && sb.Length + entry.Length + 1 > 32000)
					{
						yield return sb.ToString();
						sb.Clear();
					}
					if (sb.Length > 0)
					{
						sb.Append(';');
					}
					sb.Append(entry);
				}
			}
			if (sb.Length > 0)
			{
				yield return sb.ToString();
			}
		}

		private static void ApplySnapshot(string payload)
		{
			int num = 0;
			string[] array = payload.Split(';');
			foreach (string text in array)
			{
				int num2 = text.IndexOf('|');
				if (num2 <= 0)
				{
					continue;
				}
				Stat stat = Stats.Get(text.Substring(0, num2));
				if (stat == null)
				{
					continue;
				}
				string[] array2 = text.Substring(num2 + 1).Split(',');
				foreach (string text2 in array2)
				{
					int num3 = text2.IndexOf('=');
					if (num3 > 0 && int.TryParse(text2.Substring(num3 + 1), out var result))
					{
						stat.ApplyFromNetwork(text2.Substring(0, num3), result);
						num++;
					}
				}
			}
			StatsCorePlugin.Logger.LogDebug((object)$"[StatsNet] snapshot applied ({num} entries)");
		}
	}
}