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 v1.0.1
StatsCore.dll
Decompiled a week agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using MenuLib; using MenuLib.MonoBehaviors; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; using UnityEngine.UI; [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("1.0.1.0")] [assembly: AssemblyInformationalVersion("1.0.1+76abf6d8aba9a8968e5e7375fe5be3886b8837b9")] [assembly: AssemblyProduct("StatsCore")] [assembly: AssemblyTitle("StatsCore")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.1.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 meters.", "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>>(); 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), DayMath.Today()); } public int GetOnDay(string steamID, DateTime day) { return Bucket(Norm(steamID), DayMath.IndexOf(day)); } public int GetRange(string steamID, DateTime from, DateTime to) { if (!_daily.TryGetValue(Norm(steamID), out Dictionary<int, int> value)) { return 0; } int num = DayMath.IndexOf(from); int num2 = DayMath.IndexOf(to); 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 = DayMath.Today(); 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>(DayMath.DateOf(item.Key), item.Value)); } } return list; } private int Bucket(string id, int dayIndex) { if (!_daily.TryGetValue(id, out Dictionary<int, int> value) || !value.TryGetValue(dayIndex, out var value2)) { return 0; } return value2; } 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) { if (Lifetime == StatLifetime.Career) { StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] refused a networked write to career stat '" + Key + "'")); } else { 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 value3 = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); if (!_firstUtc.ContainsKey(steamID)) { _firstUtc[steamID] = value3; } _lastUtc[steamID] = value3; if (TracksDaily && Lifetime == StatLifetime.Career && value != num) { int key = DayMath.Today(); if (!_daily.TryGetValue(steamID, out Dictionary<int, int> value4)) { value4 = new Dictionary<int, int>(); _daily[steamID] = value4; } value4.TryGetValue(key, out var value5); value4[key] = value5 + (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 CareerFile.SanitizeSlot(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) { bool isHost = Stats.IsHost; for (int i = 0; i < _mirrors.Count; i++) { MirrorEntry mirrorEntry = _mirrors[i]; if (mirrorEntry.When != when || (!isHost && mirrorEntry.Stat.Lifetime != StatLifetime.Career && !mirrorEntry.Stat.IsClientAuthoritative)) { 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 = "1.0.1"; } [BepInPlugin("Vippy.StatsCore", "StatsCore", "1.0.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] 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(); if (Chainloader.PluginInfos.ContainsKey("nickklmao.menulib")) { StatsScreenEntry.Install(((BaseUnityPlugin)this).Config); } Logger.LogInfo((object)"StatsCore v1.0.1 loaded"); } } internal static class CareerFile { internal const int Version = 1; internal const string TotalsHeader = "# statscore career v1"; internal const string DailyHeader = "# statscore career-daily v1"; private static readonly char[] Reserved = new char[8] { '|', '=', ';', ',', ':', '\n', '\r', '\t' }; private const string MergedPrefix = "# merged "; internal static string SanitizeSlot(string slot) { if (string.IsNullOrEmpty(slot) || slot.IndexOfAny(Reserved) < 0) { return slot; } StringBuilder stringBuilder = new StringBuilder(slot.Length); foreach (char c in slot) { stringBuilder.Append((Array.IndexOf(Reserved, c) >= 0) ? '_' : c); } return stringBuilder.ToString(); } internal static Dictionary<string, Dictionary<string, CareerEntry>> ParseTotals(IEnumerable<string> lines) { Dictionary<string, Dictionary<string, CareerEntry>> dictionary = new Dictionary<string, Dictionary<string, CareerEntry>>(); foreach (string line in lines) { if (IsSkippable(line)) { continue; } int num = line.IndexOf('|'); int num2 = line.LastIndexOf('='); if (num <= 0 || num2 <= num + 1) { continue; } string key = line.Substring(0, num); string key2 = line.Substring(num + 1, num2 - num - 1); string[] array = line.Substring(num2 + 1).Split(';'); CareerEntry value = default(CareerEntry); if (TryInt(array[0], out value.Value)) { if (array.Length > 1) { TryLong(array[1], out value.FirstUtc); } if (array.Length > 2) { TryLong(array[2], out value.LastUtc); } if (!dictionary.TryGetValue(key, out var value2)) { value2 = (dictionary[key] = new Dictionary<string, CareerEntry>()); } value2[key2] = value; } } return dictionary; } internal static string SerializeTotals(IReadOnlyDictionary<string, Dictionary<string, CareerEntry>> live, IReadOnlyDictionary<string, Dictionary<string, CareerEntry>> saved, IEnumerable<string>? merged = null) { StringBuilder stringBuilder = new StringBuilder("# statscore career v1").Append('\n'); if (merged != null) { foreach (string item in merged) { stringBuilder.Append("# merged ").Append(item).Append('\n'); } } foreach (KeyValuePair<string, Dictionary<string, CareerEntry>> item2 in live) { foreach (KeyValuePair<string, CareerEntry> item3 in item2.Value) { AppendTotal(stringBuilder, item2.Key, item3.Key, item3.Value); } } foreach (KeyValuePair<string, Dictionary<string, CareerEntry>> item4 in saved) { if (live.ContainsKey(item4.Key)) { continue; } foreach (KeyValuePair<string, CareerEntry> item5 in item4.Value) { AppendTotal(stringBuilder, item4.Key, item5.Key, item5.Value); } } return stringBuilder.ToString(); } internal static HashSet<string> ParseMerged(IEnumerable<string> lines) { HashSet<string> hashSet = new HashSet<string>(); foreach (string line in lines) { if (line.StartsWith("# merged ", StringComparison.Ordinal) && line.Length > "# merged ".Length) { hashSet.Add(line.Substring("# merged ".Length).Trim()); } } return hashSet; } internal static void MergeTotals(Dictionary<string, Dictionary<string, CareerEntry>> target, IReadOnlyDictionary<string, Dictionary<string, CareerEntry>> source) { foreach (KeyValuePair<string, Dictionary<string, CareerEntry>> item in source) { if (!target.TryGetValue(item.Key, out Dictionary<string, CareerEntry> value)) { value = new Dictionary<string, CareerEntry>(); target[item.Key] = value; } foreach (KeyValuePair<string, CareerEntry> item2 in item.Value) { if (!value.TryGetValue(item2.Key, out var value2)) { value[item2.Key] = item2.Value; continue; } long val = (long)value2.Value + (long)item2.Value.Value; value2.Value = (int)Math.Max(-2147483648L, Math.Min(2147483647L, val)); value2.FirstUtc = Earlier(value2.FirstUtc, item2.Value.FirstUtc); value2.LastUtc = Math.Max(value2.LastUtc, item2.Value.LastUtc); value[item2.Key] = value2; } } } internal static void MergeDaily(Dictionary<string, Dictionary<string, Dictionary<int, int>>> target, IReadOnlyDictionary<string, Dictionary<string, Dictionary<int, int>>> source) { foreach (KeyValuePair<string, Dictionary<string, Dictionary<int, int>>> item in source) { if (!target.TryGetValue(item.Key, out Dictionary<string, Dictionary<int, int>> value)) { value = new Dictionary<string, Dictionary<int, int>>(); target[item.Key] = value; } foreach (KeyValuePair<string, Dictionary<int, int>> item2 in item.Value) { if (!value.TryGetValue(item2.Key, out var value2)) { value[item2.Key] = new Dictionary<int, int>(item2.Value); continue; } foreach (KeyValuePair<int, int> item3 in item2.Value) { value2[item3.Key] = (value2.TryGetValue(item3.Key, out var value3) ? (value3 + item3.Value) : item3.Value); } } } } private static long Earlier(long a, long b) { if (a != 0L) { if (b != 0L) { return Math.Min(a, b); } return a; } return b; } private static void AppendTotal(StringBuilder sb, string key, string slot, CareerEntry entry) { sb.Append(key).Append('|').Append(SanitizeSlot(slot)) .Append('=') .Append(entry.Value.ToString(CultureInfo.InvariantCulture)) .Append(';') .Append(entry.FirstUtc.ToString(CultureInfo.InvariantCulture)) .Append(';') .Append(entry.LastUtc.ToString(CultureInfo.InvariantCulture)) .Append('\n'); } internal static Dictionary<string, Dictionary<string, Dictionary<int, int>>> ParseDaily(IEnumerable<string> lines) { Dictionary<string, Dictionary<string, Dictionary<int, int>>> dictionary = new Dictionary<string, Dictionary<string, Dictionary<int, int>>>(); foreach (string line in lines) { if (IsSkippable(line)) { continue; } int num = line.IndexOf('|'); int num2 = ((num < 0) ? (-1) : line.IndexOf('|', num + 1)); if (num <= 0 || num2 <= num) { continue; } string key = line.Substring(0, num); string key2 = line.Substring(num + 1, num2 - num - 1); Dictionary<int, int> dictionary2 = new Dictionary<int, int>(); string[] array = line.Substring(num2 + 1).Split(';'); foreach (string text in array) { int num3 = text.IndexOf('='); if (num3 > 0 && TryInt(text.Substring(0, num3), out var value) && TryInt(text.Substring(num3 + 1), out var value2)) { dictionary2[value] = value2; } } if (dictionary2.Count != 0) { if (!dictionary.TryGetValue(key, out var value3)) { value3 = (dictionary[key] = new Dictionary<string, Dictionary<int, int>>()); } value3[key2] = dictionary2; } } return dictionary; } internal static string SerializeDaily(IReadOnlyDictionary<string, Dictionary<string, Dictionary<int, int>>> live, IReadOnlyDictionary<string, Dictionary<string, Dictionary<int, int>>> saved) { StringBuilder stringBuilder = new StringBuilder("# statscore career-daily v1").Append('\n'); foreach (KeyValuePair<string, Dictionary<string, Dictionary<int, int>>> item in live) { foreach (KeyValuePair<string, Dictionary<int, int>> item2 in item.Value) { AppendDaily(stringBuilder, item.Key, item2.Key, item2.Value); } } foreach (KeyValuePair<string, Dictionary<string, Dictionary<int, int>>> item3 in saved) { if (live.ContainsKey(item3.Key)) { continue; } foreach (KeyValuePair<string, Dictionary<int, int>> item4 in item3.Value) { AppendDaily(stringBuilder, item3.Key, item4.Key, item4.Value); } } return stringBuilder.ToString(); } private static void AppendDaily(StringBuilder sb, string key, string slot, IEnumerable<KeyValuePair<int, int>> byDay) { StringBuilder stringBuilder = new StringBuilder(); bool flag = false; foreach (KeyValuePair<int, int> item in byDay) { if (flag) { stringBuilder.Append(';'); } stringBuilder.Append(item.Key.ToString(CultureInfo.InvariantCulture)).Append('=').Append(item.Value.ToString(CultureInfo.InvariantCulture)); flag = true; } if (flag) { sb.Append(key).Append('|').Append(SanitizeSlot(slot)) .Append('|') .Append(stringBuilder) .Append('\n'); } } private static bool IsSkippable(string line) { if (!string.IsNullOrEmpty(line)) { return line[0] == '#'; } return true; } private static bool TryInt(string s, out int value) { return int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); } private static bool TryLong(string s, out long value) { return long.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out value); } } internal static class DayMath { internal static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified); internal static int IndexOf(DateTime when) { if (when.Kind == DateTimeKind.Utc) { when = when.ToLocalTime(); } return (when.Date - Epoch).Days; } internal static int Today() { return IndexOf(DateTime.Now); } internal static DateTime DateOf(int dayIndex) { return Epoch.AddDays(dayIndex); } } 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>>>(); private static readonly HashSet<string> _merged = new HashSet<string>(); private static readonly List<string> _retire = new List<string>(); internal static bool Dirty { get; private set; } private static string Dir => Path.Combine(Application.persistentDataPath, "StatsCore"); private static string LegacyDir => Path.Combine(Paths.ConfigPath, "StatsCore"); private static string FilePath => Path.Combine(Dir, "career.txt"); private static string DailyFilePath => Path.Combine(Dir, "career-daily.txt"); internal static void MarkDirty() { Dirty = true; } private static void EnsureLoaded() { if (_loaded) { return; } _loaded = true; Load(FilePath, delegate(string[] lines) { foreach (KeyValuePair<string, Dictionary<string, CareerEntry>> item in CareerFile.ParseTotals(lines)) { _saved[item.Key] = item.Value; } _merged.UnionWith(CareerFile.ParseMerged(lines)); }, "career file"); Load(DailyFilePath, delegate(string[] lines) { foreach (KeyValuePair<string, Dictionary<string, Dictionary<int, int>>> item2 in CareerFile.ParseDaily(lines)) { _savedDaily[item2.Key] = item2.Value; } }, "career daily file"); MergeLegacy(); } private static void MergeLegacy() { string text = Path.Combine(LegacyDir, "career.txt"); string text2 = Path.Combine(LegacyDir, "career-daily.txt"); bool flag = File.Exists(text); bool flag2 = File.Exists(text2); if (!flag && !flag2) { return; } string item = Token(text, text2); if (!_merged.Contains(item)) { try { string[] lines = (flag ? File.ReadAllLines(text) : Array.Empty<string>()); string[] lines2 = (flag2 ? File.ReadAllLines(text2) : Array.Empty<string>()); CareerFile.MergeTotals(_saved, CareerFile.ParseTotals(lines)); CareerFile.MergeDaily(_savedDaily, CareerFile.ParseDaily(lines2)); } catch (Exception ex) { StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't read this profile's older career file: " + ex.Message)); return; } _merged.Add(item); MarkDirty(); StatsCorePlugin.Logger.LogInfo((object)"[StatsCore] folded this profile's career history into the shared one"); } if (flag) { _retire.Add(text); } if (flag2) { _retire.Add(text2); } } private static string Token(string totalsPath, string dailyPath) { string s = Path.GetFullPath(LegacyDir) + "|" + Describe(totalsPath) + "|" + Describe(dailyPath); using SHA256 sHA = SHA256.Create(); return BitConverter.ToString(sHA.ComputeHash(Encoding.UTF8.GetBytes(s))).Replace("-", "").Substring(0, 16) .ToLowerInvariant(); } private static string Describe(string path) { if (!File.Exists(path)) { return "none"; } FileInfo fileInfo = new FileInfo(path); return fileInfo.Length + ":" + fileInfo.LastWriteTimeUtc.Ticks; } private static void RetireLegacy() { foreach (string item in _retire) { try { string text = item + ".migrated"; if (File.Exists(text)) { text = item + "." + DateTime.UtcNow.ToString("yyyyMMddHHmmss") + ".migrated"; } File.Move(item, text); } catch (Exception ex) { StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't set aside " + Path.GetFileName(item) + ": " + ex.Message)); } } _retire.Clear(); } private static void Load(string path, Action<string[]> parse, string what) { string[] array = new string[2] { path, path + ".bak" }; foreach (string text in array) { if (!File.Exists(text)) { continue; } try { parse(File.ReadAllLines(text)); if (text != path) { StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] " + what + " was missing or unreadable - recovered from " + Path.GetFileName(text))); } break; } catch (Exception ex) { StatsCorePlugin.Logger.LogWarning((object)("[StatsCore] couldn't read " + what + " (" + Path.GetFileName(text) + "): " + ex.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; } EnsureLoaded(); try { Directory.CreateDirectory(Dir); Dictionary<string, Dictionary<string, CareerEntry>> dictionary = new Dictionary<string, Dictionary<string, CareerEntry>>(); Dictionary<string, Dictionary<string, Dictionary<int, int>>> dictionary2 = new Dictionary<string, Dictionary<string, Dictionary<int, int>>>(); foreach (Stat item in Stats.All) { if (item.Lifetime != StatLifetime.Career) { continue; } Dictionary<string, CareerEntry> dictionary3 = new Dictionary<string, CareerEntry>(); foreach (KeyValuePair<string, int> item2 in item.Raw) { dictionary3[item2.Key] = new CareerEntry { Value = item2.Value, FirstUtc = item.FirstUtc(item2.Key), LastUtc = item.LastUtc(item2.Key) }; } dictionary[item.Key] = dictionary3; if (!item.TracksDaily) { continue; } Dictionary<string, Dictionary<int, int>> dictionary4 = new Dictionary<string, Dictionary<int, int>>(); foreach (KeyValuePair<string, Dictionary<int, int>> item3 in item.RawDaily) { dictionary4[item3.Key] = item3.Value; } dictionary2[item.Key] = dictionary4; } WriteAtomic(FilePath, CareerFile.SerializeTotals(dictionary, _saved, _merged)); WriteAtomic(DailyFilePath, CareerFile.SerializeDaily(dictionary2, _savedDaily)); Dirty = false; RetireLegacy(); } 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, path + ".bak"); } 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(); RunManager instance = RunManager.instance; if ((Object)(object)instance == (Object)null) { return; } TickSamples(); GameStats.SampleLocalMovement(Time.deltaTime); 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 HashSet<int> Answered = new HashSet<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"); return; } _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(typeof(StatsNet), "AttachPostfix", (Type[])null), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); if ((Object)(object)PunManager.instance != (Object)null) { Attach(PunManager.instance); } } private static void AttachPostfix(PunManager __instance) { Attach(__instance); } private static void Attach(PunManager manager) { if ((Object)(object)((Component)manager).GetComponent<StatsNet>() == (Object)null) { ((Component)manager).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) //IL_0032: 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.Answered.Add(info.Sender.ActorNumber)) { 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 OnPlayerLeftRoom(Player otherPlayer) { if (!PhotonNetwork.IsMasterClient) { return; } foreach (int item in new List<int>(_pulls.Keys)) { if (_pulls.TryGetValue(item, out PullRequest value) && !value.Completed && !value.Answered.Contains(otherPlayer.ActorNumber)) { value.Pending--; if (value.Pending <= 0) { CompletePull(item); } } } } public override void OnPlayerEnteredRoom(Player newPlayer) { if (PhotonNetwork.IsMasterClient) { SendSnapshot(newPlayer); } } public override void OnMasterClientSwitched(Player newMasterClient) { if (PhotonNetwork.IsMasterClient) { SendSnapshot(null); _pulls.Clear(); } } private void SendSnapshot(Player? target) { foreach (string item in BuildSnapshotChunks()) { if (target != null) { ((MonoBehaviourPun)this).photonView.RPC("StatsCoreSnapshotRPC", target, new object[1] { item }); } else { ((MonoBehaviourPun)this).photonView.RPC("StatsCoreSnapshotRPC", (RpcTarget)1, 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)"); } } internal static class PauseLayout { private const float DefaultStep = 32f; internal static (float QuitY, float ButtonY) Place(IReadOnlyList<float> column, float quitY) { if (column.Count == 0) { return (QuitY: quitY, ButtonY: quitY + 32f); } float[] array = column.OrderByDescending((float y) => y).ToArray(); float num = 32f; for (int num2 = 1; num2 < array.Length; num2++) { num = ((num2 == 1) ? (array[0] - array[1]) : Math.Min(num, array[num2 - 1] - array[num2])); } float num3 = array[^1]; float num4 = Math.Max(0f, 2f * num - (num3 - quitY)); return (QuitY: quitY - num4, ButtonY: num3 - num); } } internal static class StatFormatter { private static readonly CultureInfo Inv = CultureInfo.InvariantCulture; public static string Value(long value, StatFormat format) { return format switch { StatFormat.Time => Duration(value), StatFormat.Distance => Distance(value), StatFormat.Money => ((value < 0) ? "-$" : "$") + Math.Abs(value).ToString("N0", Inv), StatFormat.Percent => value.ToString("N0", Inv) + "%", _ => value.ToString("N0", Inv), }; } public static string Duration(long seconds) { if (seconds < 0) { seconds = 0L; } if (seconds < 60) { return seconds + "s"; } if (seconds < 3600) { return seconds / 60 + "m " + (seconds % 60).ToString("00", Inv) + "s"; } if (seconds < 86400) { return seconds / 3600 + "h " + (seconds % 3600 / 60).ToString("00", Inv) + "m"; } return seconds / 86400 + "d " + seconds % 86400 / 3600 + "h"; } public static string Distance(long meters) { if (meters < 1000) { return meters.ToString("N0", Inv) + " m"; } double num = (double)meters / 1000.0; return ((num < 100.0) ? num.ToString("0.0", Inv) : num.ToString("N0", Inv)) + " km"; } public static string Fallback(string key) { string text = key.Substring(key.LastIndexOf('.') + 1); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < text.Length; i++) { char c = text[i]; if (c == '_' || c == '-') { stringBuilder.Append(' '); continue; } if (i > 0 && char.IsUpper(c) && !char.IsUpper(text[i - 1]) && text[i - 1] != '_') { stringBuilder.Append(' '); } stringBuilder.Append((i == 0 || stringBuilder[stringBuilder.Length - 1] == ' ') ? char.ToUpperInvariant(c) : c); } if (stringBuilder.Length != 0) { return stringBuilder.ToString(); } return key; } public static bool LooksLikeSteamId(string key) { if (key.Length < 15) { return false; } foreach (char c in key) { if (c < '0' || c > '9') { return false; } } return true; } } internal enum StatsView { Career, Today, Week, Run } internal enum LineKind { Header, Row, Sub, Note } internal readonly struct Line { public readonly LineKind Kind; public readonly string Text; public readonly string Value; public readonly bool Dim; public Line(LineKind kind, string text, string value = "", bool dim = false) { Kind = kind; Text = text; Value = value; Dim = dim; } } internal static class StatsModel { internal const string AllCategories = "All"; internal static readonly string[] ViewNames = new string[4] { "Career", "Today", "Last 7 days", "This run" }; private static readonly string[] CategoryOrder = new string[7] { "Glory", "Mobility", "Expeditions", "Misfortune", "Recovery", "Mischief", "Expedition" }; private const int BreakdownRows = 6; internal static string LocalId() { string text = (((Object)(object)PlayerAvatar.instance != (Object)null) ? PlayerAvatar.instance.steamID : null); if (!string.IsNullOrEmpty(text)) { return text; } Dictionary<string, int> dictionary = new Dictionary<string, int>(); foreach (Stat item in Stats.All) { if (item.Lifetime != StatLifetime.Career) { continue; } foreach (string key in item.Entries.Keys) { if (StatFormatter.LooksLikeSteamId(key)) { dictionary[key] = ((!dictionary.TryGetValue(key, out var value)) ? 1 : (value + 1)); } } } if (dictionary.Count != 0) { return dictionary.OrderByDescending((KeyValuePair<string, int> c) => c.Value).First().Key; } return ""; } internal static string[] CategoryNames() { HashSet<string> hashSet = new HashSet<string>(Stats.All.Select(CategoryOf)); List<string> list = CategoryOrder.Where(hashSet.Contains).ToList(); list.AddRange(hashSet.Except(list).OrderBy<string, string>((string c) => c, StringComparer.OrdinalIgnoreCase)); list.Insert(0, "All"); return list.ToArray(); } internal static List<Line> Build(StatsView view, string category) { string id = LocalId(); List<Line> list = new List<Line>(); if (view == StatsView.Career && category == "All") { AddHighlights(list, id); } foreach (IGrouping<string, Stat> item in (from g in Stats.All.Where((Stat s) => Shown(s, view) && (category == "All" || CategoryOf(s) == category)).GroupBy(CategoryOf) orderby CategoryRank(g.Key) select g).ThenBy<IGrouping<string, Stat>, string>((IGrouping<string, Stat> g) => g.Key, StringComparer.OrdinalIgnoreCase)) { List<Line> list2 = new List<Line>(); foreach (Stat item2 in item) { AddStat(list2, item2, view, id); } if (list2.Count != 0) { list.Add(new Line(LineKind.Header, item.Key)); list.AddRange(list2); } } if (list.Count == 0) { list.Add(new Line(LineKind.Note, EmptyMessage(view))); } return list; } private static bool Shown(Stat stat, StatsView view) { switch (view) { case StatsView.Career: return stat.Lifetime == StatLifetime.Career; case StatsView.Today: case StatsView.Week: return stat.Lifetime == StatLifetime.Career && stat.TracksDaily; default: return stat.Lifetime != StatLifetime.Career; } } private static string CategoryOf(Stat stat) { if (!string.IsNullOrWhiteSpace(stat.Category)) { return stat.Category; } return "Other"; } private static int CategoryRank(string category) { int num = Array.IndexOf(CategoryOrder, category); if (num >= 0) { return num; } return CategoryOrder.Length; } private static string NameOf(Stat stat) { if (!string.IsNullOrWhiteSpace(stat.DisplayName) && !(stat.DisplayName == stat.Key)) { return stat.DisplayName; } return StatFormatter.Fallback(stat.Key); } private static void AddStat(List<Line> rows, Stat stat, StatsView view, string id) { List<KeyValuePair<string, int>> list = (from e in stat.Entries where e.Key != "_team" && !StatFormatter.LooksLikeSteamId(e.Key) && e.Value > 0 orderby e.Value descending select e).ToList(); if (list.Count > 0) { if (view == StatsView.Today || view == StatsView.Week) { return; } rows.Add(new Line(LineKind.Row, NameOf(stat), StatFormatter.Value(((IEnumerable<KeyValuePair<string, int>>)list).Sum((Func<KeyValuePair<string, int>, long>)((KeyValuePair<string, int> e) => e.Value)), stat.Format))); { foreach (KeyValuePair<string, int> item in list.Take(6)) { rows.Add(new Line(LineKind.Sub, item.Key, StatFormatter.Value(item.Value, stat.Format))); } return; } } long num = view switch { StatsView.Today => stat.GetToday(id), StatsView.Week => stat.GetLastDays(id, 7), _ => (stat.Scope == StatScope.Team) ? stat.GetTeam() : stat.Get(id), }; if (num != 0L || (view != StatsView.Today && view != StatsView.Week)) { rows.Add(new Line(LineKind.Row, NameOf(stat), StatFormatter.Value(num, stat.Format), num == 0)); } } private static void AddHighlights(List<Line> lines, string id) { long num = 0L; foreach (Stat item in Stats.All) { if (item.Lifetime != StatLifetime.Career) { continue; } foreach (string key in item.Entries.Keys) { long num2 = item.FirstUtc(key); if (num2 > 0 && (num == 0L || num2 < num)) { num = num2; } } } List<Line> list = new List<Line>(); if (num > 0) { list.Add(new Line(LineKind.Row, "Tracked since", DateTimeOffset.FromUnixTimeSeconds(num).LocalDateTime.ToString("d MMM yyyy", CultureInfo.InvariantCulture))); } AddTopEntry(list, "statscore.deathsByEnemy", "Most deadly"); AddTopEntry(list, "statscore.mapVisits", "Favorite level"); if (list.Count != 0) { lines.Add(new Line(LineKind.Header, "Highlights")); lines.AddRange(list); } } private static void AddTopEntry(List<Line> into, string key, string label) { Stat stat = Stats.Get(key); if (stat != null) { KeyValuePair<string, int> keyValuePair = (from e in stat.Entries where e.Key != "_team" && !StatFormatter.LooksLikeSteamId(e.Key) && e.Value > 0 orderby e.Value descending select e).FirstOrDefault(); if (keyValuePair.Key != null) { into.Add(new Line(LineKind.Row, label, keyValuePair.Key + " (" + StatFormatter.Value(keyValuePair.Value, StatFormat.Count) + ")")); } } } private static string EmptyMessage(StatsView view) { return view switch { StatsView.Today => "Nothing recorded today yet.", StatsView.Week => "Nothing recorded this week yet.", StatsView.Run => "Nothing tracked for this run yet.", _ => "Nothing here yet. Play a round and come back.", }; } } internal static class StatsPage { private const string RowPrefix = "StatsRow"; private static readonly Color Gold = new Color(1f, 0.78f, 0.26f); private static readonly Color Orange = new Color(1f, 0.55f, 0f); private static readonly Color Gray = new Color(0.72f, 0.72f, 0.72f); private static REPOPopupPage? _page; private static TMP_Text? _font; private static float _rowWidth = 254f; private static StatsView _view = StatsView.Career; private static string _category = "All"; internal static void Open() { //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Expected O, but got Unknown if ((Object)(object)MenuManager.instance == (Object)null) { return; } _view = StatsView.Career; _category = "All"; REPOPopupPage page = MenuAPI.CreateREPOPopupPage("Stats", (PresetSide)0, false, true, 2f); _page = page; AddSelector(page, "View", StatsModel.ViewNames, StatsModel.ViewNames[0], delegate(string choice) { _view = (StatsView)Math.Max(0, Array.IndexOf(StatsModel.ViewNames, choice)); Rebuild(); }); AddSelector(page, "Category", StatsModel.CategoryNames(), "All", delegate(string choice) { _category = choice; Rebuild(); }); Fill(page); page.AddElement((BuilderDelegate)delegate(Transform parent) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) MenuAPI.CreateREPOButton("Back", (Action)delegate { page.ClosePage(true); }, parent, new Vector2(66f, 18f)); }); page.menuPage.interactionPriority = 0; page.OpenPage(false); page.menuPage.interactionPriority = MenuManager.instance.HighestOpenPriority() + 1; ((Behaviour)page.scrollView).enabled = false; ((MonoBehaviour)page).StartCoroutine(Remeasure(page)); } private static void AddSelector(REPOPopupPage page, string label, string[] options, string current, Action<string> onChange) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown page.AddElementToScrollView((ScrollViewBuilderDelegate)delegate(Transform sv) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) REPOSlider val = MenuAPI.CreateREPOSlider(label, "", (Action<string>)delegate(string choice) { onChange(choice); }, sv, options, current, Vector2.zero, "", "", (BarBehavior)0); Rect rect = ((REPOElement)val).rectTransform.rect; float width = ((Rect)(ref rect)).width; if (width > 0f) { _rowWidth = width; } if (_font == null) { _font = (TMP_Text?)(object)val.labelTMP; } return ((REPOElement)val).rectTransform; }, 0f, 0f); } private static void Rebuild() { REPOPopupPage page = _page; if ((Object)(object)page == (Object)null) { return; } REPOScrollViewElement[] componentsInChildren = ((Component)page.scrollView).GetComponentsInChildren<REPOScrollViewElement>(true); foreach (REPOScrollViewElement val in componentsInChildren) { if (((Object)val).name.StartsWith("StatsRow")) { ((Component)val).transform.SetParent((Transform)null); Object.Destroy((Object)(object)((Component)val).gameObject); } } Fill(page); ((MonoBehaviour)page).StartCoroutine(Remeasure(page)); } private static void Fill(REPOPopupPage page) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_0101: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_0120: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_013d: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Unknown result type (might be due to invalid IL or missing references) foreach (Line item in StatsModel.Build(_view, _category)) { switch (item.Kind) { case LineKind.Header: AddRow(page, item.Text, "", 22f, 34f, Orange, Orange, 0f, 10f, 2f, rule: false); continue; case LineKind.Sub: AddRow(page, item.Text, item.Value, 14f, 20f, Gray, Gray, 12f, 0f, 0f, rule: false); continue; case LineKind.Note: AddRow(page, item.Text, "", 15f, 32f, Gray, Gray, 0f, 8f, 0f, rule: false); continue; } Color nameColor = (item.Dim ? Fade(White()) : White()); Color valueColor = (item.Dim ? Fade(Gold) : Gold); AddRow(page, item.Text, item.Value, 17f, 24f, nameColor, valueColor, 0f, 0f, 0f, rule: true); } } private static Color White() { //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) if (!((Object)(object)_font != (Object)null)) { return Color.white; } return ((Graphic)_font).color; } private static Color Fade(Color c) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) return new Color(c.r, c.g, c.b, 0.45f); } private static void AddRow(REPOPopupPage page, string name, string value, float size, float height, Color nameColor, Color valueColor, float indent, float top, float bottom, bool rule) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Expected O, but got Unknown page.AddElementToScrollView((ScrollViewBuilderDelegate)delegate(Transform sv) { //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00fc: Unknown result type (might be due to invalid IL or missing references) //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_0147: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0154: Unknown result type (might be due to invalid IL or missing references) //IL_0159: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0162: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_018c: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) //IL_01c4: Unknown result type (might be due to invalid IL or missing references) int layer = ((Component)sv).gameObject.layer; RectTransform val = (RectTransform)new GameObject("StatsRow - " + name, new Type[1] { typeof(RectTransform) }) { layer = layer }.transform; ((Transform)val).SetParent(sv, false); Vector2 anchorMin = (val.anchorMax = Vector2.zero); val.anchorMin = anchorMin; val.pivot = Vector2.zero; val.sizeDelta = new Vector2(_rowWidth, height); ((Transform)val).localPosition = Vector3.zero; float num = (string.IsNullOrEmpty(value) ? 0f : (_rowWidth * 0.36f)); AddText(val, name, size, nameColor, (TextAlignmentOptions)4097, indent, _rowWidth - indent - num - 6f, layer); if (num > 0f) { AddText(val, value, size, valueColor, (TextAlignmentOptions)4100, 0f, num, layer); } if (rule) { GameObject val2 = new GameObject("Rule", new Type[2] { typeof(RectTransform), typeof(Image) }) { layer = layer }; RectTransform val3 = (RectTransform)val2.transform; ((Transform)val3).SetParent((Transform)(object)val, false); val3.anchorMin = Vector2.zero; val3.anchorMax = new Vector2(1f, 0f); val3.pivot = new Vector2(0.5f, 0f); val3.sizeDelta = new Vector2(0f, 1f); Image component = val2.GetComponent<Image>(); ((Graphic)component).color = new Color(1f, 1f, 1f, 0.08f); ((Graphic)component).raycastTarget = false; } return val; }, top, bottom); } private static void AddText(RectTransform parent, string text, float size, Color color, TextAlignmentOptions align, float indent, float room, int layer) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Invalid comparison between Unknown and I4 //IL_00a7: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Text", new Type[2] { typeof(RectTransform), typeof(TextMeshProUGUI) }) { layer = layer }; RectTransform val2 = (RectTransform)val.transform; ((Transform)val2).SetParent((Transform)(object)parent, false); val2.anchorMin = Vector2.zero; val2.anchorMax = Vector2.one; Vector2 offsetMin = (val2.offsetMax = Vector2.zero); val2.offsetMin = offsetMin; TextMeshProUGUI component = val.GetComponent<TextMeshProUGUI>(); if ((Object)(object)_font != (Object)null) { ((TMP_Text)component).font = _font.font; ((TMP_Text)component).fontSharedMaterial = _font.fontSharedMaterial; ((TMP_Text)component).fontStyle = _font.fontStyle; } ((Graphic)component).color = color; ((TMP_Text)component).alignment = align; ((Graphic)component).raycastTarget = false; ((TMP_Text)component).enableWordWrapping = false; ((TMP_Text)component).overflowMode = (TextOverflowModes)0; ((TMP_Text)component).margin = new Vector4(indent, 0f, ((int)align == 4100) ? 2f : 0f, 0f); ((TMP_Text)component).fontSize = size; ((TMP_Text)component).text = text; float x = ((TMP_Text)component).GetPreferredValues(text).x; if (room > 0f && x > room) { ((TMP_Text)component).fontSize = Mathf.Max(9f, size * room / x); } } private static IEnumerator Remeasure(REPOPopupPage page) { yield return (object)new WaitForSecondsRealtime(0.3f); if (Object.op_Implicit((Object)(object)page)) { page.menuScrollBox.RecalculateScrollHeight(true); page.scrollView.UpdateElements(); } } } internal static class StatsScreenEntry { [CompilerGenerated] private static class <>O { public static BuilderDelegate <0>__Build; } internal static void Install(ConfigFile config) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Expected O, but got Unknown ConfigEntry<bool> obj = config.Bind<bool>("Stats screen", "Main menu button", true, "Show a Stats button in the main menu."); ConfigEntry<bool> val = config.Bind<bool>("Stats screen", "Pause menu button", true, "Show a Stats button in the pause menu."); if (obj.Value) { new Harmony("Vippy.StatsCore.Screen").PatchAll(typeof(MainMenuPatch)); } if (val.Value) { object obj2 = <>O.<0>__Build; if (obj2 == null) { BuilderDelegate val2 = PauseButton.Build; <>O.<0>__Build = val2; obj2 = (object)val2; } MenuAPI.AddElementToEscapeMenu((BuilderDelegate)obj2); } } } [HarmonyPatch(typeof(MenuPageMain), "Start")] internal static class MainMenuPatch { [HarmonyPostfix] private static void Postfix(MenuPageMain __instance) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Expected O, but got Unknown //IL_015e: Unknown result type (might be due to invalid IL or missing references) //IL_0163: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) Transform val = ((Component)__instance).transform.Find("LeftTransform/Buttons"); if ((Object)(object)val == (Object)null) { return; } VerticalLayoutGroup val2 = default(VerticalLayoutGroup); if (!((Component)val).TryGetComponent<VerticalLayoutGroup>(ref val2)) { VerticalLayoutGroup obj = ((Component)val).gameObject.AddComponent<VerticalLayoutGroup>(); ((HorizontalOrVerticalLayoutGroup)obj).childControlWidth = false; ((HorizontalOrVerticalLayoutGroup)obj).childForceExpandWidth = false; } REPOButton val3 = MenuAPI.CreateREPOButton("Stats", (Action)StatsPage.Open, val, Vector2.zero); List<Transform> list = new List<Transform>(); foreach (Transform item in val) { Transform val4 = item; if ((Object)(object)val4 != (Object)(object)((Component)val3).transform && (((Object)val4).name.Contains("Menu Button") || (Object)(object)((Component)val4).GetComponent<REPOButton>() != (Object)null)) { list.Add(val4); } } list = list.OrderByDescending((Transform t) => t.localPosition.y).ToList(); if (list.Count == 0) { return; } list.Insert(list.Count - 1, ((Component)val3).transform); ((Component)val3).transform.SetSiblingIndex(list[list.Count - 1].GetSiblingIndex()); float num = 230f; foreach (Transform item2 in list) { Vector3 localPosition = item2.localPosition; localPosition.y = num; item2.localPosition = localPosition; num -= 30f; } } } internal static class PauseButton { internal static void Build(Transform page) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Expected O, but got Unknown //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_009e: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) Transform val = page.Find("Menu Button - Quit game"); if ((Object)(object)val == (Object)null) { return; } List<float> list = new List<float>(); foreach (Transform item in page) { Transform val2 = item; if (!((Object)(object)val2 == (Object)(object)val) && ((Component)val2).gameObject.activeSelf && !((Object)(object)((Component)val2).GetComponent<MenuButton>() == (Object)null) && !((Object)(object)((Component)val2).GetComponent<REPOButton>() != (Object)null) && !(Mathf.Abs(val2.localPosition.x - val.localPosition.x) > 1f) && val2.localPosition.y > val.localPosition.y) { list.Add(val2.localPosition.y); } } var (num, num2) = PauseLayout.Place(list, val.localPosition.y); val.localPosition = new Vector3(val.localPosition.x, num, val.localPosition.z); MenuAPI.CreateREPOButton("Stats", (Action)StatsPage.Open, page, new Vector2(val.localPosition.x, num2)); } } }