// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 // Standard Library using System; // GameKit using AWS.GameKit.Common; using AWS.GameKit.Common.Models; using AWS.GameKit.Runtime.Core; using AWS.GameKit.Runtime.FeatureUtils; using AWS.GameKit.Runtime.Models; using AWS.GameKit.Runtime.Utils; namespace AWS.GameKit.Runtime.Features.GameKitUserGameplayData { /// /// User Gameplay Data feature. /// public class UserGameplayData : GameKitFeatureBase, IUserGameplayDataProvider { public override FeatureType FeatureType => FeatureType.UserGameplayData; private static FileManager _fileManager; // Automatic offline state private bool _isOfflineCacheRunning = false; private string _offlineCacheFile = string.Empty; #if UNITY_IOS private bool _shouldAutoRestartCache = false; #endif // Delegates public delegate void NetworkChangedDelegate(NetworkStatusChangeResults results); public delegate void CacheProcessedDelegate(CacheProcessedResults results); /// /// Call to get an instance of the GameKit UserGameplayData feature. /// /// An instance of the UserGameplayData feature that can be used to call UserGameplayData related methods. public static UserGameplayData Get() { _fileManager = new FileManager(); return GameKitFeature.Get(); } /// /// Used to trigger any functionality that should happen when the applications pause status is changed. /// /// True if the application is paused, else False. protected override void NotifyPause(bool isPaused) { #if UNITY_IOS // On iOS shutdown code can sometimes be unreliable since each application is paused before being shut down. // In order to ensure the users cache is being saved properly we can save when the application is suspended and reload when its opened again if (isPaused && _isOfflineCacheRunning) { OfflineSupportCleanup(); _shouldAutoRestartCache = true; } else if (!isPaused && _shouldAutoRestartCache) { if (!string.IsNullOrEmpty(_offlineCacheFile) && _fileManager.FileExists(_offlineCacheFile)) { LoadFromCache(_offlineCacheFile, result => { Logging.LogInfo($"User Gameplay Data LoadFromCache completed with result: {result}"); if (result == 0) { StartRetryBackgroundThread(); } else { Logging.LogInfo($"Since LoadFromCache was not successful, the retry thread will not be restarted to ensure calls are not overwritten."); } }); } else { StartRetryBackgroundThread(); } _shouldAutoRestartCache = false; } #endif } /// /// Used to trigger any functionality that should happen when the applications quits, before OnDispose is called. /// /// NotifyApplicationQuit is from both the Editor and Standalone whereas DestroyFeature() is only called during runtime. /// protected override void NotifyApplicationQuit() { #if !UNITY_IOS // For iOS this is handled NotifyPause OfflineSupportCleanup(); #endif } /// /// Handles stopping the retry thread, if it is running, when the User Gameplay Data feature is being destroyed. /// If automatic caching has been set up will also persist the current queue to the cache file. /// private void OfflineSupportCleanup() { if (_isOfflineCacheRunning) { StopRetryBackgroundThread(); if (!string.IsNullOrEmpty(_offlineCacheFile)) { uint result = ImmediatePersistToCache(_offlineCacheFile); Logging.LogInfo($"User Gameplay Data PersistToCache completed with result: {result}"); } } } /// /// Creates a new bundle or updates BundleItems within a specific bundle for the calling user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name of userGameplayDataBundle is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_ITEM_KEY: At least one of the bundle keys of userGameplayData are malformed. If this error is received, Check the output log for more details on which item keys are not valid.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Object containing a map of game play data to add /// Delegate that is called once the function has finished executing public void AddBundle(AddUserGameplayDataDesc addUserGameplayDataDesc, Action callback) { Call(Feature.AddUserGameplayData, addUserGameplayDataDesc, callback); } /// /// Applies the settings to the User Gameplay Data Client. Should be called immediately after the instance has been created and before any other API calls. /// /// Object containing client settings /// Delegate that is called once the function has finished executing public void SetClientSettings(UserGameplayDataClientSettings userGameplayDataClientSettings, Action callback) { Call(Feature.SetUserGameplayDataClientSettings, userGameplayDataClientSettings, callback); } /// /// Lists the bundle name of every bundle that the calling user owns.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_PARSE_JSON_FAILED: The response body from the backend could not be parsed successfully
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Delegate that is called once the function has finished executing public void ListBundles(Action callback) { Call(Feature.ListUserGameplayDataBundles, callback); } /// /// Gets all items that are associated with a certain bundle for the calling user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name of UserGameplayDataBundleName is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_PARSE_JSON_FAILED: The response body from the backend could not be parsed successfully
/// - GAMEKIT_ERROR_GENERAL: The bundle name does not exist or the request has failed for unknown reason.
///
/// Name of the data bundle items to fetch /// Delegate that is called once the function has finished executing public void GetBundle(string bundleName, Action callback) { // Callback wrapper for zero bundles returned Call(Feature.GetUserGameplayDataBundle, bundleName, (GetUserGameplayDataBundleResults result) => { if (result.ResultCode == GameKitErrors.GAMEKIT_SUCCESS && result.Bundles.Count == 0) { Logging.LogError($"UserGameplayData.GetBundle() failed with result code {GameKitErrors.GAMEKIT_ERROR_GENERAL}. No bundles found with name {bundleName}"); result.ResultCode = GameKitErrors.GAMEKIT_ERROR_GENERAL; } callback.Invoke(result); }); } /// /// Gets a single item that is associated with a certain bundle for a user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name in userGameplayDataBundleItem is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_ITEM_KEY: The bundle key in userGameplayDataBundleItem is malformed. If this error is received, Check the output log for more details on which item keys are not valid.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Object containing needed inforamtion for fetching the bundle item /// Delegate that is called once the function has finished executing public void GetBundleItem(UserGameplayDataBundleItem userGameplayDataBundleItem, Action callback) { Call(Feature.GetUserGameplayDataBundleItem, userGameplayDataBundleItem, callback); } /// /// Updates the value of an existing item inside a bundle with new item data.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name in userGameplayDataBundleItem is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_ITEM_KEY: The bundle key in userGameplayDataBundleItem is malformed. If this error is received, Check the output log for more details on which item keys are not valid.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Object containing information for the bundle item update and what to update it with /// Delegate that is called once the function has finished executing public void UpdateItem(UserGameplayDataBundleItemValue userGameplayDataBundleItemValue, Action callback) { Call(Feature.UpdateUserGameplayDataBundleItem, userGameplayDataBundleItemValue, callback); } /// /// Permanently deletes all bundles associated with a user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Delegate that is called once the function has finished executing public void DeleteAllData(Action callback) { Call(Feature.DeleteAllUserGameplayData, callback); } /// /// Permanently deletes an entire bundle, along with all corresponding items, associated with a user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name in UserGameplayDataBundleName is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// name of the bundle to delete /// Delegate that is called once the function has finished executing /// Unity JobHandle object for the call public void DeleteBundle(string bundleName, Action callback) { Call(Feature.DeleteUserGameplayDataBundle, bundleName, callback); } /// /// Permanently deletes a list of items inside of a bundle associated with a user.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_SETTINGS_FILE_READ_FAILED: The session manager does not have settings loaded in for the User Gameplay data feature.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_NAME: The bundle name in userGameplayDataBundleItemsDeleteRequest is malformed. If this error is received, Check the output log for more details on requirements.
/// - GAMEKIT_ERROR_MALFORMED_BUNDLE_ITEM_KEY: The bundle key in userGameplayDataBundleItemsDeleteRequest is malformed. If this error is received, Check the output log for more details on which item keys are not valid.
/// - GAMEKIT_ERROR_NO_ID_TOKEN: The player is not logged in. You must login the player through the Identity and Authentication feature (AwsGameKitIdentity) before calling this method.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_FAILED: The call made to the backend service has failed.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_API_CALL_DROPPED: The call made to the backend service has been dropped.
/// - GAMEKIT_WARNING_USER_GAMEPLAY_DATA_API_CALL_ENQUEUED: The call made to the backend service has been enqueued as connection may be unhealthy and will automatically be retried.
/// - GAMEKIT_ERROR_GENERAL: The request has failed unknown reason.
///
/// Object containing the bundle and list of items to delete /// Delegate that is called once the function has finished executing /// Unity JobHandle object for the call public void DeleteBundleItems(DeleteUserGameplayDataBundleItemsDesc deleteUserGameplayDataBundleItemsDesc, Action callback) { Call(Feature.DeleteUserGameplayDataBundleItems, deleteUserGameplayDataBundleItemsDesc, callback); } /// /// Enables automatic offline mode, including persisting all failed calls in the retry thread to a cache file at the end of program.

/// If the cache needs to be persisted before the application is closed it is recommended to use StopRetryBackgroundThread() to stop the thread and PersistToCache to save the cache manually, then EnableAutomaticOfflineModeWithCaching can be restarted.

/// /// Note: Methods such as DropAllCachedEvents, SetNetworkChangeDelegate, SetCacheProcessedDelegate and SetClientSettings are not handled by this method and should be called independently. ///
/// The location of the offline cache file. All persisted calls in the cache will automatically be loaded into the queue. Any failed calls in the queue at the end of the program will be cached in this file. public void EnableAutomaticOfflineModeWithCaching(string offlineCacheFile) { if (_isOfflineCacheRunning) { Logging.LogWarning("EnableAutomaticOfflineModeWithCaching: Background thread already running, ensure that the thread is stopped before attempting to start the retry thread again."); return; } _offlineCacheFile = offlineCacheFile; if (System.IO.File.Exists(_offlineCacheFile)) { LoadFromCache(offlineCacheFile, result => { Logging.LogInfo($"User Gameplay Data LoadFromCache completed with result: {result}"); StartRetryBackgroundThread(); }); } else { StartRetryBackgroundThread(); } } /// /// Start the Retry background thread.

/// /// The DestroyFeature() method, which is automatically called when the application is closing, will handle stopping the thread.
/// However, if you need to persist the files during play it is recommended that you call StopRetryBackgroundThread() and Save/Load to Cache manually.

/// /// Note: If you are Caching your offline calls, StartRetryBackgroundThread() should be called after loading from cache. ///
public void StartRetryBackgroundThread() { if (!_isOfflineCacheRunning) { _isOfflineCacheRunning = true; Feature.UserGameplayDataStartRetryBackgroundThread(); } } /// /// Stop the Retry background thread.

/// /// Note: If you are caching your offline calls, StopRetryBackgroundThread() should be called before saving to cache. ///
public void StopRetryBackgroundThread() { if (_isOfflineCacheRunning) { Feature.UserGameplayDataStopRetryBackgroundThread(); _isOfflineCacheRunning = false; } } /// /// Return information about the running state of the background thread. /// /// True if it is running, false otherwise public bool IsBackgroundThreadRunning() { return _isOfflineCacheRunning; } /// /// Clear all pending events from the user's cache. /// /// Delegate that is called once the function has finished executing /// Unity JobHandle object for the call public void DropAllCachedEvents(Action callback) { Call(Feature.UserGameplayDataDropAllCachedEvents, callback); } /// /// Set the callback to invoke when the network state changes. /// /// Delegate that should be called whenever there is a network change. public void SetNetworkChangeDelegate(NetworkChangedDelegate callback) { Feature.UserGameplayDataSetNetworkChangeCallback(callback); } /// /// Get the last known state of network connectivity. /// /// The last known state of connectivity. True means healthy, False means unhealthy public bool GetLastNetworkHealthState() { return Feature.GetLastNetworkHealthState(); } /// /// Set the callback to invoke when the offline cache finishes processing. /// /// Delegate that should be called whenever there the cache has finished processing. public void SetCacheProcessedDelegate(CacheProcessedDelegate callback) { Feature.UserGameplayDataSetCacheProcessedCallback(callback); } /// /// Write the pending API calls to cache.

/// /// Pending API calls are requests that could not be sent due to network being offline or other failures.
/// The internal queue of pending calls is cleared. It is recommended to stop the background thread before calling this method.

/// /// This is the non-blocking version persist to cache call and should be used when saving during play, if you need to persist to cache on application close the blocking version, ImmediatePersistToCache() is recommended.
/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_CACHE_WRITE_FAILED: There was an issue writing the queue to the offline cache file.
///
/// Path to the offline cache file. /// Delegate that is called once the function has finished executing. /// Unity JobHandle object for the call public void PersistToCache(string offlineCacheFile, Action callback) { Call(Feature.UserGameplayDataPersistApiCallsToCache, offlineCacheFile, callback); } /// /// Write the pending API calls to cache.

/// /// Pending API calls are requests that could not be sent due to network being offline or other failures.
/// The internal queue of pending calls is cleared. It is recommended to stop the background thread before calling this method.

/// /// This is the blocking version of the call and should be used on application close. PersistToCache() is recommended for saving to cache during play.
/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_CACHE_WRITE_FAILED: There was an issue writing the queue to the offline cache file.
///
/// Path to the offline cache file. /// The result code of persisting the queue to cache. public uint ImmediatePersistToCache(string offlineCacheFile) { return Feature.UserGameplayDataPersistApiCallsToCache(offlineCacheFile); } /// /// Read the pending API calls from cache.

/// /// The calls will be enqueued and retried as soon as the Retry background thread is started and network connectivity is up.
/// The contents of the cache are deleted.

/// /// Result status codes returned in the callback function (from GameKitErrors.cs):
/// - GAMEKIT_SUCCESS: The API call was successful.
/// - GAMEKIT_ERROR_USER_GAMEPLAY_DATA_CACHE_READ_FAILED: There was an issue loading the offline cache file to the queue.
///
/// Path to the offline cache file. /// Delegate that is called once the function has finished executing. /// Unity JobHandle object for the call public void LoadFromCache(string offlineCacheFile, Action callback) { Call(Feature.UserGameplayDataLoadApiCallsFromCache, offlineCacheFile, callback); } /// /// Forces an immediate retry of the calls that are in the queue. /// /// The Network state can transiton from Unhealthy to Healthy as a side effect of this. /// Callback invoked then the retry finishes. Returns success or failure. public void ForceRetry(Action resultCallback) { Logging.LogInfo("Forcing a Retry to synchronize the data with the backend."); Feature.ForceRetry(resultCallback); } /// /// Attempts to synchronize data in the queue with the backend and, if successful, executes another User Gameplay Data API. /// Use this when you want to be sure that the data in the backend is updated before making subsequent calls. /// /// The User Gameplay Data API to call (for example GetBundle(), GetBundleItem(), ListBundles(), or other APIs) /// Action to execute in case of error. Error can happen if the device is still offline or if the backend is experiencing problems. public void TryForceSynchronizeAndExecute(Action gameplayDataApiCall, Action onSynchronizeErrorAction) { if (GetLastNetworkHealthState()) { gameplayDataApiCall(); } else { ForceRetry((bool success) => { if (success) { gameplayDataApiCall(); } else { onSynchronizeErrorAction(); } }); } } } }