diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 08b23bd18..c1f61ee0b 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -45,7 +45,7 @@ public class LootLockerEndPoints public static EndPointClass whiteLabelLogin = new EndPointClass("white-label-login/login", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelVerifySession = new EndPointClass("white-label-login/verify-session", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelRequestPasswordReset = new EndPointClass("white-label-login/request-reset-password", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); - public static EndPointClass whiteLabelRequestAccountVerification = new EndPointClass("white-label-login/request-verification", LootLockerHTTPMethod.POST); + public static EndPointClass whiteLabelRequestAccountVerification = new EndPointClass("white-label-login/request-verification", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); public static EndPointClass whiteLabelLoginSessionRequest = new EndPointClass("v2/session/white-label", LootLockerHTTPMethod.POST, LootLockerEnums.LootLockerCallerRole.Base); // Player @@ -53,6 +53,7 @@ public class LootLockerEndPoints public static EndPointClass getInfoFromSession = new EndPointClass("player/hazy-hammock/v1/info", LootLockerHTTPMethod.GET); public static EndPointClass listPlayerInfo = new EndPointClass("player/hazy-hammock/v1/info", LootLockerHTTPMethod.POST); public static EndPointClass getInventory = new EndPointClass("v1/player/inventory/list", LootLockerHTTPMethod.GET); + public static EndPointClass listSimplifiedInventory = new EndPointClass("player/inventories/v1", LootLockerHTTPMethod.POST); public static EndPointClass getCurrencyBalance = new EndPointClass("v1/player/balance", LootLockerHTTPMethod.GET); public static EndPointClass playerAssetNotifications = new EndPointClass("v1/player/notification/assets", LootLockerHTTPMethod.GET); public static EndPointClass playerAssetDeactivationNotification = new EndPointClass("v1/player/notification/deactivations", LootLockerHTTPMethod.GET); diff --git a/Runtime/Client/LootLockerHTTPClient.cs b/Runtime/Client/LootLockerHTTPClient.cs index 5157db3f8..d7ebd77f2 100644 --- a/Runtime/Client/LootLockerHTTPClient.cs +++ b/Runtime/Client/LootLockerHTTPClient.cs @@ -461,7 +461,7 @@ private HTTPExecutionQueueProcessingResult ProcessOngoingRequest(LootLockerHTTPE if (ShouldRetryRequest(executionItem.WebRequest.responseCode, executionItem.RequestData.TimesRetried) && !(executionItem.WebRequest.responseCode == 401 && !IsAuthorizedRequest(executionItem))) { - if (ShouldRefreshSession(executionItem.WebRequest.responseCode, playerData == null ? LL_AuthPlatforms.None : playerData.CurrentPlatform.Platform) && (CanRefreshUsingRefreshToken(executionItem.RequestData) || CanStartNewSessionUsingCachedAuthData(executionItem.RequestData.ForPlayerWithUlid))) + if (ShouldRefreshSession(executionItem, playerData == null ? LL_AuthPlatforms.None : playerData.CurrentPlatform.Platform) && (CanRefreshUsingRefreshToken(executionItem.RequestData) || CanStartNewSessionUsingCachedAuthData(executionItem.RequestData.ForPlayerWithUlid))) { return HTTPExecutionQueueProcessingResult.NeedsSessionRefresh; } @@ -697,7 +697,7 @@ private void HandleSessionRefreshResult(LootLockerResponse newSessionResponse, s return; } var playerData = LootLockerStateData.GetStateForPlayerOrDefaultStateOrEmpty(executionItem.RequestData.ForPlayerWithUlid); - string tokenBeforeRefresh = executionItem.RequestData.ExtraHeaders["x-session-token"]; + string tokenBeforeRefresh = executionItem.RequestData.ExtraHeaders.TryGetValue("x-session-token", out var existingToken) ? existingToken : ""; string tokenAfterRefresh = playerData?.SessionToken; if (string.IsNullOrEmpty(tokenAfterRefresh) || tokenBeforeRefresh.Equals(playerData.SessionToken)) { @@ -731,14 +731,24 @@ private static bool ShouldRetryRequest(long statusCode, int timesRetried) return (statusCode == 401 || statusCode == 403 || statusCode == 502 || statusCode == 500 || statusCode == 503) && timesRetried < configuration.MaxRetries; } - private static bool ShouldRefreshSession(long statusCode, LL_AuthPlatforms platform) + private static bool ShouldRefreshSession(LootLockerHTTPExecutionQueueItem request, LL_AuthPlatforms platform) { - return (statusCode == 401 || statusCode == 403) && LootLockerConfig.current.allowTokenRefresh && !new List{ LL_AuthPlatforms.Steam, LL_AuthPlatforms.NintendoSwitch, LL_AuthPlatforms.None }.Contains(platform); + return IsAuthorizedGameRequest(request) && (request.WebRequest?.responseCode == 401 || request.WebRequest?.responseCode == 403) && LootLockerConfig.current.allowTokenRefresh && !new List{ LL_AuthPlatforms.Steam, LL_AuthPlatforms.NintendoSwitch, LL_AuthPlatforms.None }.Contains(platform); } private static bool IsAuthorizedRequest(LootLockerHTTPExecutionQueueItem request) { - return !string.IsNullOrEmpty(request.WebRequest?.GetRequestHeader("x-session-token")) || !string.IsNullOrEmpty(request.WebRequest?.GetRequestHeader("x-auth-token")); + return IsAuthorizedGameRequest(request) || IsAuthorizedAdminRequest(request); + } + + private static bool IsAuthorizedGameRequest(LootLockerHTTPExecutionQueueItem request) + { + return !string.IsNullOrEmpty(request.WebRequest?.GetRequestHeader("x-session-token")); + } + + private static bool IsAuthorizedAdminRequest(LootLockerHTTPExecutionQueueItem request) + { + return !string.IsNullOrEmpty(request.WebRequest?.GetRequestHeader("x-auth-token")); } private static bool CanRefreshUsingRefreshToken(LootLockerHTTPRequestData cachedRequest) diff --git a/Runtime/Client/LootLockerJson.cs b/Runtime/Client/LootLockerJson.cs index 24ffb776e..69458c38d 100644 --- a/Runtime/Client/LootLockerJson.cs +++ b/Runtime/Client/LootLockerJson.cs @@ -23,6 +23,7 @@ public static class LootLockerJsonSettings }; #else public static readonly JsonOptions Default = new JsonOptions((JsonSerializationOptions.Default | JsonSerializationOptions.EnumAsText) & ~JsonSerializationOptions.SkipGetOnly); + public static readonly JsonOptions Indented = new JsonOptions((JsonSerializationOptions.Default | JsonSerializationOptions.EnumAsText) & ~JsonSerializationOptions.SkipGetOnly); #endif } @@ -85,6 +86,25 @@ public static bool TryDeserializeObject(string json, JsonSerializerSettings o return false; } } + + public static string PrettifyJsonString(string json) + { + try + { + var parsedJson = DeserializeObject(json); + var tempSettings = new JsonSerializerSettings + { + ContractResolver = LootLockerJsonSettings.Default.ContractResolver, + Converters = LootLockerJsonSettings.Default.Converters, + Formatting = Formatting.Indented + }; + return SerializeObject(parsedJson, tempSettings); + } + catch (Exception) + { + return json; + } + } #else //LOOTLOCKER_USE_NEWTONSOFTJSON public static string SerializeObject(object obj) { @@ -142,6 +162,21 @@ public static bool TryDeserializeObject(string json, JsonOptions options, out return false; } } + + public static string PrettifyJsonString(string json) + { + try + { + var parsedJson = DeserializeObject(json); + var indentedOptions = LootLockerJsonSettings.Default.Clone(); + indentedOptions.FormattingTab = " "; + return Json.SerializeFormatted(parsedJson, indentedOptions); + } + catch (Exception) + { + return json; + } + } #endif //LOOTLOCKER_USE_NEWTONSOFTJSON } } diff --git a/Runtime/Editor/LogViewer/LootLockerLogViewerUI.cs b/Runtime/Editor/LogViewer/LootLockerLogViewerUI.cs index 1ce1cf4aa..506de51d0 100644 --- a/Runtime/Editor/LogViewer/LootLockerLogViewerUI.cs +++ b/Runtime/Editor/LogViewer/LootLockerLogViewerUI.cs @@ -216,7 +216,11 @@ private void ExportLogs() if (!string.IsNullOrEmpty(http.RequestBody)) { sb.AppendLine("Request Body:"); - sb.AppendLine(http.RequestBody); + sb.AppendLine( + LootLockerConfig.current.obfuscateLogs ? + LootLockerObfuscator.ObfuscateJsonStringForLogging(http.RequestBody) : + http.RequestBody + ); } sb.AppendLine("Response Headers:"); foreach (var h in http.ResponseHeaders ?? new Dictionary()) @@ -224,7 +228,11 @@ private void ExportLogs() if (!string.IsNullOrEmpty(http.Response?.text)) { sb.AppendLine("Response Body:"); - sb.AppendLine(http.Response.text); + sb.AppendLine( + LootLockerConfig.current.obfuscateLogs ? + LootLockerObfuscator.ObfuscateJsonStringForLogging(http.Response.text) : + http.Response.text + ); } writer.Write(sb.ToString()); } @@ -401,25 +409,59 @@ private void AddHttpLogEntryToUI(LootLockerLogger.LootLockerHttpLogEntry entry) var reqHeaders = new TextField { value = $"Request Headers: {FormatHeaders(entry.RequestHeaders)}", isReadOnly = true }; reqHeaders.AddToClassList("log-message-field"); details.Add(reqHeaders); + if (!string.IsNullOrEmpty(entry.RequestBody)) { - var reqBody = new TextField { value = $"Request Body: {entry.RequestBody}", isReadOnly = true }; - reqBody.AddToClassList("log-message-field"); - details.Add(reqBody); + var requestBodyFoldout = CreateJsonFoldout("Request Body", entry.RequestBody); + details.Add(requestBodyFoldout); } + var respHeaders = new TextField { value = $"Response Headers: {FormatHeaders(entry.ResponseHeaders)}", isReadOnly = true }; respHeaders.AddToClassList("log-message-field"); details.Add(respHeaders); + if (!string.IsNullOrEmpty(entry.Response?.text)) { - var respBody = new TextField { value = $"Response Body: {entry.Response.text}", isReadOnly = true }; - respBody.AddToClassList("log-message-field"); - details.Add(respBody); + var responseBodyFoldout = CreateJsonFoldout("Response Body", entry.Response.text); + details.Add(responseBodyFoldout); } + foldout.Add(details); logContainer.Add(foldout); } + private Foldout CreateJsonFoldout(string title, string jsonContent) + { + // Initially show minified JSON (obfuscated if configured) + var obfuscatedJson = LootLockerConfig.current.obfuscateLogs + ? LootLockerObfuscator.ObfuscateJsonStringForLogging(jsonContent) + : jsonContent; + var prettifiedJson = LootLockerJson.PrettifyJsonString(obfuscatedJson); + var collapsedTitle = $"{title}: {obfuscatedJson}"; + + var foldout = new Foldout { text = collapsedTitle, value = false }; + + // Create a container for the JSON content + var jsonContainer = new VisualElement(); + + var jsonField = new TextField { value = prettifiedJson, isReadOnly = true, multiline = true }; + jsonField.AddToClassList("log-message-field"); + #if UNITY_6000_0_OR_NEWER + jsonField.style.whiteSpace = WhiteSpace.PreWrap; + #else + jsonField.style.whiteSpace = WhiteSpace.Normal; + #endif + jsonContainer.Add(jsonField); + + foldout.RegisterValueChangedCallback(evt => + { + foldout.text = evt.newValue ? title : collapsedTitle; + }); + + foldout.Add(jsonContainer); + return foldout; + } + private string FormatHeaders(Dictionary headers) { if (headers == null) return ""; diff --git a/Runtime/Editor/ProjectSettings.cs b/Runtime/Editor/ProjectSettings.cs index d34ef016a..161f52bcb 100644 --- a/Runtime/Editor/ProjectSettings.cs +++ b/Runtime/Editor/ProjectSettings.cs @@ -159,6 +159,15 @@ private void DrawGameSettings() } EditorGUILayout.Space(); + EditorGUI.BeginChangeCheck(); + EditorGUILayout.PropertyField(m_CustomSettings.FindProperty("prettifyJson"), new GUIContent("Log JSON Formatted")); + + if (EditorGUI.EndChangeCheck()) + { + gameSettings.prettifyJson = m_CustomSettings.FindProperty("prettifyJson").boolValue; + } + EditorGUILayout.Space(); + EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(m_CustomSettings.FindProperty("allowTokenRefresh")); diff --git a/Runtime/Game/LootLockerLogger.cs b/Runtime/Game/LootLockerLogger.cs index cd7b49071..0cbdf6def 100644 --- a/Runtime/Game/LootLockerLogger.cs +++ b/Runtime/Game/LootLockerLogger.cs @@ -245,7 +245,7 @@ public static void LogHttpRequestResponse(LootLockerHttpLogEntry entry) if (!string.IsNullOrEmpty(entry.RequestBody)) { sb.AppendLine("Request Body:"); - sb.AppendLine(entry.RequestBody); + sb.AppendLine(ObfuscateAndPrettifyJsonIfConfigured(entry.RequestBody)); } sb.AppendLine("Response Headers:"); foreach (var h in entry.ResponseHeaders ?? new Dictionary()) @@ -253,7 +253,7 @@ public static void LogHttpRequestResponse(LootLockerHttpLogEntry entry) if (!string.IsNullOrEmpty(entry.Response?.text)) { sb.AppendLine("Response Body:"); - sb.AppendLine(entry.Response.text); + sb.AppendLine(ObfuscateAndPrettifyJsonIfConfigured(entry.Response.text)); } LogLevel level = entry.Response?.success ?? false ? LogLevel.Verbose : LogLevel.Error; @@ -302,6 +302,19 @@ private void ReplayHttpLogRecord(ILootLockerHttpLogListener listener) } } } + + private static string ObfuscateAndPrettifyJsonIfConfigured(string json) + { + if (LootLockerConfig.current.obfuscateLogs) + { + json = LootLockerObfuscator.ObfuscateJsonStringForLogging(json); + } + if (LootLockerConfig.current.prettifyJson) + { + json = LootLockerJson.PrettifyJsonString(json); + } + return json; + } } public interface LootLockerLogListener diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 3ac2a6499..c937a108c 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -2831,6 +2831,73 @@ public static void GetInventory(int count, Action o } + /// + /// List player inventory with default parameters (no filters, first page, default page size). + /// + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListPlayerInventoryWithDefaultParameters(Action onComplete, string forPlayerWithUlid = null) + { + ListPlayerInventory(new LootLockerListSimplifiedInventoryRequest(), 100, 1, onComplete, forPlayerWithUlid); + } + + /// + /// List player inventory with minimal response data. Due to looking up less data, this endpoint is significantly faster than GetInventory. + /// + /// Request object containing any filters to apply to the inventory listing. + /// Optional : Number of items per page. + /// Optional : Page number to retrieve. + public static void ListPlayerInventory(LootLockerListSimplifiedInventoryRequest request, int perPage, int page, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + var endPoint = LootLockerEndPoints.listSimplifiedInventory; + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + queryParams.Add("per_page", perPage > 0 ? perPage : 100); + queryParams.Add("page", page > 0 ? page : 1); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, LootLockerJson.SerializeObject(request), onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + /// + /// List character inventory with default parameters (no filters, first page, default page size). + /// + /// onComplete Action for handling the response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListCharacterInventoryWithDefaultParameters(Action onComplete, string forPlayerWithUlid = null) + { + ListCharacterInventory(new LootLockerListSimplifiedInventoryRequest(), 0, 100, 1, onComplete, forPlayerWithUlid); + } + + /// + /// List character inventory with minimal response data. Due to looking up less data, this endpoint is significantly faster than GetInventory. + /// + /// Request object containing any filters to apply to the inventory listing. + /// Optional : Filter inventory by character ID. + /// Optional : Number of items per page. + /// Optional : Page number to retrieve. + public static void ListCharacterInventory(LootLockerListSimplifiedInventoryRequest request, int characterId, int perPage, int page, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + var endPoint = LootLockerEndPoints.listSimplifiedInventory; + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (characterId > 0) + queryParams.Add("character_id", characterId); + queryParams.Add("per_page", perPage > 0 ? perPage : 100); + queryParams.Add("page", page > 0 ? page : 1); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, LootLockerJson.SerializeObject(request), onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + /// /// Get the amount of credits/currency that the player has. /// diff --git a/Runtime/Game/Requests/CatalogRequests.cs b/Runtime/Game/Requests/CatalogRequests.cs index af1f5870b..09cd6cdbd 100644 --- a/Runtime/Game/Requests/CatalogRequests.cs +++ b/Runtime/Game/Requests/CatalogRequests.cs @@ -126,6 +126,36 @@ public class LootLockerCatalogSteamStoreListing public LootLockerCatalogSteamStoreListingPrice[] prices { get; set; } } + /// + /// + public class LootLockerCatalogStripeStoreListing + { + /// + /// The currency to use for the purchase + /// + public string currency { get; set; } + /// + /// The amount to charge in the smallest unit of the currency (e.g. cents for USD) + /// + public int amount { get; set; } + } + + public class LootLockerCatalogEpicGamesStoreListing + { + /// + /// The Epic Games audience item id associated with this listing + /// + public string audience_item_id { get; set; } + } + + public class LootLockerCatalogPlaystationStoreListing + { + /// + /// The Playstation entitlement label associated with this listing + /// + public string entitlement_label { get; set; } + } + /// /// public class LootLockerCatalogEntryListings @@ -142,6 +172,18 @@ public class LootLockerCatalogEntryListings /// The listing information (if configured) for Steam Store /// public LootLockerCatalogSteamStoreListing steam_store { get; set; } + /// + /// The listing information (if configured) for Stripe Store + /// + public LootLockerCatalogStripeStoreListing stripe_store { get; set; } + /// + /// The listing information (if configured) for Epic Games Store + /// + public LootLockerCatalogEpicGamesStoreListing epic_games_store { get; set; } + /// + /// The listing information (if configured) for Playstation Store + /// + public LootLockerCatalogPlaystationStoreListing playstation_store { get; set; } } /// @@ -167,7 +209,65 @@ public override bool Equals(object obj) { return obj.GetHashCode() == GetHashCode(); } + } + + /// + /// Class to help getting asset item details including variation and rental option IDs + /// + public class LootLockerAssetItemDetailsKey + { + /// + /// The id of a catalog listing + /// + public string catalog_listing_id { get; set; } + /// + /// The id of the item + /// + public string item_id { get; set; } + /// + /// The id of the specific variation of this asset that this refers to + /// + public string variation_id { get; set; } + /// + /// The id of the specific rental option of this asset that this refers to + /// + public string rental_option_id { get; set; } + + public override int GetHashCode() + { + return catalog_listing_id.GetHashCode() + item_id.GetHashCode() + + (variation_id?.GetHashCode() ?? 0) + (rental_option_id?.GetHashCode() ?? 0); + } + + public override bool Equals(object obj) + { + if (obj is LootLockerAssetItemDetailsKey other) + { + return catalog_listing_id == other.catalog_listing_id && + item_id == other.item_id && + variation_id == other.variation_id && + rental_option_id == other.rental_option_id; + } + return false; + } + + public LootLockerAssetItemDetailsKey() + { + } + public LootLockerAssetItemDetailsKey(string catalogListingId, string itemId, string variationId, string rentalOptionId) + { + catalog_listing_id = catalogListingId; + item_id = itemId; + variation_id = variationId; + rental_option_id = rentalOptionId; + } + + public LootLockerAssetItemDetailsKey(LootLockerItemDetailsKey key) + { + catalog_listing_id = key.catalog_listing_id; + item_id = key.item_id; + } } /// @@ -257,6 +357,19 @@ public LootLockerItemDetailsKey GetItemDetailsKey() { return new LootLockerItemDetailsKey { catalog_listing_id = catalog_listing_id, item_id = id }; } + /// + /// Function to help identify asset details including variation and rental options + /// + /// The identifier for looking up asset details + public LootLockerAssetItemDetailsKey GetAssetItemDetailsKey() + { + return new LootLockerAssetItemDetailsKey { + catalog_listing_id = catalog_listing_id, + item_id = id, + variation_id = variation_id, + rental_option_id = rental_option_id + }; + } } /// @@ -480,9 +593,16 @@ public class LootLockerListCatalogPricesResponse : LootLockerResponse /// /// Lookup map for details about entities of entity type assets + /// If the asset in question has variations or rental options, those will be in the optional_asset_detail_variants structure instead /// public Dictionary asset_details { get; set; } + /// + /// This is a list of potentially matching asset details for this catalog entry, in case there are multiple variations / rental options + /// Asset Variations and Rental Options are deprecated features, this is added for backward compatibility only + /// + public Dictionary optional_asset_detail_variants { get; set; } + /// /// Lookup map for details about entities of entity type progression_points /// @@ -529,6 +649,19 @@ public void AppendCatalogItems(LootLockerListCatalogPricesResponse catalogPrices asset_details.Add(assetDetail.Key, assetDetail.Value); } + // Also append asset detail variants if they exist + if (catalogPrices.optional_asset_detail_variants != null) + { + if (optional_asset_detail_variants == null) + { + optional_asset_detail_variants = new Dictionary(); + } + foreach (var assetDetailVariant in catalogPrices.optional_asset_detail_variants) + { + optional_asset_detail_variants.Add(assetDetailVariant.Key, assetDetailVariant.Value); + } + } + foreach (var progressionPointDetail in catalogPrices.progression_points_details) { progression_points_details.Add(progressionPointDetail.Key, progressionPointDetail.Value); @@ -586,9 +719,22 @@ public LootLockerListCatalogPricesResponse(LootLockerResponse serverResponse) if (parsedResponse.assets_details != null && parsedResponse.assets_details.Length > 0) { asset_details = new Dictionary(); + optional_asset_detail_variants = new Dictionary(); + foreach (var detail in parsedResponse.assets_details) { - asset_details[detail.GetItemDetailsKey()] = detail; + if (detail == null) + continue; + if (!string.IsNullOrEmpty(detail.variation_id) || !string.IsNullOrEmpty(detail.rental_option_id)) + { + // Populate for backward compatibility + optional_asset_detail_variants[detail.GetAssetItemDetailsKey()] = detail; + continue; + } + else + { + asset_details[detail.GetItemDetailsKey()] = detail; + } } } @@ -641,6 +787,12 @@ public class LootLockerInlinedCatalogEntry : LootLockerCatalogEntry /// public LootLockerAssetDetails asset_details { get; set; } + /// + /// This is a list of potentially matching asset details for this catalog entry, in case there are multiple variations / rental options + /// Asset Variations and Rental Options are deprecated features, this is added for backward compatibility only + /// + public List optional_asset_detail_variants { get; set; } + /// /// Progression point details inlined for this catalog entry, will be null if the entity_kind is not progression_points /// @@ -679,6 +831,17 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis { asset_details = catalogListing.asset_details[entry.GetItemDetailsKey()]; } + else + { + optional_asset_detail_variants = new List(); + foreach (var optionalAssetDetailVariant in catalogListing.optional_asset_detail_variants) + { + if (entry.GetItemDetailsKey() == optionalAssetDetailVariant.Value.GetItemDetailsKey()) + { + optional_asset_detail_variants.Add(optionalAssetDetailVariant.Value); + } + } + } break; case LootLockerCatalogEntryEntityKind.currency: if (catalogListing.currency_details.ContainsKey(entry.GetItemDetailsKey())) @@ -712,6 +875,7 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis inlinedGroupDetails.id = catalogLevelGroup.id; inlinedGroupDetails.associations = catalogLevelGroup.associations; + Dictionary processedOptionalAssetDetails = new Dictionary(); foreach (var association in catalogLevelGroup.associations) { switch (association.kind) @@ -721,6 +885,19 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis { inlinedGroupDetails.assetDetails.Add(catalogListing.asset_details[association.GetItemDetailsKey()]); } + else + { + foreach (var optionalAssetDetailVariant in catalogListing.optional_asset_detail_variants) + { + if(processedOptionalAssetDetails.ContainsKey(optionalAssetDetailVariant.Key)) + continue; + if (association.GetItemDetailsKey() == optionalAssetDetailVariant.Value.GetItemDetailsKey()) + { + inlinedGroupDetails.assetDetails.Add(optionalAssetDetailVariant.Value); + processedOptionalAssetDetails[optionalAssetDetailVariant.Key] = true; + } + } + } break; case LootLockerCatalogEntryEntityKind.progression_points: if (catalogListing.progression_points_details.ContainsKey(association.GetItemDetailsKey())) @@ -789,9 +966,16 @@ public class LootLockerListCatalogPricesV2Response : LootLockerResponse /// /// Lookup map for details about entities of entity type assets + /// If the asset in question has variations or rental options, those will be in the optional_asset_detail_variants structure instead /// public Dictionary asset_details { get; set; } + /// + /// This is a list of potentially matching asset details for this catalog entry, in case there are multiple variations / rental options + /// Asset Variations and Rental Options are deprecated features, this is added for backward compatibility only + /// + public Dictionary optional_asset_detail_variants { get; set; } + /// /// Lookup map for details about entities of entity type progression_points /// @@ -844,6 +1028,19 @@ public void AppendCatalogItems(LootLockerListCatalogPricesV2Response catalogPric asset_details.Add(assetDetail.Key, assetDetail.Value); } + // Also append asset detail variants if they exist + if (catalogPrices.optional_asset_detail_variants != null) + { + if (optional_asset_detail_variants == null) + { + optional_asset_detail_variants = new Dictionary(); + } + foreach (var assetDetailVariant in catalogPrices.optional_asset_detail_variants) + { + optional_asset_detail_variants.Add(assetDetailVariant.Key, assetDetailVariant.Value); + } + } + foreach (var progressionPointDetail in catalogPrices.progression_points_details) { progression_points_details.Add(progressionPointDetail.Key, progressionPointDetail.Value); @@ -901,9 +1098,20 @@ public LootLockerListCatalogPricesV2Response(LootLockerResponse serverResponse) if (parsedResponse.assets_details != null && parsedResponse.assets_details.Length > 0) { asset_details = new Dictionary(); + optional_asset_detail_variants = new Dictionary(); foreach (var detail in parsedResponse.assets_details) { - asset_details[detail.GetItemDetailsKey()] = detail; + if (detail == null) + continue; + if (!string.IsNullOrEmpty(detail.variation_id) || !string.IsNullOrEmpty(detail.rental_option_id)) + { + // Populate for backward compatibility + optional_asset_detail_variants[detail.GetAssetItemDetailsKey()] = detail; + } + else + { + asset_details[detail.GetItemDetailsKey()] = detail; + } } } @@ -956,6 +1164,12 @@ public class LootLockerInlinedCatalogEntry : LootLockerCatalogEntry /// public LootLockerAssetDetails asset_details { get; set; } + /// + /// This is a list of potentially matching asset details for this catalog entry, in case there are multiple variations / rental options + /// Asset Variations and Rental Options are deprecated features, this is added for backward compatibility only + /// + public List optional_asset_detail_variants { get; set; } + /// /// Progression point details inlined for this catalog entry, will be null if the entity_kind is not progression_points /// @@ -994,6 +1208,17 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis { asset_details = catalogListing.asset_details[entry.GetItemDetailsKey()]; } + else + { + optional_asset_detail_variants = new List(); + foreach (var optionalAssetDetailVariant in catalogListing.optional_asset_detail_variants) + { + if (entry.GetItemDetailsKey() == optionalAssetDetailVariant.Value.GetItemDetailsKey()) + { + optional_asset_detail_variants.Add(optionalAssetDetailVariant.Value); + } + } + } break; case LootLockerCatalogEntryEntityKind.currency: if (catalogListing.currency_details.ContainsKey(entry.GetItemDetailsKey())) @@ -1027,6 +1252,7 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis inlinedGroupDetails.id = catalogLevelGroup.id; inlinedGroupDetails.associations = catalogLevelGroup.associations; + Dictionary processedOptionalAssetDetails = new Dictionary(); foreach (var association in catalogLevelGroup.associations) { switch (association.kind) @@ -1036,6 +1262,19 @@ public LootLockerInlinedCatalogEntry(LootLockerCatalogEntry entry, LootLockerLis { inlinedGroupDetails.assetDetails.Add(catalogListing.asset_details[association.GetItemDetailsKey()]); } + else + { + foreach (var optionalAssetDetailVariant in catalogListing.optional_asset_detail_variants) + { + if(processedOptionalAssetDetails.ContainsKey(optionalAssetDetailVariant.Key)) + continue; + if (association.GetItemDetailsKey() == optionalAssetDetailVariant.Value.GetItemDetailsKey()) + { + inlinedGroupDetails.assetDetails.Add(optionalAssetDetailVariant.Value); + processedOptionalAssetDetails[optionalAssetDetailVariant.Key] = true; + } + } + } break; case LootLockerCatalogEntryEntityKind.progression_points: if (catalogListing.progression_points_details.ContainsKey(association.GetItemDetailsKey())) diff --git a/Runtime/Game/Requests/PlayerRequest.cs b/Runtime/Game/Requests/PlayerRequest.cs index d23f54d4b..f15337dfc 100644 --- a/Runtime/Game/Requests/PlayerRequest.cs +++ b/Runtime/Game/Requests/PlayerRequest.cs @@ -75,6 +75,29 @@ public class LootLockerInventory public float balance { get; set; } } + /// + /// A simplified view of an inventory item + /// + public class LootLockerSimpleInventoryItem + { + /// + /// The asset id of the inventory item + /// + public int asset_id { get; set; } + /// + /// The instance id of the inventory item + /// + public int instance_id { get; set; } + /// + /// The acquisition source of the inventory item + /// + public string acquisition_source { get; set; } + /// + /// The acquisition date of the inventory item + /// + public DateTime? acquisition_date { get; set; } + } + public class LootLockerRewardObject { public int instance_id { get; set; } @@ -153,6 +176,21 @@ public class LootLockerListPlayerInfoRequest public string[] player_public_uid { get; set; } } + /// + /// Request to list a player's simplified inventory with the given filters + /// + public class LootLockerListSimplifiedInventoryRequest + { + /// + /// A list of asset ids to filter the inventory items by + /// + public int[] asset_ids { get; set; } + /// + /// A list of context ids to filter the inventory items by + /// + public int[] context_ids { get; set; } + } + //================================================== // Response Definitions //================================================== @@ -202,6 +240,22 @@ public class LootLockerInventoryResponse : LootLockerResponse { public LootLockerInventory[] inventory { get; set; } } + + /// + /// The response class for simplified inventory requests + /// + [Serializable] + public class LootLockerSimpleInventoryResponse : LootLockerResponse + { + /// + /// List of simplified inventory items according to the requested filters + /// + public LootLockerSimpleInventoryItem[] items { get; set; } + /// + /// Pagination information for the response + /// + public LootLockerExtendedPagination pagination { get; set; } + } public class LootLockerPlayerFilesResponse : LootLockerResponse { diff --git a/Runtime/Game/Resources/LootLockerConfig.cs b/Runtime/Game/Resources/LootLockerConfig.cs index 6bf4f0417..9bd8d684a 100644 --- a/Runtime/Game/Resources/LootLockerConfig.cs +++ b/Runtime/Game/Resources/LootLockerConfig.cs @@ -173,13 +173,14 @@ static void ListInstalledPackagesRequestProgress() } #endif - public static bool CreateNewSettings(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info, bool logInBuilds = false, bool errorsAsWarnings = false, bool allowTokenRefresh = false) + public static bool CreateNewSettings(string apiKey, string gameVersion, string domainKey, LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info, bool logInBuilds = false, bool errorsAsWarnings = false, bool allowTokenRefresh = false, bool prettifyJson = false) { _current = Get(); _current.apiKey = apiKey; _current.game_version = gameVersion; _current.logLevel = logLevel; + _current.prettifyJson = prettifyJson; _current.logInBuilds = logInBuilds; _current.logErrorsAsWarnings = errorsAsWarnings; _current.allowTokenRefresh = allowTokenRefresh; @@ -199,6 +200,10 @@ public static bool ClearSettings() _current.apiKey = null; _current.game_version = null; _current.logLevel = LootLockerLogger.LogLevel.Info; + _current.prettifyJson = false; + _current.logInBuilds = false; + _current.logErrorsAsWarnings = false; + _current.obfuscateLogs = true; _current.allowTokenRefresh = true; _current.domainKey = null; #if UNITY_EDITOR @@ -274,6 +279,9 @@ public static bool IsTargetingProductionEnvironment() [HideInInspector] public string baseUrl = UrlProtocol + GetUrlCore(); [HideInInspector] public float clientSideRequestTimeOut = 180f; public LootLockerLogger.LogLevel logLevel = LootLockerLogger.LogLevel.Info; + // Write JSON in a pretty and indented format when logging + public bool prettifyJson = true; + [HideInInspector] public bool obfuscateLogs = true; public bool logErrorsAsWarnings = false; public bool logInBuilds = false; public bool allowTokenRefresh = true; diff --git a/package.json b/package.json index 560b3a1a1..f2f48d630 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.lootlocker.lootlockersdk", - "version": "6.4.0", + "version": "6.5.0", "displayName": "LootLocker", "description": "LootLocker is a game backend-as-a-service with plug and play tools to upgrade your game and give your players the best experience possible. Designed for teams of all shapes and sizes, on mobile, PC and console. From solo developers, indie teams, AAA studios, and publishers. Built with cross-platform in mind.\n\n▪ Manage your game\nSave time and upgrade your game with leaderboards, progression, and more. Completely off-the-shelf features, built to work with any game and platform.\n\n▪ Manage your content\nTake charge of your game's content on all platforms, in one place. Sort, edit and manage everything, from cosmetics to currencies, UGC to DLC. Without breaking a sweat.\n\n▪ Manage your players\nStore your players' data together in one place. Access their profile and friends list cross-platform. Manage reports, messages, refunds and gifts to keep them hooked.\n", "unity": "2019.2",