Decompiled source of SidsCompetitiveRounds v1.28.2
plugins\CompetitiveRounds.dll
Decompiled a day ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Net.WebSockets; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using ExitGames.Client.Photon; using HarmonyLib; using InControl; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using Steamworks; using UnityEngine; using UnityEngine.Events; using UnityEngine.Networking; using UnityEngine.Rendering.PostProcessing; using UnityEngine.SceneManagement; [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: IgnoresAccessChecksTo("PhotonRealtime")] [assembly: IgnoresAccessChecksTo("PhotonUnityNetworking")] [assembly: AssemblyCompany("CompetitiveRounds")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+878af82075371aad57e60ea72a1f31862239cca9")] [assembly: AssemblyProduct("CompetitiveRounds")] [assembly: AssemblyTitle("CompetitiveRounds")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.0.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.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace CompetitiveRounds { public static class ApiClient { public class RecentSeriesEntry { public string winner_name; public string p1_name; public string p2_name; public int p1_wins; public int p2_wins; public int p1_rating; public int p2_rating; public float p1_rating_change; public float p2_rating_change; public string winner_steam_id; public string p1_steam_id; public string p2_steam_id; public string completed_at; public List<SeriesBetEntry> bets = new List<SeriesBetEntry>(); } public class SeriesBetEntry { public string bettor_name; public string bet_on_name; public string bettor_steam_id; public string bet_on_steam_id; public int amount; public int payout; public float odds_multiplier; public bool won; } [Serializable] public class MatchResponse { public string match_id; public string winner_steam_id; public string message; } [Serializable] public class LeaderboardData { public LeaderboardEntry[] entries; public int total_players; } [Serializable] public class LeaderboardEntry { public int rank; public string steam_id; public string display_name; public int rating; public int rd; public int total_matches; public int wins; public int losses; public float win_rate; public int level; public int gold; public string title; public string title_color; } [Serializable] public class PlayerStatsData { public string steam_id; public string display_name; public float rating; public float rating_deviation; public float peak_rating; public int total_matches; public int wins; public int losses; public float win_rate; public bool ranked_enabled; public int level; public int total_xp; public int xp_into_level; public int xp_for_next_level; public int best_ranked_streak; public int best_casual_streak; public int ranked_series_wins; public int ranked_series_losses; public int ranked_dc_count; public string discord_id; public string discord_username; public int gold_earned; public int gold_spent; public long bullets_fired; public long bullets_hit; public long blocks_activated; public long blocks_successful; public string active_title; public string active_title_color; public string active_trail_sku; public string active_trail_color; public int active_trail_price; public string active_color_sku; public List<string> active_color_skus; public string active_player_color_sku; public string active_player_color_hex; public string active_player_color_name; public string active_cursor_color_sku; public string active_cursor_color_hex; public string active_player_effect_sku; public bool hide_gold; public List<string> active_nametag_skus; public List<string> top_card_names; public List<int> top_card_picks; public List<float> top_card_win_rates; public List<string> recent_form; public List<float> rating_history; public int avg_fps; public float avg_cards_per_game; public int achievements_unlocked; public List<string> worst_card_names; public List<int> worst_card_picks; public List<float> worst_card_win_rates; public List<string> region_names; public List<int> region_matches; public string mod_version; public int h2h_ranked_wins; public int h2h_ranked_losses; public int h2h_casual_wins; public int h2h_casual_losses; public int h2h_series_wins; public int h2h_series_losses; } [Serializable] public class CardStatData { public string card_name; public string card_rarity; public int times_picked; public int wins_with_card; public float win_rate; public int times_offered; public float pass_rate; } [Serializable] private class CardStatsWrapper { public CardStatData[] items; } [Serializable] public class ModCheckResponse { public bool registered; public bool ranked; public string display_name; public bool banned; public string ban_reason; } [Serializable] public class MatchHistoryEntry { public string match_id; public string opponent_steam_id; public string opponent_name; public string opponent_title; public string opponent_title_color; public int player_rounds_won; public int opponent_rounds_won; public int player_points; public int opponent_points; public bool won; public bool is_ranked; public string ended_at; public string cards_display; public string opp_cards_display; public string series_id; public string series_score; public float series_rating_change; public int xp_gained; public int gold_gained; public int series_gold_gained; public int player_fps_avg; public int opponent_fps_avg; } [Serializable] private class MatchHistoryWrapper { public MatchHistoryEntry[] items; } [Serializable] public class AchievementData { public string achievement_key; public bool unlocked; public string unlocked_at; } [Serializable] public class ActiveSeriesEntry { public string series_id; public string p1_steam_id; public string p1_name; public int p1_rating; public int p1_wins; public float p1_odds; public string p2_steam_id; public string p2_name; public int p2_rating; public int p2_wins; public float p2_odds; public int live_p1_points; public int live_p2_points; public bool bets_locked; public string lock_reason; public bool is_private; public bool is_tournament; public string tournament_kind; public string phase; } public class MyBetEntry { public string series_id; public string bet_on_steam_id; public string bet_on_name; public string series_status; public string series_score; public int amount; public float odds_multiplier; } [Serializable] public class ActiveTeamSeriesEntry { public string series_id; public string t1a_steam; public string t1a_name; public string t1b_steam; public string t1b_name; public string t2a_steam; public string t2a_name; public string t2b_steam; public string t2b_name; public int t1_rating; public int t2_rating; public int t1a_rating; public int t1b_rating; public int t2a_rating; public int t2b_rating; public int t1_wins; public int t2_wins; public float t1_odds; public float t2_odds; public bool bets_locked; public string lock_reason; public string started_at; public string dc_grace_until; } [Serializable] public class ShopItemData { public long id; public string sku; public string kind; public string name; public string description; public int price; public string rarity; public string preview_color; public bool owned; } public class FlaggedMatchEntry { public string id; public string match_id; public string series_id; public string flag_reason; public string p1_name; public string p2_name; public List<string> player_steam_ids = new List<string>(); public bool auto_invalidated; public bool match_invalidated; public bool is_ranked; public int duration_seconds; public string review_action; public string created_at; public string flag_details_summary; } public class BannedUserEntry { public string id; public string steam_id; public string display_name; public string reason; public string banned_by_steam_id; public string banned_at; } [Serializable] public class BugReportSummary { public string id; public int bug_number; public string created_at; public string steam_id; public string display_name; public string mod_version; public string severity; public string category; public string status; public string description; public bool has_log; public int log_bytes; } [Serializable] public class BugReportEventEntry { public string id; public string actor_steam_id; public string actor_name; public string event_type; public string old_status; public string new_status; public string comment; public string created_at; } [Serializable] public class BugReportDetail { public string id; public int bug_number; public string steam_id; public string display_name; public string mod_version; public string game_version; public string severity; public string category; public string description; public string repro_steps; public string status; public string triage_notes; public string created_at; public string log_text; public int log_bytes; public List<BugReportEventEntry> events = new List<BugReportEventEntry>(); } public enum QueueState { Idle, Searching, Matched, ReadySent } [Serializable] public class QueuePollData { public string status; public int wait_time; public int queue_size; public int elo_range; public string opponent_steam_id; public string opponent_name; public float opponent_rating; public bool opponent_ready; public string room_name; public string photon_region; } public enum TeamQueueState { Idle, Searching, Matched, ReadySent } [Serializable] public class TeamQueueMember { public string steam_id; public string display_name; public int rating; public string region; public int team_assigned; public bool using_fallback_rating; public int balance_rating; public int completed_series; public bool ready; } [Serializable] public class TeamQueuePollData { public string status; public int queue_count; public int elo_range; public string series_id; public int team_assigned; public List<TeamQueueMember> teammates = new List<TeamQueueMember>(); public List<TeamQueueMember> opponents = new List<TeamQueueMember>(); public string room_name; public string room_region; public int match_age_seconds; public bool my_ready; } [Serializable] public class TeamQueueListEntry { public string steam_id; public string display_name; public int rating; public int balance_rating; public bool using_fallback_rating; public int completed_series; public string region; public string status; public int team_assigned; public string series_id; public int wait_seconds; public bool manual_pick_enabled; public int preferred_team; public string queue_type; } [Serializable] public class TeamLeaderboardEntry { public int rank; public string steam_id; public string display_name; public int rating; public int rd; public int completed_series; public int series_wins; public int series_losses; public float win_rate; public int level; public string title; public string title_color; public int avg_teammate_elo; public int team_gold_earned; public int team_xp_earned; } [Serializable] public class TeamSeriesSlot { public string steam_id; public string name; public string title; public string title_color; public int rating; public float rating_change; public int gold_earned; public int xp_earned; } [Serializable] public class TeamSeriesMatch { public string match_id; public string ended_at; public int t1_rounds_won; public int t2_rounds_won; public int t1_points_total; public int t2_points_total; public Dictionary<string, List<string>> cards_by_player = new Dictionary<string, List<string>>(); } [Serializable] public class TeamSeriesPagedEntry { public string series_id; public string completed_at; public int winner_team; public int t1_series_wins; public int t2_series_wins; public TeamSeriesSlot t1a; public TeamSeriesSlot t1b; public TeamSeriesSlot t2a; public TeamSeriesSlot t2b; public List<TeamSeriesMatch> matches = new List<TeamSeriesMatch>(); } public class TeamMatchHistoryEntry { public string match_id; public string series_id; public string ended_at; public bool won; public int my_team; public string t1a_steam_id; public string t1a_name; public string t1b_steam_id; public string t1b_name; public string t2a_steam_id; public string t2a_name; public string t2b_steam_id; public string t2b_name; public int t1_rounds_won; public int t2_rounds_won; public int t1_points_total; public int t2_points_total; public string series_score; public float series_rating_change; public Dictionary<string, int> fps_by_player = new Dictionary<string, int>(); public Dictionary<string, List<string>> cards_by_player = new Dictionary<string, List<string>>(); } [Serializable] public class TeamStatsData { public string steam_id; public string display_name; public float rating; public float rating_deviation; public float peak_rating; public int completed_series; public int series_wins; public int series_losses; public float series_win_rate; public int match_wins; public int match_losses; public int current_streak; } [Serializable] public class TournamentSnapshot { public string tournament_id; public string status; public string kind; public string default_start_ts; public string scheduled_start_ts; public string lock_at; public string started_at; public string ended_at; public int min_players; public int max_players; public string my_signup_id; public bool my_ready; public float my_penalty_pct; public bool my_discord_linked; public int force_vote_count; public string photon_region; public string[] my_votes; public string[] time_slot_options; public TimeVoteTally[] time_slot_tallies; public TournamentSignupRow[] signups; public TournamentMatchRow[] matches; } [Serializable] public class TimeVoteTally { public string slot_ts; public int votes; } [Serializable] public class TournamentSignupRow { public string signup_id; public string steam_id; public string display_name; public bool is_speculative; public int seed; public float penalty_at_signup; public bool ready; public bool forfeited; public int placed_rank; public string progress_label; } [Serializable] public class TournamentMatchRow { public string match_id; public int round; public string bracket_side; public int slot_idx; public string p1_signup_id; public string p2_signup_id; public string p1_display_name; public string p2_display_name; public bool is_bye; public string status; public string series_id; public string winner_signup_id; public int p1_series_wins; public int p2_series_wins; public string deadline_at; public string[] prereq_match_ids; public string photon_room_name; } [Serializable] public class PlayerTournamentHistory { public string steam_id; public int winner_count; public int runner_up_count; public int third_place_count; public int participant_count; public PlayerTournamentEntry[] recent; } [Serializable] public class PlayerTournamentEntry { public string tournament_id; public string ended_at; public int placed_rank; public string kind; public int signup_count; public string winner_display_name; } [Serializable] public class ActiveTournamentMatch { public string tournament_id; public string kind; public string match_id; public string status; public string bracket_side; public int round; public string opponent_steam_id; public string opponent_display_name; public string photon_room_name; public string photon_region; public bool my_ready; public bool opp_ready; } private static string baseUrl = ""; private static float lastFetchTime = 0f; private static float fetchCooldown = 5f; public static readonly Dictionary<string, string[]> AchievementDefs = new Dictionary<string, string[]> { { "untouchable", new string[2] { "Untouchable", "Win a game without taking any damage" } }, { "silent_assassin", new string[2] { "Silent Assassin", "5-0 someone with Sneaky" } }, { "total_mayhem", new string[2] { "Total Mayhem", "5-0 someone with Mayhem" } }, { "fragile_perfection", new string[2] { "Fragile Perfection", "5-0 someone with Glass Cannon" } }, { "no_escape", new string[2] { "No Escape", "5-0 someone with Chase" } }, { "rise_from_the_ashes", new string[2] { "Rise from the Ashes", "Win 5-0 with Phoenix without losing a life" } }, { "the_comeback_kid", new string[2] { "The Comeback Kid", "Win after being down 0-4" } }, { "stacked_deck", new string[2] { "Stacked Deck", "Get 5 copies of one card in a game" } }, { "regicide", new string[2] { "Regicide", "Win against Sid in a ranked series" } }, { "pacifist", new string[2] { "Pacifist", "Win a game without firing a single shot" } }, { "immovable_object", new string[2] { "Immovable Object", "Win a game without moving or jumping" } }, { "master_rank", new string[2] { "Master", "Reach 2030 rating in ranked (1v1 or 2v2)" } }, { "team_sweep", new string[2] { "Tag Team Sweep", "Win a 2v2 game 5-0" } } }; private static readonly HashSet<string> _heartbeatDispatchedMatches = new HashSet<string>(); private const string GITHUB_API_LATEST = "https://api.github.com/repos/SidNDeed/SidsCompetitiveRounds/releases/latest"; private static readonly byte[] _hkE = new byte[48] { 226, 96, 125, 70, 209, 29, 174, 212, 243, 51, 60, 47, 71, 237, 154, 50, 215, 106, 11, 24, 165, 4, 227, 236, 220, 80, 34, 24, 21, 166, 231, 46, 216, 70, 3, 51, 167, 90, 209, 209, 253, 120, 28, 58, 2, 144, 239, 45 }; private static readonly byte[] _hkX = new byte[16] { 178, 25, 79, 119, 230, 111, 150, 181, 186, 10, 106, 95, 119, 223, 174, 75 }; private static string selectedAchSteamId = ""; public static string ActiveRankedSeriesId; private static float queuePollTimer = 0f; private static float queuePollInterval = 3f; private static float queueCountTimer = 0f; private static float queueCountInterval = 10f; private static float teamQueueListTimer = 0f; private const float TEAM_QUEUE_LIST_INTERVAL = 2f; private static float teamQueuePollTimer = 0f; private static float teamQueueCountTimer = 0f; private const float TEAM_QUEUE_POLL_INTERVAL = 3f; private const float TEAM_QUEUE_COUNT_INTERVAL = 10f; public static string LastSeriesStateStatus = null; public static string LastSeriesStateReason = null; public static int LastSeriesStateConfirmations = 0; public static int LastSeriesDcGraceSeconds = 0; public static int LastSeriesDcTeamRemaining = 0; public static int LastSeriesT1Wins = 0; public static int LastSeriesT2Wins = 0; private static float teamSeriesStateTimer = 0f; private const float TEAM_SERIES_STATE_INTERVAL = 2f; private static bool blocksLoaded = false; public static float LastApiSuccessAt = -999f; public static float LastApiAttemptAt = -999f; public static bool LastResponseWasMaintenance = false; public const int BUG_REPORT_LOG_CAP_CHARS = 3500000; public static string CachedTournamentJson; public static TournamentSnapshot CachedTournament; private static float _tournamentRefreshAt; public static string TournamentKind = "sync"; public static readonly Dictionary<string, PlayerTournamentHistory> CachedPlayerTournaments = new Dictionary<string, PlayerTournamentHistory>(); public static List<ActiveTournamentMatch> CachedMyActiveTournamentMatches = new List<ActiveTournamentMatch>(); private static float _myActiveMatchesRefreshAt; public static PlayerTournamentEntry[] CachedSiteTournamentHistory; public static string[] CachedSiteTournamentHistoryNames; public static LeaderboardData CachedLeaderboard { get; private set; } public static PlayerStatsData CachedPlayerStats { get; private set; } public static List<CardStatData> CachedCardStats { get; private set; } public static bool IsLoading { get; private set; } = false; public static string LastError { get; private set; } = ""; public static string LatestModVersion { get; private set; } = null; public static string MinModVersion { get; private set; } = null; public static bool ForceUpdateRequired { get; private set; } = false; public static List<RecentSeriesEntry> CachedRecentSeries { get; private set; } public static List<MatchHistoryEntry> CachedMatchHistory { get; private set; } public static Dictionary<string, AchievementData> CachedAchievements { get; private set; } public static bool IsUpdating { get; private set; } = false; public static bool UpdateReady { get; private set; } = false; public static List<ActiveSeriesEntry> CachedActiveSeries { get; private set; } public static List<MyBetEntry> CachedMyBets { get; private set; } = new List<MyBetEntry>(); public static List<ActiveTeamSeriesEntry> CachedActiveTeamSeries { get; private set; } = new List<ActiveTeamSeriesEntry>(); public static List<ShopItemData> CachedShopItems { get; private set; } public static List<ShopItemData> CachedInventory { get; private set; } public static bool IsAdmin { get; private set; } public static List<FlaggedMatchEntry> CachedFlaggedMatches { get; private set; } = new List<FlaggedMatchEntry>(); public static List<BannedUserEntry> CachedBannedUsers { get; private set; } = new List<BannedUserEntry>(); public static List<BugReportSummary> CachedBugReports { get; private set; } = new List<BugReportSummary>(); public static BugReportDetail CachedBugReportDetail { get; set; } public static Dictionary<string, AchievementData> SelectedPlayerAchievements { get; private set; } public static QueueState CurrentQueueState { get; private set; } = QueueState.Idle; public static QueuePollData LastPollData { get; private set; } public static bool IsQueuePolling { get; private set; } = false; public static int CachedQueueSearching { get; private set; } = 0; public static int CachedQueueTotal { get; private set; } = 0; public static List<TeamQueueListEntry> CachedTeamQueueList { get; private set; } = new List<TeamQueueListEntry>(); public static List<TeamQueueListEntry> CachedTeamQueueAuto { get; private set; } = new List<TeamQueueListEntry>(); public static List<TeamQueueListEntry> CachedTeamQueueManual { get; private set; } = new List<TeamQueueListEntry>(); public static TeamQueueState CurrentTeamQueueState { get; private set; } = TeamQueueState.Idle; public static TeamQueuePollData LastTeamPollData { get; private set; } public static string ActiveTeamSeriesId { get; set; } public static bool IsTeamQueuePolling { get; private set; } = false; public static int CachedTeamQueueSearching { get; private set; } = 0; public static string CurrentTeamQueueType { get; private set; } = "auto"; public static List<TeamLeaderboardEntry> CachedTeamLeaderboard { get; private set; } = new List<TeamLeaderboardEntry>(); public static int CachedTeamLeaderboardTotal { get; private set; } = 0; public static string CachedTeamLeaderboardSort { get; private set; } = "rating"; public static List<TeamSeriesPagedEntry> CachedTeamSeriesPaged { get; private set; } = new List<TeamSeriesPagedEntry>(); public static int CachedTeamSeriesTotal { get; private set; } = 0; public static int CachedTeamSeriesPage { get; private set; } = 0; public static int CachedTeamSeriesPageSize { get; private set; } = 3; public static int CachedTeamSeriesTotalPages { get; private set; } = 0; public static List<TeamMatchHistoryEntry> CachedTeamMatchHistory { get; private set; } = new List<TeamMatchHistoryEntry>(); public static TeamStatsData CachedTeamStats { get; private set; } public static HashSet<string> BlockedSteamIds { get; private set; } = new HashSet<string>(); public static bool ApiLooksDown { get { if (LastApiAttemptAt < 0f) { return false; } float unscaledTime = Time.unscaledTime; if (unscaledTime - LastApiAttemptAt > 15f) { return false; } return unscaledTime - LastApiSuccessAt > 10f; } } public static void Initialize(string url) { baseUrl = url.TrimEnd('/'); Plugin.Log.LogInfo((object)("API client initialized: " + baseUrl)); CheckModVersion(); ((MonoBehaviour)Plugin.Instance).StartCoroutine(TournamentHeartbeatLoop()); } private static IEnumerator TournamentHeartbeatLoop() { yield return (object)new WaitForSeconds(10f); while (true) { yield return (object)new WaitForSeconds(20f); string sid = MatchTracker.LocalSteamId; if (string.IsNullOrEmpty(sid) || sid == "unknown") { continue; } FetchMyActiveTournamentMatches(sid); if (CachedMyActiveTournamentMatches == null) { continue; } HashSet<string> seen = new HashSet<string>(); foreach (ActiveTournamentMatch m in CachedMyActiveTournamentMatches) { if (!string.IsNullOrEmpty(m.tournament_id) && !seen.Contains(m.tournament_id)) { seen.Add(m.tournament_id); TournamentReady(m.tournament_id, sid); yield return (object)new WaitForSeconds(0.2f); if (m.kind == "sync" && m.status == "ready" && m.my_ready && m.opp_ready && !string.IsNullOrEmpty(m.photon_room_name) && !string.IsNullOrEmpty(m.match_id) && !_heartbeatDispatchedMatches.Contains(m.match_id) && Plugin.PendingRankedRoom != m.photon_room_name) { _heartbeatDispatchedMatches.Add(m.match_id); Plugin.SetPendingRoom(m.photon_room_name, m.photon_region); Plugin.Log.LogInfo((object)("[TOURNAMENT-HB] Dispatch from heartbeat loop: room=" + m.photon_room_name + " region=" + (m.photon_region ?? "default") + " match=" + m.match_id)); } } } if (CachedMyActiveTournamentMatches.Count == 0) { _heartbeatDispatchedMatches.Clear(); continue; } HashSet<string> stillActive = new HashSet<string>(); foreach (ActiveTournamentMatch cachedMyActiveTournamentMatch in CachedMyActiveTournamentMatches) { if (!string.IsNullOrEmpty(cachedMyActiveTournamentMatch.match_id)) { stillActive.Add(cachedMyActiveTournamentMatch.match_id); } } _heartbeatDispatchedMatches.RemoveWhere((string id) => !stillActive.Contains(id)); } } public static void CheckModVersion() { ((MonoBehaviour)Plugin.Instance).StartCoroutine(DoCheckModVersion()); } private static IEnumerator DoCheckModVersion() { UnityWebRequest req = UnityWebRequest.Get(baseUrl + "/api/v1/mod-version"); yield return req.SendWebRequest(); if ((int)req.result != 1) { yield break; } string text = ExtractJsonString(req.downloadHandler.text, "version"); string text2 = ExtractJsonString(req.downloadHandler.text, "min_version"); if (!string.IsNullOrEmpty(text)) { LatestModVersion = text; if (text != "1.28.2") { Plugin.Log.LogWarning((object)("[VERSION] Update available: v1.28.2 → v" + text)); } else { Plugin.Log.LogInfo((object)("[VERSION] Mod is up to date (v" + text + ")")); } } if (!string.IsNullOrEmpty(text2)) { MinModVersion = text2; if (CompareVersion("1.28.2", text2) < 0) { ForceUpdateRequired = true; Plugin.Log.LogWarning((object)("[VERSION] Mod is BELOW server minimum (v1.28.2 < v" + text2 + ") — update required")); } } if (!string.IsNullOrEmpty(LatestModVersion) && CompareVersion("1.28.2", LatestModVersion) < 0 && !IsUpdating && !UpdateReady) { Plugin.Log.LogInfo((object)"[VERSION] Auto-firing update on launch"); StartAutoUpdate(); } } private static int CompareVersion(string a, string b) { if (string.IsNullOrEmpty(a) || string.IsNullOrEmpty(b)) { return 0; } string[] array = a.Split('.'); string[] array2 = b.Split('.'); int num = Math.Max(array.Length, array2.Length); for (int i = 0; i < num; i++) { int.TryParse((i < array.Length) ? array[i] : "0", out var result); int.TryParse((i < array2.Length) ? array2[i] : "0", out var result2); if (result != result2) { if (result >= result2) { return 1; } return -1; } } return 0; } public static void StartAutoUpdate() { if (!IsUpdating && !UpdateReady) { IsUpdating = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(DoAutoUpdate()); } } private static IEnumerator DoAutoUpdate() { Plugin.Log.LogInfo((object)"[UPDATE] Thunderstore build — auto-update disabled, use mod manager"); CompetitiveUI.ShowNotification("Update available — update through your mod manager", Color.cyan, 6f); IsUpdating = false; NativeUI.MarkDirty(); yield break; } private static byte[] GetHmacKeyBytes() { byte[] array = new byte[_hkE.Length]; for (int i = 0; i < _hkE.Length; i++) { array[i] = (byte)(_hkE[i] ^ _hkX[i % _hkX.Length]); } return array; } private static string ComputeHmac(string p1SteamId, string p2SteamId, int p1Rounds, int p2Rounds, bool isRanked, string reporterSteamId, string roomId) { string s = $"{p1SteamId}:{p2SteamId}:{p1Rounds}:{p2Rounds}:" + (isRanked ? "true" : "false") + ":" + reporterSteamId + ":" + roomId; try { using HMACSHA256 hMACSHA = new HMACSHA256(GetHmacKeyBytes()); byte[] array = hMACSHA.ComputeHash(Encoding.UTF8.GetBytes(s)); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[HMAC] Computation failed: " + ex.Message)); return ""; } } private static string ComputeHmacHex(string message) { try { using HMACSHA256 hMACSHA = new HMACSHA256(GetHmacKeyBytes()); byte[] array = hMACSHA.ComputeHash(Encoding.UTF8.GetBytes(message)); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } catch { return ""; } } public static void FetchActiveSeries() { ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/series/active", delegate(bool ok, string resp) { if (ok) { List<ActiveSeriesEntry> list = new List<ActiveSeriesEntry>(); try { string[] array = resp.Split(new string[1] { "\"series_id\":" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = "\"series_id\":" + array[i]; ActiveSeriesEntry activeSeriesEntry = new ActiveSeriesEntry { series_id = ExtractJsonString(text, "series_id"), p1_steam_id = ExtractJsonString(text, "p1_steam_id"), p1_name = ExtractJsonString(text, "p1_name"), p1_rating = ExtractJsonInt(text, "p1_rating"), p1_wins = ExtractJsonInt(text, "p1_wins"), p1_odds = ExtractJsonFloat(text, "p1_odds"), p2_steam_id = ExtractJsonString(text, "p2_steam_id"), p2_name = ExtractJsonString(text, "p2_name"), p2_rating = ExtractJsonInt(text, "p2_rating"), p2_wins = ExtractJsonInt(text, "p2_wins"), p2_odds = ExtractJsonFloat(text, "p2_odds"), live_p1_points = ExtractJsonInt(text, "live_p1_points"), live_p2_points = ExtractJsonInt(text, "live_p2_points"), bets_locked = (text.Contains("\"bets_locked\":true") || text.Contains("\"bets_locked\": true")), lock_reason = ExtractJsonString(text, "lock_reason"), is_private = (text.Contains("\"is_private\":true") || text.Contains("\"is_private\": true")), is_tournament = (text.Contains("\"is_tournament\":true") || text.Contains("\"is_tournament\": true")), tournament_kind = ExtractJsonString(text, "tournament_kind"), phase = ExtractJsonString(text, "phase") }; if (!string.IsNullOrEmpty(activeSeriesEntry.series_id)) { list.Add(activeSeriesEntry); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BET] active-series parse: " + ex.Message)); } CachedActiveSeries = list; NativeUI.MarkDirty(); } })); } public static void FetchMyBets(string steamId) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown") { return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/players/" + steamId + "/bets?limit=50", delegate(bool ok, string resp) { if (ok) { List<MyBetEntry> list = new List<MyBetEntry>(); try { string[] array = resp.Split(new string[1] { "\"id\":" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string json = array[i]; MyBetEntry myBetEntry = new MyBetEntry { amount = ExtractJsonInt(json, "amount"), odds_multiplier = ExtractJsonFloat(json, "odds_multiplier"), series_id = ExtractJsonString(json, "series_id"), bet_on_steam_id = ExtractJsonString(json, "bet_on_steam_id"), bet_on_name = ExtractJsonString(json, "bet_on_name"), series_status = ExtractJsonString(json, "series_status"), series_score = ExtractJsonString(json, "series_score") }; if (!string.IsNullOrEmpty(myBetEntry.series_id)) { list.Add(myBetEntry); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BET] my-bets parse: " + ex.Message)); } CachedMyBets = list; NativeUI.MarkDirty(); } })); } public static MyBetEntry GetMyBetForSeries(string seriesId) { if (string.IsNullOrEmpty(seriesId) || CachedMyBets == null) { return null; } foreach (MyBetEntry cachedMyBet in CachedMyBets) { if (cachedMyBet.series_id == seriesId) { return cachedMyBet; } } return null; } public static void PostLivePoints(string seriesId, string reporterSteamId, int p1Points, int p2Points) { if (!string.IsNullOrEmpty(seriesId) && !string.IsNullOrEmpty(reporterSteamId)) { string text = ComputeHmacHex($"live-points:{seriesId}:{reporterSteamId}:{p1Points}:{p2Points}"); string url = baseUrl + "/api/v1/series/" + Escape(seriesId) + "/live-points" + $"?p1_points={p1Points}&p2_points={p2Points}" + "&reporter_steam_id=" + Escape(reporterSteamId) + "&sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[LIVE-POINTS] series={seriesId.Substring(0, Math.Min(8, seriesId.Length))} {p1Points}-{p2Points} ok={ok}"); })); } } public static void PlaceBet(string bettorSteamId, string seriesId, string betOnSteamId, int amount, Action<bool, string> callback) { string text = ComputeHmacHex($"bet:{bettorSteamId}:{seriesId}:{betOnSteamId}:{amount}"); string url = $"{baseUrl}/api/v1/bets?steam_id={Escape(bettorSteamId)}&series_id={Escape(seriesId)}&bet_on_steam_id={Escape(betOnSteamId)}&amount={amount}&sig={text}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[BET] place {amount} on {betOnSteamId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(bettorSteamId); FetchActiveSeries(); } })); } public static void FetchActiveTeamSeries() { ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/team/series/active", delegate(bool success, string response) { if (!success) { return; } try { ParseActiveTeamSeries(response); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[TEAM-BET] active parse: " + ex.Message)); } })); } private static void ParseActiveTeamSeries(string response) { List<ActiveTeamSeriesEntry> list = new List<ActiveTeamSeriesEntry>(); int num = response.IndexOf("\"series\""); if (num < 0) { CachedActiveTeamSeries = list; return; } int num2 = response.IndexOf('[', num); int num3 = FindMatchingBracket(response, num2); if (num2 < 0 || num3 < 0) { CachedActiveTeamSeries = list; return; } string text = response.Substring(num2 + 1, num3 - num2 - 1); int num4 = 0; while (num4 < text.Length) { int num5 = text.IndexOf('{', num4); if (num5 < 0) { break; } int num6 = 1; int i; for (i = num5 + 1; i < text.Length; i++) { if (num6 <= 0) { break; } if (text[i] == '{') { num6++; } else if (text[i] == '}') { num6--; } } if (num6 != 0) { break; } string json = text.Substring(num5, i - num5); list.Add(new ActiveTeamSeriesEntry { series_id = ExtractJsonString(json, "series_id"), t1a_steam = ExtractJsonString(json, "t1a_steam"), t1a_name = ExtractJsonString(json, "t1a_name"), t1b_steam = ExtractJsonString(json, "t1b_steam"), t1b_name = ExtractJsonString(json, "t1b_name"), t2a_steam = ExtractJsonString(json, "t2a_steam"), t2a_name = ExtractJsonString(json, "t2a_name"), t2b_steam = ExtractJsonString(json, "t2b_steam"), t2b_name = ExtractJsonString(json, "t2b_name"), t1_rating = ExtractJsonInt(json, "t1_rating"), t2_rating = ExtractJsonInt(json, "t2_rating"), t1a_rating = ExtractJsonInt(json, "t1a_rating"), t1b_rating = ExtractJsonInt(json, "t1b_rating"), t2a_rating = ExtractJsonInt(json, "t2a_rating"), t2b_rating = ExtractJsonInt(json, "t2b_rating"), t1_wins = ExtractJsonInt(json, "t1_wins"), t2_wins = ExtractJsonInt(json, "t2_wins"), t1_odds = ExtractJsonFloat(json, "t1_odds"), t2_odds = ExtractJsonFloat(json, "t2_odds"), bets_locked = ExtractJsonBool(json, "bets_locked"), lock_reason = ExtractJsonString(json, "lock_reason"), started_at = ExtractJsonString(json, "started_at"), dc_grace_until = ExtractJsonString(json, "dc_grace_until") }); num4 = i; } CachedActiveTeamSeries = list; } public static void PlaceTeamBet(string bettorSteamId, string seriesId, int betOnTeam, int amount, Action<bool, string> callback) { string text = ComputeHmacHex($"team-bet:{bettorSteamId}:{seriesId}:{betOnTeam}:{amount}"); string url = $"{baseUrl}/api/v1/team-bets?steam_id={Escape(bettorSteamId)}&team_series_id={Escape(seriesId)}&bet_on_team={betOnTeam}&amount={amount}&sig={text}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[TEAM-BET] place {amount} on team {betOnTeam}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(bettorSteamId); FetchActiveTeamSeries(); } })); } public static void FetchShopItems(string steamId = null) { string text = baseUrl + "/api/v1/shop/items"; if (!string.IsNullOrEmpty(steamId)) { text = text + "?steam_id=" + Escape(steamId); } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(text, delegate(bool success, string response) { if (!success) { Plugin.Log.LogWarning((object)("[SHOP] list failed: " + response)); } else { CachedShopItems = ParseShopItems(response); Plugin.Log.LogInfo((object)$"[SHOP] loaded {CachedShopItems.Count} items"); NativeUI.MarkDirty(); } })); } public static void FetchInventory(string steamId) { if (string.IsNullOrEmpty(steamId)) { return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/players/" + steamId + "/inventory", delegate(bool success, string response) { if (success) { CachedInventory = ParseShopItems(response); NativeUI.MarkDirty(); } })); } public static void PurchaseItem(string steamId, string sku, Action<bool, string> callback) { Plugin.Log.LogInfo((object)("[SHOP] PurchaseItem ENTRY sku=" + sku + " steamId=" + steamId)); try { string text = ComputeHmacHex("buy:" + steamId + ":" + sku); Plugin.Log.LogInfo((object)$"[SHOP] sig computed ({text?.Length ?? 0} chars)"); string text2 = baseUrl + "/api/v1/shop/purchase?steam_id=" + steamId + "&sku=" + sku + "&sig=" + text; Plugin.Log.LogInfo((object)("[SHOP] POST " + text2.Substring(0, Math.Min(text2.Length, 100)) + "...")); ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)string.Format("[SHOP] purchase callback {0}: ok={1} resp={2}", sku, ok, (resp != null && resp.Length > 120) ? (resp.Substring(0, 120) + "...") : resp)); try { callback?.Invoke(ok, resp); } catch (Exception ex2) { Plugin.Log.LogWarning((object)("[SHOP] callback threw: " + ex2.Message)); } if (ok) { FetchPlayerStats(steamId); FetchShopItems(steamId); FetchInventory(steamId); } })); Plugin.Log.LogInfo((object)"[SHOP] coroutine dispatched"); } catch (Exception ex) { Plugin.Log.LogError((object)$"[SHOP] PurchaseItem threw: {ex}"); try { callback?.Invoke(arg1: false, ex.Message); } catch { } } } public static void SetActiveTitle(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"title:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-title?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set title {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetActiveTrail(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"trail:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-trail?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set trail {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetActiveColor(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"color:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-color?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set color {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetActivePlayerColor(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"player_color:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-player-color?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set player_color {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetActiveCursorColor(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"cursor_color:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-cursor-color?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set cursor_color {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetActivePlayerEffect(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"player_effect:{steamId}:{itemId}"); string text2 = baseUrl + "/api/v1/players/" + steamId + "/active-player-effect?sig=" + text; if (itemId > 0) { text2 += $"&item_id={itemId}"; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(text2, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set player_effect {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void SetHideGold(string steamId, bool on, Action<bool, string> callback = null) { string text = ComputeHmacHex($"hide_gold:{steamId}:{(on ? 1 : 0)}"); string url = baseUrl + "/api/v1/players/" + steamId + "/hide-gold?on=" + (on ? "true" : "false") + "&sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] set hide_gold {on}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); FetchLeaderboard(); } })); } public static void ToggleNametagStyle(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"nametag:{steamId}:{itemId}"); string url = $"{baseUrl}/api/v1/players/{steamId}/nametag-toggle?item_id={itemId}&sig={text}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] toggle nametag {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } public static void ToggleMapColor(string steamId, long itemId, Action<bool, string> callback = null) { string text = ComputeHmacHex($"color:{steamId}:{itemId}"); string url = $"{baseUrl}/api/v1/players/{steamId}/color-toggle?item_id={itemId}&sig={text}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SHOP] toggle color {itemId}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } private static List<ShopItemData> ParseShopItems(string response) { List<ShopItemData> list = new List<ShopItemData>(); if (string.IsNullOrEmpty(response)) { return list; } string[] array = response.Split(new string[1] { "{\"id\":" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string json = "{\"id\":" + array[i]; ShopItemData shopItemData = new ShopItemData(); shopItemData.id = ExtractJsonInt(json, "id"); shopItemData.sku = ExtractJsonString(json, "sku"); shopItemData.kind = ExtractJsonString(json, "kind"); shopItemData.name = ExtractJsonString(json, "name"); shopItemData.description = ExtractJsonString(json, "description"); shopItemData.price = ExtractJsonInt(json, "price"); shopItemData.rarity = ExtractJsonString(json, "rarity"); shopItemData.preview_color = ExtractJsonString(json, "preview_color"); shopItemData.owned = ExtractJsonBool(json, "owned"); if (!string.IsNullOrEmpty(shopItemData.sku) || !string.IsNullOrEmpty(shopItemData.name)) { list.Add(shopItemData); } } return list; } public static void FetchRecentChat(int limit = 50) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest($"{baseUrl}/api/v1/chat/recent?limit={limit}", delegate(bool success, string response) { if (!success || string.IsNullOrEmpty(response)) { return; } try { string[] array = response.Split(new string[1] { "{\"source\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = "{\"source\"" + array[i]; int num = text.IndexOf("\"}"); if (num >= 0) { string obj = text.Substring(0, num + 2); try { ChatClient.OnMessage?.Invoke(obj); } catch { } } } Plugin.Log.LogInfo((object)$"[CHAT] Scrollback loaded: {array.Length - 1} messages"); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[CHAT] Scrollback parse error: " + ex.Message)); } })); } public static void DeletePlayerData(string steamId, Action<bool, string> callback) { if (string.IsNullOrEmpty(steamId)) { callback(arg1: false, "no-steam-id"); return; } string text = ComputeHmacHex("delete:" + steamId); string url = baseUrl + "/api/v1/players/" + steamId + "/data?sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(DoDelete(url, callback)); } private static IEnumerator DoDelete(string url, Action<bool, string> callback) { UnityWebRequest request = UnityWebRequest.Delete(url); try { request.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); StampVersionHeader(request); request.timeout = 20; yield return request.SendWebRequest(); bool flag = (int)request.result == 1; object arg; if (!flag) { arg = request.error; } else { DownloadHandler downloadHandler = request.downloadHandler; arg = ((downloadHandler != null) ? downloadHandler.text : null) ?? ""; } callback(flag, (string)arg); } finally { ((IDisposable)request)?.Dispose(); } } public static void CheckAdminStatus(string steamId, Action<bool> callback = null) { if (string.IsNullOrEmpty(steamId)) { IsAdmin = false; callback?.Invoke(obj: false); return; } string url = baseUrl + "/api/v1/admin/check-status?steam_id=" + steamId; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool ok, string body) { bool flag = false; if (ok && !string.IsNullOrEmpty(body)) { flag = body.Contains("\"is_admin\":true") || body.Contains("\"is_admin\": true"); } IsAdmin = flag; Plugin.Log.LogInfo((object)$"[ADMIN] check-status for {steamId}: is_admin={flag}"); callback?.Invoke(flag); })); } public static void FetchFlaggedMatches(string adminSteamId, bool includeReviewed = false, Action<bool> callback = null) { if (string.IsNullOrEmpty(adminSteamId)) { callback?.Invoke(obj: false); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":list_flagged:"); string url = baseUrl + "/api/v1/admin/flagged-matches?admin_steam_id=" + adminSteamId + "&hmac_signature=" + text + "&include_reviewed=" + (includeReviewed ? "true" : "false") + "&limit=50"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool ok, string body) { if (!ok) { Plugin.Log.LogWarning((object)("[ADMIN] flagged fetch failed: " + body)); callback?.Invoke(obj: false); return; } try { CachedFlaggedMatches = ParseFlaggedMatches(body); callback?.Invoke(obj: true); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ADMIN] flagged parse: " + ex.Message)); callback?.Invoke(obj: false); } })); } public static void FetchBannedUsers(string adminSteamId, Action<bool> callback = null) { if (string.IsNullOrEmpty(adminSteamId)) { callback?.Invoke(obj: false); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":list_bans:"); string url = baseUrl + "/api/v1/admin/banned-users?admin_steam_id=" + adminSteamId + "&hmac_signature=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool ok, string body) { if (!ok) { Plugin.Log.LogWarning((object)("[ADMIN] bans fetch failed: " + body)); callback?.Invoke(obj: false); return; } try { CachedBannedUsers = ParseBannedUsers(body); callback?.Invoke(obj: true); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ADMIN] bans parse: " + ex.Message)); callback?.Invoke(obj: false); } })); } public static void AdminBan(string adminSteamId, string targetSteamId, string reason, Action<bool, string> callback = null) { string text = ComputeHmacHex("admin:" + adminSteamId + ":ban:" + targetSteamId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"target_steam_id\":\"" + Escape(targetSteamId) + "\",\"reason\":\"" + Escape(reason) + "\",\"hmac_signature\":\"" + text + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/admin/ban", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void AdminUnban(string adminSteamId, string targetSteamId, Action<bool, string> callback = null) { string text = ComputeHmacHex("admin:" + adminSteamId + ":unban:" + targetSteamId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"target_steam_id\":\"" + Escape(targetSteamId) + "\",\"hmac_signature\":\"" + text + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/admin/unban", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void AdminGrantAchievement(string adminSteamId, string targetSteamId, string achievementKey, Action<bool, string> callback = null) { string text = ComputeHmacHex("admin:" + adminSteamId + ":grant_achievement:" + targetSteamId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"target_steam_id\":\"" + Escape(targetSteamId) + "\",\"achievement_key\":\"" + Escape(achievementKey) + "\",\"hmac_signature\":\"" + text + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/admin/grant-achievement", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void AdminReverseSeries(string adminSteamId, string seriesId, string reason, Action<bool, string> callback = null) { string text = ComputeHmacHex("admin:" + adminSteamId + ":reverse_series:" + seriesId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"series_id\":\"" + Escape(seriesId) + "\",\"reason\":\"" + Escape(reason) + "\",\"hmac_signature\":\"" + text + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/admin/reverse-series", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void AdminReviewFlag(string adminSteamId, string flagId, string action, Action<bool, string> callback = null) { string text = ComputeHmacHex("admin:" + adminSteamId + ":review_flag:" + flagId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"flag_id\":\"" + Escape(flagId) + "\",\"review_action\":\"" + Escape(action) + "\",\"hmac_signature\":\"" + text + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/admin/review-flag", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void FetchBugReports(string adminSteamId, Action<bool> callback = null) { if (string.IsNullOrEmpty(adminSteamId)) { callback?.Invoke(obj: false); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":bug_reports:list"); string url = baseUrl + "/api/v1/bug-reports?admin_steam_id=" + adminSteamId + "&hmac_signature=" + text + "&limit=100"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool ok, string body) { if (!ok) { Plugin.Log.LogWarning((object)("[BUG-REPORTS] fetch failed: " + body)); callback?.Invoke(obj: false); return; } try { CachedBugReports = ParseBugReportList(body); callback?.Invoke(obj: true); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BUG-REPORTS] parse: " + ex.Message)); callback?.Invoke(obj: false); } })); } public static void FetchBugReportDetail(string adminSteamId, string reportId, Action<bool> callback = null) { if (string.IsNullOrEmpty(adminSteamId) || string.IsNullOrEmpty(reportId)) { callback?.Invoke(obj: false); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":bug_reports:" + reportId); string url = baseUrl + "/api/v1/bug-reports/" + reportId + "?admin_steam_id=" + adminSteamId + "&hmac_signature=" + text + "&include_log=true"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool ok, string body) { if (!ok) { Plugin.Log.LogWarning((object)("[BUG-REPORTS] detail fetch failed: " + body)); callback?.Invoke(obj: false); return; } try { CachedBugReportDetail = ParseBugReportDetail(body); callback?.Invoke(obj: true); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BUG-REPORTS] detail parse: " + ex.Message)); callback?.Invoke(obj: false); } })); } private static List<BugReportSummary> ParseBugReportList(string json) { List<BugReportSummary> list = new List<BugReportSummary>(); if (string.IsNullOrEmpty(json)) { return list; } int num = json.IndexOf("\"reports\":["); if (num < 0) { return list; } int num2 = num + "\"reports\":[".Length; int num3 = 1; int i; for (i = num2; i < json.Length; i++) { if (num3 <= 0) { break; } if (json[i] == '[') { num3++; } else if (json[i] == ']') { num3--; } } if (num3 != 0) { return list; } string text = json.Substring(num2, i - num2 - 1); int num4 = 0; while (num4 < text.Length) { int num5 = text.IndexOf('{', num4); if (num5 < 0) { break; } int num6 = 1; int j; for (j = num5 + 1; j < text.Length; j++) { if (num6 <= 0) { break; } if (text[j] == '{') { num6++; } else if (text[j] == '}') { num6--; } } if (num6 != 0) { break; } string json2 = text.Substring(num5, j - num5); list.Add(new BugReportSummary { id = ExtractJsonString(json2, "id"), bug_number = ExtractJsonInt(json2, "bug_number"), created_at = ExtractJsonString(json2, "created_at"), steam_id = ExtractJsonString(json2, "steam_id"), display_name = ExtractJsonString(json2, "display_name"), mod_version = ExtractJsonString(json2, "mod_version"), severity = ExtractJsonString(json2, "severity"), category = ExtractJsonString(json2, "category"), status = ExtractJsonString(json2, "status"), description = ExtractJsonString(json2, "description"), has_log = ExtractJsonBool(json2, "has_log"), log_bytes = ExtractJsonInt(json2, "log_bytes") }); num4 = j; } return list; } private static BugReportDetail ParseBugReportDetail(string json) { BugReportDetail bugReportDetail = new BugReportDetail { id = ExtractJsonString(json, "id"), bug_number = ExtractJsonInt(json, "bug_number"), steam_id = ExtractJsonString(json, "steam_id"), display_name = ExtractJsonString(json, "display_name"), mod_version = ExtractJsonString(json, "mod_version"), game_version = ExtractJsonString(json, "game_version"), severity = ExtractJsonString(json, "severity"), category = ExtractJsonString(json, "category"), description = ExtractJsonString(json, "description"), repro_steps = ExtractJsonString(json, "repro_steps"), status = ExtractJsonString(json, "status"), triage_notes = ExtractJsonString(json, "triage_notes"), created_at = ExtractJsonString(json, "created_at"), log_text = ExtractJsonString(json, "log_text"), log_bytes = ExtractJsonInt(json, "log_bytes") }; try { int num = json.IndexOf("\"events\":["); if (num >= 0) { int num2 = num + "\"events\":[".Length; int num3 = 1; int i; for (i = num2; i < json.Length; i++) { if (num3 <= 0) { break; } if (json[i] == '[') { num3++; } else if (json[i] == ']') { num3--; } } if (num3 == 0) { string text = json.Substring(num2, i - num2 - 1); int num4 = 0; while (num4 < text.Length) { int num5 = text.IndexOf('{', num4); if (num5 < 0) { break; } int num6 = 1; int j; for (j = num5 + 1; j < text.Length; j++) { if (num6 <= 0) { break; } if (text[j] == '{') { num6++; } else if (text[j] == '}') { num6--; } } if (num6 == 0) { string json2 = text.Substring(num5, j - num5); bugReportDetail.events.Add(new BugReportEventEntry { id = ExtractJsonString(json2, "id"), actor_steam_id = ExtractJsonString(json2, "actor_steam_id"), actor_name = ExtractJsonString(json2, "actor_name"), event_type = ExtractJsonString(json2, "event_type"), old_status = ExtractJsonString(json2, "old_status"), new_status = ExtractJsonString(json2, "new_status"), comment = ExtractJsonString(json2, "comment"), created_at = ExtractJsonString(json2, "created_at") }); num4 = j; continue; } break; } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BUG-REPORTS] events parse: " + ex.Message)); } return bugReportDetail; } public static void AdminBugReportComment(string adminSteamId, string reportId, string comment, Action<bool, string> callback = null) { if (string.IsNullOrEmpty(adminSteamId) || string.IsNullOrEmpty(reportId)) { callback?.Invoke(arg1: false, "missing args"); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":bug_reports_comment:" + reportId); string json = "{\"admin_steam_id\":\"" + Escape(adminSteamId) + "\",\"hmac_signature\":\"" + text + "\",\"comment\":\"" + Escape(comment ?? "") + "\"}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(baseUrl + "/api/v1/bug-reports/" + reportId + "/comment", json, delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } public static void AdminBugReportStatus(string adminSteamId, string reportId, string newStatus, string comment, Action<bool, string> callback = null) { if (string.IsNullOrEmpty(adminSteamId) || string.IsNullOrEmpty(reportId) || string.IsNullOrEmpty(newStatus)) { callback?.Invoke(arg1: false, "missing args"); return; } string text = ComputeHmacHex("admin:" + adminSteamId + ":bug_reports_status:" + reportId); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{"); stringBuilder.Append("\"admin_steam_id\":\"" + Escape(adminSteamId) + "\""); stringBuilder.Append(",\"hmac_signature\":\"" + text + "\""); stringBuilder.Append(",\"new_status\":\"" + Escape(newStatus) + "\""); if (!string.IsNullOrEmpty(comment)) { stringBuilder.Append(",\"comment\":\"" + Escape(comment) + "\""); } stringBuilder.Append("}"); ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(baseUrl + "/api/v1/bug-reports/" + reportId + "/status", stringBuilder.ToString(), delegate(bool ok, string resp) { callback?.Invoke(ok, resp); })); } private static List<FlaggedMatchEntry> ParseFlaggedMatches(string json) { List<FlaggedMatchEntry> list = new List<FlaggedMatchEntry>(); if (string.IsNullOrEmpty(json)) { return list; } string[] array = json.Split(new string[1] { "\"id\":\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = array[i]; int num = text.IndexOf('"'); if (num >= 0) { FlaggedMatchEntry flaggedMatchEntry = new FlaggedMatchEntry(); flaggedMatchEntry.id = text.Substring(0, num); flaggedMatchEntry.match_id = ExtractJsonString(text, "match_id"); flaggedMatchEntry.series_id = ExtractJsonString(text, "series_id"); flaggedMatchEntry.flag_reason = ExtractJsonString(text, "flag_reason"); flaggedMatchEntry.p1_name = ExtractJsonString(text, "p1_name"); flaggedMatchEntry.p2_name = ExtractJsonString(text, "p2_name"); flaggedMatchEntry.auto_invalidated = text.Contains("\"auto_invalidated\":true") || text.Contains("\"auto_invalidated\": true"); flaggedMatchEntry.match_invalidated = text.Contains("\"match_invalidated\":true") || text.Contains("\"match_invalidated\": true"); flaggedMatchEntry.is_ranked = text.Contains("\"is_ranked\":true") || text.Contains("\"is_ranked\": true"); flaggedMatchEntry.duration_seconds = ExtractJsonInt(text, "duration_seconds"); flaggedMatchEntry.review_action = ExtractJsonString(text, "review_action"); flaggedMatchEntry.created_at = ExtractJsonString(text, "created_at"); flaggedMatchEntry.flag_details_summary = string.Format("{0} {1} {2}s {3}", flaggedMatchEntry.flag_reason, flaggedMatchEntry.is_ranked ? "R" : "C", flaggedMatchEntry.duration_seconds, flaggedMatchEntry.auto_invalidated ? "[auto-inv]" : "[advisory]"); list.Add(flaggedMatchEntry); } } return list; } private static List<BannedUserEntry> ParseBannedUsers(string json) { List<BannedUserEntry> list = new List<BannedUserEntry>(); if (string.IsNullOrEmpty(json)) { return list; } string[] array = json.Split(new string[1] { "\"id\":\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = array[i]; int num = text.IndexOf('"'); if (num >= 0) { BannedUserEntry bannedUserEntry = new BannedUserEntry(); bannedUserEntry.id = text.Substring(0, num); bannedUserEntry.steam_id = ExtractJsonString(text, "steam_id"); bannedUserEntry.display_name = ExtractJsonString(text, "display_name"); bannedUserEntry.reason = ExtractJsonString(text, "reason"); bannedUserEntry.banned_by_steam_id = ExtractJsonString(text, "banned_by_steam_id"); bannedUserEntry.banned_at = ExtractJsonString(text, "banned_at"); list.Add(bannedUserEntry); } } return list; } public static void CheckOpponentRanked(string steamId, Action<bool> callback) { if (string.IsNullOrEmpty(steamId) || steamId.StartsWith("photon_") || steamId == "unknown") { callback(obj: false); return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/mod/check/" + steamId, delegate(bool success, string response) { //IL_006b: Unknown result type (might be due to invalid IL or missing references) if (success) { try { ModCheckResponse modCheckResponse = JsonUtility.FromJson<ModCheckResponse>(response); if (modCheckResponse.banned) { Plugin.Log.LogWarning((object)("[BAN] Opponent " + steamId + " is mod-banned (" + modCheckResponse.ban_reason + ") — leaving match")); CompetitiveUI.ShowNotification("Opponent is banned from the mod for cheating - leaving match.", new Color(1f, 0.3f, 0.3f), 8f); try { if (PhotonNetwork.InRoom) { PhotonNetwork.LeaveRoom(true); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[BAN] LeaveRoom failed: " + ex.Message)); } callback(obj: false); } else { bool obj = modCheckResponse.registered && modCheckResponse.ranked; Plugin.Log.LogInfo((object)$"Opponent ranked check: registered={modCheckResponse.registered}, ranked={modCheckResponse.ranked}"); callback(obj); } return; } catch { callback(obj: false); return; } } Plugin.Log.LogWarning((object)("Opponent rank check failed: " + response)); callback(obj: false); })); } public static void ReportMatch(string p1SteamId, string p1Name, string p2SteamId, string p2Name, int p1RoundsWon, int p2RoundsWon, int p1PointsTotal, int p2PointsTotal, List<MatchTracker.CardPickData> p1Cards, List<MatchTracker.CardPickData> p2Cards, List<MatchTracker.CardOfferData> p1Offers, List<MatchTracker.CardOfferData> p2Offers, string photonRoomId, string region, int durationSeconds, DateTime startedAt, string reporterSteamId, bool isRanked, int localShotsFired = 0, int localBlocksRaised = 0, int localBulletsFired = 0, int localBulletsHit = 0, int localBlocksActivated = 0, int localBlocksSuccessful = 0, int localAvgFps = 0, int opponentAvgFps = 0) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("{"); stringBuilder.Append("\"player1\":{\"steam_id\":\"" + Escape(p1SteamId) + "\",\"display_name\":\"" + Escape(p1Name) + "\",\"cards\":["); AppendCards(stringBuilder, p1Cards); stringBuilder.Append("],\"card_offers\":["); AppendOffers(stringBuilder, p1Offers); stringBuilder.Append("]},"); stringBuilder.Append("\"player2\":{\"steam_id\":\"" + Escape(p2SteamId) + "\",\"display_name\":\"" + Escape(p2Name) + "\",\"cards\":["); AppendCards(stringBuilder, p2Cards); stringBuilder.Append("],\"card_offers\":["); AppendOffers(stringBuilder, p2Offers); stringBuilder.Append("]},"); stringBuilder.Append($"\"p1_rounds_won\":{p1RoundsWon},"); stringBuilder.Append($"\"p2_rounds_won\":{p2RoundsWon},"); stringBuilder.Append($"\"p1_points_total\":{p1PointsTotal},"); stringBuilder.Append($"\"p2_points_total\":{p2PointsTotal},"); stringBuilder.Append("\"photon_room_id\":\"" + Escape(photonRoomId) + "\","); stringBuilder.Append("\"game_version\":\"v" + Application.version + "\","); stringBuilder.Append("\"region\":\"" + Escape(region) + "\","); stringBuilder.Append($"\"match_duration\":{durationSeconds},"); stringBuilder.Append($"\"started_at\":\"{startedAt:yyyy-MM-ddTHH:mm:ssZ}\","); stringBuilder.Append("\"is_ranked\":" + (isRanked ? "true" : "false") + ","); stringBuilder.Append("\"reported_by_steam_id\":\"" + Escape(reporterSteamId) + "\","); stringBuilder.Append($"\"local_shots_fired\":{localShotsFired},"); stringBuilder.Append($"\"local_blocks_raised\":{localBlocksRaised},"); stringBuilder.Append($"\"local_bullets_fired\":{localBulletsFired},"); stringBuilder.Append($"\"local_bullets_hit\":{localBulletsHit},"); stringBuilder.Append($"\"local_blocks_activated\":{localBlocksActivated},"); stringBuilder.Append($"\"local_blocks_successful\":{localBlocksSuccessful},"); stringBuilder.Append($"\"local_avg_fps\":{localAvgFps},"); stringBuilder.Append($"\"opponent_avg_fps\":{opponentAvgFps},"); string text = ComputeHmac(p1SteamId, p2SteamId, p1RoundsWon, p2RoundsWon, isRanked, reporterSteamId, photonRoomId); stringBuilder.Append("\"hmac_signature\":\"" + text + "\""); stringBuilder.Append("}"); string json = stringBuilder.ToString(); string text2 = (isRanked ? "RANKED" : "CASUAL"); Plugin.Log.LogInfo((object)("Reporting " + text2 + " match to API...")); ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequestWithRetry(baseUrl + "/api/v1/matches", json, delegate(bool success, string response) { //IL_03a2: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_0212: Unknown result type (might be due to invalid IL or missing references) //IL_02cb: Unknown result type (might be due to invalid IL or missing references) //IL_030c: Unknown result type (might be due to invalid IL or missing references) if (success) { Plugin.Log.LogInfo((object)("Match reported successfully: " + response)); int num = ExtractJsonInt(response, "xp_gained"); int num2 = ExtractJsonInt(response, "level"); ExtractJsonInt(response, "total_xp"); int lastKnownLevel = CompetitiveUI.LastKnownLevel; string text3 = $"+{num} XP"; List<string> list = new List<string>(); if (response.Contains("Win x")) { list.Add("Win x1.5"); } if (response.Contains("Ranked x")) { list.Add("Ranked x1.2"); } if (response.Contains("Sweep")) { list.Add("Sweep +100"); } if (response.Contains("Top 5")) { list.Add("Top 5 +150"); } if (list.Count > 0) { text3 = text3 + " [" + string.Join(", ", list.ToArray()) + "]"; } CompetitiveUI.ShowNotification(text3, new Color(0.4f, 0.8f, 1f), 4f); int num3 = ExtractJsonInt(response, "gold_gained"); if (num3 > 0) { List<string> list2 = new List<string>(); int num4 = response.IndexOf("\"gold_bonuses\":"); if (num4 >= 0) { int num5 = response.IndexOf('[', num4); int num6 = ((num5 >= 0) ? response.IndexOf(']', num5) : (-1)); if (num5 >= 0 && num6 > num5) { string[] array = response.Substring(num5 + 1, num6 - num5 - 1).Split(','); for (int i = 0; i < array.Length; i++) { string text4 = array[i].Trim().Trim('"').Trim(); if (!string.IsNullOrEmpty(text4)) { list2.Add(text4); } } } } string text5 = $"+{num3} gold"; if (list2.Count > 0) { text5 = text5 + " [" + string.Join(", ", list2.ToArray()) + "]"; } CompetitiveUI.QueueNotification(text5, new Color(1f, 0.85f, 0.3f), 4f); } if (num2 > lastKnownLevel && lastKnownLevel >= 0) { CompetitiveUI.QueueNotification($"LEVEL UP! Level {num2}", new Color(1f, 0.85f, 0.3f), 4f); } CompetitiveUI.LastKnownLevel = num2; FetchPlayerStats(MatchTracker.LocalSteamId); FetchMatchHistory(MatchTracker.LocalSteamId); if (isRanked) { string text6 = ExtractJsonString(response, "series_status"); string text7 = ExtractJsonString(response, "series_score"); ActiveRankedSeriesId = null; if (text6 == "active") { CompetitiveUI.QueueNotification("Series: " + text7, new Color(1f, 0.85f, 0.3f)); } else if (text6 == "completed") { CompetitiveUI.QueueNotification("SERIES COMPLETE " + text7 + "!", new Color(0.3f, 1f, 0.3f), 4f); try { string localSteamId = MatchTracker.LocalSteamId; GameStateWatcher.IncrementSessionRankedSeries((!string.IsNullOrEmpty(localSteamId) && localSteamId == p1SteamId) ? (p1RoundsWon > p2RoundsWon) : (p2RoundsWon > p1RoundsWon)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("[SESSION] series tally update failed: " + ex.Message)); } GameStateWatcher.pendingRegicideCheck = false; } } } else { Plugin.Log.LogError((object)("Failed to report match: " + response)); CompetitiveUI.ShowNotification("Failed to report match", Color.red); } })); } private static void AppendCards(StringBuilder sb, List<MatchTracker.CardPickData> cards) { for (int i = 0; i < cards.Count; i++) { if (i > 0) { sb.Append(","); } MatchTracker.CardPickData cardPickData = cards[i]; sb.Append("{"); sb.Append("\"card_name\":\"" + Escape(cardPickData.CardName) + "\","); sb.Append("\"card_rarity\":\"" + Escape(cardPickData.CardRarity ?? "Unknown") + "\","); sb.Append($"\"pick_order\":{cardPickData.PickOrder},"); sb.Append($"\"round_number\":{cardPickData.RoundNumber}"); sb.Append("}"); } } private static void AppendOffers(StringBuilder sb, List<MatchTracker.CardOfferData> offers) { if (offers == null) { return; } for (int i = 0; i < offers.Count; i++) { if (i > 0) { sb.Append(","); } MatchTracker.CardOfferData cardOfferData = offers[i]; sb.Append("{"); sb.Append("\"card_name\":\"" + Escape(cardOfferData.CardName) + "\","); sb.Append($"\"round_number\":{cardOfferData.RoundNumber},"); sb.Append("\"was_picked\":" + (cardOfferData.WasPicked ? "true" : "false")); sb.Append("}"); } } public static void FetchLeaderboard(int limit = 100, int minMatches = 1) { IsLoading = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest($"{baseUrl}/api/v1/leaderboard?limit={limit}&min_matches={minMatches}", delegate(bool success, string response) { IsLoading = false; if (success) { try { LeaderboardData leaderboardData = new LeaderboardData { total_players = ExtractJsonInt(response, "total_players") }; List<LeaderboardEntry> list = new List<LeaderboardEntry>(); string[] array = response.Split(new string[1] { "\"rank\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = array[i]; LeaderboardEntry leaderboardEntry = new LeaderboardEntry(); try { int num = text.IndexOf(':'); if (num < 0) { num = -1; } int num2 = text.IndexOf(',', num + 1); if (num >= 0 && num2 > num) { string s = text.Substring(num + 1, num2 - num - 1).Trim(); leaderboardEntry.rank = int.Parse(s); } } catch { leaderboardEntry.rank = i; } leaderboardEntry.steam_id = ExtractJsonString(text, "steam_id"); leaderboardEntry.display_name = ExtractJsonString(text, "display_name"); leaderboardEntry.rating = ExtractJsonInt(text, "rating"); leaderboardEntry.rd = ExtractJsonInt(text, "rd"); leaderboardEntry.total_matches = ExtractJsonInt(text, "total_matches"); leaderboardEntry.wins = ExtractJsonInt(text, "wins"); leaderboardEntry.losses = ExtractJsonInt(text, "losses"); try { int num3 = text.IndexOf("\"win_rate\":"); if (num3 >= 0) { for (num3 += "\"win_rate\":".Length; num3 < text.Length && text[num3] == ' '; num3++) { } int j; for (j = num3; j < text.Length && (char.IsDigit(text[j]) || text[j] == '.' || text[j] == '-'); j++) { } if (j > num3) { leaderboardEntry.win_rate = float.Parse(text.Substring(num3, j - num3), CultureInfo.InvariantCulture); } } } catch { leaderboardEntry.win_rate = 0f; } leaderboardEntry.level = ExtractJsonInt(text, "level"); leaderboardEntry.gold = ExtractJsonInt(text, "gold"); leaderboardEntry.title = ExtractJsonString(text, "title"); leaderboardEntry.title_color = ExtractJsonString(text, "title_color"); if (!string.IsNullOrEmpty(leaderboardEntry.steam_id)) { list.Add(leaderboardEntry); } } leaderboardData.entries = list.ToArray(); CachedLeaderboard = leaderboardData; ManualLogSource log = Plugin.Log; LeaderboardEntry[] entries = CachedLeaderboard.entries; log.LogInfo((object)$"Leaderboard loaded: {((entries != null) ? entries.Length : 0)} entries"); return; } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to parse leaderboard: " + ex.Message)); LastError = "Failed to parse leaderboard data"; return; } } LastError = "Failed to fetch leaderboard: " + response; Plugin.Log.LogError((object)LastError); })); } public static void FetchRecentSeries(int limit = 100) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest($"{baseUrl}/api/v1/series/recent?minutes=10080&limit={limit}", delegate(bool success, string response) { if (success) { try { List<RecentSeriesEntry> list = new List<RecentSeriesEntry>(); string[] array = response.Split(new string[1] { "\"series_id\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { if (list.Count >= limit) { break; } RecentSeriesEntry recentSeriesEntry = new RecentSeriesEntry { winner_name = ExtractJsonString(array[i], "winner_name"), p1_name = ExtractJsonString(array[i], "p1_name"), p2_name = ExtractJsonString(array[i], "p2_name"), p1_wins = ExtractJsonInt(array[i], "p1_series_wins"), p2_wins = ExtractJsonInt(array[i], "p2_series_wins"), winner_steam_id = ExtractJsonString(array[i], "winner_steam_id"), p1_steam_id = ExtractJsonString(array[i], "p1_steam_id"), p2_steam_id = ExtractJsonString(array[i], "p2_steam_id") }; try { int num = array[i].IndexOf("\"p1_rating_change\":"); if (num >= 0) { num += "\"p1_rating_change\":".Length; int j; for (j = num; j < array[i].Length && (char.IsDigit(array[i][j]) || array[i][j] == '.' || array[i][j] == '-'); j++) { } if (j > num) { recentSeriesEntry.p1_rating_change = float.Parse(array[i].Substring(num, j - num), CultureInfo.InvariantCulture); } } } catch { } try { int num2 = array[i].IndexOf("\"p2_rating_change\":"); if (num2 >= 0) { num2 += "\"p2_rating_change\":".Length; int k; for (k = num2; k < array[i].Length && (char.IsDigit(array[i][k]) || array[i][k] == '.' || array[i][k] == '-'); k++) { } if (k > num2) { recentSeriesEntry.p2_rating_change = float.Parse(array[i].Substring(num2, k - num2), CultureInfo.InvariantCulture); } } } catch { } recentSeriesEntry.p1_rating = ExtractJsonInt(array[i], "p1_rating"); recentSeriesEntry.p2_rating = ExtractJsonInt(array[i], "p2_rating"); int num3 = array[i].IndexOf("\"bets\":"); if (num3 >= 0) { int num4 = array[i].IndexOf('[', num3); int num5 = array[i].IndexOf(']', num4); if (num4 >= 0 && num5 > num4) { string[] array2 = array[i].Substring(num4, num5 - num4 + 1).Split(new string[1] { "\"bettor_name\":" }, StringSplitOptions.None); for (int l = 1; l < array2.Length; l++) { string text = "\"bettor_name\":" + array2[l]; SeriesBetEntry seriesBetEntry = new SeriesBetEntry { bettor_name = ExtractJsonString(text, "bettor_name"), bettor_steam_id = ExtractJsonString(text, "bettor_steam_id"), bet_on_name = ExtractJsonString(text, "bet_on_name"), bet_on_steam_id = ExtractJsonString(text, "bet_on_steam_id"), amount = ExtractJsonInt(text, "amount"), payout = ExtractJsonInt(text, "payout"), odds_multiplier = ExtractJsonFloat(text, "odds_multiplier"), won = (text.Contains("\"won\":true") || text.Contains("\"won\": true")) }; if (!string.IsNullOrEmpty(seriesBetEntry.bettor_name)) { recentSeriesEntry.bets.Add(seriesBetEntry); } } } } if (!string.IsNullOrEmpty(recentSeriesEntry.p1_name)) { list.Add(recentSeriesEntry); } } CachedRecentSeries = list; Plugin.Log.LogInfo((object)$"Recent series loaded: {list.Count}"); NativeUI.MarkDirty(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Recent series parse error: " + ex.Message)); } } })); } public static void FetchPlayerStats(string steamId, bool force = false) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown" || (!force && IsLoading)) { return; } IsLoading = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/players/" + steamId, delegate(bool success, string response) { IsLoading = false; if (success) { try { CachedPlayerStats = JsonUtility.FromJson<PlayerStatsData>(response); ParseTopCards(CachedPlayerStats, response); Plugin.Log.LogInfo((object)("Player stats loaded for " + CachedPlayerStats.display_name)); try { TrailCosmetic.PublishLocalProps(); } catch { } try { PlayerColorCosmetic.PublishLocalProps(); } catch { } try { PlayerEffectCosmetic.PublishLocalProps(); } catch { } try { CursorColorCosmetic.ApplyFromStats(); } catch { } try { NametagStyler.PublishToPhoton(); return; } catch { return; } } catch (Exception ex) { Plugin.Log.LogError((object)("Failed to parse player stats: " + ex.Message)); return; } } if (!response.Contains("404")) { Plugin.Log.LogError((object)("Failed to fetch player stats: " + response)); } })); } public static void FetchPlayerStatsForView(string steamId, Action<PlayerStatsData> callback) { if (string.IsNullOrEmpty(steamId)) { callback(null); return; } string localSteamId = MatchTracker.LocalSteamId; string text = ((!string.IsNullOrEmpty(localSteamId) && localSteamId != "unknown" && localSteamId != steamId) ? ("?viewer_steam_id=" + Escape(localSteamId)) : ""); ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/players/" + steamId + text, delegate(bool success, string response) { if (success) { try { PlayerStatsData playerStatsData = JsonUtility.FromJson<PlayerStatsData>(response); ParseTopCards(playerStatsData, response); callback(playerStatsData); return; } catch { callback(null); return; } } callback(null); })); } private static void ParseTopCards(PlayerStatsData data, string response) { data.top_card_names = new List<string>(); data.top_card_picks = new List<int>(); data.top_card_win_rates = new List<float>(); data.recent_form = new List<string>(); data.worst_card_names = new List<string>(); data.worst_card_picks = new List<int>(); data.worst_card_win_rates = new List<float>(); data.region_names = new List<string>(); data.region_matches = new List<int>(); try { int num = response.IndexOf("\"top_cards\""); if (num >= 0) { int num2 = response.IndexOf("[", num); int num3 = FindMatchingBracket(response, num2); if (num2 >= 0 && num3 >= 0) { string text = response.Substring(num2, num3 - num2 + 1); if (text != "[]") { string[] array = text.Split(new string[1] { "\"card_name\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length && i <= 10; i++) { string text2 = ExtractJsonString(array[i], ""); int item = ExtractJsonInt(array[i], "times_picked"); float item2 = 0f; try { int num4 = array[i].IndexOf("\"win_rate\":"); if (num4 >= 0) { for (num4 += "\"win_rate\":".Length; num4 < array[i].Length && array[i][num4] == ' '; num4++) { } int j; for (j = num4; j < array[i].Length && (char.IsDigit(array[i][j]) || array[i][j] == '.' || array[i][j] == '-'); j++) { } if (j > num4) { item2 = float.Parse(array[i].Substring(num4, j - num4), CultureInfo.InvariantCulture); } } } catch { } if (!string.IsNullOrEmpty(text2)) { data.top_card_names.Add(text2); data.top_card_picks.Add(item); data.top_card_win_rates.Add(item2); } } } } } } catch { } try { int num5 = response.IndexOf("\"worst_cards\""); if (num5 >= 0) { int num6 = response.IndexOf("[", num5); int num7 = FindMatchingBracket(response, num6); if (num6 >= 0 && num7 >= 0) { string text3 = response.Substring(num6, num7 - num6 + 1); if (text3 != "[]") { string[] array2 = text3.Split(new string[1] { "\"card_name\"" }, StringSplitOptions.None); for (int k = 1; k < array2.Length && k <= 10; k++) { string text4 = ExtractJsonString(array2[k], ""); int item3 = ExtractJsonInt(array2[k], "times_picked"); float item4 = ExtractJsonFloat(array2[k], "win_rate"); if (!string.IsNullOrEmpty(text4)) { data.worst_card_names.Add(text4); data.worst_card_picks.Add(item3); data.worst_card_win_rates.Add(item4); } } } } } } catch { } try { int num8 = response.IndexOf("\"region_breakdown\""); if (num8 >= 0) { int num9 = response.IndexOf("[", num8); int num10 = FindMatchingBracket(response, num9); if (num9 >= 0 && num10 >= 0) { string text5 = response.Substring(num9, num10 - num9 + 1); if (text5 != "[]") { string[] array3 = text5.Split(new string[1] { "\"region\"" }, StringSplitOptions.None); for (int l = 1; l < array3.Length; l++) { string text6 = ExtractJsonString(array3[l], ""); int item5 = ExtractJsonInt(array3[l], "matches"); if (!string.IsNullOrEmpty(text6)) { data.region_names.Add(text6); data.region_matches.Add(item5); } } } } } } catch { } try { int num11 = response.IndexOf("\"recent_form\""); if (num11 >= 0) { int num12 = response.IndexOf("[", num11); int num13 = FindMatchingBracket(response, num12); if (num12 >= 0 && num13 >= 0) { string text7 = response.Substring(num12, num13 - num12 + 1); if (text7 != "[]") { string[] array4 = text7.Split(new string[1] { "\"result\"" }, StringSplitOptions.None); for (int m = 1; m < array4.Length; m++) { string text8 = ExtractJsonString(array4[m], ""); if (!string.IsNullOrEmpty(text8)) { data.recent_form.Add(text8); } } } } } } catch { } data.active_color_skus = new List<string>(); try { int num14 = response.IndexOf("\"active_color_skus\""); if (num14 >= 0) { int num15 = response.IndexOf("[", num14); int num16 = FindMatchingBracket(response, num15); if (num15 >= 0 && num16 >= 0 && num16 > num15) { string text9 = response.Substring(num15 + 1, num16 - num15 - 1); int num17 = 0; while (num17 < text9.Length) { int num18 = text9.IndexOf('"', num17); if (num18 >= 0) { int num19 = text9.IndexOf('"', num18 + 1); if (num19 >= 0) { string text10 = text9.Substring(num18 + 1, num19 - num18 - 1); if (!string.IsNullOrEmpty(text10)) { data.active_color_skus.Add(text10); } num17 = num19 + 1; continue; } break; } break; } } } } catch { } data.active_nametag_skus = new List<string>(); try { int num20 = response.IndexOf("\"active_nametag_skus\""); if (num20 >= 0) { int num21 = response.IndexOf("[", num20); int num22 = FindMatchingBracket(response, num21); if (num21 >= 0 && num22 >= 0 && num22 > num21) { string text11 = response.Substring(num21 + 1, num22 - num21 - 1); int num23 = 0; while (num23 < text11.Length) { int num24 = text11.IndexOf('"', num23); if (num24 >= 0) { int num25 = text11.IndexOf('"', num24 + 1); if (num25 >= 0) { string text12 = text11.Substring(num24 + 1, num25 - num24 - 1); if (!string.IsNullOrEmpty(text12)) { data.active_nametag_skus.Add(text12); } num23 = num25 + 1; continue; } break; } break; } } } } catch { } data.rating_history = new List<float>(); try { int num26 = response.IndexOf("\"recent_rating_history\""); if (num26 >= 0) { int num27 = response.IndexOf("[", num26); int num28 = FindMatchingBracket(response, num27); if (num27 >= 0 && num28 >= 0) { string text13 = response.Substring(num27, num28 - num27 + 1); if (text13 != "[]") { string[] array5 = text13.Split(new string[1] { "\"rating\"" }, StringSplitOptions.None); for (int n = 1; n < array5.Length; n++) { int num29 = array5[n].IndexOf(':'); if (num29 >= 0) { int num30; for (num30 = num29 + 1; num30 < array5[n].Length && array5[n][num30] == ' '; num30++) { } int num31; for (num31 = num30; num31 < array5[n].Length && (char.IsDigit(array5[n][num31]) || array5[n][num31] == '.' || array5[n][num31] == '-'); num31++) { } if (num31 > num30) { float item6 = float.Parse(array5[n].Substring(num30, num31 - num30), CultureInfo.InvariantCulture); data.rating_history.Add(item6); } } } } } } } catch { } if (data.rating_history.Count > 0 && data.rating_history[0] != 1500f) { data.rating_history.Insert(0, 1500f); } Plugin.Log.LogInfo((object)$"[STATS] Parsed {data.rating_history.Count} rating history points for {data.display_name} (oldest→newest, 1500 baseline prepended)"); } public static void FetchAchievementsForView(string steamId) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown" || (steamId == selectedAchSteamId && SelectedPlayerAchievements != null)) { return; } selectedAchSteamId = steamId; SelectedPlayerAchievements = null; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/achievements/" + steamId, delegate(bool success, string response) { if (success) { try { Dictionary<string, AchievementData> dictionary = new Dictionary<string, AchievementData>(); string[] array = response.Split(new string[1] { "\"achievement_key\"" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string text = ExtractJsonString(array[i], ""); if (!string.IsNullOrEmpty(text)) { bool unlocked = array[i].Contains("\"unlocked\":true") || array[i].Contains("\"unlocked\": true"); dictionary[text] = new AchievementData { achievement_key = text, unlocked = unlocked }; } } SelectedPlayerAchievements = dictionary; NativeUI.MarkDirty(); } catch { } } })); } private static int FindMatchingBracket(string s, int openPos) { if (openPos < 0 || openPos >= s.Length) { return -1; } int num = 0; for (int i = openPos; i < s.Length; i++) { if (s[i] == '[') { num++; } else if (s[i] == ']') { num--; if (num == 0) { return i; } } } return -1; } private static int FindMatchingBrace(string s, int openPos) { if (openPos < 0 || openPos >= s.Length) { return -1; } int num = 0; for (int i = openPos; i < s.Length; i++) { if (s[i] == '{') { num++; } else if (s[i] == '}') { num--; if (num == 0) { return i; } } } return -1; } public static void FetchCardTiers(string steamId, string filter, Action<Dictionary<string, string>> onLoaded) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown") { onLoaded?.Invoke(new Dictionary<string, string>()); return; } string url = baseUrl + "/api/v1/players/" + Escape(steamId) + "/card-tiers?filter=" + Escape(filter); ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(url, delegate(bool success, string response) { Dictionary<string, string> dictionary = new Dictionary<string, string>(); if (!success || string.IsNullOrEmpty(response)) { onLoaded?.Invoke(dictionary); } else { try { int num = response.IndexOf("\"tiers\""); if (num < 0) { onLoaded?.Invoke(dictionary); return; } int num2 = response.IndexOf('{', num); int num3 = FindMatchingBrace(response, num2); if (num2 < 0 || num3 < 0) { onLoaded?.Invoke(dictionary); return; } string text = response.Substring(num2 + 1, num3 - num2 - 1); int num4 = 0; while (num4 < text.Length) { int num5 = text.IndexOf('"', num4); if (num5 < 0) { break; } int num6 = text.IndexOf('"', num5 + 1); if (num6 < 0) { break; } string text2 = text.Substring(num5 + 1, num6 - num5 - 1); int num7 = text.IndexOf(':', num6); if (num7 < 0) { break; } int num8 = text.IndexOf('"', num7); if (num8 < 0) { break; } int num9 = text.IndexOf('"', num8 + 1); if (num9 < 0) { break; } string value = text.Substring(num8 + 1, num9 - num8 - 1); dictionary[text2.ToLower()] = value; num4 = num9 + 1; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[CARD-TIER] parse: " + ex.Message)); } onLoaded?.Invoke(dictionary); } })); } public static void SetCardTier(string steamId, string cardName, string filter, string tier) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown") { return; } string text = ComputeHmacHex("card-tier:" + steamId + ":" + cardName + ":" + filter + ":" + tier); string url = baseUrl + "/api/v1/players/" + Escape(steamId) + "/card-tiers?card_name=" + Escape(cardName) + "&filter=" + Escape(filter) + "&tier=" + Escape(tier) + "&sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { if (!ok) { Plugin.Log.LogWarning((object)("[CARD-TIER] set failed: " + resp)); } })); } public static void FetchCardStats(int limit = 30, string steamId = null, string sortBy = "times_picked", string isRanked = null) { IsLoading = true; string text = $"{baseUrl