From 7b0211542a8de45a1216c8971d0de52a75a90977 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 25 Sep 2026 10:18:53 +0200 Subject: [PATCH 1/7] feat: add Items and Item Templates support Adds SDK support for the new inventory system (internally "Assets 2.0"), publicly named Items and Item Templates. Game API endpoints (base player/inventory/v1): - GET / ListItemTemplates - GET /my ListPlayerItems - GET /{id} GetPlayerItem - DELETE /{id} DeletePlayerItem - POST /{id}/consume ConsumePlayerItem - POST /{id}/split SplitPlayerItemStack - POST /merge MergePlayerItemStacks Notes: - List responses use LootLockerExtendedPagination (the backend returns an extended offset pagination shape, not the simple page/per_page/total). - metadata is typed as LootLockerMetadataEntry[]. - consumable is a nullable bool so "no filter" remains reachable; the backend only applies the filter when the query value is non-empty. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Client/LootLockerEndPoints.cs | 10 + Runtime/Game/LootLockerSDKManager.cs | 137 +++++++++- Runtime/Game/Requests/ItemRequest.cs | 359 ++++++++++++++++++++++++++ 3 files changed, 505 insertions(+), 1 deletion(-) create mode 100644 Runtime/Game/Requests/ItemRequest.cs diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index 6a5aee8f8..c34f75b43 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -349,6 +349,16 @@ public class LootLockerEndPoints // Presence (WebSocket) [Header("Presence")] public static EndPointClass presenceWebSocket = new EndPointClass("presence/v1", LootLockerHTTPMethod.GET); + + // Items + [Header("Items")] + public static EndPointClass listItemTemplates = new EndPointClass("player/inventory/v1", LootLockerHTTPMethod.GET); + public static EndPointClass listPlayerItems = new EndPointClass("player/inventory/v1/my", LootLockerHTTPMethod.GET); + public static EndPointClass getPlayerItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.GET); + public static EndPointClass deletePlayerItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.DELETE); + public static EndPointClass consumePlayerItem = new EndPointClass("player/inventory/v1/{0}/consume", LootLockerHTTPMethod.POST); + public static EndPointClass splitPlayerItemStack = new EndPointClass("player/inventory/v1/{0}/split", LootLockerHTTPMethod.POST); + public static EndPointClass mergePlayerItemStacks = new EndPointClass("player/inventory/v1/merge", LootLockerHTTPMethod.POST); } [Serializable] diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 2d104dacf..2b6857351 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6626,7 +6626,142 @@ public static void DeleteAssetInstanceFromPlayerInventory(int assetInstanceID, A LootLockerAPIManager.DeleteAssetInstanceFromPlayerInventory(forPlayerWithUlid, data, onComplete); } #endregion - + + #region Items + + /// + /// Returns a paginated list of all visible item templates. + /// + /// The page of item templates to return. Defaults to 1. + /// The number of item templates to return per page. Defaults to 25. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListItemTemplates(int page, int perPage, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.ListItemTemplates(forPlayerWithUlid, page, perPage, onComplete); + } + + /// + /// Returns a paginated list of the current player's inventory items, including their item templates. + /// + /// The page of items to return. Defaults to 1. + /// The number of items to return per page. Defaults to 25. + /// (Optional) Return only items whose template name starts with the specified value. Set to null to not use this filter. + /// (Optional) Return only items of the specified type. Set to null to not use this filter. + /// (Optional) Return only items that are (or are not) consumable. Set to null to not use this filter. + /// (Optional) The field by which to sort the items. Set to null to not use this filter. + /// (Optional) The direction in which to sort the items. Set to null to not use this filter. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ListPlayerItems(int page, int perPage, string name = null, LootLockerItemType? itemType = null, bool? consumable = null, LootLockerItemSortField? sort = null, LootLockerSortOrder? order = null, Action onComplete = null, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.ListPlayerItems(forPlayerWithUlid, page, perPage, name, itemType?.ToString(), consumable, sort?.ToString(), order?.ToString(), onComplete); + } + + /// + /// Returns a single inventory item for the current player, including its item template. + /// + /// The id of the inventory item to fetch. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void GetPlayerItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.GetPlayerItem(forPlayerWithUlid, inventoryId, onComplete); + } + + /// + /// Deletes an inventory item from the current player's inventory. + /// + /// The id of the inventory item to delete. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void DeletePlayerItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerAPIManager.DeletePlayerItem(forPlayerWithUlid, inventoryId, onComplete); + } + + /// + /// Consumes one or more items from a stackable inventory item. If is null, the whole stack is consumed. + /// + /// The id of the stackable inventory item to consume. + /// (Optional) The number of items to consume. If null, the whole stack is consumed. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ConsumePlayerItem(string inventoryId, int? count, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerConsumeItemRequest data = new LootLockerConsumeItemRequest { count = count }; + LootLockerAPIManager.ConsumePlayerItem(forPlayerWithUlid, inventoryId, data, onComplete); + } + + /// + /// Splits a stackable inventory item into two stacks, moving items into a new stack. + /// + /// The id of the stackable inventory item to split. + /// The number of items to move into the new stack. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void SplitPlayerItemStack(string inventoryId, int count, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerSplitItemStackRequest data = new LootLockerSplitItemStackRequest { count = count }; + LootLockerAPIManager.SplitPlayerItemStack(forPlayerWithUlid, inventoryId, data, onComplete); + } + + /// + /// Merges two stacks of the same item into one. + /// + /// The id of the inventory item to merge from. + /// The id of the inventory item to merge into. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void MergePlayerItemStacks(string sourceInventoryId, string targetInventoryId, Action onComplete, string forPlayerWithUlid = null) + { + if (!CheckInitialized(false, forPlayerWithUlid)) + { + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + return; + } + + LootLockerMergeItemStacksRequest data = new LootLockerMergeItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }; + LootLockerAPIManager.MergePlayerItemStacks(forPlayerWithUlid, data, onComplete); + } + #endregion + #region AssetInstance progressions /// @ingroup AssetInstanceProgressions diff --git a/Runtime/Game/Requests/ItemRequest.cs b/Runtime/Game/Requests/ItemRequest.cs new file mode 100644 index 000000000..1ba1a024d --- /dev/null +++ b/Runtime/Game/Requests/ItemRequest.cs @@ -0,0 +1,359 @@ +using System; +using LootLocker.Requests; +using LootLocker.Utilities.HTTP; + +namespace LootLocker.LootLockerEnums +{ + /// + /// The type of an item, determining whether the item instances are stackable or individually tracked. + /// + public enum LootLockerItemType + { + /// Each granted item is a separate, individually tracked instance. + instanced, + /// The item is stored as a single entry with a count that can be incremented or decremented. + stackable + } + + /// + /// The field by which to order a player item list response. + /// + public enum LootLockerItemSortField + { + /// Order by when the item was created. + created_at, + /// Order by when the item was last updated. + updated_at + } + + /// + /// The direction in which to order a player item list response. + /// + public enum LootLockerSortOrder + { + /// Order ascending. + ASC, + /// Order descending. + DESC + } +} + +namespace LootLocker.Requests +{ + //================================================== + // Item Template Definitions + //================================================== + + /// + /// An item template defines a type of item that can be granted to players. + /// + public class LootLockerItemTemplate + { + /// The unique id of the item template. + public string id { get; set; } + + /// The name of the item template. + public string name { get; set; } + + /// The id of the game this item template belongs to. + public int game_id { get; set; } + + /// Indicates whether this item template is limited in how many of it can exist. + public int limited { get; set; } + + /// The type of the item (instanced or stackable). + public string item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The time that this item template was created. + public string created_at { get; set; } + + /// The time that this item template was last updated. + public string updated_at { get; set; } + } + + //================================================== + // Player Item Definitions + //================================================== + + /// + /// An item instance held in a player's inventory. + /// + public class LootLockerItem + { + /// The unique id of this inventory item. + public string id { get; set; } + + /// The id of the player that owns this inventory item. + public int player_id { get; set; } + + /// The id of the item template this inventory item is based on. + public string item_template_id { get; set; } + + /// The type of the item (instanced or stackable). + public string item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The number of items in this stack (only relevant for stackable items). + public int count { get; set; } + + /// The name of the item template this inventory item is based on. + public string name { get; set; } + + /// The source of this inventory item, or null if not specified. + public string source { get; set; } + + /// The time that this inventory item was created. + public string created_at { get; set; } + + /// The time that this inventory item was last updated. + public string updated_at { get; set; } + + /// Arbitrary metadata attached to this inventory item. + public LootLockerMetadataEntry[] metadata { get; set; } + } + + /// + /// An item granted to a player as the result of consuming another item (or via an on-grant behaviour). + /// Matches the go-backend `GrantResult` shape returned in `granted` / `behaviour_results`. + /// + public class LootLockerGrantedItem + { + /// The source id of the granted entry (inventory id, currency id, etc.). + public string source_id { get; set; } + + /// The number of items granted. + public int count { get; set; } + + /// The type of the granted entry ("item_template", "currency", or "publisher_currency"). + public string type { get; set; } + + /// The name of the granted entry, if applicable. + public string name { get; set; } + + /// The code of the granted entry, if applicable. + public string code { get; set; } + } + + //================================================== + // Request Definitions + //================================================== + + /// + /// Request to consume a stackable item from the player's inventory. + /// + public class LootLockerConsumeItemRequest + { + /// The number of items to consume. If omitted, the whole stack is consumed. + public int? count { get; set; } + } + + /// + /// Request to split a stackable item in the player's inventory into two stacks. + /// + public class LootLockerSplitItemStackRequest + { + /// The number of items to move into the new stack. + public int count { get; set; } + } + + /// + /// Request to merge two stacks of the same item into one. + /// + public class LootLockerMergeItemStacksRequest + { + /// The id of the source inventory item to merge from. + public string source_inventory_id { get; set; } + + /// The id of the target inventory item to merge into. + public string target_inventory_id { get; set; } + } + + //================================================== + // Response Definitions + //================================================== + + /// + /// Response containing a paginated list of visible item templates. + /// + public class LootLockerListItemTemplatesResponse : LootLockerResponse + { + /// The list of visible item templates. + public LootLockerItemTemplate[] items { get; set; } + + /// Pagination details for the response. + public LootLockerExtendedPagination pagination { get; set; } + } + + /// + /// Response containing a paginated list of the player's inventory items. + /// + public class LootLockerListPlayerItemsResponse : LootLockerResponse + { + /// The list of the player's inventory items. + public LootLockerItem[] items { get; set; } + + /// Pagination details for the response. + public LootLockerExtendedPagination pagination { get; set; } + } + + /// + /// Response containing the player's inventory item, returned at the top level with its template and metadata. + /// + public class LootLockerGetPlayerItemResponse : LootLockerResponse + { + /// The unique id of this inventory item. + public string id { get; set; } + + /// The id of the player that owns this inventory item. + public int player_id { get; set; } + + /// The id of the item template this inventory item is based on. + public string item_template_id { get; set; } + + /// The type of the item (instanced or stackable). + public string item_type { get; set; } + + /// True if the item can be consumed by the player. + public bool consumable { get; set; } + + /// True if the item can be deleted by the player. + public bool deletable { get; set; } + + /// The number of items in this stack (only relevant for stackable items). + public int count { get; set; } + + /// The source of this inventory item, or null if not specified. + public string source { get; set; } + + /// The time that this inventory item was created. + public string created_at { get; set; } + + /// The time that this inventory item was last updated. + public string updated_at { get; set; } + + /// The item template this inventory item is based on. + public LootLockerItemTemplate template { get; set; } + + /// Arbitrary metadata attached to this inventory item. + public LootLockerMetadataEntry[] metadata { get; set; } + } + + /// + /// Response containing the result of consuming an item, including any items granted as a result. + /// + public class LootLockerConsumeItemResponse : LootLockerResponse + { + /// True if the item was consumed. + public bool consumed { get; set; } + + /// Any items granted as a result of consuming the item. + public LootLockerGrantedItem[] granted { get; set; } + } + + /// + /// Response containing the id of the newly created stack after splitting an item. + /// + public class LootLockerSplitItemStackResponse : LootLockerResponse + { + /// The id of the newly created item stack. + public string id { get; set; } + } +} + +namespace LootLocker +{ + //================================================== + // API Class Definition + //================================================== + + public partial class LootLockerAPIManager + { + public static void ListItemTemplates(string forPlayerWithUlid, int page, int perPage, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.listItemTemplates; + + var queryParams = new QueryParamaterBuilder(); + queryParams.Add("page", page); + queryParams.Add("per_page", perPage); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void ListPlayerItems(string forPlayerWithUlid, int page, int perPage, string name, string itemType, bool? consumable, string sort, string order, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.listPlayerItems; + + var queryParams = new QueryParamaterBuilder(); + queryParams.Add("page", page); + queryParams.Add("per_page", perPage); + queryParams.Add("name", name); + queryParams.Add("item_type", itemType); + if (consumable.HasValue) + { + queryParams.Add("consumable", consumable.Value.ToString().ToLower()); + } + queryParams.Add("sort", sort); + queryParams.Add("order", order); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void GetPlayerItem(string forPlayerWithUlid, string inventoryId, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.getPlayerItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void DeletePlayerItem(string forPlayerWithUlid, string inventoryId, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.deletePlayerItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void ConsumePlayerItem(string forPlayerWithUlid, string inventoryId, LootLockerConsumeItemRequest data, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.consumePlayerItem; + + string getVariable = endPoint.WithPathParameter(inventoryId); + string json = LootLockerJson.SerializeObject(data); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void SplitPlayerItemStack(string forPlayerWithUlid, string inventoryId, LootLockerSplitItemStackRequest data, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.splitPlayerItemStack; + + string getVariable = endPoint.WithPathParameter(inventoryId); + string json = LootLockerJson.SerializeObject(data); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + + public static void MergePlayerItemStacks(string forPlayerWithUlid, LootLockerMergeItemStacksRequest data, Action onComplete) + { + EndPointClass endPoint = LootLockerEndPoints.mergePlayerItemStacks; + + string json = LootLockerJson.SerializeObject(data); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); + } + } +} From 71bd03624531e50319a926a5969bccfeb2ef7500 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Fri, 25 Sep 2026 15:11:17 +0200 Subject: [PATCH 2/7] fix: address review remarks and add Assets 2.0 metadata sources - Add ShouldSerializecount() to LootLockerConsumeItemRequest so an unset count is omitted entirely. This removes the Newtonsoft/ZeroDep asymmetry (only ZeroDep skips nulls) and avoids sending {"count":null}, which cannot decode into uint64. - Correct the consume count documentation: the backend defaults to 1, not the whole stack. To consume an entire stack, pass the item's current count. - Remove the incorrect "Defaults to 1." / "Defaults to 25." claims from the page/perPage docs on ListItemTemplates and ListPlayerItems; both are required. - Add the missing ItemRequest.cs.meta file. - Add inventory and item_template to LootLockerMetadataSources and correct the misleading item comment (item is the legacy source, inventory is Assets 2.0). - Add 7 serialization/deserialization tests covering the new item endpoints. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Game/LootLockerSDKManager.cs | 8 +- Runtime/Game/Requests/ItemRequest.cs | 12 +- Runtime/Game/Requests/ItemRequest.cs.meta | 11 ++ Runtime/Game/Requests/MetadataRequests.cs | 4 +- Tests/LootLockerTests/PlayMode/JsonTests.cs | 179 +++++++++++++++++++- 5 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 Runtime/Game/Requests/ItemRequest.cs.meta diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 2b6857351..ad5cbdcef 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6632,8 +6632,8 @@ public static void DeleteAssetInstanceFromPlayerInventory(int assetInstanceID, A /// /// Returns a paginated list of all visible item templates. /// - /// The page of item templates to return. Defaults to 1. - /// The number of item templates to return per page. Defaults to 25. + /// The page of item templates to return. + /// The number of item templates to return per page. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ListItemTemplates(int page, int perPage, Action onComplete, string forPlayerWithUlid = null) @@ -6650,8 +6650,8 @@ public static void ListItemTemplates(int page, int perPage, Action /// Returns a paginated list of the current player's inventory items, including their item templates. /// - /// The page of items to return. Defaults to 1. - /// The number of items to return per page. Defaults to 25. + /// The page of items to return. + /// The number of items to return per page. /// (Optional) Return only items whose template name starts with the specified value. Set to null to not use this filter. /// (Optional) Return only items of the specified type. Set to null to not use this filter. /// (Optional) Return only items that are (or are not) consumable. Set to null to not use this filter. diff --git a/Runtime/Game/Requests/ItemRequest.cs b/Runtime/Game/Requests/ItemRequest.cs index 1ba1a024d..fb3954cb2 100644 --- a/Runtime/Game/Requests/ItemRequest.cs +++ b/Runtime/Game/Requests/ItemRequest.cs @@ -154,8 +154,18 @@ public class LootLockerGrantedItem /// public class LootLockerConsumeItemRequest { - /// The number of items to consume. If omitted, the whole stack is consumed. + /// + /// The number of items to consume. Defaults to 1 when omitted. To consume an entire stack, + /// pass the item's current count. + /// public int? count { get; set; } + + public bool ShouldSerializecount() + { + // Omit count entirely when unset so the backend applies its default of 1. This also keeps + // the Newtonsoft and ZeroDep JSON backends consistent, since only ZeroDep skips nulls. + return count.HasValue; + } } /// diff --git a/Runtime/Game/Requests/ItemRequest.cs.meta b/Runtime/Game/Requests/ItemRequest.cs.meta new file mode 100644 index 000000000..0f33bdad8 --- /dev/null +++ b/Runtime/Game/Requests/ItemRequest.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 1c184783fa724925b081edf9211f3855 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/Game/Requests/MetadataRequests.cs b/Runtime/Game/Requests/MetadataRequests.cs index 896c274c3..f05197318 100644 --- a/Runtime/Game/Requests/MetadataRequests.cs +++ b/Runtime/Game/Requests/MetadataRequests.cs @@ -25,7 +25,9 @@ public enum LootLockerMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for asset instances (player inventory items), while the "asset" source is for the asset in general + item = 8, // This is the source for legacy items, while the "inventory" source is for Assets 2.0 item instances + inventory = 9, // This is the source for Assets 2.0 item instances (player inventory items) + item_template = 10, // This is the source for Assets 2.0 item templates }; /// diff --git a/Tests/LootLockerTests/PlayMode/JsonTests.cs b/Tests/LootLockerTests/PlayMode/JsonTests.cs index f78fe1a6d..9570399e6 100644 --- a/Tests/LootLockerTests/PlayMode/JsonTests.cs +++ b/Tests/LootLockerTests/PlayMode/JsonTests.cs @@ -1,4 +1,5 @@ -using LootLocker; +using LootLocker; +using LootLocker.LootLockerEnums; using LootLocker.Requests; using System; using System.Collections; @@ -315,6 +316,181 @@ public void Json_DeserializationCaseConversion_CaseIsConvertedToSnake() #endif } + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingConsumeItemRequestWithoutCount_OmitsCount() + { + // Given + var request = new LootLockerConsumeItemRequest(); + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsFalse(serializedJson.Contains("count"), + "Not serialized correctly, contains count even though it was never set"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingConsumeItemRequestWithCount_IncludesCount() + { + // Given + var request = new LootLockerConsumeItemRequest { count = 3 }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("count"), + "Not serialized correctly, does not contain count"); + Assert.IsTrue(serializedJson.Contains("3"), + "Not serialized correctly, does not contain the count value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingSplitItemStackRequest_IncludesCount() + { + // Given + var request = new LootLockerSplitItemStackRequest { count = 5 }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("count"), + "Not serialized correctly, does not contain count"); + Assert.IsTrue(serializedJson.Contains("5"), + "Not serialized correctly, does not contain the count value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_SerializingMergeItemStacksRequest_IncludesBothInventoryIds() + { + // Given + var request = new LootLockerMergeItemStacksRequest + { + source_inventory_id = "01HZZZZZZZZZZZZZZZZZZZZZZZ", + target_inventory_id = "01HYYYYYYYYYYYYYYYYYYYYYYY" + }; + + // When + string serializedJson = LootLockerJson.SerializeObject(request); + + // Then + Assert.IsNotNull(serializedJson, "Not serialized, is null"); + Assert.IsTrue(serializedJson.Contains("source_inventory_id"), + "Not serialized correctly, does not contain source_inventory_id"); + Assert.IsTrue(serializedJson.Contains("target_inventory_id"), + "Not serialized correctly, does not contain target_inventory_id"); + Assert.IsTrue(serializedJson.Contains("01HZZZZZZZZZZZZZZZZZZZZZZZ"), + "Not serialized correctly, does not contain the source inventory id value"); + Assert.IsTrue(serializedJson.Contains("01HYYYYYYYYYYYYYYYYYYYYYYY"), + "Not serialized correctly, does not contain the target inventory id value"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingListPlayerItemsResponse_Succeeds() + { + // Given + const string listPlayerItemsResponse = + "{\"success\":true,\"items\":[{\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"stackable\",\"consumable\":true,\"count\":7,\"source\":\"grant\",\"name\":\"Health Potion\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\"}],\"pagination\":{\"total\":1,\"offset\":0,\"per_page\":25,\"last_page\":1,\"current_page\":1,\"next_page\":null,\"prev_page\":null}}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(listPlayerItemsResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsNotNull(deserialized.items, "Not deserialized, items is null"); + Assert.AreEqual(1, deserialized.items.Length, "Not deserialized, wrong number of items"); + Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.items[0].id, "Wrong item id"); + Assert.AreEqual(3, deserialized.items[0].player_id, "Wrong player id"); + Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].item_template_id, "Wrong item template id"); + Assert.AreEqual("stackable", deserialized.items[0].item_type, "Wrong item type"); + Assert.IsTrue(deserialized.items[0].consumable, "Wrong consumable value"); + Assert.AreEqual(7, deserialized.items[0].count, "Wrong count"); + Assert.AreEqual("Health Potion", deserialized.items[0].name, "Wrong name"); + Assert.IsTrue(deserialized.items[0].deletable, "Wrong deletable value"); + Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); + Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); + Assert.AreEqual(25, deserialized.pagination.per_page, "Wrong pagination per_page"); + Assert.IsNull(deserialized.pagination.next_page, "Wrong pagination next_page"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingGetPlayerItemResponseWithTemplateAndMetadata_Succeeds() + { + // Given + const string getPlayerItemResponse = + "{\"success\":true,\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"instanced\",\"consumable\":false,\"count\":1,\"source\":\"grant\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\",\"template\":{\"id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"name\":\"Sword\",\"game_id\":42,\"limited\":0,\"item_type\":\"instanced\",\"consumable\":false,\"deletable\":true},\"metadata\":[{\"key\":\"damage\",\"value\":12,\"type\":\"number\",\"access\":[\"game_api.read\"],\"tags\":[\"combat\"]}]}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(getPlayerItemResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.id, "Wrong item id"); + Assert.AreEqual("instanced", deserialized.item_type, "Wrong item type"); + Assert.IsTrue(deserialized.deletable, "Wrong deletable value"); + Assert.IsNotNull(deserialized.template, "Not deserialized, template is null"); + Assert.AreEqual("Sword", deserialized.template.name, "Wrong template name"); + Assert.AreEqual(42, deserialized.template.game_id, "Wrong template game id"); + Assert.IsNotNull(deserialized.metadata, "Not deserialized, metadata is null"); + Assert.AreEqual(1, deserialized.metadata.Length, "Not deserialized, wrong number of metadata entries"); + Assert.AreEqual("damage", deserialized.metadata[0].key, "Wrong metadata key"); + Assert.AreEqual(LootLockerMetadataTypes.Number, deserialized.metadata[0].type, "Wrong metadata type"); + Assert.IsNotNull(deserialized.metadata[0].access, "Not deserialized, metadata access is null"); + Assert.AreEqual("game_api.read", deserialized.metadata[0].access[0], "Wrong metadata access"); + Assert.IsNotNull(deserialized.metadata[0].tags, "Not deserialized, metadata tags is null"); + Assert.AreEqual("combat", deserialized.metadata[0].tags[0], "Wrong metadata tag"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingConsumeItemResponseWithGrantedItems_Succeeds() + { + // Given + const string consumeItemResponse = + "{\"success\":true,\"consumed\":true,\"granted\":[{\"source_id\":\"01HBBBBBBBBBBBBBBBBBBBBBBB\",\"count\":2,\"type\":\"currency\",\"name\":\"Gold\",\"code\":\"gold\"}]}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(consumeItemResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsTrue(deserialized.consumed, "Wrong consumed value"); + Assert.IsNotNull(deserialized.granted, "Not deserialized, granted is null"); + Assert.AreEqual(1, deserialized.granted.Length, "Not deserialized, wrong number of granted items"); + Assert.AreEqual("01HBBBBBBBBBBBBBBBBBBBBBBB", deserialized.granted[0].source_id, "Wrong granted source id"); + Assert.AreEqual(2, deserialized.granted[0].count, "Wrong granted count"); + Assert.AreEqual("currency", deserialized.granted[0].type, "Wrong granted type"); + Assert.AreEqual("Gold", deserialized.granted[0].name, "Wrong granted name"); + Assert.AreEqual("gold", deserialized.granted[0].code, "Wrong granted code"); + } + + [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + public void Json_DeserializingListItemTemplatesResponse_Succeeds() + { + // Given + const string listItemTemplatesResponse = + "{\"success\":true,\"items\":[{\"id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"name\":\"Sword\",\"game_id\":42,\"limited\":10,\"item_type\":\"instanced\",\"consumable\":false,\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\",\"updated_at\":\"2024-01-02T00:00:00Z\"}],\"pagination\":{\"total\":1,\"offset\":0,\"per_page\":25,\"last_page\":1,\"current_page\":1,\"next_page\":null,\"prev_page\":null}}"; + + // When + var deserialized = LootLockerJson.DeserializeObject(listItemTemplatesResponse); + + // Then + Assert.IsNotNull(deserialized, "Not deserialized, is null"); + Assert.IsNotNull(deserialized.items, "Not deserialized, items is null"); + Assert.AreEqual(1, deserialized.items.Length, "Not deserialized, wrong number of item templates"); + Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].id, "Wrong item template id"); + Assert.AreEqual("Sword", deserialized.items[0].name, "Wrong item template name"); + Assert.AreEqual(42, deserialized.items[0].game_id, "Wrong item template game id"); + Assert.AreEqual(10, deserialized.items[0].limited, "Wrong item template limited value"); + Assert.IsTrue(deserialized.items[0].deletable, "Wrong item template deletable value"); + Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); + Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); + } + #if !LOOTLOCKER_USE_NEWTONSOFTJSON [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] public void Json_SimpleTypeSerialization_Succeeds() @@ -450,6 +626,7 @@ public void Json_SerializingAndDesierializingEnumArrays_Works() Assert.IsNotEmpty(deserialized.enumArray); Assert.AreNotEqual(defaultEnumValue, deserialized.enumArray[0]); } + } class CustomOptions : JsonOptions From ff0c489936ac87740718fa4a77b42073ff218ec9 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Mon, 28 Sep 2026 12:31:32 +0200 Subject: [PATCH 3/7] fix(unity): correct item DTO types and inline API manager methods Address PR review feedback on the Items / Item Templates DTOs: - Remove game_id from item and item template DTOs; the SDK always operates in a game context so the field is redundant. - Type item_type as LootLockerItemType instead of string. - Type created_at / updated_at as DateTime (updated_at nullable, since the backend omits it when unset). - Type granted item `type` as the new LootLockerRewardKind enum. - Add `source` to LootLockerItemSortField. - Only append query parameters when they carry a meaningful value, so unset filters are not sent as empty/zero values. - Inline the seven item methods from the LootLockerAPIManager partial class into LootLockerSDKManager, matching the convention used by the rest of the SDK. - Update the item JSON tests for the new types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Game/LootLockerSDKManager.cs | 64 +++++-- Runtime/Game/Requests/ItemRequest.cs | 158 ++++++------------ Runtime/Game/Requests/MetadataRequests.cs | 6 +- .../LootLockerTestConfigurationMetadata.cs | 2 + Tests/LootLockerTests/PlayMode/JsonTests.cs | 15 +- 5 files changed, 122 insertions(+), 123 deletions(-) diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index ad5cbdcef..20b8eab01 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6644,7 +6644,15 @@ public static void ListItemTemplates(int page, int perPage, Action 0) + queryParams.Add("page", page); + if (perPage > 0) + queryParams.Add("per_page", perPage); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6667,7 +6675,24 @@ public static void ListPlayerItems(int page, int perPage, string name = null, Lo return; } - LootLockerAPIManager.ListPlayerItems(forPlayerWithUlid, page, perPage, name, itemType?.ToString(), consumable, sort?.ToString(), order?.ToString(), onComplete); + EndPointClass endPoint = LootLockerEndPoints.listPlayerItems; + + var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); + if (page > 0) + queryParams.Add("page", page); + if (perPage > 0) + queryParams.Add("per_page", perPage); + queryParams.Add("name", name); + if (itemType.HasValue) + queryParams.Add("item_type", itemType.Value.ToString()); + if (consumable.HasValue) + queryParams.Add("consumable", consumable.Value.ToString().ToLower()); + if (sort.HasValue) + queryParams.Add("sort", sort.Value.ToString()); + if (order.HasValue) + queryParams.Add("order", order.Value.ToString()); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6684,7 +6709,11 @@ public static void GetPlayerItem(string inventoryId, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6701,7 +6730,11 @@ public static void DeletePlayerItem(string inventoryId, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6719,8 +6752,12 @@ public static void ConsumePlayerItem(string inventoryId, int? count, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6738,8 +6775,12 @@ public static void SplitPlayerItemStack(string inventoryId, int count, Action { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } /// @@ -6757,8 +6798,11 @@ public static void MergePlayerItemStacks(string sourceInventoryId, string target return; } - LootLockerMergeItemStacksRequest data = new LootLockerMergeItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }; - LootLockerAPIManager.MergePlayerItemStacks(forPlayerWithUlid, data, onComplete); + EndPointClass endPoint = LootLockerEndPoints.mergePlayerItemStacks; + + string json = LootLockerJson.SerializeObject(new LootLockerMergeItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }); + + LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } #endregion diff --git a/Runtime/Game/Requests/ItemRequest.cs b/Runtime/Game/Requests/ItemRequest.cs index fb3954cb2..b3f375259 100644 --- a/Runtime/Game/Requests/ItemRequest.cs +++ b/Runtime/Game/Requests/ItemRequest.cs @@ -1,6 +1,6 @@ using System; +using LootLocker.LootLockerEnums; using LootLocker.Requests; -using LootLocker.Utilities.HTTP; namespace LootLocker.LootLockerEnums { @@ -23,7 +23,44 @@ public enum LootLockerItemSortField /// Order by when the item was created. created_at, /// Order by when the item was last updated. - updated_at + updated_at, + /// Order by the source that granted the item. + source + } + + /// + /// The kind of reward that was granted as a result of a behaviour. + /// + public enum LootLockerRewardKind + { + /// The reward kind could not be determined. + unknown, + /// An asset. + asset, + /// Progression points. + progression_points, + /// A progression reset. + progression_reset, + /// A currency. + currency, + /// A group. + group, + /// A reward. + reward, + /// A platform key. + platform_key, + /// A publisher currency. + publisher_currency, + /// Publisher progression points. + publisher_progression_points, + /// Player metadata. + player_metadata, + /// A file. + file, + /// A Discord role. + discord_role, + /// An item template. + item_template } /// @@ -55,14 +92,11 @@ public class LootLockerItemTemplate /// The name of the item template. public string name { get; set; } - /// The id of the game this item template belongs to. - public int game_id { get; set; } - /// Indicates whether this item template is limited in how many of it can exist. public int limited { get; set; } /// The type of the item (instanced or stackable). - public string item_type { get; set; } + public LootLockerItemType item_type { get; set; } /// True if the item can be consumed by the player. public bool consumable { get; set; } @@ -71,10 +105,10 @@ public class LootLockerItemTemplate public bool deletable { get; set; } /// The time that this item template was created. - public string created_at { get; set; } + public DateTime created_at { get; set; } - /// The time that this item template was last updated. - public string updated_at { get; set; } + /// The time that this item template was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } } //================================================== @@ -96,7 +130,7 @@ public class LootLockerItem public string item_template_id { get; set; } /// The type of the item (instanced or stackable). - public string item_type { get; set; } + public LootLockerItemType item_type { get; set; } /// True if the item can be consumed by the player. public bool consumable { get; set; } @@ -114,10 +148,10 @@ public class LootLockerItem public string source { get; set; } /// The time that this inventory item was created. - public string created_at { get; set; } + public DateTime created_at { get; set; } - /// The time that this inventory item was last updated. - public string updated_at { get; set; } + /// The time that this inventory item was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } /// Arbitrary metadata attached to this inventory item. public LootLockerMetadataEntry[] metadata { get; set; } @@ -135,8 +169,8 @@ public class LootLockerGrantedItem /// The number of items granted. public int count { get; set; } - /// The type of the granted entry ("item_template", "currency", or "publisher_currency"). - public string type { get; set; } + /// The type of the granted entry. + public LootLockerRewardKind type { get; set; } /// The name of the granted entry, if applicable. public string name { get; set; } @@ -232,7 +266,7 @@ public class LootLockerGetPlayerItemResponse : LootLockerResponse public string item_template_id { get; set; } /// The type of the item (instanced or stackable). - public string item_type { get; set; } + public LootLockerItemType item_type { get; set; } /// True if the item can be consumed by the player. public bool consumable { get; set; } @@ -247,10 +281,10 @@ public class LootLockerGetPlayerItemResponse : LootLockerResponse public string source { get; set; } /// The time that this inventory item was created. - public string created_at { get; set; } + public DateTime created_at { get; set; } - /// The time that this inventory item was last updated. - public string updated_at { get; set; } + /// The time that this inventory item was last updated, or null if it has never been updated. + public DateTime? updated_at { get; set; } /// The item template this inventory item is based on. public LootLockerItemTemplate template { get; set; } @@ -281,89 +315,3 @@ public class LootLockerSplitItemStackResponse : LootLockerResponse } } -namespace LootLocker -{ - //================================================== - // API Class Definition - //================================================== - - public partial class LootLockerAPIManager - { - public static void ListItemTemplates(string forPlayerWithUlid, int page, int perPage, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.listItemTemplates; - - var queryParams = new QueryParamaterBuilder(); - queryParams.Add("page", page); - queryParams.Add("per_page", perPage); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void ListPlayerItems(string forPlayerWithUlid, int page, int perPage, string name, string itemType, bool? consumable, string sort, string order, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.listPlayerItems; - - var queryParams = new QueryParamaterBuilder(); - queryParams.Add("page", page); - queryParams.Add("per_page", perPage); - queryParams.Add("name", name); - queryParams.Add("item_type", itemType); - if (consumable.HasValue) - { - queryParams.Add("consumable", consumable.Value.ToString().ToLower()); - } - queryParams.Add("sort", sort); - queryParams.Add("order", order); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint + queryParams.Build(), endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void GetPlayerItem(string forPlayerWithUlid, string inventoryId, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.getPlayerItem; - - string getVariable = endPoint.WithPathParameter(inventoryId); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void DeletePlayerItem(string forPlayerWithUlid, string inventoryId, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.deletePlayerItem; - - string getVariable = endPoint.WithPathParameter(inventoryId); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, null, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void ConsumePlayerItem(string forPlayerWithUlid, string inventoryId, LootLockerConsumeItemRequest data, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.consumePlayerItem; - - string getVariable = endPoint.WithPathParameter(inventoryId); - string json = LootLockerJson.SerializeObject(data); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void SplitPlayerItemStack(string forPlayerWithUlid, string inventoryId, LootLockerSplitItemStackRequest data, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.splitPlayerItemStack; - - string getVariable = endPoint.WithPathParameter(inventoryId); - string json = LootLockerJson.SerializeObject(data); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - - public static void MergePlayerItemStacks(string forPlayerWithUlid, LootLockerMergeItemStacksRequest data, Action onComplete) - { - EndPointClass endPoint = LootLockerEndPoints.mergePlayerItemStacks; - - string json = LootLockerJson.SerializeObject(data); - - LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); - } - } -} diff --git a/Runtime/Game/Requests/MetadataRequests.cs b/Runtime/Game/Requests/MetadataRequests.cs index f05197318..f99d0fa0d 100644 --- a/Runtime/Game/Requests/MetadataRequests.cs +++ b/Runtime/Game/Requests/MetadataRequests.cs @@ -25,9 +25,9 @@ public enum LootLockerMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for legacy items, while the "inventory" source is for Assets 2.0 item instances - inventory = 9, // This is the source for Assets 2.0 item instances (player inventory items) - item_template = 10, // This is the source for Assets 2.0 item templates + item = 8, // This is the source for legacy items, while the "inventory" source is for item instances + inventory = 9, // This is the source for item instances (player inventory items) + item_template = 10, // This is the source for item templates }; /// diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs index 329e69284..8ff483081 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs @@ -49,6 +49,8 @@ public enum LootLockerTestMetadataSources self = 6, asset = 7, item = 8, // This is the source for asset instances (player inventory items), while the "asset" source is for the asset in general + inventory = 9, // This is the source for inventory items + item_template = 10, // This is the source for item templates }; /// diff --git a/Tests/LootLockerTests/PlayMode/JsonTests.cs b/Tests/LootLockerTests/PlayMode/JsonTests.cs index 9570399e6..408f6c099 100644 --- a/Tests/LootLockerTests/PlayMode/JsonTests.cs +++ b/Tests/LootLockerTests/PlayMode/JsonTests.cs @@ -407,11 +407,13 @@ public void Json_DeserializingListPlayerItemsResponse_Succeeds() Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.items[0].id, "Wrong item id"); Assert.AreEqual(3, deserialized.items[0].player_id, "Wrong player id"); Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].item_template_id, "Wrong item template id"); - Assert.AreEqual("stackable", deserialized.items[0].item_type, "Wrong item type"); + Assert.AreEqual(LootLockerItemType.stackable, deserialized.items[0].item_type, "Wrong item type"); Assert.IsTrue(deserialized.items[0].consumable, "Wrong consumable value"); Assert.AreEqual(7, deserialized.items[0].count, "Wrong count"); Assert.AreEqual("Health Potion", deserialized.items[0].name, "Wrong name"); Assert.IsTrue(deserialized.items[0].deletable, "Wrong deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.items[0].created_at, "Wrong created_at value"); + Assert.IsNull(deserialized.items[0].updated_at, "Wrong updated_at value"); Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); Assert.AreEqual(25, deserialized.pagination.per_page, "Wrong pagination per_page"); @@ -431,11 +433,12 @@ public void Json_DeserializingGetPlayerItemResponseWithTemplateAndMetadata_Succe // Then Assert.IsNotNull(deserialized, "Not deserialized, is null"); Assert.AreEqual("01HZZZZZZZZZZZZZZZZZZZZZZZ", deserialized.id, "Wrong item id"); - Assert.AreEqual("instanced", deserialized.item_type, "Wrong item type"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.item_type, "Wrong item type"); Assert.IsTrue(deserialized.deletable, "Wrong deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.created_at, "Wrong created_at value"); Assert.IsNotNull(deserialized.template, "Not deserialized, template is null"); Assert.AreEqual("Sword", deserialized.template.name, "Wrong template name"); - Assert.AreEqual(42, deserialized.template.game_id, "Wrong template game id"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.template.item_type, "Wrong template item type"); Assert.IsNotNull(deserialized.metadata, "Not deserialized, metadata is null"); Assert.AreEqual(1, deserialized.metadata.Length, "Not deserialized, wrong number of metadata entries"); Assert.AreEqual("damage", deserialized.metadata[0].key, "Wrong metadata key"); @@ -463,7 +466,7 @@ public void Json_DeserializingConsumeItemResponseWithGrantedItems_Succeeds() Assert.AreEqual(1, deserialized.granted.Length, "Not deserialized, wrong number of granted items"); Assert.AreEqual("01HBBBBBBBBBBBBBBBBBBBBBBB", deserialized.granted[0].source_id, "Wrong granted source id"); Assert.AreEqual(2, deserialized.granted[0].count, "Wrong granted count"); - Assert.AreEqual("currency", deserialized.granted[0].type, "Wrong granted type"); + Assert.AreEqual(LootLockerRewardKind.currency, deserialized.granted[0].type, "Wrong granted type"); Assert.AreEqual("Gold", deserialized.granted[0].name, "Wrong granted name"); Assert.AreEqual("gold", deserialized.granted[0].code, "Wrong granted code"); } @@ -484,9 +487,11 @@ public void Json_DeserializingListItemTemplatesResponse_Succeeds() Assert.AreEqual(1, deserialized.items.Length, "Not deserialized, wrong number of item templates"); Assert.AreEqual("01HAAAAAAAAAAAAAAAAAAAAAAA", deserialized.items[0].id, "Wrong item template id"); Assert.AreEqual("Sword", deserialized.items[0].name, "Wrong item template name"); - Assert.AreEqual(42, deserialized.items[0].game_id, "Wrong item template game id"); + Assert.AreEqual(LootLockerItemType.instanced, deserialized.items[0].item_type, "Wrong item template item type"); Assert.AreEqual(10, deserialized.items[0].limited, "Wrong item template limited value"); Assert.IsTrue(deserialized.items[0].deletable, "Wrong item template deletable value"); + Assert.AreNotEqual(default(DateTime), deserialized.items[0].created_at, "Wrong item template created_at value"); + Assert.IsNotNull(deserialized.items[0].updated_at, "Wrong item template updated_at value"); Assert.IsNotNull(deserialized.pagination, "Not deserialized, pagination is null"); Assert.AreEqual(1, deserialized.pagination.total, "Wrong pagination total"); } From 28760ee709b736f38722bde03e4aaf8882eab6e9 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Mon, 28 Sep 2026 12:50:48 +0200 Subject: [PATCH 4/7] test(unity): add functional tests for items and item templates Add PlayMode coverage for the seven item endpoints against a live backend, using the admin API for setup (create template with a public audience, grant to the session player). - Add admin endpoint constants for the item template and player inventory routes. - Add a LootLockerTestItems helper mirroring the go-backend admin routes rather than the published ApiDog spec, which is stale for this feature: the list endpoint returns `templates` (not `items`), grant returns `id` (not `inventory_id`), consume requires a body and returns a count (not a bool), and the player inventory listing uses `template_name` and has no `deletable`. - Add ItemTests with 10 cases covering list templates, list player items (with and without filters), get, partial and whole-stack consume, split, merge, delete, and the forbidden delete of a non-deletable item. - Only the happy-path test carries LootLockerCIFast; the rest are LootLockerCI only. Also correct the ConsumePlayerItem doc comment: a null count consumes a single item, not the whole stack, matching the backend default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Game/LootLockerSDKManager.cs | 4 +- .../LootLockerTestConfigurationEndpoints.cs | 14 + .../LootLockerTestConfigurationItem.cs | 276 ++++++++++ .../LootLockerTestConfigurationItem.cs.meta | 11 + Tests/LootLockerTests/PlayMode/ItemTests.cs | 512 ++++++++++++++++++ .../PlayMode/ItemTests.cs.meta | 11 + 6 files changed, 826 insertions(+), 2 deletions(-) create mode 100644 Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs create mode 100644 Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta create mode 100644 Tests/LootLockerTests/PlayMode/ItemTests.cs create mode 100644 Tests/LootLockerTests/PlayMode/ItemTests.cs.meta diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 20b8eab01..9a19e2f44 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6738,10 +6738,10 @@ public static void DeletePlayerItem(string inventoryId, Action - /// Consumes one or more items from a stackable inventory item. If is null, the whole stack is consumed. + /// Consumes one or more items from a stackable inventory item. If is null, a single item is consumed. /// /// The id of the stackable inventory item to consume. - /// (Optional) The number of items to consume. If null, the whole stack is consumed. + /// (Optional) The number of items to consume. If null, the backend consumes 1. To consume the whole stack, pass the item's current count. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. public static void ConsumePlayerItem(string inventoryId, int? count, Action onComplete, string forPlayerWithUlid = null) diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs index c8f22a1e0..b638d9352 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs @@ -109,6 +109,20 @@ public class LootLockerTestConfigurationEndpoints [Header("LootLocker Admin API Inventory Operations")] public static EndPointClass adminGrantAssetToPlayerInventory = new EndPointClass("/game/#GAMEID#/player/{0}/inventory/grant", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + [Header("LootLocker Admin API Item Template Operations")] + public static EndPointClass createItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass listItemTemplates = new EndPointClass("game/#GAMEID#/templates/v1", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass getItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass updateItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.PATCH, LootLockerCallerRole.Admin); + public static EndPointClass deleteItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); + public static EndPointClass grantItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/grant", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass setItemTemplateAudiences = new EndPointClass("game/#GAMEID#/templates/v1/{0}/audiences", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminListPlayerItems = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass adminConsumePlayerItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/consume", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminDeletePlayerItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); + public static EndPointClass adminSplitPlayerItemStack = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/split", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminMergePlayerItemStacks = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/merge", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + [Header("LootLocker Admin API Metadata Operations")] public static EndPointClass metadataOperations = new EndPointClass("game/#GAMEID#/metadata", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs new file mode 100644 index 000000000..e6e5158bf --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs @@ -0,0 +1,276 @@ +using System; +using LootLocker; +using Random = UnityEngine.Random; + +namespace LootLockerTestConfigurationUtils +{ + /// + /// Admin API helpers for setting up item templates and player inventory items in tests. + /// + /// + /// These helpers deliberately mirror the go-backend admin routes rather than the published + /// ApiDog spec, which is stale for this feature. Notable differences from the spec: + /// the list endpoint returns templates (not items), grant returns id + /// (not inventory_id), consume requires a body and returns a count (not a bool), + /// and the player inventory listing uses template_name and has no deletable. + /// + public static class LootLockerTestItems + { + public static string GetRandomItemTemplateName() + { + string[] colors = { "Green", "Blue", "Red", "Black", "Yellow", "Orange", "Purple", "Indigo", "Clear", "White", "Magenta", "Marine", "Crimson", "Teal" }; + string[] items = { "Rod", "House", "Wand", "Staff", "Car", "Sword", "Shield", "Gun", "Shovel", "Boomstick", "Rifle", "Hut", "Boat", "Bicycle", "Wheelchair" }; + + return colors[Random.Range(0, colors.Length)] + " " + items[Random.Range(0, items.Length)]; + } + + /// + /// Create an item template. Audiences are set inline by the create endpoint, which is + /// required for the template to be visible to the player through the game API. + /// + public static void CreateItemTemplate(string name, string itemType, bool consumable, bool deletable, string[] audiences, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestItemTemplateResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var request = new LootLockerTestCreateItemTemplateRequest + { + name = name, + item_type = itemType, + consumable = consumable, + deletable = deletable, + audiences = audiences + }; + + var endpoint = LootLockerTestConfigurationEndpoints.createItemTemplate; + string json = LootLockerJson.SerializeObject(request); + + LootLockerAdminRequest.Send(endpoint.endPoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + /// + /// Grant an item template to a player. Returns the id of the created inventory item. + /// + public static void GrantItemToPlayer(int playerId, string itemTemplateId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestGrantItemResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var request = new LootLockerTestGrantItemRequest + { + player_id = playerId, + item_template_id = itemTemplateId, + count = count + }; + + var endpoint = LootLockerTestConfigurationEndpoints.grantItemTemplate; + string json = LootLockerJson.SerializeObject(request); + + LootLockerAdminRequest.Send(endpoint.endPoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void SetItemTemplateAudiences(string itemTemplateId, string[] audiences, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.setItemTemplateAudiences; + string formattedEndpoint = string.Format(endpoint.endPoint, itemTemplateId); + string json = LootLockerJson.SerializeObject(new LootLockerTestSetItemTemplateAudiencesRequest { audiences = audiences }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); + } + + public static void ListPlayerItemsAdmin(int playerId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestAdminListPlayerItemsResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminListPlayerItems; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + /// + /// Consume items through the admin API. The endpoint requires a body carrying the + /// player id; of 0 lets the backend apply its default of 1. + /// + public static void ConsumePlayerItemAdmin(int playerId, string inventoryId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestAdminConsumeItemResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminConsumePlayerItem; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerTestAdminConsumeItemRequest { player_id = playerId, count = count }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void DeletePlayerItemAdmin(int playerId, string inventoryId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminDeletePlayerItem; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete, true); + } + + public static void SplitPlayerItemStackAdmin(int playerId, string inventoryId, int count, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerTestSplitItemStackResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminSplitPlayerItemStack; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); + string json = LootLockerJson.SerializeObject(new LootLockerTestSplitItemStackRequest { count = count }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete: (serverResponse) => + { + var response = LootLockerResponse.Deserialize(serverResponse); + onComplete?.Invoke(response); + }, true); + } + + public static void MergePlayerItemStacksAdmin(int playerId, string sourceInventoryId, string targetInventoryId, Action onComplete) + { + if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) + { + onComplete?.Invoke(new LootLockerResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + return; + } + + var endpoint = LootLockerTestConfigurationEndpoints.adminMergePlayerItemStacks; + string formattedEndpoint = string.Format(endpoint.endPoint, playerId); + string json = LootLockerJson.SerializeObject(new LootLockerTestMergeItemStacksRequest + { + source_inventory_id = sourceInventoryId, + target_inventory_id = targetInventoryId + }); + + LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); + } + } + + public class LootLockerTestCreateItemTemplateRequest + { + public string name { get; set; } + public string item_type { get; set; } + public bool consumable { get; set; } + public bool deletable { get; set; } + public string[] audiences { get; set; } + } + + public class LootLockerTestItemTemplateResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestGrantItemRequest + { + public int player_id { get; set; } + public string item_template_id { get; set; } + public int count { get; set; } + } + + public class LootLockerTestGrantItemResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestSetItemTemplateAudiencesRequest + { + public string[] audiences { get; set; } + } + + public class LootLockerTestAdminConsumeItemRequest + { + public int player_id { get; set; } + public int count { get; set; } + } + + public class LootLockerTestAdminConsumeItemResponse : LootLockerResponse + { + public int consumed { get; set; } + } + + public class LootLockerTestSplitItemStackRequest + { + public int count { get; set; } + } + + public class LootLockerTestSplitItemStackResponse : LootLockerResponse + { + public string id { get; set; } + } + + public class LootLockerTestMergeItemStacksRequest + { + public string source_inventory_id { get; set; } + public string target_inventory_id { get; set; } + } + + public class LootLockerTestAdminListPlayerItemsResponse : LootLockerResponse + { + public LootLockerTestAdminPlayerItem[] items { get; set; } + } + + /// + /// An item as returned by the admin player inventory listing. Note that this shape differs + /// from the game API item: the template name is exposed as template_name and there is + /// no deletable field. + /// + public class LootLockerTestAdminPlayerItem + { + public string id { get; set; } + public int player_id { get; set; } + public string item_template_id { get; set; } + public string template_name { get; set; } + public string item_type { get; set; } + public bool consumable { get; set; } + public int count { get; set; } + public string source { get; set; } + public string created_at { get; set; } + public string updated_at { get; set; } + } +} diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta new file mode 100644 index 000000000..3bbd24fd3 --- /dev/null +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 99fdfa0f2eea476a8a97204dcb3005e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file diff --git a/Tests/LootLockerTests/PlayMode/ItemTests.cs b/Tests/LootLockerTests/PlayMode/ItemTests.cs new file mode 100644 index 000000000..f43700cfc --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/ItemTests.cs @@ -0,0 +1,512 @@ +using System; +using System.Collections; +using System.Linq; +using LootLocker; +using LootLocker.LootLockerEnums; +using LootLocker.Requests; +using LootLockerTestConfigurationUtils; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace LootLockerTests.PlayMode +{ + public class ItemTests + { + private LootLockerTestGame gameUnderTest = null; + private LootLockerConfig configCopy = null; + private static int TestCounter = 0; + private bool SetupFailed = false; + private int sessionPlayerId = 0; + private string createdTemplateId = string.Empty; + private string createdTemplateName = string.Empty; + private string grantedInventoryId = string.Empty; + + private const string PublicAudience = "public"; + + [UnitySetUp] + public IEnumerator Setup() + { + TestCounter++; + configCopy = LootLockerConfig.current; + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} setup #####"); + + if (!LootLockerConfig.ClearSettings()) + { + Debug.LogError("Could not clear LootLocker config"); + } + + bool gameCreationCallCompleted = false; + LootLockerTestGame.CreateGame(testName: this.GetType().Name + TestCounter + " ", onComplete: (success, errorMessage, game) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + gameUnderTest = game; + gameCreationCallCompleted = true; + }); + yield return new WaitUntil(() => gameCreationCallCompleted); + if (SetupFailed) + { + yield break; + } + + gameUnderTest?.SwitchToStageEnvironment(); + + bool enableGuestLoginCallCompleted = false; + gameUnderTest?.EnableGuestLogin((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + SetupFailed = true; + } + enableGuestLoginCallCompleted = true; + }); + yield return new WaitUntil(() => enableGuestLoginCallCompleted); + if (SetupFailed) + { + yield break; + } + + Assert.IsTrue(gameUnderTest?.InitializeLootLockerSDK(), "Successfully created test game and initialized LootLocker"); + + // Create a stackable, consumable, deletable template with a public audience. The + // audience is required: the game API only lists templates that have an audience row, + // so without it the template would be invisible even to the player it was granted to. + createdTemplateName = LootLockerTestItems.GetRandomItemTemplateName(); + bool createTemplateCallCompleted = false; + LootLockerTestItems.CreateItemTemplate(createdTemplateName, "stackable", true, true, new[] { PublicAudience }, (templateResponse) => + { + if (templateResponse == null || !templateResponse.success) + { + Debug.LogError("Failed to create item template: " + templateResponse?.errorData?.message); + SetupFailed = true; + createTemplateCallCompleted = true; + return; + } + + createdTemplateId = templateResponse.id; + createTemplateCallCompleted = true; + }); + yield return new WaitUntil(() => createTemplateCallCompleted); + if (SetupFailed) + { + yield break; + } + + bool guestLoginCompleted = false; + LootLockerSDKManager.StartGuestSession(Guid.NewGuid().ToString(), response => + { + SetupFailed |= !response.success; + sessionPlayerId = response.player_id; + guestLoginCompleted = true; + }); + yield return new WaitUntil(() => guestLoginCompleted); + if (SetupFailed) + { + yield break; + } + + bool grantCallCompleted = false; + LootLockerTestItems.GrantItemToPlayer(sessionPlayerId, createdTemplateId, 5, (grantResponse) => + { + if (grantResponse == null || !grantResponse.success) + { + Debug.LogError("Failed to grant item to player: " + grantResponse?.errorData?.message); + SetupFailed = true; + grantCallCompleted = true; + return; + } + + grantedInventoryId = grantResponse.id; + grantCallCompleted = true; + }); + yield return new WaitUntil(() => grantCallCompleted); + + Debug.Log($"##### Start of {this.GetType().Name} test no.{TestCounter} test case #####"); + } + + [UnityTearDown] + public IEnumerator TearDown() + { + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} test case #####"); + if (gameUnderTest != null) + { + bool gameDeletionCallCompleted = false; + gameUnderTest.DeleteGame(((success, errorMessage) => + { + if (!success) + { + Debug.LogError(errorMessage); + } + + gameUnderTest = null; + gameDeletionCallCompleted = true; + })); + yield return new WaitUntil(() => gameDeletionCallCompleted); + } + + LootLockerStateData.ClearAllSavedStates(); + LootLockerConfig.CreateNewSettings(configCopy); + Debug.Log($"##### End of {this.GetType().Name} test no.{TestCounter} tear down #####"); + } + + /// + /// The designated fast test: it covers the whole setup chain (create template with an + /// audience, grant to player) plus the central read path, so it gives early signal + /// without paying for the full suite. + /// + [UnityTest, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] + [Timeout(360_000)] + public IEnumerator Items_ListItemTemplates_ReturnsCreatedTemplate() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListItemTemplatesResponse response = null; + LootLockerSDKManager.ListItemTemplates(1, 100, (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListItemTemplates call failed"); + Assert.IsNotNull(response.items, "Item templates should not be null"); + Assert.IsTrue(response.items.Any(t => t.id == createdTemplateId), "Expected the created template to be visible to the player"); + + var template = response.items.First(t => t.id == createdTemplateId); + Assert.AreEqual(createdTemplateName, template.name, "Template name should match the created template"); + Assert.AreEqual(LootLockerItemType.stackable, template.item_type, "Template should be stackable"); + Assert.IsTrue(template.consumable, "Template should be consumable"); + Assert.IsTrue(template.deletable, "Template should be deletable"); + Assert.AreNotEqual(default(DateTime), template.created_at, "Template created_at should be populated"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ListPlayerItems_ReturnsGrantedItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse response = null; + LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListPlayerItems call failed"); + Assert.IsNotNull(response.items, "Player items should not be null"); + Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to be listed"); + + var item = response.items.First(i => i.id == grantedInventoryId); + Assert.AreEqual(5, item.count, "Granted count should be 5"); + Assert.AreEqual(createdTemplateName, item.name, "Item name should be the template name"); + Assert.AreEqual(LootLockerItemType.stackable, item.item_type, "Item should be stackable"); + Assert.IsTrue(item.deletable, "Item should be deletable"); + Assert.IsNotNull(item.source, "Item source should be populated"); + Assert.AreNotEqual(default(DateTime), item.created_at, "Item created_at should be populated"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ListPlayerItems_WithFilters_ReturnsFilteredItems() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse response = null; + LootLockerSDKManager.ListPlayerItems(1, 100, name: createdTemplateName, itemType: LootLockerItemType.stackable, + consumable: true, sort: LootLockerItemSortField.created_at, order: LootLockerSortOrder.DESC, onComplete: (r) => + { + response = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "Filtered ListPlayerItems call failed"); + Assert.IsNotNull(response.items, "Player items should not be null"); + Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to match the filters"); + Assert.IsTrue(response.items.All(i => i.item_type == LootLockerItemType.stackable), "All returned items should be stackable"); + Assert.IsTrue(response.items.All(i => i.consumable), "All returned items should be consumable"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_GetPlayerItem_ReturnsItemWithTemplate() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool getCallCompleted = false; + LootLockerGetPlayerItemResponse response = null; + LootLockerSDKManager.GetPlayerItem(grantedInventoryId, (r) => + { + response = r; + getCallCompleted = true; + }); + yield return new WaitUntil(() => getCallCompleted); + + // Then + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "GetPlayerItem call failed"); + Assert.AreEqual(grantedInventoryId, response.id, "Returned item id should match the requested id"); + Assert.AreEqual(5, response.count, "Item count should be 5"); + Assert.AreEqual(LootLockerItemType.stackable, response.item_type, "Item should be stackable"); + Assert.IsTrue(response.deletable, "Item should be deletable"); + Assert.IsNotNull(response.template, "Item template should be populated on the single item response"); + Assert.AreEqual(createdTemplateId, response.template.id, "Template id should match the created template"); + Assert.AreEqual(createdTemplateName, response.template.name, "Template name should match the created template"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ConsumePlayerItem_PartialStack_DecrementsCount() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool consumeCallCompleted = false; + LootLockerConsumeItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerItem(grantedInventoryId, 2, (r) => + { + consumeResponse = r; + consumeCallCompleted = true; + }); + yield return new WaitUntil(() => consumeCallCompleted); + + // Then + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerItem call failed"); + Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); + + bool getCallCompleted = false; + LootLockerGetPlayerItemResponse getResponse = null; + LootLockerSDKManager.GetPlayerItem(grantedInventoryId, (r) => + { + getResponse = r; + getCallCompleted = true; + }); + yield return new WaitUntil(() => getCallCompleted); + + Assert.IsTrue(getResponse.success, getResponse.errorData?.ToString() ?? "GetPlayerItem after consume failed"); + Assert.AreEqual(3, getResponse.count, "Consuming 2 of 5 should leave 3"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_ConsumePlayerItem_WholeStack_RemovesItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When - passing the full count consumes the entire row + bool consumeCallCompleted = false; + LootLockerConsumeItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerItem(grantedInventoryId, 5, (r) => + { + consumeResponse = r; + consumeCallCompleted = true; + }); + yield return new WaitUntil(() => consumeCallCompleted); + + // Then + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerItem call failed"); + Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); + + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after consume failed"); + Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Fully consumed item should no longer be listed"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_SplitPlayerItemStack_CreatesSecondStack() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool splitCallCompleted = false; + LootLockerSplitItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerItemStack(grantedInventoryId, 2, (r) => + { + splitResponse = r; + splitCallCompleted = true; + }); + yield return new WaitUntil(() => splitCallCompleted); + + // Then + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerItemStack call failed"); + Assert.IsNotNull(splitResponse.id, "Split should return the id of the new stack"); + Assert.AreNotEqual(grantedInventoryId, splitResponse.id, "The new stack should have a different id"); + + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after split failed"); + var original = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); + var split = listResponse.items.FirstOrDefault(i => i.id == splitResponse.id); + Assert.IsNotNull(original, "Original stack should still be listed"); + Assert.IsNotNull(split, "New stack should be listed"); + Assert.AreEqual(3, original.count, "Original stack should be left with 3"); + Assert.AreEqual(2, split.count, "New stack should hold 2"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_MergePlayerItemStacks_CombinesCounts() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // Given - split off a second stack to merge back + bool splitCallCompleted = false; + LootLockerSplitItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerItemStack(grantedInventoryId, 2, (r) => + { + splitResponse = r; + splitCallCompleted = true; + }); + yield return new WaitUntil(() => splitCallCompleted); + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerItemStack call failed"); + + // When - merge the new stack back into the original + bool mergeCallCompleted = false; + LootLockerResponse mergeResponse = null; + LootLockerSDKManager.MergePlayerItemStacks(splitResponse.id, grantedInventoryId, (r) => + { + mergeResponse = r; + mergeCallCompleted = true; + }); + yield return new WaitUntil(() => mergeCallCompleted); + + // Then + Assert.IsTrue(mergeResponse.success, mergeResponse.errorData?.ToString() ?? "MergePlayerItemStacks call failed"); + + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after merge failed"); + var target = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); + Assert.IsNotNull(target, "Target stack should still be listed"); + Assert.AreEqual(5, target.count, "Merged stack should hold the combined count of 5"); + Assert.IsFalse(listResponse.items.Any(i => i.id == splitResponse.id), "Source stack should be gone after merging"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_DeletePlayerItem_RemovesItem() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // When + bool deleteCallCompleted = false; + LootLockerResponse deleteResponse = null; + LootLockerSDKManager.DeletePlayerItem(grantedInventoryId, (r) => + { + deleteResponse = r; + deleteCallCompleted = true; + }); + yield return new WaitUntil(() => deleteCallCompleted); + + // Then + Assert.IsTrue(deleteResponse.success, deleteResponse.errorData?.ToString() ?? "DeletePlayerItem call failed"); + + bool listCallCompleted = false; + LootLockerListPlayerItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + { + listResponse = r; + listCallCompleted = true; + }); + yield return new WaitUntil(() => listCallCompleted); + + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after delete failed"); + Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Deleted item should no longer be listed"); + } + + [UnityTest, Category("LootLocker"), Category("LootLockerCI")] + [Timeout(360_000)] + public IEnumerator Items_DeletePlayerItem_NotDeletable_ReturnsForbidden() + { + Assert.IsFalse(SetupFailed, "Failed to setup game"); + + // Given - a second template that is explicitly not deletable + bool createTemplateCallCompleted = false; + string nonDeletableTemplateId = string.Empty; + LootLockerTestItems.CreateItemTemplate(LootLockerTestItems.GetRandomItemTemplateName(), "stackable", false, false, new[] { PublicAudience }, (templateResponse) => + { + if (templateResponse == null || !templateResponse.success) + { + Debug.LogError("Failed to create non-deletable item template: " + templateResponse?.errorData?.message); + createTemplateCallCompleted = true; + return; + } + + nonDeletableTemplateId = templateResponse.id; + createTemplateCallCompleted = true; + }); + yield return new WaitUntil(() => createTemplateCallCompleted); + Assert.IsNotEmpty(nonDeletableTemplateId, "Failed to create the non-deletable template"); + + bool grantCallCompleted = false; + string nonDeletableInventoryId = string.Empty; + LootLockerTestItems.GrantItemToPlayer(sessionPlayerId, nonDeletableTemplateId, 1, (grantResponse) => + { + if (grantResponse == null || !grantResponse.success) + { + Debug.LogError("Failed to grant non-deletable item: " + grantResponse?.errorData?.message); + grantCallCompleted = true; + return; + } + + nonDeletableInventoryId = grantResponse.id; + grantCallCompleted = true; + }); + yield return new WaitUntil(() => grantCallCompleted); + Assert.IsNotEmpty(nonDeletableInventoryId, "Failed to grant the non-deletable item"); + + // When + bool deleteCallCompleted = false; + LootLockerResponse deleteResponse = null; + LootLockerSDKManager.DeletePlayerItem(nonDeletableInventoryId, (r) => + { + deleteResponse = r; + deleteCallCompleted = true; + }); + yield return new WaitUntil(() => deleteCallCompleted); + + // Then + Assert.IsFalse(deleteResponse.success, "Deleting a non-deletable item should fail"); + Assert.AreEqual(403, deleteResponse.statusCode, "Deleting a non-deletable item should return 403 Forbidden"); + } + } +} diff --git a/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta b/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta new file mode 100644 index 000000000..354a1e1fb --- /dev/null +++ b/Tests/LootLockerTests/PlayMode/ItemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7fed9865e8244b08bd29cda5c063248a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: \ No newline at end of file From 844b00e2ed11fd0fc55dfd64887e21768fc684a8 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 30 Sep 2026 07:16:32 +0200 Subject: [PATCH 5/7] refactor(unity): rename player item APIs to player inventory items Carry the "inventory" concept through to the end user for the Assets 2.0 (Items / Item Templates) surface, and align the metadata source with the backend rename. - Metadata source `inventory` -> `inventory_item` (numeric value unchanged). - Public methods renamed, e.g. `ListPlayerItems` -> `ListPlayerInventoryItems`, `GetPlayerItem` -> `GetPlayerInventoryItem`, `ConsumePlayerItem` -> `ConsumePlayerInventoryItem`, `DeletePlayerItem` -> `DeletePlayerInventoryItem`, `SplitPlayerItemStack` -> `SplitPlayerInventoryItemStack`, `MergePlayerItemStacks` -> `MergePlayerInventoryItemStacks`, `GrantItemToPlayer` -> `GrantItemToPlayerInventory`. - DTOs renamed to match, e.g. `LootLockerItem` -> `LootLockerInventoryItem`. - Endpoint constants and the test surface renamed to match. These methods deal with items and item templates. They are unrelated to the pre-existing asset/asset-instance inventory methods (`GetInventory`, `ListPlayerInventory`, `ListCharacterInventory`, ...), which share the `player/inventory` URL prefix. Doc comments on both families now state this explicitly. Item templates are not inventory, so `ListItemTemplates` and the template, type, sort-field and reward types keep their names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Runtime/Client/LootLockerEndPoints.cs | 12 +-- Runtime/Game/LootLockerSDKManager.cs | 84 +++++++++++---- Runtime/Game/Requests/ItemRequest.cs | 23 ++-- Runtime/Game/Requests/MetadataRequests.cs | 4 +- .../LootLockerTestConfigurationEndpoints.cs | 10 +- .../LootLockerTestConfigurationItem.cs | 26 ++--- .../LootLockerTestConfigurationMetadata.cs | 4 +- Tests/LootLockerTests/PlayMode/ItemTests.cs | 100 +++++++++--------- Tests/LootLockerTests/PlayMode/JsonTests.cs | 18 ++-- 9 files changed, 166 insertions(+), 115 deletions(-) diff --git a/Runtime/Client/LootLockerEndPoints.cs b/Runtime/Client/LootLockerEndPoints.cs index c34f75b43..499611a39 100644 --- a/Runtime/Client/LootLockerEndPoints.cs +++ b/Runtime/Client/LootLockerEndPoints.cs @@ -353,12 +353,12 @@ public class LootLockerEndPoints // Items [Header("Items")] public static EndPointClass listItemTemplates = new EndPointClass("player/inventory/v1", LootLockerHTTPMethod.GET); - public static EndPointClass listPlayerItems = new EndPointClass("player/inventory/v1/my", LootLockerHTTPMethod.GET); - public static EndPointClass getPlayerItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.GET); - public static EndPointClass deletePlayerItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.DELETE); - public static EndPointClass consumePlayerItem = new EndPointClass("player/inventory/v1/{0}/consume", LootLockerHTTPMethod.POST); - public static EndPointClass splitPlayerItemStack = new EndPointClass("player/inventory/v1/{0}/split", LootLockerHTTPMethod.POST); - public static EndPointClass mergePlayerItemStacks = new EndPointClass("player/inventory/v1/merge", LootLockerHTTPMethod.POST); + public static EndPointClass listPlayerInventoryItems = new EndPointClass("player/inventory/v1/my", LootLockerHTTPMethod.GET); + public static EndPointClass getPlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.GET); + public static EndPointClass deletePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}", LootLockerHTTPMethod.DELETE); + public static EndPointClass consumePlayerInventoryItem = new EndPointClass("player/inventory/v1/{0}/consume", LootLockerHTTPMethod.POST); + public static EndPointClass splitPlayerInventoryItemStack = new EndPointClass("player/inventory/v1/{0}/split", LootLockerHTTPMethod.POST); + public static EndPointClass mergePlayerInventoryItemStacks = new EndPointClass("player/inventory/v1/merge", LootLockerHTTPMethod.POST); } [Serializable] diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index 9a19e2f44..d63b3734a 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -3476,6 +3476,11 @@ public static void ListPlayerInfo(string[] playerIdsToLookUp, int[] playerLegacy /// /// Get the players inventory. /// + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// /// Amount of assets to retrieve /// The instance ID the list should start from /// onComplete Action for handling the response of type LootLockerInventoryResponse @@ -3541,6 +3546,11 @@ public static void GetInventory(int count, Action o /// /// List player inventory with default parameters (no filters, first page, default page size). /// + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// /// 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) @@ -3564,6 +3574,11 @@ public static void ListPlayerInventoryWithDefaultParameters(ActionOptional : Page number to retrieve. /// onComplete Action for handling the response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + /// + /// Operates on assets and asset instances. For the item-based inventory, see + /// and friends, which deal with items and + /// item templates and are a separate system. + /// public static void ListPlayerInventory(LootLockerListSimplifiedInventoryRequest request, int perPage, int page, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) @@ -6629,6 +6644,10 @@ public static void DeleteAssetInstanceFromPlayerInventory(int assetInstanceID, A #region Items + // These methods operate on items and item templates. They are unrelated to the asset-based + // inventory API (GetInventory, ListPlayerInventory, ...), which deals with assets and asset + // instances. The two share a URL prefix but are separate systems. + /// /// Returns a paginated list of all visible item templates. /// @@ -6658,6 +6677,12 @@ public static void ListItemTemplates(int page, int perPage, Action /// Returns a paginated list of the current player's inventory items, including their item templates. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API (, , + /// , ), which deals with + /// assets and asset instances. The two share a URL prefix but are separate systems. + /// /// The page of items to return. /// The number of items to return per page. /// (Optional) Return only items whose template name starts with the specified value. Set to null to not use this filter. @@ -6667,15 +6692,15 @@ public static void ListItemTemplates(int page, int perPage, Action(Optional) The direction in which to sort the items. Set to null to not use this filter. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void ListPlayerItems(int page, int perPage, string name = null, LootLockerItemType? itemType = null, bool? consumable = null, LootLockerItemSortField? sort = null, LootLockerSortOrder? order = null, Action onComplete = null, string forPlayerWithUlid = null) + public static void ListPlayerInventoryItems(int page, int perPage, string name = null, LootLockerItemType? itemType = null, bool? consumable = null, LootLockerItemSortField? sort = null, LootLockerSortOrder? order = null, Action onComplete = null, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); return; } - EndPointClass endPoint = LootLockerEndPoints.listPlayerItems; + EndPointClass endPoint = LootLockerEndPoints.listPlayerInventoryItems; var queryParams = new LootLocker.Utilities.HTTP.QueryParamaterBuilder(); if (page > 0) @@ -6698,18 +6723,22 @@ public static void ListPlayerItems(int page, int perPage, string name = null, Lo /// /// Returns a single inventory item for the current player, including its item template. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// /// The id of the inventory item to fetch. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void GetPlayerItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + public static void GetPlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); return; } - EndPointClass endPoint = LootLockerEndPoints.getPlayerItem; + EndPointClass endPoint = LootLockerEndPoints.getPlayerInventoryItem; string getVariable = endPoint.WithPathParameter(inventoryId); @@ -6719,10 +6748,15 @@ public static void GetPlayerItem(string inventoryId, Action /// Deletes an inventory item from the current player's inventory. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API (), which deals with + /// assets and asset instances. + /// /// The id of the inventory item to delete. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void DeletePlayerItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + public static void DeletePlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -6730,7 +6764,7 @@ public static void DeletePlayerItem(string inventoryId, Action /// Consumes one or more items from a stackable inventory item. If is null, a single item is consumed. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// /// The id of the stackable inventory item to consume. /// (Optional) The number of items to consume. If null, the backend consumes 1. To consume the whole stack, pass the item's current count. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void ConsumePlayerItem(string inventoryId, int? count, Action onComplete, string forPlayerWithUlid = null) + public static void ConsumePlayerInventoryItem(string inventoryId, int? count, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); return; } - EndPointClass endPoint = LootLockerEndPoints.consumePlayerItem; + EndPointClass endPoint = LootLockerEndPoints.consumePlayerInventoryItem; string getVariable = endPoint.WithPathParameter(inventoryId); - string json = LootLockerJson.SerializeObject(new LootLockerConsumeItemRequest { count = count }); + string json = LootLockerJson.SerializeObject(new LootLockerConsumeInventoryItemRequest { count = count }); LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } @@ -6763,22 +6801,26 @@ public static void ConsumePlayerItem(string inventoryId, int? count, Action /// Splits a stackable inventory item into two stacks, moving items into a new stack. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// /// The id of the stackable inventory item to split. /// The number of items to move into the new stack. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void SplitPlayerItemStack(string inventoryId, int count, Action onComplete, string forPlayerWithUlid = null) + public static void SplitPlayerInventoryItemStack(string inventoryId, int count, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { - onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); + onComplete?.Invoke(LootLockerResponseFactory.SDKNotInitializedError(forPlayerWithUlid)); return; } - EndPointClass endPoint = LootLockerEndPoints.splitPlayerItemStack; + EndPointClass endPoint = LootLockerEndPoints.splitPlayerInventoryItemStack; string getVariable = endPoint.WithPathParameter(inventoryId); - string json = LootLockerJson.SerializeObject(new LootLockerSplitItemStackRequest { count = count }); + string json = LootLockerJson.SerializeObject(new LootLockerSplitInventoryItemStackRequest { count = count }); LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } @@ -6786,11 +6828,15 @@ public static void SplitPlayerItemStack(string inventoryId, int count, Action /// Merges two stacks of the same item into one. /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// /// The id of the inventory item to merge from. /// The id of the inventory item to merge into. /// Delegate for handling the server response /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. - public static void MergePlayerItemStacks(string sourceInventoryId, string targetInventoryId, Action onComplete, string forPlayerWithUlid = null) + public static void MergePlayerInventoryItemStacks(string sourceInventoryId, string targetInventoryId, Action onComplete, string forPlayerWithUlid = null) { if (!CheckInitialized(false, forPlayerWithUlid)) { @@ -6798,9 +6844,9 @@ public static void MergePlayerItemStacks(string sourceInventoryId, string target return; } - EndPointClass endPoint = LootLockerEndPoints.mergePlayerItemStacks; + EndPointClass endPoint = LootLockerEndPoints.mergePlayerInventoryItemStacks; - string json = LootLockerJson.SerializeObject(new LootLockerMergeItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }); + string json = LootLockerJson.SerializeObject(new LootLockerMergeInventoryItemStacksRequest { source_inventory_id = sourceInventoryId, target_inventory_id = targetInventoryId }); LootLockerServerRequest.CallAPI(forPlayerWithUlid, endPoint.endPoint, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } diff --git a/Runtime/Game/Requests/ItemRequest.cs b/Runtime/Game/Requests/ItemRequest.cs index b3f375259..4112d9501 100644 --- a/Runtime/Game/Requests/ItemRequest.cs +++ b/Runtime/Game/Requests/ItemRequest.cs @@ -118,7 +118,12 @@ public class LootLockerItemTemplate /// /// An item instance held in a player's inventory. /// - public class LootLockerItem + /// + /// This represents an item granted from an item template. It is unrelated to the + /// asset-based inventory API (, GetInventory, + /// ListPlayerInventory), which deals with assets and asset instances. + /// + public class LootLockerInventoryItem { /// The unique id of this inventory item. public string id { get; set; } @@ -186,7 +191,7 @@ public class LootLockerGrantedItem /// /// Request to consume a stackable item from the player's inventory. /// - public class LootLockerConsumeItemRequest + public class LootLockerConsumeInventoryItemRequest { /// /// The number of items to consume. Defaults to 1 when omitted. To consume an entire stack, @@ -205,7 +210,7 @@ public bool ShouldSerializecount() /// /// Request to split a stackable item in the player's inventory into two stacks. /// - public class LootLockerSplitItemStackRequest + public class LootLockerSplitInventoryItemStackRequest { /// The number of items to move into the new stack. public int count { get; set; } @@ -214,7 +219,7 @@ public class LootLockerSplitItemStackRequest /// /// Request to merge two stacks of the same item into one. /// - public class LootLockerMergeItemStacksRequest + public class LootLockerMergeInventoryItemStacksRequest { /// The id of the source inventory item to merge from. public string source_inventory_id { get; set; } @@ -242,10 +247,10 @@ public class LootLockerListItemTemplatesResponse : LootLockerResponse /// /// Response containing a paginated list of the player's inventory items. /// - public class LootLockerListPlayerItemsResponse : LootLockerResponse + public class LootLockerListPlayerInventoryItemsResponse : LootLockerResponse { /// The list of the player's inventory items. - public LootLockerItem[] items { get; set; } + public LootLockerInventoryItem[] items { get; set; } /// Pagination details for the response. public LootLockerExtendedPagination pagination { get; set; } @@ -254,7 +259,7 @@ public class LootLockerListPlayerItemsResponse : LootLockerResponse /// /// Response containing the player's inventory item, returned at the top level with its template and metadata. /// - public class LootLockerGetPlayerItemResponse : LootLockerResponse + public class LootLockerGetPlayerInventoryItemResponse : LootLockerResponse { /// The unique id of this inventory item. public string id { get; set; } @@ -296,7 +301,7 @@ public class LootLockerGetPlayerItemResponse : LootLockerResponse /// /// Response containing the result of consuming an item, including any items granted as a result. /// - public class LootLockerConsumeItemResponse : LootLockerResponse + public class LootLockerConsumeInventoryItemResponse : LootLockerResponse { /// True if the item was consumed. public bool consumed { get; set; } @@ -308,7 +313,7 @@ public class LootLockerConsumeItemResponse : LootLockerResponse /// /// Response containing the id of the newly created stack after splitting an item. /// - public class LootLockerSplitItemStackResponse : LootLockerResponse + public class LootLockerSplitInventoryItemStackResponse : LootLockerResponse { /// The id of the newly created item stack. public string id { get; set; } diff --git a/Runtime/Game/Requests/MetadataRequests.cs b/Runtime/Game/Requests/MetadataRequests.cs index f99d0fa0d..4a9e4fdcc 100644 --- a/Runtime/Game/Requests/MetadataRequests.cs +++ b/Runtime/Game/Requests/MetadataRequests.cs @@ -25,8 +25,8 @@ public enum LootLockerMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for legacy items, while the "inventory" source is for item instances - inventory = 9, // This is the source for item instances (player inventory items) + item = 8, // This is the source for legacy items, while the "inventory_item" source is for item instances + inventory_item = 9, // This is the source for item instances (player inventory items) item_template = 10, // This is the source for item templates }; diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs index b638d9352..c5ed49b6a 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationEndpoints.cs @@ -117,11 +117,11 @@ public class LootLockerTestConfigurationEndpoints public static EndPointClass deleteItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/{0}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); public static EndPointClass grantItemTemplate = new EndPointClass("game/#GAMEID#/templates/v1/grant", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); public static EndPointClass setItemTemplateAudiences = new EndPointClass("game/#GAMEID#/templates/v1/{0}/audiences", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); - public static EndPointClass adminListPlayerItems = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); - public static EndPointClass adminConsumePlayerItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/consume", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); - public static EndPointClass adminDeletePlayerItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); - public static EndPointClass adminSplitPlayerItemStack = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/split", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); - public static EndPointClass adminMergePlayerItemStacks = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/merge", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminListPlayerInventoryItems = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory", LootLockerHTTPMethod.GET, LootLockerCallerRole.Admin); + public static EndPointClass adminConsumePlayerInventoryItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/consume", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminDeletePlayerInventoryItem = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}", LootLockerHTTPMethod.DELETE, LootLockerCallerRole.Admin); + public static EndPointClass adminSplitPlayerInventoryItemStack = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/{1}/split", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); + public static EndPointClass adminMergePlayerInventoryItemStacks = new EndPointClass("game/#GAMEID#/templates/v1/players/{0}/inventory/merge", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); [Header("LootLocker Admin API Metadata Operations")] public static EndPointClass metadataOperations = new EndPointClass("game/#GAMEID#/metadata", LootLockerHTTPMethod.POST, LootLockerCallerRole.Admin); diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs index e6e5158bf..35c01b040 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationItem.cs @@ -98,20 +98,20 @@ public static void SetItemTemplateAudiences(string itemTemplateId, string[] audi LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, json, onComplete, true); } - public static void ListPlayerItemsAdmin(int playerId, Action onComplete) + public static void ListPlayerInventoryItemsAdmin(int playerId, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) { - onComplete?.Invoke(new LootLockerTestAdminListPlayerItemsResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); + onComplete?.Invoke(new LootLockerTestAdminListPlayerInventoryItemsResponse { success = false, errorData = new LootLockerErrorData { message = "Not logged in" } }); return; } - var endpoint = LootLockerTestConfigurationEndpoints.adminListPlayerItems; + var endpoint = LootLockerTestConfigurationEndpoints.adminListPlayerInventoryItems; string formattedEndpoint = string.Format(endpoint.endPoint, playerId); LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete: (serverResponse) => { - var response = LootLockerResponse.Deserialize(serverResponse); + var response = LootLockerResponse.Deserialize(serverResponse); onComplete?.Invoke(response); }, true); } @@ -120,7 +120,7 @@ public static void ListPlayerItemsAdmin(int playerId, Action of 0 lets the backend apply its default of 1. /// - public static void ConsumePlayerItemAdmin(int playerId, string inventoryId, int count, Action onComplete) + public static void ConsumePlayerInventoryItemAdmin(int playerId, string inventoryId, int count, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) { @@ -128,7 +128,7 @@ public static void ConsumePlayerItemAdmin(int playerId, string inventoryId, int return; } - var endpoint = LootLockerTestConfigurationEndpoints.adminConsumePlayerItem; + var endpoint = LootLockerTestConfigurationEndpoints.adminConsumePlayerInventoryItem; string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); string json = LootLockerJson.SerializeObject(new LootLockerTestAdminConsumeItemRequest { player_id = playerId, count = count }); @@ -139,7 +139,7 @@ public static void ConsumePlayerItemAdmin(int playerId, string inventoryId, int }, true); } - public static void DeletePlayerItemAdmin(int playerId, string inventoryId, Action onComplete) + public static void DeletePlayerInventoryItemAdmin(int playerId, string inventoryId, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) { @@ -147,13 +147,13 @@ public static void DeletePlayerItemAdmin(int playerId, string inventoryId, Actio return; } - var endpoint = LootLockerTestConfigurationEndpoints.adminDeletePlayerItem; + var endpoint = LootLockerTestConfigurationEndpoints.adminDeletePlayerInventoryItem; string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); LootLockerAdminRequest.Send(formattedEndpoint, endpoint.httpMethod, null, onComplete, true); } - public static void SplitPlayerItemStackAdmin(int playerId, string inventoryId, int count, Action onComplete) + public static void SplitPlayerInventoryItemStackAdmin(int playerId, string inventoryId, int count, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) { @@ -161,7 +161,7 @@ public static void SplitPlayerItemStackAdmin(int playerId, string inventoryId, i return; } - var endpoint = LootLockerTestConfigurationEndpoints.adminSplitPlayerItemStack; + var endpoint = LootLockerTestConfigurationEndpoints.adminSplitPlayerInventoryItemStack; string formattedEndpoint = string.Format(endpoint.endPoint, playerId, inventoryId); string json = LootLockerJson.SerializeObject(new LootLockerTestSplitItemStackRequest { count = count }); @@ -172,7 +172,7 @@ public static void SplitPlayerItemStackAdmin(int playerId, string inventoryId, i }, true); } - public static void MergePlayerItemStacksAdmin(int playerId, string sourceInventoryId, string targetInventoryId, Action onComplete) + public static void MergePlayerInventoryItemStacksAdmin(int playerId, string sourceInventoryId, string targetInventoryId, Action onComplete) { if (string.IsNullOrEmpty(LootLockerConfig.current.adminToken)) { @@ -180,7 +180,7 @@ public static void MergePlayerItemStacksAdmin(int playerId, string sourceInvento return; } - var endpoint = LootLockerTestConfigurationEndpoints.adminMergePlayerItemStacks; + var endpoint = LootLockerTestConfigurationEndpoints.adminMergePlayerInventoryItemStacks; string formattedEndpoint = string.Format(endpoint.endPoint, playerId); string json = LootLockerJson.SerializeObject(new LootLockerTestMergeItemStacksRequest { @@ -250,7 +250,7 @@ public class LootLockerTestMergeItemStacksRequest public string target_inventory_id { get; set; } } - public class LootLockerTestAdminListPlayerItemsResponse : LootLockerResponse + public class LootLockerTestAdminListPlayerInventoryItemsResponse : LootLockerResponse { public LootLockerTestAdminPlayerItem[] items { get; set; } } diff --git a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs index 8ff483081..2c8742877 100644 --- a/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs +++ b/Tests/LootLockerTestUtils/LootLockerTestConfigurationMetadata.cs @@ -48,8 +48,8 @@ public enum LootLockerTestMetadataSources player = 5, self = 6, asset = 7, - item = 8, // This is the source for asset instances (player inventory items), while the "asset" source is for the asset in general - inventory = 9, // This is the source for inventory items + item = 8, // This is the source for legacy items, while the "inventory_item" source is for item instances + inventory_item = 9, // This is the source for item instances (player inventory items) item_template = 10, // This is the source for item templates }; diff --git a/Tests/LootLockerTests/PlayMode/ItemTests.cs b/Tests/LootLockerTests/PlayMode/ItemTests.cs index f43700cfc..46ed96bc0 100644 --- a/Tests/LootLockerTests/PlayMode/ItemTests.cs +++ b/Tests/LootLockerTests/PlayMode/ItemTests.cs @@ -190,14 +190,14 @@ public IEnumerator Items_ListItemTemplates_ReturnsCreatedTemplate() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_ListPlayerItems_ReturnsGrantedItem() + public IEnumerator Items_ListPlayerInventoryItems_ReturnsGrantedItem() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool listCallCompleted = false; - LootLockerListPlayerItemsResponse response = null; - LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + LootLockerListPlayerInventoryItemsResponse response = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => { response = r; listCallCompleted = true; @@ -205,7 +205,7 @@ public IEnumerator Items_ListPlayerItems_ReturnsGrantedItem() yield return new WaitUntil(() => listCallCompleted); // Then - Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListPlayerItems call failed"); + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "ListPlayerInventoryItems call failed"); Assert.IsNotNull(response.items, "Player items should not be null"); Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to be listed"); @@ -220,14 +220,14 @@ public IEnumerator Items_ListPlayerItems_ReturnsGrantedItem() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_ListPlayerItems_WithFilters_ReturnsFilteredItems() + public IEnumerator Items_ListPlayerInventoryItems_WithFilters_ReturnsFilteredItems() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool listCallCompleted = false; - LootLockerListPlayerItemsResponse response = null; - LootLockerSDKManager.ListPlayerItems(1, 100, name: createdTemplateName, itemType: LootLockerItemType.stackable, + LootLockerListPlayerInventoryItemsResponse response = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, name: createdTemplateName, itemType: LootLockerItemType.stackable, consumable: true, sort: LootLockerItemSortField.created_at, order: LootLockerSortOrder.DESC, onComplete: (r) => { response = r; @@ -236,7 +236,7 @@ public IEnumerator Items_ListPlayerItems_WithFilters_ReturnsFilteredItems() yield return new WaitUntil(() => listCallCompleted); // Then - Assert.IsTrue(response.success, response.errorData?.ToString() ?? "Filtered ListPlayerItems call failed"); + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "Filtered ListPlayerInventoryItems call failed"); Assert.IsNotNull(response.items, "Player items should not be null"); Assert.IsTrue(response.items.Any(i => i.id == grantedInventoryId), "Expected the granted item to match the filters"); Assert.IsTrue(response.items.All(i => i.item_type == LootLockerItemType.stackable), "All returned items should be stackable"); @@ -245,14 +245,14 @@ public IEnumerator Items_ListPlayerItems_WithFilters_ReturnsFilteredItems() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_GetPlayerItem_ReturnsItemWithTemplate() + public IEnumerator Items_GetPlayerInventoryItem_ReturnsItemWithTemplate() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool getCallCompleted = false; - LootLockerGetPlayerItemResponse response = null; - LootLockerSDKManager.GetPlayerItem(grantedInventoryId, (r) => + LootLockerGetPlayerInventoryItemResponse response = null; + LootLockerSDKManager.GetPlayerInventoryItem(grantedInventoryId, (r) => { response = r; getCallCompleted = true; @@ -260,7 +260,7 @@ public IEnumerator Items_GetPlayerItem_ReturnsItemWithTemplate() yield return new WaitUntil(() => getCallCompleted); // Then - Assert.IsTrue(response.success, response.errorData?.ToString() ?? "GetPlayerItem call failed"); + Assert.IsTrue(response.success, response.errorData?.ToString() ?? "GetPlayerInventoryItem call failed"); Assert.AreEqual(grantedInventoryId, response.id, "Returned item id should match the requested id"); Assert.AreEqual(5, response.count, "Item count should be 5"); Assert.AreEqual(LootLockerItemType.stackable, response.item_type, "Item should be stackable"); @@ -272,14 +272,14 @@ public IEnumerator Items_GetPlayerItem_ReturnsItemWithTemplate() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_ConsumePlayerItem_PartialStack_DecrementsCount() + public IEnumerator Items_ConsumePlayerInventoryItem_PartialStack_DecrementsCount() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool consumeCallCompleted = false; - LootLockerConsumeItemResponse consumeResponse = null; - LootLockerSDKManager.ConsumePlayerItem(grantedInventoryId, 2, (r) => + LootLockerConsumeInventoryItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerInventoryItem(grantedInventoryId, 2, (r) => { consumeResponse = r; consumeCallCompleted = true; @@ -287,32 +287,32 @@ public IEnumerator Items_ConsumePlayerItem_PartialStack_DecrementsCount() yield return new WaitUntil(() => consumeCallCompleted); // Then - Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerItem call failed"); + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerInventoryItem call failed"); Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); bool getCallCompleted = false; - LootLockerGetPlayerItemResponse getResponse = null; - LootLockerSDKManager.GetPlayerItem(grantedInventoryId, (r) => + LootLockerGetPlayerInventoryItemResponse getResponse = null; + LootLockerSDKManager.GetPlayerInventoryItem(grantedInventoryId, (r) => { getResponse = r; getCallCompleted = true; }); yield return new WaitUntil(() => getCallCompleted); - Assert.IsTrue(getResponse.success, getResponse.errorData?.ToString() ?? "GetPlayerItem after consume failed"); + Assert.IsTrue(getResponse.success, getResponse.errorData?.ToString() ?? "GetPlayerInventoryItem after consume failed"); Assert.AreEqual(3, getResponse.count, "Consuming 2 of 5 should leave 3"); } [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_ConsumePlayerItem_WholeStack_RemovesItem() + public IEnumerator Items_ConsumePlayerInventoryItem_WholeStack_RemovesItem() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When - passing the full count consumes the entire row bool consumeCallCompleted = false; - LootLockerConsumeItemResponse consumeResponse = null; - LootLockerSDKManager.ConsumePlayerItem(grantedInventoryId, 5, (r) => + LootLockerConsumeInventoryItemResponse consumeResponse = null; + LootLockerSDKManager.ConsumePlayerInventoryItem(grantedInventoryId, 5, (r) => { consumeResponse = r; consumeCallCompleted = true; @@ -320,32 +320,32 @@ public IEnumerator Items_ConsumePlayerItem_WholeStack_RemovesItem() yield return new WaitUntil(() => consumeCallCompleted); // Then - Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerItem call failed"); + Assert.IsTrue(consumeResponse.success, consumeResponse.errorData?.ToString() ?? "ConsumePlayerInventoryItem call failed"); Assert.IsTrue(consumeResponse.consumed, "Item should report as consumed"); bool listCallCompleted = false; - LootLockerListPlayerItemsResponse listResponse = null; - LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => { listResponse = r; listCallCompleted = true; }); yield return new WaitUntil(() => listCallCompleted); - Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after consume failed"); + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after consume failed"); Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Fully consumed item should no longer be listed"); } [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_SplitPlayerItemStack_CreatesSecondStack() + public IEnumerator Items_SplitPlayerInventoryItemStack_CreatesSecondStack() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool splitCallCompleted = false; - LootLockerSplitItemStackResponse splitResponse = null; - LootLockerSDKManager.SplitPlayerItemStack(grantedInventoryId, 2, (r) => + LootLockerSplitInventoryItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerInventoryItemStack(grantedInventoryId, 2, (r) => { splitResponse = r; splitCallCompleted = true; @@ -353,20 +353,20 @@ public IEnumerator Items_SplitPlayerItemStack_CreatesSecondStack() yield return new WaitUntil(() => splitCallCompleted); // Then - Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerItemStack call failed"); + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerInventoryItemStack call failed"); Assert.IsNotNull(splitResponse.id, "Split should return the id of the new stack"); Assert.AreNotEqual(grantedInventoryId, splitResponse.id, "The new stack should have a different id"); bool listCallCompleted = false; - LootLockerListPlayerItemsResponse listResponse = null; - LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => { listResponse = r; listCallCompleted = true; }); yield return new WaitUntil(() => listCallCompleted); - Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after split failed"); + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after split failed"); var original = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); var split = listResponse.items.FirstOrDefault(i => i.id == splitResponse.id); Assert.IsNotNull(original, "Original stack should still be listed"); @@ -377,25 +377,25 @@ public IEnumerator Items_SplitPlayerItemStack_CreatesSecondStack() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_MergePlayerItemStacks_CombinesCounts() + public IEnumerator Items_MergePlayerInventoryItemStacks_CombinesCounts() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // Given - split off a second stack to merge back bool splitCallCompleted = false; - LootLockerSplitItemStackResponse splitResponse = null; - LootLockerSDKManager.SplitPlayerItemStack(grantedInventoryId, 2, (r) => + LootLockerSplitInventoryItemStackResponse splitResponse = null; + LootLockerSDKManager.SplitPlayerInventoryItemStack(grantedInventoryId, 2, (r) => { splitResponse = r; splitCallCompleted = true; }); yield return new WaitUntil(() => splitCallCompleted); - Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerItemStack call failed"); + Assert.IsTrue(splitResponse.success, splitResponse.errorData?.ToString() ?? "SplitPlayerInventoryItemStack call failed"); // When - merge the new stack back into the original bool mergeCallCompleted = false; LootLockerResponse mergeResponse = null; - LootLockerSDKManager.MergePlayerItemStacks(splitResponse.id, grantedInventoryId, (r) => + LootLockerSDKManager.MergePlayerInventoryItemStacks(splitResponse.id, grantedInventoryId, (r) => { mergeResponse = r; mergeCallCompleted = true; @@ -403,18 +403,18 @@ public IEnumerator Items_MergePlayerItemStacks_CombinesCounts() yield return new WaitUntil(() => mergeCallCompleted); // Then - Assert.IsTrue(mergeResponse.success, mergeResponse.errorData?.ToString() ?? "MergePlayerItemStacks call failed"); + Assert.IsTrue(mergeResponse.success, mergeResponse.errorData?.ToString() ?? "MergePlayerInventoryItemStacks call failed"); bool listCallCompleted = false; - LootLockerListPlayerItemsResponse listResponse = null; - LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => { listResponse = r; listCallCompleted = true; }); yield return new WaitUntil(() => listCallCompleted); - Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after merge failed"); + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after merge failed"); var target = listResponse.items.FirstOrDefault(i => i.id == grantedInventoryId); Assert.IsNotNull(target, "Target stack should still be listed"); Assert.AreEqual(5, target.count, "Merged stack should hold the combined count of 5"); @@ -423,14 +423,14 @@ public IEnumerator Items_MergePlayerItemStacks_CombinesCounts() [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_DeletePlayerItem_RemovesItem() + public IEnumerator Items_DeletePlayerInventoryItem_RemovesItem() { Assert.IsFalse(SetupFailed, "Failed to setup game"); // When bool deleteCallCompleted = false; LootLockerResponse deleteResponse = null; - LootLockerSDKManager.DeletePlayerItem(grantedInventoryId, (r) => + LootLockerSDKManager.DeletePlayerInventoryItem(grantedInventoryId, (r) => { deleteResponse = r; deleteCallCompleted = true; @@ -438,24 +438,24 @@ public IEnumerator Items_DeletePlayerItem_RemovesItem() yield return new WaitUntil(() => deleteCallCompleted); // Then - Assert.IsTrue(deleteResponse.success, deleteResponse.errorData?.ToString() ?? "DeletePlayerItem call failed"); + Assert.IsTrue(deleteResponse.success, deleteResponse.errorData?.ToString() ?? "DeletePlayerInventoryItem call failed"); bool listCallCompleted = false; - LootLockerListPlayerItemsResponse listResponse = null; - LootLockerSDKManager.ListPlayerItems(1, 100, onComplete: (r) => + LootLockerListPlayerInventoryItemsResponse listResponse = null; + LootLockerSDKManager.ListPlayerInventoryItems(1, 100, onComplete: (r) => { listResponse = r; listCallCompleted = true; }); yield return new WaitUntil(() => listCallCompleted); - Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerItems after delete failed"); + Assert.IsTrue(listResponse.success, listResponse.errorData?.ToString() ?? "ListPlayerInventoryItems after delete failed"); Assert.IsFalse(listResponse.items.Any(i => i.id == grantedInventoryId), "Deleted item should no longer be listed"); } [UnityTest, Category("LootLocker"), Category("LootLockerCI")] [Timeout(360_000)] - public IEnumerator Items_DeletePlayerItem_NotDeletable_ReturnsForbidden() + public IEnumerator Items_DeletePlayerInventoryItem_NotDeletable_ReturnsForbidden() { Assert.IsFalse(SetupFailed, "Failed to setup game"); @@ -497,7 +497,7 @@ public IEnumerator Items_DeletePlayerItem_NotDeletable_ReturnsForbidden() // When bool deleteCallCompleted = false; LootLockerResponse deleteResponse = null; - LootLockerSDKManager.DeletePlayerItem(nonDeletableInventoryId, (r) => + LootLockerSDKManager.DeletePlayerInventoryItem(nonDeletableInventoryId, (r) => { deleteResponse = r; deleteCallCompleted = true; diff --git a/Tests/LootLockerTests/PlayMode/JsonTests.cs b/Tests/LootLockerTests/PlayMode/JsonTests.cs index 408f6c099..f9dbc1697 100644 --- a/Tests/LootLockerTests/PlayMode/JsonTests.cs +++ b/Tests/LootLockerTests/PlayMode/JsonTests.cs @@ -320,7 +320,7 @@ public void Json_DeserializationCaseConversion_CaseIsConvertedToSnake() public void Json_SerializingConsumeItemRequestWithoutCount_OmitsCount() { // Given - var request = new LootLockerConsumeItemRequest(); + var request = new LootLockerConsumeInventoryItemRequest(); // When string serializedJson = LootLockerJson.SerializeObject(request); @@ -335,7 +335,7 @@ public void Json_SerializingConsumeItemRequestWithoutCount_OmitsCount() public void Json_SerializingConsumeItemRequestWithCount_IncludesCount() { // Given - var request = new LootLockerConsumeItemRequest { count = 3 }; + var request = new LootLockerConsumeInventoryItemRequest { count = 3 }; // When string serializedJson = LootLockerJson.SerializeObject(request); @@ -352,7 +352,7 @@ public void Json_SerializingConsumeItemRequestWithCount_IncludesCount() public void Json_SerializingSplitItemStackRequest_IncludesCount() { // Given - var request = new LootLockerSplitItemStackRequest { count = 5 }; + var request = new LootLockerSplitInventoryItemStackRequest { count = 5 }; // When string serializedJson = LootLockerJson.SerializeObject(request); @@ -369,7 +369,7 @@ public void Json_SerializingSplitItemStackRequest_IncludesCount() public void Json_SerializingMergeItemStacksRequest_IncludesBothInventoryIds() { // Given - var request = new LootLockerMergeItemStacksRequest + var request = new LootLockerMergeInventoryItemStacksRequest { source_inventory_id = "01HZZZZZZZZZZZZZZZZZZZZZZZ", target_inventory_id = "01HYYYYYYYYYYYYYYYYYYYYYYY" @@ -391,14 +391,14 @@ public void Json_SerializingMergeItemStacksRequest_IncludesBothInventoryIds() } [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] - public void Json_DeserializingListPlayerItemsResponse_Succeeds() + public void Json_DeserializingListPlayerInventoryItemsResponse_Succeeds() { // Given const string listPlayerItemsResponse = "{\"success\":true,\"items\":[{\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"stackable\",\"consumable\":true,\"count\":7,\"source\":\"grant\",\"name\":\"Health Potion\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\"}],\"pagination\":{\"total\":1,\"offset\":0,\"per_page\":25,\"last_page\":1,\"current_page\":1,\"next_page\":null,\"prev_page\":null}}"; // When - var deserialized = LootLockerJson.DeserializeObject(listPlayerItemsResponse); + var deserialized = LootLockerJson.DeserializeObject(listPlayerItemsResponse); // Then Assert.IsNotNull(deserialized, "Not deserialized, is null"); @@ -421,14 +421,14 @@ public void Json_DeserializingListPlayerItemsResponse_Succeeds() } [Test, Category("LootLocker"), Category("LootLockerCI"), Category("LootLockerCIFast")] - public void Json_DeserializingGetPlayerItemResponseWithTemplateAndMetadata_Succeeds() + public void Json_DeserializingGetPlayerInventoryItemResponseWithTemplateAndMetadata_Succeeds() { // Given const string getPlayerItemResponse = "{\"success\":true,\"id\":\"01HZZZZZZZZZZZZZZZZZZZZZZZ\",\"player_id\":3,\"item_template_id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"item_type\":\"instanced\",\"consumable\":false,\"count\":1,\"source\":\"grant\",\"deletable\":true,\"created_at\":\"2024-01-01T00:00:00Z\",\"template\":{\"id\":\"01HAAAAAAAAAAAAAAAAAAAAAAA\",\"name\":\"Sword\",\"game_id\":42,\"limited\":0,\"item_type\":\"instanced\",\"consumable\":false,\"deletable\":true},\"metadata\":[{\"key\":\"damage\",\"value\":12,\"type\":\"number\",\"access\":[\"game_api.read\"],\"tags\":[\"combat\"]}]}"; // When - var deserialized = LootLockerJson.DeserializeObject(getPlayerItemResponse); + var deserialized = LootLockerJson.DeserializeObject(getPlayerItemResponse); // Then Assert.IsNotNull(deserialized, "Not deserialized, is null"); @@ -457,7 +457,7 @@ public void Json_DeserializingConsumeItemResponseWithGrantedItems_Succeeds() "{\"success\":true,\"consumed\":true,\"granted\":[{\"source_id\":\"01HBBBBBBBBBBBBBBBBBBBBBBB\",\"count\":2,\"type\":\"currency\",\"name\":\"Gold\",\"code\":\"gold\"}]}"; // When - var deserialized = LootLockerJson.DeserializeObject(consumeItemResponse); + var deserialized = LootLockerJson.DeserializeObject(consumeItemResponse); // Then Assert.IsNotNull(deserialized, "Not deserialized, is null"); From 86db89f859b45547fc782b929f504a0692c9f7f9 Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 30 Sep 2026 07:17:00 +0200 Subject: [PATCH 6/7] fix(unity): allow insecure http in local test runs The generated test project did not set `insecureHttpOption`, so every request to the local devenv (plain http on localhost:8080) failed with `InvalidOperationException: Insecure connection not allowed`. The CI workflow sets this for `TARGET_ENVIRONMENT=LOCAL`, but the local runner script did not, which meant local functional tests could never reach the local backend. Set it when `-UseLocalEnv` is passed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/run-tests.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/scripts/run-tests.ps1 b/.github/scripts/run-tests.ps1 index fe5154e33..15d5f7139 100644 --- a/.github/scripts/run-tests.ps1 +++ b/.github/scripts/run-tests.ps1 @@ -124,6 +124,10 @@ function Initialize-TestProject { if (-not [string]::IsNullOrEmpty($scriptingDefines)) { $psContent += $nl + ' scriptingDefineSymbols:' + $nl + ' 1: ' + $scriptingDefines } + if ($UseLocalEnv) { + # The local devenv is plain http; Unity blocks insecure connections unless this is set. + $psContent += $nl + ' insecureHttpOption: 2' + } [IO.File]::WriteAllText((Join-Path $TempProject 'ProjectSettings\ProjectSettings.asset'), $psContent) } From da514a6a6570c1d9dada53df899992e06711ab7f Mon Sep 17 00:00:00 2001 From: Erik Bylund Date: Wed, 30 Sep 2026 08:58:04 +0200 Subject: [PATCH 7/7] fix(unity): support optional item consumption and local test settings Add the no-count consume overload so callers can use the documented default. Apply localhost HTTP and command-line routing settings to reused and custom Unity projects, not just freshly generated test projects. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/scripts/run-tests.ps1 | 40 ++++++++++++++++++++++++++++ Runtime/Game/LootLockerSDKManager.cs | 15 +++++++++++ 2 files changed, 55 insertions(+) diff --git a/.github/scripts/run-tests.ps1 b/.github/scripts/run-tests.ps1 index 15d5f7139..18acde9c3 100644 --- a/.github/scripts/run-tests.ps1 +++ b/.github/scripts/run-tests.ps1 @@ -148,6 +148,46 @@ if (-not [string]::IsNullOrWhiteSpace($CustomProject) -and (Test-Path $CustomPro $ProjectPath = $TempProject } +# Reused and custom projects need the same local settings as freshly initialized projects. +if ($UseLocalEnv) { + $ProjectSettingsFile = Join-Path $ProjectPath "ProjectSettings\ProjectSettings.asset" + if (Test-Path $ProjectSettingsFile) { + $nl = [char]10 + $settingsContent = [IO.File]::ReadAllText($ProjectSettingsFile) + if ($settingsContent -notmatch 'LOOTLOCKER_COMMANDLINE_SETTINGS') { + $defineProperty = [Regex]::Match($settingsContent, '(?m)^ scriptingDefineSymbols:[^\r\n]*(?:\r?\n|$)') + if ($defineProperty.Success) { + $groupOne = [Regex]::Match($settingsContent.Substring($defineProperty.Index + $defineProperty.Length), '\A(?: [^\r\n]*\r?\n)*? 1:[ \t]*[^\r\n]*') + if ($groupOne.Success) { + $start = $defineProperty.Index + $defineProperty.Length + $groupOne.Index + $newGroup = if ($groupOne.Value -match '^ 1:[ \t]*$') { + ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + } else { + $groupOne.Value + ';LOOTLOCKER_COMMANDLINE_SETTINGS' + } + $settingsContent = $settingsContent.Remove($start, $groupOne.Length).Insert($start, $newGroup) + } else { + $settingsContent = $settingsContent.Insert($defineProperty.Index + $defineProperty.Length, ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl) + } + } else { + $settingsContent = $settingsContent.TrimEnd() + $nl + ' scriptingDefineSymbols:' + $nl + ' 1: LOOTLOCKER_COMMANDLINE_SETTINGS' + $nl + } + } + if ($settingsContent -match '(?m)^[ \t]*insecureHttpOption:') { + $updatedContent = [Regex]::Replace($settingsContent, '(?m)^[ \t]*insecureHttpOption:.*$', ' insecureHttpOption: 2') + } else { + $updatedContent = $settingsContent.TrimEnd() + $nl + ' insecureHttpOption: 2' + $nl + } + if ($updatedContent -ne $settingsContent) { + [IO.File]::WriteAllText($ProjectSettingsFile, $updatedContent) + Write-Step "Enabled insecure http in $ProjectSettingsFile" + } + } else { + Write-Fail "ProjectSettings.asset not found at $ProjectSettingsFile; cannot enable local http." + exit 1 + } +} + $ResultsFile = Join-Path $ProjectPath "TestResults.xml" $LogFile = Join-Path $ProjectPath "test-run.log" diff --git a/Runtime/Game/LootLockerSDKManager.cs b/Runtime/Game/LootLockerSDKManager.cs index d63b3734a..204a42240 100644 --- a/Runtime/Game/LootLockerSDKManager.cs +++ b/Runtime/Game/LootLockerSDKManager.cs @@ -6798,6 +6798,21 @@ public static void ConsumePlayerInventoryItem(string inventoryId, int? count, Ac LootLockerServerRequest.CallAPI(forPlayerWithUlid, getVariable, endPoint.httpMethod, json, onComplete: (serverResponse) => { LootLockerResponse.Deserialize(onComplete, serverResponse); }); } + /// + /// Consumes a single item from a stackable inventory item. + /// + /// + /// Operates on items and item templates. This is unrelated to the asset-based + /// inventory API, which deals with assets and asset instances. + /// + /// The id of the stackable inventory item to consume. + /// Delegate for handling the server response + /// Optional : Execute the request for the specified player. If not supplied, the default player will be used. + public static void ConsumePlayerInventoryItem(string inventoryId, Action onComplete, string forPlayerWithUlid = null) + { + ConsumePlayerInventoryItem(inventoryId, null, onComplete, forPlayerWithUlid); + } + /// /// Splits a stackable inventory item into two stacks, moving items into a new stack. ///