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 _values = new Dictionary(); private readonly Dictionary _firstUtc = new Dictionary(); private readonly Dictionary _lastUtc = new Dictionary(); private readonly Dictionary> _daily = new Dictionary>(); 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 Entries => _values; internal IEnumerable> Raw => _values; internal IEnumerable>> RawDaily => _daily; public event Action? 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 value in _values) { if (value.Key != "_team") { num += value.Value; } } return num; } public List> 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 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 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 value)) { return 0; } int num = DayEpochNow(); int num2 = num - (days - 1); int num3 = 0; foreach (KeyValuePair item in value) { if (item.Key >= num2 && item.Key <= num) { num3 += item.Value; } } return num3; } public List> DailyHistory(string steamID) { List> list = new List>(); if (_daily.TryGetValue(Norm(steamID), out Dictionary value)) { foreach (KeyValuePair item in value.OrderBy((KeyValuePair k) => k.Key)) { list.Add(new KeyValuePair(DateOfEpoch(item.Key), item.Value)); } } return list; } private int Bucket(string id, int dayEpoch) { if (!_daily.TryGetValue(id, out Dictionary 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 value3)) { value3 = new Dictionary(); _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 saved) { foreach (KeyValuePair 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 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 _stats = new Dictionary(); private static readonly char[] ReservedKeyChars = new char[9] { '|', ',', ':', ';', '=', ' ', '\t', '\n', '\r' }; public static bool IsHost => StatsNet.CanWriteSynced; public static IEnumerable All => _stats.Values; public static event Action? 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> 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? Global; public Func? PerPlayer; } private static readonly Harmony _harmony = new Harmony("Vippy.StatsCore.Trackers"); private static readonly Dictionary> _byMethod = new Dictionary>(); private static readonly List _mirrors = new List(); 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 value)) { value = new List(); _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 value)) { for (int i = 0; i < value.Count; i++) { value[i].Fire(__instance, __args); } } } public static void Mirror(Stat stat, Func read, SampleWhen when = SampleWhen.LevelChange) { _mirrors.Add(new MirrorEntry { Stat = stat, Global = read, When = when }); } public static void MirrorPlayers(Stat stat, Func 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(); 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> _saved = new Dictionary>(); private static readonly Dictionary>> _savedDaily = new Dictionary>>(); 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 value2)) { value2 = new Dictionary(); _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 dictionary = new Dictionary(); 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> value3)) { value3 = new Dictionary>(); _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 value)) { stat.HydrateCareer(value); } } internal static void HydrateDaily(Stat stat) { EnsureLoaded(); if (!_savedDaily.TryGetValue(stat.Key, out Dictionary> value)) { return; } foreach (KeyValuePair> item in value) { stat.HydrateDailyEntry(item.Key, new Dictionary(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 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> item3 in item.RawDaily) { if (item3.Value.Count == 0) { continue; } stringBuilder2.Append(item.Key).Append('|').Append(item3.Key) .Append('|'); bool flag = false; foreach (KeyValuePair 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 Results = new Dictionary(); public int Pending; public bool Completed; public Action> 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 _pulls = new Dictionary(); 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() == (Object)null) { ((Component)__instance).gameObject.AddComponent(); } } 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> 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> done, float timeoutSeconds) { int num = ++_nextRequestId; PullRequest pullRequest = new PullRequest { Done = done, Pending = ((PhotonNetwork.CurrentRoom != null) ? (PhotonNetwork.CurrentRoom.PlayerCount - 1) : 0) }; foreach (KeyValuePair 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 LocalSlot(string key) { Dictionary dictionary = new Dictionary(); 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 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 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)"); } } }