Decompiled source of SidsCompetitiveRounds v1.35.5
plugins\CompetitiveRounds.dll
Decompiled 2 days 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; 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.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [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("Thunderstore")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+297d79046324c6db5231e1bf5776ee47a55497ba")] [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; public string rank_name; public string rank_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 lfp_seconds_left; public int best_ranked_game_streak; public int current_ranked_game_streak; public int best_ranked_series_streak; public int current_ranked_series_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_title_sku; 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 bool appear_offline; public string discord_display_name; public bool show_discord; 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 List<float> rating_history_times; 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; public float avg_keys_per_sec; public int avg_keys_per_game; public int avg_game_seconds; public int bets_won; public int bets_lost; public int bet_gold_net; public int sweeps_given; public int sweeps_taken; public int casual_wins; public int casual_losses; public string rank_name; public string rank_color; public float team_rating; public int team_completed_series; public int ovt_solo_wins; public int ovt_solo_losses; public int ovt_duo_wins; public int ovt_duo_losses; public int ffa_games; public int ffa_wins; public int ffa_top3; public float ffa_avg_placement; public float ffa_avg_kills; public float ffa_avg_damage; public int ffa_damage_games; } [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; public int player_bullets_fired; public int player_bullets_hit; public int player_blocks_activated; public int player_blocks_successful; public int player_keys_pressed; public float player_active_seconds; public int opp_bullets_fired; public int opp_bullets_hit; public int opp_blocks_activated; public int opp_blocks_successful; public int opp_keys_pressed; public float opp_active_seconds; public string player_fps_timeline; public string opp_fps_timeline; public int player_ping_avg; public int opponent_ping_avg; public string player_ping_timeline; public string opp_ping_timeline; public string player_hit_timeline; public string opp_hit_timeline; public string player_block_timeline; public string opp_block_timeline; public string point_times; public string point_timeline; public int duration_seconds; } [Serializable] private class MatchHistoryWrapper { public MatchHistoryEntry[] items; } [Serializable] public class AchievementData { public string achievement_key; public bool unlocked; public string unlocked_at; public float global_pct; public int gold; } private class PendingReport { public string url; public string json; public int attempts; public float nextAt; } [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; public bool settled; public int payout; public string vs_name; public string created_at; } public class RankTierEntry { public float floor; public string name; public string color; } public class MultimodeSeriesEntry { public string mode; public string id; public string ended_at; public string left_label; public string right_label; public string score; public float left_rating_change; public float right_rating_change; public List<MultimodeBet> bets = new List<MultimodeBet>(); } public class MultimodeBet { public string bettor_name; public string bettor_steam_id; public string bet_on_label; public int amount; public int payout; public float odds_multiplier; public string state; } [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 string artist_steam_id; public string artist_name; public int stock_limit; public int stock_sold; } public class FlaggedMatchEntry { public string id; public string match_id; public string game_code; public string series_id; public string flag_reason; public string p1_name; public string p2_name; public string p1_steam_id; public string p2_steam_id; public string suspect_steam_text; public List<string> player_steam_ids = new List<string>(); public bool auto_invalidated; public bool match_invalidated; public bool is_ranked; public bool restoration_required; public int duration_seconds; public int discord_evidence_revision; public string duration_text; public string review_action; public string match_invalidation_reason; public string created_at; public string score_summary; public string cards_summary; public string point_progress; public string evidence_summary; public string telemetry_summary; public string connection_summary; public string match_context; } 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; } public class QuarantineReportEntry { public string id; public string mode; public string reason; public string created_at; public string score; public string room; public string status; public int http_status; public List<string> player_names = new List<string>(); public bool can_accept; public string accept_blocked_reason; } public class ArtistItemEntry { public string sku; public string kind; public string name; public string rarity; public string description; public int price; public int stock_limit; public int sold; public int gifted; public bool catalog_ready; public int earned; } public class ArtistBlockEntry { public string steam_id; public string display_name; } public class ArtistSaleEntry { public string item; public string buyer; public string when; public int price; public int earned; } public class PlayerSearchResult { public string steam_id; public string display_name; public int rating; } public class CosmeticSubmission { public int id; public float render_scale = 1f; public float render_offset_x; public float render_offset_y; public int placement_revision = 1; public string placement_status; public string placement_review_note; public string review_kind; public float approved_render_scale; public float approved_render_offset_x; public float approved_render_offset_y; public int approved_placement_revision; public int published_placement_revision; public string name; public string slot; public string status; public string review_note; public string shop_sku; public string created_at; public string artist_name; public string artist_steam_id; public string png_base64; } public class CosmeticReleaseCandidate { public int id; public int placement_revision; public int published_placement_revision; public int png_bytes; public string shop_sku; public string name; public string slot; public string artist_name; public string artist_steam_id; public string approved_at; public float render_scale; public float render_offset_x; public float render_offset_y; public bool catalog_ready; } public class AdminSeriesEntry { public string ladder; public string series_id; public string status; public string created_at; public string completed_at; public string score; public int series_number; public int winner_team; public int num_players; public bool incomplete; public string dc_name; public string dc_steam; public string p1_name; public string p1_steam; public string p1_cards; public int p1_team; public string p2_name; public string p2_steam; public string p2_cards; public int p2_team; public string p3_name; public string p3_steam; public string p3_cards; public int p3_team; public string p4_name; public string p4_steam; public string p4_cards; public int p4_team; } [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, Leaving } [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 class OnlinePlayerEntry { public string name; public int rating; public int minutesAgo; public string title; public string titleColor; } public class NewestCosmeticEntry { public string sku; public string kind; public string name; public string rarity; public int price; public string previewColor; public string artistName; public string added; public bool onSale; } public class ReleaseNoteEntry { public string tag; public string title; public string date; public string body; } public enum TeamQueueState { Idle, Searching, Matched, ReadySent, Leaving } [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; } public class TeamTelemetry { public string fpsTimeline; public string pingTimeline; public string hitTimeline; public string blockTimeline; public int pingAvg; public int bulletsFired; public int bulletsHit; public int blocksActivated; public int blocksSuccessful; public int keysPressed; public float activeSeconds; } [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 TeamPlayerTele { public string fps_timeline; public string ping_timeline; public string hit_timeline; public string block_timeline; public int ping_avg; public int bullets_fired; public int bullets_hit; public int blocks_activated; public int blocks_successful; } [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 int duration_seconds; public Dictionary<string, int> fps_by_player = new Dictionary<string, int>(); public Dictionary<string, TeamPlayerTele> telemetry_by_player = new Dictionary<string, TeamPlayerTele>(); 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; } public class OvtLeaderboardEntry { public int rank; public string steam_id; public string display_name; public int games_played; public int wins; public int losses; public int solo_games; public int duo_games; public int level; public int solo_wins; public int solo_losses; public int duo_wins; public int duo_losses; public float win_rate; public string title; public string title_color; public string last_played; } public class OvtQueueListEntry { public string steam_id; public string display_name; public string status; public string region; public int rating_1v1; public int rating_2v2; public int preferred_side; public int side_assigned; public int wait_seconds; public bool solo_extra_pick; } public class OvtRecentMatch { public string match_id; public string ended_at; public int winner_side; public int solo_rounds; public int duo_rounds; public int solo_points; public int duo_points; public int duration_seconds; public Dictionary<string, int> fps_by_steam = new Dictionary<string, int>(); public Dictionary<string, List<string>> cards_by_steam = new Dictionary<string, List<string>>(); } public class OvtRecentSeries { public string series_id; public string status; public string created_at; public string completed_at; public int winner_side; public int solo_wins; public int duo_wins; public bool solo_extra_pick; public string solo_steam; public string solo_name; public string duo_a_steam; public string duo_a_name; public string duo_b_steam; public string duo_b_name; public Dictionary<string, int> gold_by_steam = new Dictionary<string, int>(); public Dictionary<string, int> xp_by_steam = new Dictionary<string, int>(); public List<OvtRecentMatch> matches = new List<OvtRecentMatch>(); } public class FfaLeaderboardEntry { public int rank; public int rating; public int rd; public int games_played; public int wins; public int top3; public int level; public string steam_id; public string display_name; public string title; public string title_color; public float avg_placement; public float win_rate; public int ffa_gold_earned; public int ffa_xp_earned; } public class FfaRecentPlayer { public string steam_id; public string display_name; public string title; public string title_color; public int slot; public int placement; public int rounds_won; public int points_total; public int kills; public int fps_avg; public int xp_gained; public int gold_gained; public bool left_early; public float rating_change; public bool has_rating_change; public List<FfaRecentCard> cards = new List<FfaRecentCard>(); } public class FfaRecentCard { public string name; public bool rolled; } public class FfaRecentMatch { public string match_id; public string photon_room_id; public string ended_at; public string winner_steam_id; public string timeline; public int player_count; public int duration_seconds; public bool is_ranked; public List<FfaRecentPlayer> players = new List<FfaRecentPlayer>(); } public class PlayerTeamHistoryEntry { public string series_id; public string score; public string mate; public string completed_at; public bool won; public bool has_rating_change; public float rating_change; public List<string> opponents = new List<string>(); } public class PlayerOvtHistoryEntry { public string match_id; public string role; public string score; public string solo; public string ended_at; public bool won; public List<string> duo = new List<string>(); public int gold_gained; public int series_gold_gained; } public class PlayerFfaHistoryEntry { public string match_id; public string ended_at; public int player_count; public int placement; public int kills; public int rounds_won; public int points_total; public bool has_rating_change; public float rating_change; public List<string> participants = new List<string>(); } public class FfaBettablePlayer { public string steam_id; public string display_name; public int rating; public float odds_multiplier; public bool bettable; } public class FfaBettableLobby { public string lobby_id; public int player_count; public int game_number; public bool is_member; public bool already_bet; public bool bets_open; public List<FfaBettablePlayer> players = new List<FfaBettablePlayer>(); } public class FfaLobbyMemberEntry { public string steam_id; public string display_name; public int rating; public int rating_1v1; public int wait_seconds; public bool is_host; } public class FfaOpenLobbyEntry { public string lobby_id; public string host_name; public int player_count; public int max_players; public int age_seconds; } public class FfaQueueListEntry { public string steam_id; public string display_name; public string status; public string region; public int rating; public int rating_1v1; public int slot; public int wait_seconds; } public class FfaReportPlayer { public string steamId; public string displayName; public int slot; public int rounds; public int points; public int kills; public int fps; public bool leftEarly; public bool absent; public List<MatchTracker.CardPickData> cards; public TeamTelemetry telemetry; public int damageDealt; public string killTimeline; public string damageTimeline; } public class AchEarnerData { public string display_name; public int rating; public string unlocked_at; public string title; public string title_color; } [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 int prize_players; public int prize_gold_1; public int prize_gold_2; public int prize_gold_3; public int prize_xp_1; public int prize_xp_2; public int prize_xp_3; 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; public int ready_seconds_left; public int scheduled_seconds_left; public bool my_early_ok; public bool opp_early_ok; public float fetched_at_realtime; } 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] { "Sid Slayer", "Win against Sid in a ranked series" } }, { "stan_slayer", new string[2] { "Stan Slayer", "Win against Stan 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 1980 rating in ranked (1v1 or 2v2)" } }, { "team_sweep", new string[2] { "Tag Team Sweep", "Win a 2v2 game 5-0" } }, { "grand_master", new string[2] { "Grand Master", "Reach 2330 rating in ranked (1v1 or 2v2)" } }, { "flawless", new string[2] { "Flawless", "Win five games 5-0 in a row" } }, { "bullet_hell", new string[2] { "Bullet Hell", "Win a game 5-0 with Barrage in your build" } }, { "spray_and_pray", new string[2] { "Spray and Pray", "Win a game 5-0 with Spray in your build" } }, { "demolitionist", new string[2] { "Demolitionist", "Win a game 5-0 with Explosive Bullet in your build" } }, { "controlled_burst", new string[2] { "Controlled Burst", "Win a game 5-0 with Burst in your build" } }, { "field_medic", new string[2] { "Field Medic", "Win a game 5-0 with Healing Field in your build" } }, { "god_build", new string[2] { "God Build", "Win with Shields Up, exactly 1 ammo, and a lightning-fast reload" } }, { "double_nova", new string[2] { "Double Nova", "Win with two or more Supernovas" } }, { "lumberjack", new string[2] { "Lumberjack", "Win with two or more Saws" } }, { "pristine_perfection", new string[2] { "Pristine Perfection", "Win with two or more Pristines" } }, { "silent_drill", new string[2] { "Silly Drill", "Win with Sneaky and Drill together" } }, { "double_glass", new string[2] { "Living on the Edge", "Win with two Glass Cannons in your build" } }, { "sustained_power", new string[2] { "Sustained Power", "Win with Empower and Healing Field together" } }, { "deep_end", new string[2] { "Into the Deep End", "Win with Abyssal Countdown as your FIRST pick, activating it every round" } }, { "clutch", new string[2] { "Clutch", "Win a game after being down 0-3" } }, { "collector", new string[2] { "Collector", "Win with four copies of the same card" } }, { "grounded", new string[2] { "Grounded", "Win a game without ever jumping" } }, { "instinct", new string[2] { "Instinct", "Win taking only the left-most card on every pick, without ever looking at the others" } }, { "rising_star", new string[2] { "Rising Star", "Reach 1700 rating in ranked (1v1 or 2v2)" } }, { "on_fire", new string[2] { "On Fire", "Win 25 ranked series in a row" } }, { "unstoppable", new string[2] { "Unstoppable", "Win 50 ranked series in a row" } }, { "immortal", new string[2] { "Immortal", "Win 100 ranked series in a row" } }, { "casual_century", new string[2] { "Century Club", "Win 100 casual games in a row" } }, { "casual_conqueror", new string[2] { "Casual Conqueror", "Win 200 casual games in a row" } }, { "touch_grass", new string[2] { "Touch Grass", "Win 500 casual games in a row" } }, { "twins", new string[2] { "Twins!", "Finish a game with the exact same 5 cards (and copies) as your opponent" } } }; private static readonly List<PendingReport> _pendingReports = new List<PendingReport>(); private static bool _outboxLoopStarted; private const int OUTBOX_MAX_ATTEMPTS = 20; private const float OUTBOX_RETRY_SECONDS = 60f; private static readonly HashSet<string> _heartbeatDispatchedMatches = new HashSet<string>(); private static readonly Dictionary<string, float> _heartbeatDispatchTime = new Dictionary<string, float>(); private static int _hbTournamentCurrentTick; 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 bool _adminSecretWarned = false; public static List<RankTierEntry> CachedRankTiers = null; private static bool _rankTiersInFlight; private static float _rankTiersAt = -99999f; private const float RANK_TIERS_TTL = 420f; public static List<MultimodeSeriesEntry> CachedRecentMultimode = null; private static bool _multimodeInFlight; private static string _lastPublishedCosmeticSig; private static string selectedAchSteamId = ""; public static readonly Dictionary<string, Dictionary<string, AchievementData>> CompareAchievements = new Dictionary<string, Dictionary<string, AchievementData>>(); public static readonly Dictionary<string, string> AchievementDisplayNames = new Dictionary<string, string>(); private static readonly HashSet<string> _compareAchFetching = new HashSet<string>(); public const int HISTORY_CHUNK = 400; public static bool MatchHistoryLoadedAll; private static bool _historyFetchInFlight; public static int HistoryTotalMatches = -1; public static int HistoryTotalRankedGroups = -1; public static int HistoryTotalCasual = -1; public static readonly Dictionary<string, int[]> CachedOppLifetime = new Dictionary<string, int[]>(); private static readonly HashSet<string> _oppLifetimeRequested = new HashSet<string>(); private static readonly Dictionary<string, float> _oppLifetimeRefetchAfter = new Dictionary<string, float>(); public static string ActiveRankedSeriesId; private static float queuePollTimer = 0f; private static float queuePollInterval = 3f; private const float LEAVE_STUCK_TIMEOUT = 45f; private static float rankedLeavingSince = -999f; private static int queueGen = 0; private static float _ffaRoomlessSince = -1f; private const string ESCAPE_PREF = "cr_escape_pending"; private static float _escapeRetryAt = -999f; private static float queueCountTimer = 0f; private static float queueCountInterval = 10f; public static List<OnlinePlayerEntry> CachedOnlinePlayers; public static List<OnlinePlayerEntry> CachedRecentPlayers; public static int CachedOnlineListCount; public static List<NewestCosmeticEntry> CachedNewestCosmetics; public static List<ReleaseNoteEntry> CachedReleaseNotes; private static bool _releasesFetchInFlight; private const string GITHUB_API_RELEASES = "https://api.github.com/repos/SidNDeed/SidsCompetitiveRounds/releases?per_page=3"; private static float teamQueueListTimer = 0f; private const float TEAM_QUEUE_LIST_INTERVAL = 2f; private static float teamLeavingSince = -999f; private static int teamGen = 0; 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; public static List<OvtLeaderboardEntry> CachedOvtLeaderboard = null; public static int CachedOvtLeaderboardTotal = 0; public static List<OvtLeaderboardEntry> CachedOvtLeaderboardSolo = null; public static List<OvtLeaderboardEntry> CachedOvtLeaderboardDuo = null; public static string OvtQueueStatus = ""; public static int OvtQueueCount = 0; public static string ActiveOvt1v2SeriesId = null; public static int OvtMySide = 0; public static bool OvtSoloExtraPick = false; public static bool IsOvtQueuePolling = false; private static float _ovtLastPollAt = -999f; public static string OvtLockedSoloName = null; public static List<TeamQueueMember> OvtLockedDuo = null; public static List<OvtQueueListEntry> CachedOvtQueueList = null; private static float _ovtListLastAt = -999f; private static int _ovtLastPreferredSide = 0; private static bool _ovtLastSoloExtraPick = false; private static float _ovtLastAutoRejoinAt = -999f; private static float _ovtLeavingSince = -999f; private static int ovtGen = 0; public static List<OvtRecentSeries> CachedOvtRecent = null; public static int CachedOvtRecentTotal = 0; public static int CachedOvtRecentPages = 0; public static List<FfaLeaderboardEntry> CachedFfaLeaderboard = null; public static int CachedFfaLeaderboardTotal = 0; public static List<FfaRecentMatch> CachedFfaRecent = null; public static int CachedFfaRecentTotal = 0; public static int CachedFfaRecentPages = 0; public static readonly Dictionary<string, List<PlayerTeamHistoryEntry>> CachedPlayerTeamHistory = new Dictionary<string, List<PlayerTeamHistoryEntry>>(); public static readonly Dictionary<string, List<PlayerOvtHistoryEntry>> CachedPlayerOvtHistory = new Dictionary<string, List<PlayerOvtHistoryEntry>>(); public static readonly Dictionary<string, List<PlayerFfaHistoryEntry>> CachedPlayerFfaHistory = new Dictionary<string, List<PlayerFfaHistoryEntry>>(); private static readonly HashSet<string> _playerTeamHistoryInFlight = new HashSet<string>(); private static readonly HashSet<string> _playerOvtHistoryInFlight = new HashSet<string>(); private static readonly HashSet<string> _playerFfaHistoryInFlight = new HashSet<string>(); public static List<FfaBettableLobby> CachedFfaBettable = null; private static bool _ffaBettableInFlight; public static string FfaQueueStatus = ""; public static int FfaQueueCount = 0; public static int FfaGatherSecondsLeft = -1; public static string ActiveFfaLobbyId = null; public static int FfaMySlot = -1; public static int FfaLobbyPlayerCount = 0; public static bool IsFfaQueuePolling = false; public static List<TeamQueueMember> FfaLockedRoster = null; private static float _ffaLastPollAt = -999f; public static string OpenFfaLobbyId = null; public static bool FfaLobbyIsHost = false; public static bool FfaLobbyCanStart = false; public static int FfaLobbyMemberCount = 0; public static int FfaLobbyMinPlayers = 3; public static int FfaLobbyMaxPlayers = 10; public static List<FfaLobbyMemberEntry> FfaLobbyMembers = null; public static List<FfaOpenLobbyEntry> CachedFfaLobbies = null; public static bool FfaLobbiesUnavailable = false; private static float _ffaLobbiesLastAt = -999f; private static float _ffaLobbyRejoinAt = -999f; private static bool _ffaLobbyActionInFlight = false; private static float _ffaLobbyActionAt = -999f; private static int _ffaLobbyActionGen = 0; private static bool _ffaLeaveIntent = false; private static float _ffaLeaveRetryAt = -999f; private static string _ffaLeaveTargetLobby = null; private static float _ffaAmbiguousPollUntil = -999f; public static List<FfaQueueListEntry> CachedFfaQueueList = null; private static float _ffaListLastAt = -999f; private static float _ffaLastAutoRejoinAt = -999f; private static string _ffaLastReadyFireKey = null; private static int _ffaReadyFireCount = 0; private static string _ovtLastReadyFireKey = null; private static int _ovtReadyFireCount = 0; private static float _ffaLeavingSince = -999f; private static int ffaGen = 0; private static bool _ffaProbed = false; private static string _ffaJoinCountdownKey; private static int _ffaJoinCountdownToken; private static int _ffaJoinCountdownActiveToken; private static float _ffaJoinCountdownArmedAt = -999f; private static bool blocksLoaded = false; public static float LastApiSuccessAt = -999f; public static float LastApiAttemptAt = -999f; public static bool LastResponseWasMaintenance = false; private static bool _earnersInFlight; private static string _earnersWantedKey; 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 UsingLegacyFallback { get; private set; } private static string OutboxPath => Path.Combine(Paths.ConfigPath, "CompetitiveRounds.pending-reports.jsonl"); 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<QuarantineReportEntry> CachedQuarantineReports { get; private set; } = new List<QuarantineReportEntry>(); public static bool IsAdminQuarantineLoading { get; private set; } public static bool AdminQuarantineLoaded { get; private set; } public static string AdminQuarantineError { get; private set; } = ""; public static bool IsArtist { get; private set; } public static List<ArtistItemEntry> CachedArtistItems { get; private set; } = new List<ArtistItemEntry>(); public static List<ArtistBlockEntry> CachedArtistBlocks { get; private set; } = new List<ArtistBlockEntry>(); public static List<ArtistSaleEntry> CachedArtistSales { get; private set; } = new List<ArtistSaleEntry>(); public static List<ArtistBlockEntry> CachedAllArtists { get; private set; } = new List<ArtistBlockEntry>(); public static List<CosmeticSubmission> CachedMySubmissions { get; private set; } = new List<CosmeticSubmission>(); public static List<AdminSeriesEntry> CachedAdminSeries { get; private set; } = new List<AdminSeriesEntry>(); 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 bool EscapePending { get { try { return PlayerPrefs.GetInt("cr_escape_pending", 0) == 1; } catch { return false; } } private set { try { PlayerPrefs.SetInt("cr_escape_pending", value ? 1 : 0); PlayerPrefs.Save(); } catch { } } } public static int CachedQueueSearching { get; private set; } = 0; public static int CachedQueueTotal { get; private set; } = 0; public static int CachedOnlineCount { 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 string EarnersKey { get; private set; } public static List<AchEarnerData> CachedEarners { get; private set; } public static int CachedEarnersTotal { get; private set; } public static void Initialize(string url) { baseUrl = url.TrimEnd('/'); Plugin.Log.LogInfo((object)("API client initialized: " + baseUrl)); if (string.Equals(baseUrl, "https://competitive-rounds.duckdns.org:8444".TrimEnd('/'), StringComparison.OrdinalIgnoreCase)) { ((MonoBehaviour)Plugin.Instance).StartCoroutine(ProbeEndpointThenStart()); } else { CheckModVersion(); } ((MonoBehaviour)Plugin.Instance).StartCoroutine(TournamentHeartbeatLoop()); ((MonoBehaviour)Plugin.Instance).StartCoroutine(PresenceLoop()); ((MonoBehaviour)Plugin.Instance).StartCoroutine(SteamAuthLoop()); LoadOutbox(); } private static IEnumerator SteamAuthLoop() { yield return (object)new WaitForSecondsRealtime(2f); while (true) { try { SteamAuth.MaybeRefresh(); } catch { } yield return (object)new WaitForSecondsRealtime(SteamAuth.HasFreshToken ? 30f : 3f); } } public static void EnqueueFailedReport(string url, string json) { //IL_010f: Unknown result type (might be due to invalid IL or missing references) try { if (string.IsNullOrEmpty(url) || string.IsNullOrEmpty(json)) { return; } foreach (PendingReport pendingReport in _pendingReports) { if (pendingReport.url == url && pendingReport.json == json) { return; } } float num = (IsSilentOutboxUrl(url) ? 120f : 30f); _pendingReports.Add(new PendingReport { url = url, json = json, attempts = 0, nextAt = Time.realtimeSinceStartup + num }); PersistOutbox(); EnsureOutboxLoop(); if (!IsSilentOutboxUrl(url)) { Plugin.Log.LogWarning((object)$"[OUTBOX] report queued for retry ({_pendingReports.Count} pending)"); CompetitiveUI.ShowNotification("Report failed — will retry in background", new Color(1f, 0.75f, 0.3f), 4f); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[OUTBOX] enqueue failed: " + ex.Message)); } } private static bool IsSilentOutboxUrl(string url) { return !string.IsNullOrEmpty(url) && url.EndsWith("/api/v1/matches/macro-evidence", StringComparison.Ordinal); } private static void RemovePendingReport(string url, string json) { bool flag = false; for (int num = _pendingReports.Count - 1; num >= 0; num--) { PendingReport pendingReport = _pendingReports[num]; if (!(pendingReport.url != url) && !(pendingReport.json != json)) { _pendingReports.RemoveAt(num); flag = true; } } if (flag) { PersistOutbox(); } } private static void PersistOutbox() { try { StringBuilder stringBuilder = new StringBuilder(); foreach (PendingReport pendingReport in _pendingReports) { stringBuilder.Append(pendingReport.url).Append('\t').Append(pendingReport.json.Replace("\n", " ").Replace("\r", " ")) .Append('\n'); } if (stringBuilder.Length == 0) { if (File.Exists(OutboxPath)) { File.Delete(OutboxPath); } } else { File.WriteAllText(OutboxPath, stringBuilder.ToString()); } } catch { } } private static void LoadOutbox() { try { if (!File.Exists(OutboxPath)) { return; } string[] array = File.ReadAllLines(OutboxPath); foreach (string text in array) { int num = text.IndexOf('\t'); if (num > 0 && num < text.Length - 1) { _pendingReports.Add(new PendingReport { url = text.Substring(0, num), json = text.Substring(num + 1), attempts = 0, nextAt = Time.realtimeSinceStartup + 20f }); } } if (_pendingReports.Count > 0) { Plugin.Log.LogInfo((object)$"[OUTBOX] loaded {_pendingReports.Count} unsent report(s) from previous session"); EnsureOutboxLoop(); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[OUTBOX] load failed: " + ex.Message)); } } private static void EnsureOutboxLoop() { if (!_outboxLoopStarted && !((Object)(object)Plugin.Instance == (Object)null)) { _outboxLoopStarted = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(OutboxLoop()); } } private static IEnumerator OutboxLoop() { while (true) { yield return (object)new WaitForSecondsRealtime(10f); if (_pendingReports.Count == 0) { continue; } float now = Time.realtimeSinceStartup; bool changed = false; for (int i = _pendingReports.Count - 1; i >= 0; i--) { PendingReport p = _pendingReports[i]; if (!(p.nextAt > now)) { p.attempts++; p.nextAt = now + 60f * (float)Math.Min(4, p.attempts); bool done = false; bool ok = false; string resp = null; yield return PostRequest(p.url, p.json, delegate(bool s, string r) { done = true; ok = s; resp = r; }); while (!done) { yield return null; } if (ok) { Plugin.Log.LogInfo((object)$"[OUTBOX] queued report delivered on retry {p.attempts}"); if (!IsSilentOutboxUrl(p.url)) { CompetitiveUI.ShowNotification("Match report delivered", new Color(0.4f, 1f, 0.5f), 4f); } _pendingReports.RemoveAt(i); changed = true; } else { bool retryableMacroResponse = IsSilentOutboxUrl(p.url) && resp != null && (resp.Contains("401") || resp.Contains("409") || resp.Contains("429")); bool retryableTransient = resp != null && (resp.Contains("HTTP 429") || resp.Contains("HTTP/1.1 429") || resp.Contains("HTTP 401") || resp.Contains("HTTP/1.1 401")); if ((!retryableMacroResponse && !retryableTransient && resp != null && (resp.StartsWith("HTTP 4", StringComparison.Ordinal) || resp.Contains("HTTP/1.1 4") || resp.Contains("duplicate key"))) || p.attempts >= 20) { Plugin.Log.LogWarning((object)string.Format("[OUTBOX] dropping report after {0} attempt(s): {1}", p.attempts, Trunc(resp ?? "(no response)", 160))); _pendingReports.RemoveAt(i); changed = true; } } } } if (changed || _pendingReports.Count > 0) { PersistOutbox(); } } } private static string Trunc(string s, int n) { return (string.IsNullOrEmpty(s) || s.Length <= n) ? s : (s.Substring(0, n) + ".."); } 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; } if (++_hbTournamentCurrentTick >= 3) { _hbTournamentCurrentTick = 0; try { FetchTournamentCurrent(sid); } catch { } } 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)) { continue; } seen.Add(m.tournament_id); TournamentReady(m.tournament_id, sid); yield return (object)new WaitForSeconds(0.2f); bool isSyncReady = m.kind == "sync" && m.status == "ready" && m.my_ready && m.opp_ready && !string.IsNullOrEmpty(m.photon_room_name) && !string.IsNullOrEmpty(m.match_id); if (isSyncReady && !_heartbeatDispatchedMatches.Contains(m.match_id)) { if (Plugin.PendingRankedRoom != m.photon_room_name) { _heartbeatDispatchedMatches.Add(m.match_id); _heartbeatDispatchTime[m.match_id] = Time.unscaledTime; 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)); } } else if (isSyncReady && _heartbeatDispatchedMatches.Contains(m.match_id)) { float at; bool oldEnough = !_heartbeatDispatchTime.TryGetValue(m.match_id, out at) || Time.unscaledTime - at > 60f; bool inTargetRoom = false; try { inTargetRoom = PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && PhotonNetwork.CurrentRoom.Name == m.photon_room_name; } catch { } if (oldEnough && !inTargetRoom && Plugin.PendingRankedRoom != m.photon_room_name) { _heartbeatDispatchedMatches.Remove(m.match_id); Plugin.Log.LogInfo((object)("[TOURNAMENT-HB] Not in match room 60s after dispatch — re-arming auto-connect for " + m.match_id)); } } } if (CachedMyActiveTournamentMatches.Count == 0) { _heartbeatDispatchedMatches.Clear(); continue; } HashSet<string> stillActive = new HashSet<string>(); foreach (ActiveTournamentMatch m2 in CachedMyActiveTournamentMatches) { if (!string.IsNullOrEmpty(m2.match_id)) { stillActive.Add(m2.match_id); } } _heartbeatDispatchedMatches.RemoveWhere((string id) => !stillActive.Contains(id)); } } private static IEnumerator ProbeEndpointThenStart() { bool reachable = false; for (int i = 0; i < 2; i++) { if (reachable) { break; } UnityWebRequest probe = UnityWebRequest.Get(baseUrl + "/api/v1/mod-version"); try { probe.timeout = 8; StampVersionHeader(probe); yield return probe.SendWebRequest(); object obj; if ((int)probe.result != 1) { obj = ""; } else { DownloadHandler downloadHandler = probe.downloadHandler; obj = ((downloadHandler != null) ? downloadHandler.text : null) ?? ""; } string body = (string)obj; if (!string.IsNullOrEmpty(ExtractJsonString(body, "version"))) { reachable = true; } else { Plugin.Log.LogWarning((object)($"[API] TLS endpoint probe {i + 1}/{2} unusable: " + $"result={probe.result} code={probe.responseCode} err={probe.error}")); } } finally { ((IDisposable)probe)?.Dispose(); } } if (!reachable) { baseUrl = "http://competitive-rounds.duckdns.org:8443".TrimEnd('/'); UsingLegacyFallback = true; Plugin.Log.LogWarning((object)("[API] https://competitive-rounds.duckdns.org:8444 unreachable — falling back to " + baseUrl + " for this session. Config is left untouched, so TLS is retried on next launch.")); try { ChatClient.Retarget(baseUrl); } catch (Exception ex) { Exception ex2 = ex; Plugin.Log.LogWarning((object)("[API] chat retarget failed: " + ex2.Message)); } } CheckModVersion(); } 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 ver = ExtractJsonString(req.downloadHandler.text, "version"); string minVer = ExtractJsonString(req.downloadHandler.text, "min_version"); if (!string.IsNullOrEmpty(ver)) { LatestModVersion = ver; if (ver != "1.35.5") { Plugin.Log.LogWarning((object)("[VERSION] Update available: v1.35.5 → v" + ver)); } else { Plugin.Log.LogInfo((object)("[VERSION] Mod is up to date (v" + ver + ")")); } } if (!string.IsNullOrEmpty(minVer)) { MinModVersion = minVer; if (CompareVersion("1.35.5", minVer) < 0) { ForceUpdateRequired = true; Plugin.Log.LogWarning((object)("[VERSION] Mod is BELOW server minimum (v1.35.5 < v" + minVer + ") — update required")); } } if (!string.IsNullOrEmpty(LatestModVersion) && CompareVersion("1.35.5", 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) { return (result >= result2) ? 1 : (-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 ""; } } private static string ComputeAdminHmacHex(string message) { string text = null; try { text = Plugin.AdminHmacSecret?.Value; } catch { } if (string.IsNullOrWhiteSpace(text)) { if (!_adminSecretWarned) { _adminSecretWarned = true; Plugin.Log.LogWarning((object)"[ADMIN] AdminSecret is not set in the config — admin actions will be rejected by the server (security A2)."); } return ""; } try { using HMACSHA256 hMACSHA = new HMACSHA256(Encoding.UTF8.GetBytes(text.Trim())); 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 ""; } } private static string ComputeSha256Hex(string value) { try { using SHA256 sHA = SHA256.Create(); byte[] array = sHA.ComputeHash(Encoding.UTF8.GetBytes(value ?? "")); 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 FetchRankTiers() { if (_rankTiersInFlight || (CachedRankTiers != null && Time.realtimeSinceStartup - _rankTiersAt < 420f)) { return; } _rankTiersAt = Time.realtimeSinceStartup; _rankTiersInFlight = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/rank-tiers", delegate(bool ok, string resp) { _rankTiersInFlight = false; if (!ok || string.IsNullOrEmpty(resp)) { return; } try { List<RankTierEntry> list = new List<RankTierEntry>(); int num = resp.IndexOf("\"tiers\":", StringComparison.Ordinal); int num2 = ((num >= 0) ? resp.IndexOf('[', num) : (-1)); int num3 = ((num2 >= 0) ? FindMatchingBracketStringAware(resp, num2) : (-1)); if (num2 >= 0 && num3 > num2) { foreach (string item in SliceTopLevelObjects(resp.Substring(num2 + 1, num3 - num2 - 1))) { RankTierEntry rankTierEntry = new RankTierEntry { floor = ExtractJsonInt(item, "floor"), name = ExtractJsonString(item, "name"), color = ExtractJsonString(item, "color") }; if (!string.IsNullOrEmpty(rankTierEntry.name)) { list.Add(rankTierEntry); } } if (list.Count > 0) { CachedRankTiers = list; NativeUI.MarkDirty(); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[RANK-TIERS] parse failed: " + ex.Message)); } })); } public static void FetchRecentMultimodeSeries(int limit = 60) { if (_multimodeInFlight) { return; } _multimodeInFlight = true; ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest($"{baseUrl}/api/v1/series/recent-multimode?limit={limit}", delegate(bool ok, string resp) { _multimodeInFlight = false; if (!ok || string.IsNullOrEmpty(resp)) { return; } try { List<MultimodeSeriesEntry> list = new List<MultimodeSeriesEntry>(); int num = resp.IndexOf("\"entries\":", StringComparison.Ordinal); int num2 = ((num >= 0) ? resp.IndexOf('[', num) : (-1)); int num3 = ((num2 >= 0) ? FindMatchingBracketStringAware(resp, num2) : (-1)); if (num2 >= 0 && num3 > num2) { foreach (string item in SliceTopLevelObjects(resp.Substring(num2 + 1, num3 - num2 - 1))) { MultimodeSeriesEntry multimodeSeriesEntry = new MultimodeSeriesEntry { mode = ExtractJsonString(item, "mode"), id = ExtractJsonString(item, "id"), ended_at = ExtractJsonString(item, "ended_at"), left_label = ExtractJsonString(item, "left_label"), right_label = ExtractJsonString(item, "right_label"), score = ExtractJsonString(item, "score"), left_rating_change = ExtractJsonFloat(item, "left_rating_change"), right_rating_change = ExtractJsonFloat(item, "right_rating_change") }; int num4 = item.IndexOf("\"bets\":", StringComparison.Ordinal); int num5 = ((num4 >= 0) ? item.IndexOf('[', num4) : (-1)); int num6 = ((num5 >= 0) ? FindMatchingBracketStringAware(item, num5) : (-1)); if (num5 >= 0 && num6 > num5) { foreach (string item2 in SliceTopLevelObjects(item.Substring(num5 + 1, num6 - num5 - 1))) { MultimodeBet multimodeBet = new MultimodeBet { bettor_name = ExtractJsonString(item2, "bettor_name"), bettor_steam_id = ExtractJsonString(item2, "bettor_steam_id"), bet_on_label = ExtractJsonString(item2, "bet_on_label"), amount = ExtractJsonInt(item2, "amount"), payout = ExtractJsonInt(item2, "payout"), odds_multiplier = ExtractJsonFloat(item2, "odds_multiplier"), state = ExtractJsonString(item2, "state") }; if (!string.IsNullOrEmpty(multimodeBet.bettor_name)) { multimodeSeriesEntry.bets.Add(multimodeBet); } } } if (!string.IsNullOrEmpty(multimodeSeriesEntry.mode)) { list.Add(multimodeSeriesEntry); } } CachedRecentMultimode = list; Plugin.Log.LogInfo((object)$"Recent multimode series loaded: {list.Count}"); NativeUI.MarkDirty(); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[MULTIMODE] parse failed: " + ex.Message)); } })); } 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"), settled = !string.IsNullOrEmpty(ExtractJsonString(json, "settled_at")), payout = ExtractJsonInt(json, "payout"), vs_name = ExtractJsonString(json, "vs_name"), created_at = ExtractJsonString(json, "created_at") }; 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 SetAppearOffline(string steamId, bool on, Action<bool, string> callback = null) { string text = ComputeHmacHex($"appear_offline:{steamId}:{(on ? 1 : 0)}"); string url = baseUrl + "/api/v1/players/" + steamId + "/appear-offline?on=" + (on ? "true" : "false") + "&sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[HOME] set appear_offline {on}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); FetchOnlinePlayers(); } })); } public static void SetShowDiscord(string steamId, bool on, Action<bool, string> callback = null) { string text = ComputeHmacHex($"show_discord:{steamId}:{(on ? 1 : 0)}"); string url = baseUrl + "/api/v1/players/" + steamId + "/show-discord?on=" + (on ? "true" : "false") + "&sig=" + text; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[SETTINGS] set show_discord {on}: ok={ok} resp={resp}"); callback?.Invoke(ok, resp); if (ok) { FetchPlayerStats(steamId); } })); } 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"); shopItemData.artist_steam_id = ExtractJsonString(json, "artist_steam_id"); shopItemData.artist_name = ExtractJsonString(json, "artist_name"); shopItemData.stock_limit = ExtractJsonInt(json, "stock_limit"); shopItemData.stock_sold = ExtractJsonInt(json, "stock_sold"); 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(); string sentToken = SteamAuth.SessionToken; StampVersionHeader(request); request.timeout = 20; yield return request.SendWebRequest(); bool success = (int)request.result == 1; if (!success) { HandleSessionReject(request, sentToken); } object arg; if (!success) { arg = request.error; } else { DownloadHandler downloadHandler = request.downloadHandler; arg = ((downloadHandler != null) ? downloadHandler.text : null) ?? ""; } callback(success, (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); CheckArtistStatus(steamId); })); } public static void CheckArtistStatus(string steamId, Action<bool> callback = null) { if (string.IsNullOrEmpty(steamId)) { IsArtist = false; callback?.Invoke(obj: false); return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/artist/status/" + steamId, delegate(bool ok, string body) { bool flag = false; if (ok && !string.IsNullOrEmpty(body)) { flag = body.Contains("\"is_artist\":true") || body.Contains("\"is_artist\": true"); } IsArtist = flag; if (flag) { Plugin.Log.LogInfo((object)("[ARTIST] " + steamId + " is an artist")); } callback?.Invoke(flag); })); } public static void FetchArtistItems(string steamId) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown") { return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/artist/" + steamId + "/items", delegate(bool ok, string resp) { if (!ok) { Plugin.Log.LogWarning((object)("[ARTIST] items fetch failed: " + resp)); } else { List<ArtistItemEntry> list = new List<ArtistItemEntry>(); List<ArtistBlockEntry> list2 = new List<ArtistBlockEntry>(); try { int num = resp.IndexOf("\"blocked\"", StringComparison.Ordinal); string text = ((num > 0) ? resp.Substring(0, num) : resp); string text2 = ((num > 0) ? resp.Substring(num) : ""); string[] array = text.Split(new string[1] { "\"sku\":" }, StringSplitOptions.None); for (int i = 1; i < array.Length; i++) { string json = "\"sku\":" + array[i]; ArtistItemEntry artistItemEntry = new ArtistItemEntry { sku = ExtractJsonString(json, "sku"), kind = ExtractJsonString(json, "kind"), name = ExtractJsonString(json, "name"), description = ExtractJsonString(json, "description"), rarity = ExtractJsonString(json, "rarity"), price = ExtractJsonInt(json, "price"), stock_limit = ExtractJsonInt(json, "stock_limit"), catalog_ready = ExtractJsonBool(json, "catalog_ready"), sold = ExtractJsonInt(json, "sold"), gifted = ExtractJsonInt(json, "gifted"), earned = ExtractJsonInt(json, "earned") }; if (!string.IsNullOrEmpty(artistItemEntry.sku)) { list.Add(artistItemEntry); } } string[] array2 = text2.Split(new string[1] { "\"steam_id\":" }, StringSplitOptions.None); for (int j = 1; j < array2.Length; j++) { string json2 = "\"steam_id\":" + array2[j]; ArtistBlockEntry artistBlockEntry = new ArtistBlockEntry { steam_id = ExtractJsonString(json2, "steam_id"), display_name = ExtractJsonString(json2, "display_name") }; if (!string.IsNullOrEmpty(artistBlockEntry.steam_id)) { list2.Add(artistBlockEntry); } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ARTIST] items parse: " + ex.Message)); } CachedArtistItems = list; CachedArtistBlocks = list2; NativeUI.MarkDirty(); } })); } public static void FetchArtistSales(string steamId) { if (string.IsNullOrEmpty(steamId) || steamId == "unknown") { return; } ((MonoBehaviour)Plugin.Instance).StartCoroutine(GetRequest(baseUrl + "/api/v1/artist/" + steamId + "/sales?limit=50", delegate(bool ok, string resp) { if (!ok) { Plugin.Log.LogWarning((object)("[ARTIST] sales fetch failed: " + resp)); } else { List<ArtistSaleEntry> list = new List<ArtistSaleEntry>(); try { int num = resp.IndexOf("\"sales\"", StringComparison.Ordinal); int num2 = ((num >= 0) ? resp.IndexOf('[', num) : (-1)); int num3 = ((num2 >= 0) ? FindMatchingBracketStringAware(resp, num2) : (-1)); if (num2 >= 0 && num3 > num2) { foreach (string item in SliceTopLevelObjects(resp.Substring(num2 + 1, num3 - num2 - 1))) { ArtistSaleEntry artistSaleEntry = new ArtistSaleEntry { item = ExtractJsonString(item, "item"), buyer = ExtractJsonString(item, "buyer"), when = ExtractJsonString(item, "when"), price = ExtractJsonInt(item, "price"), earned = ExtractJsonInt(item, "earned") }; if (!string.IsNullOrEmpty(artistSaleEntry.item)) { list.Add(artistSaleEntry); } } } } catch (Exception ex) { Plugin.Log.LogWarning((object)("[ARTIST] sales parse: " + ex.Message)); } CachedArtistSales = list; NativeUI.MarkDirty(); } })); } public static void ArtistSetPrice(string steamId, string sku, int price, Action<bool, string> callback = null) { string text = ComputeHmacHex($"artist:{steamId}:set-price:{sku}:{price}"); string url = $"{baseUrl}/api/v1/artist/set-price?steam_id={Escape(steamId)}&sku={Escape(sku)}&price={price}&sig={text}"; ((MonoBehaviour)Plugin.Instance).StartCoroutine(PostRequest(url, "", delegate(bool ok, string resp) { Plugin.Log.LogInfo((object)$"[ARTIST] set-price {sku}={price}: ok={ok}"); if (o