using System; using System.Collections; using System.Text; using System.Text.RegularExpressions; using BlockSpace.Account; using BlockSpace.Voxels; using ExitGames.Client.Photon; using Photon.Pun; using Photon.Realtime; using Photon.VR; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; namespace BlockSpace.Worlds { [DisallowMultipleComponent] public sealed class WorldCommandPoller : MonoBehaviourPunCallbacks { [Serializable] private sealed class PollRequest { public string playerId; } [Serializable] private sealed class PollResponse { public bool ok; public Command command; public string error; } [Serializable] private sealed class Command { public string type; public string worldName; public string roomName; } [Serializable] private sealed class PresencePingRequest { public string playerId; public string roomName; } [SerializeField] private bool enable = true; [SerializeField] private string backendBaseUrl = "https://blockspacebackend-production.up.railway.app"; [SerializeField] private float pollIntervalSeconds = 2f; [SerializeField] private float onlinePingIntervalSeconds = 5f; [Header("Loading Screen")] [SerializeField] private GameObject loadingScreenObject; [SerializeField] private Slider loadingProgressSlider; [SerializeField] private WorldLoadingScreenController loadingScreen; private const string DefaultLobbyWorldName = "Lobby"; private const string DefaultLobbyWorldId = "ZT5HjbaGAT5adlkvAirJ0A"; private const int PublicWorldJoinRetryCount = 5; private const string RoomWorldVersionKey = "worldVersion"; private Coroutine pollCoroutine; private Coroutine pingCoroutine; private bool joinInProgress; private bool joinResultReady; private bool joinSucceeded; private bool leftRoomReady; private bool lobbyReady; private bool startupLobbyJoinRequested; private new void OnEnable() { StartLoopsIfNeeded(); } private void Start() { Debug.Log("[WorldCommandPoller] Start: beginning lobby startup sequence."); StartCoroutine(JoinLobbyWhenBackendReady()); } private IEnumerator JoinLobbyWhenBackendReady() { BeginLoadingScreen(0.02f); float connectStart = Time.realtimeSinceStartup; while (PhotonNetwork.NetworkClientState != ClientState.ConnectedToMasterServer && Time.realtimeSinceStartup - connectStart < 15f) { yield return null; } Debug.Log($"[WorldCommandPoller] JoinLobbyWhenBackendReady: Photon state={PhotonNetwork.NetworkClientState} after wait."); SetLoadingProgress(0.08f); if (startupLobbyJoinRequested) { yield break; } string playerId = BlockSpaceAccountPrefs.GetUserFolderId(); WorldBackendClient client = new WorldBackendClient(backendBaseUrl); float backendStart = Time.realtimeSinceStartup; while (Time.realtimeSinceStartup - backendStart < 20f) { WorldBackendClient.ResolveWorldResponse resolve = null; yield return client.ResolveWorld("Lobby", playerId, delegate(WorldBackendClient.ResolveWorldResponse r) { resolve = r; }); if (resolve != null && resolve.ok) { Debug.Log("[WorldCommandPoller] JoinLobbyWhenBackendReady: backend resolve succeeded for Lobby."); SetLoadingProgress(0.14f); break; } if (resolve != null) { Debug.LogWarning($"[WorldCommandPoller] JoinLobbyWhenBackendReady: backend responded but resolve failed. Continuing to lobby join. ok={resolve.ok}, error={resolve.error}"); SetLoadingProgress(0.14f); break; } Debug.LogWarning("[WorldCommandPoller] JoinLobbyWhenBackendReady: backend resolve returned null."); yield return new WaitForSecondsRealtime(1f); } startupLobbyJoinRequested = true; Debug.Log("[WorldCommandPoller] JoinLobbyWhenBackendReady: proceeding to join default Lobby world."); SetLoadingProgress(0.2f); yield return JoinWorldInstance("Lobby"); yield return IncrementVisitForCurrentWorldIfMatches("Lobby"); } private IEnumerator IncrementVisitForCurrentWorldIfMatches(string baseWorldName) { if (!PhotonNetwork.InRoom || PhotonNetwork.CurrentRoom == null || !string.Equals(GetBaseWorldName(PhotonNetwork.CurrentRoom.Name), baseWorldName, StringComparison.OrdinalIgnoreCase)) { yield break; } string userFolderId = BlockSpaceAccountPrefs.GetUserFolderId(); if (!string.IsNullOrEmpty(userFolderId)) { WorldBackendClient client = new WorldBackendClient(backendBaseUrl); WorldBackendClient.ResolveWorldResponse resolve = null; yield return client.ResolveWorld(baseWorldName, userFolderId, delegate(WorldBackendClient.ResolveWorldResponse r) { resolve = r; }); if (resolve != null && resolve.ok && resolve.exists && !string.IsNullOrEmpty(resolve.worldId)) { yield return client.IncrementVisit(resolve.worldId, null); } } } private static string RandomPrivateCode() { char c = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[UnityEngine.Random.Range(0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Length)]; char c2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[UnityEngine.Random.Range(0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Length)]; char c3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[UnityEngine.Random.Range(0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".Length)]; return new string(new char[3] { c, c2, c3 }); } private IEnumerator JoinPrivateWorldInstance(string worldName) { for (int attempt = 0; attempt < 10; attempt++) { string text = RandomPrivateCode(); string roomName = worldName + "|" + text; yield return JoinExactRoom(roomName); if (PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && string.Equals(PhotonNetwork.CurrentRoom.Name, roomName, StringComparison.OrdinalIgnoreCase)) { break; } } } private new void OnDisable() { StopAllCoroutines(); pollCoroutine = null; pingCoroutine = null; SetLoadingScreen(visible: false, 0f); } private void StartLoopsIfNeeded() { if (enable) { if (pollCoroutine == null) { pollCoroutine = StartCoroutine(PollLoop()); } if (pingCoroutine == null) { pingCoroutine = StartCoroutine(PresencePingLoop()); } } } private void SetLoadingScreen(bool visible, float progress) { WorldLoadingScreenController worldLoadingScreenController = ResolveLoadingScreen(); if (!(worldLoadingScreenController == null)) { worldLoadingScreenController.SetLoading(visible); worldLoadingScreenController.SetProgress(progress); } } private void BeginLoadingScreen(float progress) { WorldLoadingScreenController worldLoadingScreenController = ResolveLoadingScreen(); if (!(worldLoadingScreenController == null)) { worldLoadingScreenController.BeginLoading(progress); } } private void SetLoadingProgress(float progress) { WorldLoadingScreenController worldLoadingScreenController = ResolveLoadingScreen(); if (!(worldLoadingScreenController == null)) { worldLoadingScreenController.SetCheckpoint(progress); } } private WorldLoadingScreenController ResolveLoadingScreen() { if (loadingScreen == null) { loadingScreen = ((WorldLoadingScreenController.Instance != null) ? WorldLoadingScreenController.Instance : UnityEngine.Object.FindFirstObjectByType()); } if (loadingScreen == null && loadingScreenObject != null) { loadingScreen = base.gameObject.AddComponent(); } if (loadingScreen != null) { loadingScreen.Configure(loadingScreenObject, loadingProgressSlider); } return loadingScreen; } private IEnumerator PresencePingLoop() { while (enable) { yield return new WaitForSecondsRealtime(Mathf.Clamp(onlinePingIntervalSeconds, 1f, 60f)); if (!BlockSpaceAccountPrefs.IsLinked) { continue; } string linkedPlayerId = BlockSpaceAccountPrefs.GetLinkedPlayerId(); if (!string.IsNullOrEmpty(linkedPlayerId)) { string url = (backendBaseUrl ?? string.Empty).Trim().TrimEnd('/') + "/api/presence/ping"; string roomName = ((PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null) ? PhotonNetwork.CurrentRoom.Name : ""); string s = JsonUtility.ToJson(new PresencePingRequest { playerId = linkedPlayerId, roomName = roomName }); using UnityWebRequest request = new UnityWebRequest(url, "POST"); byte[] bytes = Encoding.UTF8.GetBytes(s); request.uploadHandler = new UploadHandlerRaw(bytes); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); } } } private IEnumerator PollLoop() { while (enable) { yield return new WaitForSecondsRealtime(Mathf.Clamp(pollIntervalSeconds, 0.5f, 30f)); if (!BlockSpaceAccountPrefs.IsLinked || joinInProgress) { continue; } string linkedPlayerId = BlockSpaceAccountPrefs.GetLinkedPlayerId(); if (string.IsNullOrEmpty(linkedPlayerId)) { continue; } string text = (backendBaseUrl ?? string.Empty).Trim().TrimEnd('/'); if (string.IsNullOrEmpty(text)) { continue; } string url = text + "/api/commands/poll"; string s = JsonUtility.ToJson(new PollRequest { playerId = linkedPlayerId }); PollResponse response = null; using (UnityWebRequest request = new UnityWebRequest(url, "POST")) { byte[] bytes = Encoding.UTF8.GetBytes(s); request.uploadHandler = new UploadHandlerRaw(bytes); request.downloadHandler = new DownloadHandlerBuffer(); request.SetRequestHeader("Content-Type", "application/json"); yield return request.SendWebRequest(); if (request.result == UnityWebRequest.Result.Success) { try { response = JsonUtility.FromJson(request.downloadHandler.text); } catch { } } } if (response == null || !response.ok || response.command == null) { continue; } if (!string.Equals(response.command.type, "join_world", StringComparison.OrdinalIgnoreCase)) { if (string.Equals(response.command.type, "world_reload", StringComparison.OrdinalIgnoreCase)) { string text2 = (response.command.worldName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(text2)) { yield return JoinWorldInstance(text2); } } else if (string.Equals(response.command.type, "world_force_save", StringComparison.OrdinalIgnoreCase)) { string text3 = (response.command.worldName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(text3)) { yield return ForceSaveWorld(text3); } } else if (string.Equals(response.command.type, "join_room", StringComparison.OrdinalIgnoreCase)) { string text4 = (response.command.roomName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(text4)) { yield return JoinExactRoom(text4); } } else if (string.Equals(response.command.type, "join_world_private", StringComparison.OrdinalIgnoreCase)) { string text5 = (response.command.worldName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(text5)) { yield return JoinPrivateWorldInstance(text5); } } } else { string text6 = (response.command.worldName ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(text6)) { yield return JoinWorldInstance(text6); } } } } private static string GetBaseWorldName(string roomName) { string text = (roomName ?? string.Empty).Trim(); if (string.IsNullOrEmpty(text)) { return text; } int num = text.LastIndexOf('|'); if (num <= 0) { return text; } string text2 = text.Substring(num + 1); if (text2.Length == 3 && Regex.IsMatch(text2, "^[A-Za-z0-9]{3}$")) { return text.Substring(0, num); } return text; } private IEnumerator ForceSaveWorld(string baseWorldName) { if (PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && string.Equals(GetBaseWorldName(PhotonNetwork.CurrentRoom.Name), baseWorldName, StringComparison.OrdinalIgnoreCase)) { VoxelGroupPhotonSync voxelGroupPhotonSync = UnityEngine.Object.FindFirstObjectByType(); if (!(voxelGroupPhotonSync == null)) { yield return voxelGroupPhotonSync.ForceSaveNow(); } } } private IEnumerator JoinWorldInstance(string worldName) { Debug.Log("[WorldCommandPoller] JoinWorldInstance: starting join for world '" + worldName + "'."); joinInProgress = true; BeginLoadingScreen(0.2f); if (PhotonNetwork.InRoom) { SetLoadingProgress(0.24f); Debug.Log("[WorldCommandPoller] JoinWorldInstance: leaving current room '" + PhotonNetwork.CurrentRoom?.Name + "'."); leftRoomReady = false; PhotonNetwork.LeaveRoom(); float start = Time.realtimeSinceStartup; while (!leftRoomReady && Time.realtimeSinceStartup - start < 10f) { yield return null; } SetLoadingProgress(0.3f); Debug.Log($"[WorldCommandPoller] JoinWorldInstance: left room ready={leftRoomReady}, inRoom={PhotonNetwork.InRoom}."); } SetLoadingProgress(0.34f); float masterStart = Time.realtimeSinceStartup; while (PhotonNetwork.NetworkClientState != ClientState.ConnectedToMasterServer && Time.realtimeSinceStartup - masterStart < 10f) { yield return null; } SetLoadingProgress(0.4f); if (PhotonNetwork.NetworkClientState != ClientState.ConnectedToMasterServer) { joinInProgress = false; SetLoadingScreen(visible: false, 0f); yield break; } WorldBackendClient client = new WorldBackendClient(backendBaseUrl); int maxPlayers = ((PhotonVRManager.Manager != null) ? PhotonVRManager.Manager.DefaultRoomLimit : 16); for (int attempt = 0; attempt < 5; attempt++) { SetLoadingProgress(0.46f); WorldBackendClient.LobbyTargetResponse target = null; yield return client.ResolveLobbyTarget(worldName, maxPlayers, delegate(WorldBackendClient.LobbyTargetResponse r) { target = r; }); if (target == null || !target.ok || string.IsNullOrEmpty(target.roomName)) { Debug.LogWarning(string.Format("[WorldCommandPoller] JoinWorldInstance: lobby target failed for '{0}' [attempt {1}/{2}]. error={3}", worldName, attempt + 1, 5, (target != null) ? target.error : "")); yield return new WaitForSecondsRealtime(1f); continue; } joinResultReady = false; joinSucceeded = false; SetLoadingProgress(0.54f); ExitGames.Client.Photon.Hashtable hashtable = new ExitGames.Client.Photon.Hashtable(); if (!string.IsNullOrEmpty(target.worldVersion)) { hashtable["worldVersion"] = target.worldVersion; } RoomOptions roomOptions = new RoomOptions(); roomOptions.IsVisible = true; roomOptions.IsOpen = true; roomOptions.MaxPlayers = (byte)Mathf.Clamp(maxPlayers, 1, 255); roomOptions.CustomRoomProperties = hashtable; roomOptions.CustomRoomPropertiesForLobby = new string[1] { "worldVersion" }; RoomOptions roomOptions2 = roomOptions; Debug.Log($"[WorldCommandPoller] JoinWorldInstance: backend selected {target.action}('{target.roomName}') worldVersion='{target.worldVersion}' roomsSeen={target.roomsSeen} [attempt {attempt + 1}/{5}]."); if (string.Equals(target.action, "join", StringComparison.OrdinalIgnoreCase)) { SetLoadingProgress(0.6f); PhotonNetwork.JoinRoom(target.roomName); } else { SetLoadingProgress(0.6f); PhotonNetwork.JoinOrCreateRoom(target.roomName, roomOptions2, null); } float start = Time.realtimeSinceStartup; while (!joinResultReady && Time.realtimeSinceStartup - start < 12f) { yield return null; } if (!joinResultReady && PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && string.Equals(PhotonNetwork.CurrentRoom.Name, target.roomName, StringComparison.OrdinalIgnoreCase)) { joinSucceeded = true; joinResultReady = true; Debug.Log("[WorldCommandPoller] JoinWorldInstance: detected successful join by current room '" + target.roomName + "' without callback."); } Debug.Log($"[WorldCommandPoller] JoinWorldInstance: room '{target.roomName}' join result ready={joinResultReady}, succeeded={joinSucceeded}, inRoom={PhotonNetwork.InRoom}, currentRoom={PhotonNetwork.CurrentRoom?.Name}."); if (joinSucceeded && PhotonNetwork.InRoom) { Debug.Log("[WorldCommandPoller] JoinWorldInstance: successfully joined room '" + target.roomName + "'."); joinInProgress = false; SetLoadingProgress(0.66f); yield break; } if (PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && !string.Equals(PhotonNetwork.CurrentRoom.Name, target.roomName, StringComparison.OrdinalIgnoreCase)) { Debug.LogWarning("[WorldCommandPoller] JoinWorldInstance: still in different room '" + PhotonNetwork.CurrentRoom.Name + "' after attempting '" + target.roomName + "', leaving before next attempt."); leftRoomReady = false; PhotonNetwork.LeaveRoom(); float leaveStart = Time.realtimeSinceStartup; while (!leftRoomReady && Time.realtimeSinceStartup - leaveStart < 10f) { yield return null; } SetLoadingProgress(0.3f); } yield return new WaitForSecondsRealtime(0.5f); } joinInProgress = false; SetLoadingScreen(visible: false, 0f); } private IEnumerator JoinExactRoom(string roomName) { Debug.Log("[WorldCommandPoller] JoinExactRoom: starting join for exact room '" + roomName + "'."); joinInProgress = true; BeginLoadingScreen(0.2f); if (PhotonNetwork.InRoom) { SetLoadingProgress(0.24f); Debug.Log("[WorldCommandPoller] JoinExactRoom: leaving current room '" + PhotonNetwork.CurrentRoom?.Name + "'."); leftRoomReady = false; PhotonNetwork.LeaveRoom(); float start = Time.realtimeSinceStartup; while (!leftRoomReady && Time.realtimeSinceStartup - start < 10f) { yield return null; } SetLoadingProgress(0.3f); Debug.Log($"[WorldCommandPoller] JoinExactRoom: left room ready={leftRoomReady}, inRoom={PhotonNetwork.InRoom}."); } SetLoadingProgress(0.34f); float masterStart = Time.realtimeSinceStartup; while (PhotonNetwork.NetworkClientState != ClientState.ConnectedToMasterServer && Time.realtimeSinceStartup - masterStart < 10f) { yield return null; } SetLoadingProgress(0.4f); if (PhotonNetwork.NetworkClientState != ClientState.ConnectedToMasterServer) { joinInProgress = false; SetLoadingScreen(visible: false, 0f); yield break; } if (!PhotonNetwork.InLobby) { lobbyReady = false; PhotonNetwork.JoinLobby(); float start = Time.realtimeSinceStartup; while (!lobbyReady && Time.realtimeSinceStartup - start < 10f) { yield return null; } SetLoadingProgress(0.5f); if (!PhotonNetwork.InLobby) { joinInProgress = false; SetLoadingScreen(visible: false, 0f); yield break; } } joinResultReady = false; joinSucceeded = false; RoomOptions roomOptions = new RoomOptions { IsVisible = true, IsOpen = true, MaxPlayers = ((PhotonVRManager.Manager != null) ? ((byte)PhotonVRManager.Manager.DefaultRoomLimit) : 16) }; Debug.Log("[WorldCommandPoller] JoinExactRoom: requesting JoinOrCreateRoom('" + roomName + "')."); SetLoadingProgress(0.6f); PhotonNetwork.JoinOrCreateRoom(roomName, roomOptions, null); float waitStart = Time.realtimeSinceStartup; while (!joinResultReady && Time.realtimeSinceStartup - waitStart < 10f) { yield return null; } Debug.Log($"[WorldCommandPoller] JoinExactRoom: room '{roomName}' join result ready={joinResultReady}, succeeded={joinSucceeded}, inRoom={PhotonNetwork.InRoom}."); joinInProgress = false; if (joinSucceeded && PhotonNetwork.InRoom) { SetLoadingProgress(0.66f); } else { SetLoadingScreen(visible: false, 0f); } } public override void OnLeftRoom() { leftRoomReady = true; base.OnLeftRoom(); } public override void OnJoinedRoom() { joinSucceeded = true; joinResultReady = true; Debug.Log("[WorldCommandPoller] OnJoinedRoom: joined room '" + PhotonNetwork.CurrentRoom?.Name + "' successfully."); base.OnJoinedRoom(); } public override void OnJoinedLobby() { lobbyReady = true; Debug.Log("[WorldCommandPoller] OnJoinedLobby: Photon lobby joined successfully."); base.OnJoinedLobby(); } public override void OnJoinRoomFailed(short returnCode, string message) { joinSucceeded = false; joinResultReady = true; Debug.LogWarning($"[WorldCommandPoller] OnJoinRoomFailed: code={returnCode}, message={message}, state={PhotonNetwork.NetworkClientState}."); base.OnJoinRoomFailed(returnCode, message); } public override void OnCreateRoomFailed(short returnCode, string message) { joinSucceeded = false; joinResultReady = true; Debug.LogWarning($"[WorldCommandPoller] OnCreateRoomFailed: code={returnCode}, message={message}, state={PhotonNetwork.NetworkClientState}."); base.OnCreateRoomFailed(returnCode, message); } } }