diff --git a/Runtime/Client/LootLockerHTTPClient.cs b/Runtime/Client/LootLockerHTTPClient.cs index db67275b..22c5da80 100644 --- a/Runtime/Client/LootLockerHTTPClient.cs +++ b/Runtime/Client/LootLockerHTTPClient.cs @@ -625,7 +625,10 @@ private HTTPExecutionQueueProcessingResult ProcessOngoingRequest(LootLockerHTTPE if (ShouldRetryRequest(executionItem.WebRequest.responseCode, executionItem.RequestData.TimesRetried) && !(executionItem.WebRequest.responseCode == 401 && !IsAuthorizedRequest(executionItem))) { var playerData = LootLockerStateData.GetPlayerDataForPlayerWithUlidWithoutChangingState(executionItem.RequestData.ForPlayerWithUlid); - if (ShouldRefreshSession(executionItem, playerData == null ? LL_AuthPlatforms.None : playerData.CurrentPlatform.Platform) && (CanRefreshUsingRefreshToken(executionItem.RequestData) || CanStartNewSessionUsingCachedAuthData(executionItem.RequestData.ForPlayerWithUlid))) + bool shouldRefreshSession = ShouldRefreshSession(executionItem, playerData == null ? LL_AuthPlatforms.None : playerData.CurrentPlatform.Platform); + bool canRefreshWithToken = CanRefreshUsingRefreshToken(executionItem.RequestData); + bool canReAuth = CanStartNewSessionUsingCachedAuthData(executionItem.RequestData.ForPlayerWithUlid); + if (shouldRefreshSession && (canRefreshWithToken || canReAuth)) { return HTTPExecutionQueueProcessingResult.NeedsSessionRefresh; } @@ -909,7 +912,10 @@ private static bool ShouldRetryRequest(long statusCode, int timesRetried) private static bool ShouldRefreshSession(LootLockerHTTPExecutionQueueItem request, LL_AuthPlatforms platform) { - return IsAuthorizedGameRequest(request) && (request.WebRequest?.responseCode == 401 || request.WebRequest?.responseCode == 403) && LootLockerConfig.current.allowTokenRefresh && !new List{ LL_AuthPlatforms.Steam, LL_AuthPlatforms.NintendoSwitch, LL_AuthPlatforms.None }.Contains(platform); + bool isAuthorized = IsAuthorizedRequest(request); + bool isRefreshableResponseCode = (request.WebRequest?.responseCode == 401 || request.WebRequest?.responseCode == 403); + bool isRefreshablePlatform = LootLockerConfig.current.allowTokenRefresh && !new List{ LL_AuthPlatforms.Steam, LL_AuthPlatforms.NintendoSwitch, LL_AuthPlatforms.None }.Contains(platform); + return isAuthorized && isRefreshableResponseCode && isRefreshablePlatform; } private static bool IsAuthorizedRequest(LootLockerHTTPExecutionQueueItem request) diff --git a/Runtime/Client/LootLockerLifecycleManager.cs b/Runtime/Client/LootLockerLifecycleManager.cs index 5a47292a..e7b8c3f6 100644 --- a/Runtime/Client/LootLockerLifecycleManager.cs +++ b/Runtime/Client/LootLockerLifecycleManager.cs @@ -11,6 +11,11 @@ namespace LootLocker /// public enum LifecycleManagerState { + /// + /// Manager is not initialized or has been destroyed - can be recreated + /// + Uninitialized, + /// /// Normal operation - services can be accessed and managed /// @@ -132,6 +137,9 @@ private static void TeardownInstance() _instanceId = 0; _hostingGameObject = null; } + + // Set to Uninitialized after teardown to allow recreation + _state = LifecycleManagerState.Uninitialized; } } @@ -180,7 +188,7 @@ static void OnEnterPlaymodeInEditor(UnityEditor.EnterPlayModeOptions options) private bool _isInitialized = false; private bool _serviceHealthMonitoringEnabled = true; private Coroutine _healthMonitorCoroutine = null; - private static LifecycleManagerState _state = LifecycleManagerState.Ready; + private static LifecycleManagerState _state = LifecycleManagerState.Uninitialized; private readonly object _serviceLock = new object(); /// diff --git a/Runtime/Client/LootLockerPresenceClient.cs b/Runtime/Client/LootLockerPresenceClient.cs index 6ec7abb8..502bb554 100644 --- a/Runtime/Client/LootLockerPresenceClient.cs +++ b/Runtime/Client/LootLockerPresenceClient.cs @@ -246,6 +246,11 @@ public class LootLockerPresenceClient : MonoBehaviour, IDisposable /// public string PlayerUlid => playerUlid; + /// + /// The session token this client is using for authentication + /// + public string SessionToken => sessionToken; + /// /// The last status that was sent to the server (e.g., "online", "in_game", "away") /// @@ -384,6 +389,14 @@ internal void Initialize(string playerUlid, string sessionToken) ChangeConnectionState(LootLockerPresenceConnectionState.Initializing); } + /// + /// Update the session token for this client (used during token refresh) + /// + internal void UpdateSessionToken(string newSessionToken) + { + this.sessionToken = newSessionToken; + } + /// /// Connect to the Presence WebSocket /// @@ -983,6 +996,14 @@ private void HandleAuthenticationResponse(string message) pingCoroutine = StartCoroutine(PingCoroutine()); + // Auto-resend last status if we have one + if (!string.IsNullOrEmpty(connectionStats.lastSentStatus)) + { + LootLockerLogger.Log($"Auto-resending last status '{connectionStats.lastSentStatus}' after reconnection", LootLockerLogger.LogLevel.Debug); + // Use a coroutine to avoid blocking the authentication flow + StartCoroutine(AutoResendLastStatusCoroutine()); + } + // Reset reconnect attempts on successful authentication reconnectAttempts = 0; } @@ -1124,6 +1145,33 @@ private IEnumerator ScheduleReconnectCoroutine(float customDelay = -1f) } } + /// + /// Coroutine to auto-resend the last status after successful reconnection + /// + private IEnumerator AutoResendLastStatusCoroutine() + { + // Wait a frame to ensure we're fully connected + yield return null; + + // Double-check we're still connected and have a status to send + if (IsConnectedAndAuthenticated && !string.IsNullOrEmpty(connectionStats.lastSentStatus)) + { + // Find the last sent metadata if any + // Note: We don't store metadata currently, so we'll resend with null metadata + // This could be enhanced later if metadata preservation is needed + UpdateStatus(connectionStats.lastSentStatus, null, (success, error) => { + if (success) + { + LootLockerLogger.Log($"Successfully auto-resent status '{connectionStats.lastSentStatus}' after reconnection", LootLockerLogger.LogLevel.Debug); + } + else + { + LootLockerLogger.Log($"Failed to auto-resend status after reconnection: {error}", LootLockerLogger.LogLevel.Warning); + } + }); + } + } + #endregion } } \ No newline at end of file diff --git a/Runtime/Client/LootLockerPresenceManager.cs b/Runtime/Client/LootLockerPresenceManager.cs index e5685253..a741d95a 100644 --- a/Runtime/Client/LootLockerPresenceManager.cs +++ b/Runtime/Client/LootLockerPresenceManager.cs @@ -25,6 +25,7 @@ public class LootLockerPresenceManager : MonoBehaviour, ILootLockerService // Instance fields private readonly Dictionary _activeClients = new Dictionary(); + private readonly Dictionary _disconnectedClients = new Dictionary(); // Track disconnected but not destroyed clients private readonly HashSet _connectingClients = new HashSet(); // Track clients that are in the process of connecting private readonly object _activeClientsLock = new object(); // Thread safety for _activeClients dictionary private bool _isEnabled = true; @@ -88,7 +89,9 @@ public static IEnumerable ActiveClientUlids lock (instance._activeClientsLock) { - return new List(instance._activeClients.Keys); + var result = new List(instance._activeClients.Keys); + result.AddRange(instance._disconnectedClients.Keys); + return result; } } } @@ -106,6 +109,12 @@ public static IEnumerable ActiveClientUlids /// public static LootLockerPresenceManager Get() { + // During Unity shutdown, don't create new instances + if (!Application.isPlaying) + { + return _instance; + } + if (_instance != null) { return _instance; @@ -170,7 +179,10 @@ public void SetEventSystem(LootLockerEventSystem eventSystemInstance) void ILootLockerService.Reset() { - _DisconnectAll(); + if (!_isShuttingDown) + { + _DestroyAllClients(); + } _UnsubscribeFromEvents(); @@ -182,52 +194,50 @@ void ILootLockerService.Reset() } } - // TODO: Handle pause/focus better to avoid concurrency issues void ILootLockerService.HandleApplicationPause(bool pauseStatus) { - if(!IsInitialized || !_autoDisconnectOnFocusChange || !_isEnabled) + if(!IsInitialized || !_isEnabled) { return; } - if (pauseStatus) + if (pauseStatus && _autoDisconnectOnFocusChange) { - LootLockerLogger.Log("Application paused - disconnecting all presence connections (auto-disconnect enabled)", LootLockerLogger.LogLevel.Debug); DisconnectAll(); } else { - LootLockerLogger.Log("Application resumed - will reconnect presence connections", LootLockerLogger.LogLevel.Debug); StartCoroutine(_AutoConnectExistingSessions()); } } void ILootLockerService.HandleApplicationFocus(bool hasFocus) { - if(!IsInitialized || !_autoDisconnectOnFocusChange || !_isEnabled) + if(!IsInitialized || !_isEnabled) return; if (hasFocus) { // App gained focus - ensure presence is reconnected - LootLockerLogger.Log("Application gained focus - ensuring presence connections (auto-disconnect enabled)", LootLockerLogger.LogLevel.Debug); StartCoroutine(_AutoConnectExistingSessions()); } - else + else if (_autoDisconnectOnFocusChange) { // App lost focus - disconnect presence to save resources - LootLockerLogger.Log("Application lost focus - disconnecting presence (auto-disconnect enabled)", LootLockerLogger.LogLevel.Debug); DisconnectAll(); } } void ILootLockerService.HandleApplicationQuit() { - _isShuttingDown = true; - - _UnsubscribeFromEvents(); - _DisconnectAll(); - _connectedSessions?.Clear(); + if (!_isShuttingDown) + { + _isShuttingDown = true; + + _UnsubscribeFromEvents(); + _DestroyAllClients(); + _connectedSessions?.Clear(); + } } #endregion @@ -361,17 +371,32 @@ private void _HandleSessionStartedEvent(LootLockerSessionStartedEventData eventD var playerData = eventData.playerData; if (playerData != null && !string.IsNullOrEmpty(playerData.ULID)) { - LootLockerLogger.Log($"Session started event received for {playerData.ULID}, auto-connecting presence", LootLockerLogger.LogLevel.Debug); + LootLockerLogger.Log($"Session started event received for {playerData.ULID}, checking for existing clients", LootLockerLogger.LogLevel.Debug); - // Create and initialize client immediately, but defer connection - var client = _CreatePresenceClientWithoutConnecting(playerData); - if (client == null) + // Check if we have existing clients (active or disconnected) + bool hasExistingClient = false; + lock (_activeClientsLock) { - return; + hasExistingClient = _activeClients.ContainsKey(playerData.ULID) || _disconnectedClients.ContainsKey(playerData.ULID); } - // Start auto-connect in a coroutine to avoid blocking the event thread - StartCoroutine(_DelayPresenceClientConnection(playerData)); + if (hasExistingClient) + { + // Update existing client with new session token and reconnect + _UpdateClientSessionTokenAndReconnect(playerData); + } + else + { + // Create and initialize new client, then defer connection + var client = _CreatePresenceClientWithoutConnecting(playerData); + if (client == null) + { + return; + } + + // Start auto-connect in a coroutine to avoid blocking the event thread + StartCoroutine(_DelayPresenceClientConnection(playerData)); + } } } @@ -388,19 +413,32 @@ private void _HandleSessionRefreshedEvent(LootLockerSessionRefreshedEventData ev var playerData = eventData.playerData; if (playerData != null && !string.IsNullOrEmpty(playerData.ULID)) { - LootLockerLogger.Log($"Session refreshed event received for {playerData.ULID}, reconnecting presence with new token", LootLockerLogger.LogLevel.Debug); + LootLockerLogger.Log($"Session refreshed event received for {playerData.ULID}, checking for existing clients", LootLockerLogger.LogLevel.Debug); - // Disconnect existing connection first, then reconnect with new session token - DisconnectPresence(playerData.ULID, (disconnectSuccess, disconnectError) => { - if (disconnectSuccess) + // Check if we have existing clients (active or disconnected) + bool hasExistingClient = false; + lock (_activeClientsLock) + { + hasExistingClient = _activeClients.ContainsKey(playerData.ULID) || _disconnectedClients.ContainsKey(playerData.ULID); + } + + if (hasExistingClient) + { + // Update existing client with new session token and reconnect + _UpdateClientSessionTokenAndReconnect(playerData); + } + else + { + // Create and initialize new client, then defer connection + var client = _CreatePresenceClientWithoutConnecting(playerData); + if (client == null) { - // Only reconnect if auto-connect is enabled - if (_autoConnectEnabled) - { - ConnectPresence(playerData.ULID); - } + return; } - }); + + // Start auto-connect in a coroutine to avoid blocking the event thread + StartCoroutine(_DelayPresenceClientConnection(playerData)); + } } } @@ -415,8 +453,8 @@ private void _HandleSessionEndedEvent(LootLockerSessionEndedEventData eventData) } if (!string.IsNullOrEmpty(eventData.playerUlid)) { - LootLockerLogger.Log($"Session ended event received for {eventData.playerUlid}, disconnecting presence", LootLockerLogger.LogLevel.Debug); - _DisconnectPresenceForUlid(eventData.playerUlid); + LootLockerLogger.Log($"Session ended event received for {eventData.playerUlid}, destroying presence client", LootLockerLogger.LogLevel.Debug); + _DestroyPresenceClientForUlid(eventData.playerUlid); } } @@ -431,15 +469,15 @@ private void _HandleSessionExpiredEvent(LootLockerSessionExpiredEventData eventD } if (!string.IsNullOrEmpty(eventData.playerUlid)) { - LootLockerLogger.Log($"Session expired event received for {eventData.playerUlid}, disconnecting presence", LootLockerLogger.LogLevel.Debug); - _DisconnectPresenceForUlid(eventData.playerUlid); + LootLockerLogger.Log($"Session expired event received for {eventData.playerUlid}, destroying presence client", LootLockerLogger.LogLevel.Debug); + _DestroyPresenceClientForUlid(eventData.playerUlid); } } /// /// Handle local session deactivated events - /// Note: If this is part of a session end flow, presence will already be disconnected by _HandleSessionEndedEvent - /// This handler only disconnects presence for local state management scenarios + /// Note: If this is part of a session end flow, presence will already be destroyed by _HandleSessionEndedEvent + /// This handler destroys presence client for local state management scenarios /// private void _HandleLocalSessionDeactivatedEvent(LootLockerLocalSessionDeactivatedEventData eventData) { @@ -449,8 +487,8 @@ private void _HandleLocalSessionDeactivatedEvent(LootLockerLocalSessionDeactivat } if (!string.IsNullOrEmpty(eventData.playerUlid)) { - LootLockerLogger.Log($"Local session deactivated event received for {eventData.playerUlid}, disconnecting presence", LootLockerLogger.LogLevel.Debug); - _DisconnectPresenceForUlid(eventData.playerUlid); + LootLockerLogger.Log($"Local session deactivated event received for {eventData.playerUlid}, destroying presence client", LootLockerLogger.LogLevel.Debug); + _DestroyPresenceClientForUlid(eventData.playerUlid); } } @@ -478,8 +516,7 @@ private void _HandleLocalSessionActivatedEvent(LootLockerLocalSessionActivatedEv /// private void _HandleClientConnectionStateChanged(LootLockerPresenceConnectionStateChangedEventData eventData) { - if (eventData.newState == LootLockerPresenceConnectionState.Disconnected || - eventData.newState == LootLockerPresenceConnectionState.Failed) + if (eventData.newState == LootLockerPresenceConnectionState.Destroyed) { LootLockerLogger.Log($"Auto-cleaning up presence client for {eventData.playerUlid} due to state change: {eventData.newState}", LootLockerLogger.LogLevel.Debug); @@ -491,6 +528,10 @@ private void _HandleClientConnectionStateChanged(LootLockerPresenceConnectionSta { _activeClients.Remove(eventData.playerUlid); } + else if (_disconnectedClients.TryGetValue(eventData.playerUlid, out clientToCleanup)) + { + _disconnectedClients.Remove(eventData.playerUlid); + } } // Destroy the GameObject to fully clean up resources @@ -499,6 +540,53 @@ private void _HandleClientConnectionStateChanged(LootLockerPresenceConnectionSta UnityEngine.Object.Destroy(clientToCleanup.gameObject); } } + else if (eventData.newState == LootLockerPresenceConnectionState.Disconnected) + { + // Move client from active to disconnected state (don't destroy) + LootLockerPresenceClient clientToMove = null; + lock (_activeClientsLock) + { + if (_activeClients.TryGetValue(eventData.playerUlid, out clientToMove)) + { + _activeClients.Remove(eventData.playerUlid); + _disconnectedClients[eventData.playerUlid] = clientToMove; + LootLockerLogger.Log($"Moved presence client for {eventData.playerUlid} to disconnected state", LootLockerLogger.LogLevel.Debug); + } + } + } + else if (eventData.newState == LootLockerPresenceConnectionState.Failed) + { + // For failed states, we need to check if it's an authentication failure or network failure + // Authentication failures should destroy, network failures should move to disconnected + LootLockerPresenceClient clientToHandle = null; + lock (_activeClientsLock) + { + if (_activeClients.TryGetValue(eventData.playerUlid, out clientToHandle)) + { + _activeClients.Remove(eventData.playerUlid); + } + } + + if (clientToHandle != null) + { + // If the error indicates authentication failure, destroy the client + // Otherwise, move to disconnected state for potential reconnection + if (eventData.errorMessage != null && (eventData.errorMessage.Contains("authentication") || eventData.errorMessage.Contains("unauthorized") || eventData.errorMessage.Contains("invalid token"))) + { + LootLockerLogger.Log($"Destroying presence client for {eventData.playerUlid} due to authentication failure: {eventData.errorMessage}", LootLockerLogger.LogLevel.Debug); + UnityEngine.Object.Destroy(clientToHandle.gameObject); + } + else + { + // Network or other failure - move to disconnected for potential reconnection + lock (_activeClientsLock) + { + _disconnectedClients[eventData.playerUlid] = clientToHandle; + } + LootLockerLogger.Log($"Moved presence client for {eventData.playerUlid} to disconnected state due to failure: {eventData.errorMessage}", LootLockerLogger.LogLevel.Debug); + } + } + } } #endregion @@ -557,6 +645,7 @@ public static void ConnectPresence(string playerUlid = null, LootLockerPresenceC return; } + // Check for active client first if (instance._activeClients.ContainsKey(ulid)) { var existingClient = instance._activeClients[ulid]; @@ -592,6 +681,30 @@ public static void ConnectPresence(string playerUlid = null, LootLockerPresenceC return; } + // Check for disconnected client that can be reused + if (instance._disconnectedClients.ContainsKey(ulid)) + { + var disconnectedClient = instance._disconnectedClients[ulid]; + + // Check if the session token needs to be updated + if (disconnectedClient.SessionToken != playerData.SessionToken) + { + LootLockerLogger.Log($"Session token changed for {ulid}, updating token on existing client", LootLockerLogger.LogLevel.Debug); + // Update the session token on the existing client + disconnectedClient.UpdateSessionToken(playerData.SessionToken); + } + + // Reuse the disconnected client (with updated token if needed) + LootLockerLogger.Log($"Reusing disconnected presence client for {ulid}", LootLockerLogger.LogLevel.Debug); + instance._disconnectedClients.Remove(ulid); + instance._activeClients[ulid] = disconnectedClient; + instance._connectingClients.Add(ulid); + + // Reconnect the existing client outside the lock + instance._ConnectPresenceClient(ulid, disconnectedClient, onComplete); + return; + } + // Mark as connecting to prevent race conditions instance._connectingClients.Add(ulid); } @@ -713,6 +826,12 @@ public static bool IsPresenceConnected(string playerUlid = null) /// public static LootLockerPresenceConnectionStats GetPresenceConnectionStats(string playerUlid = null) { + // Return empty stats during shutdown to prevent service access + if (!Application.isPlaying) + { + return new LootLockerPresenceConnectionStats(); + } + var instance = Get(); if (instance == null) return new LootLockerPresenceConnectionStats(); @@ -781,6 +900,170 @@ private void _SetAutoConnectEnabled(bool enabled) } } + /// + /// Destroy a presence client immediately (for session ending scenarios) + /// + private void _DestroyPresenceClientForUlid(string playerUlid, LootLockerPresenceCallback onComplete = null) + { + if (!_isEnabled) + { + onComplete?.Invoke(false, "Presence is disabled"); + return; + } + else if (_isShuttingDown) + { + onComplete?.Invoke(true); + return; + } + + if (string.IsNullOrEmpty(playerUlid)) + { + onComplete?.Invoke(true); + return; + } + + LootLockerPresenceClient clientToDestroy = null; + + lock (_activeClientsLock) + { + // Remove from both active and disconnected clients + if (_activeClients.TryGetValue(playerUlid, out clientToDestroy)) + { + _activeClients.Remove(playerUlid); + } + else if (_disconnectedClients.TryGetValue(playerUlid, out clientToDestroy)) + { + _disconnectedClients.Remove(playerUlid); + } + + // Also remove from connecting clients if it's there + _connectingClients.Remove(playerUlid); + } + + // Destroy the client + if (clientToDestroy != null) + { + UnityEngine.Object.Destroy(clientToDestroy.gameObject); + onComplete?.Invoke(true); + } + else + { + onComplete?.Invoke(true); + } + } + + /// + /// Destroy all presence clients (for shutdown scenarios) + /// + private void _DestroyAllClients() + { + List clientsToDestroy = new List(); + + lock (_activeClientsLock) + { + // Collect all clients from both active and disconnected collections + clientsToDestroy.AddRange(_activeClients.Values); + clientsToDestroy.AddRange(_disconnectedClients.Values); + + // Clear all collections + _activeClients.Clear(); + _disconnectedClients.Clear(); + _connectingClients.Clear(); + } + + // During Unity shutdown, don't destroy objects manually to avoid conflicts with LifecycleManager + if (!Application.isPlaying || _isShuttingDown) + { + // Just clear the collections, let Unity handle object destruction during shutdown + return; + } + + // Destroy all clients outside the lock (only during normal operation) + foreach (var client in clientsToDestroy) + { + if (client != null) + { + UnityEngine.Object.Destroy(client.gameObject); + } + } + } + + /// + /// Update session token on existing client and reconnect + /// + private void _UpdateClientSessionTokenAndReconnect(LootLockerPlayerData playerData) + { + if (playerData == null || string.IsNullOrEmpty(playerData.ULID) || string.IsNullOrEmpty(playerData.SessionToken)) + { + LootLockerLogger.Log("Cannot update client session token: Invalid player data", LootLockerLogger.LogLevel.Warning); + return; + } + + LootLockerPresenceClient clientToUpdate = null; + bool wasActiveClient = false; + bool wasDisconnectedClient = false; + + lock (_activeClientsLock) + { + // Find client in active clients + if (_activeClients.TryGetValue(playerData.ULID, out clientToUpdate)) + { + wasActiveClient = true; + } + // Or in disconnected clients + else if (_disconnectedClients.TryGetValue(playerData.ULID, out clientToUpdate)) + { + wasDisconnectedClient = true; + } + } + + if (clientToUpdate != null) + { + // Capture current status before any operations + string lastStatus = clientToUpdate.LastSentStatus; + + // Update the session token + clientToUpdate.UpdateSessionToken(playerData.SessionToken); + + if (wasActiveClient) + { + // For active clients: disconnect first, then reconnect + LootLockerLogger.Log($"Disconnecting active client for {playerData.ULID} to update session token", LootLockerLogger.LogLevel.Debug); + + DisconnectPresence(playerData.ULID, (disconnectSuccess, disconnectError) => { + if (disconnectSuccess) + { + // After disconnect, the client should be in disconnected state + // Now reconnect with the updated token + LootLockerLogger.Log($"Reconnecting presence for {playerData.ULID} with updated session token", LootLockerLogger.LogLevel.Debug); + ConnectPresence(playerData.ULID); + } + else + { + LootLockerLogger.Log($"Failed to disconnect presence for session token update: {disconnectError}", LootLockerLogger.LogLevel.Warning); + } + }); + } + else if (wasDisconnectedClient && _autoConnectEnabled) + { + // For disconnected clients: just reconnect with new token + LootLockerLogger.Log($"Reconnecting disconnected client for {playerData.ULID} with updated session token", LootLockerLogger.LogLevel.Debug); + ConnectPresence(playerData.ULID); + } + + LootLockerLogger.Log($"Updated session token for presence client {playerData.ULID}, last status was: {lastStatus}", LootLockerLogger.LogLevel.Debug); + } + else + { + // No existing client, create new one if auto-connect is enabled + if (_autoConnectEnabled) + { + LootLockerLogger.Log($"No existing client found for {playerData.ULID}, creating new one", LootLockerLogger.LogLevel.Debug); + ConnectPresence(playerData.ULID); + } + } + } + private IEnumerator _AutoConnectExistingSessions() { // Wait a frame to ensure everything is initialized @@ -820,13 +1103,13 @@ private IEnumerator _AutoConnectExistingSessions() { shouldConnect = false; } - else if (!_activeClients.ContainsKey(state.ULID)) + else if (!_activeClients.ContainsKey(state.ULID) && !_disconnectedClients.ContainsKey(state.ULID)) { shouldConnect = true; } - else + else if (_activeClients.ContainsKey(state.ULID)) { - // Check if existing client is in a failed or disconnected state + // Check if existing active client is in a failed or disconnected state var existingClient = _activeClients[state.ULID]; var clientState = existingClient.ConnectionState; @@ -836,6 +1119,11 @@ private IEnumerator _AutoConnectExistingSessions() shouldConnect = true; } } + else if (_disconnectedClients.ContainsKey(state.ULID)) + { + // Have disconnected client - should reconnect + shouldConnect = true; + } } if (shouldConnect) @@ -878,6 +1166,12 @@ private void _DisconnectPresenceForUlid(string playerUlid, LootLockerPresenceCal { if (!_activeClients.TryGetValue(playerUlid, out client)) { + // Check if already in disconnected state + if (_disconnectedClients.ContainsKey(playerUlid)) + { + onComplete?.Invoke(true); + return; + } onComplete?.Invoke(true); return; } @@ -901,7 +1195,14 @@ private void _DisconnectPresenceForUlid(string playerUlid, LootLockerPresenceCal { if (alreadyDisconnectedOrFailed) { - UnityEngine.Object.Destroy(client); + // Move to disconnected clients instead of destroying + lock (_activeClientsLock) + { + if (!_disconnectedClients.ContainsKey(playerUlid)) + { + _disconnectedClients[playerUlid] = client; + } + } onComplete?.Invoke(true); } else @@ -911,7 +1212,14 @@ private void _DisconnectPresenceForUlid(string playerUlid, LootLockerPresenceCal { LootLockerLogger.Log($"Error disconnecting presence for {playerUlid}: {error}", LootLockerLogger.LogLevel.Debug); } - UnityEngine.Object.Destroy(client); + // Move to disconnected clients instead of destroying + lock (_activeClientsLock) + { + if (!_disconnectedClients.ContainsKey(playerUlid)) + { + _disconnectedClients[playerUlid] = client; + } + } onComplete?.Invoke(success, error); }); } @@ -996,11 +1304,15 @@ private void _ConnectPresenceClient(string ulid, LootLockerPresenceClient client client.Connect((success, error) => { + lock (_activeClientsLock) + { + // Remove from connecting clients + _connectingClients.Remove(ulid); + } if (!success) { DisconnectPresence(ulid); - } - + } onComplete?.Invoke(success, error); }); } @@ -1044,12 +1356,19 @@ private LootLockerPresenceClient _GetPresenceClientForUlid(string playerUlid) lock (_activeClientsLock) { - if (!_activeClients.ContainsKey(ulid)) + // Check active clients first + if (_activeClients.TryGetValue(ulid, out LootLockerPresenceClient activeClient)) + { + return activeClient; + } + + // Then check disconnected clients + if (_disconnectedClients.TryGetValue(ulid, out LootLockerPresenceClient disconnectedClient)) { - return null; + return disconnectedClient; } - return _activeClients[ulid]; + return null; } } @@ -1059,14 +1378,22 @@ private LootLockerPresenceClient _GetPresenceClientForUlid(string playerUlid) private void OnDestroy() { + // During Unity shutdown, avoid any complex operations + if (!Application.isPlaying) + { + return; + } + if (!_isShuttingDown) { _isShuttingDown = true; _UnsubscribeFromEvents(); - _DisconnectAll(); + // Only destroy clients if we're not in Unity shutdown + _DestroyAllClients(); } + // Skip lifecycle manager operations during shutdown if(!LootLockerLifecycleManager.IsReady) return; // Only unregister if the LifecycleManager exists and we're actually registered diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index a8bd633c..bfa69e83 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -47,11 +47,25 @@ static bool Init() /// The current version of the game in the format 1.2.3.4 (the 3 and 4 being optional but recommended) /// Extra key needed for some endpoints, can be found by going to https://console.lootlocker.com/settings/api-keys and click on the API-tab /// What log level to use for the SDKs internal logging + /// If true, logs will also be printed in builds. If false, logs will only be printed in the editor. + /// If true, errors will be logged as warnings instead of errors. + /// If true, the SDK will attempt to refresh tokens automatically. + /// If true, JSON logs will be prettified. + /// If true, sensitive information in logs will be obfuscated. + /// If true, presence features will be enabled. + /// If true, presence will auto-connect. + /// If true, presence will auto-disconnect on focus change. + /// If true, presence will be enabled in the editor. + /// /// True if initialized successfully, false otherwise - public static bool Init(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info) + public static bool Init(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info, + bool logInBuilds = false, bool errorsAsWarnings = false, bool allowTokenRefresh = true, bool prettifyJson = false, bool obfuscateLogs = true, + bool enablePresence = false, bool enablePresenceAutoConnect = true, bool enablePresenceAutoDisconnectOnFocusChange = false, bool enablePresenceInEditor = true) { // Create new settings first - bool configResult = LootLockerConfig.CreateNewSettings(apiKey, gameVersion, domainKey, logLevel); + bool configResult = LootLockerConfig.CreateNewSettings(apiKey, gameVersion, domainKey, + logLevel, logInBuilds, errorsAsWarnings, allowTokenRefresh, prettifyJson, obfuscateLogs, + enablePresence, enablePresenceAutoConnect, enablePresenceAutoDisconnectOnFocusChange, enablePresenceInEditor); if (!configResult) { return false; @@ -172,6 +186,15 @@ public static List GetActivePlayerUlids() return LootLockerStateData.GetActivePlayerULIDs(); } + /// + /// Make the state for the player with the specified ULID to be "active". + /// + /// Whether the player was successfully activated or not + public static bool MakePlayerActive(string playerUlid) + { + return !string.IsNullOrEmpty(LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(playerUlid)?.ULID); + } + /// /// Make the state for the player with the specified ULID to be "inactive". /// @@ -7911,7 +7934,7 @@ public static void ListBlockedPlayersPaginated(int PerPage, int Page, Action 0) queryParams.Add("per_page", PerPage.ToString()); - string endpointWithParams = LootLockerEndPoints.listOutgoingFriendRequests.endPoint + queryParams.ToString(); + string endpointWithParams = LootLockerEndPoints.listBlockedPlayers.endPoint + queryParams.ToString(); LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpointWithParams, LootLockerEndPoints.listBlockedPlayers.httpMethod, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } diff --git a/Runtime/Game/Resources/LootLockerConfig.cs b/Runtime/Game/Resources/LootLockerConfig.cs index 9b2c51be..54875667 100644 --- a/Runtime/Game/Resources/LootLockerConfig.cs +++ b/Runtime/Game/Resources/LootLockerConfig.cs @@ -266,18 +266,60 @@ static void ListInstalledPackagesRequestProgress() } #endif - public static bool CreateNewSettings(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info, bool logInBuilds = false, bool errorsAsWarnings = false, bool allowTokenRefresh = false, bool prettifyJson = false) + public static bool CreateNewSettings(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info, + bool logInBuilds = false, bool errorsAsWarnings = false, bool allowTokenRefresh = false, bool prettifyJson = false, bool obfuscateLogs = true, + bool enablePresence = false, bool enablePresenceAutoConnect = true, bool enablePresenceAutoDisconnectOnFocusChange = false, bool enablePresenceInEditor = true) { _current = Get(); _current.apiKey = apiKey; _current.game_version = gameVersion; + _current.domainKey = domainKey; _current.logLevel = logLevel; - _current.prettifyJson = prettifyJson; _current.logInBuilds = logInBuilds; _current.logErrorsAsWarnings = errorsAsWarnings; _current.allowTokenRefresh = allowTokenRefresh; - _current.domainKey = domainKey; + _current.prettifyJson = prettifyJson; + _current.obfuscateLogs = obfuscateLogs; + _current.enablePresence = enablePresence; + _current.enablePresenceAutoConnect = enablePresenceAutoConnect; + _current.enablePresenceAutoDisconnectOnFocusChange = enablePresenceAutoDisconnectOnFocusChange; + _current.enablePresenceInEditor = enablePresenceInEditor; +#if UNITY_EDITOR + _current.adminToken = null; +#endif //UNITY_EDITOR +#if LOOTLOCKER_COMMANDLINE_SETTINGS + _current.CheckForSettingOverrides(); +#endif + _current.ConstructUrls(); + return true; + } + + public static bool CreateNewSettings(LootLockerConfig newConfig) + { + if(newConfig == null) + { + return false; + } + _current = Get(); + if (_current == null) + { + return false; + } + + _current.apiKey = newConfig.apiKey; + _current.game_version = newConfig.game_version; + _current.domainKey = newConfig.domainKey; + _current.logLevel = newConfig.logLevel; + _current.logInBuilds = newConfig.logInBuilds; + _current.logErrorsAsWarnings = newConfig.logErrorsAsWarnings; + _current.allowTokenRefresh = newConfig.allowTokenRefresh; + _current.prettifyJson = newConfig.prettifyJson; + _current.obfuscateLogs = newConfig.obfuscateLogs; + _current.enablePresence = newConfig.enablePresence; + _current.enablePresenceAutoConnect = newConfig.enablePresenceAutoConnect; + _current.enablePresenceAutoDisconnectOnFocusChange = newConfig.enablePresenceAutoDisconnectOnFocusChange; + _current.enablePresenceInEditor = newConfig.enablePresenceInEditor; #if UNITY_EDITOR _current.adminToken = null; #endif //UNITY_EDITOR @@ -319,6 +361,10 @@ public static bool ClearSettings() _current.obfuscateLogs = true; _current.allowTokenRefresh = true; _current.domainKey = null; + _current.enablePresence = false; + _current.enablePresenceAutoConnect = true; + _current.enablePresenceAutoDisconnectOnFocusChange = false; + _current.enablePresenceInEditor = true; #if UNITY_EDITOR _current.adminToken = null; #endif //UNITY_EDITOR diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationGame.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationGame.cs index a82ac949..19b6d927 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationGame.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationGame.cs @@ -134,7 +134,7 @@ public void DeleteGame(Action onCompl public bool InitializeLootLockerSDK() { string adminToken = LootLockerConfig.current.adminToken; - bool result = LootLockerSDKManager.Init(GetApiKeyForActiveEnvironment(), GameVersion, GameDomainKey, LootLockerLogger.LogLevel.Debug); + bool result = LootLockerSDKManager.Init(GetApiKeyForActiveEnvironment(), GameVersion, GameDomainKey, LootLockerLogger.LogLevel.Debug, false, false, true, true, false, false, true, false, true); LootLockerConfig.current.adminToken = adminToken; LootLockerSDKManager.ClearAllPlayerCaches(); return result; diff --git a/Tests/LootLockerTests/PlayMode/AssetTests.cs b/Tests/LootLockerTests/PlayMode/AssetTests.cs index fc1dc6c9..51b737b9 100644 --- a/Tests/LootLockerTests/PlayMode/AssetTests.cs +++ b/Tests/LootLockerTests/PlayMode/AssetTests.cs @@ -229,8 +229,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/FollowersTests.cs b/Tests/LootLockerTests/PlayMode/FollowersTests.cs index 7db0e819..267121a3 100644 --- a/Tests/LootLockerTests/PlayMode/FollowersTests.cs +++ b/Tests/LootLockerTests/PlayMode/FollowersTests.cs @@ -91,8 +91,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/FriendsTests.cs b/Tests/LootLockerTests/PlayMode/FriendsTests.cs index c3af4145..07f13c47 100644 --- a/Tests/LootLockerTests/PlayMode/FriendsTests.cs +++ b/Tests/LootLockerTests/PlayMode/FriendsTests.cs @@ -92,8 +92,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs index fcd947eb..e7026db7 100644 --- a/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs +++ b/Tests/LootLockerTests/PlayMode/GuestSessionTest.cs @@ -95,8 +95,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs index ff5aac31..dc26565a 100644 --- a/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs +++ b/Tests/LootLockerTests/PlayMode/LeaderboardTest.cs @@ -127,8 +127,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/MultiUserTests.cs b/Tests/LootLockerTests/PlayMode/MultiUserTests.cs index 3eb30913..4592d59e 100644 --- a/Tests/LootLockerTests/PlayMode/MultiUserTests.cs +++ b/Tests/LootLockerTests/PlayMode/MultiUserTests.cs @@ -145,9 +145,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, - configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); #if LOOTLOCKER_ENABLE_OVERRIDABLE_STATE_WRITER LootLockerSDKManager.SetStateWriter(new LootLockerPlayerPrefsStateWriter()); diff --git a/Tests/LootLockerTests/PlayMode/NotificationTests.cs b/Tests/LootLockerTests/PlayMode/NotificationTests.cs index e1820025..690de3e7 100644 --- a/Tests/LootLockerTests/PlayMode/NotificationTests.cs +++ b/Tests/LootLockerTests/PlayMode/NotificationTests.cs @@ -129,8 +129,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } @@ -562,8 +561,6 @@ public IEnumerator Notifications_MarkAllNotificationsAsReadUsingConvenienceMetho Assert.AreEqual(CreatedTriggers.Count - notificationIdsToMarkAsRead.Length, listUnreadNotificationsAfterMarkAsReadResponse.Notifications.Length, "Not all notifications that were marked as read actually were"); } - - //TODO: Populate with new types [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public IEnumerator Notifications_ConvenienceLookupTable_CanLookUpAllNotificationTypes() { diff --git a/Tests/LootLockerTests/PlayMode/PingTest.cs b/Tests/LootLockerTests/PlayMode/PingTest.cs index aa97e024..1b8ee17a 100644 --- a/Tests/LootLockerTests/PlayMode/PingTest.cs +++ b/Tests/LootLockerTests/PlayMode/PingTest.cs @@ -91,8 +91,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs index 38b16763..24b94a55 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs @@ -106,8 +106,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs b/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs index 41691e94..d7e5960b 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerInfoTest.cs @@ -94,8 +94,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs b/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs index 26bbfd3e..58d560d9 100644 --- a/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs +++ b/Tests/LootLockerTests/PlayMode/PlayerStorageTest.cs @@ -99,8 +99,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/PresenceTests.cs b/Tests/LootLockerTests/PlayMode/PresenceTests.cs index 5a32e84b..497b2174 100644 --- a/Tests/LootLockerTests/PlayMode/PresenceTests.cs +++ b/Tests/LootLockerTests/PlayMode/PresenceTests.cs @@ -111,8 +111,7 @@ public IEnumerator Teardown() LootLockerSDKManager.ResetSDK(); yield return LootLockerLifecycleManager.CleanUpOldInstances(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} teardown #####"); @@ -301,7 +300,10 @@ public IEnumerator PresenceConnection_DisconnectPresence_DisconnectsCleanly() // Verify no active clients var activeClients = LootLockerSDKManager.ListPresenceConnections().ToList(); - Assert.AreEqual(0, activeClients.Count, "Should have no active presence clients after disconnect"); + foreach (var client in activeClients) + { + Assert.AreEqual(LootLockerSDKManager.GetPresenceConnectionState(client), LootLockerPresenceConnectionState.Disconnected, $"Client {client} should not be connected after disconnect"); + } yield return null; } @@ -418,5 +420,206 @@ public IEnumerator PresenceConnection_WhenDisabled_DoesNotConnect() yield return null; } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PresenceConnection_DisconnectVsDestroy_PreservesClientAndAutoResendsStatus() + { + if (SetupFailed) + { + yield break; + } + + // Setup session and presence connection + LootLockerSDKManager.SetPresenceEnabled(true); + LootLockerSDKManager.SetPresenceAutoConnectEnabled(false); + + bool sessionStarted = false; + LootLockerGuestSessionResponse sessionResponse = null; + + LootLockerSDKManager.StartGuestSession((response) => + { + sessionResponse = response; + sessionStarted = true; + }); + + yield return new WaitUntil(() => sessionStarted); + Assert.IsTrue(sessionResponse.success, "Session should start successfully"); + + // Connect presence + bool presenceConnected = false; + bool connectionSuccess = false; + + LootLockerSDKManager.ForceStartPresenceConnection((success, error) => + { + connectionSuccess = success; + presenceConnected = true; + }); + + yield return new WaitUntil(() => presenceConnected); + Assert.IsTrue(connectionSuccess, "Presence should connect successfully"); + + // Wait for connection to stabilize + yield return new WaitForSeconds(2f); + + // Set a status to test auto-resend + bool statusUpdated = false; + bool updateSuccess = false; + const string testStatus = "testing_disconnect_vs_destroy"; + + LootLockerSDKManager.UpdatePresenceStatus(testStatus, null, (success) => + { + updateSuccess = success; + statusUpdated = true; + }); + + yield return new WaitUntil(() => statusUpdated); + Assert.IsTrue(updateSuccess, "Status update should succeed"); + + // Verify the status was set + var statsBeforeDisconnect = LootLockerSDKManager.GetPresenceConnectionStats(null); + Assert.AreEqual(testStatus, statsBeforeDisconnect.lastSentStatus, "Status should be set before disconnect"); + + // Get initial client count (should be tracked even when disconnected) + var clientsBeforeDisconnect = LootLockerSDKManager.ListPresenceConnections().ToList(); + int initialClientCount = clientsBeforeDisconnect.Count; + Assert.Greater(initialClientCount, 0, "Should have clients before disconnect"); + + // Test disconnection (should preserve client) + bool presenceDisconnected = false; + bool disconnectSuccess = false; + + LootLockerSDKManager.ForceStopPresenceConnection((success, error) => + { + disconnectSuccess = success; + presenceDisconnected = true; + }); + + yield return new WaitUntil(() => presenceDisconnected); + Assert.IsTrue(disconnectSuccess, "Presence disconnection should succeed"); + + // Wait for disconnection to process + yield return new WaitForSeconds(1f); + + // Verify disconnection state + Assert.IsFalse(LootLockerSDKManager.IsPresenceConnected(), "Should not be connected after disconnect"); + + // Check that client is still tracked (preserved but disconnected) + var clientsAfterDisconnect = LootLockerSDKManager.ListPresenceConnections().ToList(); + Assert.AreEqual(initialClientCount, clientsAfterDisconnect.Count, "Client count should remain the same after disconnect (client preserved)"); + + // Verify we can still get stats from the disconnected client + var statsAfterDisconnect = LootLockerSDKManager.GetPresenceConnectionStats(null); + Assert.IsNotNull(statsAfterDisconnect, "Should still be able to get stats from disconnected client"); + Assert.AreEqual(testStatus, statsAfterDisconnect.lastSentStatus, "Last sent status should be preserved in disconnected client"); + + // Test reconnection (should reuse the same client) + bool reconnected = false; + bool reconnectionSuccess = false; + string errorMessage = null; + + LootLockerSDKManager.ForceStartPresenceConnection((success, error) => + { + reconnectionSuccess = success; + errorMessage = error; + reconnected = true; + }); + + yield return new WaitUntil(() => reconnected); + Assert.IsTrue(reconnectionSuccess, $"Reconnection failed: {errorMessage}"); + + // Wait for reconnection to stabilize and auto-resend to complete + yield return new WaitForSeconds(3f); + + // Verify reconnection state + Assert.IsTrue(LootLockerSDKManager.IsPresenceConnected(), "Should be connected after reconnection"); + + // Verify client was reused (not recreated) + var clientsAfterReconnect = LootLockerSDKManager.ListPresenceConnections().ToList(); + Assert.AreEqual(initialClientCount, clientsAfterReconnect.Count, "Client count should remain the same after reconnect (client reused)"); + + // Verify status was automatically resent + var statsAfterReconnect = LootLockerSDKManager.GetPresenceConnectionStats(null); + Assert.IsNotNull(statsAfterReconnect, "Should be able to get stats after reconnect"); + Assert.AreEqual(testStatus, statsAfterReconnect.lastSentStatus, "Status should be auto-resent after reconnection"); + + yield return null; + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + public IEnumerator PresenceConnection_SessionRefresh_ReconnectsWithNewToken() + { + if (SetupFailed) + { + yield break; + } + + LootLockerConfig.current.allowTokenRefresh = true; + // Setup session and presence connection + LootLockerSDKManager.SetPresenceEnabled(true); + LootLockerSDKManager.SetPresenceAutoConnectEnabled(true); // Enable auto-connect for session refresh test + + bool sessionStarted = false; + LootLockerGuestSessionResponse sessionResponse = null; + + LootLockerSDKManager.StartGuestSession((response) => + { + sessionResponse = response; + sessionStarted = true; + }); + + yield return new WaitUntil(() => sessionStarted); + Assert.IsTrue(sessionResponse.success, "Session should start successfully"); + + // Wait for auto-connection + yield return new WaitForSeconds(3f); + Assert.IsTrue(LootLockerSDKManager.IsPresenceConnected(), "Presence should auto-connect"); + + // Set a status + bool statusUpdated = false; + const string testStatus = "before_session_refresh"; + + LootLockerSDKManager.UpdatePresenceStatus(testStatus, null, (success) => + { + statusUpdated = true; + }); + + yield return new WaitUntil(() => statusUpdated); + + // Get stats before refresh + var statsBeforeRefresh = LootLockerSDKManager.GetPresenceConnectionStats(null); + Assert.AreEqual(testStatus, statsBeforeRefresh.lastSentStatus, "Status should be set before refresh"); + + var playerDataBeforeRefresh = LootLockerStateData.GetPlayerDataForPlayerWithUlidWithoutChangingState(sessionResponse.player_ulid); + string oldSessionToken = playerDataBeforeRefresh.SessionToken; + playerDataBeforeRefresh.SessionToken = "invalid_token_for_test"; // Invalidate token to force refresh + LootLockerStateData.SetPlayerData(playerDataBeforeRefresh); + + // End current session first + bool getPlayerNameCompleted = false; + PlayerNameResponse playerNameResponse = null; + LootLockerSDKManager.GetPlayerName((response) => + { + playerNameResponse = response; + getPlayerNameCompleted = true; + }); + yield return new WaitUntil(() => getPlayerNameCompleted); + Assert.IsTrue(playerNameResponse.success, "Get player name succeeded despite invalid token (refresh was performed)"); + + // Wait for auto-reconnect with new token + yield return new WaitForSeconds(3f); + + // Verify new connection + Assert.IsTrue(LootLockerSDKManager.IsPresenceConnected(), "Should reconnect after session refresh"); + + // Verify client was preserved and status was auto-resent (new behavior) + var statsAfterRefresh = LootLockerSDKManager.GetPresenceConnectionStats(null); + Assert.IsNotNull(statsAfterRefresh, "Should have stats for preserved client"); + + // The client should have auto-resent the previous status after token refresh + Assert.AreEqual(testStatus, statsAfterRefresh.lastSentStatus, + "Client should auto-resend previous status after session token refresh"); + + yield return null; + } } } diff --git a/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs b/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs index 2610d0c9..57e7f3a8 100644 --- a/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs +++ b/Tests/LootLockerTests/PlayMode/SessionRefreshTest.cs @@ -116,8 +116,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs b/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs index 40994c9a..e70508a9 100644 --- a/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs +++ b/Tests/LootLockerTests/PlayMode/SubmitScoreTest.cs @@ -151,8 +151,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/TriggerTests.cs b/Tests/LootLockerTests/PlayMode/TriggerTests.cs index 71896fc4..e99991d3 100644 --- a/Tests/LootLockerTests/PlayMode/TriggerTests.cs +++ b/Tests/LootLockerTests/PlayMode/TriggerTests.cs @@ -107,8 +107,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs index dd56a19b..9ebb58a8 100644 --- a/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs +++ b/Tests/LootLockerTests/PlayMode/WhiteLabelLoginTest.cs @@ -87,8 +87,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); } public string GetRandomName() diff --git a/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs b/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs index 875bca5d..91efb647 100644 --- a/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs +++ b/Tests/LootLockerTests/PlayMode/leaderboardDetailsTest.cs @@ -131,8 +131,7 @@ public IEnumerator TearDown() LootLockerStateData.ClearAllSavedStates(); - LootLockerConfig.CreateNewSettings(configCopy.apiKey, configCopy.game_version, configCopy.domainKey, - configCopy.logLevel, configCopy.logInBuilds, configCopy.logErrorsAsWarnings, configCopy.allowTokenRefresh); + LootLockerConfig.CreateNewSettings(configCopy); Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); }