diff --git a/.github/workflows/run-tests-and-package.yml b/.github/workflows/run-tests-and-package.yml
index 9eb0f9b78..ce5a45a23 100644
--- a/.github/workflows/run-tests-and-package.yml
+++ b/.github/workflows/run-tests-and-package.yml
@@ -227,7 +227,7 @@ jobs:
runs-on: [ubuntu-latest]
needs: [editor-smoke-test]
timeout-minutes: 20
- if: false && (startsWith(github.ref, 'refs/pull') && endsWith(github.base_ref, 'main')) || startsWith(github.ref, 'refs/tags/v') || (startsWith(github.ref, 'refs/heads') && endsWith(github.ref, 'main'))
+ if: false && ((startsWith(github.ref, 'refs/pull') && endsWith(github.base_ref, 'main')) || startsWith(github.ref, 'refs/tags/v') || (startsWith(github.ref, 'refs/heads') && endsWith(github.ref, 'main')))
env:
LL_USE_STAGE: false
strategy:
@@ -465,15 +465,14 @@ jobs:
- name: Enable beta features
run: |
sed -i -e 's/1: LOOTLOCKER_COMMANDLINE_SETTINGS/1: LOOTLOCKER_COMMANDLINE_SETTINGS;${{ VARS.CURRENT_BETA_FEATURES }}/g' TestProject/ProjectSettings/ProjectSettings.asset
+ - name: Set presence to disabled
+ run: |
+ echo "PRESENCE_CONFIG=-enablepresence false -enablepresenceautoconnect false -enablepresenceautodisconnectonfocuschange false" >> $GITHUB_ENV
- name: Set the project to use Newtonsoft json
if: ${{ ENV.JSON_LIBRARY == 'newtonsoft' }}
run: |
sed -i -e 's/1: LOOTLOCKER_COMMANDLINE_SETTINGS/1: LOOTLOCKER_COMMANDLINE_SETTINGS;LOOTLOCKER_USE_NEWTONSOFTJSON/g' TestProject/ProjectSettings/ProjectSettings.asset
sed -i -e 's/"nunit.framework.dll"/"nunit.framework.dll",\n\t\t"Newtonsoft.Json.dll"/g' sdk/Tests/LootLockerTests/PlayMode/PlayModeTests.asmdef
- - name: Use Legacy HTTP Stack
- if: ${{ ENV.USE_HTTP_EXECUTION_QUEUE == 'false' }}
- run: |
- sed -i -e 's/1: LOOTLOCKER_COMMANDLINE_SETTINGS/1: LOOTLOCKER_COMMANDLINE_SETTINGS;LOOTLOCKER_LEGACY_HTTP_STACK/g' TestProject/ProjectSettings/ProjectSettings.asset
- name: Set LootLocker to target stage environment
if: ${{ ENV.TARGET_ENVIRONMENT == 'STAGE' }}
run: |
@@ -520,7 +519,7 @@ jobs:
checkName: Integration tests (${{ matrix.unityVersion }}-${{ ENV.JSON_LIBRARY }}) Test Results
artifactsPath: ${{ matrix.unityVersion }}-${{ ENV.JSON_LIBRARY }}-artifacts
githubToken: ${{ secrets.GITHUB_TOKEN }}
- customParameters: -lootlockerurl ${{ ENV.LOOTLOCKER_URL }} ${{ ENV.USER_COMMANDLINE_ARGUMENTS }} ${{ ENV.TEST_CATEGORY }}
+ customParameters: -lootlockerurl ${{ ENV.LOOTLOCKER_URL }} ${{ ENV.USER_COMMANDLINE_ARGUMENTS }} ${{ ENV.TEST_CATEGORY }} ${{ ENV.PRESENCE_CONFIG }}
useHostNetwork: true
####### CLEANUP ###########
- name: Bring down Go Backend
diff --git a/Runtime/Client/ILootLockerService.cs b/Runtime/Client/ILootLockerService.cs
new file mode 100644
index 000000000..a4dc65792
--- /dev/null
+++ b/Runtime/Client/ILootLockerService.cs
@@ -0,0 +1,43 @@
+namespace LootLocker
+{
+ ///
+ /// Interface that all LootLocker services must implement to be managed by the LifecycleManager
+ ///
+ public interface ILootLockerService
+ {
+ ///
+ /// Initialize the service
+ ///
+ void Initialize();
+
+ ///
+ /// Reset/cleanup the service state
+ ///
+ void Reset();
+
+ ///
+ /// Handle application pause events (optional - default implementation does nothing)
+ ///
+ void HandleApplicationPause(bool pauseStatus);
+
+ ///
+ /// Handle application focus events (optional - default implementation does nothing)
+ ///
+ void HandleApplicationFocus(bool hasFocus);
+
+ ///
+ /// Handle application quit events
+ ///
+ void HandleApplicationQuit();
+
+ ///
+ /// Whether the service has been initialized
+ ///
+ bool IsInitialized { get; }
+
+ ///
+ /// Service name for logging and identification
+ ///
+ string ServiceName { get; }
+ }
+}
\ No newline at end of file
diff --git a/Runtime/Client/LootLockerServerApi.cs.meta b/Runtime/Client/ILootLockerService.cs.meta
similarity index 83%
rename from Runtime/Client/LootLockerServerApi.cs.meta
rename to Runtime/Client/ILootLockerService.cs.meta
index 7123fe2b6..09251ecba 100644
--- a/Runtime/Client/LootLockerServerApi.cs.meta
+++ b/Runtime/Client/ILootLockerService.cs.meta
@@ -1,5 +1,5 @@
fileFormatVersion: 2
-guid: b6b4735df3c936946a538c8a2acc6e43
+guid: a4444c443da40fa4fb63253b5299702c
MonoImporter:
externalObjects: {}
serializedVersion: 2
diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs
index c1f61ee0b..d0d0071d8 100644
--- a/Runtime/Client/LootLockerEndPoints.cs
+++ b/Runtime/Client/LootLockerEndPoints.cs
@@ -336,6 +336,10 @@ public class LootLockerEndPoints
// Broadcasts
[Header("Broadcasts")]
public static EndPointClass ListBroadcasts = new EndPointClass("broadcasts/v1", LootLockerHTTPMethod.GET);
+
+ // Presence (WebSocket)
+ [Header("Presence")]
+ public static EndPointClass presenceWebSocket = new EndPointClass("presence/v1", LootLockerHTTPMethod.GET);
}
[Serializable]
diff --git a/Runtime/Client/LootLockerEventSystem.cs b/Runtime/Client/LootLockerEventSystem.cs
new file mode 100644
index 000000000..94550e089
--- /dev/null
+++ b/Runtime/Client/LootLockerEventSystem.cs
@@ -0,0 +1,581 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace LootLocker
+{
+ #region Event Data Classes
+
+ ///
+ /// Base class for all LootLocker event data
+ ///
+ [Serializable]
+ public abstract class LootLockerEventData
+ {
+ public DateTime timestamp { get; private set; }
+ public LootLockerEventType eventType { get; private set; }
+
+ protected LootLockerEventData(LootLockerEventType eventType)
+ {
+ this.eventType = eventType;
+ this.timestamp = DateTime.UtcNow;
+ }
+ }
+
+ ///
+ /// Event data for session started events
+ ///
+ [Serializable]
+ public class LootLockerSessionStartedEventData : LootLockerEventData
+ {
+ ///
+ /// The complete player data for the player whose session started
+ ///
+ public LootLockerPlayerData playerData { get; set; }
+
+ public LootLockerSessionStartedEventData(LootLockerPlayerData playerData)
+ : base(LootLockerEventType.SessionStarted)
+ {
+ this.playerData = playerData;
+ }
+ }
+
+ ///
+ /// Event data for session refreshed events
+ ///
+ [Serializable]
+ public class LootLockerSessionRefreshedEventData : LootLockerEventData
+ {
+ ///
+ /// The complete player data for the player whose session was refreshed
+ ///
+ public LootLockerPlayerData playerData { get; set; }
+
+ public LootLockerSessionRefreshedEventData(LootLockerPlayerData playerData)
+ : base(LootLockerEventType.SessionRefreshed)
+ {
+ this.playerData = playerData;
+ }
+ }
+
+ ///
+ /// Event data for session ended events
+ ///
+ [Serializable]
+ public class LootLockerSessionEndedEventData : LootLockerEventData
+ {
+ ///
+ /// The ULID of the player whose session ended
+ ///
+ public string playerUlid { get; set; }
+
+ ///
+ /// Whether local state should be cleared for this player
+ ///
+ public bool clearLocalState { get; set; }
+
+ public LootLockerSessionEndedEventData(string playerUlid, bool clearLocalState = false)
+ : base(LootLockerEventType.SessionEnded)
+ {
+ this.playerUlid = playerUlid;
+ this.clearLocalState = clearLocalState;
+ }
+ }
+
+ ///
+ /// Event data for session expired events
+ ///
+ [Serializable]
+ public class LootLockerSessionExpiredEventData : LootLockerEventData
+ {
+ ///
+ /// The ULID of the player whose session expired
+ ///
+ public string playerUlid { get; set; }
+
+ public LootLockerSessionExpiredEventData(string playerUlid)
+ : base(LootLockerEventType.SessionExpired)
+ {
+ this.playerUlid = playerUlid;
+ }
+ }
+
+ ///
+ /// Event data for local session deactivated events
+ ///
+ [Serializable]
+ public class LootLockerLocalSessionDeactivatedEventData : LootLockerEventData
+ {
+ ///
+ /// The ULID of the player whose local session was deactivated (null if all sessions were deactivated)
+ ///
+ public string playerUlid { get; set; }
+
+ public LootLockerLocalSessionDeactivatedEventData(string playerUlid)
+ : base(LootLockerEventType.LocalSessionDeactivated)
+ {
+ this.playerUlid = playerUlid;
+ }
+ }
+
+ ///
+ /// Event data for local session activated events
+ ///
+ [Serializable]
+ public class LootLockerLocalSessionActivatedEventData : LootLockerEventData
+ {
+ ///
+ /// The complete player data for the player whose session was activated
+ ///
+ public LootLockerPlayerData playerData { get; set; }
+
+ public LootLockerLocalSessionActivatedEventData(LootLockerPlayerData playerData)
+ : base(LootLockerEventType.LocalSessionActivated)
+ {
+ this.playerData = playerData;
+ }
+ }
+
+
+ ///
+ /// Event data for presence connection state changed events
+ ///
+ [Serializable]
+ public class LootLockerPresenceConnectionStateChangedEventData : LootLockerEventData
+ {
+ ///
+ /// The ULID of the player whose presence connection state changed
+ ///
+ public string playerUlid { get; set; }
+
+ ///
+ /// The previous connection state
+ ///
+ public LootLockerPresenceConnectionState previousState { get; set; }
+
+ ///
+ /// The new connection state
+ ///
+ public LootLockerPresenceConnectionState newState { get; set; }
+
+ ///
+ /// Error message if the state change was due to an error
+ ///
+ public string errorMessage { get; set; }
+
+ public LootLockerPresenceConnectionStateChangedEventData(string playerUlid, LootLockerPresenceConnectionState previousState, LootLockerPresenceConnectionState newState, string errorMessage = null)
+ : base(LootLockerEventType.PresenceConnectionStateChanged)
+ {
+ this.playerUlid = playerUlid;
+ this.previousState = previousState;
+ this.newState = newState;
+ this.errorMessage = errorMessage;
+ }
+ }
+
+ ///
+ /// Event data for event system reset events
+ ///
+ [Serializable]
+ public class LootLockerEventSystemResetEventData : LootLockerEventData
+ {
+ string message { get; set; } = "The LootLocker Event System has been reset and all subscribers have been cleared. If you were subscribed to events, you will need to re-subscribe.";
+ public LootLockerEventSystemResetEventData()
+ : base(LootLockerEventType.EventSystemReset)
+ {
+ }
+ }
+
+ #endregion
+
+ #region Event Delegates
+
+ ///
+ /// Delegate for LootLocker events
+ ///
+ public delegate void LootLockerEventHandler(T eventData) where T : LootLockerEventData;
+
+ #endregion
+
+ #region Event Types
+
+ ///
+ /// Predefined event types for the LootLocker SDK
+ ///
+ public enum LootLockerEventType
+ {
+ // Session Events
+ SessionStarted,
+ SessionRefreshed,
+ SessionEnded,
+ SessionExpired,
+ LocalSessionDeactivated,
+ LocalSessionActivated,
+ // Presence Events
+ PresenceConnectionStateChanged,
+ // Meta Events
+ EventSystemReset
+ }
+
+ #endregion
+
+ ///
+ /// Centralized event system for the LootLocker SDK
+ /// Manages event subscriptions, event firing, and event data
+ ///
+ public class LootLockerEventSystem : MonoBehaviour, ILootLockerService
+ {
+ #region ILootLockerService Implementation
+
+ public bool IsInitialized { get; private set; } = false;
+ public string ServiceName => "EventSystem";
+
+ void ILootLockerService.Initialize()
+ {
+ if (IsInitialized) return;
+
+ // Initialize event system configuration
+ logEvents = false;
+ IsInitialized = true;
+ }
+
+ void ILootLockerService.Reset()
+ {
+ ClearAllSubscribersInternal();
+ logEvents = false;
+ IsInitialized = false;
+ }
+
+ void ILootLockerService.HandleApplicationPause(bool pauseStatus)
+ {
+ // Event system doesn't need to handle pause events
+ }
+
+ void ILootLockerService.HandleApplicationFocus(bool hasFocus)
+ {
+ // Event system doesn't need to handle focus events
+ }
+
+ void ILootLockerService.HandleApplicationQuit()
+ {
+ ClearAllSubscribersInternal();
+ }
+
+ #endregion
+
+ #region Singleton Management
+
+ private static LootLockerEventSystem _instance;
+ private static readonly object _instanceLock = new object();
+
+ ///
+ /// Get the EventSystem service instance through the LifecycleManager.
+ /// Services are automatically registered and initialized on first access if needed.
+ ///
+ private static LootLockerEventSystem GetInstance()
+ {
+ if (_instance != null)
+ {
+ return _instance;
+ }
+
+ lock (_instanceLock)
+ {
+ if (_instance == null)
+ {
+ // Register with LifecycleManager (will auto-initialize if needed)
+ _instance = LootLockerLifecycleManager.GetService();
+ }
+ return _instance;
+ }
+ }
+
+ #endregion
+
+ #region Private Fields
+
+ // Event storage with strong references to prevent premature GC
+ // Using regular List instead of WeakReference to avoid delegate GC issues
+ private Dictionary> eventSubscribers = new Dictionary>();
+ private readonly object eventSubscribersLock = new object(); // Thread safety for event subscribers
+
+ // Configuration
+ private bool logEvents = false;
+
+ #endregion
+
+ #region Public Properties
+
+
+
+ ///
+ /// Whether to log events to the console for debugging
+ ///
+ public static bool LogEvents
+ {
+ get => GetInstance()?.logEvents ?? false;
+ set { var instance = GetInstance(); if (instance != null) instance.logEvents = value; }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Subscribe to a specific event type with typed event data
+ ///
+ public static void Subscribe(LootLockerEventType eventType, LootLockerEventHandler handler) where T : LootLockerEventData
+ {
+ GetInstance()?.SubscribeInstance(eventType, handler);
+ }
+
+ ///
+ /// Unsubscribe from a specific event type with typed handler
+ ///
+ public static void Unsubscribe(LootLockerEventType eventType, LootLockerEventHandler handler) where T : LootLockerEventData
+ {
+ GetInstance()?.UnsubscribeInstance(eventType, handler);
+ }
+
+ ///
+ /// Instance method to subscribe to events without triggering circular dependency through GetInstance()
+ /// Used during initialization when we already have the EventSystem instance
+ ///
+ public void SubscribeInstance(LootLockerEventType eventType, LootLockerEventHandler handler) where T : LootLockerEventData
+ {
+ if (handler == null)
+ return;
+
+ lock (eventSubscribersLock)
+ {
+ if (!eventSubscribers.ContainsKey(eventType))
+ {
+ eventSubscribers[eventType] = new List