diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs
index 20238cb82..6a5aee8f8 100644
--- a/Runtime/Client/LootLockerEndPoints.cs
+++ b/Runtime/Client/LootLockerEndPoints.cs
@@ -74,6 +74,14 @@ public class LootLockerEndPoints
public static EndPointClass uploadPlayerFile = new EndPointClass("player/files", LootLockerHTTPMethod.UPLOAD_FILE);
public static EndPointClass updatePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.UPDATE_FILE);
public static EndPointClass deletePlayerFile = new EndPointClass("/player/files/{0}", LootLockerHTTPMethod.DELETE);
+ public static EndPointClass listPlayerFileRevisions = new EndPointClass("player/files/{0}/revisions", LootLockerHTTPMethod.GET);
+ public static EndPointClass getPlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}", LootLockerHTTPMethod.GET);
+ public static EndPointClass promotePlayerFileRevision = new EndPointClass("player/files/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST);
+ public static EndPointClass getPlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.GET);
+ public static EndPointClass listPlayerFileRevisionsByKey = new EndPointClass("player/files/key/{0}/revisions", LootLockerHTTPMethod.GET);
+ public static EndPointClass getPlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}", LootLockerHTTPMethod.GET);
+ public static EndPointClass promotePlayerFileRevisionByKey = new EndPointClass("player/files/key/{0}/revisions/{1}/current", LootLockerHTTPMethod.POST);
+ public static EndPointClass deletePlayerFileByKey = new EndPointClass("player/files/key/{0}", LootLockerHTTPMethod.DELETE);
// Player Progressions
[Header("Player Progressions")]
diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs
index 10e5395f3..9571f92ed 100644
--- a/Runtime/Game/LootLockerSDKManager.cs
+++ b/Runtime/Game/LootLockerSDKManager.cs
@@ -4095,7 +4095,6 @@ public static void UploadPlayerFile(string pathToFile, string filePurpose, bool
{ "public", isPublic.ToString().ToLower() }
};
-
var fileBytes = new byte[] { };
try
{
@@ -4179,7 +4178,7 @@ public static void UploadPlayerFile(FileStream fileStream, string filePurpose, b
/// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
public static void UploadPlayerFile(FileStream fileStream, string filePurpose, Action onComplete, string forPlayerWithUlid = null)
{
- UploadPlayerFile(fileStream, filePurpose, false, onComplete, forPlayerWithUlid);
+ UploadPlayerFile(fileStream, filePurpose, isPublic: false, onComplete, forPlayerWithUlid: forPlayerWithUlid);
}
/// @ingroup PlayerFiles
@@ -4224,7 +4223,185 @@ public static void UploadPlayerFile(byte[] fileBytes, string fileName, string fi
/// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
public static void UploadPlayerFile(byte[] fileBytes, string fileName, string filePurpose, Action onComplete, string forPlayerWithUlid = null)
{
- UploadPlayerFile(fileBytes, fileName, filePurpose, false, onComplete, forPlayerWithUlid);
+ UploadPlayerFile(fileBytes, fileName, filePurpose, isPublic: false, onComplete, forPlayerWithUlid: forPlayerWithUlid);
+ }
+
+ /// @ingroup PlayerFiles
+ ///////////////////////////////////////////////////////////////////////////////
+
+ // ================================================================
+ // UploadPlayerFileByKey — dedicated overloads for upsert-by-key
+ // ================================================================
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file with the provided name and content, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ ///
+ /// Path to the file, example: Application.persistentDataPath + "/" + fileName;
+ /// Purpose of the file, example: savefile/config
+ /// Should this file be viewable by other players?
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(string pathToFile, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ var body = new Dictionary()
+ {
+ { "purpose", filePurpose },
+ { "public", isPublic.ToString().ToLower() },
+ { "key", key }
+ };
+
+ var fileBytes = new byte[] { };
+ try
+ {
+ fileBytes = File.ReadAllBytes(pathToFile);
+ }
+ catch (Exception e)
+ {
+ LootLockerLogger.Log($"File error: {e.Message}", LootLockerLogger.LogLevel.Error);
+ return;
+ }
+
+ LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(pathToFile), "multipart/form-data", body,
+ onComplete: (serverResponse) =>
+ {
+ LootLockerResponse.Deserialize(onComplete, serverResponse);
+ });
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file with the provided name and content, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ /// The file will not be viewable by other players.
+ ///
+ /// Path to the file, example: Application.persistentDataPath + "/" + fileName;
+ /// Purpose of the file, example: savefile/config
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(string pathToFile, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ UploadPlayerFileByKey(pathToFile, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file using a Filestream, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ ///
+ /// Filestream to upload
+ /// Purpose of the file, example: savefile/config
+ /// Should this file be viewable by other players?
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(FileStream fileStream, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ var body = new Dictionary()
+ {
+ { "purpose", filePurpose },
+ { "public", isPublic.ToString().ToLower() },
+ { "key", key }
+ };
+
+ var fileBytes = new byte[fileStream.Length];
+ try
+ {
+ fileStream.Read(fileBytes, 0, Convert.ToInt32(fileStream.Length));
+ }
+ catch (Exception e)
+ {
+ LootLockerLogger.Log($"File error: {e.Message}", LootLockerLogger.LogLevel.Error);
+ return;
+ }
+
+ LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(fileStream.Name), "multipart/form-data", body,
+ onComplete: (serverResponse) =>
+ {
+ LootLockerResponse.Deserialize(onComplete, serverResponse);
+ });
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file using a Filestream, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ /// The file will not be viewable by other players.
+ ///
+ /// Filestream to upload
+ /// Purpose of the file, example: savefile/config
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(FileStream fileStream, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ UploadPlayerFileByKey(fileStream, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file using a byte array, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ ///
+ /// Byte array to upload
+ /// Name of the file on LootLocker
+ /// Purpose of the file, example: savefile/config
+ /// Should this file be viewable by other players?
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(byte[] fileBytes, string fileName, string filePurpose, bool isPublic, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ var body = new Dictionary()
+ {
+ { "purpose", filePurpose },
+ { "public", isPublic.ToString().ToLower() },
+ { "key", key }
+ };
+
+ LootLockerServerRequest.UploadFile(forPlayerWithUlid, LootLockerEndPoints.uploadPlayerFile, fileBytes, Path.GetFileName(fileName), "multipart/form-data", body,
+ onComplete: (serverResponse) =>
+ {
+ LootLockerResponse.Deserialize(onComplete, serverResponse);
+ });
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Upload a file using a byte array, using a key for upsert behavior.
+ /// If a file with the given key already exists for this player, it will be updated.
+ /// The file will not be viewable by other players.
+ ///
+ /// Byte array to upload
+ /// Name of the file on LootLocker
+ /// Purpose of the file, example: savefile/config
+ /// Key for upsert behavior. If a file with this key already exists, it will be updated.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void UploadPlayerFileByKey(byte[] fileBytes, string fileName, string filePurpose, string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ UploadPlayerFileByKey(fileBytes, fileName, filePurpose, isPublic: false, key, onComplete, forPlayerWithUlid: forPlayerWithUlid);
}
/// @ingroup PlayerFiles
@@ -4345,6 +4522,154 @@ public static void DeletePlayerFile(int fileId, Action onCom
LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.DELETE, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
}
+
+ /// @ingroup PlayerFiles
+ ///
+ /// List all revisions for a player file.
+ ///
+ /// Id of the file.
+ /// onComplete Action for handling the response of type LootLockerPlayerFileRevisionsResponse
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void GetPlayerFileRevisions(int fileId, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.ListPlayerFileRevisions(forPlayerWithUlid, fileId, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Get a specific revision of a player file by its revision id.
+ ///
+ /// Id of the file.
+ /// The ULID of the revision to retrieve.
+ /// onComplete Action for handling the response of type LootLockerPlayerFileContent
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void GetPlayerFileRevision(int fileId, string revisionId, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.GetPlayerFileRevision(forPlayerWithUlid, fileId, revisionId, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Promote a specific revision to be the current (active) revision of a player file.
+ ///
+ /// Id of the file.
+ /// The ULID of the revision to promote.
+ /// onComplete Action for handling the response of type LootLockerResponse
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void PromotePlayerFileRevision(int fileId, string revisionId, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.PromotePlayerFileRevision(forPlayerWithUlid, fileId, revisionId, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Get a player file by its key.
+ ///
+ /// The key of the file.
+ /// onComplete Action for handling the response of type LootLockerPlayerFile
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void GetPlayerFileByKey(string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.GetPlayerFileByKey(forPlayerWithUlid, key, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// List all revisions for a player file identified by its key.
+ ///
+ /// The key of the file.
+ /// onComplete Action for handling the response of type LootLockerPlayerFileRevisionsResponse
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void GetPlayerFileRevisionsByKey(string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.ListPlayerFileRevisionsByKey(forPlayerWithUlid, key, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Get a specific revision of a player file by its key and revision id.
+ ///
+ /// The key of the file.
+ /// The ULID of the revision to retrieve.
+ /// onComplete Action for handling the response of type LootLockerPlayerFileContent
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void GetPlayerFileRevisionByKey(string key, string revisionId, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.GetPlayerFileRevisionByKey(forPlayerWithUlid, key, revisionId, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Promote a specific revision to be the current (active) revision of a player file identified by its key.
+ ///
+ /// The key of the file.
+ /// The ULID of the revision to promote.
+ /// onComplete Action for handling the response of type LootLockerResponse
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void PromotePlayerFileRevisionByKey(string key, string revisionId, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.PromotePlayerFileRevisionByKey(forPlayerWithUlid, key, revisionId, onComplete);
+ }
+
+ /// @ingroup PlayerFiles
+ ///
+ /// Delete a player file by its key.
+ ///
+ /// The key of the file to delete.
+ /// onComplete Action for handling the response of type LootLockerResponse
+ /// Optional : Execute the request for the specified player. If not supplied, the default player will be used.
+ public static void DeletePlayerFileByKey(string key, Action onComplete, string forPlayerWithUlid = null)
+ {
+ if (!CheckInitialized(false, forPlayerWithUlid))
+ {
+ onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid));
+ return;
+ }
+
+ LootLockerAPIManager.DeletePlayerFileByKey(forPlayerWithUlid, key, onComplete);
+ }
#endregion
#region Player progressions
diff --git a/Runtime/Game/Requests/PlayerRequest.cs b/Runtime/Game/Requests/PlayerRequest.cs
index 22783a442..3680fa732 100644
--- a/Runtime/Game/Requests/PlayerRequest.cs
+++ b/Runtime/Game/Requests/PlayerRequest.cs
@@ -380,6 +380,8 @@ public class LootLockerPlayerFile : LootLockerResponse
public string revision_id { get; set; }
/// The file name.
public string name { get; set; }
+ /// The optional key for upsert operations.
+ public string key { get; set; }
/// The file size in bytes.
public int size { get; set; }
/// The purpose or category tag for this file.
@@ -400,6 +402,53 @@ public class LootLockerPlayerFile : LootLockerResponse
public DateTime created_at { get; set; }
}
+ ///
+ /// Response containing a list of revisions for a player file.
+ ///
+ public class LootLockerPlayerFileRevisionsResponse : LootLockerResponse
+ {
+ /// The list of revisions.
+ public LootLockerPlayerFileContent[] revisions { get; set; }
+ /// Metadata about the file.
+ public LootLockerPlayerFileMetadata file { get; set; }
+ /// The ULID of the current (active) revision.
+ public string current_revision_id { get; set; }
+ }
+
+ ///
+ /// Metadata about a player file, returned as part of the revisions response.
+ ///
+ public class LootLockerPlayerFileMetadata
+ {
+ /// When the file was created.
+ public DateTime created_at { get; set; }
+ /// The file name.
+ public string name { get; set; }
+ /// The optional key for upsert operations.
+ public string key { get; set; }
+ /// The purpose or category tag for this file.
+ public string purpose { get; set; }
+ /// The unique identifier of this player file.
+ public int id { get; set; }
+ /// Whether this file is publicly accessible.
+ public bool is_public { get; set; }
+ }
+
+ ///
+ /// A single file revision with download URL and metadata.
+ ///
+ public class LootLockerPlayerFileContent : LootLockerResponse
+ {
+ /// The ULID of this revision.
+ public string id { get; set; }
+ /// The signed URL to download this revision.
+ public string url { get; set; }
+ /// The file size in bytes.
+ public int size { get; set; }
+ /// When this revision was created.
+ public DateTime created_at { get; set; }
+ }
+
///
/// Response containing asset reward notifications for the current player.
///
@@ -470,5 +519,53 @@ public static void LookupPlayer1stPartyPlatformIDs(string forPlayerWithUlid, Loo
LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
}
+
+ public static void ListPlayerFileRevisions(string forPlayerWithUlid, int fileId, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.listPlayerFileRevisions.WithPathParameter(fileId);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void GetPlayerFileRevision(string forPlayerWithUlid, int fileId, string revisionId, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.getPlayerFileRevision.WithPathParameters(fileId, revisionId);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void PromotePlayerFileRevision(string forPlayerWithUlid, int fileId, string revisionId, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.promotePlayerFileRevision.WithPathParameters(fileId, revisionId);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.POST, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void GetPlayerFileByKey(string forPlayerWithUlid, string key, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.getPlayerFileByKey.WithPathParameter(key);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void ListPlayerFileRevisionsByKey(string forPlayerWithUlid, string key, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.listPlayerFileRevisionsByKey.WithPathParameter(key);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void GetPlayerFileRevisionByKey(string forPlayerWithUlid, string key, string revisionId, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.getPlayerFileRevisionByKey.WithPathParameters(key, revisionId);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.GET, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void PromotePlayerFileRevisionByKey(string forPlayerWithUlid, string key, string revisionId, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.promotePlayerFileRevisionByKey.WithPathParameters(key, revisionId);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.POST, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
+
+ public static void DeletePlayerFileByKey(string forPlayerWithUlid, string key, Action onComplete)
+ {
+ var endpoint = LootLockerEndPoints.deletePlayerFileByKey.WithPathParameter(key);
+ LootLockerServerRequest.CallAPI(forPlayerWithUlid, endpoint, LootLockerHTTPMethod.DELETE, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); });
+ }
}
}
diff --git a/Runtime/Game/Requests/RemoteSessionRequest.cs b/Runtime/Game/Requests/RemoteSessionRequest.cs
index e536b8374..efec97937 100644
--- a/Runtime/Game/Requests/RemoteSessionRequest.cs
+++ b/Runtime/Game/Requests/RemoteSessionRequest.cs
@@ -56,12 +56,17 @@ public class LootLockerLeaseRemoteSessionRequest
/// The Game Version configured for the game
///
public string game_version { get; set; }
+ ///
+ /// Optional list of identity providers to restrict the remote session to (e.g., "steam", "apple")
+ ///
+ public string[] providers { get; set; }
- public LootLockerLeaseRemoteSessionRequest(string titleId, string environmentId)
+ public LootLockerLeaseRemoteSessionRequest(string titleId, string environmentId, string[] providers = null)
{
title_id = titleId;
environment_id = environmentId;
game_version = LootLockerConfig.current.game_version;
+ this.providers = providers;
}
}
@@ -379,7 +384,7 @@ protected IEnumerator ContinualPollingAction(Guid processGuid)
{
yield break;
}
- yield return new WaitForSeconds(preProcess.PollingIntervalSeconds);
+ yield return new WaitForSecondsRealtime(preProcess.PollingIntervalSeconds);
while (_remoteSessionsProcesses.TryGetValue(processGuid, out var process))
{
// Check if we should continue the polling
@@ -418,13 +423,21 @@ protected IEnumerator ContinualPollingAction(Guid processGuid)
yield break;
}
+ // If the process was cancelled while the HTTP poll was in-flight, skip
+ // the status-update callback and let the next while-iteration handle it
+ // via the ShouldCancel check at the top of the loop.
+ if (processAfterStatusCheck.ShouldCancel)
+ {
+ continue;
+ }
+
if (!startSessionResponse.success)
{
if (startSessionResponse.statusCode >= 500 && startSessionResponse.statusCode <= 599 && processAfterStatusCheck.Retries <= _leasingProcessPollingRetryLimit)
{
// Recoverable error
processAfterStatusCheck.Retries++;
- yield return new WaitForSeconds(processAfterStatusCheck.PollingIntervalSeconds);
+ yield return new WaitForSecondsRealtime(processAfterStatusCheck.PollingIntervalSeconds);
continue;
}
@@ -451,7 +464,7 @@ protected IEnumerator ContinualPollingAction(Guid processGuid)
processAfterStatusCheck.LastUpdatedStatus = pollingResponse.lease_status;
// Sleep for a bit before checking again
- yield return new WaitForSeconds(processAfterStatusCheck.PollingIntervalSeconds);
+ yield return new WaitForSecondsRealtime(processAfterStatusCheck.PollingIntervalSeconds);
}
}
@@ -549,29 +562,18 @@ private void LeaseRemoteSession(
Action onComplete,
string providerUrlParam = null)
{
+ string[] providers = string.IsNullOrEmpty(providerUrlParam) ? null : new[] { providerUrlParam };
LootLockerLeaseRemoteSessionRequest leaseRemoteSessionRequest =
- new LootLockerLeaseRemoteSessionRequest(titleId, environmentId);
+ new LootLockerLeaseRemoteSessionRequest(titleId, environmentId, providers);
EndPointClass endPoint = leaseIntent == LootLockerRemoteSessionLeaseIntent.login ? LootLockerEndPoints.leaseRemoteSession : LootLockerEndPoints.leaseRemoteSessionForLinking;
+
LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint,
endPoint.httpMethod,
LootLockerJson.SerializeObject(leaseRemoteSessionRequest),
(serverResponse) =>
{
var response = LootLockerResponse.Deserialize(serverResponse);
- if (!string.IsNullOrEmpty(providerUrlParam) && response != null)
- {
- if (response.redirect_url != null)
- {
- string separator = response.redirect_url.Contains("?") ? "&" : "?";
- response.redirect_url = response.redirect_url + separator + "provider=" + providerUrlParam;
- }
- if (response.display_url != null)
- {
- string separator = response.display_url.Contains("?") ? "&" : "?";
- response.display_url = response.display_url + separator + "provider=" + providerUrlParam;
- }
- }
onComplete?.Invoke(response);
},
leaseIntent == LootLockerRemoteSessionLeaseIntent.link);
diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs
index 6d5dc6d66..751990bd3 100644
--- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs
+++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationTitleConfig.cs
@@ -8,7 +8,8 @@ public static class LootLockerTestConfigurationTitleConfig
public enum TitleConfigKeys
{
- global_player_presence
+ global_player_presence,
+ white_label_custom_signup_fields
}
public class PresenceTitleConfigRequest
@@ -17,6 +18,21 @@ public class PresenceTitleConfigRequest
public bool advanced_mode { get; set; }
}
+ public class WhiteLabelCustomSignUpFieldDefinition
+ {
+ public string question_text { get; set; }
+ public string metadata_key { get; set; }
+ public string field_type { get; set; }
+ public bool required { get; set; }
+ public bool sensitive { get; set; }
+ public int sort_order { get; set; }
+ }
+
+ public class WhiteLabelCustomSignUpFieldsConfigRequest
+ {
+ public WhiteLabelCustomSignUpFieldDefinition[] fields { get; set; }
+ }
+
public static void GetGameConfig(TitleConfigKeys ConfigKey, Action onComplete)
{
if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken))
@@ -52,5 +68,29 @@ public static void UpdateGameConfig(TitleConfigKeys ConfigKey, bool Enabled, boo
onComplete?.Invoke(serverResponse);
}, true);
}
+
+ public static void SetCustomSignUpFields(WhiteLabelCustomSignUpFieldDefinition[] fields, Action onComplete)
+ {
+ if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken))
+ {
+ onComplete?.Invoke(false, "Not logged in");
+ return;
+ }
+
+ var request = new WhiteLabelCustomSignUpFieldsConfigRequest
+ {
+ fields = fields
+ };
+ string json = LootLockerJson.SerializeObject(request);
+ LootLockerTestGameAdmin.SetGameConfig("white_label_custom_signup_fields", json, response =>
+ {
+ if (response == null)
+ {
+ onComplete?.Invoke(false, "Null response from SetGameConfig");
+ return;
+ }
+ onComplete?.Invoke(response.success, response.errorData?.message);
+ });
+ }
}
}
diff --git a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs
index 24b94a55f..7e2b1e1f2 100644
--- a/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs
+++ b/Tests/LootLockerTests/PlayMode/PlayerFilesTest.cs
@@ -6,7 +6,6 @@
using System;
using System.Collections;
using System.IO;
-using System.Net;
using UnityEngine;
using UnityEngine.TestTools;
@@ -78,10 +77,7 @@ public IEnumerator Setup()
});
yield return new WaitUntil(() => guestLoginCompleted);
-
-
Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####");
-
}
[UnityTearDown]
@@ -110,17 +106,28 @@ public IEnumerator TearDown()
Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####");
}
+ // --- Helpers ---
- [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ private string CreateTempFile(string content, string suffix = ".txt")
+ {
+ string path = Application.temporaryCachePath + $"/{this.GetType().Name}{TestCounter}-{Guid.NewGuid()}{suffix}";
+ using (TextWriter writer = new StreamWriter(path))
+ {
+ writer.WriteLine(content);
+ }
+ return path;
+ }
+
+ // ================================================================
+ // Phase 1: Core Upload & Key Tests
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")]
public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds()
{
Assert.IsFalse(SetupFailed, "Failed to setup game");
// Given
- string path = Application.temporaryCachePath + "/PlayerFileCanBeCreatedWithPathUpdatedAndThenDeleted-creation.txt";
- string content = "First added line";
- TextWriter writer = new StreamWriter(path);
- writer.WriteLine(content);
- writer.Close();
+ string path = CreateTempFile("First added line");
// When
LootLockerPlayerFile actualResponse = new LootLockerPlayerFile();
@@ -132,7 +139,6 @@ public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds()
playerFileUploadCompleted = true;
});
- // Wait for response
yield return new WaitUntil(() => playerFileUploadCompleted);
// Then
@@ -140,5 +146,835 @@ public IEnumerator PlayerFiles_UploadSimplePublicFile_Succeeds()
Assert.Greater(actualResponse.size, 0, "File Size was 0");
Assert.AreEqual(setToPublic, actualResponse.is_public, "File does not have the same public setting");
}
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UploadWithKey_ReturnsKeyInResponse()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string path = CreateTempFile("Content with key");
+ string fileKey = "test-key-" + TestCounter;
+
+ // When
+ LootLockerPlayerFile actualResponse = new LootLockerPlayerFile();
+ bool completed = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse =>
+ {
+ actualResponse = fileResponse;
+ completed = true;
+ });
+
+ yield return new WaitUntil(() => completed);
+
+ // Then
+ Assert.IsTrue(actualResponse.success, "File upload with key failed");
+ Assert.AreEqual(fileKey, actualResponse.key, "Key in response does not match");
+ Assert.Greater(actualResponse.size, 0, "File Size was 0");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UploadWithSameKeyTwice_UpdatesExistingFile()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "upsert-key-" + TestCounter;
+ string pathA = CreateTempFile("Original content");
+ string pathB = CreateTempFile("Updated content that is longer");
+
+ // When — first upload
+ LootLockerPlayerFile firstResponse = new LootLockerPlayerFile();
+ bool firstDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, fileResponse =>
+ {
+ firstResponse = fileResponse;
+ firstDone = true;
+ });
+ yield return new WaitUntil(() => firstDone);
+ Assert.IsTrue(firstResponse.success, "First upload failed");
+
+ // When — second upload with same key
+ LootLockerPlayerFile secondResponse = new LootLockerPlayerFile();
+ bool secondDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, fileResponse =>
+ {
+ secondResponse = fileResponse;
+ secondDone = true;
+ });
+ yield return new WaitUntil(() => secondDone);
+
+ // Then
+ Assert.IsTrue(secondResponse.success, "Second upload (upsert) failed");
+ Assert.AreEqual(firstResponse.id, secondResponse.id, "File ID should be the same after upsert");
+ Assert.AreNotEqual(firstResponse.size, secondResponse.size, "File size should differ after upsert with different content");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UploadWithoutKey_ReturnsEmptyKey()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string path = CreateTempFile("No key content");
+
+ // When
+ LootLockerPlayerFile actualResponse = new LootLockerPlayerFile();
+ bool completed = false;
+ LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse =>
+ {
+ actualResponse = fileResponse;
+ completed = true;
+ });
+
+ yield return new WaitUntil(() => completed);
+
+ // Then
+ Assert.IsTrue(actualResponse.success, "File upload without key failed");
+ Assert.IsTrue(string.IsNullOrEmpty(actualResponse.key), "Key should be null or empty when not provided");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UploadPrivateFile_Succeeds()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string path = CreateTempFile("Private content");
+
+ // When
+ LootLockerPlayerFile actualResponse = new LootLockerPlayerFile();
+ bool completed = false;
+ LootLockerSDKManager.UploadPlayerFile(path, "test", false, fileResponse =>
+ {
+ actualResponse = fileResponse;
+ completed = true;
+ });
+
+ yield return new WaitUntil(() => completed);
+
+ // Then
+ Assert.IsTrue(actualResponse.success, "Private file upload failed");
+ Assert.IsFalse(actualResponse.is_public, "File should not be public");
+ }
+
+ // ================================================================
+ // Phase 2: Key-Based Lookup & Delete
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetFileByKey_ReturnsCorrectFile()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "lookup-key-" + TestCounter;
+ string path = CreateTempFile("Lookup by key content");
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Upload for lookup test failed");
+
+ // When
+ LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse =>
+ {
+ fetchedFile = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+
+ // Then
+ Assert.IsTrue(fetchedFile.success, "GetPlayerFileByKey failed");
+ Assert.AreEqual(uploadedFile.id, fetchedFile.id, "File ID should match");
+ Assert.AreEqual(fileKey, fetchedFile.key, "Key should match");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetFileByKey_NonExistentKey_Fails()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // When
+ bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings;
+ LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure
+ LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFileByKey("nonexistent-key-" + TestCounter, fileResponse =>
+ {
+ fetchedFile = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+ LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting;
+
+ // Then
+ Assert.IsFalse(fetchedFile.success, "GetPlayerFileByKey should fail for non-existent key");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_DeleteFileByKey_RemovesFile()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "delete-key-" + TestCounter;
+ string path = CreateTempFile("To be deleted by key");
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Upload for delete-by-key test failed");
+
+ // When — delete by key
+ LootLockerResponse deleteResponse = new LootLockerResponse();
+ bool deleteDone = false;
+ LootLockerSDKManager.DeletePlayerFileByKey(fileKey, response =>
+ {
+ deleteResponse = response;
+ deleteDone = true;
+ });
+ yield return new WaitUntil(() => deleteDone);
+
+ // Then — verify deletion
+ Assert.IsTrue(deleteResponse.success, "DeletePlayerFileByKey failed");
+
+ bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings;
+ LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure
+ LootLockerPlayerFile fetchedFileResponse = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse =>
+ {
+ fetchedFileResponse = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+ Assert.IsFalse(fetchedFileResponse.success, "File should no longer exist after deletion by key");
+ LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting;
+ }
+
+ // ================================================================
+ // Phase 3: Revisions by ID
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UpdateFile_CreatesNewRevision()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string pathA = CreateTempFile("Original revision content");
+ string pathB = CreateTempFile("Updated revision content");
+
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Initial upload failed");
+
+ // When — update the file
+ LootLockerPlayerFile updatedFile = new LootLockerPlayerFile();
+ bool updateDone = false;
+ LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, fileResponse =>
+ {
+ updatedFile = fileResponse;
+ updateDone = true;
+ });
+ yield return new WaitUntil(() => updateDone);
+ Assert.IsTrue(updatedFile.success, "Update failed");
+
+ // Then — list revisions
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+
+ Assert.IsTrue(revisionsResponse.success, "List revisions failed");
+ Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions after update");
+ Assert.IsNotNull(revisionsResponse.current_revision_id, "Current revision ID should be set");
+ Assert.AreEqual(revisionsResponse.current_revision_id, revisionsResponse.revisions[revisionsResponse.revisions.Length - 1].id,
+ "Current revision should be the latest");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetFileRevision_ReturnsSpecificRevision()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string pathA = CreateTempFile("First revision");
+ string pathB = CreateTempFile("Second revision");
+
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Initial upload failed");
+
+ // Update to create a second revision
+ LootLockerPlayerFile updateResponse = new LootLockerPlayerFile();
+ bool updateDone = false;
+ LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, response =>
+ {
+ updateResponse = response;
+ updateDone = true;
+ });
+ yield return new WaitUntil(() => updateDone);
+ Assert.IsTrue(updateResponse.success, "Update to create revision 2 failed");
+
+ // Get revision list to find the first revision ID
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+ Assert.IsTrue(revisionsResponse.success, "List revisions failed");
+ Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions");
+
+ // When — get the first (oldest) revision
+ string firstRevisionId = revisionsResponse.revisions[0].id;
+ LootLockerPlayerFileContent revisionContent = new LootLockerPlayerFileContent();
+ bool getRevisionDone = false;
+ LootLockerSDKManager.GetPlayerFileRevision(uploadedFile.id, firstRevisionId, response =>
+ {
+ revisionContent = response;
+ getRevisionDone = true;
+ });
+ yield return new WaitUntil(() => getRevisionDone);
+
+ // Then
+ Assert.IsTrue(revisionContent.success, "GetPlayerFileRevision failed");
+ Assert.AreEqual(firstRevisionId, revisionContent.id, "Revision ID should match");
+ Assert.Greater(revisionContent.size, 0, "Revision size should be > 0");
+ Assert.IsFalse(string.IsNullOrEmpty(revisionContent.url), "Revision URL should not be empty");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_PromoteFileRevision_RestoresOldRevision()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string pathA = CreateTempFile("First revision content");
+ string pathB = CreateTempFile("Second revision content");
+
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Initial upload failed");
+
+ // Update to create revision 2
+ LootLockerPlayerFile updateResponse = new LootLockerPlayerFile();
+ bool updateDone = false;
+ LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, response =>
+ {
+ updateResponse = response;
+ updateDone = true;
+ });
+ yield return new WaitUntil(() => updateDone);
+ Assert.IsTrue(updateResponse.success, "Update to create revision 2 failed");
+
+ // Get revision list to find the first revision ID
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisions(uploadedFile.id, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+ Assert.IsTrue(revisionsResponse.success, "List revisions failed");
+ string firstRevisionId = revisionsResponse.revisions[0].id;
+
+ // When — promote the first revision back to current
+ LootLockerResponse promoteResponse = new LootLockerResponse();
+ bool promoteDone = false;
+ LootLockerSDKManager.PromotePlayerFileRevision(uploadedFile.id, firstRevisionId, response =>
+ {
+ promoteResponse = response;
+ promoteDone = true;
+ });
+ yield return new WaitUntil(() => promoteDone);
+
+ // Then
+ Assert.IsTrue(promoteResponse.success, "Promote revision failed");
+
+ // Verify the current revision changed
+ LootLockerPlayerFile refreshedFile = new LootLockerPlayerFile();
+ bool refreshDone = false;
+ LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse =>
+ {
+ refreshedFile = fileResponse;
+ refreshDone = true;
+ });
+ yield return new WaitUntil(() => refreshDone);
+ Assert.IsTrue(refreshedFile.success, "GetPlayerFile after promote failed");
+ Assert.AreEqual(firstRevisionId, refreshedFile.revision_id, "Current revision should be the promoted one");
+ }
+
+ // ================================================================
+ // Phase 4: Revisions by Key
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetFileRevisionsByKey_ReturnsRevisions()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "rev-key-" + TestCounter;
+ string pathA = CreateTempFile("Revision A by key");
+ string pathB = CreateTempFile("Revision B by key");
+
+ // Upload with key (creates revision 1)
+ LootLockerPlayerFile firstUpload = new LootLockerPlayerFile();
+ bool firstDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response =>
+ {
+ firstUpload = response;
+ firstDone = true;
+ });
+ yield return new WaitUntil(() => firstDone);
+ Assert.IsTrue(firstUpload.success, "First upload for revisions by key test failed");
+
+ // Upsert with same key (creates revision 2)
+ LootLockerPlayerFile secondUpload = new LootLockerPlayerFile();
+ bool secondDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response =>
+ {
+ secondUpload = response;
+ secondDone = true;
+ });
+ yield return new WaitUntil(() => secondDone);
+ Assert.IsTrue(secondUpload.success, "Second upload for revisions by key test failed");
+
+ // When
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+
+ // Then
+ Assert.IsTrue(revisionsResponse.success, "GetPlayerFileRevisionsByKey failed");
+ Assert.GreaterOrEqual(revisionsResponse.revisions.Length, 2, "Should have at least 2 revisions");
+ Assert.AreEqual(fileKey, revisionsResponse.file.key, "File metadata key should match");
+ Assert.IsNotNull(revisionsResponse.current_revision_id, "Current revision ID should be set");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetFileRevisionByKey_ReturnsSpecificRevision()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "rev-get-key-" + TestCounter;
+ string pathA = CreateTempFile("First revision by key");
+ string pathB = CreateTempFile("Second revision by key");
+
+ LootLockerPlayerFile firstUpload = new LootLockerPlayerFile();
+ bool firstDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response =>
+ {
+ firstUpload = response;
+ firstDone = true;
+ });
+ yield return new WaitUntil(() => firstDone);
+ Assert.IsTrue(firstUpload.success, "First upload for get revision by key test failed");
+
+ LootLockerPlayerFile secondUpload = new LootLockerPlayerFile();
+ bool secondDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response =>
+ {
+ secondUpload = response;
+ secondDone = true;
+ });
+ yield return new WaitUntil(() => secondDone);
+ Assert.IsTrue(secondUpload.success, "Second upload for get revision by key test failed");
+
+ // Get revision list to find a revision ID
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+ Assert.IsTrue(revisionsResponse.success, "List revisions by key failed");
+ string firstRevisionId = revisionsResponse.revisions[0].id;
+
+ // When
+ LootLockerPlayerFileContent revisionContent = new LootLockerPlayerFileContent();
+ bool getRevisionDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisionByKey(fileKey, firstRevisionId, response =>
+ {
+ revisionContent = response;
+ getRevisionDone = true;
+ });
+ yield return new WaitUntil(() => getRevisionDone);
+
+ // Then
+ Assert.IsTrue(revisionContent.success, "GetPlayerFileRevisionByKey failed");
+ Assert.AreEqual(firstRevisionId, revisionContent.id, "Revision ID should match");
+ Assert.Greater(revisionContent.size, 0, "Revision size should be > 0");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_PromoteFileRevisionByKey_PromotesRevision()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "promote-key-" + TestCounter;
+ string pathA = CreateTempFile("First revision for promote by key");
+ string pathB = CreateTempFile("Second revision for promote by key");
+
+ LootLockerPlayerFile firstUpload = new LootLockerPlayerFile();
+ bool firstDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response =>
+ {
+ firstUpload = response;
+ firstDone = true;
+ });
+ yield return new WaitUntil(() => firstDone);
+ Assert.IsTrue(firstUpload.success, "First upload for promote by key test failed");
+
+ LootLockerPlayerFile secondUpload = new LootLockerPlayerFile();
+ bool secondDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response =>
+ {
+ secondUpload = response;
+ secondDone = true;
+ });
+ yield return new WaitUntil(() => secondDone);
+ Assert.IsTrue(secondUpload.success, "Second upload for promote by key test failed");
+
+ // Get revision list to find the first revision ID
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisionsByKey(fileKey, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+ Assert.IsTrue(revisionsResponse.success, "List revisions by key failed");
+ string firstRevisionId = revisionsResponse.revisions[0].id;
+
+ // When — promote the first revision
+ LootLockerResponse promoteResponse = new LootLockerResponse();
+ bool promoteDone = false;
+ LootLockerSDKManager.PromotePlayerFileRevisionByKey(fileKey, firstRevisionId, response =>
+ {
+ promoteResponse = response;
+ promoteDone = true;
+ });
+ yield return new WaitUntil(() => promoteDone);
+
+ // Then
+ Assert.IsTrue(promoteResponse.success, "PromotePlayerFileRevisionByKey failed");
+
+ // Verify the current revision changed
+ LootLockerPlayerFile refreshedFile = new LootLockerPlayerFile();
+ bool refreshDone = false;
+ LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse =>
+ {
+ refreshedFile = fileResponse;
+ refreshDone = true;
+ });
+ yield return new WaitUntil(() => refreshDone);
+ Assert.IsTrue(refreshedFile.success, "GetPlayerFileByKey after promote failed");
+ Assert.AreEqual(firstRevisionId, refreshedFile.revision_id, "Current revision should be the promoted one");
+ }
+
+ // ================================================================
+ // Phase 5: Existing Operations Backfill
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")]
+ public IEnumerator PlayerFiles_GetPlayerFile_ReturnsCorrectFile()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string path = CreateTempFile("Get by ID content");
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Upload for get test failed");
+
+ // When
+ LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse =>
+ {
+ fetchedFile = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+
+ // Then
+ Assert.IsTrue(fetchedFile.success, "GetPlayerFile failed");
+ Assert.AreEqual(uploadedFile.id, fetchedFile.id, "File ID should match");
+ Assert.AreEqual(uploadedFile.name, fetchedFile.name, "File name should match");
+ Assert.Greater(fetchedFile.size, 0, "File size should be > 0");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_GetAllPlayerFiles_ReturnsFiles()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given — upload two files
+ string pathA = CreateTempFile("First list file");
+ string pathB = CreateTempFile("Second list file");
+
+ LootLockerPlayerFile uploadA = new LootLockerPlayerFile();
+ bool uploadADone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, response =>
+ {
+ uploadA = response;
+ uploadADone = true;
+ });
+ yield return new WaitUntil(() => uploadADone);
+ Assert.IsTrue(uploadA.success, "First upload for list test failed");
+
+ LootLockerPlayerFile uploadB = new LootLockerPlayerFile();
+ bool uploadBDone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathB, "test", true, response =>
+ {
+ uploadB = response;
+ uploadBDone = true;
+ });
+ yield return new WaitUntil(() => uploadBDone);
+ Assert.IsTrue(uploadB.success, "Second upload for list test failed");
+
+ // When
+ LootLockerPlayerFilesResponse listResponse = new LootLockerPlayerFilesResponse();
+ bool listDone = false;
+ LootLockerSDKManager.GetAllPlayerFiles(response =>
+ {
+ listResponse = response;
+ listDone = true;
+ });
+ yield return new WaitUntil(() => listDone);
+
+ // Then
+ Assert.IsTrue(listResponse.success, "GetAllPlayerFiles failed");
+ Assert.GreaterOrEqual(listResponse.items.Length, 2, "Should have at least 2 files");
+ foreach (var item in listResponse.items)
+ {
+ Assert.Greater(item.id, 0, "Each file should have a positive ID");
+ Assert.IsFalse(string.IsNullOrEmpty(item.name), "Each file should have a name");
+ Assert.IsFalse(string.IsNullOrEmpty(item.url), "Each file should have a URL");
+ }
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_DeletePlayerFile_RemovesFile()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string path = CreateTempFile("To be deleted");
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(path, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Upload for delete test failed");
+
+ // When
+ LootLockerResponse deleteResponse = new LootLockerResponse();
+ bool deleteDone = false;
+ LootLockerSDKManager.DeletePlayerFile(uploadedFile.id, response =>
+ {
+ deleteResponse = response;
+ deleteDone = true;
+ });
+ yield return new WaitUntil(() => deleteDone);
+
+ // Then
+ Assert.IsTrue(deleteResponse.success, "DeletePlayerFile failed");
+
+ // Verify deletion
+ bool preLogErrorsAsWarningsSetting = LootLockerConfig.current.logErrorsAsWarnings;
+ LootLockerConfig.current.logErrorsAsWarnings = true; // Suppress error logs for expected failure
+ LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFile(uploadedFile.id, fileResponse =>
+ {
+ fetchedFile = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+ Assert.IsFalse(fetchedFile.success, "File should no longer exist after deletion");
+ LootLockerConfig.current.logErrorsAsWarnings = preLogErrorsAsWarningsSetting;
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_UpdatePlayerFile_ChangesContent()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string pathA = CreateTempFile("Original content for update");
+ string pathB = CreateTempFile("Updated content for update");
+
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFile(pathA, "test", true, fileResponse =>
+ {
+ uploadedFile = fileResponse;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Initial upload failed");
+ int originalSize = uploadedFile.size;
+ string originalRevisionId = uploadedFile.revision_id;
+
+ // When
+ LootLockerPlayerFile updatedFile = new LootLockerPlayerFile();
+ bool updateDone = false;
+ LootLockerSDKManager.UpdatePlayerFile(uploadedFile.id, pathB, fileResponse =>
+ {
+ updatedFile = fileResponse;
+ updateDone = true;
+ });
+ yield return new WaitUntil(() => updateDone);
+
+ // Then
+ Assert.IsTrue(updatedFile.success, "UpdatePlayerFile failed");
+ Assert.AreNotEqual(originalRevisionId, updatedFile.revision_id, "Revision ID should change after update");
+ Assert.AreNotEqual(originalSize, updatedFile.size, "File size should change after update with different content");
+ }
+
+ // ================================================================
+ // Phase 6: Response Field Verification
+ // ================================================================
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_ListResponse_IncludesKeyField()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "list-key-" + TestCounter;
+ string path = CreateTempFile("List response key check");
+
+ LootLockerPlayerFile uploadedFile = new LootLockerPlayerFile();
+ bool uploadDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(path, "test", true, fileKey, response =>
+ {
+ uploadedFile = response;
+ uploadDone = true;
+ });
+ yield return new WaitUntil(() => uploadDone);
+ Assert.IsTrue(uploadedFile.success, "Upload for list response key test failed");
+
+ // When
+ LootLockerPlayerFilesResponse listResponse = new LootLockerPlayerFilesResponse();
+ bool listDone = false;
+ LootLockerSDKManager.GetAllPlayerFiles(response =>
+ {
+ listResponse = response;
+ listDone = true;
+ });
+ yield return new WaitUntil(() => listDone);
+
+ // Then
+ Assert.IsTrue(listResponse.success, "GetAllPlayerFiles failed");
+ bool foundKey = false;
+ foreach (var item in listResponse.items)
+ {
+ if (item.key == fileKey)
+ {
+ foundKey = true;
+ break;
+ }
+ }
+ Assert.IsTrue(foundKey, "List response should contain an item with the uploaded key");
+ }
+
+ [UnityTest, Category("LootLocker"), Category("LootLockerCI")]
+ public IEnumerator PlayerFiles_RevisionsResponse_FileMetadataHasKey()
+ {
+ Assert.IsFalse(SetupFailed, "Failed to setup game");
+ // Given
+ string fileKey = "meta-key-" + TestCounter;
+ string pathA = CreateTempFile("Metadata key revision A");
+ string pathB = CreateTempFile("Metadata key revision B");
+
+ LootLockerPlayerFile firstUpload = new LootLockerPlayerFile();
+ bool firstDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathA, "test", true, fileKey, response =>
+ {
+ firstUpload = response;
+ firstDone = true;
+ });
+ yield return new WaitUntil(() => firstDone);
+ Assert.IsTrue(firstUpload.success, "First upload for metadata key test failed");
+
+ LootLockerPlayerFile secondUpload = new LootLockerPlayerFile();
+ bool secondDone = false;
+ LootLockerSDKManager.UploadPlayerFileByKey(pathB, "test", true, fileKey, response =>
+ {
+ secondUpload = response;
+ secondDone = true;
+ });
+ yield return new WaitUntil(() => secondDone);
+ Assert.IsTrue(secondUpload.success, "Second upload for metadata key test failed");
+
+ // Get file ID for the ID-based revisions call
+ LootLockerPlayerFile fetchedFile = new LootLockerPlayerFile();
+ bool fetchDone = false;
+ LootLockerSDKManager.GetPlayerFileByKey(fileKey, fileResponse =>
+ {
+ fetchedFile = fileResponse;
+ fetchDone = true;
+ });
+ yield return new WaitUntil(() => fetchDone);
+ Assert.IsTrue(fetchedFile.success, "GetPlayerFileByKey failed");
+
+ // When — get revisions by ID
+ LootLockerPlayerFileRevisionsResponse revisionsResponse = new LootLockerPlayerFileRevisionsResponse();
+ bool revisionsDone = false;
+ LootLockerSDKManager.GetPlayerFileRevisions(fetchedFile.id, response =>
+ {
+ revisionsResponse = response;
+ revisionsDone = true;
+ });
+ yield return new WaitUntil(() => revisionsDone);
+
+ // Then
+ Assert.IsTrue(revisionsResponse.success, "GetPlayerFileRevisions failed");
+ Assert.AreEqual(fileKey, revisionsResponse.file.key, "File metadata should contain the key");
+ Assert.AreEqual(fetchedFile.id, revisionsResponse.file.id, "File metadata ID should match");
+ Assert.IsFalse(string.IsNullOrEmpty(revisionsResponse.file.name), "File metadata should have a name");
+ }
}
}
diff --git a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs
index c501aee98..51d021dd6 100644
--- a/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs
+++ b/Tests/LootLockerTests/PlayMode/WhiteLabelSignUpFieldsTest.cs
@@ -66,6 +66,45 @@ public IEnumerator Setup()
yield break;
}
+ // Configure custom sign-up fields on the game
+ bool fieldsConfigured = false;
+ LootLockerTestConfigurationTitleConfig.SetCustomSignUpFields(
+ new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition[]
+ {
+ new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition
+ {
+ question_text = "When were you born?",
+ metadata_key = "birth_date",
+ field_type = "date",
+ required = true,
+ sensitive = false,
+ sort_order = 1
+ },
+ new LootLockerTestConfigurationTitleConfig.WhiteLabelCustomSignUpFieldDefinition
+ {
+ question_text = "Do you agree to the terms?",
+ metadata_key = "tos_agree",
+ field_type = "checkbox",
+ required = true,
+ sensitive = false,
+ sort_order = 2
+ }
+ },
+ (success, errorMessage) =>
+ {
+ if (!success)
+ {
+ Debug.LogError($"Failed to configure custom sign-up fields: {errorMessage}");
+ SetupFailed = true;
+ }
+ fieldsConfigured = true;
+ });
+ yield return new WaitUntil(() => fieldsConfigured);
+ if (SetupFailed)
+ {
+ yield break;
+ }
+
Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Failed to initialize LootLockerSDK");
Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####");
@@ -114,8 +153,23 @@ public IEnumerator GetSignUpFields_WithWhiteLabelEnabled_ReturnsFieldsResponse()
// Then
Assert.IsTrue(actualResponse.success, "GetSignUpFields returned unsuccessful: " + actualResponse.errorData?.message);
- // Fields array should be present (empty if no custom fields configured on this game)
Assert.IsNotNull(actualResponse.fields, "Fields array should not be null");
+ Assert.AreEqual(2, actualResponse.fields.Length, "Expected 2 custom sign-up fields to be configured");
+
+ // Verify the configured fields round-trip correctly (order-agnostic)
+ var fieldsByKey = new System.Collections.Generic.Dictionary();
+ foreach (var field in actualResponse.fields)
+ {
+ fieldsByKey[field.metadata_key] = field;
+ }
+
+ Assert.IsTrue(fieldsByKey.ContainsKey("birth_date"), "Expected birth_date field in response");
+ Assert.AreEqual("date", fieldsByKey["birth_date"].field_type, "birth_date field_type mismatch");
+ Assert.AreEqual("When were you born?", fieldsByKey["birth_date"].question_text, "birth_date question_text mismatch");
+
+ Assert.IsTrue(fieldsByKey.ContainsKey("tos_agree"), "Expected tos_agree field in response");
+ Assert.AreEqual("checkbox", fieldsByKey["tos_agree"].field_type, "tos_agree field_type mismatch");
+ Assert.AreEqual("Do you agree to the terms?", fieldsByKey["tos_agree"].question_text, "tos_agree question_text mismatch");
}
// Verifies serialization round-trip for the @params keyword-escaped property