Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of StatsCore v0.1.0
StatsCore.dll
Decompiled 2 days agousing 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)"); } } }