Decompiled source of SBGL UnifiedMod v0.1.20
SBGL.UnifiedMod.dll
Decompiled 3 weeks 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.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Security.Permissions; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using FMODUnity; using HarmonyLib; using Microsoft.CodeAnalysis; using Mirror; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SBGL.UnifiedMod.Core; using SBGL.UnifiedMod.Features; using SBGL.UnifiedMod.Features.CompetitivePluginCheck; using SBGL.UnifiedMod.Features.HitTracker; using SBGL.UnifiedMod.Features.Poll; using SBGL.UnifiedMod.Patches; using SBGL.UnifiedMod.Utils; using SBGLLiveLeaderboard; using SBGLeagueAutomation; using Steamworks; using Steamworks.Data; using TMPro; using UnityEngine; using UnityEngine.Events; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Localization; using UnityEngine.Networking; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: IgnoresAccessChecksTo("GameAssembly")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("SBGL.UnifiedMod")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("0.1.20.0")] [assembly: AssemblyInformationalVersion("0.1.20+54a0e429eca82ffe2ab88e5208f61fd03cfa0465")] [assembly: AssemblyProduct("Super Battle Golf League Unified Mod")] [assembly: AssemblyTitle("SBGL.UnifiedMod")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.20.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 SBGLeagueAutomation { public class SBGLPlugin : MonoBehaviour { [HarmonyPatch] private static class CourseManager_RpcInformEndingCourse_Patch { private static MethodBase TargetMethod() { Type type = AccessTools.TypeByName("CourseManager"); if (type == null) { return null; } return AccessTools.Method(type, "UserCode_RpcInformEndingCourse", (Type[])null, (Type[])null); } private static void Postfix() { try { SBGLPlugin sBGLPlugin = Object.FindAnyObjectByType<SBGLPlugin>((FindObjectsInactive)1); if ((Object)(object)sBGLPlugin != (Object)null) { sBGLPlugin.OnCourseRpcInformEndingCourse(); } } catch (Exception arg) { Debug.Log((object)$"[SBGL.Matchmaking] CourseManager.RpcInformEndingCourse postfix error: {arg}"); } } } public class PlayerProfile { public string id; public string display_name; public string region; public string state_province; public float current_mmr; } public class MatchmakingSession { public string id; public string lobby_name; public string lobby_password; public string host_player_id; public string status; public string match_id; public string lobby_id; public string host_steam_id; public string steam_lobby_link; public List<string> player_ids; public List<string> accepted_player_ids; public string match_type; public string selected_course; public int season; } public class MatchStats { public string matchmaking_session_id; public string match_id; public string player_id; public string player_name; public string match_date; public int duration_seconds; public bool is_host; public string status; public string course_name; public string match_type; public int season; } private class CachedLeaderboardPlayer { public string Name; public int BaseScore; public string RawStrokes; public string MMR; public int ProjectedDelta; } public struct PlayerData { public string name; public string mmr; } [CompilerGenerated] private sealed class <UploadMatchScreenshotFile>d__153 : IEnumerator<object>, IEnumerator, IDisposable { private int <>1__state; private object <>2__current; public Action<string> onUploaded; public SBGLPlugin <>4__this; private string <uploadUrl>5__1; private WWWForm <form>5__2; private UnityWebRequest <req>5__3; private JObject <response>5__4; private string <fileUrl>5__5; private string <errorMsg>5__6; private int <length>5__7; object IEnumerator<object>.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public <UploadMatchScreenshotFile>d__153(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { int num = <>1__state; if (num == -3 || num == 1) { try { } finally { <>m__Finally1(); } } <uploadUrl>5__1 = null; <form>5__2 = null; <req>5__3 = null; <response>5__4 = null; <fileUrl>5__5 = null; <errorMsg>5__6 = null; <>1__state = -2; } private bool MoveNext() { //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 //IL_0148: Unknown result type (might be due to invalid IL or missing references) try { switch (<>1__state) { default: return false; case 0: <>1__state = -1; onUploaded?.Invoke(null); return false; case 1: <>1__state = -3; if ((int)<req>5__3.result == 1) { <response>5__4 = <>4__this.ParseApiSingleObject(<req>5__3.downloadHandler.text); JObject obj = <response>5__4; string text = (string)((obj != null) ? obj["file_url"] : null); if (text == null) { JObject obj2 = <response>5__4; text = (string)((obj2 != null) ? obj2["url"] : null); } <fileUrl>5__5 = text; if (!string.IsNullOrWhiteSpace(<fileUrl>5__5)) { <>4__this.Log("<color=green>[Match Screenshot] ✓ Screenshot uploaded to file storage</color>"); onUploaded?.Invoke(<fileUrl>5__5); } else { <>4__this.Log("<color=yellow>[Match Screenshot] UploadFile succeeded but response did not contain file_url</color>"); onUploaded?.Invoke(null); } <response>5__4 = null; <fileUrl>5__5 = null; } else { <errorMsg>5__6 = $"[Match Screenshot] UploadFile failed: {<req>5__3.result}"; if (!string.IsNullOrEmpty(<req>5__3.error)) { <errorMsg>5__6 = <errorMsg>5__6 + " - " + <req>5__3.error; } if (<req>5__3.responseCode > 0) { <errorMsg>5__6 += $" (HTTP {<req>5__3.responseCode})"; } DownloadHandler downloadHandler = <req>5__3.downloadHandler; if (!string.IsNullOrEmpty((downloadHandler != null) ? downloadHandler.text : null)) { <length>5__7 = Math.Min(240, <req>5__3.downloadHandler.text.Length); <errorMsg>5__6 = <errorMsg>5__6 + " - Response: " + <req>5__3.downloadHandler.text.Substring(0, <length>5__7); } <>4__this.Log("<color=red>" + <errorMsg>5__6 + "</color>"); onUploaded?.Invoke(null); <errorMsg>5__6 = null; } <>m__Finally1(); <req>5__3 = null; <uploadUrl>5__1 = null; <form>5__2 = null; break; } } catch { //try-fault ((IDisposable)this).Dispose(); throw; } ((IDisposable)this).Dispose(); return true; } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } private void <>m__Finally1() { <>1__state = -1; if (<req>5__3 != null) { ((IDisposable)<req>5__3).Dispose(); } } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } private Dictionary<string, string> _randomizedPlayerMap = null; private static readonly List<string> _sbglTestNames = new List<string> { "TrashxCat", "KingCire03", "FucklTheIRS", "Jackie", "TikTok Z4C_FN", "adthykrshnn", "mrguy7608", "Noah_Boatt", "YoMaMMeJr", "TheMrEStudio", "Odius9064", "Jake paul", "f19chy", "Kodakblackarack", "ArrowTheFighter", "limbo", "Marcus_Pipes", "That Traynor", "Blinkerfluid", "TaxiCAB", "Achunl2", "Cinereous", "Liafeon", "Peter", "Inkie", "LLENN", "Nanainasuit", "Midnight", "PattyTits", "Bekuh", "Lunwik", "yackback", "AlpineMilk", "HandsomeSkippy", "TheOneKP", "RealJosher", "Ooshida", "Moto", "Jozza", "Patrick Swayze", "ryan.scibetta", "Zaikr", "Thalosii", "paradiorevey", "Ricee", "Slacker87", "MRGoldberg", "trusted", "designedsilence", "The Oreo Orgy", "Zoboomafoo", "Yoda Cage", "Black Dolphin", "Pengini", "antyde", "Glider", "lyth", "Jabobus.o7", "dkgaming219", "Cody", "Sidimmu", "снусмумрик", "Skydown26", "Jeb", "Vac", "Slem Dogg", "Batto", "Broj0e" }; private ConfigEntry<bool> _showLogsConfig; private ConfigEntry<bool> _showFlowDebugConfig; private ConfigEntry<bool> _showUploadNoticesConfig; private ConfigEntry<bool> _ignoreSbglLobbyRequirementConfig; private ConfigEntry<string> _testPlayerOverridesConfig; private ConfigEntry<bool> _testRandomizeAllPlayersConfig; private ManualLogSource _bepinexLogger; private bool _isInitializing = true; private int _onlineCount = 0; private int _queuedCount = 0; private int _matchedCount = 0; public static bool IsRankedTriggered = false; public static string CurrentMatchId = null; private bool _isQueueing = false; private string _webStatus = "IDLE"; private string _currentQueueId = ""; private PlayerProfile _userProfile = null; private MatchmakingSession _currentSession = null; private bool _isHost = false; private bool _hasAccepted = false; private string _hostRulesetSelection = "ranked"; private string _queueTypeSelection = "ranked"; private DateTime? _queueStartTime = null; private bool _hostLobbyStarted = false; private bool _hostServerWasActive = false; private bool _hostCancelSent = false; private bool _matchStatsSubmitted = false; private bool _proSeriesSkipLogged = false; private DateTime? _matchStartTime = null; private Dictionary<string, int> _cachedLeaderboardScores = new Dictionary<string, int>(); private Dictionary<string, int> _cachedLeaderboardScoresVsPar = new Dictionary<string, int>(); private string __currentMatchId = null; private HashSet<string> _finalizedMatchIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private Dictionary<string, string> _playerMatchEntryIds = new Dictionary<string, string>(); private HashSet<string> _matchEntryCreationInProgress = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private Dictionary<string, string> _playerIdsByName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); private Dictionary<string, int> _lastSubmittedScores = new Dictionary<string, int>(); private Dictionary<string, int> _lastSubmittedScoresVsPar = new Dictionary<string, int>(); private int _matchExpectedPlayerCount = 2; private int _lastUploadedPlayerCount = -1; private List<CachedLeaderboardPlayer> _finalLeaderboardSnapshot = new List<CachedLeaderboardPlayer>(); private byte[] _pendingMatchScreenshotBytes = null; private string _pendingMatchScreenshotFileName = null; private string _pendingUploadedScreenshotUrl = null; private bool _matchScreenshotUploadInProgress = false; private bool _matchScreenshotUploadCompleted = false; private bool _matchEntriesCreated = false; private bool _matchCreationInProgress = false; private bool _isInGameplay = false; private bool _cachedIsMenuScene = false; private bool _matchEndedReceived = false; private Coroutine _monitorCoroutine = null; private Coroutine _lobbyMonitorCoroutine = null; private Coroutine _endOfMatchSignalCoroutine = null; private Coroutine _matchScreenshotSignalCoroutine = null; private Coroutine _courseEndRpcCoroutine = null; private float _nextEnsureMatchCreateAttemptAt = 0f; private string _localManualSessionId = null; private const int MatchScreenshotMaxWidth = 1920; private const int MatchScreenshotMaxHeight = 1080; private const int MatchScreenshotTargetMaxBytes = 819200; private const int MatchScreenshotHardMaxBytes = 1048576; private string _activeSeasonId = null; private string _activeSeasonName = null; private bool _activeSeasonFetched = false; private List<string> _debugLogs = new List<string>(); private List<PlayerData> _queuedPlayers = new List<PlayerData>(); private Vector2 _logScroll; private Texture2D _profileTexture = null; private Texture2D _solidBgTex = null; private bool _hasFetchedProfilePic = false; private GUIStyle _centerLabelStyle = null; private GUIStyle _debugLineStyle = null; private string _uploadNotification = ""; private DateTime _uploadNotificationTime = DateTime.MinValue; private const float _uploadNotificationDuration = 4f; private Color _uploadNotificationColor = new Color(0.2f, 0.85f, 1f); private string _debugLobbySessionSource = ""; private string _debugLobbyPrefsSource = ""; private string _debugLobbyCapturedSource = ""; private string _debugLobbyResolved = ""; private string _debugLobbyResolvedBy = "none"; private MatchResultSubmissionService _matchResultSubmission; private static readonly WaitForSeconds _syncLoopDelay = new WaitForSeconds(5f); private static readonly WaitForSeconds _readyTransitionDelay = new WaitForSeconds(0.5f); private int _syncTickCount = 0; private int _lobbyCreatedEventCount = 0; private int _steamLinkUploadAttempts = 0; private int _steamLinkUploadSuccesses = 0; private int _steamLinkUploadFailures = 0; private int _autoJoinAttempts = 0; private int _autoJoinSuccesses = 0; private int _autoJoinFailures = 0; private DateTime? _lastLobbyCreatedAt = null; private DateTime? _lastUploadAttemptAt = null; private DateTime? _lastUploadSuccessAt = null; private DateTime? _lastAutoJoinAttemptAt = null; private DateTime? _lastAutoJoinSuccessAt = null; private string _lastGeneratedSteamLink = "-"; private string _lastUploadedSteamLink = "-"; private string _lastAutoJoinSteamLink = "-"; private string _lastUploadError = "-"; private string _lastAutoJoinError = "-"; private string _lastCallApiError = ""; private bool IgnoreSbglLobbyRequirementEnabled => _ignoreSbglLobbyRequirementConfig?.Value ?? false; private string _currentMatchId { get { return __currentMatchId; } set { __currentMatchId = value; CurrentMatchId = value; } } private bool IsActiveSeason => _activeSeasonFetched && !string.IsNullOrEmpty(_activeSeasonId); public bool IsQueueing => _isQueueing; public bool IsHost => _isHost; public bool HasAccepted => _hasAccepted; public PlayerProfile UserProfile => _userProfile; public MatchmakingSession CurrentSession => _currentSession; private string GetBaseApiUrl() { return UnifiedPlugin.GetCurrentBaseApi(); } private string GetAppId() { return UnifiedPlugin.GetCurrentAppId(); } private string GetAuthToken() { return UnifiedPlugin.GetCurrentAuthToken(); } public void SetConfig(ConfigEntry<bool> showLogs, ConfigEntry<bool> showFlowDebug, ConfigEntry<bool> showUploadNotices, ConfigEntry<bool> ignoreSbglLobbyRequirement, ConfigEntry<string> testPlayerOverrides, ConfigEntry<bool> testRandomizeAllPlayers, ManualLogSource bepinexLogger) { _showLogsConfig = showLogs; _showFlowDebugConfig = showFlowDebug; _showUploadNoticesConfig = showUploadNotices; _ignoreSbglLobbyRequirementConfig = ignoreSbglLobbyRequirement; _testPlayerOverridesConfig = testPlayerOverrides; _testRandomizeAllPlayersConfig = testRandomizeAllPlayers; _bepinexLogger = bepinexLogger; } private Dictionary<string, string> GetTestPlayerOverrides(List<string> currentPlayerNames = null) { if (_testRandomizeAllPlayersConfig != null && _testRandomizeAllPlayersConfig.Value && currentPlayerNames != null) { if (_randomizedPlayerMap == null || _randomizedPlayerMap.Count != currentPlayerNames.Count || !_randomizedPlayerMap.Keys.SequenceEqual(currentPlayerNames)) { List<string> source = new List<string>(_sbglTestNames); Random rand = new Random(); Dictionary<string, string> dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); List<string> list = source.OrderBy((string x) => rand.Next()).ToList(); int num = 0; foreach (string currentPlayerName in currentPlayerNames) { if (string.IsNullOrWhiteSpace(currentPlayerName)) { continue; } if (_userProfile != null && string.Equals(currentPlayerName, _userProfile.display_name, StringComparison.OrdinalIgnoreCase)) { dictionary[currentPlayerName] = currentPlayerName; continue; } if (num >= list.Count) { num = 0; } dictionary[currentPlayerName] = list[num++]; } _randomizedPlayerMap = dictionary; } return _randomizedPlayerMap; } Dictionary<string, string> dictionary2 = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); string text = _testPlayerOverridesConfig?.Value; if (string.IsNullOrWhiteSpace(text)) { return dictionary2; } string[] array = text.Split(','); foreach (string text2 in array) { int num3 = text2.IndexOf('='); if (num3 > 0 && num3 < text2.Length - 1) { string text3 = text2.Substring(0, num3).Trim(); string value = text2.Substring(num3 + 1).Trim(); if (!string.IsNullOrWhiteSpace(text3) && !string.IsNullOrWhiteSpace(value)) { dictionary2[text3] = value; } } } return dictionary2; } private string ApplyTestPlayerOverride(string displayName, Dictionary<string, string> overrides) { if (overrides != null && overrides.TryGetValue(displayName, out var value)) { Log("<color=magenta>[Test Override] '" + displayName + "' → '" + value + "'</color>"); return value; } return displayName; } public IEnumerator MatchmakingLoopCoroutine() { return MatchmakingLoop(); } public IEnumerator AcceptMatchCoroutine() { return AcceptMatch(); } public void InitiateHostSequencePublic() { InitiateHostSequence(); } public IEnumerator ClaimHostRoleCoroutine() { if (_currentSession == null || _userProfile == null || _isHost) { yield break; } Log("<color=cyan>[PDS] Claiming host role for session " + _currentSession.id + "...</color>"); yield return CallAPI(json: "{\"host_player_id\":\"" + _userProfile.id + "\"}", endpoint: "/matchmaking_session?id=eq." + _currentSession.id, method: "PATCH", onSuccess: delegate(string res) { JObject val = ParseApiSingleObject(res); if (val != null) { _currentSession.host_player_id = _userProfile.id; _isHost = true; Log("<color=green>[PDS] ✓ Host role claimed — session host_player_id set to our player ID.</color>"); } else { Log("<color=red>[PDS] Failed to claim host role — API response was null.</color>"); } }); } private static bool IsProSeriesMatchType(string matchType) { return !string.IsNullOrWhiteSpace(matchType) && matchType.IndexOf("pro_series", StringComparison.OrdinalIgnoreCase) >= 0; } private static bool IsCasualMatchType(string matchType) { return !string.IsNullOrWhiteSpace(matchType) && matchType.IndexOf("casual", StringComparison.OrdinalIgnoreCase) >= 0; } private bool IsCurrentMatchCasual() { if (IsCasualMatchType(_currentSession?.match_type)) { return true; } return IsCasualMatchType(PlayerPrefs.GetString("MatchType", "")); } private string GetQueueMatchTypePayload() { string internalMatchType = PlayerPrefs.GetString("MatchType", "ranked_season_2"); return Season2RuleSet.ToQueueMatchType(internalMatchType); } private void Awake() { //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Expected O, but got Unknown //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) _solidBgTex = new Texture2D(1, 1); _solidBgTex.SetPixel(0, 0, new Color(0.02f, 0.02f, 0.02f, 1f)); _solidBgTex.Apply(); SceneManager.sceneLoaded += OnSceneLoaded; CourseManager.MatchStateChanged += OnCourseManagerMatchStateChanged; CourseManager.ForceDisplayScoreboardChanged += OnCourseManagerForceDisplayScoreboardChanged; new Harmony("com.sbgl.matchmaking").PatchAll(); Log("Plugin Loaded v6.2.1. Self-Sync Reconciliation Active."); _matchResultSubmission = new MatchResultSubmissionService(GetBaseApiUrl, Log, CallAPI, ParseApiSingleObject, delegate(IEnumerator coro) { ((MonoBehaviour)this).StartCoroutine(coro); }); UnifiedPlugin.ApiConfigChanged += OnApiConfigChanged; ((MonoBehaviour)this).StartCoroutine(BackgroundSyncLoop()); } private void OnApiConfigChanged() { Log("⚡ API Configuration changed - Switching environments and refreshing profile"); _userProfile = null; _isInitializing = true; _webStatus = "ENVIRONMENT SWITCHING..."; ResetPluginState(); } private void OnEnable() { try { SteamMatchmaking.OnLobbyCreated += OnLobbyCreatedCallback; } catch (Exception ex) { Log("Warning: Could not hook LobbyCreated event: " + ex.Message); } } private void OnDisable() { try { SteamMatchmaking.OnLobbyCreated -= OnLobbyCreatedCallback; } catch { } } private void OnLobbyCreatedCallback(Result result, Lobby lobby) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Invalid comparison between Unknown and I4 //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) if ((int)result != 1) { _lastUploadError = $"Lobby create callback failed: {result}"; Log($"Lobby creation failed: {result}"); return; } SteamId id = ((Lobby)(ref lobby)).Id; if (!((SteamId)(ref id)).IsValid) { _lastUploadError = "Lobby created callback had invalid lobby id."; Log("Lobby ID is invalid!"); } else { if (!_isHost || _currentSession == null) { return; } ulong value = ((Lobby)(ref lobby)).Id.Value; ulong num = 0uL; try { if (SteamClient.IsValid) { num = SteamClient.SteamId.Value; } } catch { } if (num != 0) { string text = $"steam://joinlobby/4069520/{value}/{num}"; _lobbyCreatedEventCount++; _lastLobbyCreatedAt = DateTime.Now; _lastGeneratedSteamLink = text; Log($"Lobby created! ID: {value}, uploading link..."); ((MonoBehaviour)this).StartCoroutine(UploadSteamLobbyLink(text)); } else { _lastUploadError = "Steam client invalid while trying to build host steam link."; Log("<color=orange>Lobby created but Steam ID could not be resolved.</color>"); } } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { string text = ((Scene)(ref scene)).name.ToLower(); _cachedIsMenuScene = text.Contains("menu"); Log("<color=cyan>[Scene] Loaded: " + ((Scene)(ref scene)).name + "</color>"); string text2 = ResolveCurrentLobbyName(); bool flag = !string.IsNullOrEmpty(text2) && text2.StartsWith("SBGL-", StringComparison.OrdinalIgnoreCase); bool flag2 = IgnoreSbglLobbyRequirementEnabled || IsRankedTriggered || flag; if (!text.Contains("menu") && !text.Contains("drivingrange") && !text.Contains("driving range") && !text.Contains("lobby")) { _isInGameplay = true; if (_currentSession != null && string.Equals(_currentSession.status, "completed", StringComparison.OrdinalIgnoreCase)) { Log("<color=cyan>[Match] Previous MatchmakingSession status was 'completed' — clearing session so new match can be created</color>"); _currentSession = null; } if (flag2 && ((_currentSession != null && (_currentSession.status == "ready" || _currentSession.status == "in_progress")) || _currentSession == null) && !_matchEntriesCreated) { Log("<color=yellow>[Match] Entering gameplay - validating match eligibility...</color>"); ((MonoBehaviour)this).StartCoroutine(ValidateMatchUpload(delegate(bool shouldUpload) { if (shouldUpload) { Log("<color=yellow>[Match] Creating match records...</color>"); ((MonoBehaviour)this).StartCoroutine(CreateMatchAndEntries()); } else { Log("<color=orange>[Match] Match does not meet upload criteria at load - starting lobby rename monitor</color>"); } })); } if (_monitorCoroutine == null) { _monitorCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorAndUpdateScores()); } if (_lobbyMonitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_lobbyMonitorCoroutine); } _lobbyMonitorCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorLobbyNameForUpload()); } if (_isInGameplay && (text.Contains("drivingrange") || text.Contains("driving range"))) { _isInGameplay = false; StopGameplayCoroutinesForMatchEnd("scene transition to driving range"); TryCaptureFinalLeaderboardSnapshot("scene transition"); } if (text.Contains("drivingrange") || text.Contains("driving range")) { if (!_matchStatsSubmitted && !string.IsNullOrEmpty(_currentMatchId)) { Log("<color=cyan>[Match Stats] Returned to Driving Range - running final match finalization before reset...</color>"); ((MonoBehaviour)this).StartCoroutine(FinalizeAndResetAfterDrivingRange()); } else { if (_matchStatsSubmitted && !string.IsNullOrWhiteSpace(_currentMatchId)) { _finalizedMatchIds.Add(_currentMatchId); } ResetPerMatchState(); } if (_isQueueing && !_hasAccepted) { Log("<color=orange>[Queue] Player entered Driving Range while queued - cancelling queue entry...</color>"); ((MonoBehaviour)this).StartCoroutine(LeaveQueue()); } } if (text.Contains("menu")) { if (_isHost && _hostLobbyStarted && _hostServerWasActive && _currentSession != null) { ((MonoBehaviour)this).StartCoroutine(CancelSessionAsHost("host_returned_to_menu")); } ResetPluginState(); Log("Returned to Menu: State Reset."); } } private void OnCourseManagerMatchStateChanged(MatchState previousState, MatchState currentState) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) if (_isInGameplay) { Log($"<color=cyan>[Match Signal] CourseManager.MatchStateChanged: {previousState} -> {currentState} (ignored for course-end)</color>"); } } private void OnCourseManagerForceDisplayScoreboardChanged() { if (_isInGameplay && CourseManager.ForceDisplayScoreboard) { Log("<color=cyan>[Match Signal] CourseManager forced the scoreboard — hole complete</color>"); if (_matchScreenshotSignalCoroutine == null && !_matchScreenshotUploadCompleted) { _matchScreenshotSignalCoroutine = ((MonoBehaviour)this).StartCoroutine(CaptureAndStorePerHoleScreenshot("CourseManager.ForceDisplayScoreboardChanged")); } if (!string.IsNullOrEmpty(_currentMatchId)) { ((MonoBehaviour)this).StartCoroutine(PushPerHoleScoreUpdate()); } } } private IEnumerator PushPerHoleScoreUpdate() { LiveLeaderboardPlugin liveLeaderboard = Object.FindAnyObjectByType<LiveLeaderboardPlugin>((FindObjectsInactive)1); if ((Object)(object)liveLeaderboard == (Object)null) { yield break; } List<LiveLeaderboardPlugin.SBGLPlayer> players = liveLeaderboard.GetCurrentLeaderboard(); if (players == null || players.Count == 0) { yield break; } List<string> names = (from p in players where p != null && !string.IsNullOrWhiteSpace(p.Name) select p.Name).ToList(); Dictionary<string, string> testOverrides = GetTestPlayerOverrides(names); foreach (LiveLeaderboardPlugin.SBGLPlayer player in players) { if (player == null || string.IsNullOrWhiteSpace(player.Name) || player.BaseScore == 0) { continue; } int gamePoints = player.BaseScore; int scoreVsPar = 0; if (!string.IsNullOrEmpty(player.RawStrokes)) { string s = player.RawStrokes.Replace("±", "").Trim(); int.TryParse(s, out scoreVsPar); } string playerId = null; if (!TryGetPlayerIdForName(player.Name, out playerId)) { string overrideName = ApplyTestPlayerOverride(player.Name, testOverrides); yield return ResolvePlayerIdByNameFromApi(overrideName, delegate(string id) { playerId = id; }); if (!string.IsNullOrEmpty(playerId)) { _playerIdsByName[player.Name.Trim()] = playerId; } } if (string.IsNullOrEmpty(playerId)) { continue; } string entryId = null; _playerMatchEntryIds.TryGetValue(playerId, out entryId); string preHoleMmr = null; if (_userProfile != null && string.Equals(player.Name, _userProfile.display_name, StringComparison.OrdinalIgnoreCase)) { preHoleMmr = ((_userProfile.current_mmr > 0f) ? _userProfile.current_mmr.ToString(CultureInfo.InvariantCulture) : null); } float result; if (string.IsNullOrEmpty(preHoleMmr) && !string.IsNullOrEmpty(playerId)) { string lbMmr = player.MMR; if (!string.IsNullOrEmpty(lbMmr) && lbMmr != "..." && lbMmr != "--" && float.TryParse(lbMmr, NumberStyles.Float, CultureInfo.InvariantCulture, out result)) { preHoleMmr = lbMmr; } else { yield return CallAPI("/player?id=eq." + playerId + "&select=current_mmr&limit=1", "GET", "", delegate(string res) { JObject val = ParseApiSingleObject(res); object obj = ((val != null) ? val["current_mmr"] : null); if (obj != null) { preHoleMmr = obj.ToString(); } }); } } string postHoleMmr = null; if (!string.IsNullOrEmpty(preHoleMmr) && float.TryParse(preHoleMmr, NumberStyles.Float, CultureInfo.InvariantCulture, out var preHoleMmrF)) { int delta = ParseProjectedMmrDelta(player.ProjectedDisplay); result = preHoleMmrF + (float)delta; postHoleMmr = result.ToString(CultureInfo.InvariantCulture); } if (string.IsNullOrEmpty(entryId)) { yield return EnsureMatchEntryForPlayer("Per-Hole Push", playerId, player.Name, preHoleMmr, postHoleMmr, gamePoints, scoreVsPar, 0, "Created on hole completion", delegate(string id) { entryId = id; }); } if (!string.IsNullOrEmpty(entryId)) { yield return UpdateMatchEntry(entryId, playerId, player.Name, gamePoints, scoreVsPar, 0, postHoleMmr, preHoleMmr); _lastSubmittedScores[player.Name] = gamePoints; _lastSubmittedScoresVsPar[player.Name] = scoreVsPar; } } } private IEnumerator CaptureAndStorePerHoleScreenshot(string source) { try { yield return CaptureMatchScreenshotForReview(source + " (per-hole immediate)"); } finally { _matchScreenshotSignalCoroutine = null; } } private IEnumerator CaptureAndUploadScreenshotFromForcedScoreboard(string source) { try { yield return (object)new WaitForSeconds(2f); float waited = 0f; float snapshotTimeout = 6f; bool gotFinalSnapshot = false; for (; waited < snapshotTimeout; waited += 0.25f) { if (_matchEndedReceived) { gotFinalSnapshot = true; break; } try { if (TryCaptureFinalLeaderboardSnapshot(source + " (screenshot-wait)")) { gotFinalSnapshot = true; break; } } catch { } yield return (object)new WaitForSeconds(0.25f); } if (!gotFinalSnapshot) { Log($"<color=yellow>[Match Screenshot] Final leaderboard snapshot not available within {snapshotTimeout}s - skipping forced-scoreboard capture ({source})</color>"); yield break; } yield return CaptureMatchScreenshotForReview(source); yield return UploadCapturedMatchScreenshotIfNeeded(source); } finally { _matchScreenshotSignalCoroutine = null; } } private void TryBeginAssemblyDrivenFinalization(string source) { if (_endOfMatchSignalCoroutine == null && !_matchStatsSubmitted && !string.IsNullOrWhiteSpace(_currentMatchId)) { _endOfMatchSignalCoroutine = ((MonoBehaviour)this).StartCoroutine(FinalizeFromGameplaySignal(source)); } } private IEnumerator FinalizeFromGameplaySignal(string source) { Log("<color=cyan>[Match Signal] " + source + " detected - attempting early end-of-match finalization...</color>"); StopGameplayCoroutinesForMatchEnd(source); yield return CaptureMatchScreenshotForReview(source); for (int attempt = 0; attempt < 12; attempt++) { if (TryCaptureFinalLeaderboardSnapshot(source)) { break; } yield return (object)new WaitForSeconds(0.25f); } if (_finalLeaderboardSnapshot == null || _finalLeaderboardSnapshot.Count == 0) { yield return UploadCapturedMatchScreenshotIfNeeded(source + " (no final snapshot yet)"); Log("<color=yellow>[Match Signal] " + source + " did not yield a final snapshot yet - falling back to Driving Range finalization</color>"); _endOfMatchSignalCoroutine = null; yield break; } if (!_matchStatsSubmitted && !string.IsNullOrWhiteSpace(_currentMatchId)) { yield return FinalizeMatchStats(); if (IsRankedTriggered && _currentSession != null) { yield return UpdateSessionStatus("completed"); } } yield return UploadCapturedMatchScreenshotIfNeeded(source); _endOfMatchSignalCoroutine = null; } private void StopGameplayCoroutinesForMatchEnd(string source) { if (_monitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_monitorCoroutine); _monitorCoroutine = null; Log("<color=cyan>[Match] Score monitoring stopped (" + source + ")</color>"); } if (_lobbyMonitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_lobbyMonitorCoroutine); _lobbyMonitorCoroutine = null; } } private bool TryCaptureFinalLeaderboardSnapshot(string source) { try { LiveLeaderboardPlugin liveLeaderboardPlugin = Object.FindAnyObjectByType<LiveLeaderboardPlugin>((FindObjectsInactive)1); if ((Object)(object)liveLeaderboardPlugin == (Object)null) { return _finalLeaderboardSnapshot != null && _finalLeaderboardSnapshot.Count > 0; } liveLeaderboardPlugin.CaptureLeaderboardSnapshot(); CacheLeaderboardSnapshot(liveLeaderboardPlugin.GetFinalLeaderboardSnapshot(), source); if (_finalLeaderboardSnapshot != null && _finalLeaderboardSnapshot.Count > 0) { Log("<color=green>[Match] ✓ Leaderboard snapshot captured from " + source + "</color>"); return true; } } catch (Exception ex) { Log("<color=yellow>[Match] Could not capture " + source + " snapshot: " + ex.Message + "</color>"); } return _finalLeaderboardSnapshot != null && _finalLeaderboardSnapshot.Count > 0; } private IEnumerator CaptureMatchScreenshotForReview(string source) { if (_matchScreenshotUploadCompleted || _matchScreenshotUploadInProgress) { yield break; } if (_pendingMatchScreenshotBytes != null && _pendingMatchScreenshotBytes.Length != 0) { Log("<color=cyan>[Match Screenshot] Replacing previous pending screenshot with a newer capture (" + source + ")</color>"); } float waitedSeconds = 0f; while (!CourseManager.ForceDisplayScoreboard && waitedSeconds < 4f) { yield return null; waitedSeconds += Mathf.Max(Time.unscaledDeltaTime, 0.016f); } if (!CourseManager.ForceDisplayScoreboard) { Log("<color=yellow>[Match Screenshot] Forced scoreboard was not visible in time for " + source + " - skipping capture for now</color>"); yield break; } yield return (object)new WaitForEndOfFrame(); int screenWidth = Screen.width; int screenHeight = Screen.height; if (screenWidth <= 0 || screenHeight <= 0) { Log("<color=yellow>[Match Screenshot] Screen dimensions were invalid - skipping capture</color>"); yield break; } Texture2D capturedTexture = null; Texture2D encodedTexture = null; try { capturedTexture = new Texture2D(screenWidth, screenHeight, (TextureFormat)3, false); capturedTexture.ReadPixels(new Rect(0f, 0f, (float)screenWidth, (float)screenHeight), 0, 0, false); capturedTexture.Apply(false, false); encodedTexture = ResizeTextureToFit(capturedTexture, 1920, 1080); byte[] imageBytes = EncodeScreenshotBytes(encodedTexture); if (imageBytes == null || imageBytes.Length == 0) { Log("<color=yellow>[Match Screenshot] JPEG encoding returned no data - skipping capture</color>"); yield break; } _pendingMatchScreenshotBytes = imageBytes; _pendingMatchScreenshotFileName = BuildMatchScreenshotFileName(); _pendingUploadedScreenshotUrl = null; Log($"<color=green>[Match Screenshot] ✓ Captured final scoreboard screenshot from {source} ({((Texture)encodedTexture).width}x{((Texture)encodedTexture).height}, {(float)imageBytes.Length / 1024f:0.#} KB)</color>"); } catch (Exception ex) { Exception ex2 = ex; Log("<color=yellow>[Match Screenshot] Could not capture screenshot from " + source + ": " + ex2.Message + "</color>"); } finally { if ((Object)(object)encodedTexture != (Object)null && encodedTexture != capturedTexture) { Object.Destroy((Object)(object)encodedTexture); } if ((Object)(object)capturedTexture != (Object)null) { Object.Destroy((Object)(object)capturedTexture); } } } private IEnumerator UploadCapturedMatchScreenshotIfNeeded(string source) { if (_matchScreenshotUploadCompleted || _matchScreenshotUploadInProgress || (string.IsNullOrWhiteSpace(_pendingUploadedScreenshotUrl) && (_pendingMatchScreenshotBytes == null || _pendingMatchScreenshotBytes.Length == 0)) || string.IsNullOrWhiteSpace(_currentMatchId)) { yield break; } _matchScreenshotUploadInProgress = true; try { float staggerDelay = GetCurrentPlayerScreenshotUploadDelaySeconds(); if (staggerDelay > 0f) { yield return (object)new WaitForSeconds(staggerDelay); } string existingScreenshotUrl = null; bool readSucceeded = false; yield return CallAPI("/match?id=eq." + _currentMatchId + "&select=*", "GET", "", delegate(string res) { readSucceeded = true; JObject val = ParseApiSingleObject(res); existingScreenshotUrl = (string)((val != null) ? val["screenshot_url"] : null); }); if (readSucceeded && !string.IsNullOrWhiteSpace(existingScreenshotUrl)) { _matchScreenshotUploadCompleted = true; ClearPendingMatchScreenshot(); Log("<color=cyan>[Match Screenshot] Match " + _currentMatchId + " already has a screenshot - skipping duplicate upload (" + source + ")</color>"); yield break; } if (string.IsNullOrWhiteSpace(_pendingUploadedScreenshotUrl)) { string uploadedFileUrl = null; yield return UploadMatchScreenshotFile(delegate(string url) { uploadedFileUrl = url; }); if (string.IsNullOrWhiteSpace(uploadedFileUrl)) { Log("<color=yellow>[Match Screenshot] UploadFile did not return a file URL (" + source + ") - keeping screenshot for retry</color>"); yield break; } _pendingUploadedScreenshotUrl = uploadedFileUrl; } JObject payload = new JObject { ["screenshot_url"] = JToken.op_Implicit(_pendingUploadedScreenshotUrl) }; bool uploadSucceeded = false; yield return CallAPI("/match?id=eq." + _currentMatchId, "PATCH", ((JToken)payload).ToString((Formatting)0, Array.Empty<JsonConverter>()), delegate(string res) { JObject val = ParseApiSingleObject(res); uploadSucceeded = val != null; }); if (uploadSucceeded) { _matchScreenshotUploadCompleted = true; ClearPendingMatchScreenshot(); Log("<color=green>[Match Screenshot] ✓ Attached scoreboard screenshot to Match " + _currentMatchId + "</color>"); ShowUploadNotification("Attached end-of-match screenshot for review."); } else { Log("<color=yellow>[Match Screenshot] Screenshot upload did not succeed yet (" + source + ") - will keep the screenshot for fallback retry</color>"); } } finally { _matchScreenshotUploadInProgress = false; } } private IEnumerator UploadMatchScreenshotFile(Action<string> onUploaded) { try { int num = default(int); switch (num) { default: yield break; case 0: onUploaded?.Invoke(null); yield break; case 1: { UnityWebRequest req = default(UnityWebRequest); if ((int)req.result == 1) { JObject response = ParseApiSingleObject(req.downloadHandler.text); string text = (string)((response != null) ? response["file_url"] : null); if (text == null) { text = (string)((response != null) ? response["url"] : null); } string fileUrl = text; if (!string.IsNullOrWhiteSpace(fileUrl)) { Log("<color=green>[Match Screenshot] ✓ Screenshot uploaded to file storage</color>"); onUploaded?.Invoke(fileUrl); } else { Log("<color=yellow>[Match Screenshot] UploadFile succeeded but response did not contain file_url</color>"); onUploaded?.Invoke(null); } } else { string errorMsg = $"[Match Screenshot] UploadFile failed: {req.result}"; if (!string.IsNullOrEmpty(req.error)) { errorMsg = errorMsg + " - " + req.error; } if (req.responseCode > 0) { errorMsg += $" (HTTP {req.responseCode})"; } DownloadHandler downloadHandler = req.downloadHandler; if (!string.IsNullOrEmpty((downloadHandler != null) ? downloadHandler.text : null)) { int length = Math.Min(240, req.downloadHandler.text.Length); errorMsg = errorMsg + " - Response: " + req.downloadHandler.text.Substring(0, length); } Log("<color=red>" + errorMsg + "</color>"); onUploaded?.Invoke(null); } ((<UploadMatchScreenshotFile>d__153)/*Error near IL_0291: stateMachine*/).<>m__Finally1(); break; } } } catch { //try-fault ((IDisposable)/*Error near IL_02af: stateMachine*/).Dispose(); throw; } ((IDisposable)/*Error near IL_02b7: stateMachine*/).Dispose(); /*Error near IL_02c1: Unexpected return in MoveNext()*/; } private string BuildMatchScreenshotFileName() { string text = ((!string.IsNullOrWhiteSpace(_currentMatchId)) ? _currentMatchId : DateTime.UtcNow.ToString("yyyyMMddHHmmss")); return "match-" + text + ".jpg"; } private string GetCoreIntegrationEndpointUrl(string functionName) { string baseApiUrl = GetBaseApiUrl(); int num = baseApiUrl.LastIndexOf("/entities", StringComparison.OrdinalIgnoreCase); string text = ((num >= 0) ? baseApiUrl.Substring(0, num) : baseApiUrl); return text + "/integration-endpoints/Core/" + functionName; } private void ApplyIntegrationHeaders(UnityWebRequest req) { if (req != null) { string authToken = GetAuthToken(); if (!string.IsNullOrWhiteSpace(authToken)) { req.SetRequestHeader("apikey", authToken); req.SetRequestHeader("Authorization", "Bearer " + authToken); } } } private void ClearPendingMatchScreenshot() { _pendingMatchScreenshotBytes = null; _pendingMatchScreenshotFileName = null; _pendingUploadedScreenshotUrl = null; } public void OnCourseRpcInformEndingCourse() { try { if (_courseEndRpcCoroutine == null) { _courseEndRpcCoroutine = ((MonoBehaviour)this).StartCoroutine(HandleCourseEndRpcCoroutine()); } } catch (Exception ex) { Log("<color=yellow>[Match Signal] Error starting CourseEnd RPC handler: " + ex.Message + "</color>"); } } private IEnumerator HandleCourseEndRpcCoroutine() { try { Log("<color=cyan>[Match Signal] CourseManager client RPC InformEndingCourse received — preparing final upload</color>"); for (int i = 0; i < 8; i++) { try { if (TryCaptureFinalLeaderboardSnapshot("CourseManager.RpcInformEndingCourse")) { break; } } catch { } yield return (object)new WaitForSeconds(0.125f); } yield return CaptureMatchScreenshotForReview("CourseManager.RpcInformEndingCourse (forced)"); float waited = 0f; while (string.IsNullOrWhiteSpace(_currentMatchId) && waited < 6f) { yield return (object)new WaitForSeconds(0.25f); waited += 0.25f; } yield return UploadCapturedMatchScreenshotIfNeeded("CourseManager.RpcInformEndingCourse"); } finally { _courseEndRpcCoroutine = null; } } private float GetCurrentPlayerScreenshotUploadDelaySeconds() { if (_currentSession?.player_ids == null || string.IsNullOrWhiteSpace(_userProfile?.id)) { return 0f; } List<string> list = _currentSession.player_ids.Where((string id) => !string.IsNullOrWhiteSpace(id)).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string id) => id, StringComparer.Ordinal) .ToList(); if (list.Count == 0) { return 0f; } int num = list.FindIndex((string id) => string.Equals(id, _userProfile.id, StringComparison.Ordinal)); if (num <= 0) { return 0f; } float num2 = (float)num * 0.5f; Log($"<color=cyan>[Match Screenshot] Upload slot {num + 1}/{list.Count}; waiting {num2:0.#}s before checking Match.screenshot_url</color>"); return num2; } private Texture2D ResizeTextureToFit(Texture2D source, int maxWidth, int maxHeight) { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Expected O, but got Unknown //IL_00da: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)source == (Object)null || ((Texture)source).width <= 0 || ((Texture)source).height <= 0) { return source; } if (((Texture)source).width <= maxWidth && ((Texture)source).height <= maxHeight) { return source; } float num = Mathf.Min((float)maxWidth / (float)((Texture)source).width, (float)maxHeight / (float)((Texture)source).height); int num2 = Mathf.Max(1, Mathf.RoundToInt((float)((Texture)source).width * num)); int num3 = Mathf.Max(1, Mathf.RoundToInt((float)((Texture)source).height * num)); RenderTexture active = RenderTexture.active; RenderTexture temporary = RenderTexture.GetTemporary(num2, num3, 0, (RenderTextureFormat)0); try { Graphics.Blit((Texture)(object)source, temporary); RenderTexture.active = temporary; Texture2D val = new Texture2D(num2, num3, (TextureFormat)3, false); val.ReadPixels(new Rect(0f, 0f, (float)num2, (float)num3), 0, 0, false); val.Apply(false, false); return val; } finally { RenderTexture.active = active; RenderTexture.ReleaseTemporary(temporary); } } private byte[] EncodeScreenshotBytes(Texture2D texture) { if ((Object)(object)texture == (Object)null) { return null; } byte[] array = null; int[] array2 = new int[5] { 90, 80, 70, 60, 50 }; int[] array3 = array2; foreach (int num in array3) { byte[] array4 = ImageConversion.EncodeToJPG(texture, num); if (array4 != null && array4.Length != 0) { array = array4; if (array4.Length <= 819200) { return array4; } } } if (array != null && array.Length <= 1048576) { return array; } if (array != null) { Log($"<color=yellow>[Match Screenshot] Best JPEG was still too large ({(float)array.Length / 1024f:0.#} KB) - skipping screenshot upload</color>"); } return null; } private IEnumerator CheckAndSubmitMatchStats() { if (_userProfile == null || _currentSession == null) { Log("<color=red>[Match Stats] Failed: Missing profile or session</color>"); yield break; } Log("<color=cyan>[Match Stats] Waiting for leaderboard to populate...</color>"); yield return (object)new WaitForSeconds(1.5f); string fullUrl = GetBaseApiUrl() + "/match?matchmaking_session_id=eq." + _currentSession.id; Log("<color=cyan>[Match Stats] Checking for existing match entry...</color>"); Log("<color=cyan>[Match Stats] Query URL: " + fullUrl.Substring(0, Math.Min(150, fullUrl.Length)) + "...</color>"); UnityWebRequest req = UnityWebRequest.Get(fullUrl); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { List<JObject> existingMatches = ParseApiObjectList(req.downloadHandler.text); if (existingMatches.Count > 0) { Log($"<color=orange>[Match Stats] Match already exists for this session (found {existingMatches.Count} entries)</color>"); _matchStatsSubmitted = true; yield break; } Log("<color=cyan>[Match Stats] No existing match found - proceeding with submission</color>"); } else { Log($"<color=yellow>[Match Stats] Could not query existing matches: {req.result} - proceeding anyway</color>"); } } finally { ((IDisposable)req)?.Dispose(); } yield return SubmitMatchStats(); } private IEnumerator ResolveExistingMatchIdForCurrentSession(Action<string> onResolved, bool logMisses = true) { string resolvedId = null; string activeSessionId = ((_currentSession != null) ? _currentSession.id : _localManualSessionId); if (string.IsNullOrWhiteSpace(activeSessionId)) { onResolved?.Invoke(null); yield break; } if (!string.IsNullOrWhiteSpace(_currentSession?.match_id)) { resolvedId = _currentSession.match_id; if (!_finalizedMatchIds.Contains(resolvedId)) { Log("<color=cyan>[Match Creation] Session already linked to Match ID " + resolvedId + "</color>"); onResolved?.Invoke(resolvedId); yield break; } Log("<color=yellow>[Match Creation] Session match_id " + resolvedId + " was already finalized — clearing link and forcing new match</color>"); _currentSession.match_id = null; resolvedId = null; } string fullUrl = GetBaseApiUrl() + "/match?matchmaking_session_id=eq." + activeSessionId + "&order=created_at.asc"; UnityWebRequest req = UnityWebRequest.Get(fullUrl); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { JObject existingMatch = SelectNewestReusableMatch(ParseApiObjectList(req.downloadHandler.text)); resolvedId = (string)((existingMatch != null) ? existingMatch["id"] : null); if (!string.IsNullOrWhiteSpace(resolvedId)) { if (_currentSession != null) { _currentSession.match_id = resolvedId; } Log("<color=green>[Match Creation] ✓ Reusing newest active Match ID from API for session " + activeSessionId + ": " + resolvedId + "</color>"); } else if (logMisses) { Log("<color=cyan>[Match Creation] No existing Match found yet for session " + activeSessionId + "</color>"); } } else { Log($"<color=yellow>[Match Creation] Existing Match lookup failed: {req.result} - {req.error}</color>"); } } finally { ((IDisposable)req)?.Dispose(); } onResolved?.Invoke(resolvedId); } private JObject SelectNewestReusableMatch(IEnumerable<JObject> matches) { return (from match in matches?.Where((JObject match) => match != null).Where(delegate(JObject match) { string text = (string)match["id"]; return !string.IsNullOrWhiteSpace(text) && !_finalizedMatchIds.Contains(text); }) orderby ParseApiTimestamp((string)match["created_at"]) descending select match).FirstOrDefault(); } private static DateTime ParseApiTimestamp(string value) { if (DateTime.TryParse(value, null, DateTimeStyles.RoundtripKind, out var result)) { return result; } return DateTime.MinValue; } private float GetCurrentPlayerMatchUploadDelaySeconds() { if (_currentSession?.player_ids == null || string.IsNullOrWhiteSpace(_userProfile?.id)) { return 0f; } List<string> list = _currentSession.player_ids.Where((string id) => !string.IsNullOrWhiteSpace(id)).Distinct<string>(StringComparer.Ordinal).OrderBy<string, string>((string id) => id, StringComparer.Ordinal) .ToList(); if (list.Count == 0) { return 0f; } int num = list.FindIndex((string id) => string.Equals(id, _userProfile.id, StringComparison.Ordinal)); if (num <= 0) { return 0f; } float num2 = (float)num * 2f; Log($"<color=cyan>[Match Creation] Fallback upload slot {num + 1}/{list.Count}; waiting {num2:0.#}s before POST</color>"); return num2; } private IEnumerator WaitForExistingMatchBeforeFallback(float delaySeconds, Action<string> onResolved) { string activeSessionId = _currentSession?.id ?? _localManualSessionId; if (string.IsNullOrWhiteSpace(activeSessionId) || delaySeconds <= 0f) { onResolved?.Invoke(null); yield break; } for (float waited = 0f; waited < delaySeconds; waited += 1f) { string existingMatchId = null; yield return ResolveExistingMatchIdForCurrentSession(delegate(string id) { existingMatchId = id; }, logMisses: false); if (!string.IsNullOrEmpty(existingMatchId)) { onResolved?.Invoke(existingMatchId); yield break; } yield return (object)new WaitForSeconds(1f); } onResolved?.Invoke(null); } private string BuildStableManualLocalSessionId() { string currentSteamLobbyId = CompetitivePluginCheck.GetCurrentSteamLobbyId(); if (!string.IsNullOrWhiteSpace(currentSteamLobbyId)) { return "local-lobby-" + currentSteamLobbyId; } string text = ResolveCurrentLobbyName(); if (!string.IsNullOrWhiteSpace(text)) { string text2 = Regex.Replace(text.Trim(), "[^A-Za-z0-9_-]+", "-").Trim('-'); if (!string.IsNullOrWhiteSpace(text2)) { return "local-name-" + text2; } } return "local-manual"; } private IEnumerator ResolveExistingMatchEntryId(string matchId, string playerId, Action<string> onResolved, bool logMisses = false) { string resolvedId = null; if (string.IsNullOrWhiteSpace(matchId) || string.IsNullOrWhiteSpace(playerId)) { onResolved?.Invoke(null); yield break; } yield return CallAPI("/match_entry?match_id=eq." + matchId + "&player_id=eq." + playerId + "&limit=1&order=created_at.asc", "GET", "", delegate(string res) { JObject val = ParseApiObjectList(res).FirstOrDefault(); if (val != null) { resolvedId = (string)val["id"]; } }); if (logMisses && string.IsNullOrWhiteSpace(resolvedId)) { Log("<color=cyan>[Match Entry] No existing MatchEntry found yet for player " + playerId + "</color>"); } onResolved?.Invoke(resolvedId); } private IEnumerator WaitForPendingMatchEntryCreation(string playerId, Action<string> onResolved) { float waited = 0f; while (!string.IsNullOrWhiteSpace(playerId) && _matchEntryCreationInProgress.Contains(playerId) && waited < 3f) { if (_playerMatchEntryIds.TryGetValue(playerId, out var entryId) && !string.IsNullOrWhiteSpace(entryId)) { onResolved?.Invoke(entryId); yield break; } yield return (object)new WaitForSeconds(0.25f); waited += 0.25f; entryId = null; } if (!string.IsNullOrWhiteSpace(playerId) && _playerMatchEntryIds.TryGetValue(playerId, out var resolvedEntryId) && !string.IsNullOrWhiteSpace(resolvedEntryId)) { onResolved?.Invoke(resolvedEntryId); } else { onResolved?.Invoke(null); } } private IEnumerator EnsureMatchEntryForPlayer(string sourceTag, string playerId, string playerName, string preMatchMmr, string postMatchMmr, int gamePoints, int scoreVsPar, int finishPosition, string notes, Action<string> onResolved) { if (string.IsNullOrWhiteSpace(_currentMatchId) || string.IsNullOrWhiteSpace(playerId)) { onResolved?.Invoke(null); yield break; } if (_playerMatchEntryIds.TryGetValue(playerId, out var cachedEntryId) && !string.IsNullOrWhiteSpace(cachedEntryId)) { onResolved?.Invoke(cachedEntryId); yield break; } if (_matchEntryCreationInProgress.Contains(playerId)) { yield return WaitForPendingMatchEntryCreation(playerId, onResolved); yield break; } _matchEntryCreationInProgress.Add(playerId); try { string existingEntryId = null; yield return ResolveExistingMatchEntryId(_currentMatchId, playerId, delegate(string id) { existingEntryId = id; }); string resolvedPlayerName = (string.IsNullOrWhiteSpace(playerName) ? playerId : playerName.Trim()); if (!string.IsNullOrWhiteSpace(existingEntryId)) { _playerMatchEntryIds[playerId] = existingEntryId; if (!string.IsNullOrWhiteSpace(resolvedPlayerName)) { _playerIdsByName[resolvedPlayerName] = playerId; } Log("<color=green>[" + sourceTag + "] ✓ Adopted existing MatchEntry for " + resolvedPlayerName + ": " + existingEntryId + "</color>"); onResolved?.Invoke(existingEntryId); yield break; } int adjustedScore = gamePoints + scoreVsPar * -10; if (gamePoints == 0) { Log("<color=yellow>[" + sourceTag + "] Skipping MatchEntry POST for " + resolvedPlayerName + " — score is 0</color>"); onResolved?.Invoke(null); yield break; } JObject payload = new JObject { ["id"] = JToken.op_Implicit(Guid.NewGuid().ToString()), ["match_id"] = JToken.op_Implicit(_currentMatchId), ["player_id"] = JToken.op_Implicit(playerId), ["player_name"] = JToken.op_Implicit(resolvedPlayerName), ["game_points"] = JToken.op_Implicit(gamePoints), ["over_under"] = JToken.op_Implicit(scoreVsPar), ["score_vs_par"] = JToken.op_Implicit(scoreVsPar), ["adjusted_match_score"] = JToken.op_Implicit(adjustedScore), ["notes"] = JToken.op_Implicit(notes ?? string.Empty) }; float preMmr = 0f; float postMmr = 0f; if (!string.IsNullOrWhiteSpace(preMatchMmr) && float.TryParse(preMatchMmr, NumberStyles.Float, CultureInfo.InvariantCulture, out preMmr)) { payload["pre_match_mmr"] = JToken.op_Implicit(preMmr); } if (!string.IsNullOrWhiteSpace(postMatchMmr) && float.TryParse(postMatchMmr, NumberStyles.Float, CultureInfo.InvariantCulture, out postMmr)) { payload["post_match_mmr"] = JToken.op_Implicit(postMmr); payload["mmr_change"] = JToken.op_Implicit(postMmr - preMmr); } if (finishPosition > 0) { payload["finish_position"] = JToken.op_Implicit(finishPosition); } string createdEntryId = null; yield return CallAPI("/match_entry", "POST", ((JToken)payload).ToString((Formatting)0, Array.Empty<JsonConverter>()), delegate(string res) { try { JObject val = ParseApiSingleObject(res); if (val != null) { createdEntryId = (string)val["id"]; } } catch (Exception ex) { Log("<color=yellow>[" + sourceTag + "] Could not parse MatchEntry response: " + ex.Message + "</color>"); } }); if (!string.IsNullOrWhiteSpace(createdEntryId)) { _playerMatchEntryIds[playerId] = createdEntryId; if (!string.IsNullOrWhiteSpace(resolvedPlayerName)) { _playerIdsByName[resolvedPlayerName] = playerId; } Log("<color=green>[" + sourceTag + "] ✓ MatchEntry created for " + resolvedPlayerName + ": " + createdEntryId + "</color>"); } onResolved?.Invoke(createdEntryId); } finally { _matchEntryCreationInProgress.Remove(playerId); } } private IEnumerator FinalizeAndResetAfterDrivingRange() { if (!_matchStatsSubmitted && !string.IsNullOrEmpty(_currentMatchId)) { yield return FinalizeMatchStats(); } yield return UploadCapturedMatchScreenshotIfNeeded("Driving Range fallback"); if (IsRankedTriggered && _currentSession != null) { yield return UpdateSessionStatus("completed"); } if (!string.IsNullOrWhiteSpace(_currentMatchId)) { _finalizedMatchIds.Add(_currentMatchId); } ResetPerMatchState(); } private void ResetPerMatchState() { if (_endOfMatchSignalCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_endOfMatchSignalCoroutine); _endOfMatchSignalCoroutine = null; } if (_matchScreenshotSignalCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_matchScreenshotSignalCoroutine); _matchScreenshotSignalCoroutine = null; } _currentMatchId = null; if (_currentSession != null) { _currentSession.match_id = null; } _playerMatchEntryIds.Clear(); _matchEntryCreationInProgress.Clear(); _playerIdsByName.Clear(); _lastSubmittedScores.Clear(); _lastSubmittedScoresVsPar.Clear(); _matchEntriesCreated = false; _matchStatsSubmitted = false; _proSeriesSkipLogged = false; _matchEndedReceived = false; ClearPendingMatchScreenshot(); _matchScreenshotUploadInProgress = false; _matchScreenshotUploadCompleted = false; _localManualSessionId = null; _cachedLeaderboardScores.Clear(); _cachedLeaderboardScoresVsPar.Clear(); _finalLeaderboardSnapshot.Clear(); MatchResultSubmissionService.ReceivedP2PMatchId = null; _lastUploadedPlayerCount = -1; Log("<color=cyan>[Match] Per-match state reset - ready for new round</color>"); } private void ResetPluginState() { if (_monitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_monitorCoroutine); _monitorCoroutine = null; } if (_endOfMatchSignalCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_endOfMatchSignalCoroutine); _endOfMatchSignalCoroutine = null; } if (_matchScreenshotSignalCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_matchScreenshotSignalCoroutine); _matchScreenshotSignalCoroutine = null; } IsRankedTriggered = false; _isQueueing = false; _webStatus = "IDLE"; _currentQueueId = ""; _currentSession = null; _isHost = false; _hasAccepted = false; _hostRulesetSelection = "ranked"; _queueStartTime = null; _hostLobbyStarted = false; _hostServerWasActive = false; _hostCancelSent = false; _matchStatsSubmitted = false; _proSeriesSkipLogged = false; _matchStartTime = null; _syncTickCount = 0; _lobbyCreatedEventCount = 0; _steamLinkUploadAttempts = 0; _steamLinkUploadSuccesses = 0; _steamLinkUploadFailures = 0; _autoJoinAttempts = 0; _autoJoinSuccesses = 0; _autoJoinFailures = 0; _lastLobbyCreatedAt = null; _lastUploadAttemptAt = null; _lastUploadSuccessAt = null; _lastAutoJoinAttemptAt = null; _lastAutoJoinSuccessAt = null; _lastGeneratedSteamLink = "-"; _lastUploadedSteamLink = "-"; _lastAutoJoinSteamLink = "-"; _lastUploadError = "-"; _lastAutoJoinError = "-"; if (_lobbyMonitorCoroutine != null) { ((MonoBehaviour)this).StopCoroutine(_lobbyMonitorCoroutine); _lobbyMonitorCoroutine = null; } _currentMatchId = null; _finalizedMatchIds.Clear(); _playerMatchEntryIds.Clear(); _matchEntryCreationInProgress.Clear(); _playerIdsByName.Clear(); _lastSubmittedScores.Clear(); _lastSubmittedScoresVsPar.Clear(); _matchCreationInProgress = false; _matchEntriesCreated = false; _isInGameplay = false; _matchEndedReceived = false; ClearPendingMatchScreenshot(); _matchScreenshotUploadInProgress = false; _matchScreenshotUploadCompleted = false; _localManualSessionId = null; _cachedLeaderboardScores.Clear(); _cachedLeaderboardScoresVsPar.Clear(); _finalLeaderboardSnapshot.Clear(); _lastUploadedPlayerCount = -1; _nextEnsureMatchCreateAttemptAt = 0f; } private void OnDestroy() { SceneManager.sceneLoaded -= OnSceneLoaded; CourseManager.MatchStateChanged -= OnCourseManagerMatchStateChanged; CourseManager.ForceDisplayScoreboardChanged -= OnCourseManagerForceDisplayScoreboardChanged; UnifiedPlugin.ApiConfigChanged -= OnApiConfigChanged; } private void OnApplicationQuit() { if (_isHost && _hostLobbyStarted && _hostServerWasActive && _currentSession != null) { ((MonoBehaviour)this).StartCoroutine(CancelSessionAsHost("host_application_quit")); } } private void Log(string msg) { string item = $"[{DateTime.Now:HH:mm:ss}] {msg}"; _debugLogs.Add(item); if (_debugLogs.Count > 30) { _debugLogs.RemoveAt(0); } _logScroll.y = float.MaxValue; if (_bepinexLogger != null) { string text = Regex.Replace(msg, "<[^>]+>", ""); _bepinexLogger.LogInfo((object)("[MatchmakingAssistant] " + text)); } } private bool TryUseSharedProfile(string logContext) { SharedPlayerProfile playerProfile = UnifiedPlugin.GetPlayerProfile(); if (playerProfile == null || !UnifiedPlugin.IsPlayerProfileResolved() || string.IsNullOrWhiteSpace(playerProfile.ID)) { return false; } _userProfile = new PlayerProfile { id = playerProfile.ID, display_name = playerProfile.DisplayName, current_mmr = playerProfile.CurrentMMR, region = playerProfile.Region, state_province = string.Empty }; Log("<color=green>[" + logContext + "] ✓ Using shared profile: " + _userProfile.display_name + " (" + _userProfile.id + ")</color>"); return true; } private string TryGetSteamPlayerName() { try { if (SteamClient.IsValid) { return SteamClient.Name; } } catch { } return string.Empty; } private IEnumerator EnsureUserProfileResolved(string logContext, float maxWaitSeconds = 0f) { if (_userProfile != null) { yield break; } bool attemptedSteamLookup = false; float waited = 0f; while (_userProfile == null && !TryUseSharedProfile(logContext)) { if (!attemptedSteamLookup) { string steamName = TryGetSteamPlayerName(); if (!string.IsNullOrWhiteSpace(steamName) && steamName != "Player") { attemptedSteamLookup = true; Log("<color=cyan>[" + logContext + "] Attempting local profile resolution for '" + steamName + "'</color>"); yield return ResolveProfile(steamName); if (_userProfile != null || TryUseSharedProfile(logContext)) { break; } } } if (waited >= maxWaitSeconds) { break; } yield return (object)new WaitForSeconds(0.5f); waited += 0.5f; } } private IEnumerator BackgroundSyncLoop() { int retryCount = 0; while (true) { _syncTickCount++; if (_userProfile == null) { yield return EnsureUserProfileResolved("Sync"); if (_userProfile != null) { retryCount = 0; } else { retryCount++; if (retryCount == 1) { Log($"<color=yellow>Waiting for player profile resolution... (Attempt {retryCount}/{10})</color>"); } if (retryCount % 3 == 0) { Log($"<color=yellow>Still waiting for player profile... (Attempt {retryCount}/{10})</color>"); } if (retryCount >= 10) { break; } } } if (_userProfile != null) { if (!_activeSeasonFetched) { yield return FetchActiveSeasonId(); } Scene activeScene = SceneManager.GetActiveScene(); bool isInMenuScene = ((Scene)(ref activeScene)).name.ToLower().Contains("menu"); if (!_isQueueing && _currentSession == null && !_isInGameplay) { yield return CheckExistingQueueEntry(); } if (isInMenuScene && !IsRankedTriggered && !_isInGameplay) { yield return RefreshPlayerList(); } if (_isInitializing) { _isInitializing = false; Log("Initialization Complete: Queue Unlocked."); } if (_isQueueing && _currentSession == null) { yield return CheckForMatch(); } if (_currentSession != null && _currentSession.status == "pending_accept") { yield return PollSessionStatus(); } if (_isHost && _hostLobbyStarted && _currentSession != null && !_hostCancelSent) { if (NetworkServer.active) { _hostServerWasActive = true; } else if (_hostServerWasActive && _currentSession.status != "completed") { Log("Host lobby appears closed. Marking MatchmakingSession as completed..."); yield return CancelSessionAsHost("host_left_lobby"); } } if (_currentSession != null && !_isHost && _currentSession.status == "ready" && !string.IsNullOrEmpty(_currentSession.steam_lobby_link) && !_hasAccepted) { Log("Auto-joining match as non-host..."); yield return AutoJoinMatch(); } } yield return _syncLoopDelay; } Log("<color=orange>Player profile resolution timed out. Background sync will stop until the next reload.</color>"); } private IEnumerator RefreshPlayerList() { string fullUrl = GetBaseApiUrl() + "/matchmaking_queue?status=in.(queued,matched)"; Log("<color=cyan>[Sync] GET /matchmaking_queue (fetching queued players)</color>"); Log("<color=cyan>[Sync] Full URL: " + fullUrl.Substring(0, Math.Min(150, fullUrl.Length)) + "...</color>"); UnityWebRequest req = UnityWebRequest.Get(fullUrl); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { string rawJson = req.downloadHandler.text; List<JObject> queueEntries = ParseApiObjectList(rawJson); List<PlayerData> queuedPlayers = new List<PlayerData>(queueEntries.Count); JObject myEntry = null; string myStatus = null; int countQueued = 0; int countMatched = 0; foreach (JObject entry in queueEntries) { string status = (string)entry["status"]; if (string.Equals(status, "queued", StringComparison.OrdinalIgnoreCase)) { countQueued++; queuedPlayers.Add(new PlayerData { name = (((string)entry["display_name"]) ?? ((string)entry["user_id"]) ?? "Unknown"), mmr = (((object)entry["mmr_snapshot"])?.ToString() ?? "0") }); } else if (string.Equals(status, "matched", StringComparison.OrdinalIgnoreCase)) { countMatched++; } if (_userProfile != null && myEntry == null && string.Equals((string)entry["player_id"], _userProfile.id, StringComparison.Ordinal)) { myEntry = entry; myStatus = status; } } _queuedPlayers = queuedPlayers; _onlineCount = countQueued + countMatched; _queuedCount = countQueued; _matchedCount = countMatched; if (_userProfile == null || myEntry == null) { yield break; } string status2 = myStatus; if (status2 == "queued" && !_isQueueing) { _isQueueing = true; _currentQueueId = ((string)myEntry["id"]) ?? _currentQueueId; _webStatus = "QUEUED (SYNCED)"; string serverTime = (string)myEntry["queued_at"]; if (DateTime.TryParse(serverTime, null, DateTimeStyles.RoundtripKind, out var parsedTime)) { _queueStartTime = parsedTime.ToUniversalTime(); } Log("Sync: Found active session on server. Reconnecting UI..."); } else if (status2 == "matched" && _currentSession == null) { _webStatus = "MATCH FOUND (SYNCED)"; yield return CheckForMatch(); } yield break; } Log($"<color=red>[Sync] RefreshPlayerList failed: {req.result} - {req.error}</color>"); } finally { ((IDisposable)req)?.Dispose(); } } private IEnumerator CheckExistingQueueEntry() { if (_userProfile == null) { yield break; } string checkUrl = GetBaseApiUrl() + "/matchmaking_queue?player_id=eq." + _userProfile.id + "&status=eq.queued"; UnityWebRequest req = UnityWebRequest.Get(checkUrl); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result != 1) { yield break; } List<JObject> existing = ParseApiObjectList(req.downloadHandler.text); if (existing == null || existing.Count <= 0) { yield break; } _currentQueueId = (string)existing[0]["id"]; _isQueueing = true; _webStatus = "QUEUED"; _queueStartTime = DateTime.Now; Log("<color=yellow>[Queue] Detected existing queue entry " + _currentQueueId + " from website — rejoining.</color>"); yield return CallAPI(json: "{\"has_mod\":true,\"created_by\":\"SBGL_UnifiedMod\",\"match_type\":\"" + GetQueueMatchTypePayload() + "\"}", endpoint: "/matchmaking_queue?id=eq." + _currentQueueId, method: "PATCH", onSuccess: delegate(string res) { if (ParseApiSingleObject(res) != null) { Log("<color=green>[Queue] ✓ Patched has_mod:true onto existing entry " + _currentQueueId + ".</color>"); } }); } finally { ((IDisposable)req)?.Dispose(); } } private IEnumerator MatchmakingLoop() { if (_userProfile == null) { Log("<color=red>No profile loaded.</color>"); yield break; } _isQueueing = true; _webStatus = "JOINING..."; _queueStartTime = DateTime.Now; string existingId = null; string checkUrl = GetBaseApiUrl() + "/matchmaking_queue?player_id=eq." + _userProfile.id + "&status=eq.queued"; Log("<color=cyan>[Queue] Checking for existing entry for player " + _userProfile.id + "...</color>"); UnityWebRequest checkReq = UnityWebRequest.Get(checkUrl); try { ApplyApiHeaders(checkReq); yield return checkReq.SendWebRequest(); if ((int)checkReq.result == 1) { List<JObject> existing = ParseApiObjectList(checkReq.downloadHandler.text); if (existing != null && existing.Count > 0) { existingId = (string)existing[0]["id"]; string foundUserId = (string)existing[0]["user_id"]; if (!string.IsNullOrEmpty(foundUserId)) { PlayerPrefs.SetString("SBGLUserAuthUUID", foundUserId); PlayerPrefs.Save(); } Log("<color=yellow>[Queue] Found existing entry " + existingId + " — patching has_mod:true</color>"); } else { Log("<color=cyan>[Queue] No existing entry found — will create new.</color>"); } } else { Log($"<color=orange>[Queue] Could not check existing entries: {checkReq.result} — will attempt POST anyway.</color>"); } } finally { ((IDisposable)checkReq)?.Dispose(); } if (existingId != null) { yield return CallAPI(json: "{\"has_mod\":true,\"created_by\":\"SBGL_UnifiedMod\",\"match_type\":\"" + GetQueueMatchTypePayload() + "\"," + $"\"queued_at\":\"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\"" + "}", endpoint: "/matchmaking_queue?id=eq." + existingId, method: "PATCH", onSuccess: delegate(string res) { JObject val = ParseApiSingleObject(res); if (val != null) { _currentQueueId = existingId; _webStatus = "QUEUED"; Log("<color=green>[Queue] ✓ Updated existing entry " + existingId + " with has_mod:true.</color>"); } else { Log("<color=red>[Queue] Failed to update existing entry " + existingId + ".</color>"); } }); yield break; } string json = "{" + $"\"id\":\"{Guid.NewGuid()}\"," + "\"user_id\":\"" + GetOrDeriveUserUUID() + "\",\"player_id\":\"" + _userProfile.id + "\"," + $"\"mmr_snapshot\":{_userProfile.current_mmr}," + "\"region\":\"" + _userProfile.region + "\",\"state_province\":\"" + _userProfile.state_province + "\",\"match_type\":\"" + GetQueueMatchTypePayload() + "\",\"session_id\":\"\",\"has_mod\":true,\"created_by\":\"SBGL_UnifiedMod\",\"status\":\"queued\"," + $"\"queued_at\":\"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ss.fffZ}\"" + "}"; yield return CallAPI("/matchmaking_queue", "POST", json, delegate(string res) { JObject val = ParseApiSingleObject(res); _currentQueueId = ((string)((val != null) ? val["id"] : null)) ?? _currentQueueId; _webStatus = "QUEUED"; Log("<color=green>[Queue] ✓ Created new entry " + _currentQueueId + " with has_mod:true.</color>"); }); } private IEnumerator LeaveQueue() { if (string.IsNullOrEmpty(_currentQueueId)) { ResetPluginState(); yield break; } _webStatus = "LEAVING..."; string json = "{\"player_id\":\"" + _userProfile.id + "\",\"status\":\"cancelled\"}"; bool leaveSuccess = false; yield return CallAPI("/matchmaking_queue?id=eq." + _currentQueueId, "PATCH", json, delegate(string res) { if (ParseApiSingleObject(res) != null) { ResetPluginState(); Log("✓ Left Queue."); leaveSuccess = true; } }); if (!leaveSuccess) { Log("<color=orange>Leave queue request failed. Check logs above for details.</color>"); ResetPluginState(); } } private IEnumerator CheckForMatch() { Log("<color=cyan>[Sync] GET /matchmaking_session (checking for pending matches)</color>"); JObject activeSession = null; string urlHost = GetBaseApiUrl() + "/matchmaking_session?host_player_id=eq." + _userProfile.id + "&status=eq.pending_accept"; Log("<color=cyan>[Sync] Full URL: " + urlHost.Substring(0, Math.Min(150, urlHost.Length)) + "...</color>"); UnityWebRequest req = UnityWebRequest.Get(urlHost); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { List<JObject> sessions = ParseApiObjectList(req.downloadHandler.text); activeSession = ((IEnumerable<JObject>)sessions).FirstOrDefault((Func<JObject, bool>)((JObject s) => !string.IsNullOrEmpty((string)s["lobby_name"]))); } else { SBGLPlugin sBGLPlugin = this; object arg = req.result; string error = req.error; DownloadHandler downloadHandler = req.downloadHandler; sBGLPlugin.Log($"<color=red>[Sync] CheckForMatch (host) failed: {arg} - {error} | body: {((downloadHandler != null) ? downloadHandler.text : null)}</color>"); } } finally { ((IDisposable)req)?.Dispose(); } if (activeSession == null) { string urlPlayer = string.Concat(str2: "%5B%22" + _userProfile.id + "%22%5D", str0: GetBaseApiUrl(), str1: "/matchmaking_session?player_ids=cs.", str3: "&status=eq.pending_accept"); Log("<color=cyan>[Sync] Full URL: " + urlPlayer.Substring(0, Math.Min(150, urlPlayer.Length)) + "...</color>"); UnityWebRequest req2 = UnityWebRequest.Get(urlPlayer); try { ApplyApiHeaders(req2); yield return req2.SendWebRequest(); if ((int)req2.result == 1) { List<JObject> sessions2 = ParseApiObjectList(req2.downloadHandler.text); activeSession = ((IEnumerable<JObject>)sessions2).FirstOrDefault((Func<JObject, bool>)((JObject s) => !string.IsNullOrEmpty((string)s["lobby_name"]))); } else { SBGLPlugin sBGLPlugin2 = this; object arg2 = req2.result; string error2 = req2.error; DownloadHandler downloadHandler2 = req2.downloadHandler; sBGLPlugin2.Log($"<color=red>[Sync] CheckForMatch (player) failed: {arg2} - {error2} | body: {((downloadHandler2 != null) ? downloadHandler2.text : null)}</color>"); } } finally { ((IDisposable)req2)?.Dispose(); } } if (activeSession != null) { _currentSession = ParseSessionFromJson(activeSession); _isHost = string.Equals(_currentSession?.host_player_id, _userProfile?.id, StringComparison.Ordinal); _webStatus = "MATCH FOUND: PENDING"; Log($"<color=yellow>[CheckForMatch] Session {_currentSession?.id} found. Host: {_isHost}</color>"); } } private IEnumerator PollSessionStatus() { string fullUrl = GetBaseApiUrl() + "/matchmaking_session?id=eq." + _currentSession.id + "&select=*"; Log("<color=cyan>[Sync] GET /matchmaking_session/" + _currentSession.id + " (polling status)</color>"); Log("<color=cyan>[Sync] Full URL: " + fullUrl + "</color>"); UnityWebRequest req = UnityWebRequest.Get(fullUrl); try { ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { string raw = req.downloadHandler.text; JObject session = ParseApiSingleObject(raw); if (session == null) { yield break; } string statusFromApi = (string)session["status"]; _currentSession.status = statusFromApi ?? _currentSession.status; Log("<color=cyan>[Sync] Status from API: " + statusFromApi + "</color>"); string acceptedIdsRaw = ((object)session["accepted_player_ids"])?.ToString(); if (!string.IsNullOrEmpty(acceptedIdsRaw)) { List<string> acceptedIds = ParseTextJsonArray(acceptedIdsRaw); _currentSession.accepted_player_ids = acceptedIds; Log($"<color=cyan>[Sync] Updated accepted_player_ids: {acceptedIds.Count} players have accepted</color>"); if (_currentSession.status == "pending_accept" && _currentSession.player_ids != null && _currentSession.accepted_player_ids.Count == _currentSession.player_ids.Count) { Log($"<color=green>[Sync] All {_currentSession.player_ids.Count} players have accepted! Transitioning to ready...</color>"); _currentSession.status = "ready"; _webStatus = (_isHost ? "READY: HOST" : "READY: JOIN"); string readyJson = "{\"status\":\"ready\"}"; yield return CallAPI("/matchmaking_session?id=eq." + _currentSession.id, "PATCH", readyJson, delegate { Log("<color=green>[Sync] ✓ Session transitioned to READY</color>"); }); } } string latestSteamLobbyLink = (string)session["steam_lobby_link"]; if (!string.IsNullOrEmpty(latestSteamLobbyLink) && latestSteamLobbyLink != _currentSession.steam_lobby_link) { _currentSession.steam_lobby_link = latestSteamLobbyLink; Log("Sync: steam_lobby_link received/updated from API."); } string latestMatchId = (string)session["match_id"]; if (!string.IsNullOrEmpty(latestMatchId) && latestMatchId != _currentSession.match_id) { _currentSession.match_id = latestMatchId; Log("Sync: match_id received/updated from API: " + latestMatchId); } if (_currentSession.status == "ready") { _webStatus = (_isHost ? "READY: HOST" : "READY: JOIN"); Log("<color=green>[Sync] Session is READY - Host should start game</color>"); } else if (_currentSession.status == "cancelled" || _currentSession.status == "completed") { Log("Match ended with status: " + _currentSession.status + "."); ResetPluginState(); } } else { Log($"<color=red>[Sync] PollSessionStatus failed: {req.result} - {req.error}</color>"); } } finally { ((IDisposable)req)?.Dispose(); } } private IEnumerator AcceptMatch() { if (_currentSession != null) { _hasAccepted = true; Log("<color=cyan>[Accept] Refreshing session data before accepting...</color>"); yield return PollSessionStatus(); List<string> acceptedIds = new List<string>(); if (_currentSession.accepted_player_ids != null) { acceptedIds.AddRange(_currentSession.accepted_player_ids); } if (!acceptedIds.Contains(_userProfile.id)) { acceptedIds.Add(_userProfile.id); } string innerArray = ((JToken)JArray.FromObject((object)acceptedIds)).ToString((Formatting)0, Array.Empty<JsonConverter>()); JObject acceptPayload = new JObject { ["accepted_player_ids"] = JToken.op_Implicit(innerArray) }; string acceptJson = ((JToken)acceptPayload).ToString((Formatting)0, Array.Empty<JsonConverter>()); Log("<color=cyan>[Accept] Current player accepting: " + _userProfile.id + "</color>"); Log($"<color=cyan>[Accept] Total accepted players: {acceptedIds.Count}</color>"); Log("<color=cyan>[Accept] JSON payload: " + acceptJson + "</color>"); _currentSession.accepted_player_ids = new List<string>(acceptedIds); yield return CallAPI("/matchmaking_session?id=eq." + _currentSession.id, "PATCH", acceptJson, delegate { Log("<color=green>✓ Match Accepted. Waiting for all players to accept before transitioning to ready...</color>"); }); } } private IEnumerator AutoJoinMatch() { if (_currentSession != null && !string.IsNullOrEmpty(_currentSession.steam_lobby_link)) { _autoJoinAttempts++; _lastAutoJoinAttemptAt = DateTime.Now; _lastAutoJoinSteamLink = _currentSession.steam_lobby_link; _hasAccepted = true; Log("Attempting automatic join with link: " + _currentSession.steam_lobby_link); JoinBySteamLink(_currentSession.steam_lobby_link, _currentSession.lobby_password); } yield break; } private IEnumerator TransitionToReady() { yield return _readyTransitionDelay; } private void StoreMatchConfigurationInPlayerPrefs(MatchmakingSession session) { try { if (session == null) { Log("<color=yellow>[Config] Session is null, cannot store configuration</color>"); return; } string matchType = session.match_type ?? string.Empty; bool flag = IsCasualMatchType(matchType); bool flag2 = IsProSeriesMatchType(matchType); string text = (flag ? "casual" : (flag2 ? "pro_series_season_2" : "ranked_season_2")); int num = ((!flag) ? ((session.season > 0) ? session.season : 2) : 0); string text2 = (flag ? "casual" : (flag2 ? "pro_series" : "ranked")); if (flag) { string text3 = ((!string.IsNullOrWhiteSpace(session.selected_course)) ? session.selected_course : PlayerPrefs.GetString("SelectedCourse", "")); PlayerPrefs.SetString("MatchType", text); if (!string.IsNullOrWhiteSpace(text3)) { PlayerPrefs.SetString("SelectedCourse", text3); } PlayerPrefs.SetInt("Season", num); PlayerPrefs.SetString("HostRuleset", text2); PlayerPrefs.Save(); Log(string.Format("<color=cyan>[Config] Stored casual match configuration: Type={0}, Course={1}, Season={2}</color>", text, string.IsNullOrWhiteSpace(text3) ? "manual" : text3, num)); return; } try { RuleSetManager.SetLogger(_bepinexLogger); string text4 = ((!string.IsNullOrEmpty(session.selected_course) && RuleSetManager.ValidateCourseForRanked(session.selected_course)) ? session.selected_course : MapPoolConfig.GetRandomApprovedCourse().Name); PlayerPrefs.SetString("MatchType", text); PlayerPrefs.SetString("SelectedCourse", text4); PlayerPrefs.SetInt("Season", num); PlayerPrefs.SetString("HostRuleset", text2); PlayerPrefs.Save(); Log($"<color=cyan>[Config] Stored match configuration: Type={text}, Course={text4}, Season={num}</color>"); RuleSetManager.LogMatchConfiguration(text, text4, num); } catch (Exception ex) { Log("<color=yellow>[Config] Error during rule validation, storing fallback config: " + ex.Message + "</color>"); PlayerPrefs.SetString("MatchType", text); PlayerPrefs.SetString("SelectedCourse", string.IsNullOrEmpty(session.selected_course) ? MapPoolConfig.GetRandomApprovedCourse().Name : session.selected_course); PlayerPrefs.SetInt("Season", num); PlayerPrefs.SetString("HostRuleset", text2); PlayerPrefs.Save(); } } catch (Exception ex2) { Log("<color=red>[Config] Exception storing match configuration: " + ex2.Message + " | StackTrace: " + ex2.StackTrace + "</color>"); } } private void ClearMatchConfigurationFromPlayerPrefs() { PlayerPrefs.DeleteKey("MatchType"); PlayerPrefs.DeleteKey("SelectedCourse"); PlayerPrefs.DeleteKey("Season"); PlayerPrefs.Save(); Log("[Config] Cleared match configuration from PlayerPrefs"); } private void InitiateHostSequence() { if (_currentSession == null) { return; } if (!_hasAccepted) { Log("<color=yellow>[Host] Must accept match first before initializing lobby</color>"); ((MonoBehaviour)this).StartCoroutine(AcceptMatch()); return; } string a = PlayerPrefs.GetString("HostRuleset", ""); if (string.Equals(a, "pro_series", StringComparison.OrdinalIgnoreCase)) { _hostRulesetSelection = "pro_series"; } else if (string.Equals(a, "casual", StringComparison.OrdinalIgnoreCase)) { _hostRulesetSelection = "casual"; } else { _hostRulesetSelection = "ranked"; } MainMenu val = Object.FindAnyObjectByType<MainMenu>(); if ((Object)(object)val != (Object)null) { IsRankedTriggered = true; _hostLobbyStarted = true; _hostServerWasActive = false; _hostCancelSent = false; _matchStartTime = DateTime.UtcNow; _matchStatsSubmitted = false; PlayerPrefs.SetString("LobbyName", _currentSession.lobby_name); PlayerPrefs.SetString("LobbyPassword", _currentSession.lobby_password); PlayerPrefs.SetString("HostRuleset", _hostRulesetSelection); PlayerPrefs.Save(); Log("Host ruleset stored: " + _hostRulesetSelection); val.StartHost(); Log("Host lobby initiated. Waiting for Steamworks lobby creation callback..."); ((MonoBehaviour)this).StartCoroutine(UpdateSessionStatus("in_progress")); } } private IEnumerator CancelSessionAsHost(string reason) { if (_hostCancelSent || !_isHost || _currentSession == null || string.IsNullOrEmpty(_currentSession.id) || _currentSession.id == "DEBUG") { yield break; } _hostCancelSent = true; string sessionId = _currentSession.id; string json = "{\"status\":\"completed\"}"; UnityWebRequest req = new UnityWebRequest(GetBaseApiUrl() + "/matchmaking_session?id=eq." + sessionId, "PATCH"); try { req.uploadHandler = (UploadHandler)new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { if (_currentSession != null && _currentSession.id == sessionId) { _currentSession.status = "completed"; } Log("Host leave update sent (" + reason + "): session marked completed."); } else { _hostCancelSent = false; Log($"<color=red>Host cancel failed ({reason}): {req.result} {req.error}</color>"); } } finally { ((IDisposable)req)?.Dispose(); } } private IEnumerator UploadSteamLobbyLink(string steamLink) { if (_currentSession == null || string.IsNullOrEmpty(steamLink)) { yield break; } if (string.Equals(_currentSession.steam_lobby_link, steamLink, StringComparison.Ordinal)) { Log("Steam Lobby Link already synced. Skipping redundant upload."); yield break; } _steamLinkUploadAttempts++; _lastUploadAttemptAt = DateTime.Now; _lastGeneratedSteamLink = steamLink; Log("Uploading Steam Lobby Link to API..."); string json = "{\"steam_lobby_link\":\"" + steamLink + "\"}"; UnityWebRequest req = new UnityWebRequest(GetBaseApiUrl() + "/matchmaking_session?id=eq." + _currentSession.id, "PATCH"); try { req.uploadHandler = (UploadHandler)new UploadHandlerRaw(Encoding.UTF8.GetBytes(json)); req.downloadHandler = (DownloadHandler)new DownloadHandlerBuffer(); ApplyApiHeaders(req); yield return req.SendWebRequest(); if ((int)req.result == 1) { _steamLinkUploadSuccesses++; _lastUploadSuccessAt = DateTime.Now; _lastUploadedSteamLink = steamLink; _currentSession.steam_lobby_link = steamLink; _lastUploadError = "-"; Log("<color=green>Steam Lobby Link uploaded successfully.</color>"); } else { _steamLinkUploadFailures++; _lastUploadError = $"{req.result}: {req.error}"; Log("<color=red>Steam Lobby Link upload failed: " + _lastUploadError + "</color>"); } } finally { ((IDisposable)req)?.Dispose(); } } private IEnumerator CreateMatchAndEntries() { if (_matchCreationInProgress) { Log("<color=orange>[Match Creation] Already in progress - skipping duplicate call</color>"); yield break; } _matchCreationInProgress = true; yield return CreateMatchAndEntriesInternal(); _matchCreationInProgress = false; } private IEnumerator CreateMatchAndEntriesInternal() { if (_userProfile == null) { Log("<color=red>[Match Creation] Failed: Missing player profile</color>"); yield break; } if (_currentSession == null) { if (string.IsNullOrWhiteSpace(_localManualSessionId)) { _localManualSessionId = BuildStableManualLocalSessionId(); } Log("<color=cyan>[Match Creation] Manual local lobby detected. Session surrogate: " + _localManualSessionId + "</color>"); } string currentMatchType = _currentSession?.match_type ?? PlayerPrefs.GetString("MatchType", ""); bool isCasualMatch = IsCurrentMatchCasual(); if (IsProSeriesMatchType(currentMatchType) || IsProSeriesMatchType(PlayerPrefs.GetString("MatchType", ""))) { if (!_proSeriesSkipLogged) { Log("<color=yellow>[Match] Pro Series match — automated upload skipped</color>"); _proSeriesSkipLogged = true; } _matchEntriesCreated = true; yield break; } string activeSessionId = ((_currentSession != null) ? _currentSession.id : _localManualSessionId); Log("<color=cyan>[Match Creation] Starting new match for session " + activeSessionId + "</color>"); _matchStartTime = DateTime.UtcNow; Dictionary<string, int> playerScores = new Dictionary<string, int>(); Dictionary<string, int> playerScoresVsPar = new Dictionary<string, int>(); List<LiveLeaderboardPlugin.SBGLPlayer> startingLeaderboard = null; try { LiveLeaderboardPlugin liveLeaderboard = Object.FindAnyObjectByType<LiveLeaderboardPlugin>((FindObjectsInactive)1); if ((Object)(object)liveLeaderboard != (Object)null) { List<LiveLeaderboardPlugin.SBGLPlayer> allLeaderboardPlayers = liveLeaderboard.GetCurrentLeaderboard(); startingLeaderboard = new List<LiveLeaderboardPlugin.SBGLPlayer>(allLeaderboardPlayers); Log($"<color=cyan>[Match Creation] Found {allLeaderboardPlayers.Count} players on leaderboard</color>"); foreach (LiveLeaderboardPlugin.SBGLPlayer player in allLeaderboardPlayers) { if (player != null) { playerScores[player.Name] = player.BaseScore; if (!string.IsNullOrEmpty(player.RawStrokes)) { string strokeStr = player.RawStrokes.Replace("±", "").Trim(); int.TryParse(strokeStr, out var vsPar); playerScoresVsPar[player.Name] = vsPar; } } } } } catch (Exception ex) { Log("<color=yellow>[Match Creation] Error prefetching leaderboard: " + ex.Message + "</color>"); } _cachedLeaderboardScores = playerScores; _cachedLeaderboardScoresVsPar = playerScoresVsPar; _matchExpectedPlayerCount = startingLeaderboard?.Count ?? 0; Dictionary<string, int> startingPositionMap = ((startingLeaderboard != null) ? BuildFinishPositionMap(startingLeaderboard) : new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase)); if (isCasualMatch) { Log("<color=cyan>[Casual] Casual match detected - submitting match record for website to process</color>"); } if (_isHost || NetworkServer.active) { Log("<color=cyan>[Match Creation] We are the host — creating Match record with priority</color>"); if (!string.IsNullOrEmpty(_currentMatchId)) { Log("<color=cyan>[Match Creation] Match already exists for this session: " + _currentMatchId + " — skipping duplicate POST</color>"); } else { if (_currentSession != null) { string existingMatchId = null; yield return ResolveExistingMatchIdForCurrentSession(delegate(string id) { existingMatchId = id; }); if (!string.IsNullOrEmpty(existingMatchId)) { _currentMatchId = existingMatchId; ShowUploadNotification("Match record already exists; reusing existing upload."); goto IL_08ee; } } Log("<color=cyan>[Match Creation] No existing Match found — proceeding with POST</color>"); string prePostSessionId = ((_currentSession != null) ? _currentSession.id : _localManualSessionId); if (!string.IsNullOrWhiteSpace(prePostSessionId)) { string finalCheckId = null; yield return CallAPI("/match?matchmaking_session_id=eq." + prePostSessionId + "&order=created_at.asc", "GET", "", delegate(string res) { JObject obj = SelectNewestReusableMatch(ParseApiObjectList(res)); finalCheckId = (string)((obj != null) ? obj["id"] : null); }); if (!string.IsNullOrWhiteSpace(finalCheckId)) { Log("<color=green>[Match Creation] ✓ Final pre-POST check found existing active match: " + finalCheckId + " — skipping duplicate POST</color>"); _currentMatchId = finalCheckId; if (_currentSession != null) { _currentSession.match_id = finalCheckId; } ShowUploadNotification("Match record already exists; reusing existing upload."); goto IL_08ee; } } string newMatchId = null; yield return SubmitMatchEntry(CollectMatchStats(0f), delegate(string id) { newMatchId = id; }); if (string.IsNullOrEmpty(newMatchId)) { string apiErr = ((!string.IsNullOrEmpty(_lastCallApiError)) ? (" (" + _lastCallApiError + ")") : ""); Log("<color=red>[Match Creation] Failed to create Match record" + apiErr + "</color>"); ShowUploadNotification("Upload failed: could not create match record." + apiErr, "failure"); yield break; } _currentMatchId = newMatchId; Log("<color=green>[Match Creation] ✓ Match created: " + newMatchId + "</color>"); ShowUploadNotification("Upload success: match record created.", "success"); MatchResultSubmissionService.BroadcastMatchId(peers: CompetitivePluginCheck.GetKnownPeers(), matchId: newMatchId); } goto IL_08ee; } Log("<color=cyan>[Match Creation] Not host — host will handle all uploads</color>"); _matchEntriesCreated = true; yield break; IL_08ee: if (_currentSession != null) { string linkMatchId = _currentMatchId; yield return CallAPI("/matchmaking_session?id=eq." + _currentSession.id, "PATCH", "{\"match_id\":\"" + linkMatchId + "\"}", delegate(string res) { JObject val = ParseApiSingleObject(res); if (val != null) { _currentSession.match_id = linkMatchId; Log("<color=green>[Match Creation] ✓ MatchmakingSession " + _currentSession.id + " linked to match: " + linkMatchId + "</color>"); } else { Log("<color=yellow>[Match Creation] Could not confirm MatchmakingSession update</color>"); } }); } else { Log("<color=cyan>[Match Creation] Local lobby mode: skipping MatchmakingSession link</color>"); } _playerMatchEntryIds.Clear(); _playerIdsByName.Clear(); _lastSubmittedScores.Clear(); _lastSubmittedScoresVsPar.Clear(); if (startingLeaderboard == null || startingLeaderboard.Count == 0) { Log("<color=yellow>[Match Creation] Leaderboard empty at round start — deferring MatchEntry creation until the gameplay monitor sees players</color>"); if (_monitorCoroutine == null) { _monitorCoroutine = ((MonoBehaviour)this).StartCoroutine(MonitorAndUpdateScores()); } yield break; } List<string> playerIds = ((_currentSession != null && _currentSession.player_ids != null && _currentSession.player_ids.Count > 0) ? _currentSession.player_ids : new List<string> { _userProfile.id }); yield return EnrichPlayerIdsFromLeaderboard(startingLeaderboard, playerIds); _matchExpectedPlayerCount = playerIds.Count; foreach (string playerId in playerIds) { string playerName = null; string preMatchMmr = null; int gamePoints = 0; int scoreVsPar = 0; if (playerId == _userProfile.id) { playerName = _userProfile.display_name; preMatchMmr = _userProfile.current_mmr.ToString(); Log("<color=cyan>[Match Creation] Current player: " + playerName + " (MMR: " + preMatchMmr + ")</color>"); } else { yield return CallAPI("/player?id=eq." + playerId + "&limit=1", "GET", "", delegate(string res) { try { JObject val = ParseApiSingleObject(res); if (val != null) { playerName = (string)val["display_name"]; if (string.IsNullOrEmpty(playerName)) { Log("<color=yellow>[Match Creation] Player " + playerId + " has no display_name, using ID</color>"); playerName = playerId; } object obj = val["current_mmr"];