From 4180df2cb17ca38f94dddb315b4e2ede7ad3e117 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Thu, 6 May 2021 23:21:16 +0900 Subject: [PATCH 1/2] Fix Material sell - Register same material - Update Inventory Tradable API - Update TradableMaterial.Equals --- .Lib9c.Tests/Action/SellCancellationTest.cs | 56 ++++-- .Lib9c.Tests/Action/SellTest.cs | 156 ++++++++-------- .Lib9c.Tests/Model/Item/InventoryTest.cs | 186 ++++++++++++++++++++ Lib9c/Action/Sell.cs | 103 ++++++----- Lib9c/Action/SellCancellation.cs | 22 ++- Lib9c/Model/Item/Inventory.cs | 112 ++++++++++-- Lib9c/Model/Item/TradableMaterial.cs | 7 +- 7 files changed, 468 insertions(+), 174 deletions(-) diff --git a/.Lib9c.Tests/Action/SellCancellationTest.cs b/.Lib9c.Tests/Action/SellCancellationTest.cs index 9bf6558fa5..1c097ebd6b 100644 --- a/.Lib9c.Tests/Action/SellCancellationTest.cs +++ b/.Lib9c.Tests/Action/SellCancellationTest.cs @@ -73,14 +73,16 @@ public SellCancellationTest(ITestOutputHelper outputHelper) } [Theory] - [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", true)] - [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", true)] - [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", false)] - [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", false)] - public void Execute(ItemType itemType, string guid, bool contain) + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", true, 1)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", true, 1)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", true, 1)] + [InlineData(ItemType.Material, "15396359-04db-68d5-f24a-d89c18665900", true, 2)] + [InlineData(ItemType.Equipment, "F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4", false, 1)] + [InlineData(ItemType.Costume, "936DA01F-9ABD-4d9d-80C7-02AF85C822A8", false, 1)] + public void Execute(ItemType itemType, string guid, bool contain, int itemCount) { var avatarState = _initialState.GetAvatarState(_avatarAddress); - INonFungibleItem nonFungibleItem; + ITradableItem tradableItem; Guid itemId = new Guid(guid); Guid productId = itemId; ItemSubType itemSubType; @@ -92,16 +94,23 @@ public void Execute(ItemType itemType, string guid, bool contain) _tableSheets.EquipmentItemSheet.First, itemId, requiredBlockIndex); - nonFungibleItem = itemUsable; + tradableItem = itemUsable; itemSubType = itemUsable.ItemSubType; } - else + else if (itemType == ItemType.Costume) { var costume = ItemFactory.CreateCostume(_tableSheets.CostumeItemSheet.First, itemId); costume.Update(requiredBlockIndex); - nonFungibleItem = costume; + tradableItem = costume; itemSubType = costume.ItemSubType; } + else + { + var material = ItemFactory.CreateTradableMaterial( + _tableSheets.MaterialItemSheet.OrderedList.First(r => r.ItemSubType == ItemSubType.Hourglass)); + itemSubType = material.ItemSubType; + tradableItem = material; + } var result = new DailyReward.DailyRewardResult() { @@ -123,12 +132,13 @@ public void Execute(ItemType itemType, string guid, bool contain) productId, new FungibleAssetValue(_goldCurrencyState.Currency, 100, 0), requiredBlockIndex, - nonFungibleItem); + tradableItem, + itemCount); if (contain) { shopState.Register(shopItem); - avatarState.inventory.AddItem((ItemBase)nonFungibleItem); + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); Assert.Empty(legacyShopState.Products); Assert.Single(shopState.Products); } @@ -139,8 +149,11 @@ public void Execute(ItemType itemType, string guid, bool contain) Assert.Empty(shopState.Products); } - Assert.Equal(requiredBlockIndex, nonFungibleItem.RequiredBlockIndex); - Assert.Equal(contain, avatarState.inventory.TryGetNonFungibleItem(itemId, out _)); + Assert.Equal(requiredBlockIndex, tradableItem.RequiredBlockIndex); + Assert.Equal( + contain, + avatarState.inventory.TryGetTradableItems(itemId, requiredBlockIndex, itemCount, out _) + ); IAccountStateDelta prevState = _initialState .SetState(_avatarAddress, avatarState.Serialize()) @@ -166,8 +179,21 @@ public void Execute(ItemType itemType, string guid, bool contain) Assert.Empty(nextShopState.Products); var nextAvatarState = nextState.GetAvatarState(_avatarAddress); - Assert.True(nextAvatarState.inventory.TryGetNonFungibleItem(itemId, out INonFungibleItem nextNonFungibleItem)); - Assert.Equal(1, nextNonFungibleItem.RequiredBlockIndex); + Assert.False(nextAvatarState.inventory.TryGetTradableItems( + itemId, + requiredBlockIndex, + itemCount, + out List _ + )); + Assert.True(nextAvatarState.inventory.TryGetTradableItems( + itemId, + 1, + itemCount, + out List inventoryItems + )); + Assert.Single(inventoryItems); + ITradableItem nextTradableItem = (ITradableItem)inventoryItems.First().item; + Assert.Equal(1, nextTradableItem.RequiredBlockIndex); Assert.Equal(30, nextAvatarState.mailBox.Count); ShopState nextLegacyShopState = nextState.GetShopState(); Assert.Empty(nextLegacyShopState.Products); diff --git a/.Lib9c.Tests/Action/SellTest.cs b/.Lib9c.Tests/Action/SellTest.cs index a7ff8907e8..faabb464a6 100644 --- a/.Lib9c.Tests/Action/SellTest.cs +++ b/.Lib9c.Tests/Action/SellTest.cs @@ -69,28 +69,6 @@ public SellTest(ITestOutputHelper outputHelper) }; agentState.avatarAddresses[0] = _avatarAddress; - var equipment = ItemFactory.CreateItemUsable( - _tableSheets.EquipmentItemSheet.First, - Guid.NewGuid(), - 0); - _avatarState.inventory.AddItem(equipment); - - var consumable = ItemFactory.CreateItemUsable( - _tableSheets.ConsumableItemSheet.First, - Guid.NewGuid(), - 0); - _avatarState.inventory.AddItem(consumable); - - var costume = ItemFactory.CreateCostume( - _tableSheets.CostumeItemSheet.First, - Guid.NewGuid()); - _avatarState.inventory.AddItem(costume); - - var tradableMaterialRow = _tableSheets.MaterialItemSheet.OrderedList - .FirstOrDefault(row => row.ItemSubType == ItemSubType.Hourglass); - var tradableMaterial = ItemFactory.CreateTradableMaterial(tradableMaterialRow); - _avatarState.inventory.AddItem(tradableMaterial); - _initialState = _initialState .SetState(GoldCurrencyState.Address, goldCurrencyState.Serialize()) .SetState(Addresses.Shop, shopState.Serialize()) @@ -99,26 +77,64 @@ public SellTest(ITestOutputHelper outputHelper) } [Theory] - [InlineData(ItemType.Consumable, true, 2)] - [InlineData(ItemType.Costume, true, 2)] - [InlineData(ItemType.Equipment, true, 2)] - [InlineData(ItemType.Material, true, 2)] - [InlineData(ItemType.Consumable, false, 0)] - [InlineData(ItemType.Costume, false, 0)] - [InlineData(ItemType.Equipment, false, 0)] - [InlineData(ItemType.Material, false, 0)] - public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) + [InlineData(ItemType.Consumable, true, 2, 1, 1, 1)] + [InlineData(ItemType.Costume, true, 2, 1, 1, 1)] + [InlineData(ItemType.Equipment, true, 2, 1, 1, 1)] + [InlineData(ItemType.Consumable, false, 0, 1, 1, 1)] + [InlineData(ItemType.Costume, false, 0, 1, 1, 1)] + [InlineData(ItemType.Equipment, false, 0, 1, 1, 1)] + [InlineData(ItemType.Material, true, 1, 2, 1, 1)] + [InlineData(ItemType.Material, true, 1, 1, 2, 1)] + [InlineData(ItemType.Material, true, 2, 1, 2, 2)] + [InlineData(ItemType.Material, true, 3, 2, 2, 2)] + [InlineData(ItemType.Material, false, 1, 1, 1, 1)] + public void Execute( + ItemType itemType, + bool shopItemExist, + long blockIndex, + int itemCount, + int prevCount, + int expectedProductsCount + ) { var avatarState = _initialState.GetAvatarState(_avatarAddress); - var inventoryItems = avatarState.inventory.Items - .Where(i => i.item.ItemType == itemType) - .ToList(); - Assert.NotEmpty(inventoryItems); + + ITradableItem tradableItem; + switch (itemType) + { + case ItemType.Consumable: + tradableItem = ItemFactory.CreateItemUsable( + _tableSheets.ConsumableItemSheet.First, + Guid.NewGuid(), + 0); + break; + case ItemType.Costume: + tradableItem = ItemFactory.CreateCostume( + _tableSheets.CostumeItemSheet.First, + Guid.NewGuid()); + break; + case ItemType.Equipment: + tradableItem = ItemFactory.CreateItemUsable( + _tableSheets.EquipmentItemSheet.First, + Guid.NewGuid(), + 0); + break; + case ItemType.Material: + var tradableMaterialRow = _tableSheets.MaterialItemSheet.OrderedList + .First(row => row.ItemSubType == ItemSubType.Hourglass); + tradableItem = ItemFactory.CreateTradableMaterial(tradableMaterialRow); + break; + default: + throw new ArgumentOutOfRangeException(nameof(itemType), itemType, null); + } + + Assert.Equal(0, tradableItem.RequiredBlockIndex); + avatarState.inventory.AddItem((ItemBase)tradableItem, itemCount); var previousStates = _initialState; + previousStates = previousStates.SetState(_avatarAddress, avatarState.Serialize()); var currencyState = previousStates.GetGoldCurrency(); var price = new FungibleAssetValue(currencyState, ProductPrice, 0); - var tradableItem = (ITradableItem)inventoryItems.First().item; var productId = new Guid("6f460c1a755d48e4ad6765d5f519dbc8"); var shardedShopAddress = ShardedShopState.DeriveAddress( tradableItem.ItemSubType, @@ -130,10 +146,12 @@ public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) var shopItem = new ShopItem( _agentAddress, _avatarAddress, - productId, + expectedProductsCount == 2 ? Guid.NewGuid() : productId, price, blockIndex, - tradableItem); + tradableItem, + prevCount + ); var shardedShopState = new ShardedShopState(shardedShopAddress); shardedShopState.Register(shopItem); @@ -151,7 +169,7 @@ public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) { sellerAvatarAddress = _avatarAddress, tradableId = tradableItem.TradableId, - count = 1, + count = itemCount, price = price, itemSubType = tradableItem.ItemSubType, }; @@ -168,11 +186,14 @@ public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) // Check AvatarState and Inventory var nextAvatarState = nextState.GetAvatarState(_avatarAddress); - Assert.True(nextAvatarState.inventory.TryGetTradableItem( + Assert.Single(nextAvatarState.inventory.Items); + Assert.True(nextAvatarState.inventory.TryGetTradableItems( tradableItem.TradableId, - out var nextInventoryItem)); - var nextTradableItem = nextInventoryItem.item as ITradableItem; - Assert.NotNull(nextTradableItem); + expiredBlockIndex, + 1, + out var inventoryItems)); + Assert.Single(inventoryItems); + ITradableItem nextTradableItem = (ITradableItem)inventoryItems.First().item; Assert.Equal(expiredBlockIndex, nextTradableItem.RequiredBlockIndex); // Check ShardedShopState and ShopItem @@ -180,9 +201,9 @@ public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) Assert.NotNull(nextSerializedShardedShopState); var nextShardedShopState = new ShardedShopState((Dictionary)nextSerializedShardedShopState); - Assert.Single(nextShardedShopState.Products); + Assert.Equal(expectedProductsCount, nextShardedShopState.Products.Count); - var nextShopItem = nextShardedShopState.Products.Values.First(); + var nextShopItem = nextShardedShopState.Products.Values.First(s => s.ExpiredBlockIndex == expiredBlockIndex); ITradableItem nextTradableItemInShopItem; switch (itemType) { @@ -211,25 +232,34 @@ public void Execute(ItemType itemType, bool shopItemExist, int blockIndex) var mail = mailList.First() as SellCancelMail; Assert.NotNull(mail); Assert.Equal(expiredBlockIndex, mail.requiredBlockIndex); + + ITradableItem attachmentItem; + int attachmentCount = 0; switch (itemType) { case ItemType.Consumable: case ItemType.Equipment: Assert.NotNull(mail.attachment.itemUsable); + attachmentItem = mail.attachment.itemUsable; Assert.Equal(tradableItem, mail.attachment.itemUsable); break; case ItemType.Costume: Assert.NotNull(mail.attachment.costume); + attachmentItem = mail.attachment.costume; Assert.Equal(tradableItem, mail.attachment.costume); break; case ItemType.Material: Assert.NotNull(mail.attachment.tradableFungibleItem); - Assert.Equal(tradableItem, mail.attachment.tradableFungibleItem); - Assert.True(mail.attachment.tradableFungibleItemCount == 1); + attachmentItem = mail.attachment.tradableFungibleItem; + attachmentCount = mail.attachment.tradableFungibleItemCount; break; default: throw new ArgumentOutOfRangeException(nameof(itemType), itemType, null); } + + Assert.Equal(attachmentCount, nextShopItem.TradableFungibleItemCount); + Assert.Equal(nextTradableItem, attachmentItem); + Assert.Equal(nextTradableItemInShopItem, attachmentItem); } [Fact] @@ -347,37 +377,7 @@ public void Execute_Throw_InvalidItemTypeException() Assert.Throws(() => action.Execute(new ActionContext { - BlockIndex = 0, - PreviousStates = _initialState, - Signer = _agentAddress, - Random = new TestRandom(), - })); - } - - [Fact] - public void Execute_Throw_RequiredBlockIndexException() - { - var equipmentId = Guid.NewGuid(); - var equipment = ItemFactory.CreateItemUsable( - _tableSheets.EquipmentItemSheet.First, - equipmentId, - 10); - _avatarState.inventory.AddItem(equipment); - - _initialState = _initialState.SetState(_avatarAddress, _avatarState.Serialize()); - - var action = new Sell - { - sellerAvatarAddress = _avatarAddress, - tradableId = equipmentId, - count = 1, - price = 0 * _currency, - itemSubType = equipment.ItemSubType, - }; - - Assert.Throws(() => action.Execute(new ActionContext - { - BlockIndex = 0, + BlockIndex = 11, PreviousStates = _initialState, Signer = _agentAddress, Random = new TestRandom(), diff --git a/.Lib9c.Tests/Model/Item/InventoryTest.cs b/.Lib9c.Tests/Model/Item/InventoryTest.cs index bf9a852b0d..6a378f9395 100644 --- a/.Lib9c.Tests/Model/Item/InventoryTest.cs +++ b/.Lib9c.Tests/Model/Item/InventoryTest.cs @@ -1,10 +1,13 @@ namespace Lib9c.Tests.Model.Item { using System; + using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; + using Lib9c.Tests.Action; using Nekoyume.Model.Item; + using Nekoyume.TableData; using Xunit; using BxList = Bencodex.Types.List; @@ -133,5 +136,188 @@ public void RemoveTradableFungibleItem() Assert.False(inventory.RemoveTradableFungibleItem(row.ItemId)); Assert.Single(inventory.Materials); } + + [Fact] + public void AddItem_TradableMaterial() + { + var row = TableSheets.MaterialItemSheet.First; + Assert.NotNull(row); + var tradableMaterial = ItemFactory.CreateTradableMaterial(row); + tradableMaterial.RequiredBlockIndex = 0; + var inventory = new Inventory(); + Assert.Empty(inventory.Items); + + inventory.AddItem(tradableMaterial); + Assert.Single(inventory.Items); + Assert.Single(inventory.Materials); + + // Add new tradable material + var tradableMaterial2 = ItemFactory.CreateTradableMaterial(row); + tradableMaterial2.RequiredBlockIndex = 1; + inventory.AddItem(tradableMaterial2); + Assert.Equal(2, inventory.Items.Count); + Assert.Equal(2, inventory.Materials.Count()); + Assert.True(inventory.HasItem(row.Id, 2)); + } + + [Theory] + [InlineData(ItemType.Equipment, 1)] + [InlineData(ItemType.Costume, 2)] + [InlineData(ItemType.Consumable, 3)] + [InlineData(ItemType.Material, 4)] + public void HasItem(ItemType itemType, int itemCount) + { + ItemSheet.Row row = null; + var inventory = new Inventory(); + switch (itemType) + { + case ItemType.Consumable: + row = TableSheets.EquipmentItemSheet.First; + break; + case ItemType.Costume: + row = TableSheets.CostumeItemSheet.First; + break; + case ItemType.Equipment: + row = TableSheets.ConsumableItemSheet.First; + break; + case ItemType.Material: + row = TableSheets.MaterialItemSheet.First; + break; + } + + Assert.NotNull(row); + for (int i = 0; i < itemCount; i++) + { + inventory.AddItem(ItemFactory.CreateItem(row, new TestRandom())); + } + + Assert.True(inventory.HasItem(row.Id, itemCount)); + } + + [Theory] + [InlineData(3, 3, 1)] + [InlineData(3, 2, 2)] + [InlineData(3, 1, 3)] + public void SellItem_Material_Multiple_Slot(int totalCount, int sellCount, int expectedCount) + { + var inventory = new Inventory(); + var row = TableSheets.MaterialItemSheet.First; + Assert.NotNull(row); + for (int i = 1; i < totalCount + 1; i++) + { + var tradableItem = ItemFactory.CreateTradableMaterial(row); + tradableItem.RequiredBlockIndex = i; + inventory.AddItem(tradableItem, 1); + Assert.True(inventory.TryGetTradableItems(tradableItem.TradableId, i, i, out _)); + } + + Assert.Equal(totalCount, inventory.Items.Count); + Assert.True(inventory.HasItem(row.Id, totalCount)); + inventory.SellItem(TradableMaterial.DeriveTradableId(row.ItemId), totalCount, sellCount); + Assert.Equal(expectedCount, inventory.Items.Count); + } + + [Theory] + [InlineData(ItemType.Equipment, 1, 1, 1)] + [InlineData(ItemType.Costume, 1, 1, 1)] + [InlineData(ItemType.Consumable, 1, 1, 1)] + [InlineData(ItemType.Material, 1, 1, 1)] + [InlineData(ItemType.Material, 2, 1, 2)] + public void SellItem(ItemType itemType, int itemCount, int sellCount, int inventoryCount) + { + ItemSheet.Row row; + switch (itemType) + { + case ItemType.Consumable: + row = TableSheets.EquipmentItemSheet.First; + break; + case ItemType.Costume: + row = TableSheets.CostumeItemSheet.First; + break; + case ItemType.Equipment: + row = TableSheets.ConsumableItemSheet.First; + break; + case ItemType.Material: + row = TableSheets.MaterialItemSheet.First; + break; + default: + throw new Exception(); + } + + var inventory = new Inventory(); + ITradableItem tradableItem; + if (itemType == ItemType.Material) + { + tradableItem = ItemFactory.CreateTradableMaterial((MaterialItemSheet.Row)row); + } + else + { + tradableItem = (ITradableItem)ItemFactory.CreateItem(row, new TestRandom()); + } + + inventory.AddItem((ItemBase)tradableItem, itemCount); + Assert.Single(inventory.Items); + inventory.SellItem(tradableItem.TradableId, 1, sellCount); + Assert.Equal(inventoryCount, inventory.Items.Count); + } + + [Theory] + [InlineData(ItemType.Equipment, 0, true)] + [InlineData(ItemType.Costume, 0, true)] + [InlineData(ItemType.Consumable, 0, true)] + [InlineData(ItemType.Material, 0, true)] + [InlineData(ItemType.Equipment, 1, false)] + [InlineData(ItemType.Costume, 1, false)] + [InlineData(ItemType.Consumable, 1, false)] + [InlineData(ItemType.Material, 1, false)] + public void TryGetTradableItems(ItemType itemType, long blockIndex, bool expected) + { + ItemSheet.Row row; + switch (itemType) + { + case ItemType.Consumable: + row = TableSheets.EquipmentItemSheet.First; + break; + case ItemType.Costume: + row = TableSheets.CostumeItemSheet.First; + break; + case ItemType.Equipment: + row = TableSheets.ConsumableItemSheet.First; + break; + case ItemType.Material: + row = TableSheets.MaterialItemSheet.First; + break; + default: + throw new Exception(); + } + + var inventory = new Inventory(); + ITradableItem tradableItem; + if (itemType == ItemType.Material) + { + tradableItem = (ITradableItem)ItemFactory.CreateTradableMaterial((MaterialItemSheet.Row)row); + } + else + { + tradableItem = (ITradableItem)ItemFactory.CreateItem(row, new TestRandom()); + } + + tradableItem.RequiredBlockIndex = blockIndex; + inventory.AddItem((ItemBase)tradableItem, 1); + Assert.Single(inventory.Items); + Assert.Equal( + expected, + inventory.TryGetTradableItems(tradableItem.TradableId, 0, 1, out List items) + ); + if (expected) + { + Assert.Single(items); + Assert.Equal((ITradableItem)items.First().item, tradableItem); + } + else + { + Assert.Empty(items); + } + } } } diff --git a/Lib9c/Action/Sell.cs b/Lib9c/Action/Sell.cs index d9bdc0f60e..fa8eb1e2bf 100644 --- a/Lib9c/Action/Sell.cs +++ b/Lib9c/Action/Sell.cs @@ -10,6 +10,7 @@ using Nekoyume.Model.Item; using Nekoyume.Model.Mail; using Nekoyume.Model.State; +using Nekoyume.TableData; using Serilog; using BxDictionary = Bencodex.Types.Dictionary; using BxList = Bencodex.Types.List; @@ -123,45 +124,35 @@ public override IAccountStateDelta Execute(IActionContext context) $"{addressesHex}Aborted because {nameof(count)}({count}) should be greater than or equal to 1."); } - if (!avatarState.inventory.TryGetTradableItem( - tradableId, - out var inventoryItem) || - !(inventoryItem.item is ITradableItem tradableItem)) + if (!avatarState.inventory.TryGetTradableItems(tradableId, context.BlockIndex, count, out List inventoryItems)) { throw new ItemDoesNotExistException( $"{addressesHex}Aborted because the tradable item({tradableId}) was failed to load from avatar's inventory."); } - if (inventoryItem.count < count) - { - throw new ItemDoesNotExistException( - $"{addressesHex}Aborted because inventory item count({inventoryItem.count}) should be greater than or equal to {nameof(count)}({count})."); - } - - if (!tradableItem.ItemSubType.Equals(itemSubType)) - { - throw new InvalidItemTypeException( - $"{addressesHex}Expected ItemSubType: {tradableItem.ItemSubType}. Actual ItemSubType: {itemSubType}"); - } - - switch (tradableItem) - { - case INonFungibleItem _ when count != 1: - throw new ArgumentOutOfRangeException( - $"{addressesHex}Aborted because {nameof(count)}({count}) should be 1 because {nameof(tradableId)}({tradableId}) is non-fungible item."); - case INonFungibleItem nonFungibleItem when nonFungibleItem.RequiredBlockIndex > context.BlockIndex: - throw new RequiredBlockIndexException( - $"{addressesHex}Aborted because the non-fungible item({tradableId}) to sell is not available yet; it will be available at the block #{nonFungibleItem.RequiredBlockIndex}."); - } - + IEnumerable tradableItems = inventoryItems.Select(i => (ITradableItem)i.item).ToList(); var expiredBlockIndex = context.BlockIndex + ExpiredBlockIndex; - tradableItem.RequiredBlockIndex = expiredBlockIndex; - if (tradableItem is IEquippableItem equippableItem) + foreach (var ti in tradableItems) { - equippableItem.Unequip(); + if (!ti.ItemSubType.Equals(itemSubType)) + { + throw new InvalidItemTypeException( + $"{addressesHex}Expected ItemSubType: {ti.ItemSubType}. Actual ItemSubType: {itemSubType}"); + } + + if (ti is INonFungibleItem) + { + if (count != 1) + { + throw new ArgumentOutOfRangeException( + $"{addressesHex}Aborted because {nameof(count)}({count}) should be 1 because {nameof(tradableId)}({tradableId}) is non-fungible item."); + } + } } + ITradableItem tradableItem = avatarState.inventory.SellItem(tradableId, context.BlockIndex, count); + var productId = context.Random.GenerateRandomGuid(); var shardedShopAddress = ShardedShopState.DeriveAddress(itemSubType, productId); if (!states.TryGetState(shardedShopAddress, out BxDictionary serializedSharedShopState)) @@ -204,30 +195,27 @@ public override IAccountStateDelta Execute(IActionContext context) } BxDictionary serializedProductDictionary; - switch (tradableItem.ItemType) + if (tradableItem.ItemType == ItemType.Material) { - case ItemType.Consumable: - case ItemType.Costume: - case ItemType.Equipment: - var serializedTradeId = tradableItem.TradableId.Serialize(); - serializedProductDictionary = serializedProductList - .Select(p => (BxDictionary) p) - .FirstOrDefault(p => - ((BxDictionary) p[productKey])[itemIdKey].Equals(serializedTradeId)); - break; - case ItemType.Material: - serializedProductDictionary = serializedProductList - .Select(p => (BxDictionary) p) - .FirstOrDefault(p => - { - var materialItemId = - ((BxDictionary) p[productKey])[itemIdKey].ToItemId(); - return TradableMaterial.DeriveTradableId(materialItemId) - .Equals(tradableItem.TradableId); - }); - break; - default: - throw new ArgumentOutOfRangeException(); + // Find expired TradableMaterial + serializedProductDictionary = serializedProductList + .Select(p => (BxDictionary) p) + .FirstOrDefault(p => + { + var materialItemId = + ((BxDictionary) p[productKey])[itemIdKey].ToItemId(); + var requiredBlockIndex = p[ExpiredBlockIndexKey].ToLong(); + return TradableMaterial.DeriveTradableId(materialItemId) + .Equals(tradableItem.TradableId) && requiredBlockIndex <= context.BlockIndex; + }); + } + else + { + var serializedTradeId = tradableItem.TradableId.Serialize(); + serializedProductDictionary = serializedProductList + .Select(p => (BxDictionary) p) + .FirstOrDefault(p => + ((BxDictionary) p[productKey])[itemIdKey].Equals(serializedTradeId)); } ShopItem shopItem; @@ -255,13 +243,20 @@ public override IAccountStateDelta Execute(IActionContext context) // Update ITradableItem.RequiredBlockIndex var inChainShopItem = (BxDictionary) serializedProductDictionary[productKey]; inChainShopItem = inChainShopItem - .SetItem(requiredBlockIndexKey, expiredBlockIndex.Serialize()) - .SetItem(TradableFungibleItemCountKey, count.Serialize()); + .SetItem(requiredBlockIndexKey, expiredBlockIndex.Serialize()); // Update ShopItem.ExpiredBlockIndex serializedProductDictionary = serializedProductDictionary .SetItem(ExpiredBlockIndexKey, expiredBlockIndex.Serialize()) .SetItem(productKey, inChainShopItem); + + // Update only Material for backwardCompatible. + if (tradableItem.ItemType == ItemType.Material) + { + serializedProductDictionary = serializedProductDictionary + .SetItem(TradableFungibleItemCountKey, count.Serialize()); + } + serializedProductList = serializedProductList.Add(serializedProductDictionary); shopItem = new ShopItem(serializedProductDictionary); } diff --git a/Lib9c/Action/SellCancellation.cs b/Lib9c/Action/SellCancellation.cs index bb44fe97b3..fa0dbcfc90 100644 --- a/Lib9c/Action/SellCancellation.cs +++ b/Lib9c/Action/SellCancellation.cs @@ -122,6 +122,11 @@ public override IAccountStateDelta Execute(IActionContext context) var backwardCompatible = false; if (productSerialized.Equals(BxDictionary.Empty)) { + if (itemSubType == ItemSubType.Hourglass || itemSubType == ItemSubType.ApStone) + { + throw new ItemDoesNotExistException( + $"{addressesHex}Aborted as the shop item ({productId}) could not be found from the shop."); + } // Backward compatibility. var rawShop = states.GetState(Addresses.Shop); if (!(rawShop is null)) @@ -162,7 +167,7 @@ public override IAccountStateDelta Execute(IActionContext context) } ITradableItem tradableItem; - var tradableItemCount = 1; + int itemCount = 1; if (!(shopItem.ItemUsable is null)) { tradableItem = shopItem.ItemUsable; @@ -174,25 +179,26 @@ public override IAccountStateDelta Execute(IActionContext context) else if (!(shopItem.TradableFungibleItem is null)) { tradableItem = shopItem.TradableFungibleItem; - tradableItemCount = shopItem.TradableFungibleItemCount; + itemCount = shopItem.TradableFungibleItemCount; } else { - throw new Exception(); + throw new InvalidShopItemException($"{addressesHex}Tradable Item is null."); } - if (avatarState.inventory.TryGetTradableItem( + if (avatarState.inventory.TryGetTradableItems( tradableItem.TradableId, - out var inventoryItem) && - inventoryItem.item is INonFungibleItem nonFungibleItemInInventory) + tradableItem.RequiredBlockIndex, + itemCount, + out var tradableItems)) { - nonFungibleItemInInventory.RequiredBlockIndex = context.BlockIndex; + ITradableItem tradableItemInInventory = (ITradableItem) tradableItems.First().item; + tradableItemInInventory.RequiredBlockIndex = context.BlockIndex; } if (tradableItem is INonFungibleItem nonFungibleItem) { nonFungibleItem.RequiredBlockIndex = context.BlockIndex; - if (backwardCompatible) { switch (nonFungibleItem) diff --git a/Lib9c/Model/Item/Inventory.cs b/Lib9c/Model/Item/Inventory.cs index 3d0c714a38..e7446c79c9 100644 --- a/Lib9c/Model/Item/Inventory.cs +++ b/Lib9c/Model/Item/Inventory.cs @@ -377,22 +377,43 @@ public bool TryGetNonFungibleItem(Guid itemId, out T outNonFungibleItem) return false; } - public bool TryGetTradableItem(Guid tradeId, out Item outItem) + public bool TryGetTradableItems(Guid tradeId, long blockIndex, int count, out List outItem) { - foreach (var item in _items) + outItem = new List(); + List items = _items + .Where(i => + i.item is ITradableItem item && + item.TradableId.Equals(tradeId) && + item.RequiredBlockIndex <= blockIndex + ) + .OrderBy(i => ((ITradableItem)i.item).RequiredBlockIndex) + .ThenBy(i => i.count) + .ToList(); + int totalCount = items.Sum(i => i.count); + if (totalCount < count) + { + return false; + } + + foreach (var item in items) { - if (!(item.item is ITradableItem tradableItem) || - !tradableItem.TradableId.Equals(tradeId)) + outItem.Add(item); + count -= item.count; + if (count < 0) { - continue; + break; } - - outItem = item; - return true; } + return true; - outItem = null; - return false; + // outItem = _items + // .Where(i => i.count >= count) + // .Select(i => i.item) + // .OfType() + // .Where(t => t.TradableId.Equals(tradeId) && t.RequiredBlockIndex <= blockIndex) + // .OrderBy(r => r.RequiredBlockIndex) + // .FirstOrDefault(); + // return !(outItem is null); } // public bool TryGetTradableItemWithoutNonTradableFungibleItem( @@ -447,9 +468,9 @@ public bool TryGetTradableItem(Guid tradeId, out Item outItem) #region Has public bool HasItem(int rowId, int count = 1) => _items - .Exists(item => - item.item.Id == rowId && - item.count >= count); + .Where(item => + item.item.Id == rowId + ).Sum(item => item.count) >= count; public bool HasFungibleItem(HashDigest fungibleId, int count = 1) => _items .Exists(item => @@ -462,10 +483,12 @@ public bool HasNonFungibleItem(Guid nonFungibleId) => _items .OfType() .Any(i => i.NonFungibleId.Equals(nonFungibleId)); - public bool HasTradableItem(Guid tradableId) => _items - .Select(i => i.item) - .OfType() - .Any(i => i.TradableId.Equals(tradableId)); + public bool HasTradableItem(Guid tradableId, long blockIndex, int count) => _items + .Where(i => + i.item is ITradableItem tradableItem && + tradableItem.TradableId.Equals(tradableId) && + tradableItem.RequiredBlockIndex <= blockIndex) + .Sum(i => i.count) >= count; #endregion @@ -505,5 +528,60 @@ public bool HasNotification(int level, long blockIndex) return false; } + + public ITradableItem SellItem(Guid tradableId, long blockIndex, int count) + { + if (TryGetTradableItems(tradableId, blockIndex, count, out List items)) + { + int remain = count; + long requiredBlockIndex = blockIndex + Sell.ExpiredBlockIndex; + for (int i = 0; i < items.Count; i++) + { + Item item = items[i]; + if (item.count > remain) + { + item.count -= remain; + break; + } + + if (item.count <= remain) + { + _items.Remove(item); + remain -= item.count; + } + + if (remain <= 0) + { + break; + } + } + + ITradableItem tradableItem = (ITradableItem) items.First().item; + if (tradableItem is IEquippableItem equippableItem) + { + equippableItem.Unequip(); + } + + // Copy new TradableMaterial + if (items.First().item is TradableMaterial tradableMaterial) + { + var material = new TradableMaterial((Dictionary) tradableMaterial.Serialize()) + { + RequiredBlockIndex = requiredBlockIndex + }; + AddItem(material, count); + return material; + } + else + { + tradableItem.RequiredBlockIndex = requiredBlockIndex; + AddItem((ItemBase)tradableItem, count); + return tradableItem; + } + + } + + throw new ItemDoesNotExistException(tradableId.ToString()); + } } } diff --git a/Lib9c/Model/Item/TradableMaterial.cs b/Lib9c/Model/Item/TradableMaterial.cs index 2d9383fb2a..aa2476d4cb 100644 --- a/Lib9c/Model/Item/TradableMaterial.cs +++ b/Lib9c/Model/Item/TradableMaterial.cs @@ -54,7 +54,7 @@ protected TradableMaterial(SerializationInfo info, StreamingContext _) protected bool Equals(TradableMaterial other) { - return base.Equals(other) && TradableId.Equals(other.TradableId); + return base.Equals(other) && RequiredBlockIndex == other.RequiredBlockIndex && TradableId.Equals(other.TradableId); } public override bool Equals(object obj) @@ -69,7 +69,10 @@ public override int GetHashCode() { unchecked { - return (base.GetHashCode() * 397) ^ TradableId.GetHashCode(); + var hashCode = base.GetHashCode(); + hashCode = (hashCode * 397) ^ RequiredBlockIndex.GetHashCode(); + hashCode = (hashCode * 397) ^ TradableId.GetHashCode(); + return hashCode; } } From e9e580d4549cb7e9e4f86b8add5f29f458dc3ff1 Mon Sep 17 00:00:00 2001 From: Yang Chun Ung Date: Fri, 7 May 2021 11:10:14 +0900 Subject: [PATCH 2/2] Apply review suggestions --- Lib9c/Model/Item/Inventory.cs | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/Lib9c/Model/Item/Inventory.cs b/Lib9c/Model/Item/Inventory.cs index e7446c79c9..619f1826ec 100644 --- a/Lib9c/Model/Item/Inventory.cs +++ b/Lib9c/Model/Item/Inventory.cs @@ -405,15 +405,6 @@ i.item is ITradableItem item && } } return true; - - // outItem = _items - // .Where(i => i.count >= count) - // .Select(i => i.item) - // .OfType() - // .Where(t => t.TradableId.Equals(tradeId) && t.RequiredBlockIndex <= blockIndex) - // .OrderBy(r => r.RequiredBlockIndex) - // .FirstOrDefault(); - // return !(outItem is null); } // public bool TryGetTradableItemWithoutNonTradableFungibleItem( @@ -544,11 +535,8 @@ public ITradableItem SellItem(Guid tradableId, long blockIndex, int count) break; } - if (item.count <= remain) - { - _items.Remove(item); - remain -= item.count; - } + _items.Remove(item); + remain -= item.count; if (remain <= 0) { @@ -563,7 +551,7 @@ public ITradableItem SellItem(Guid tradableId, long blockIndex, int count) } // Copy new TradableMaterial - if (items.First().item is TradableMaterial tradableMaterial) + if (tradableItem is TradableMaterial tradableMaterial) { var material = new TradableMaterial((Dictionary) tradableMaterial.Serialize()) { @@ -572,12 +560,11 @@ public ITradableItem SellItem(Guid tradableId, long blockIndex, int count) AddItem(material, count); return material; } - else - { - tradableItem.RequiredBlockIndex = requiredBlockIndex; - AddItem((ItemBase)tradableItem, count); - return tradableItem; - } + + // NonFungibleItem case. + tradableItem.RequiredBlockIndex = requiredBlockIndex; + AddItem((ItemBase)tradableItem, count); + return tradableItem; }