コード例 #1
0
        public override IAccountStateDelta Execute(IActionContext context)
        {
            IActionContext ctx         = context;
            var            states      = ctx.PreviousStates;
            var            slotAddress = AvatarAddress.Derive(
                string.Format(
                    CultureInfo.InvariantCulture,
                    CombinationSlotState.DeriveFormat,
                    slotIndex
                    )
                );

            if (ctx.Rehearsal)
            {
                return(states
                       .SetState(AvatarAddress, MarkChanged)
                       .SetState(ctx.Signer, MarkChanged)
                       .SetState(slotAddress, MarkChanged));
            }

            CheckObsolete(BlockChain.Policy.BlockPolicySource.V100080ObsoleteIndex, context);

            var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress);

            var sw = new Stopwatch();

            sw.Start();
            var started = DateTimeOffset.UtcNow;

            Log.Verbose("{AddressesHex}Combination exec started", addressesHex);

            if (!states.TryGetAvatarState(ctx.Signer, AvatarAddress, out AvatarState avatarState))
            {
                throw new FailedLoadStateException($"{addressesHex}Aborted as the avatar state of the signer was failed to load.");
            }

            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed);
            sw.Restart();

            if (!avatarState.worldInformation.IsStageCleared(GameConfig.RequireClearedStageLevel.CombinationEquipmentAction))
            {
                avatarState.worldInformation.TryGetLastClearedStageId(out var current);
                throw new NotEnoughClearedStageLevelException(
                          addressesHex,
                          GameConfig.RequireClearedStageLevel.CombinationEquipmentAction,
                          current);
            }

            var slotState = states.GetCombinationSlotState(AvatarAddress, slotIndex);

            if (slotState is null)
            {
                throw new FailedLoadStateException($"{addressesHex}Aborted as the slot state is failed to load: # {slotIndex}");
            }

            if (!slotState.Validate(avatarState, ctx.BlockIndex))
            {
                throw new CombinationSlotUnlockException(
                          $"{addressesHex}Aborted as the slot state is invalid: {slotState} @ {slotIndex}");
            }

            Log.Verbose("{AddressesHex}Execute Combination; player: {Player}", addressesHex, AvatarAddress);
            var consumableItemSheet = states.GetSheet <ConsumableItemSheet>();
            var recipeRow           = states.GetSheet <ConsumableItemRecipeSheet>().Values.FirstOrDefault(r => r.Id == recipeId);

            if (recipeRow is null)
            {
                throw new SheetRowNotFoundException(addressesHex, nameof(ConsumableItemRecipeSheet), recipeId);
            }
            var materials = new Dictionary <Material, int>();

            foreach (var materialInfo in recipeRow.Materials.OrderBy(r => r.Id))
            {
                var materialId = materialInfo.Id;
                var count      = materialInfo.Count;
                if (avatarState.inventory.HasItem(materialId, count))
                {
                    avatarState.inventory.TryGetItem(materialId, out var inventoryItem);
                    if (!(inventoryItem.item is Material material))
                    {
                        throw new InvalidMaterialException($"Aborted because material id({materialId}) not valid");
                    }

                    materials[material] = count;
                    avatarState.inventory.RemoveFungibleItem2(material, count);
                }
                else
                {
                    throw new NotEnoughMaterialException(
                              $"{addressesHex}Aborted as the player has no enough material ({materialId} * {count})");
                }
            }

            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Remove Materials: {Elapsed}", addressesHex, sw.Elapsed);
            sw.Restart();

            var result = new CombinationConsumable5.ResultModel
            {
                materials = materials,
                itemType  = ItemType.Consumable,
            };

            var costAP = recipeRow.RequiredActionPoint;

            if (avatarState.actionPoint < costAP)
            {
                throw new NotEnoughActionPointException(
                          $"{addressesHex}Aborted due to insufficient action point: {avatarState.actionPoint} < {costAP}"
                          );
            }

            // ap 차감.
            avatarState.actionPoint -= costAP;
            result.actionPoint       = costAP;

            var resultConsumableItemId = recipeRow.ResultConsumableItemId;

            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Get Food id: {Elapsed}", addressesHex, sw.Elapsed);
            sw.Restart();
            result.recipeId = recipeRow.Id;

            if (!consumableItemSheet.TryGetValue(resultConsumableItemId, out var consumableItemRow))
            {
                throw new SheetRowNotFoundException(addressesHex, nameof(ConsumableItemSheet), resultConsumableItemId);
            }

            // 조합 결과 획득.
            var requiredBlockIndex = ctx.BlockIndex + recipeRow.RequiredBlockIndex;
            var itemId             = ctx.Random.GenerateRandomGuid();
            var itemUsable         = GetFood(consumableItemRow, itemId, requiredBlockIndex);

            // 액션 결과
            result.itemUsable = itemUsable;
            var mail = new CombinationMail(
                result,
                ctx.BlockIndex,
                ctx.Random.GenerateRandomGuid(),
                requiredBlockIndex
                );

            result.id = mail.id;
            avatarState.Update(mail);
            avatarState.UpdateFromCombination2(itemUsable);
            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Update AvatarState: {Elapsed}", addressesHex, sw.Elapsed);
            sw.Restart();

            var materialSheet = states.GetSheet <MaterialItemSheet>();

            avatarState.UpdateQuestRewards2(materialSheet);

            avatarState.updatedAt  = ctx.BlockIndex;
            avatarState.blockIndex = ctx.BlockIndex;
            states = states.SetState(AvatarAddress, avatarState.Serialize());
            slotState.Update(result, ctx.BlockIndex, requiredBlockIndex);
            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed);
            var ended = DateTimeOffset.UtcNow;

            Log.Verbose("{AddressesHex}Combination Total Executed Time: {Elapsed}", addressesHex, ended - started);
            return(states
                   .SetState(slotAddress, slotState.Serialize()));
        }
コード例 #2
0
ファイル: CombinationEquipment5.cs プロジェクト: dahlia/lib9c
        public override IAccountStateDelta Execute(IActionContext context)
        {
            IActionContext ctx         = context;
            var            states      = ctx.PreviousStates;
            var            slotAddress = AvatarAddress.Derive(
                string.Format(
                    CultureInfo.InvariantCulture,
                    CombinationSlotState.DeriveFormat,
                    SlotIndex
                    )
                );

            if (ctx.Rehearsal)
            {
                return(states
                       .SetState(AvatarAddress, MarkChanged)
                       .SetState(slotAddress, MarkChanged)
                       .SetState(ctx.Signer, MarkChanged)
                       .MarkBalanceChanged(GoldCurrencyMock, ctx.Signer, BlacksmithAddress));
            }

            CheckObsolete(BlockChain.Policy.BlockPolicySource.V100080ObsoleteIndex, context);

            var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress);

            if (!states.TryGetAgentAvatarStates(ctx.Signer, AvatarAddress, out var agentState,
                                                out var avatarState))
            {
                throw new FailedLoadStateException($"{addressesHex}Aborted as the avatar state of the signer was failed to load.");
            }

            var slotState = states.GetCombinationSlotState(AvatarAddress, SlotIndex);

            if (slotState is null)
            {
                throw new FailedLoadStateException($"{addressesHex}Aborted as the slot state is failed to load");
            }

            if (!slotState.Validate(avatarState, ctx.BlockIndex))
            {
                throw new CombinationSlotUnlockException(
                          $"{addressesHex}Aborted as the slot state is invalid: {slotState} @ {SlotIndex}");
            }

            var recipeSheet   = states.GetSheet <EquipmentItemRecipeSheet>();
            var materialSheet = states.GetSheet <MaterialItemSheet>();
            var materials     = new Dictionary <Material, int>();

            // Validate recipe.
            if (!recipeSheet.TryGetValue(RecipeId, out var recipe))
            {
                throw new SheetRowNotFoundException(addressesHex, nameof(EquipmentItemRecipeSheet), RecipeId);
            }

            if (!(SubRecipeId is null))
            {
                if (!recipe.SubRecipeIds.Contains((int)SubRecipeId))
                {
                    throw new SheetRowColumnException(
                              $"{addressesHex}Aborted as the sub recipe {SubRecipeId} was failed to load from the sheet."
                              );
                }
            }

            // Validate main recipe is unlocked.
            if (!avatarState.worldInformation.IsStageCleared(recipe.UnlockStage))
            {
                avatarState.worldInformation.TryGetLastClearedStageId(out var current);
                throw new NotEnoughClearedStageLevelException(addressesHex, recipe.UnlockStage, current);
            }

            if (!materialSheet.TryGetValue(recipe.MaterialId, out var material))
            {
                throw new SheetRowNotFoundException(addressesHex, nameof(MaterialItemSheet), recipe.MaterialId);
            }

            if (!avatarState.inventory.RemoveFungibleItem2(material.ItemId, recipe.MaterialCount))
            {
                throw new NotEnoughMaterialException(
                          $"{addressesHex}Aborted as the player has no enough material ({material} * {recipe.MaterialCount})"
                          );
            }

            var equipmentMaterial = ItemFactory.CreateMaterial(materialSheet, material.Id);

            materials[equipmentMaterial] = recipe.MaterialCount;

            BigInteger requiredGold        = recipe.RequiredGold;
            var        requiredActionPoint = recipe.RequiredActionPoint;
            var        equipmentItemSheet  = states.GetSheet <EquipmentItemSheet>();

            // Validate equipment id.
            if (!equipmentItemSheet.TryGetValue(recipe.ResultEquipmentId, out var equipRow))
            {
                throw new SheetRowNotFoundException(addressesHex, nameof(equipmentItemSheet), recipe.ResultEquipmentId);
            }

            var requiredBlockIndex = ctx.BlockIndex + recipe.RequiredBlockIndex;
            var equipment          = (Equipment)ItemFactory.CreateItemUsable(
                equipRow,
                ctx.Random.GenerateRandomGuid(),
                requiredBlockIndex
                );

            // Validate sub recipe.
            HashSet <int> optionIds = null;

            if (SubRecipeId.HasValue)
            {
                var subSheet = states.GetSheet <EquipmentItemSubRecipeSheet>();
                var subId    = (int)SubRecipeId;
                if (!subSheet.TryGetValue(subId, out var subRecipe))
                {
                    throw new SheetRowNotFoundException(addressesHex, nameof(EquipmentItemSubRecipeSheet), subId);
                }

                requiredBlockIndex  += subRecipe.RequiredBlockIndex;
                requiredGold        += subRecipe.RequiredGold;
                requiredActionPoint += subRecipe.RequiredActionPoint;

                foreach (var materialInfo in subRecipe.Materials)
                {
                    if (!materialSheet.TryGetValue(materialInfo.Id, out var subMaterialRow))
                    {
                        throw new SheetRowNotFoundException(addressesHex, nameof(MaterialItemSheet), materialInfo.Id);
                    }

                    if (!avatarState.inventory.RemoveFungibleItem2(subMaterialRow.ItemId,
                                                                   materialInfo.Count))
                    {
                        throw new NotEnoughMaterialException(
                                  $"{addressesHex}Aborted as the player has no enough material ({subMaterialRow} * {materialInfo.Count})"
                                  );
                    }

                    var subMaterial = ItemFactory.CreateMaterial(materialSheet, materialInfo.Id);
                    materials[subMaterial] = materialInfo.Count;
                }

                optionIds = SelectOption(states.GetSheet <EquipmentItemOptionSheet>(), states.GetSheet <SkillSheet>(),
                                         subRecipe, ctx.Random, equipment);
                equipment.Update(requiredBlockIndex);
            }

            // Validate NCG.
            FungibleAssetValue agentBalance = states.GetBalance(ctx.Signer, states.GetGoldCurrency());

            if (agentBalance < states.GetGoldCurrency() * requiredGold)
            {
                throw new InsufficientBalanceException(
                          ctx.Signer,
                          agentBalance,
                          $"{addressesHex}Aborted as the agent ({ctx.Signer}) has no sufficient gold: {agentBalance} < {requiredGold}"
                          );
            }

            if (avatarState.actionPoint < requiredActionPoint)
            {
                throw new NotEnoughActionPointException(
                          $"{addressesHex}Aborted due to insufficient action point: {avatarState.actionPoint} < {requiredActionPoint}"
                          );
            }

            avatarState.actionPoint -= requiredActionPoint;
            if (!(optionIds is null))
            {
                foreach (var id in optionIds.OrderBy(id => id))
                {
                    agentState.unlockedOptions.Add(id);
                }
            }

            // FIXME: BlacksmithAddress just accumulate NCG. we need plan how to circulate this.
            if (requiredGold > 0)
            {
                states = states.TransferAsset(
                    ctx.Signer,
                    BlacksmithAddress,
                    states.GetGoldCurrency() * requiredGold
                    );
            }

            var result = new CombinationConsumable5.ResultModel
            {
                actionPoint = requiredActionPoint,
                gold        = requiredGold,
                materials   = materials,
                itemUsable  = equipment,
                recipeId    = RecipeId,
                subRecipeId = SubRecipeId,
                itemType    = ItemType.Equipment,
            };

            slotState.Update(result, ctx.BlockIndex, requiredBlockIndex);
            var mail = new CombinationMail(result, ctx.BlockIndex, ctx.Random.GenerateRandomGuid(),
                                           requiredBlockIndex);

            result.id = mail.id;
            avatarState.Update(mail);
            avatarState.questList.UpdateCombinationEquipmentQuest(RecipeId);
            avatarState.UpdateFromCombination2(equipment);
            avatarState.UpdateQuestRewards2(materialSheet);
            return(states
                   .SetState(AvatarAddress, avatarState.Serialize())
                   .SetState(slotAddress, slotState.Serialize())
                   .SetState(ctx.Signer, agentState.Serialize()));
        }
コード例 #3
0
        public override IAccountStateDelta Execute(IActionContext context)
        {
            var states      = context.PreviousStates;
            var slotAddress = avatarAddress.Derive(
                string.Format(
                    CultureInfo.InvariantCulture,
                    CombinationSlotState.DeriveFormat,
                    slotIndex
                    )
                );
            var inventoryAddress        = avatarAddress.Derive(LegacyInventoryKey);
            var worldInformationAddress = avatarAddress.Derive(LegacyWorldInformationKey);
            var questListAddress        = avatarAddress.Derive(LegacyQuestListKey);

            if (context.Rehearsal)
            {
                return(states
                       .SetState(avatarAddress, MarkChanged)
                       .SetState(slotAddress, MarkChanged)
                       .SetState(context.Signer, MarkChanged)
                       .SetState(inventoryAddress, MarkChanged)
                       .SetState(worldInformationAddress, MarkChanged)
                       .SetState(questListAddress, MarkChanged)
                       .MarkBalanceChanged(GoldCurrencyMock, context.Signer, BlacksmithAddress));
            }

            var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress);

            if (!states.TryGetAgentAvatarStatesV2(context.Signer, avatarAddress, out var agentState,
                                                  out var avatarState))
            {
                throw new FailedLoadStateException(
                          $"{addressesHex}Aborted as the avatar state of the signer was failed to load.");
            }

            // Validate Required Cleared Stage
            if (!avatarState.worldInformation.IsStageCleared(
                    GameConfig.RequireClearedStageLevel.CombinationEquipmentAction))
            {
                avatarState.worldInformation.TryGetLastClearedStageId(out var current);
                throw new NotEnoughClearedStageLevelException(
                          addressesHex,
                          GameConfig.RequireClearedStageLevel.CombinationEquipmentAction,
                          current);
            }
            // ~Validate Required Cleared Stage

            // Validate SlotIndex
            var slotState = states.GetCombinationSlotState(avatarAddress, slotIndex);

            if (slotState is null)
            {
                throw new FailedLoadStateException(
                          $"{addressesHex}Aborted as the slot state is failed to load: # {slotIndex}");
            }

            if (!slotState.Validate(avatarState, context.BlockIndex))
            {
                throw new CombinationSlotUnlockException(
                          $"{addressesHex}Aborted as the slot state is invalid: {slotState} @ {slotIndex}");
            }
            // ~Validate SlotIndex

            // Validate Work
            var costActionPoint       = 0;
            var costNCG               = 0L;
            var endBlockIndex         = context.BlockIndex;
            var requiredFungibleItems = new Dictionary <int, int>();

            // Validate RecipeId
            var equipmentItemRecipeSheet = states.GetSheet <EquipmentItemRecipeSheet>();

            if (!equipmentItemRecipeSheet.TryGetValue(recipeId, out var recipeRow))
            {
                throw new SheetRowNotFoundException(
                          addressesHex,
                          nameof(EquipmentItemRecipeSheet),
                          recipeId);
            }
            // ~Validate RecipeId

            // Validate Recipe Unlocked.
            if (!avatarState.worldInformation.IsStageCleared(recipeRow.UnlockStage))
            {
                avatarState.worldInformation.TryGetLastClearedStageId(out var current);
                throw new NotEnoughClearedStageLevelException(
                          addressesHex,
                          recipeRow.UnlockStage,
                          current);
            }
            // ~Validate Recipe Unlocked

            // Validate Recipe ResultEquipmentId
            var equipmentItemSheet = states.GetSheet <EquipmentItemSheet>();

            if (!equipmentItemSheet.TryGetValue(recipeRow.ResultEquipmentId, out var equipmentRow))
            {
                throw new SheetRowNotFoundException(
                          addressesHex,
                          nameof(equipmentItemSheet),
                          recipeRow.ResultEquipmentId);
            }
            // ~Validate Recipe ResultEquipmentId

            // Validate Recipe Material
            var materialItemSheet = states.GetSheet <MaterialItemSheet>();

            if (!materialItemSheet.TryGetValue(recipeRow.MaterialId, out var materialRow))
            {
                throw new SheetRowNotFoundException(
                          addressesHex,
                          nameof(MaterialItemSheet),
                          recipeRow.MaterialId);
            }

            if (requiredFungibleItems.ContainsKey(materialRow.Id))
            {
                requiredFungibleItems[materialRow.Id] += recipeRow.MaterialCount;
            }
            else
            {
                requiredFungibleItems[materialRow.Id] = recipeRow.MaterialCount;
            }
            // ~Validate Recipe Material

            // Validate SubRecipeId
            EquipmentItemSubRecipeSheetV2.Row subRecipeRow = null;
            if (subRecipeId.HasValue)
            {
                if (!recipeRow.SubRecipeIds.Contains(subRecipeId.Value))
                {
                    throw new SheetRowColumnException(
                              $"{addressesHex}Aborted as the sub recipe {subRecipeId.Value} was failed to load from the sheet."
                              );
                }

                var equipmentItemSubRecipeSheetV2 = states.GetSheet <EquipmentItemSubRecipeSheetV2>();
                if (!equipmentItemSubRecipeSheetV2.TryGetValue(subRecipeId.Value, out subRecipeRow))
                {
                    throw new SheetRowNotFoundException(
                              addressesHex,
                              nameof(EquipmentItemSubRecipeSheetV2),
                              subRecipeId.Value);
                }

                // Validate SubRecipe Material
                for (var i = subRecipeRow.Materials.Count; i > 0; i--)
                {
                    var materialInfo = subRecipeRow.Materials[i - 1];
                    if (!materialItemSheet.TryGetValue(materialInfo.Id, out materialRow))
                    {
                        throw new SheetRowNotFoundException(
                                  addressesHex,
                                  nameof(MaterialItemSheet),
                                  materialInfo.Id);
                    }

                    if (requiredFungibleItems.ContainsKey(materialRow.Id))
                    {
                        requiredFungibleItems[materialRow.Id] += materialInfo.Count;
                    }
                    else
                    {
                        requiredFungibleItems[materialRow.Id] = materialInfo.Count;
                    }
                }
                // ~Validate SubRecipe Material

                costActionPoint += subRecipeRow.RequiredActionPoint;
                costNCG         += subRecipeRow.RequiredGold;
                endBlockIndex   += subRecipeRow.RequiredBlockIndex;
            }
            // ~Validate SubRecipeId

            costActionPoint += recipeRow.RequiredActionPoint;
            costNCG         += recipeRow.RequiredGold;
            endBlockIndex   += recipeRow.RequiredBlockIndex;
            // ~Validate Work

            // Remove Required Materials
            var inventory = avatarState.inventory;

            foreach (var pair in requiredFungibleItems.OrderBy(pair => pair.Key))
            {
                if (!materialItemSheet.TryGetValue(pair.Key, out materialRow) ||
                    !inventory.RemoveFungibleItem(materialRow.ItemId, context.BlockIndex, pair.Value))
                {
                    throw new NotEnoughMaterialException(
                              $"{addressesHex}Aborted as the player has no enough material ({pair.Key} * {pair.Value})");
                }
            }
            // ~Remove Required Materials

            // Subtract Required ActionPoint
            if (costActionPoint > 0)
            {
                if (avatarState.actionPoint < costActionPoint)
                {
                    throw new NotEnoughActionPointException(
                              $"{addressesHex}Aborted due to insufficient action point: {avatarState.actionPoint} < {costActionPoint}"
                              );
                }

                avatarState.actionPoint -= costActionPoint;
            }
            // ~Subtract Required ActionPoint

            // Transfer Required NCG
            if (costNCG > 0L)
            {
                states = states.TransferAsset(
                    context.Signer,
                    BlacksmithAddress,
                    states.GetGoldCurrency() * costNCG
                    );
            }
            // ~Transfer Required NCG

            // Create Equipment
            var equipment = (Equipment)ItemFactory.CreateItemUsable(
                equipmentRow,
                context.Random.GenerateRandomGuid(),
                endBlockIndex);

            if (!(subRecipeRow is null))
            {
                AddAndUnlockOption(
                    agentState,
                    equipment,
                    context.Random,
                    subRecipeRow,
                    states.GetSheet <EquipmentItemOptionSheet>(),
                    states.GetSheet <SkillSheet>()
                    );
                endBlockIndex = equipment.RequiredBlockIndex;
            }
            // ~Create Equipment

            // Add or Update Equipment
            avatarState.blockIndex = context.BlockIndex;
            avatarState.updatedAt  = context.BlockIndex;
            avatarState.questList.UpdateCombinationEquipmentQuest(recipeId);
            avatarState.UpdateFromCombination(equipment);
            avatarState.UpdateQuestRewards(materialItemSheet);
            // ~Add or Update Equipment

            // Update Slot
            var mailId           = context.Random.GenerateRandomGuid();
            var attachmentResult = new CombinationConsumable5.ResultModel
            {
                id          = mailId,
                actionPoint = costActionPoint,
                gold        = costNCG,
                materials   = requiredFungibleItems.ToDictionary(
                    e => ItemFactory.CreateMaterial(materialItemSheet, e.Key),
                    e => e.Value),
                itemUsable  = equipment,
                recipeId    = recipeId,
                subRecipeId = subRecipeId,
            };

            slotState.Update(attachmentResult, context.BlockIndex, endBlockIndex);
            // ~Update Slot

            // Create Mail
            var mail = new CombinationMail(
                attachmentResult,
                context.BlockIndex,
                mailId,
                endBlockIndex);

            avatarState.Update(mail);
            // ~Create Mail

            return(states
                   .SetState(avatarAddress, avatarState.SerializeV2())
                   .SetState(inventoryAddress, avatarState.inventory.Serialize())
                   .SetState(worldInformationAddress, avatarState.worldInformation.Serialize())
                   .SetState(questListAddress, avatarState.questList.Serialize())
                   .SetState(slotAddress, slotState.Serialize())
                   .SetState(context.Signer, agentState.Serialize()));
        }
コード例 #4
0
        public override IAccountStateDelta Execute(IActionContext context)
        {
            var states      = context.PreviousStates;
            var slotAddress = avatarAddress.Derive(
                string.Format(
                    CultureInfo.InvariantCulture,
                    CombinationSlotState.DeriveFormat,
                    slotIndex
                    )
                );
            var inventoryAddress        = avatarAddress.Derive(LegacyInventoryKey);
            var worldInformationAddress = avatarAddress.Derive(LegacyWorldInformationKey);
            var questListAddress        = avatarAddress.Derive(LegacyQuestListKey);

            if (context.Rehearsal)
            {
                return(states
                       .SetState(avatarAddress, MarkChanged)
                       .SetState(context.Signer, MarkChanged)
                       .SetState(inventoryAddress, MarkChanged)
                       .SetState(worldInformationAddress, MarkChanged)
                       .SetState(questListAddress, MarkChanged)
                       .SetState(slotAddress, MarkChanged));
            }

            var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress);

            if (!states.TryGetAvatarStateV2(context.Signer, avatarAddress, out var avatarState, out _))
            {
                throw new FailedLoadStateException(
                          $"{addressesHex}Aborted as the avatar state of the signer was failed to load.");
            }

            // Validate Required Cleared Stage
            if (!avatarState.worldInformation.IsStageCleared(
                    GameConfig.RequireClearedStageLevel.CombinationConsumableAction))
            {
                avatarState.worldInformation.TryGetLastClearedStageId(out var current);
                throw new NotEnoughClearedStageLevelException(
                          addressesHex,
                          GameConfig.RequireClearedStageLevel.CombinationConsumableAction,
                          current);
            }
            // ~Validate Required Cleared Stage

            // Validate SlotIndex
            var slotState = states.GetCombinationSlotState(avatarAddress, slotIndex);

            if (slotState is null)
            {
                throw new FailedLoadStateException(
                          $"{addressesHex}Aborted as the slot state is failed to load: # {slotIndex}");
            }

            if (!slotState.Validate(avatarState, context.BlockIndex))
            {
                throw new CombinationSlotUnlockException(
                          $"{addressesHex}Aborted as the slot state is invalid: {slotState} @ {slotIndex}");
            }
            // ~Validate SlotIndex

            // Validate Work
            var costActionPoint       = 0;
            var endBlockIndex         = context.BlockIndex;
            var requiredFungibleItems = new Dictionary <int, int>();

            // Validate RecipeId
            var consumableItemRecipeSheet = states.GetSheet <ConsumableItemRecipeSheet>();

            if (!consumableItemRecipeSheet.TryGetValue(recipeId, out var recipeRow))
            {
                throw new SheetRowNotFoundException(
                          addressesHex,
                          nameof(ConsumableItemRecipeSheet),
                          recipeId);
            }
            // ~Validate RecipeId

            // Validate Recipe ResultEquipmentId
            var consumableItemSheet = states.GetSheet <ConsumableItemSheet>();

            if (!consumableItemSheet.TryGetValue(recipeRow.ResultConsumableItemId, out var consumableRow))
            {
                throw new SheetRowNotFoundException(
                          addressesHex,
                          nameof(consumableItemSheet),
                          recipeRow.ResultConsumableItemId);
            }
            // ~Validate Recipe ResultEquipmentId

            // Validate Recipe Material
            var materialItemSheet = states.GetSheet <MaterialItemSheet>();

            for (var i = recipeRow.Materials.Count; i > 0; i--)
            {
                var materialInfo = recipeRow.Materials[i - 1];
                if (!materialItemSheet.TryGetValue(materialInfo.Id, out var materialRow))
                {
                    throw new SheetRowNotFoundException(
                              addressesHex,
                              nameof(MaterialItemSheet),
                              materialInfo.Id);
                }

                if (requiredFungibleItems.ContainsKey(materialRow.Id))
                {
                    requiredFungibleItems[materialRow.Id] += materialInfo.Count;
                }
                else
                {
                    requiredFungibleItems[materialRow.Id] = materialInfo.Count;
                }
            }
            // ~Validate Recipe Material

            costActionPoint += recipeRow.RequiredActionPoint;
            endBlockIndex   += recipeRow.RequiredBlockIndex;
            // ~Validate Work

            // Remove Required Materials
            var inventory = avatarState.inventory;

            foreach (var pair in requiredFungibleItems.OrderBy(pair => pair.Key))
            {
                if (!materialItemSheet.TryGetValue(pair.Key, out var materialRow) ||
                    !inventory.RemoveFungibleItem(materialRow.ItemId, context.BlockIndex, pair.Value))
                {
                    throw new NotEnoughMaterialException(
                              $"{addressesHex}Aborted as the player has no enough material ({pair.Key} * {pair.Value})");
                }
            }
            // ~Remove Required Materials

            // Subtract Required ActionPoint
            if (costActionPoint > 0)
            {
                if (avatarState.actionPoint < costActionPoint)
                {
                    throw new NotEnoughActionPointException(
                              $"{addressesHex}Aborted due to insufficient action point: {avatarState.actionPoint} < {costActionPoint}"
                              );
                }

                avatarState.actionPoint -= costActionPoint;
            }
            // ~Subtract Required ActionPoint

            // Create Consumable
            var consumable = (Consumable)ItemFactory.CreateItemUsable(
                consumableRow,
                context.Random.GenerateRandomGuid(),
                endBlockIndex
                );

            // ~Create Consumable

            // Add or Update Consumable
            avatarState.blockIndex = context.BlockIndex;
            avatarState.updatedAt  = context.BlockIndex;
            avatarState.UpdateFromCombination(consumable);
            avatarState.UpdateQuestRewards(materialItemSheet);
            // ~Add or Update Consumable

            // Update Slot
            var mailId           = context.Random.GenerateRandomGuid();
            var attachmentResult = new CombinationConsumable5.ResultModel
            {
                id          = mailId,
                actionPoint = costActionPoint,
                materials   = requiredFungibleItems.ToDictionary(
                    e => ItemFactory.CreateMaterial(materialItemSheet, e.Key),
                    e => e.Value),
                itemUsable = consumable,
                recipeId   = recipeId,
            };

            slotState.Update(attachmentResult, context.BlockIndex, endBlockIndex);
            // ~Update Slot

            // Create Mail
            var mail = new CombinationMail(
                attachmentResult,
                context.BlockIndex,
                mailId,
                endBlockIndex);

            avatarState.Update(mail);
            // ~Create Mail

            return(states
                   .SetState(avatarAddress, avatarState.SerializeV2())
                   .SetState(inventoryAddress, avatarState.inventory.Serialize())
                   .SetState(worldInformationAddress, avatarState.worldInformation.Serialize())
                   .SetState(questListAddress, avatarState.questList.Serialize())
                   .SetState(slotAddress, slotState.Serialize()));
        }