From ebbafd0caa89774b06e083b1bf78a7ea9e71d293 Mon Sep 17 00:00:00 2001 From: Davetsa Date: Tue, 7 Apr 2026 11:17:21 +0300 Subject: [PATCH 01/39] feat: Add InRoomInviteSelectorPanel for inviting online players - Implemented InRoomInviteSelectorPanel to allow users to select and invite online players to a premade room. - Added UI elements including title, close button, and player list with dynamic row generation. - Included fallback styles for UI elements to ensure consistent appearance. - Created InviteDecisionPopupHandler for handling invite decisions with customizable messages and button texts. - Updated RoomSetupManager and MatchmakingPanel to handle new game type InRoom_ and ensure proper UI interactions. - Enhanced error handling and null checks across various scripts to improve stability. --- Assets/Altzone/Scripts/Photon/GameTypeEnum.cs | 1 + Assets/Altzone/Scripts/Photon/LobbyManager.cs | 1242 ++++++++++++++++- .../Scripts/Photon/PhotonBattleRoom.cs | 14 + .../Scripts/Photon/PhotonRealtimeClient.cs | 37 + Assets/MenuUi/MenuUi.asmdef | 1 + .../Prefabs/Panels/UIOverlayPanel.prefab | 20 + .../Prefabs/UI Components/Battle Popup.prefab | 18 +- .../MenuUi/Resources/GameTypeReference.asset | 6 + .../Resources/InviteDecisionPanel.prefab | 966 +++++++++++++ .../Resources/InviteDecisionPanel.prefab.meta | 7 + .../Scripts/Lobby/BattlePopupPanelManager.cs | 3 + .../Lobby/InLobby/InLobbyController.cs | 117 ++ .../InLobby/LobbyRoomListingController.cs | 8 + .../Scripts/Lobby/InRoom/InRoomController.cs | 450 ++++++ .../Lobby/InRoom/InRoomInviteSelectorPanel.cs | 593 ++++++++ .../InRoom/InRoomInviteSelectorPanel.cs.meta | 11 + .../Scripts/Lobby/InRoom/RoomSetupManager.cs | 30 +- .../MenuUi/Scripts/Lobby/MatchmakingPanel.cs | 2 +- .../Scripts/Lobby/MiniMatchmakingPanel.cs | 2 +- .../BattlePopupCharacterSlotController.cs | 13 +- .../Window/InviteDecisionPopupHandler.cs | 265 ++++ .../Window/InviteDecisionPopupHandler.cs.meta | 11 + 22 files changed, 3728 insertions(+), 89 deletions(-) create mode 100644 Assets/MenuUi/Resources/InviteDecisionPanel.prefab create mode 100644 Assets/MenuUi/Resources/InviteDecisionPanel.prefab.meta create mode 100644 Assets/MenuUi/Scripts/Lobby/InRoom/InRoomInviteSelectorPanel.cs create mode 100644 Assets/MenuUi/Scripts/Lobby/InRoom/InRoomInviteSelectorPanel.cs.meta create mode 100644 Assets/MenuUi/Scripts/Window/InviteDecisionPopupHandler.cs create mode 100644 Assets/MenuUi/Scripts/Window/InviteDecisionPopupHandler.cs.meta diff --git a/Assets/Altzone/Scripts/Photon/GameTypeEnum.cs b/Assets/Altzone/Scripts/Photon/GameTypeEnum.cs index 52c75fcc0a..5ce0858908 100644 --- a/Assets/Altzone/Scripts/Photon/GameTypeEnum.cs +++ b/Assets/Altzone/Scripts/Photon/GameTypeEnum.cs @@ -9,6 +9,7 @@ public enum GameType Custom = 0, Random2v2 = 1, Clan2v2 = 2, + InRoom_ = 3, } } diff --git a/Assets/Altzone/Scripts/Photon/LobbyManager.cs b/Assets/Altzone/Scripts/Photon/LobbyManager.cs index 7e06c8f724..586d140ede 100644 --- a/Assets/Altzone/Scripts/Photon/LobbyManager.cs +++ b/Assets/Altzone/Scripts/Photon/LobbyManager.cs @@ -135,6 +135,18 @@ public class LobbyManager : MonoBehaviour, ILobbyCallbacks, IMatchmakingCallback // Timestamp of the last CancelGameStart handling (used to detect quick rejoins) private float _lastStartCancelTime = -100f; private string[] _teammates = null; + private bool _isPremadeMatchmakingFlow = false; + private string _premadeTeammateUserId = string.Empty; + private string _lastAutoInviteRoomName = string.Empty; + private float _lastAutoInviteJoinTime = -100f; + private string _pendingInRoomInviteRoomName = string.Empty; + private string _pendingAcceptedInRoomInviteRoomName = string.Empty; + private float _pendingAcceptedInRoomInviteStartTime = -100f; + private readonly Dictionary _declinedInRoomInviteUntil = new(); + private const float InRoomInvitePromptThrottleSeconds = 5f; + private const float InRoomInviteDeclineCooldownSeconds = 30f; + private const float InRoomInviteValiditySeconds = 60f; + private const float InRoomInviteJoinTimeoutSeconds = 12f; private List _friendList; @@ -262,6 +274,12 @@ public class LobbyManager : MonoBehaviour, ILobbyCallbacks, IMatchmakingCallback public delegate void KickedOutOfTheRoom(GetKickedEvent.ReasonType reason); public static event KickedOutOfTheRoom OnKickedOutOfTheRoom; + public delegate void InRoomInviteReceived(InRoomInviteInfo inviteInfo); + public static event InRoomInviteReceived OnInRoomInviteReceived; + + public delegate void InRoomInviteJoinFailed(string roomName, short returnCode, string message); + public static event InRoomInviteJoinFailed OnInRoomInviteJoinFailed; + #endregion #region Public API & Helpers @@ -277,10 +295,76 @@ public static void NotifyGamePlayedOut() _gamePlayedOut = true; } + public void AcceptInRoomInvite(string roomName) + { + if (string.IsNullOrEmpty(roomName)) return; + if (!PhotonRealtimeClient.InLobby || PhotonRealtimeClient.InRoom) return; + + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId; + if (string.IsNullOrEmpty(localUserId)) return; + + _pendingInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteRoomName = roomName; + _pendingAcceptedInRoomInviteStartTime = Time.time; + _lastAutoInviteRoomName = roomName; + _lastAutoInviteJoinTime = Time.time; + Debug.Log($"AcceptInRoomInvite: joining room '{roomName}'."); + PhotonRealtimeClient.JoinRoom(roomName, new[] { localUserId }); + } + + public void DeclineInRoomInvite(string roomName) + { + if (string.IsNullOrEmpty(roomName)) return; + + _declinedInRoomInviteUntil[roomName] = Time.time + InRoomInviteDeclineCooldownSeconds; + if (_pendingInRoomInviteRoomName == roomName) + { + _pendingInRoomInviteRoomName = string.Empty; + } + if (_pendingAcceptedInRoomInviteRoomName == roomName) + { + _pendingAcceptedInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteStartTime = -100f; + } + Debug.Log($"DeclineInRoomInvite: declined room '{roomName}'."); + } + + private bool IsInRoomInviteDeclinedRecently(string roomName) + { + if (!_declinedInRoomInviteUntil.TryGetValue(roomName, out float until)) return false; + + if (Time.time > until) + { + _declinedInRoomInviteUntil.Remove(roomName); + return false; + } + + return true; + } + private IEnumerator FormMatchFromQueue(string[] selected, int roomGameTypeInt, string clanName, int soulhomeRank) { try { + bool queuePremadeMode = false; + string queuePremadeUserId1 = string.Empty; + string queuePremadeUserId2 = string.Empty; + int queuePremadeTargetGameType = roomGameTypeInt; + try + { + if (PhotonRealtimeClient.CurrentRoom != null) + { + queuePremadeMode = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeModeKey, false); + queuePremadeUserId1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, string.Empty); + queuePremadeUserId2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + queuePremadeTargetGameType = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, roomGameTypeInt); + } + } + catch (Exception ex) + { + Debug.LogWarning($"FormMatchFromQueue: failed to read premade metadata from queue room: {ex.Message}"); + } + // Notify queue members that leader is forming a match so they can start follow flow try { @@ -350,6 +434,56 @@ private IEnumerator FormMatchFromQueue(string[] selected, int roomGameTypeInt, s try { PhotonRealtimeClient.CurrentRoom.SetCustomProperty("qe", selected.Length); } catch (Exception ex) { Debug.LogWarning($"FormMatchFromQueue: {ex.Message}"); } try { if (selected != null && selected.Length > 0) PhotonRealtimeClient.CurrentRoom.SetCustomProperty("eu", selected); } catch (Exception ex) { Debug.LogWarning($"FormMatchFromQueue: {ex.Message}"); } try { PhotonRealtimeClient.CurrentRoom.SetCustomProperty(QueueFormedMatchKey, true); } catch (Exception ex) { Debug.LogWarning($"FormMatchFromQueue: {ex.Message}"); } + + // Preserve premade metadata when a premade duo came from queue to keep same-side reservation logic active. + try + { + HashSet matchUserIds = new(); + if (!string.IsNullOrEmpty(PhotonRealtimeClient.LocalPlayer?.UserId)) + { + matchUserIds.Add(PhotonRealtimeClient.LocalPlayer.UserId); + } + if (selected != null) + { + foreach (string uid in selected) + { + if (!string.IsNullOrEmpty(uid)) matchUserIds.Add(uid); + } + } + + bool queuePremadePairInThisMatch = queuePremadeMode + && !string.IsNullOrEmpty(queuePremadeUserId1) + && !string.IsNullOrEmpty(queuePremadeUserId2) + && matchUserIds.Contains(queuePremadeUserId1) + && matchUserIds.Contains(queuePremadeUserId2); + + bool localPremadePairInThisMatch = _isPremadeMatchmakingFlow + && !string.IsNullOrEmpty(_premadeTeammateUserId) + && !string.IsNullOrEmpty(PhotonRealtimeClient.LocalPlayer?.UserId) + && matchUserIds.Contains(PhotonRealtimeClient.LocalPlayer.UserId) + && matchUserIds.Contains(_premadeTeammateUserId); + + if (queuePremadePairInThisMatch || localPremadePairInThisMatch) + { + string premadeUserId1 = queuePremadePairInThisMatch ? queuePremadeUserId1 : PhotonRealtimeClient.LocalPlayer.UserId; + string premadeUserId2 = queuePremadePairInThisMatch ? queuePremadeUserId2 : _premadeTeammateUserId; + int premadeTargetGameType = queuePremadePairInThisMatch ? queuePremadeTargetGameType : roomGameTypeInt; + + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, premadeTargetGameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, PhotonRealtimeClient.LocalPlayer.UserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, premadeUserId1); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, premadeUserId2); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + + // Keep runtime fields in sync for any downstream logic that still checks them. + _isPremadeMatchmakingFlow = true; + if (premadeUserId1 == PhotonRealtimeClient.LocalPlayer.UserId) _premadeTeammateUserId = premadeUserId2; + else if (premadeUserId2 == PhotonRealtimeClient.LocalPlayer.UserId) _premadeTeammateUserId = premadeUserId1; + } + } + catch (Exception ex) { Debug.LogWarning($"FormMatchFromQueue: failed to set premade metadata: {ex.Message}"); } + if (PhotonRealtimeClient.LocalPlayer != null && PhotonRealtimeClient.LocalPlayer.IsMasterClient && _matchmakingHolder == null) { _matchmakingHolder = StartCoroutine(WaitForMatchmakingPlayers()); @@ -405,14 +539,134 @@ private bool SafeRaiseEvent(byte eventCode, object content, RaiseEventArgs raise return false; } + private bool CanMutateRoomPropertiesNow(string context = null, bool logWhenNotReady = false) + { + var client = PhotonRealtimeClient.Client; + bool ready = client != null + && client.Server == ServerConnection.GameServer + && client.IsConnectedAndReady + && PhotonRealtimeClient.InRoom + && PhotonRealtimeClient.CurrentRoom != null + && client.State != ClientState.Leaving + && client.State != ClientState.DisconnectingFromGameServer + && client.State != ClientState.DisconnectingFromMasterServer; + + if (!ready && logWhenNotReady) + { + string source = string.IsNullOrEmpty(context) ? "RoomProperties" : context; + Debug.LogWarning($"{source}: skipping room property write - client not ready (State={client?.State}, Server={client?.Server}, Ready={client?.IsConnectedAndReady}, InRoom={PhotonRealtimeClient.InRoom})"); + } + + return ready; + } + + private int GetFirstFreePositionWithoutVerification() + { + if (PhotonRealtimeClient.CurrentRoom == null) return PlayerPositionGuest; + + int maxPlayers = PhotonRealtimeClient.CurrentRoom.MaxPlayers; + int[] candidatePositions = maxPlayers >= 4 + ? new[] { PhotonBattleRoom.PlayerPosition1, PhotonBattleRoom.PlayerPosition2, PhotonBattleRoom.PlayerPosition3, PhotonBattleRoom.PlayerPosition4 } + : new[] { PhotonBattleRoom.PlayerPosition1, PhotonBattleRoom.PlayerPosition2 }; + + foreach (int position in candidatePositions) + { + string value = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.GetPositionKey(position), string.Empty); + if (string.IsNullOrEmpty(value)) return position; + } + + return PlayerPositionGuest; + } + + private void ClearStaleHumanPositionReservations(string context) + { + if (!CanMutateRoomPropertiesNow()) return; + + try + { + Room room = PhotonRealtimeClient.CurrentRoom; + if (room == null) return; + + var existingUserIds = new HashSet(room.Players.Values.Select(p => p.UserId)); + string[] posKeys = { + PhotonBattleRoom.PlayerPositionKey1, + PhotonBattleRoom.PlayerPositionKey2, + PhotonBattleRoom.PlayerPositionKey3, + PhotonBattleRoom.PlayerPositionKey4 + }; + + foreach (string key in posKeys) + { + string val = room.GetCustomProperty(key, string.Empty); + if (string.IsNullOrEmpty(val) || val == "Bot") continue; + if (existingUserIds.Contains(val)) continue; + + var emptyPosition = new LobbyPhotonHashtable(new Dictionary { { key, "" } }); + var expectedValue = new LobbyPhotonHashtable(new Dictionary { { key, val } }); + if (PhotonRealtimeClient.LobbyCurrentRoom != null && PhotonRealtimeClient.LobbyCurrentRoom.SetCustomProperties(emptyPosition, expectedValue)) + { + Debug.Log($"{context}: cleared stale position {key} (value {val})."); + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"{context}: failed to clear stale reservations: {ex.Message}"); + } + } + // AutoJoinLargestMatchmakingRoom removed: client-side opportunistic joining // is now fully deprecated in favor of centralized queue-based matchmaking. // Requeue the local player into the persistent queue room for the given game type. - private IEnumerator RequeueToPersistentQueue(GameType gameType) + private IEnumerator RequeueToPersistentQueue(GameType gameType, bool premadeMode = false, string premadeUserId1 = "", string premadeUserId2 = "", int premadeTargetGameType = -1) { try { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId ?? string.Empty; + if (premadeTargetGameType < 0) premadeTargetGameType = (int)gameType; + + // Snapshot premade info before leaving room so non-master requeues can preserve same-side constraints. + try + { + if ((!premadeMode || string.IsNullOrEmpty(premadeUserId1) || string.IsNullOrEmpty(premadeUserId2)) && PhotonRealtimeClient.CurrentRoom != null) + { + bool roomPremadeMode = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeModeKey, false); + string roomPremadeUserId1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, string.Empty); + string roomPremadeUserId2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + int roomPremadeTargetGameType = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)gameType); + + if (roomPremadeMode && !string.IsNullOrEmpty(roomPremadeUserId1) && !string.IsNullOrEmpty(roomPremadeUserId2)) + { + premadeMode = true; + premadeUserId1 = roomPremadeUserId1; + premadeUserId2 = roomPremadeUserId2; + premadeTargetGameType = roomPremadeTargetGameType; + } + } + } + catch (Exception ex) { Debug.LogWarning($"RequeueToPersistentQueue: failed to snapshot room premade metadata: {ex.Message}"); } + + if ((!premadeMode || string.IsNullOrEmpty(premadeUserId1) || string.IsNullOrEmpty(premadeUserId2)) + && _isPremadeMatchmakingFlow + && !string.IsNullOrEmpty(localUserId) + && !string.IsNullOrEmpty(_premadeTeammateUserId)) + { + premadeMode = true; + premadeUserId1 = localUserId; + premadeUserId2 = _premadeTeammateUserId; + premadeTargetGameType = (int)gameType; + } + + if (premadeMode) + { + bool localInPair = !string.IsNullOrEmpty(localUserId) && (localUserId == premadeUserId1 || localUserId == premadeUserId2); + if (!localInPair || string.IsNullOrEmpty(premadeUserId1) || string.IsNullOrEmpty(premadeUserId2) || premadeUserId1 == premadeUserId2) + { + premadeMode = false; + } + } + Debug.Log($"RequeueToPersistentQueue: rejoining persistent queue for {gameType}"); try { StopMatchmakingCoroutines(); } catch (Exception ex) { Debug.LogWarning($"RequeueToPersistentQueue: failed to stop matchmaking coroutines: {ex.Message}"); } try { StopHolderCoroutines(); } catch (Exception ex) { Debug.LogWarning($"RequeueToPersistentQueue: failed to stop holder coroutines: {ex.Message}"); } @@ -441,6 +695,42 @@ private IEnumerator RequeueToPersistentQueue(GameType gameType) Debug.LogWarning("RequeueToPersistentQueue: JoinOrCreateQueueRoom failed; attempting server-side fallback JoinOrCreateMatchmakingRoom."); try { PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(gameType, _teammates); } catch (Exception ex) { Debug.LogWarning($"RequeueToPersistentQueue: JoinOrCreateMatchmakingRoom failed: {ex.Message}"); } } + else + { + float joinStart = Time.time; + while (!PhotonRealtimeClient.InRoom && Time.time - joinStart < 6f) + { + yield return null; + } + + if (premadeMode && PhotonRealtimeClient.InRoom && PhotonRealtimeClient.CurrentRoom != null) + { + bool isQueueRoom = false; + try { isQueueRoom = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.IsQueueKey, false); } catch { } + if (isQueueRoom) + { + try + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, premadeTargetGameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, premadeUserId1); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, premadeUserId2); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + + _isPremadeMatchmakingFlow = true; + if (localUserId == premadeUserId1) _premadeTeammateUserId = premadeUserId2; + else if (localUserId == premadeUserId2) _premadeTeammateUserId = premadeUserId1; + + Debug.Log($"RequeueToPersistentQueue: preserved premade metadata in queue room ({premadeUserId1},{premadeUserId2})."); + } + catch (Exception ex) + { + Debug.LogWarning($"RequeueToPersistentQueue: failed to preserve premade metadata: {ex.Message}"); + } + } + } + } } else { @@ -564,6 +854,8 @@ private void StopHolderCoroutines() StopCoroutine(_matchmakingHolder); _matchmakingHolder = null; _teammates = null; + _premadeTeammateUserId = string.Empty; + _isPremadeMatchmakingFlow = false; } if (_autoJoinHolder != null) @@ -583,6 +875,45 @@ private IEnumerator LeaveAndAutoRequeue(GameType gameType) { try { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId ?? string.Empty; + bool requeuePremadeMode = false; + string requeuePremadeUserId1 = string.Empty; + string requeuePremadeUserId2 = string.Empty; + int requeuePremadeTargetGameType = (int)gameType; + + // Capture premade metadata before stopping coroutines/leave so requeue preserves same-side pairing. + try + { + if (PhotonRealtimeClient.CurrentRoom != null) + { + requeuePremadeMode = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeModeKey, false); + requeuePremadeUserId1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, string.Empty); + requeuePremadeUserId2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + requeuePremadeTargetGameType = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)gameType); + } + } + catch (Exception ex) { Debug.LogWarning($"LeaveAndAutoRequeue: failed to read premade metadata from current room: {ex.Message}"); } + + if ((!requeuePremadeMode || string.IsNullOrEmpty(requeuePremadeUserId1) || string.IsNullOrEmpty(requeuePremadeUserId2)) + && _isPremadeMatchmakingFlow + && !string.IsNullOrEmpty(localUserId) + && !string.IsNullOrEmpty(_premadeTeammateUserId)) + { + requeuePremadeMode = true; + requeuePremadeUserId1 = localUserId; + requeuePremadeUserId2 = _premadeTeammateUserId; + requeuePremadeTargetGameType = (int)gameType; + } + + if (requeuePremadeMode) + { + bool localInPair = !string.IsNullOrEmpty(localUserId) && (localUserId == requeuePremadeUserId1 || localUserId == requeuePremadeUserId2); + if (!localInPair || string.IsNullOrEmpty(requeuePremadeUserId1) || string.IsNullOrEmpty(requeuePremadeUserId2) || requeuePremadeUserId1 == requeuePremadeUserId2) + { + requeuePremadeMode = false; + } + } + Debug.Log($"LeaveAndAutoRequeue: preparing to leave and requeue for {gameType}"); // Stop any existing matchmaking/holder coroutines to avoid conflicts @@ -602,6 +933,13 @@ private IEnumerator LeaveAndAutoRequeue(GameType gameType) // If local player is master, attempt to start matchmaking flow (master may already have started it) if (PhotonRealtimeClient.LocalPlayer.IsMasterClient) { + if (requeuePremadeMode) + { + _isPremadeMatchmakingFlow = true; + if (localUserId == requeuePremadeUserId1) _premadeTeammateUserId = requeuePremadeUserId2; + else if (localUserId == requeuePremadeUserId2) _premadeTeammateUserId = requeuePremadeUserId1; + } + if (_matchmakingHolder != null) { StopCoroutine(_matchmakingHolder); @@ -620,7 +958,7 @@ private IEnumerator LeaveAndAutoRequeue(GameType gameType) StopCoroutine(_autoJoinHolder); _autoJoinHolder = null; } - _autoJoinHolder = StartCoroutine(RequeueToPersistentQueue(gameType)); + _autoJoinHolder = StartCoroutine(RequeueToPersistentQueue(gameType, requeuePremadeMode, requeuePremadeUserId1, requeuePremadeUserId2, requeuePremadeTargetGameType)); } else { @@ -829,6 +1167,8 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange bool keepHolder = false; try { + string localUserId = PhotonRealtimeClient.LocalPlayer.UserId; + // Closing the room so that no others can join PhotonRealtimeClient.CurrentRoom.IsOpen = false; @@ -858,6 +1198,7 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange } } _teammates = expectedUsers.ToArray(); + _premadeTeammateUserId = _isPremadeMatchmakingFlow && _teammates.Length > 0 ? _teammates[0] : string.Empty; // Sending other players in the room the room change request, setting own leader id key as own userid to indicate being the leader PhotonRealtimeClient.LocalPlayer.SetCustomProperty(PhotonBattleRoom.LeaderIdKey, PhotonRealtimeClient.LocalPlayer.UserId); @@ -865,9 +1206,15 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange if (broadcastRoomChange && PhotonRealtimeClient.Client != null && PhotonRealtimeClient.Client.Server == ServerConnection.GameServer && PhotonRealtimeClient.Client.IsConnectedAndReady && PhotonRealtimeClient.InRoom) { + object roomChangePayload = PhotonRealtimeClient.LocalPlayer.UserId; + if (_isPremadeMatchmakingFlow) + { + roomChangePayload = new object[] { localUserId, _teammates, $"Queue_{gameType}" }; + } + SafeRaiseEvent( PhotonRealtimeClient.PhotonEvent.RoomChangeRequested, - PhotonRealtimeClient.LocalPlayer.UserId, + roomChangePayload, new RaiseEventArgs { Receivers = ReceiverGroup.Others }, SendOptions.SendReliable ); @@ -884,6 +1231,90 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange // Wait for lobby and initial room listing; room search below depends on CurrentRooms. yield return new WaitUntil(() => PhotonRealtimeClient.InLobby && CurrentRooms != null); + // In premade InRoom flow we must join queue first, not a matchmaking room directly. + if (_isPremadeMatchmakingFlow) + { + bool queueJoinRequested = false; + try + { + queueJoinRequested = PhotonRealtimeClient.JoinOrCreateQueueRoom(gameType); + } + catch (Exception ex) + { + Debug.LogWarning($"StartMatchmaking: JoinOrCreateQueueRoom failed for premade flow: {ex.Message}"); + } + + if (queueJoinRequested) + { + float queueJoinStart = Time.time; + yield return new WaitUntil(() => PhotonRealtimeClient.InRoom || Time.time > queueJoinStart + 8f); + + bool joinedQueueRoom = false; + string queueRoomName = string.Empty; + try + { + joinedQueueRoom = PhotonRealtimeClient.InRoom + && PhotonRealtimeClient.CurrentRoom != null + && PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.IsQueueKey); + if (joinedQueueRoom) + { + queueRoomName = PhotonRealtimeClient.CurrentRoom.Name; + } + } + catch (Exception ex) + { + Debug.LogWarning($"StartMatchmaking: failed to verify queue room join: {ex.Message}"); + } + + if (joinedQueueRoom) + { + try + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)gameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, _premadeTeammateUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty("qe", _teammates?.Length ?? 0); + if (_teammates != null && _teammates.Length > 0) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty("eu", _teammates); + } + } + catch (Exception ex) + { + Debug.LogWarning($"StartMatchmaking: failed to stamp premade metadata to queue room: {ex.Message}"); + } + + if (broadcastRoomChange && !string.IsNullOrEmpty(queueRoomName)) + { + SafeRaiseEvent( + PhotonRealtimeClient.PhotonEvent.RoomChangeRequested, + new object[] { localUserId, _teammates, queueRoomName }, + new RaiseEventArgs { Receivers = ReceiverGroup.Others }, + SendOptions.SendReliable + ); + } + + // Queue room handling takes over from OnJoinedRoom (queue timer / queue master logic). + yield break; + } + + Debug.LogWarning("StartMatchmaking: premade queue join did not land in a queue room, falling back to direct matchmaking flow."); + } + else + { + Debug.LogWarning("StartMatchmaking: premade queue join request failed, falling back to direct matchmaking flow."); + } + + // Ensure fallback path starts from lobby if queue join attempt left us in a room. + if (PhotonRealtimeClient.InRoom) + { + PhotonRealtimeClient.LeaveRoom(); + yield return new WaitUntil(() => PhotonRealtimeClient.InLobby); + } + } + // Searching for suitable room and attempting to join each candidate. // If a JoinRoom attempt fails (room filled/closed) we continue searching other rooms. bool roomFound = false; @@ -923,7 +1354,7 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange case GameType.Random2v2: if (room.MaxPlayers - room.PlayerCount >= _teammates.Length + 1) { - shouldTryJoin = true; + shouldTryJoin = !_isPremadeMatchmakingFlow || RoomHasSameSideCapacityForPremade(room); } break; } @@ -956,10 +1387,24 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange switch (gameType) { case GameType.Clan2v2: - PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Clan2v2, _teammates, clanName, soulhomeRank); + if (_isPremadeMatchmakingFlow) + { + PhotonRealtimeClient.JoinRandomOrCreateClan2v2Room(clanName, soulhomeRank, _teammates, true); + } + else + { + PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Clan2v2, _teammates, clanName, soulhomeRank); + } break; case GameType.Random2v2: - PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, _teammates); + if (_isPremadeMatchmakingFlow) + { + PhotonRealtimeClient.JoinRandomOrCreateRandom2v2Room(_teammates, true); + } + else + { + PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, _teammates); + } break; } } @@ -967,6 +1412,23 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange // Block until our matchmaking-room join completes. yield return new WaitUntil(() => PhotonRealtimeClient.InRoom); + if (_isPremadeMatchmakingFlow && !string.IsNullOrEmpty(_premadeTeammateUserId)) + { + try + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)gameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, _premadeTeammateUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + } + catch (Exception ex) + { + Debug.LogWarning($"StartMatchmaking: failed to set premade matchmaking room properties: {ex.Message}"); + } + } + // Reconcile split groups caused by near-simultaneous room creation. // Strategy: deterministically converge toward one shared room to improve fill/start speed. try @@ -1092,6 +1554,15 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange { StartCoroutine(ReserveFreePosition()); } + else if (_isPremadeMatchmakingFlow && !string.IsNullOrEmpty(_premadeTeammateUserId)) + { + if (!TryReservePremadePairToSameSide(localUserId, _premadeTeammateUserId, out _)) + { + Debug.LogWarning("StartMatchmaking: no same-side capacity for premade duo, requeueing."); + StartCoroutine(LeaveAndAutoRequeue(gameType)); + yield break; + } + } else // Queuing with a teammate TODO: untested code, when queueing with teammate is possible test this and fix any issues { // Checking if position is free and if so setting userid from old room to that position @@ -1132,17 +1603,209 @@ private IEnumerator StartMatchmaking(GameType gameType, bool broadcastRoomChange PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PlayerPositionKey4, positionValue4); } - // Stopping coroutine if not a master client - if (!PhotonRealtimeClient.LocalPlayer.IsMasterClient) yield break; + // Stopping coroutine if not a master client + if (!PhotonRealtimeClient.LocalPlayer.IsMasterClient) yield break; + + _matchmakingHolder = StartCoroutine(WaitForMatchmakingPlayers()); + keepHolder = true; + } + finally + { + if (!keepHolder) _matchmakingHolder = null; + } + + } + + private bool RoomHasSameSideCapacityForPremade(LobbyRoomInfo room) + { + if (room == null || room.CustomProperties == null) return false; + + string positionValue1 = room.CustomProperties.ContainsKey(PhotonBattleRoom.PlayerPositionKey1) + ? room.CustomProperties[PhotonBattleRoom.PlayerPositionKey1]?.ToString() + : string.Empty; + string positionValue2 = room.CustomProperties.ContainsKey(PhotonBattleRoom.PlayerPositionKey2) + ? room.CustomProperties[PhotonBattleRoom.PlayerPositionKey2]?.ToString() + : string.Empty; + string positionValue3 = room.CustomProperties.ContainsKey(PhotonBattleRoom.PlayerPositionKey3) + ? room.CustomProperties[PhotonBattleRoom.PlayerPositionKey3]?.ToString() + : string.Empty; + string positionValue4 = room.CustomProperties.ContainsKey(PhotonBattleRoom.PlayerPositionKey4) + ? room.CustomProperties[PhotonBattleRoom.PlayerPositionKey4]?.ToString() + : string.Empty; + + int alphaFree = (string.IsNullOrEmpty(positionValue1) ? 1 : 0) + (string.IsNullOrEmpty(positionValue2) ? 1 : 0); + int betaFree = (string.IsNullOrEmpty(positionValue3) ? 1 : 0) + (string.IsNullOrEmpty(positionValue4) ? 1 : 0); + return alphaFree >= 2 || betaFree >= 2; + } + + private bool TryReservePremadePairToSameSide(string userId1, string userId2, out int teamNumber) + { + teamNumber = PhotonBattleRoom.NoTeamValue; + if (string.IsNullOrEmpty(userId1) || string.IsNullOrEmpty(userId2) || userId1 == userId2) return false; + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) return false; + if (!CanMutateRoomPropertiesNow()) return false; + + Dictionary currentMap = new() + { + { PhotonBattleRoom.PlayerPosition1, PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey1, string.Empty) }, + { PhotonBattleRoom.PlayerPosition2, PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey2, string.Empty) }, + { PhotonBattleRoom.PlayerPosition3, PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey3, string.Empty) }, + { PhotonBattleRoom.PlayerPosition4, PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey4, string.Empty) }, + }; + + bool IsBotValue(string value) => string.Equals(value, "Bot", StringComparison.Ordinal); + bool IsRealPlayerValue(string value) => !string.IsNullOrEmpty(value) && !IsBotValue(value); + + int FindPosition(Dictionary map, string userId) + { + foreach (var kvp in map) + { + if (kvp.Value == userId) return kvp.Key; + } + return PlayerPositionGuest; + } + + int GetTeamFromPosition(int position) + { + if (position == PhotonBattleRoom.PlayerPosition1 || position == PhotonBattleRoom.PlayerPosition2) return PhotonBattleRoom.TeamAlphaValue; + if (position == PhotonBattleRoom.PlayerPosition3 || position == PhotonBattleRoom.PlayerPosition4) return PhotonBattleRoom.TeamBetaValue; + return PhotonBattleRoom.NoTeamValue; + } + + bool MapsEqual(Dictionary first, Dictionary second) + { + foreach (int key in first.Keys) + { + string firstValue = first[key] ?? string.Empty; + string secondValue = second[key] ?? string.Empty; + if (!string.Equals(firstValue, secondValue, StringComparison.Ordinal)) return false; + } + return true; + } + + bool TryBuildTeamMap(Dictionary sourceMap, int targetTeamNumber, out Dictionary resultMap, out int displacedBots) + { + resultMap = null; + displacedBots = int.MaxValue; + + int[] teamSlots = targetTeamNumber == PhotonBattleRoom.TeamAlphaValue + ? new[] { PhotonBattleRoom.PlayerPosition1, PhotonBattleRoom.PlayerPosition2 } + : new[] { PhotonBattleRoom.PlayerPosition3, PhotonBattleRoom.PlayerPosition4 }; + + int[] otherSlots = targetTeamNumber == PhotonBattleRoom.TeamAlphaValue + ? new[] { PhotonBattleRoom.PlayerPosition3, PhotonBattleRoom.PlayerPosition4 } + : new[] { PhotonBattleRoom.PlayerPosition1, PhotonBattleRoom.PlayerPosition2 }; + + // Never displace non-premade humans. + if (teamSlots.Any(slot => IsRealPlayerValue(sourceMap[slot]))) return false; + + Dictionary workingMap = new(sourceMap); + displacedBots = teamSlots.Count(slot => IsBotValue(workingMap[slot])); + + workingMap[teamSlots[0]] = userId1; + workingMap[teamSlots[1]] = userId2; + + if (displacedBots > 0) + { + List freeOtherSlots = new(); + foreach (int slot in otherSlots) + { + if (string.IsNullOrEmpty(workingMap[slot])) freeOtherSlots.Add(slot); + } + + if (freeOtherSlots.Count < displacedBots) return false; + + for (int i = 0; i < displacedBots; i++) + { + workingMap[freeOtherSlots[i]] = "Bot"; + } + } + + resultMap = workingMap; + return true; + } + + int userId1Position = FindPosition(currentMap, userId1); + int userId2Position = FindPosition(currentMap, userId2); + int userId1Team = GetTeamFromPosition(userId1Position); + int userId2Team = GetTeamFromPosition(userId2Position); + + // Already on same team: don't churn room properties. + if (userId1Team != PhotonBattleRoom.NoTeamValue && userId1Team == userId2Team) + { + teamNumber = userId1Team; + return true; + } + + Dictionary baseMap = new(currentMap); + foreach (int key in baseMap.Keys.ToArray()) + { + if (baseMap[key] == userId1 || baseMap[key] == userId2) + { + baseMap[key] = string.Empty; + } + } + + bool alphaValid = TryBuildTeamMap(baseMap, PhotonBattleRoom.TeamAlphaValue, out Dictionary alphaMap, out int alphaDisplacedBots); + bool betaValid = TryBuildTeamMap(baseMap, PhotonBattleRoom.TeamBetaValue, out Dictionary betaMap, out int betaDisplacedBots); + + if (!alphaValid && !betaValid) return false; + + int alphaAffinity = (userId1Team == PhotonBattleRoom.TeamAlphaValue ? 1 : 0) + (userId2Team == PhotonBattleRoom.TeamAlphaValue ? 1 : 0); + int betaAffinity = (userId1Team == PhotonBattleRoom.TeamBetaValue ? 1 : 0) + (userId2Team == PhotonBattleRoom.TeamBetaValue ? 1 : 0); + + Dictionary targetMap; + if (alphaValid && betaValid) + { + if (alphaAffinity > betaAffinity) + { + teamNumber = PhotonBattleRoom.TeamAlphaValue; + targetMap = alphaMap; + } + else if (betaAffinity > alphaAffinity) + { + teamNumber = PhotonBattleRoom.TeamBetaValue; + targetMap = betaMap; + } + else if (alphaDisplacedBots < betaDisplacedBots) + { + teamNumber = PhotonBattleRoom.TeamAlphaValue; + targetMap = alphaMap; + } + else if (betaDisplacedBots < alphaDisplacedBots) + { + teamNumber = PhotonBattleRoom.TeamBetaValue; + targetMap = betaMap; + } + else + { + teamNumber = PhotonBattleRoom.TeamAlphaValue; + targetMap = alphaMap; + } + } + else if (alphaValid) + { + teamNumber = PhotonBattleRoom.TeamAlphaValue; + targetMap = alphaMap; + } + else + { + teamNumber = PhotonBattleRoom.TeamBetaValue; + targetMap = betaMap; + } + + if (MapsEqual(currentMap, targetMap)) return true; - _matchmakingHolder = StartCoroutine(WaitForMatchmakingPlayers()); - keepHolder = true; - } - finally - { - if (!keepHolder) _matchmakingHolder = null; - } + foreach (int key in targetMap.Keys) + { + string currentValue = currentMap[key] ?? string.Empty; + string targetValue = targetMap[key] ?? string.Empty; + if (string.Equals(currentValue, targetValue, StringComparison.Ordinal)) continue; + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.GetPositionKey(key), targetValue); + } + + return true; } /// @@ -1330,6 +1993,39 @@ private IEnumerator WaitForMatchmakingPlayers() // instead of waiting the full matchmaking timeout. if (!botBackfillApplied && currentGameType == GameType.Random2v2) { + ClearStaleHumanPositionReservations("WaitForMatchmakingPlayers"); + + bool expectedFollowersPresent = expectedUsers != null && expectedUsers.Length > 0 && !expectedUsersMissing; + if (expectedFollowersPresent) + { + bool shouldYieldAfterPremadeReservation = false; + try + { + bool premadeMode = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeModeKey, false); + string earlyPremadeUserId1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, string.Empty); + string earlyPremadeUserId2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + + if (premadeMode && !string.IsNullOrEmpty(earlyPremadeUserId1) && !string.IsNullOrEmpty(earlyPremadeUserId2)) + { + if (!TryReservePremadePairToSameSide(earlyPremadeUserId1, earlyPremadeUserId2, out _)) + { + Debug.LogWarning($"WaitForMatchmakingPlayers: failed premade same-side reservation before botfill for ({earlyPremadeUserId1},{earlyPremadeUserId2}), requeueing."); + StartCoroutine(LeaveAndAutoRequeue(currentGameType)); + yield break; + } + + shouldYieldAfterPremadeReservation = true; + } + } + catch (Exception ex) { Debug.LogWarning($"WaitForMatchmakingPlayers: premade reservation before botfill failed: {ex.Message}"); } + + if (shouldYieldAfterPremadeReservation) + { + // Let room properties settle before filling remaining slots with bots. + yield return null; + } + } + bool appliedEarly = false; try { @@ -1424,6 +2120,28 @@ private IEnumerator WaitForMatchmakingPlayers() string positionValue3 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey3); string positionValue4 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey4); + bool isPremadeMatch = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeModeKey, false); + string premadeUserId1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, string.Empty); + string premadeUserId2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + if (isPremadeMatch && !string.IsNullOrEmpty(premadeUserId1) && !string.IsNullOrEmpty(premadeUserId2)) + { + if (!TryReservePremadePairToSameSide(premadeUserId1, premadeUserId2, out _)) + { + GameType requeueGameType = GameType.Random2v2; + try { requeueGameType = (GameType)PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.GameTypeKey); } catch { } + Debug.LogWarning($"WaitForMatchmakingPlayers: could not keep premade pair ({premadeUserId1},{premadeUserId2}) on same side, requeueing."); + StartCoroutine(LeaveAndAutoRequeue(requeueGameType)); + yield break; + } + + // Refresh position snapshot after forced pair reservation. + yield return null; + positionValue1 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey1); + positionValue2 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey2); + positionValue3 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey3); + positionValue4 = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey4); + } + foreach (var player in PhotonRealtimeClient.CurrentRoom.Players) { int position = PhotonBattleRoom.PlayerPositionGuest; @@ -1434,20 +2152,41 @@ private IEnumerator WaitForMatchmakingPlayers() else if (player.Value.UserId == positionValue4) position = PhotonBattleRoom.PlayerPosition4; else { - // If player isn't in any position, getting the first free player position. - // This method checks for duplicate and missing players - position = PhotonLobbyRoom.GetFirstFreePlayerPos(new(player.Value)); // TODO: if Clan2v2 ensure that player ends on the correct side + // Prefer player's existing position if it can be reclaimed from empty/Bot value. + int preferredPosition = player.Value.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey, PhotonBattleRoom.PlayerPositionGuest); + if (PhotonLobbyRoom.IsValidPlayerPos(preferredPosition)) + { + string preferredKey = PhotonBattleRoom.GetPositionKey(preferredPosition); + string preferredValue = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(preferredKey, string.Empty); + if (string.IsNullOrEmpty(preferredValue) || preferredValue == "Bot" || preferredValue == player.Value.UserId) + { + position = preferredPosition; + } + } + + // Fall back to side-effect-free room scanning to avoid VerifyPlayerPositions clearing bot slots. + if (!PhotonLobbyRoom.IsValidPlayerPos(position)) + { + position = GetFirstFreePositionWithoutVerification(); // TODO: if Clan2v2 ensure that player ends on the correct side + } + if (!PhotonLobbyRoom.IsValidPlayerPos(position)) continue; string positionKey = PhotonBattleRoom.GetPositionKey(position); // Setting position to room and waiting until it's synced - PhotonRealtimeClient.CurrentRoom.SetCustomProperty(positionKey, player.Value.UserId); - yield return new WaitUntil(() => PhotonRealtimeClient.CurrentRoom.GetCustomProperty(positionKey) == player.Value.UserId); + if (PhotonRealtimeClient.CurrentRoom.GetCustomProperty(positionKey, string.Empty) != player.Value.UserId) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(positionKey, player.Value.UserId); + yield return new WaitUntil(() => PhotonRealtimeClient.CurrentRoom.GetCustomProperty(positionKey) == player.Value.UserId); + } } // Setting position to player properties and waiting until it's synced - player.Value.SetCustomProperty(PhotonBattleRoom.PlayerPositionKey, position); - yield return new WaitUntil(() => player.Value.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey) == position); + if (player.Value.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey, PhotonBattleRoom.PlayerPositionGuest) != position) + { + player.Value.SetCustomProperty(PhotonBattleRoom.PlayerPositionKey, position); + yield return new WaitUntil(() => player.Value.GetCustomProperty(PhotonBattleRoom.PlayerPositionKey) == position); + } } // Checking that the clan names are in order @@ -1502,9 +2241,32 @@ private IEnumerator WaitForMatchmakingPlayers() yield return null; } - // Starting gameplay coroutine if all positions are filled (real players + bots), else we loop again + // If botfill is active, reconcile any still-empty slots to Bot before start check. + bool botFillActive = false; + try { botFillActive = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.BotFillKey, false); } + catch (Exception ex) { Debug.LogWarning($"WaitForMatchmakingPlayers: failed to read BotFillKey before start check: {ex.Message}"); } + + if (roomGameType == GameType.Random2v2 && botFillActive) + { + int[] positions = { + PhotonBattleRoom.PlayerPosition1, PhotonBattleRoom.PlayerPosition2, + PhotonBattleRoom.PlayerPosition3, PhotonBattleRoom.PlayerPosition4 + }; + foreach (int pos in positions) + { + if (PhotonBattleRoom.CheckIfPositionIsFree(pos)) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.GetPositionKey(pos), "Bot"); + } + } + } + + // Starting gameplay coroutine if all positions are filled (real players + bots), else we loop again. + // When botfill is active in Random2v2, allow start even if bot position replication is still catching up. int botCount = PhotonBattleRoom.GetBotCount(); - if (PhotonRealtimeClient.CurrentRoom.PlayerCount + botCount >= PhotonRealtimeClient.CurrentRoom.MaxPlayers) + bool roomIsFullWithBots = PhotonRealtimeClient.CurrentRoom.PlayerCount + botCount >= PhotonRealtimeClient.CurrentRoom.MaxPlayers; + bool canStartWithBotFill = roomGameType == GameType.Random2v2 && botFillActive; + if (roomIsFullWithBots || canStartWithBotFill) { if (_startGameHolder != null) { @@ -1581,6 +2343,8 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom { try { + bool queueRoomRequested = !string.IsNullOrEmpty(leaderRoomName) && leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal); + // Don't follow leader away from this room if we're in a Custom game. try { @@ -1620,11 +2384,83 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom newRoomJoined = true; } } + + // Queue room may not be visible immediately after leader leaves old room. + // Retry joins and then fall back to JoinOrCreateQueueRoom so follower reliably converges. + if (!newRoomJoined && queueRoomRequested && PhotonRealtimeClient.InLobby) + { + float queueRetryStart = Time.time; + while (!newRoomJoined && Time.time - queueRetryStart < 8f) + { + try + { + PhotonRealtimeClient.JoinRoom(leaderRoomName); + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: queue JoinRoom retry failed: {ex.Message}"); + } + + float retryJoinWaitStart = Time.time; + while (!PhotonRealtimeClient.InRoom && Time.time - retryJoinWaitStart < 2.5f) + { + yield return null; + } + + if (PhotonRealtimeClient.InRoom) + { + newRoomJoined = true; + break; + } + + yield return new WaitForSeconds(0.4f); + } + + if (!newRoomJoined) + { + GameType queueGameType = GameType.Random2v2; + try + { + if (leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal) + && Enum.TryParse(leaderRoomName.Substring("Queue_".Length), out GameType parsedQueueType)) + { + queueGameType = parsedQueueType; + } + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: queue game type parse failed: {ex.Message}"); + } + + bool joinedOrCreatedQueue = false; + try + { + joinedOrCreatedQueue = PhotonRealtimeClient.JoinOrCreateQueueRoom(queueGameType); + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: JoinOrCreateQueueRoom fallback failed: {ex.Message}"); + } + + if (joinedOrCreatedQueue) + { + float queueJoinStart = Time.time; + while (!PhotonRealtimeClient.InRoom && Time.time - queueJoinStart < 6f) + { + yield return null; + } + if (PhotonRealtimeClient.InRoom) + { + newRoomJoined = true; + } + } + } + } } // Try to find leader via friends list; fallback to joining the matchmaking room with most players int attempts = 0; - do + while (!newRoomJoined && attempts < 10 && !queueRoomRequested) { attempts++; _friendList = null; @@ -1633,7 +2469,15 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom !string.IsNullOrEmpty(leaderUserId) && PhotonRealtimeClient.LocalPlayer != null && leaderUserId != PhotonRealtimeClient.LocalPlayer.UserId) { PhotonRealtimeClient.Client.OpFindFriends(new string[1] { leaderUserId }); - yield return new WaitUntil(() => _friendList != null ); + float friendLookupStart = Time.time; + while (_friendList == null && Time.time - friendLookupStart < 4f) + { + yield return null; + } + if (_friendList == null) + { + _friendList = new List(); + } } else { @@ -1697,7 +2541,7 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom // Small delay to avoid tight loop; give state time to update yield return new WaitForSeconds(0.5f); - } while (!newRoomJoined && attempts < 10); + } // If we couldn't join the leader's room or any candidate, create/join a new matchmaking room bool attemptedFollowJoinCreate = false; @@ -1705,17 +2549,33 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom { if (!newRoomJoined && PhotonRealtimeClient.InLobby) { - Debug.Log("FollowLeaderToNewRoom: failed to join leader room; attempting server-side JoinOrCreate matchmaking room."); - try + if (queueRoomRequested) { - Debug.Log($"FollowLeaderToNewRoom: calling JoinOrCreateMatchmakingRoom, teammates count={_teammates?.Length ?? 0}"); - PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, _teammates); - Debug.Log("FollowLeaderToNewRoom: JoinOrCreateMatchmakingRoom call returned; awaiting join result..."); + GameType queueGameType = GameType.Random2v2; + if (!string.IsNullOrEmpty(leaderRoomName) + && leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal) + && Enum.TryParse(leaderRoomName.Substring("Queue_".Length), out GameType parsedQueueType)) + { + queueGameType = parsedQueueType; + } + Debug.Log($"FollowLeaderToNewRoom: failed initial queue join, using JoinOrCreateQueueRoom for {queueGameType}."); + PhotonRealtimeClient.JoinOrCreateQueueRoom(queueGameType); } - catch (Exception ex) + else { - Debug.LogWarning($"FollowLeaderToNewRoom: JoinOrCreateMatchmakingRoom failed: {ex.Message}"); + Debug.Log("FollowLeaderToNewRoom: failed to join leader room; attempting server-side JoinOrCreate matchmaking room."); + try + { + Debug.Log($"FollowLeaderToNewRoom: calling JoinOrCreateMatchmakingRoom, teammates count={_teammates?.Length ?? 0}"); + PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, _teammates); + Debug.Log("FollowLeaderToNewRoom: JoinOrCreateMatchmakingRoom call returned; awaiting join result..."); + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: JoinOrCreateMatchmakingRoom failed: {ex.Message}"); + } } + attemptedFollowJoinCreate = true; } } @@ -1740,13 +2600,33 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom if (PhotonRealtimeClient.Client != null && PhotonRealtimeClient.Client.Server == ServerConnection.MasterServer && PhotonRealtimeClient.InLobby) { - try + if (queueRoomRequested && !string.IsNullOrEmpty(leaderRoomName)) { - PhotonRealtimeClient.JoinRandomOrCreateRandom2v2Room(_teammates, true); + try + { + GameType queueGameType = GameType.Random2v2; + if (leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal) + && Enum.TryParse(leaderRoomName.Substring("Queue_".Length), out GameType parsedQueueType)) + { + queueGameType = parsedQueueType; + } + PhotonRealtimeClient.JoinOrCreateQueueRoom(queueGameType); + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: second queue JoinOrCreate failed: {ex.Message}"); + } } - catch (Exception ex) + else { - Debug.LogWarning($"FollowLeaderToNewRoom: second JoinRandomOrCreate failed: {ex.Message}"); + try + { + PhotonRealtimeClient.JoinRandomOrCreateRandom2v2Room(_teammates, true); + } + catch (Exception ex) + { + Debug.LogWarning($"FollowLeaderToNewRoom: second JoinRandomOrCreate failed: {ex.Message}"); + } } float joinStart2 = Time.time; @@ -1862,6 +2742,26 @@ private void OnStartMatchmakingEvent(StartMatchmakingEvent data) if (!PhotonRealtimeClient.InRoom) return; + _isPremadeMatchmakingFlow = data.IsPremadeInRoom; + if (!_isPremadeMatchmakingFlow) + { + _premadeTeammateUserId = string.Empty; + } + + if (_isPremadeMatchmakingFlow && PhotonRealtimeClient.CurrentRoom != null) + { + try + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)data.SelectedGameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, PhotonRealtimeClient.LocalPlayer.UserId); + } + catch (Exception ex) + { + Debug.LogWarning($"OnStartMatchmakingEvent: failed to set premade properties: {ex.Message}"); + } + } + // Starting matchmaking coroutine if (_matchmakingHolder == null) { @@ -1872,6 +2772,8 @@ private void OnStartMatchmakingEvent(StartMatchmakingEvent data) private void OnStopMatchmakingEvent(StopMatchmakingEvent data) { Debug.Log($"onEvent {data}"); + _isPremadeMatchmakingFlow = false; + _premadeTeammateUserId = string.Empty; try { // If we're in a persistent queue room and the local player is the master, @@ -3001,36 +3903,44 @@ private IEnumerator VerifyRoomPositionsLoop() { while (PhotonRealtimeClient.InRoom && PhotonRealtimeClient.LocalPlayer.IsMasterClient) { + bool waitShortAndContinue = false; try { - Room room = PhotonRealtimeClient.CurrentRoom; - if (room != null) + if (!CanMutateRoomPropertiesNow()) { - var existingUserIds = new HashSet(room.Players.Values.Select(p => p.UserId)); - string[] posKeys = { - PhotonBattleRoom.PlayerPositionKey1, - PhotonBattleRoom.PlayerPositionKey2, - PhotonBattleRoom.PlayerPositionKey3, - PhotonBattleRoom.PlayerPositionKey4 - }; - - foreach (var key in posKeys) + waitShortAndContinue = true; + } + else + { + Room room = PhotonRealtimeClient.CurrentRoom; + if (room != null) { - string val = room.GetCustomProperty(key, ""); - if (string.IsNullOrEmpty(val)) continue; - if (val == "Bot") continue; - if (!existingUserIds.Contains(val)) + var existingUserIds = new HashSet(room.Players.Values.Select(p => p.UserId)); + string[] posKeys = { + PhotonBattleRoom.PlayerPositionKey1, + PhotonBattleRoom.PlayerPositionKey2, + PhotonBattleRoom.PlayerPositionKey3, + PhotonBattleRoom.PlayerPositionKey4 + }; + + foreach (var key in posKeys) { - var emptyPosition = new LobbyPhotonHashtable(new Dictionary { { key, "" } }); - var expectedValue = new LobbyPhotonHashtable(new Dictionary { { key, val } }); - try - { - PhotonRealtimeClient.LobbyCurrentRoom.SetCustomProperties(emptyPosition, expectedValue); - Debug.Log($"VerifyRoomPositionsLoop: cleared stale position {key} (value {val})."); - } - catch (Exception ex) + string val = room.GetCustomProperty(key, ""); + if (string.IsNullOrEmpty(val)) continue; + if (val == "Bot") continue; + if (!existingUserIds.Contains(val)) { - Debug.LogWarning($"VerifyRoomPositionsLoop: failed to clear {key}: {ex.Message}"); + var emptyPosition = new LobbyPhotonHashtable(new Dictionary { { key, "" } }); + var expectedValue = new LobbyPhotonHashtable(new Dictionary { { key, val } }); + try + { + PhotonRealtimeClient.LobbyCurrentRoom.SetCustomProperties(emptyPosition, expectedValue); + Debug.Log($"VerifyRoomPositionsLoop: cleared stale position {key} (value {val})."); + } + catch (Exception ex) + { + Debug.LogWarning($"VerifyRoomPositionsLoop: failed to clear {key}: {ex.Message}"); + } } } } @@ -3040,6 +3950,13 @@ private IEnumerator VerifyRoomPositionsLoop() { Debug.LogWarning($"VerifyRoomPositionsLoop: unexpected error: {ex.Message}"); } + + if (waitShortAndContinue) + { + yield return new WaitForSeconds(0.5f); + continue; + } + yield return new WaitForSeconds(2f); } } @@ -3299,7 +4216,10 @@ public void OnPlayerLeftRoom(Player otherPlayer) var emptyPosition = new LobbyPhotonHashtable(new Dictionary { { positionKey, "" } }); var expectedValue = new LobbyPhotonHashtable(new Dictionary { { positionKey, otherPlayer.UserId } }); - PhotonRealtimeClient.LobbyCurrentRoom.SetCustomProperties(emptyPosition, expectedValue); + if (CanMutateRoomPropertiesNow("OnPlayerLeftRoom: clear leaving player slot", true) && PhotonRealtimeClient.LobbyCurrentRoom != null) + { + PhotonRealtimeClient.LobbyCurrentRoom.SetCustomProperties(emptyPosition, expectedValue); + } if(_posChangeQueue.Contains(otherPlayer.UserId)) _posChangeQueue.Remove(otherPlayer.UserId); } @@ -3368,7 +4288,7 @@ public void OnPlayerLeftRoom(Player otherPlayer) { try { - if (room != null) + if (room != null && CanMutateRoomPropertiesNow("OnPlayerLeftRoom: clear stale positions", true)) { var existingUserIds = new HashSet(room.Players.Values.Select(p => p.UserId)); string[] posKeys = { @@ -3457,6 +4377,9 @@ public void OnPlayerLeftRoom(Player otherPlayer) public void OnJoinedRoom() { _gamePlayedOut = false; + _pendingInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteStartTime = -100f; // Enable: PhotonNetwork.CloseConnection needs to to work across all clients - to kick off invalid players! PhotonRealtimeClient.EnableCloseConnection = true; @@ -3485,6 +4408,22 @@ public void OnJoinedRoom() } catch { } + try + { + if (PhotonRealtimeClient.CurrentRoom != null && PhotonRealtimeClient.CurrentRoom.CustomProperties != null + && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.GameTypeKey) + && (GameType)PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.GameTypeKey) == GameType.InRoom_) + { + string invitedUserId = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, string.Empty); + if (!string.IsNullOrEmpty(invitedUserId) && invitedUserId == PhotonRealtimeClient.LocalPlayer.UserId) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, invitedUserId); + } + } + } + catch (Exception ex) { Debug.LogWarning($"OnJoinedRoom: failed to update InRoom_ invite state: {ex.Message}"); } + if (PhotonRealtimeClient.InMatchmakingRoom) { // If we previously got a CancelGameStart and now rejoined matchmaking, leave and return to main menu @@ -3585,6 +4524,9 @@ public void OnJoinedRoom() public void OnLeftRoom() // IMatchmakingCallbacks { _gamePlayedOut = false; + _pendingInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteStartTime = -100f; // Clearing player position key from own custom properties if (PhotonRealtimeClient.LocalPlayer.HasCustomProperty(PlayerPositionKey)) PhotonRealtimeClient.LocalPlayer.RemoveCustomProperty(PlayerPositionKey); @@ -3616,6 +4558,13 @@ public void OnLeftRoom() // IMatchmakingCallbacks StopCoroutine(_joinTimeoutWatcherHolder); _joinTimeoutWatcherHolder = null; } + + if (_matchmakingHolder == null && _followLeaderHolder == null) + { + _isPremadeMatchmakingFlow = false; + _premadeTeammateUserId = string.Empty; + } + LobbyOnLeftRoom?.Invoke(); // Goto lobby if we left (in)voluntarily any room // - typically master client kicked us off before starting a new game as we did not qualify to participate. @@ -3647,8 +4596,105 @@ public void OnRoomListUpdate(List roomList) { lobbyRoomList.Add(new(roomInfo)); } + + if (!string.IsNullOrEmpty(_pendingAcceptedInRoomInviteRoomName) + && !PhotonRealtimeClient.InRoom + && _pendingAcceptedInRoomInviteStartTime > 0f + && Time.time - _pendingAcceptedInRoomInviteStartTime > InRoomInviteJoinTimeoutSeconds) + { + string timedOutRoomName = _pendingAcceptedInRoomInviteRoomName; + _pendingAcceptedInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteStartTime = -100f; + Debug.LogWarning($"Accepted InRoom invite join timed out for room '{timedOutRoomName}'."); + OnInRoomInviteJoinFailed?.Invoke(timedOutRoomName, -1, "timeout"); + } + LobbyOnRoomListUpdate?.Invoke(lobbyRoomList); + TryAutoJoinInRoomInvite(lobbyRoomList); + } + + private void TryAutoJoinInRoomInvite(List roomList) + { + if (roomList == null || roomList.Count == 0) return; + if (!PhotonRealtimeClient.InLobby || PhotonRealtimeClient.InRoom) return; + + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId; + if (string.IsNullOrEmpty(localUserId)) return; + + if (!string.IsNullOrEmpty(_pendingInRoomInviteRoomName) + && roomList.All(room => room == null || room.RemovedFromList || room.Name != _pendingInRoomInviteRoomName)) + { + _pendingInRoomInviteRoomName = string.Empty; + } + + foreach (LobbyRoomInfo room in roomList) + { + if (room == null || room.RemovedFromList || !room.IsOpen || room.CustomProperties == null) continue; + if (!room.CustomProperties.ContainsKey(PhotonBattleRoom.GameTypeKey)) continue; + + GameType gameType; + try { gameType = (GameType)room.CustomProperties[PhotonBattleRoom.GameTypeKey]; } + catch { continue; } + + if (gameType != GameType.InRoom_) continue; + + string invitedUserId = room.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeInvitedUserIdKey) + ? room.CustomProperties[PhotonBattleRoom.PremadeInvitedUserIdKey]?.ToString() + : string.Empty; + if (invitedUserId != localUserId) continue; + + int inviteState = PhotonBattleRoom.PremadeInviteStateNone; + if (room.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeInviteStateKey)) + { + try { inviteState = Convert.ToInt32(room.CustomProperties[PhotonBattleRoom.PremadeInviteStateKey]); } + catch { inviteState = PhotonBattleRoom.PremadeInviteStateNone; } + } + if (inviteState != PhotonBattleRoom.PremadeInviteStatePending) continue; + + long inviteTimestampSeconds = 0; + if (room.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeInviteTimestampKey)) + { + try { inviteTimestampSeconds = Convert.ToInt64(room.CustomProperties[PhotonBattleRoom.PremadeInviteTimestampKey]); } + catch { inviteTimestampSeconds = 0; } + } + + if (inviteTimestampSeconds > 0) + { + long nowSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (nowSeconds - inviteTimestampSeconds > InRoomInviteValiditySeconds) + { + continue; + } + } + + if (IsInRoomInviteDeclinedRecently(room.Name)) continue; + if (_pendingInRoomInviteRoomName == room.Name) continue; + + if (_lastAutoInviteRoomName == room.Name && Time.time - _lastAutoInviteJoinTime < InRoomInvitePromptThrottleSeconds) + { + continue; + } + + string leaderUserId = room.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeLeaderUserIdKey) + ? room.CustomProperties[PhotonBattleRoom.PremadeLeaderUserIdKey]?.ToString() + : string.Empty; + + GameType targetGameType = GameType.Random2v2; + if (room.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeTargetGameTypeKey)) + { + try { targetGameType = (GameType)Convert.ToInt32(room.CustomProperties[PhotonBattleRoom.PremadeTargetGameTypeKey]); } + catch { targetGameType = GameType.Random2v2; } + } + + _lastAutoInviteRoomName = room.Name; + _lastAutoInviteJoinTime = Time.time; + _pendingInRoomInviteRoomName = room.Name; + Debug.Log($"Detected pending InRoom_ invite to room '{room.Name}', requesting decision from UI."); + OnInRoomInviteReceived?.Invoke(new InRoomInviteInfo(room.Name, leaderUserId, invitedUserId, targetGameType)); + break; + } } + public void OnLeftLobby() { LobbyOnLeftLobby?.Invoke(); } public void OnLobbyStatisticsUpdate(List lobbyStatistics) { LobbyOnLobbyStatisticsUpdate?.Invoke(); } public void OnFriendListUpdate(List friendList) { @@ -3667,6 +4713,17 @@ public void OnJoinRoomFailed(short returnCode, string message) _joinRoomFailed = true; LobbyOnJoinRoomFailed?.Invoke(returnCode, message); + bool failedInviteAcceptJoin = !string.IsNullOrEmpty(_pendingAcceptedInRoomInviteRoomName); + string failedInviteRoomName = _pendingAcceptedInRoomInviteRoomName; + if (failedInviteAcceptJoin) + { + _pendingAcceptedInRoomInviteRoomName = string.Empty; + _pendingAcceptedInRoomInviteStartTime = -100f; + Debug.LogWarning($"JoinRoomFailed during accepted InRoom invite join. Room='{failedInviteRoomName}', code={returnCode}, msg={message}"); + OnInRoomInviteJoinFailed?.Invoke(failedInviteRoomName, returnCode, message); + return; + } + // If the failure is a full-game error, loop back to queue/requeue flow try { @@ -3794,7 +4851,8 @@ public void OnEvent(EventData photonEvent) // Clear BattleID so cancelled start does not leave stale room state try { - if (PhotonRealtimeClient.CurrentRoom != null && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.BattleID)) + if (CanMutateRoomPropertiesNow("CancelGameStart: clear BattleID", true) + && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.BattleID)) { PhotonRealtimeClient.CurrentRoom.SetCustomProperties(new PhotonHashtable { { PhotonBattleRoom.BattleID, "" } }); } @@ -3813,6 +4871,7 @@ public void OnEvent(EventData photonEvent) if (countdown > 0) { _gamePlayedOut = false; + _lastCountdownStartTime = Time.time; // Countdown started: reset auto-requeue attempts and stop any join watcher _autoRequeueAttempts = 0; try { if (_joinTimeoutWatcherHolder != null) { StopCoroutine(_joinTimeoutWatcherHolder); _joinTimeoutWatcherHolder = null; } } catch { } @@ -3907,7 +4966,7 @@ public void OnEvent(EventData photonEvent) { if (photonEvent.CustomData is object[] arr && arr.Length > 0) { - if (arr[0] is string s) leaderUserId = s; + leaderUserId = arr[0]?.ToString() ?? string.Empty; if (arr.Length > 1 && arr[1] is object[] uarr) { expectedUsers = uarr.Select(o => o?.ToString()).Where(x => !string.IsNullOrEmpty(x)).ToArray(); @@ -4026,6 +5085,27 @@ public void OnPlayerEnteredRoom(Player newPlayer) Room room = PhotonRealtimeClient.CurrentRoom; int playerCount = room.PlayerCount; int botCount = PhotonBattleRoom.GetBotCount(); + + try + { + if (room != null && room.CustomProperties != null && room.CustomProperties.ContainsKey(PhotonBattleRoom.GameTypeKey) + && (GameType)room.GetCustomProperty(PhotonBattleRoom.GameTypeKey) == GameType.InRoom_) + { + string invitedUserId = room.GetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, string.Empty); + if (!string.IsNullOrEmpty(invitedUserId) && newPlayer != null && newPlayer.UserId == invitedUserId) + { + room.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + room.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, invitedUserId); + } + + if (PhotonRealtimeClient.LocalPlayer.IsMasterClient && room.PlayerCount >= room.MaxPlayers) + { + room.IsOpen = false; + } + } + } + catch (Exception ex) { Debug.LogWarning($"OnPlayerEnteredRoom: failed to update InRoom_ premade state: {ex.Message}"); } + // If this room is a queue room, let master form matches of 4 players try { @@ -4239,7 +5319,8 @@ public void OnMasterClientSwitched(Player newMasterClient) { // Ensure any stale BattleID is cleared when master changes so new master does not see a hanging start try { - if (PhotonRealtimeClient.CurrentRoom != null && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.BattleID)) + if (CanMutateRoomPropertiesNow("OnMasterClientSwitched: clear BattleID", true) + && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.BattleID)) { PhotonRealtimeClient.CurrentRoom.SetCustomProperties(new PhotonHashtable { { PhotonBattleRoom.BattleID, "" } }); } @@ -4471,15 +5552,38 @@ public class StartRaidTestEvent public class StartMatchmakingEvent { public readonly GameType SelectedGameType; + public readonly bool IsPremadeInRoom; - public StartMatchmakingEvent(GameType gameType) + public StartMatchmakingEvent(GameType gameType, bool isPremadeInRoom = false) { SelectedGameType = gameType; + IsPremadeInRoom = isPremadeInRoom; + } + + public override string ToString() + { + return $"{nameof(SelectedGameType)}: {SelectedGameType}, {nameof(IsPremadeInRoom)}: {IsPremadeInRoom}"; + } + } + + public class InRoomInviteInfo + { + public readonly string RoomName; + public readonly string LeaderUserId; + public readonly string InvitedUserId; + public readonly GameType TargetGameType; + + public InRoomInviteInfo(string roomName, string leaderUserId, string invitedUserId, GameType targetGameType) + { + RoomName = roomName; + LeaderUserId = leaderUserId; + InvitedUserId = invitedUserId; + TargetGameType = targetGameType; } public override string ToString() { - return $"{nameof(SelectedGameType)}: {SelectedGameType}"; + return $"{nameof(RoomName)}: {RoomName}, {nameof(LeaderUserId)}: {LeaderUserId}, {nameof(InvitedUserId)}: {InvitedUserId}, {nameof(TargetGameType)}: {TargetGameType}"; } } diff --git a/Assets/Altzone/Scripts/Photon/PhotonBattleRoom.cs b/Assets/Altzone/Scripts/Photon/PhotonBattleRoom.cs index 1cc5bd82d1..76d0695ac7 100644 --- a/Assets/Altzone/Scripts/Photon/PhotonBattleRoom.cs +++ b/Assets/Altzone/Scripts/Photon/PhotonBattleRoom.cs @@ -50,6 +50,20 @@ public class PhotonBattleRoom public const string ClanNameKey = "c"; public const string ClanOpponentNameKey = "c2"; public const string LeaderIdKey = "lid"; + public const string PremadeModeKey = "pm"; + public const string PremadeTargetGameTypeKey = "ptg"; + public const string PremadeLeaderUserIdKey = "plid"; + public const string PremadeInvitedUserIdKey = "piu"; + public const string PremadeInviteStateKey = "pis"; + public const string PremadeInviteTimestampKey = "pits"; + public const string PremadeUserId1Key = "pm1"; + public const string PremadeUserId2Key = "pm2"; + + public const int PremadeInviteStateNone = 0; + public const int PremadeInviteStatePending = 1; + public const int PremadeInviteStateAccepted = 2; + public const int PremadeInviteStateDeclined = 3; + public const int PremadeInviteStateExpired = 4; public static string PlayerPositionKey1 { get => PlayerPosition1.ToString(); } public static string PlayerPositionKey2 { get => PlayerPosition2.ToString(); } diff --git a/Assets/Altzone/Scripts/Photon/PhotonRealtimeClient.cs b/Assets/Altzone/Scripts/Photon/PhotonRealtimeClient.cs index 04aa036c35..adcb62fd52 100644 --- a/Assets/Altzone/Scripts/Photon/PhotonRealtimeClient.cs +++ b/Assets/Altzone/Scripts/Photon/PhotonRealtimeClient.cs @@ -671,6 +671,25 @@ private static RoomOptions GetRoomOptions(GameType gameType, bool isMatchmaking List propertiesShowingToLobby = new() { PhotonBattleRoom.GameTypeKey, PhotonBattleRoom.IsMatchmakingKey }; + if (gameType == GameType.InRoom_) + { + customRoomProperties.Add(PhotonBattleRoom.PremadeModeKey, true); + customRoomProperties.Add(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)GameType.Random2v2); + customRoomProperties.Add(PhotonBattleRoom.PremadeLeaderUserIdKey, LocalPlayer.UserId); + customRoomProperties.Add(PhotonBattleRoom.PremadeInvitedUserIdKey, ""); + customRoomProperties.Add(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateNone); + customRoomProperties.Add(PhotonBattleRoom.PremadeInviteTimestampKey, 0L); + customRoomProperties.Add(PhotonBattleRoom.PremadeUserId1Key, LocalPlayer.UserId); + customRoomProperties.Add(PhotonBattleRoom.PremadeUserId2Key, ""); + + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeModeKey); + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeTargetGameTypeKey); + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeInvitedUserIdKey); + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeInviteStateKey); + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeInviteTimestampKey); + propertiesShowingToLobby.Add(PhotonBattleRoom.PremadeLeaderUserIdKey); + } + int maxPlayers; switch (gameType) @@ -690,6 +709,9 @@ private static RoomOptions GetRoomOptions(GameType gameType, bool isMatchmaking maxPlayers = 2; } break; + case GameType.InRoom_: + maxPlayers = 2; + break; } if (maxPlayers == 4) { @@ -792,6 +814,21 @@ public static bool CreateCustomLobbyRoom(string roomName, string mapId, Emotion ); } + public static bool CreateInRoomPremadeLobbyRoom(string[] expectedUsers = null) + { + string roomName = $"InRoom_{LocalPlayer.UserId}_{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; + RoomOptions roomOptions = GetRoomOptions( + gameType: GameType.InRoom_, + roomName: roomName + ); + + return CreateRoom( + roomName: roomName, + roomOptions: roomOptions, + expectedUsers: expectedUsers + ); + } + public static bool CreateRoom(string roomName = "", RoomOptions roomOptions = null, TypedLobby typedLobby = null, string[] expectedUsers = null) { /*if (OfflineMode) diff --git a/Assets/MenuUi/MenuUi.asmdef b/Assets/MenuUi/MenuUi.asmdef index 3443a1ffd8..8788132a9a 100644 --- a/Assets/MenuUi/MenuUi.asmdef +++ b/Assets/MenuUi/MenuUi.asmdef @@ -3,6 +3,7 @@ "rootNamespace": "", "references": [ "GUID:7815ee0347c9e8f43bd8a6105ce3de40", + "GUID:831409e8f9d13b5479a3baef9822ad34", "GUID:8a58992e18163b845af14daeb9deb549", "GUID:75469ad4d38634e559750d17036d5f7c", "GUID:6055be8ebefd69e48b49212b09b47b2f", diff --git a/Assets/MenuUi/Prefabs/Panels/UIOverlayPanel.prefab b/Assets/MenuUi/Prefabs/Panels/UIOverlayPanel.prefab index 87ab4075b3..27c753f5a9 100644 --- a/Assets/MenuUi/Prefabs/Panels/UIOverlayPanel.prefab +++ b/Assets/MenuUi/Prefabs/Panels/UIOverlayPanel.prefab @@ -1002,6 +1002,7 @@ RectTransform: - {fileID: 8633110126328995404} - {fileID: 5550763520609190324} - {fileID: 1177530891263587228} + - {fileID: 888120001223344551} m_Father: {fileID: 8476621660179101015} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -5158,6 +5159,25 @@ RectTransform: type: 3} m_PrefabInstance: {fileID: 1627773713561907960} m_PrefabAsset: {fileID: 0} +--- !u!1001 &888120001223344550 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 5704505538415407603} + m_Modifications: [] + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 21472cc085cd46439b268f7b2469d1ac, type: 3} +--- !u!224 &888120001223344551 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 3909196248017474539, guid: 21472cc085cd46439b268f7b2469d1ac, + type: 3} + m_PrefabInstance: {fileID: 888120001223344550} + m_PrefabAsset: {fileID: 0} --- !u!1001 &1649264663230407380 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/MenuUi/Prefabs/UI Components/Battle Popup.prefab b/Assets/MenuUi/Prefabs/UI Components/Battle Popup.prefab index f96485ee08..9e8790dfba 100644 --- a/Assets/MenuUi/Prefabs/UI Components/Battle Popup.prefab +++ b/Assets/MenuUi/Prefabs/UI Components/Battle Popup.prefab @@ -1879,7 +1879,7 @@ MonoBehaviour: _toggleBotPlayerP2: {fileID: 0} _toggleBotPlayerP3: {fileID: 0} _toggleBotPlayerP4: {fileID: 0} - _buttonStartPlay: {fileID: 0} + _buttonStartPlay: {fileID: 116578133472790575} _buttonRaidTest: {fileID: 0} _nameP1: {fileID: 0} _nameP2: {fileID: 0} @@ -1926,6 +1926,9 @@ MonoBehaviour: _roomSwitcher: {fileID: 4116104174498300769} _noticeText: {fileID: 8001834333424065541} _sendInviteToFriendText: {fileID: 6755532399491747094} + _premadeTargetModeDropdown: {fileID: 0} + _inviteOnlinePlayerButton: {fileID: 6233693051039505318} + _inviteSelectorPanel: {fileID: 0} --- !u!1 &927223887747304151 GameObject: m_ObjectHideFlags: 0 @@ -5369,6 +5372,9 @@ MonoBehaviour: _roomSwitcher: {fileID: 4116104174498300769} _noticeText: {fileID: 0} _sendInviteToFriendText: {fileID: 0} + _premadeTargetModeDropdown: {fileID: 0} + _inviteOnlinePlayerButton: {fileID: 0} + _inviteSelectorPanel: {fileID: 0} --- !u!1 &2927705854601579417 GameObject: m_ObjectHideFlags: 0 @@ -17977,6 +17983,11 @@ PrefabInstance: propertyPath: m_Name value: SelectedCharactersPlayer2 objectReference: {fileID: 0} + - target: {fileID: 6082567443366930256, guid: 8e246b0a47c7e6b489fcb7b2eeb10763, + type: 3} + propertyPath: _isInRoom + value: 1 + objectReference: {fileID: 0} - target: {fileID: 4197650550993359056, guid: 8e246b0a47c7e6b489fcb7b2eeb10763, type: 3} propertyPath: m_Pivot.x @@ -21491,6 +21502,11 @@ PrefabInstance: propertyPath: m_Name value: SelectedCharactersPlayer1 objectReference: {fileID: 0} + - target: {fileID: 6082567443366930256, guid: 8e246b0a47c7e6b489fcb7b2eeb10763, + type: 3} + propertyPath: _isInRoom + value: 1 + objectReference: {fileID: 0} - target: {fileID: 4197650550993359056, guid: 8e246b0a47c7e6b489fcb7b2eeb10763, type: 3} propertyPath: m_Pivot.x diff --git a/Assets/MenuUi/Resources/GameTypeReference.asset b/Assets/MenuUi/Resources/GameTypeReference.asset index 9d289bc248..d74ae06757 100644 --- a/Assets/MenuUi/Resources/GameTypeReference.asset +++ b/Assets/MenuUi/Resources/GameTypeReference.asset @@ -13,6 +13,12 @@ MonoBehaviour: m_Name: GameTypeReference m_EditorClassIdentifier: _info: + - Icon: {fileID: 21300000, guid: 1d36eff9a0140f74a924f240aa6325bc, type: 3} + gameType: 3 + FinnishName: InRoom_ + FinnishDescription: Kutsu online-pelaaja ja mene matchmakingiin kaverina + EnglishName: InRoom_ + EnglishDescription: Invite an online player and enter matchmaking as a premade duo - Icon: {fileID: 21300000, guid: 3f5f253fe82e2404bb2074d41793da96, type: 3} gameType: 2 FinnishName: Klaani 2v2 diff --git a/Assets/MenuUi/Resources/InviteDecisionPanel.prefab b/Assets/MenuUi/Resources/InviteDecisionPanel.prefab new file mode 100644 index 0000000000..318858c381 --- /dev/null +++ b/Assets/MenuUi/Resources/InviteDecisionPanel.prefab @@ -0,0 +1,966 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1 &1975582657999255549 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1314412372844923886} + - component: {fileID: 398518435663336821} + - component: {fileID: 2139956585484295374} + - component: {fileID: 802595462673599422} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1314412372844923886 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1975582657999255549} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 5146851720205949467} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.1, y: 0.1} + m_AnchorMax: {x: 0.9, y: 0.9} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &398518435663336821 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1975582657999255549} + m_CullTransparentMesh: 1 +--- !u!114 &2139956585484295374 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1975582657999255549} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Liity + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 35c39ce28c397b848b84f7de2f26c5ac, type: 2} + m_sharedMaterial: {fileID: 2109022488393823743, guid: 35c39ce28c397b848b84f7de2f26c5ac, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281413937 + m_fontColor: {r: 0.19215688, g: 0.19215688, b: 0.19215688, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 45.8 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!114 &802595462673599422 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1975582657999255549} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 220ef737e6ba13e48841e379d56ad79b, type: 3} + m_Name: + m_EditorClassIdentifier: + _finnishText: Liity + _englishText: Join +--- !u!1 &2138903436155762796 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 3909196248017474539} + - component: {fileID: 4139031153675824652} + - component: {fileID: 3915831388473070548} + m_Layer: 5 + m_Name: InviteDecisionPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &3909196248017474539 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2138903436155762796} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 8517968017114738386} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4139031153675824652 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2138903436155762796} + m_CullTransparentMesh: 1 +--- !u!114 &3915831388473070548 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2138903436155762796} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 1d9c775637884d7a8a3e9f4eeaf376f9, type: 3} + m_Name: + m_EditorClassIdentifier: + _popup: {fileID: 7706476986623892885} + _returnButton: {fileID: 2589222831719803575} + _closeButton: {fileID: 1005031969331987872} + _messageText: {fileID: 2864667504755610613} + _returnButtonText: {fileID: 661011559652948227} + _closeButtonText: {fileID: 2139956585484295374} + _defaultMessage: "Sinut kutsuttiin huoneeseen. Liityt\xE4\xE4nk\xF6?" + _defaultDeclineText: "Hylk\xE4\xE4" + _defaultAcceptText: Liity +--- !u!1 &2956264022003729281 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2427020280311030317} + - component: {fileID: 1842480168047400331} + - component: {fileID: 2864667504755610613} + - component: {fileID: 7981972590442812495} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2427020280311030317 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2956264022003729281} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 344869134700871957} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.1, y: 0.4} + m_AnchorMax: {x: 0.9, y: 0.9} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &1842480168047400331 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2956264022003729281} + m_CullTransparentMesh: 1 +--- !u!114 &2864667504755610613 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2956264022003729281} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "Sinut kutsuttiin huoneeseen. Liityt\xE4\xE4nk\xF6?" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 35c39ce28c397b848b84f7de2f26c5ac, type: 2} + m_sharedMaterial: {fileID: 2109022488393823743, guid: 35c39ce28c397b848b84f7de2f26c5ac, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 42.75 + m_fontSizeBase: 48 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 18 + m_fontSizeMax: 48 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!114 &7981972590442812495 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2956264022003729281} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 220ef737e6ba13e48841e379d56ad79b, type: 3} + m_Name: + m_EditorClassIdentifier: + _finnishText: "Sinut kutsuttiin huoneeseen. Liityt\xE4\xE4nk\xF6?" + _englishText: You have been invited to a room. Join? +--- !u!1 &5779282171730594791 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 5146851720205949467} + - component: {fileID: 4587718236259447372} + - component: {fileID: 8377236895389338371} + - component: {fileID: 1005031969331987872} + - component: {fileID: 3796417596236718709} + m_Layer: 5 + m_Name: AcceptButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &5146851720205949467 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5779282171730594791} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1314412372844923886} + m_Father: {fileID: 344869134700871957} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.55, y: 0.2} + m_AnchorMax: {x: 0.85, y: 0.2} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 103.68001} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4587718236259447372 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5779282171730594791} + m_CullTransparentMesh: 1 +--- !u!114 &8377236895389338371 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5779282171730594791} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 0.1462264, b: 0.1462264, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &1005031969331987872 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5779282171730594791} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 8377236895389338371} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &3796417596236718709 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 5779282171730594791} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 86710e43de46f6f4bac7c8e50813a599, type: 3} + m_Name: + m_EditorClassIdentifier: + m_AspectMode: 1 + m_AspectRatio: 2.5 +--- !u!1 &6681462213237707818 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 344869134700871957} + - component: {fileID: 966922201092353245} + - component: {fileID: 4345241651748636663} + - component: {fileID: 5701180948673388135} + m_Layer: 5 + m_Name: Image + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &344869134700871957 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6681462213237707818} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2427020280311030317} + - {fileID: 7064126630016018865} + - {fileID: 5146851720205949467} + m_Father: {fileID: 8517968017114738386} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.1, y: 0.5} + m_AnchorMax: {x: 0.9, y: 0.5} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 576} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &966922201092353245 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6681462213237707818} + m_CullTransparentMesh: 1 +--- !u!114 &4345241651748636663 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6681462213237707818} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.19223924, g: 0.26528567, b: 0.3018868, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 21300000, guid: 7a057d2e8f87d9145bcea87d66268b50, type: 3} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &5701180948673388135 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6681462213237707818} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 86710e43de46f6f4bac7c8e50813a599, type: 3} + m_Name: + m_EditorClassIdentifier: + m_AspectMode: 1 + m_AspectRatio: 1.5 +--- !u!1 &7706476986623892885 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 8517968017114738386} + - component: {fileID: 4794758951583287868} + - component: {fileID: 6070586354270729784} + m_Layer: 5 + m_Name: Blocker + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!224 &8517968017114738386 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7706476986623892885} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 344869134700871957} + m_Father: {fileID: 3909196248017474539} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4794758951583287868 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7706476986623892885} + m_CullTransparentMesh: 1 +--- !u!114 &6070586354270729784 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7706476986623892885} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.392} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10907, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!1 &7780434279961174456 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 7064126630016018865} + - component: {fileID: 4042545883980374730} + - component: {fileID: 1044697738140997009} + - component: {fileID: 2589222831719803575} + - component: {fileID: 750114685933657428} + m_Layer: 5 + m_Name: RejectButton + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &7064126630016018865 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780434279961174456} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 2949104592673960180} + m_Father: {fileID: 344869134700871957} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.15, y: 0.2} + m_AnchorMax: {x: 0.45, y: 0.2} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 103.67999} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4042545883980374730 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780434279961174456} + m_CullTransparentMesh: 1 +--- !u!114 &1044697738140997009 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780434279961174456} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 0.13679248, g: 0.86327857, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &2589222831719803575 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780434279961174456} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1044697738140997009} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &750114685933657428 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 7780434279961174456} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 86710e43de46f6f4bac7c8e50813a599, type: 3} + m_Name: + m_EditorClassIdentifier: + m_AspectMode: 1 + m_AspectRatio: 2.5 +--- !u!1 &8573024165001618612 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2949104592673960180} + - component: {fileID: 4111480193382309282} + - component: {fileID: 661011559652948227} + - component: {fileID: 3098847835943497535} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &2949104592673960180 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8573024165001618612} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 7064126630016018865} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.1, y: 0.1} + m_AnchorMax: {x: 0.9, y: 0.9} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &4111480193382309282 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8573024165001618612} + m_CullTransparentMesh: 1 +--- !u!114 &661011559652948227 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8573024165001618612} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: "Hylk\xE4\xE4" + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 35c39ce28c397b848b84f7de2f26c5ac, type: 2} + m_sharedMaterial: {fileID: 2109022488393823743, guid: 35c39ce28c397b848b84f7de2f26c5ac, + type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 36.9 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 1 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 0 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!114 &3098847835943497535 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 8573024165001618612} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 220ef737e6ba13e48841e379d56ad79b, type: 3} + m_Name: + m_EditorClassIdentifier: + _finnishText: "Hylk\xE4\xE4" + _englishText: Decline diff --git a/Assets/MenuUi/Resources/InviteDecisionPanel.prefab.meta b/Assets/MenuUi/Resources/InviteDecisionPanel.prefab.meta new file mode 100644 index 0000000000..685fa86faf --- /dev/null +++ b/Assets/MenuUi/Resources/InviteDecisionPanel.prefab.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 21472cc085cd46439b268f7b2469d1ac +PrefabImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/MenuUi/Scripts/Lobby/BattlePopupPanelManager.cs b/Assets/MenuUi/Scripts/Lobby/BattlePopupPanelManager.cs index b9c77b81da..528a9fa9b9 100644 --- a/Assets/MenuUi/Scripts/Lobby/BattlePopupPanelManager.cs +++ b/Assets/MenuUi/Scripts/Lobby/BattlePopupPanelManager.cs @@ -39,6 +39,9 @@ public void SwitchRoom(GameType gameType) case GameType.Custom: SwitchCustomRoom(CustomGameMode.TwoVersusTwo); break; + case GameType.InRoom_: + _clanAndRandom2v2WaitingRoom.SetActive(true); + break; case GameType.Clan2v2: _clanAndRandom2v2WaitingRoom.SetActive(true); break; diff --git a/Assets/MenuUi/Scripts/Lobby/InLobby/InLobbyController.cs b/Assets/MenuUi/Scripts/Lobby/InLobby/InLobbyController.cs index b13983ddce..fe6c41b520 100644 --- a/Assets/MenuUi/Scripts/Lobby/InLobby/InLobbyController.cs +++ b/Assets/MenuUi/Scripts/Lobby/InLobby/InLobbyController.cs @@ -2,12 +2,15 @@ using System.Collections; using Altzone.Scripts; using Altzone.Scripts.Config; +using Altzone.Scripts.Model.Poco.Game; using Altzone.Scripts.Model.Poco.Player; using Altzone.Scripts.Lobby; using UnityEngine; using UnityEngine.SceneManagement; using MenuUi.Scripts.Signals; using Altzone.Scripts.Battle.Photon; +using MenuUi.Scripts.Window; +using PopupSignalBus = MenuUI.Scripts.SignalBus; namespace MenuUi.Scripts.Signals { @@ -56,11 +59,22 @@ public class InLobbyController : AltMonoBehaviour private Coroutine _creatingRoomCoroutineHolder = null; public static GameType SelectedGameType { get; private set; } + public static GameType SelectedPremadeTargetGameType { get; private set; } = GameType.Random2v2; + + public static void SetPremadeTargetGameType(GameType gameType) + { + if (gameType == GameType.Random2v2 || gameType == GameType.Clan2v2) + { + SelectedPremadeTargetGameType = gameType; + } + } private void Awake() { SignalBus.OnBattlePopupRequested += OpenWindow; SignalBus.OnCloseBattlePopupRequested += CloseWindow; + LobbyManager.OnInRoomInviteReceived += OnInRoomInviteReceived; + LobbyManager.OnInRoomInviteJoinFailed += OnInRoomInviteJoinFailed; // Register runtime popup reference for other components to find (safe to set here because serialized field is available in Awake) PopupContentsInstance = _popupContents; OnPopupContentsInstanceAssigned?.Invoke(PopupContentsInstance); @@ -71,6 +85,8 @@ private void OnDestroy() { SignalBus.OnBattlePopupRequested -= OpenWindow; SignalBus.OnCloseBattlePopupRequested -= CloseWindow; + LobbyManager.OnInRoomInviteReceived -= OnInRoomInviteReceived; + LobbyManager.OnInRoomInviteJoinFailed -= OnInRoomInviteJoinFailed; if (PopupContentsInstance == _popupContents) { PopupContentsInstance = null; @@ -226,6 +242,25 @@ private void OpenWindow(GameType gameType) } } break; + case GameType.InRoom_: + if (PhotonRealtimeClient.InMatchmakingRoom && gameType == SelectedGameType) + { + _roomSwitcher.SwitchToMatchmakingPanel(PhotonRealtimeClient.LocalLobbyPlayer.IsMasterClient); + return; + } + + if (PhotonRealtimeClient.InRoom) + { + if (gameType == SelectedGameType) + { + _roomSwitcher.SwitchRoom(GameType.InRoom_); + return; + } + + LobbyManager.Instance.StopMatchmakingCoroutines(); + PhotonRealtimeClient.LeaveRoom(); + } + break; default: return; } @@ -285,5 +320,87 @@ private void QuickGameButtonOnClick() { Debug.Log($"{PhotonRealtimeClient.LobbyNetworkClientState}"); } + + private void OnInRoomInviteReceived(LobbyManager.InRoomInviteInfo inviteInfo) + { + if (inviteInfo == null || string.IsNullOrEmpty(inviteInfo.RoomName)) return; + + string inviterName = ResolveOnlinePlayerName(inviteInfo.LeaderUserId); + string targetMode = inviteInfo.TargetGameType == GameType.Clan2v2 ? "Clan 2v2" : "Random 2v2"; + string message = $"{inviterName} kutsui sinut InRoom_ huoneeseen. Haettava pelimuoto: {targetMode}. Liitytäänkö huoneeseen?"; + + bool popupShown = InviteDecisionPopupHandler.RequestInviteDecisionPrompt( + message, + "Liity", + "Hylkää", + accepted => + { + if (LobbyManager.Instance == null) return; + if (accepted) + { + OpenBattlePopupForInviteAccept(); + LobbyManager.Instance.AcceptInRoomInvite(inviteInfo.RoomName); + } + else LobbyManager.Instance.DeclineInRoomInvite(inviteInfo.RoomName); + }); + + if (!popupShown) + { + Debug.LogWarning("OnInRoomInviteReceived: decision popup unavailable, auto-joining invite."); + OpenBattlePopupForInviteAccept(); + LobbyManager.Instance?.AcceptInRoomInvite(inviteInfo.RoomName); + PopupSignalBus.OnChangePopupInfoSignal("InRoom_ kutsu saatu, liityttiin automaattisesti."); + } + } + + private void OpenBattlePopupForInviteAccept() + { + SelectedGameType = GameType.InRoom_; + + if (_popupContents != null && !_popupContents.activeSelf) + { + _popupContents.SetActive(true); + } + + RefreshTopInfo(); + _roomSwitcher?.SwitchRoom(GameType.InRoom_); + } + + private string ResolveOnlinePlayerName(string userId) + { + if (string.IsNullOrEmpty(userId)) return "Pelaaja"; + + try + { + var onlinePlayers = ServerManager.Instance?.OnlinePlayers; + if (onlinePlayers != null) + { + foreach (ServerOnlinePlayer player in onlinePlayers) + { + if (player == null || player._id != userId) continue; + if (!string.IsNullOrWhiteSpace(player.name)) return player.name; + break; + } + } + } + catch (Exception ex) + { + Debug.LogWarning($"ResolveOnlinePlayerName failed: {ex.Message}"); + } + + return userId; + } + + private void OnInRoomInviteJoinFailed(string roomName, short returnCode, string message) + { + bool isRoomFull = returnCode == 32765 + || (!string.IsNullOrEmpty(message) && message.ToLowerInvariant().Contains("game full")); + + string popupMessage = isRoomFull + ? "InRoom_ kutsuun liittyminen epaonnistui: huone on taynna tai kutsu ei ole enaa voimassa." + : "InRoom_ kutsuun liittyminen epaonnistui. Yrita uudelleen, jos kutsu on yha voimassa."; + + PopupSignalBus.OnChangePopupInfoSignal(popupMessage); + } } } diff --git a/Assets/MenuUi/Scripts/Lobby/InLobby/LobbyRoomListingController.cs b/Assets/MenuUi/Scripts/Lobby/InLobby/LobbyRoomListingController.cs index 9b7b11b7a0..85f0adea6a 100644 --- a/Assets/MenuUi/Scripts/Lobby/InLobby/LobbyRoomListingController.cs +++ b/Assets/MenuUi/Scripts/Lobby/InLobby/LobbyRoomListingController.cs @@ -124,6 +124,9 @@ public IEnumerator StartCreatingRoom(GameType gameType, Action callback) { switch (gameType) { + case GameType.InRoom_: + CreateInRoomPremadeRoom(); + break; case GameType.Clan2v2: CreateClan2v2Room(); break; @@ -181,6 +184,11 @@ private void CreateCustomRoom() })); } + private void CreateInRoomPremadeRoom() + { + PhotonRealtimeClient.CreateInRoomPremadeLobbyRoom(); + } + private void JoinRoom(string roomName) { Debug.Log($"{roomName}"); diff --git a/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomController.cs b/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomController.cs index 43c7c19235..9e601062e0 100644 --- a/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomController.cs +++ b/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomController.cs @@ -30,6 +30,13 @@ public class InRoomController : MonoBehaviour [SerializeField] private BattlePopupPanelManager _roomSwitcher; [SerializeField] private TMP_Text _noticeText; [SerializeField] private TMP_Text _sendInviteToFriendText; + [SerializeField] private TMP_Dropdown _premadeTargetModeDropdown; + [SerializeField] private Button _inviteOnlinePlayerButton; + [SerializeField] private InRoomInviteSelectorPanel _inviteSelectorPanel; + + private Coroutine _inviteLifecycleHolder; + private const float InviteLifecycleTickSeconds = 1f; + private const long InviteExpirationSeconds = 60; private void Awake() { @@ -37,17 +44,29 @@ private void Awake() //buttons[1].onClick.AddListener(SetPlayerAsSpectator); _startGameButton.onClick.AddListener(StartPlaying); _backButton.onClick.AddListener(GoBack); + if (_premadeTargetModeDropdown != null) _premadeTargetModeDropdown.onValueChanged.AddListener(OnPremadeTargetModeChanged); + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.onClick.AddListener(OnInviteOnlinePlayerButtonPressed); + EnsureInviteSelectorPanel(); //buttons[3].onClick.AddListener(StartRaidTest); } private void OnEnable() { + if (_startGameButton != null) _startGameButton.interactable = true; + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.interactable = InLobbyController.SelectedGameType == GameType.InRoom_; + switch (InLobbyController.SelectedGameType) { case GameType.Custom: if (_title != null) StartCoroutine(SetRoomTitle()); if (_conflictText != null) StartCoroutine(CycleConflicts()); break; + case GameType.InRoom_: + if (_title != null) _title.text = "InRoom_"; + if (_noticeText != null) _noticeText.text = "Kutsu yksi online-pelaaja ja valitse haettava 2v2 pelimuoto."; + if (_sendInviteToFriendText != null) _sendInviteToFriendText.text = "Kutsu online-pelaaja"; + ConfigurePremadeTargetSelector(); + break; case GameType.Random2v2: //if (_title != null) _title.text = "Keräily 2v2"; //if (_noticeText != null) _noticeText.text = "Tätä pelimuotoa voi mennä pelaamaan yksin tai kaverin kanssa (työn alla). Huom. Jos menet pelaamaan yksin, paikan valinnalla ei ole merkitystä."; @@ -61,14 +80,33 @@ private void OnEnable() if (_sendInviteToFriendText != null) _sendInviteToFriendText.text = "Lähetä kutsu yhdelle klaanin jäsenelle"; break; } + + if (InLobbyController.SelectedGameType == GameType.InRoom_) + { + StartInviteLifecycleMonitoring(); + } + else + { + StopInviteLifecycleMonitoring(); + } } private void OnDestroy() { + if (_premadeTargetModeDropdown != null) _premadeTargetModeDropdown.onValueChanged.RemoveListener(OnPremadeTargetModeChanged); + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.onClick.RemoveListener(OnInviteOnlinePlayerButtonPressed); + if (_inviteSelectorPanel != null) _inviteSelectorPanel.HideSilently(); + StopInviteLifecycleMonitoring(); _startGameButton.onClick.RemoveAllListeners(); _backButton.onClick.RemoveAllListeners(); } + private void OnDisable() + { + if (_inviteSelectorPanel != null) _inviteSelectorPanel.HideSilently(); + StopInviteLifecycleMonitoring(); + } + private void SetPlayerAsGuest() { Debug.Log($"setPlayerAsGuest {PhotonLobbyRoom.PlayerPositionGuest}"); @@ -83,6 +121,11 @@ private void SetPlayerAsSpectator() private void StartPlaying() { + void RestoreStartButton() + { + if (_startGameButton != null) _startGameButton.interactable = true; + } + //if (!PhotonLobbyRoom.IsValidAllSelectedCharacters()) //{ // SignalBus.OnChangePopupInfoSignal("Kaikkien pelaajien pitää ensin valita 3 puolustushahmoa."); @@ -96,6 +139,50 @@ private void StartPlaying() this.Publish(new LobbyManager.StartPlayingEvent()); break; + case GameType.InRoom_: + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) + { + RestoreStartButton(); + return; + } + + if (!PhotonRealtimeClient.LocalLobbyPlayer.IsMasterClient) + { + PopupSignalBus.OnChangePopupInfoSignal("Vain huoneen johtaja voi aloittaa matchmakingin."); + RestoreStartButton(); + return; + } + + if (PhotonLobbyRoom.CountRealPlayers() != PhotonRealtimeClient.LobbyCurrentRoom.MaxPlayers) + { + PopupSignalBus.OnChangePopupInfoSignal($"Huoneessa pitää olla {PhotonRealtimeClient.LobbyCurrentRoom.MaxPlayers} pelaajaa."); + RestoreStartButton(); + return; + } + + GameType targetGameType = InLobbyController.SelectedPremadeTargetGameType; + if (targetGameType != GameType.Random2v2 && targetGameType != GameType.Clan2v2) + { + targetGameType = GameType.Random2v2; + } + + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId ?? string.Empty; + string teammateUserId = string.Empty; + foreach (var player in PhotonRealtimeClient.CurrentRoom.Players.Values) + { + if (player == null || player.UserId == localUserId) continue; + teammateUserId = player.UserId; + break; + } + + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)targetGameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, teammateUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + this.Publish(new LobbyManager.StartMatchmakingEvent(targetGameType, true)); + break; + case GameType.Clan2v2: if (PhotonLobbyRoom.CountRealPlayers() == PhotonRealtimeClient.LobbyCurrentRoom.MaxPlayers) { @@ -106,6 +193,7 @@ private void StartPlaying() if (curr != null && curr.GetCustomProperty(PhotonBattleRoom.IsQueueKey)) { Debug.Log("StartPlaying suppressed: current room is a queue room (Clan2v2)."); + RestoreStartButton(); return; } } @@ -115,6 +203,7 @@ private void StartPlaying() else { PopupSignalBus.OnChangePopupInfoSignal($"Huoneessa pitää olla {PhotonRealtimeClient.LobbyCurrentRoom.MaxPlayers} pelaajaa."); + RestoreStartButton(); } break; case GameType.Random2v2: @@ -125,6 +214,7 @@ private void StartPlaying() if (curr != null && curr.GetCustomProperty(PhotonBattleRoom.IsQueueKey)) { Debug.Log("StartPlaying suppressed: current room is a queue room (Random2v2)."); + RestoreStartButton(); return; } } @@ -133,6 +223,366 @@ private void StartPlaying() break; } } + + private void ConfigurePremadeTargetSelector() + { + if (_premadeTargetModeDropdown == null) return; + + _premadeTargetModeDropdown.options = new List + { + new TMP_Dropdown.OptionData("Random 2v2"), + new TMP_Dropdown.OptionData("Clan 2v2"), + }; + + int targetValue = InLobbyController.SelectedPremadeTargetGameType == GameType.Clan2v2 ? 1 : 0; + _premadeTargetModeDropdown.SetValueWithoutNotify(targetValue); + _premadeTargetModeDropdown.interactable = true; + } + + private void OnPremadeTargetModeChanged(int value) + { + InLobbyController.SetPremadeTargetGameType(value == 1 ? GameType.Clan2v2 : GameType.Random2v2); + } + + private void OnInviteOnlinePlayerButtonPressed() + { + if (InLobbyController.SelectedGameType != GameType.InRoom_) return; + StartCoroutine(InviteOnlinePlayerRoutine()); + } + + private IEnumerator InviteOnlinePlayerRoutine() + { + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) yield break; + + if (!PhotonRealtimeClient.LocalLobbyPlayer.IsMasterClient) + { + PopupSignalBus.OnChangePopupInfoSignal("Vain huoneen johtaja voi lähettää kutsun."); + yield break; + } + + List candidates = null; + yield return StartCoroutine(GetInviteCandidatesRoutine(result => candidates = result)); + + if (candidates == null || candidates.Count == 0) + { + PopupSignalBus.OnChangePopupInfoSignal("Ei sopivia online-pelaajia kutsuttavaksi."); + yield break; + } + + if (candidates.Count == 1) + { + SendInviteToOnlinePlayer(candidates[0]); + yield break; + } + + EnsureInviteSelectorPanel(); + if (_inviteSelectorPanel == null) + { + SendInviteToOnlinePlayer(candidates[0]); + yield break; + } + + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.interactable = false; + _inviteSelectorPanel.Show( + candidates, + selectedPlayer => + { + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.interactable = true; + SendInviteToOnlinePlayer(selectedPlayer); + }, + () => + { + if (_inviteOnlinePlayerButton != null) _inviteOnlinePlayerButton.interactable = true; + PopupSignalBus.OnChangePopupInfoSignal("Kutsun lähetys peruttu."); + }); + } + + private IEnumerator GetInviteCandidatesRoutine(Action> callback) + { + List onlinePlayers = ServerManager.Instance?.OnlinePlayers; + if ((onlinePlayers == null || onlinePlayers.Count == 0) && ServerManager.Instance != null) + { + List fetchedPlayers = null; + yield return StartCoroutine(ServerManager.Instance.GetOnlinePlayersFromServer(players => fetchedPlayers = players)); + onlinePlayers = fetchedPlayers; + } + + callback?.Invoke(FilterInviteCandidates(onlinePlayers)); + } + + private List FilterInviteCandidates(List onlinePlayers) + { + List candidates = new(); + if (onlinePlayers == null || onlinePlayers.Count == 0) return candidates; + + string localUserId = GetLocalUserId(); + foreach (ServerOnlinePlayer onlinePlayer in onlinePlayers) + { + if (onlinePlayer == null || string.IsNullOrEmpty(onlinePlayer._id)) continue; + if (onlinePlayer._id == localUserId) continue; + if (IsPlayerAlreadyInCurrentRoom(onlinePlayer._id)) continue; + candidates.Add(onlinePlayer); + } + + return candidates; + } + + private void SendInviteToOnlinePlayer(ServerOnlinePlayer onlinePlayer) + { + if (onlinePlayer == null || string.IsNullOrEmpty(onlinePlayer._id)) + { + PopupSignalBus.OnChangePopupInfoSignal("Virheellinen kutsuttava pelaaja."); + return; + } + + if (!TrySendInviteToUserId(onlinePlayer._id)) + { + return; + } + + PopupSignalBus.OnChangePopupInfoSignal($"Kutsu lähetetty pelaajalle {GetOnlinePlayerDisplayName(onlinePlayer)}."); + } + + private bool TrySendInviteToUserId(string invitedUserId) + { + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) + { + return false; + } + + if (!PhotonRealtimeClient.LocalLobbyPlayer.IsMasterClient) + { + PopupSignalBus.OnChangePopupInfoSignal("Vain huoneen johtaja voi lähettää kutsun."); + return false; + } + + if (string.IsNullOrEmpty(invitedUserId)) + { + PopupSignalBus.OnChangePopupInfoSignal("Sopivaa kutsuttavaa online-pelaajaa ei löytynyt."); + return false; + } + + string localUserId = GetLocalUserId(); + if (string.IsNullOrEmpty(localUserId) || invitedUserId == localUserId) return false; + if (IsPlayerAlreadyInCurrentRoom(invitedUserId)) + { + PopupSignalBus.OnChangePopupInfoSignal("Valittu pelaaja on jo huoneessa."); + return false; + } + + int inviteState = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateNone); + string currentInvitedUserId = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, string.Empty); + if (inviteState == PhotonBattleRoom.PremadeInviteStatePending && currentInvitedUserId == invitedUserId) + { + PopupSignalBus.OnChangePopupInfoSignal("Kutsu on jo lähetetty tälle pelaajalle."); + return false; + } + + try + { + PhotonRealtimeClient.LobbyCurrentRoom.ClearExpectedUsers(); + PhotonRealtimeClient.LobbyCurrentRoom.SetExpectedUsers(new[] { invitedUserId }); + } + catch (Exception ex) + { + Debug.LogWarning($"TrySendInviteToUserId: failed to set expected users: {ex.Message}"); + } + + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeModeKey, true); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeLeaderUserIdKey, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, invitedUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStatePending); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteTimestampKey, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeTargetGameTypeKey, (int)InLobbyController.SelectedPremadeTargetGameType); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId1Key, localUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + + return true; + } + + private string GetLocalUserId() + { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId; + if (string.IsNullOrEmpty(localUserId) && ServerManager.Instance?.Player != null) + { + localUserId = ServerManager.Instance.Player._id; + } + + return localUserId; + } + + private bool IsPlayerAlreadyInCurrentRoom(string userId) + { + if (string.IsNullOrEmpty(userId) || PhotonRealtimeClient.CurrentRoom == null) return false; + + foreach (var player in PhotonRealtimeClient.CurrentRoom.Players.Values) + { + if (player == null || string.IsNullOrEmpty(player.UserId)) continue; + if (player.UserId == userId) return true; + } + + return false; + } + + private static string GetOnlinePlayerDisplayName(ServerOnlinePlayer onlinePlayer) + { + if (onlinePlayer == null) return "Tuntematon"; + if (!string.IsNullOrWhiteSpace(onlinePlayer.name)) return onlinePlayer.name; + return string.IsNullOrEmpty(onlinePlayer._id) ? "Tuntematon" : onlinePlayer._id; + } + + private void StartInviteLifecycleMonitoring() + { + if (_inviteLifecycleHolder != null) + { + return; + } + + _inviteLifecycleHolder = StartCoroutine(InviteLifecycleRoutine()); + } + + private void StopInviteLifecycleMonitoring() + { + if (_inviteLifecycleHolder == null) + { + return; + } + + StopCoroutine(_inviteLifecycleHolder); + _inviteLifecycleHolder = null; + } + + private IEnumerator InviteLifecycleRoutine() + { + WaitForSecondsRealtime delay = new(InviteLifecycleTickSeconds); + + while (true) + { + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) + { + yield return delay; + continue; + } + + var localLobbyPlayer = PhotonRealtimeClient.LocalLobbyPlayer; + if (localLobbyPlayer == null || !localLobbyPlayer.IsMasterClient) + { + yield return delay; + continue; + } + + GameType roomGameType = GameType.InRoom_; + bool failedToReadRoomGameType = false; + try + { + roomGameType = (GameType)PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.GameTypeKey); + } + catch + { + failedToReadRoomGameType = true; + } + + if (failedToReadRoomGameType) + { + yield return delay; + continue; + } + + if (roomGameType != GameType.InRoom_) + { + yield return delay; + continue; + } + + int inviteState = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateNone); + if (inviteState != PhotonBattleRoom.PremadeInviteStatePending) + { + yield return delay; + continue; + } + + string invitedUserId = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, string.Empty); + if (string.IsNullOrEmpty(invitedUserId)) + { + yield return delay; + continue; + } + + if (IsPlayerAlreadyInCurrentRoom(invitedUserId)) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateAccepted); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, invitedUserId); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteTimestampKey, 0L); + if (PhotonRealtimeClient.CurrentRoom.PlayerCount >= PhotonRealtimeClient.CurrentRoom.MaxPlayers) + { + PhotonRealtimeClient.CurrentRoom.IsOpen = false; + } + yield return delay; + continue; + } + + long inviteTimestampSeconds = 0; + try + { + if (PhotonRealtimeClient.CurrentRoom.CustomProperties != null + && PhotonRealtimeClient.CurrentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.PremadeInviteTimestampKey)) + { + inviteTimestampSeconds = Convert.ToInt64(PhotonRealtimeClient.CurrentRoom.CustomProperties[PhotonBattleRoom.PremadeInviteTimestampKey]); + } + } + catch + { + inviteTimestampSeconds = 0; + } + + long nowSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + if (inviteTimestampSeconds <= 0) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteTimestampKey, nowSeconds); + yield return delay; + continue; + } + + if (nowSeconds - inviteTimestampSeconds >= InviteExpirationSeconds) + { + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteStateKey, PhotonBattleRoom.PremadeInviteStateExpired); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInvitedUserIdKey, string.Empty); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeUserId2Key, string.Empty); + PhotonRealtimeClient.CurrentRoom.SetCustomProperty(PhotonBattleRoom.PremadeInviteTimestampKey, 0L); + + try { PhotonRealtimeClient.LobbyCurrentRoom.ClearExpectedUsers(); } + catch (Exception ex) { Debug.LogWarning($"InviteLifecycleRoutine: failed to clear expected users on expiry: {ex.Message}"); } + + PopupSignalBus.OnChangePopupInfoSignal("Kutsu vanheni. Voit lahettaa uuden kutsun."); + } + + yield return delay; + } + } + + private void EnsureInviteSelectorPanel() + { + if (_inviteSelectorPanel != null) + { + if (_inviteOnlinePlayerButton != null) + { + _inviteSelectorPanel.ConfigureVisualStyle(_inviteOnlinePlayerButton); + } + return; + } + + _inviteSelectorPanel = GetComponentInChildren(true); + if (_inviteSelectorPanel == null) + { + _inviteSelectorPanel = InRoomInviteSelectorPanel.CreateRuntime(transform); + } + + if (_inviteSelectorPanel != null && _inviteOnlinePlayerButton != null) + { + _inviteSelectorPanel.ConfigureVisualStyle(_inviteOnlinePlayerButton); + } + } + private void GoBack() { Debug.Log($"leavingRoom"); diff --git a/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomInviteSelectorPanel.cs b/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomInviteSelectorPanel.cs new file mode 100644 index 0000000000..e0f2cddd85 --- /dev/null +++ b/Assets/MenuUi/Scripts/Lobby/InRoom/InRoomInviteSelectorPanel.cs @@ -0,0 +1,593 @@ +using System; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.UI; + +namespace MenuUi.Scripts.Lobby.InRoom +{ + /// + /// Dedicated panel for selecting which online player to invite into an InRoom_ premade room. + /// Can be assigned from prefab or created at runtime by code. + /// + public class InRoomInviteSelectorPanel : MonoBehaviour + { + [SerializeField] private GameObject _root; + [SerializeField] private RectTransform _contentRoot; + [SerializeField] private Button _closeButton; + [SerializeField] private TMP_Text _titleText; + [SerializeField] private TMP_Text _emptyText; + [SerializeField] private Image _overlayImage; + [SerializeField] private Image _cardImage; + [SerializeField] private Image _scrollBackgroundImage; + [SerializeField] private Image _closeButtonImage; + [SerializeField] private TMP_Text _closeButtonText; + + [Header("Fallback Style")] + [SerializeField] private Color _fallbackOverlayColor = new(0f, 0f, 0f, 0.62f); + [SerializeField] private Color _fallbackCardColor = new(1f, 1f, 1f, 0.98f); + [SerializeField] private Color _fallbackScrollColor = new(0.95f, 0.95f, 0.95f, 0.95f); + [SerializeField] private Color _fallbackButtonColor = new(0.841f, 0.635f, 0.973f, 1f); + [SerializeField] private Color _fallbackTextColor = new(0.196f, 0.196f, 0.196f, 1f); + [SerializeField] private float _fallbackRowFontSize = 24f; + + private readonly List _spawnedRows = new(); + private Action _onSelected; + private Action _onCancelled; + + private Sprite _rowSprite; + private Material _rowMaterial; + private Image.Type _rowImageType = Image.Type.Simple; + private Color _rowColor; + private ColorBlock _rowColorBlock; + private TMP_FontAsset _rowFontAsset; + private Color _rowTextColor; + private float _rowFontSize; + private bool _rowStyleInitialized; + + public bool IsVisible => _root != null && _root.activeSelf; + + private void Awake() + { + if (_root == null) + { + _root = gameObject; + } + + EnsureFallbackStyleInitialized(); + WireUi(); + if (_root != null) + { + _root.SetActive(false); + } + } + + private void OnDestroy() + { + if (_closeButton != null) + { + _closeButton.onClick.RemoveListener(OnClosePressed); + } + } + + public void Show(List players, Action onSelected, Action onCancelled = null) + { + if (_root == null || _contentRoot == null) + { + Debug.LogWarning("InRoomInviteSelectorPanel: missing UI references."); + return; + } + + _onSelected = onSelected; + _onCancelled = onCancelled; + + if (_titleText != null) + { + _titleText.text = "Valitse kutsuttava online-pelaaja"; + } + + BuildPlayerRows(players); + _root.SetActive(true); + } + + public void ConfigureVisualStyle(Button styleSourceButton) + { + if (styleSourceButton == null) + { + return; + } + + EnsureFallbackStyleInitialized(); + + Image primaryImage = styleSourceButton.targetGraphic as Image; + if (primaryImage != null) + { + _rowSprite = primaryImage.sprite; + _rowMaterial = primaryImage.material; + _rowImageType = primaryImage.type; + _rowColor = primaryImage.color; + } + + _rowColorBlock = styleSourceButton.colors; + + TMP_Text sourceText = styleSourceButton.GetComponentInChildren(true); + if (sourceText != null) + { + _rowFontAsset = sourceText.font; + _rowTextColor = sourceText.color; + _rowFontSize = Mathf.Max(18f, sourceText.fontSize); + } + + Image decorativeFrameImage = FindDecorativeFrameImage(styleSourceButton, primaryImage); + if (decorativeFrameImage != null) + { + ApplyImageStyle(_cardImage, decorativeFrameImage.sprite, decorativeFrameImage.material, decorativeFrameImage.type, new Color(1f, 1f, 1f, 0.97f)); + ApplyImageStyle(_scrollBackgroundImage, decorativeFrameImage.sprite, decorativeFrameImage.material, decorativeFrameImage.type, new Color(1f, 1f, 1f, 0.9f)); + } + + if (_overlayImage != null) + { + _overlayImage.color = _fallbackOverlayColor; + } + + if (_closeButtonImage == null && _closeButton != null) + { + _closeButtonImage = _closeButton.targetGraphic as Image; + } + + if (_closeButtonImage != null) + { + ApplyImageStyle(_closeButtonImage, _rowSprite, _rowMaterial, _rowImageType, _rowColor); + } + + if (_closeButton != null) + { + _closeButton.colors = _rowColorBlock; + } + + if (_closeButtonText == null && _closeButton != null) + { + _closeButtonText = _closeButton.GetComponentInChildren(true); + } + + if (_closeButtonText != null) + { + _closeButtonText.color = _rowTextColor; + if (_rowFontAsset != null) _closeButtonText.font = _rowFontAsset; + _closeButtonText.fontSize = _rowFontSize; + } + + if (_titleText != null) + { + _titleText.color = _rowTextColor; + if (_rowFontAsset != null) _titleText.font = _rowFontAsset; + } + + if (_emptyText != null) + { + _emptyText.color = _rowTextColor; + if (_rowFontAsset != null) _emptyText.font = _rowFontAsset; + } + } + + public void Hide(bool invokeCancel) + { + if (_root == null) + { + return; + } + + bool wasVisible = _root.activeSelf; + _root.SetActive(false); + ClearRows(); + + Action onCancelled = _onCancelled; + _onSelected = null; + _onCancelled = null; + + if (invokeCancel && wasVisible) + { + onCancelled?.Invoke(); + } + } + + public void HideSilently() + { + Hide(false); + } + + private void WireUi() + { + if (_closeButton == null) + { + return; + } + + if (_closeButtonImage == null) + { + _closeButtonImage = _closeButton.targetGraphic as Image; + } + + if (_closeButtonText == null) + { + _closeButtonText = _closeButton.GetComponentInChildren(true); + } + + _closeButton.onClick.RemoveListener(OnClosePressed); + _closeButton.onClick.AddListener(OnClosePressed); + } + + private void OnClosePressed() + { + Hide(true); + } + + private void OnPlayerPressed(ServerOnlinePlayer player) + { + Action onSelected = _onSelected; + Hide(false); + onSelected?.Invoke(player); + } + + private void BuildPlayerRows(List players) + { + ClearRows(); + + int candidateCount = players?.Count ?? 0; + if (_emptyText != null) + { + _emptyText.gameObject.SetActive(candidateCount == 0); + _emptyText.text = "Ei kutsuttavia online-pelaajia."; + } + + if (candidateCount == 0) + { + return; + } + + foreach (ServerOnlinePlayer player in players) + { + if (player == null) + { + continue; + } + + GameObject row = CreateRowObject(_contentRoot, player); + Button button = row.GetComponent - private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoomName = null) + private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoomName = null, string[] expectedUsersOverride = null) { try { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId; + string[] followTeammates = expectedUsersOverride != null + ? expectedUsersOverride + .Where(uid => !string.IsNullOrEmpty(uid) && uid != localUserId) + .Distinct(StringComparer.Ordinal) + .ToArray() + : _teammates; bool queueRoomRequested = !string.IsNullOrEmpty(leaderRoomName) && leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal); // Duplicate handoff events are common during queue->match transitions. @@ -3608,8 +4128,8 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom Debug.Log("FollowLeaderToNewRoom: failed to join leader room; attempting server-side JoinOrCreate matchmaking room."); try { - Debug.Log($"FollowLeaderToNewRoom: calling JoinOrCreateMatchmakingRoom, teammates count={_teammates?.Length ?? 0}"); - PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, _teammates); + Debug.Log($"FollowLeaderToNewRoom: calling JoinOrCreateMatchmakingRoom, teammates count={followTeammates?.Length ?? 0}"); + PhotonRealtimeClient.JoinOrCreateMatchmakingRoom(GameType.Random2v2, followTeammates); Debug.Log("FollowLeaderToNewRoom: JoinOrCreateMatchmakingRoom call returned; awaiting join result..."); } catch (Exception ex) @@ -3663,7 +4183,7 @@ private IEnumerator FollowLeaderToNewRoom(string leaderUserId, string leaderRoom { try { - PhotonRealtimeClient.JoinRandomOrCreateRandom2v2Room(_teammates, true); + PhotonRealtimeClient.JoinRandomOrCreateRandom2v2Room(followTeammates, true); } catch (Exception ex) { @@ -3997,10 +4517,28 @@ private IEnumerator StartTheGameplay(bool isCloseRoom, string blueTeamName, stri foreach (Player roomPlayer in players) { int playerPos = roomPlayer.GetCustomProperty(PlayerPositionKey, PlayerPositionGuest); - if (!PhotonLobbyRoom.IsValidPlayerPos(playerPos)) + bool hasSlotConflict = PhotonLobbyRoom.IsValidPlayerPos(playerPos) + && !string.IsNullOrEmpty(playerUserIds[playerPos - 1]) + && playerUserIds[playerPos - 1] != roomPlayer.UserId; + + if (!PhotonLobbyRoom.IsValidPlayerPos(playerPos) || hasSlotConflict) { // If player position is not valid we get new position for them, this method checks for duplicate and missing player positions int newPos = PhotonLobbyRoom.GetFirstFreePlayerPos(new(roomPlayer)); + if (!PhotonLobbyRoom.IsValidPlayerPos(newPos)) + { + newPos = GetFirstFreePositionWithoutVerification(); + } + + bool newPosConflict = PhotonLobbyRoom.IsValidPlayerPos(newPos) + && !string.IsNullOrEmpty(playerUserIds[newPos - 1]) + && playerUserIds[newPos - 1] != roomPlayer.UserId; + + if (newPosConflict) + { + newPos = PhotonBattleRoom.PlayerPositionGuest; + } + if (!PhotonLobbyRoom.IsValidPlayerPos(newPos)) continue; // Setting the new position to player and room properties and waiting until it's synced @@ -4019,6 +4557,48 @@ private IEnumerator StartTheGameplay(bool isCloseRoom, string blueTeamName, stri playerCount += 1; } + if (PhotonRealtimeClient.InMatchmakingRoom) + { + HashSet mappedHumanUserIds = new( + playerUserIds.Where(uid => !string.IsNullOrEmpty(uid) && uid != "Bot"), + StringComparer.Ordinal); + + List missingHumanUserIds = players + .Where(p => p != null && !string.IsNullOrEmpty(p.UserId) && p.UserId != "Bot") + .Select(p => p.UserId) + .Where(uid => !mappedHumanUserIds.Contains(uid)) + .Distinct(StringComparer.Ordinal) + .ToList(); + + if (missingHumanUserIds.Count > 0) + { + Debug.LogWarning($"StartTheGameplay: refusing partial start because {missingHumanUserIds.Count} human players are missing from start slots. missing=[{string.Join(",", missingHumanUserIds)}], slots=[{string.Join(",", playerUserIds)}]."); + + _lastStartCancelTime = Time.time; + SafeRaiseEvent( + PhotonRealtimeClient.PhotonEvent.CancelGameStart, + null, + new RaiseEventArgs { Receivers = ReceiverGroup.All }, + SendOptions.SendReliable + ); + + if (_startGameHolder != null) + { + StopCoroutine(_startGameHolder); + _startGameHolder = null; + } + + if (_matchmakingHolder != null) + { + StopCoroutine(_matchmakingHolder); + _matchmakingHolder = null; + } + + _matchmakingHolder = StartCoroutine(WaitForMatchmakingPlayers()); + yield break; + } + } + int j = 1; foreach(PlayerType type in playerTypes) { @@ -4529,6 +5109,24 @@ private IEnumerator ReserveFreePosition(bool setToPlayerProperties = false) freePosition = PhotonLobbyRoom.GetFirstFreePlayerPos(); if (!PhotonLobbyRoom.IsValidPlayerPos(freePosition)) { + // Queue-formed rooms can be full with pre-reserved slots for expected users. + // If our user is already reserved, sync local player property instead of treating this as a kick. + int reservedPosition = GetReservedRoomPositionForUser(PhotonRealtimeClient.LocalPlayer?.UserId ?? string.Empty); + if (PhotonLobbyRoom.IsValidPlayerPos(reservedPosition)) + { + success = true; + if (setToPlayerProperties) + { + try + { + PhotonRealtimeClient.LocalPlayer.SetCustomProperty(PlayerPositionKey, reservedPosition); + Debug.Log($"ReserveFreePosition: room is full but local user already reserved in slot {reservedPosition}; synchronized PlayerPositionKey."); + } + catch (Exception ex) { Debug.LogWarning($"ReserveFreePosition: failed to set reserved local position: {ex.Message}"); } + } + break; + } + this.Publish(new(GetKickedEvent.ReasonType.FullRoom)); yield break; } @@ -5445,9 +6043,13 @@ public void OnJoinedRoom() { _gamePlayedOut = false; _matchHasStartedInCurrentRoom = false; + _countdownActive = false; + _lastCountdownStartTime = -100f; _pendingInRoomInviteRoomName = string.Empty; _pendingAcceptedInRoomInviteRoomName = string.Empty; _pendingAcceptedInRoomInviteStartTime = -100f; + _queuePendingLeaderUntil.Clear(); + _queuePendingExpectedUserUntil.Clear(); // Enable: PhotonNetwork.CloseConnection needs to to work across all clients - to kick off invalid players! PhotonRealtimeClient.EnableCloseConnection = true; @@ -5525,6 +6127,7 @@ public void OnJoinedRoom() try { bool isQueueRoom = false; + bool queueExpectedJoinFlowActive = false; try { var curr = PhotonRealtimeClient.CurrentRoom; @@ -5542,15 +6145,24 @@ public void OnJoinedRoom() bool queueFormedMatch = false; int expectedFollowers = 0; string[] expectedUsers = null; + string[] photonExpectedUsers = null; try { queueFormedMatch = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(QueueFormedMatchKey, false); } catch { } try { expectedFollowers = PhotonRealtimeClient.CurrentRoom.GetCustomProperty("qe", 0); } catch { } try { expectedUsers = PhotonRealtimeClient.CurrentRoom.GetCustomProperty("eu", null); } catch { } + try { photonExpectedUsers = PhotonRealtimeClient.CurrentRoom.ExpectedUsers; } catch { } bool hasExpectedUsers = expectedUsers != null && expectedUsers.Any(uid => !string.IsNullOrEmpty(uid)); - bool queueExpectedJoinFlow = queueFormedMatch || expectedFollowers > 0 || hasExpectedUsers; + bool hasPhotonExpectedUsers = photonExpectedUsers != null && photonExpectedUsers.Any(uid => !string.IsNullOrEmpty(uid)); + if (!hasExpectedUsers && hasPhotonExpectedUsers) + { + expectedUsers = photonExpectedUsers; + hasExpectedUsers = true; + } + bool queueExpectedJoinFlowForWatcher = queueFormedMatch || expectedFollowers > 0 || hasExpectedUsers || hasPhotonExpectedUsers; + queueExpectedJoinFlowActive = queueExpectedJoinFlowForWatcher; if (_joinTimeoutWatcherHolder != null) { StopCoroutine(_joinTimeoutWatcherHolder); _joinTimeoutWatcherHolder = null; } - if (!queueExpectedJoinFlow) + if (!queueExpectedJoinFlowForWatcher) { float effectiveTimeout = MatchmakingJoinTimeoutSeconds; Debug.Log($"OnJoinedRoom: non-master joined matchmaking room '{PhotonRealtimeClient.CurrentRoom?.Name}' with PlayerCount={PhotonRealtimeClient.CurrentRoom?.PlayerCount}, starting MatchmakingJoinWatcher(timeout={effectiveTimeout}s) (qe={PhotonRealtimeClient.CurrentRoom?.GetCustomProperty("qe", -999)})"); @@ -5558,17 +6170,38 @@ public void OnJoinedRoom() } else { - Debug.Log($"OnJoinedRoom: non-master joined queue-formed matchmaking room '{PhotonRealtimeClient.CurrentRoom?.Name}' (qe={expectedFollowers}, euCount={(expectedUsers?.Length ?? 0)}); skipping MatchmakingJoinWatcher."); + Debug.Log($"OnJoinedRoom: non-master joined queue-formed matchmaking room '{PhotonRealtimeClient.CurrentRoom?.Name}' (qe={expectedFollowers}, euCount={(expectedUsers?.Length ?? 0)}, photonExpectedCount={(photonExpectedUsers?.Length ?? 0)}); skipping MatchmakingJoinWatcher."); } - // Attempt to reserve a free position for non-master clients so start proceeds quicker - try + // Attempt to reserve a free position for non-master clients when no slot is already reserved. + try + { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId ?? string.Empty; + int reservedPosition = GetReservedRoomPositionForUser(localUserId); + + if (PhotonLobbyRoom.IsValidPlayerPos(reservedPosition)) { - if (_reserveFreePositionHolder == null) + int localPosition = PhotonRealtimeClient.LocalPlayer.HasCustomProperty(PlayerPositionKey) + ? PhotonRealtimeClient.LocalPlayer.GetCustomProperty(PlayerPositionKey) + : PlayerPositionGuest; + + if (localPosition != reservedPosition) { - _reserveFreePositionHolder = StartCoroutine(ReserveFreePosition(true)); + PhotonRealtimeClient.LocalPlayer.SetCustomProperty(PlayerPositionKey, reservedPosition); + Debug.Log($"OnJoinedRoom: synchronized local PlayerPositionKey to reserved room slot {reservedPosition}."); } } - catch (Exception ex) { Debug.LogWarning($"OnJoinedRoom: failed to start ReserveFreePosition: {ex.Message}"); } + else if (!PhotonRealtimeClient.LocalPlayer.HasCustomProperty(PlayerPositionKey) && _reserveFreePositionHolder == null) + { + _reserveFreePositionHolder = StartCoroutine(ReserveFreePosition(true)); + } + } + catch (Exception ex) { Debug.LogWarning($"OnJoinedRoom: failed to ensure local position reservation: {ex.Message}"); } + + if (queueExpectedJoinFlowActive) + { + // Queue-formed expected-user rooms can temporarily look sparse for followers; avoid false auto-requeue. + try { _autoRequeueAttempts = 0; } catch { } + } } } catch (Exception ex) { Debug.LogWarning($"OnJoinedRoom: start join watcher failed: {ex.Message}"); } @@ -5586,7 +6219,24 @@ public void OnJoinedRoom() } catch { } - if (!PhotonRealtimeClient.LocalPlayer.IsMasterClient && PhotonRealtimeClient.CurrentRoom != null && PhotonRealtimeClient.CurrentRoom.PlayerCount <= 1 && !inCustomRoom) + bool queueExpectedJoinFlow = false; + try + { + if (PhotonRealtimeClient.CurrentRoom != null) + { + bool queueFormedMatch = PhotonRealtimeClient.CurrentRoom.GetCustomProperty(QueueFormedMatchKey, false); + int expectedFollowers = PhotonRealtimeClient.CurrentRoom.GetCustomProperty("qe", 0); + string[] expectedUsers = PhotonRealtimeClient.CurrentRoom.GetCustomProperty("eu", null); + string[] photonExpectedUsers = PhotonRealtimeClient.CurrentRoom.ExpectedUsers; + + bool hasExpectedUsers = expectedUsers != null && expectedUsers.Any(uid => !string.IsNullOrEmpty(uid)); + bool hasPhotonExpectedUsers = photonExpectedUsers != null && photonExpectedUsers.Any(uid => !string.IsNullOrEmpty(uid)); + queueExpectedJoinFlow = queueFormedMatch || expectedFollowers > 0 || hasExpectedUsers || hasPhotonExpectedUsers; + } + } + catch { } + + if (!PhotonRealtimeClient.LocalPlayer.IsMasterClient && PhotonRealtimeClient.CurrentRoom != null && PhotonRealtimeClient.CurrentRoom.PlayerCount <= 1 && !inCustomRoom && !queueExpectedJoinFlow) { Debug.Log($"OnJoinedRoom: non-master appears alone in matchmaking room (PlayerCount={PhotonRealtimeClient.CurrentRoom.PlayerCount}); starting auto-requeue."); if (_autoJoinHolder == null) @@ -5610,9 +6260,12 @@ public void OnLeftRoom() // IMatchmakingCallbacks { _gamePlayedOut = false; _matchHasStartedInCurrentRoom = false; + _countdownActive = false; + _lastCountdownStartTime = -100f; _pendingInRoomInviteRoomName = string.Empty; _pendingAcceptedInRoomInviteRoomName = string.Empty; _pendingAcceptedInRoomInviteStartTime = -100f; + _queuePendingExpectedUserUntil.Clear(); // Clearing player position key from own custom properties if (PhotonRealtimeClient.LocalPlayer.HasCustomProperty(PlayerPositionKey)) PhotonRealtimeClient.LocalPlayer.RemoveCustomProperty(PlayerPositionKey); @@ -6176,6 +6829,54 @@ public void OnEvent(EventData photonEvent) bool leaderMatch = leaderUserId == matchmakingLeaderId; bool hasExplicitLeaderRoom = !string.IsNullOrEmpty(leaderRoomName); bool hasExpectedUsers = expectedUsers != null && expectedUsers.Length > 0; + bool isQueueLeaderHandoff = isQueueRoom + && hasExplicitLeaderRoom + && hasExpectedUsers + && !string.IsNullOrEmpty(leaderUserId) + && leaderRoomName.StartsWith("Queue_", StringComparison.Ordinal); + + if (isQueueLeaderHandoff) + { + _queuePendingLeaderUntil[leaderUserId] = Time.time + QueuePendingLeaderGraceSeconds; + int pendingExpectedAdded = 0; + try + { + HashSet presentQueueUsers = new( + PhotonRealtimeClient.CurrentRoom.Players.Values + .Where(p => p != null && !string.IsNullOrEmpty(p.UserId) && p.UserId != "Bot") + .Select(p => p.UserId), + StringComparer.Ordinal); + + foreach (string expectedUserId in expectedUsers) + { + if (string.IsNullOrEmpty(expectedUserId) || expectedUserId == leaderUserId) continue; + if (presentQueueUsers.Contains(expectedUserId)) continue; + + _queuePendingExpectedUserUntil[expectedUserId] = Time.time + QueuePendingLeaderGraceSeconds; + pendingExpectedAdded++; + } + } + catch (Exception ex) + { + Debug.LogWarning($"RoomChangeRequested: failed to record pending expected queue users: {ex.Message}"); + } + + Debug.Log($"RoomChangeRequested: recorded pending queue duo handoff for leader {leaderUserId} (expectedUsers={expectedUsers.Length}, grace={QueuePendingLeaderGraceSeconds}s)."); + if (pendingExpectedAdded > 0) + { + Debug.Log($"RoomChangeRequested: recorded {pendingExpectedAdded} unresolved expected queue users for pending duo handoff."); + } + } + + string[] followExpectedUsers = null; + if (hasExpectedUsers && shouldFollow) + { + string localUserId = PhotonRealtimeClient.LocalPlayer?.UserId; + followExpectedUsers = expectedUsers + .Where(uid => !string.IsNullOrEmpty(uid) && uid != localUserId) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } bool leaderOnlyNoRoomFallback = !hasExplicitLeaderRoom && !hasExpectedUsers; bool alreadyInExplicitLeaderRoom = false; @@ -6205,11 +6906,15 @@ public void OnEvent(EventData photonEvent) { if (isQueueRoom) { - if (_followLeaderHolder == null) + if (_followLeaderHolder != null) { - Debug.Log("RoomChangeRequested: targeted queue pre-notify without explicit room name, starting immediate follow flow."); - _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId)); + try { StopCoroutine(_followLeaderHolder); } catch { } + _followLeaderHolder = null; + Debug.Log("RoomChangeRequested: replacing stale follow coroutine for targeted queue pre-notify handoff."); } + + Debug.Log("RoomChangeRequested: targeted queue pre-notify without explicit room name, starting immediate follow flow."); + _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId, null, followExpectedUsers)); } else { @@ -6233,8 +6938,8 @@ public void OnEvent(EventData photonEvent) if (_followLeaderHolder == null) { - if (hasExplicitLeaderRoom) _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId, leaderRoomName)); - else _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId)); + if (hasExplicitLeaderRoom) _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId, leaderRoomName, followExpectedUsers)); + else _followLeaderHolder = StartCoroutine(FollowLeaderToNewRoom(leaderUserId, null, followExpectedUsers)); } } else if (isCustomRoom) @@ -6357,6 +7062,17 @@ public void OnPlayerEnteredRoom(Player newPlayer) public void OnMasterClientSwitched(Player newMasterClient) { LobbyOnMasterClientSwitched?.Invoke(new(newMasterClient)); + bool currentRoomIsQueue = false; + try + { + Room room = PhotonRealtimeClient.CurrentRoom; + currentRoomIsQueue = room != null + && room.CustomProperties != null + && room.CustomProperties.ContainsKey(PhotonBattleRoom.IsQueueKey) + && room.GetCustomProperty(PhotonBattleRoom.IsQueueKey); + } + catch { } + // Cancel any in-progress countdown locally when master changes (previous master might have started it) if (_startGameHolder != null) { @@ -6372,7 +7088,7 @@ public void OnMasterClientSwitched(Player newMasterClient) { { var room = PhotonRealtimeClient.CurrentRoom; bool wasStarting = !_matchHasStartedInCurrentRoom && IsGameStartTransitionActive(); - if (wasStarting && PhotonRealtimeClient.InMatchmakingRoom && !PhotonRealtimeClient.LocalPlayer.IsMasterClient) + if (wasStarting && PhotonRealtimeClient.InMatchmakingRoom && !currentRoomIsQueue && !PhotonRealtimeClient.LocalPlayer.IsMasterClient) { // Mirror CancelGameStart handling with requeue=true for non-master clients _lastStartCancelTime = Time.time; @@ -6404,7 +7120,7 @@ public void OnMasterClientSwitched(Player newMasterClient) { } // If this client became the new master while a countdown was active, // take over leader-led requeue (create new matchmaking room) so players follow. - if (wasStarting && PhotonRealtimeClient.InMatchmakingRoom && PhotonRealtimeClient.LocalPlayer.IsMasterClient) + if (wasStarting && PhotonRealtimeClient.InMatchmakingRoom && !currentRoomIsQueue && PhotonRealtimeClient.LocalPlayer.IsMasterClient) { _lastStartCancelTime = Time.time; try { if (_matchmakingHolder != null) { StopCoroutine(_matchmakingHolder); _matchmakingHolder = null; } } catch { } @@ -6480,12 +7196,26 @@ public void OnMasterClientSwitched(Player newMasterClient) { } // If we are in a matchmaking room, new master should continue matchmaking; others stay and wait - if (PhotonRealtimeClient.InMatchmakingRoom) + if (PhotonRealtimeClient.InMatchmakingRoom && !currentRoomIsQueue) { + bool isQueueRoom = false; + try + { + Room currentRoom = PhotonRealtimeClient.CurrentRoom; + isQueueRoom = currentRoom != null + && currentRoom.CustomProperties != null + && currentRoom.CustomProperties.ContainsKey(PhotonBattleRoom.IsQueueKey) + && currentRoom.GetCustomProperty(PhotonBattleRoom.IsQueueKey); + } + catch { } + // Update local player's known leader id to the current master so returning/disconnected players don't reclaim leadership. try { - PhotonRealtimeClient.LocalPlayer.SetCustomProperty(PhotonBattleRoom.LeaderIdKey, newMasterClient.UserId); + if (!isQueueRoom) + { + PhotonRealtimeClient.LocalPlayer.SetCustomProperty(PhotonBattleRoom.LeaderIdKey, newMasterClient.UserId); + } try { OnRoomLeaderChanged?.Invoke(newMasterClient.UserId == PhotonRealtimeClient.LocalPlayer.UserId); } catch { } } catch (Exception ex) From ed1fcd7b7fd1aa58dbe9834cb24ccb5c21f29af7 Mon Sep 17 00:00:00 2001 From: Davetsa Date: Thu, 16 Apr 2026 09:07:07 +0300 Subject: [PATCH 09/39] feat(lobby): implement battle start checks and UI readiness notifications --- Assets/Altzone/Scripts/Photon/LobbyManager.cs | 84 ++++++++++++++++++- .../Windows/HomeScreen/KotiView.prefab | 7 +- .../Scripts/MainMenu/GameModeChoiceScript.cs | 72 +++++++++------- 3 files changed, 129 insertions(+), 34 deletions(-) diff --git a/Assets/Altzone/Scripts/Photon/LobbyManager.cs b/Assets/Altzone/Scripts/Photon/LobbyManager.cs index 77a56f0782..b0e7e3f663 100644 --- a/Assets/Altzone/Scripts/Photon/LobbyManager.cs +++ b/Assets/Altzone/Scripts/Photon/LobbyManager.cs @@ -131,6 +131,7 @@ public class LobbyManager : MonoBehaviour, ILobbyCallbacks, IMatchmakingCallback private Coroutine _serviceHolder = null; private Coroutine _autoJoinHolder = null; private Coroutine _verifyPositionsHolder = null; + private Coroutine _canBattleStartCheckHolder = null; private Coroutine _queueTimerHolder = null; private const float QueueWaitSeconds = 30f; private const float QueueReadyStartDelaySeconds = 2f; @@ -181,6 +182,7 @@ public class LobbyManager : MonoBehaviour, ILobbyCallbacks, IMatchmakingCallback private static bool _isActive = false; private static bool _gamePlayedOut = false; + private static bool _battleStartUiReady = false; public bool RunnerActive => _runner != null; #endregion @@ -307,12 +309,65 @@ public static void NotifyGamePlayedOut() _gamePlayedOut = true; } + public static void NotifyBattleStartUiReady() + { + _battleStartUiReady = true; + } + private bool IsGameStartTransitionActive() { // BattleID can remain set after match end, so rely on transient runtime flags instead. return !_gamePlayedOut && (_countdownActive || _startGameHolder != null || _startQuantumHolder != null); } + private void QueueCustomBattleStartCheck() + { + if (_canBattleStartCheckHolder != null) return; + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) return; + if (PhotonRealtimeClient.LocalPlayer == null || !PhotonRealtimeClient.LocalPlayer.IsMasterClient) return; + + GameType gameType; + try + { + gameType = (GameType)PhotonRealtimeClient.CurrentRoom.GetCustomProperty(PhotonBattleRoom.GameTypeKey); + } + catch + { + return; + } + + if (gameType != GameType.Custom) return; + _canBattleStartCheckHolder = StartCoroutine(CheckIfBattleCanStart()); + } + + private IEnumerator CheckIfBattleCanStart() + { + try + { + yield return new WaitUntil(() => _posChangeQueue.Count == 0 && !_playerPosChangeInProgress); + + if (!PhotonRealtimeClient.InRoom || PhotonRealtimeClient.CurrentRoom == null) yield break; + if (PhotonRealtimeClient.LocalPlayer == null || !PhotonRealtimeClient.LocalPlayer.IsMasterClient) yield break; + if (_startGameHolder != null || _startQuantumHolder != null) yield break; + + Room room = PhotonRealtimeClient.CurrentRoom; + if (room.PlayerCount != room.MaxPlayers) yield break; + + if (CheckIfAllPlayersInPosition()) + { + GameType gameType = (GameType)room.GetCustomProperty(PhotonBattleRoom.GameTypeKey); + if (gameType == GameType.Custom) + { + OnStartPlayingEvent(new StartPlayingEvent()); + } + } + } + finally + { + _canBattleStartCheckHolder = null; + } + } + public void AcceptInRoomInvite(string roomName) { if (string.IsNullOrEmpty(roomName)) return; @@ -2238,6 +2293,12 @@ private void StopHolderCoroutines() StopCoroutine(_followLeaderHolder); _followLeaderHolder = null; } + + if (_canBattleStartCheckHolder != null) + { + StopCoroutine(_canBattleStartCheckHolder); + _canBattleStartCheckHolder = null; + } } private IEnumerator LeaveAndAutoRequeue(GameType gameType) @@ -2525,6 +2586,12 @@ public void StopMatchmakingCoroutines() StopCoroutine(_autoJoinHolder); _autoJoinHolder = null; } + + if (_canBattleStartCheckHolder != null) + { + StopCoroutine(_canBattleStartCheckHolder); + _canBattleStartCheckHolder = null; + } } /// @@ -4888,9 +4955,22 @@ bool AreAllExpectedPlayersPresent() long sendTime = data.StartTime; // Start Battle Countdown (request UI to show countdown) + _battleStartUiReady = false; OnLobbyWindowChangeRequest?.Invoke(LobbyWindowTarget.BattleLoad); _isStartFinished = false; + // Wait for BattleLoad UI to initialize and signal readiness. + const float onStartUiReadyTimeout = 5f; + float uiReadyStart = Time.time; + while (!_battleStartUiReady && Time.time - uiReadyStart < onStartUiReadyTimeout) + { + yield return null; + } + if (!_battleStartUiReady) + { + Debug.LogWarning("StartQuantum: Battle start UI was not ready after timeout; proceeding."); + } + // Wait for UI to subscribe to OnStartTimeSet (timeout to avoid hanging) const float onStartSubscribeTimeout = 5f; float subscribeStart = Time.time; @@ -6711,7 +6791,7 @@ public void OnEvent(EventData photonEvent) } else Debug.LogError($"Player {photonEvent.Sender} not found in room"); - // previously started a no-op battle-start check coroutine; removed as unused + QueueCustomBattleStartCheck(); break; case PhotonRealtimeClient.PhotonEvent.RoomChangeRequested: @@ -7006,7 +7086,7 @@ public void OnPlayerEnteredRoom(Player newPlayer) { if (playerCount + botCount == room.MaxPlayers && room.IsOpen) PhotonRealtimeClient.CloseRoom(); - // Previously would start a CheckIfBattleCanStart coroutine here; removed as it's unused. + QueueCustomBattleStartCheck(); // Ensure master continues matchmaking loop so countdowns can be restarted when new players join if (PhotonRealtimeClient.InMatchmakingRoom && _matchmakingHolder == null) diff --git a/Assets/MenuUi/Prefabs/Windows/HomeScreen/KotiView.prefab b/Assets/MenuUi/Prefabs/Windows/HomeScreen/KotiView.prefab index cf1ce527a6..1d0f215132 100644 --- a/Assets/MenuUi/Prefabs/Windows/HomeScreen/KotiView.prefab +++ b/Assets/MenuUi/Prefabs/Windows/HomeScreen/KotiView.prefab @@ -535,12 +535,15 @@ MonoBehaviour: _gameModeButtons: - {fileID: 4091377289515798502} - {fileID: 2829572788807415862} + - {fileID: 7233449278219608806} _gameModeHeaders: - {fileID: 2489717828359045447} - {fileID: 4144495222954848854} + - {fileID: 4144495222954848854} _gameModeButtonsAsButtons: - {fileID: 5177981418448575191} - {fileID: 4032366852551734761} + - {fileID: 5200448605748152348} --- !u!1 &2489717828359045447 GameObject: m_ObjectHideFlags: 0 @@ -733,7 +736,7 @@ MonoBehaviour: m_PressedTrigger: Pressed m_SelectedTrigger: Selected m_DisabledTrigger: Disabled - m_Interactable: 0 + m_Interactable: 1 m_TargetGraphic: {fileID: 6769611933994000033} m_OnClick: m_PersistentCalls: @@ -1710,7 +1713,7 @@ MonoBehaviour: m_PressedTrigger: Pressed m_SelectedTrigger: Selected m_DisabledTrigger: Disabled - m_Interactable: 0 + m_Interactable: 1 m_TargetGraphic: {fileID: 7682855185394912867} m_OnClick: m_PersistentCalls: diff --git a/Assets/MenuUi/Scripts/MainMenu/GameModeChoiceScript.cs b/Assets/MenuUi/Scripts/MainMenu/GameModeChoiceScript.cs index 5abd8dfe0c..606bef3cfa 100644 --- a/Assets/MenuUi/Scripts/MainMenu/GameModeChoiceScript.cs +++ b/Assets/MenuUi/Scripts/MainMenu/GameModeChoiceScript.cs @@ -2,6 +2,8 @@ using UnityEngine; using Prg.Scripts.Common; using UnityEngine.UI; +using Altzone.Scripts.Lobby; +using MenuUi.Scripts.Signals; public class GameModeChoiceScript : MonoBehaviour @@ -10,6 +12,7 @@ public class GameModeChoiceScript : MonoBehaviour [SerializeField] private List _gameModeButtons = new(); [SerializeField] private List _gameModeHeaders = new(); [SerializeField] private List