Exemple #1
0
        public void Read(CombinationMail mail)
        {
            var avatarAddress   = States.Instance.CurrentAvatarState.address;
            var attachment      = (CombinationConsumable.ResultModel)mail.attachment;
            var itemBase        = attachment.itemUsable ?? (ItemBase)attachment.costume;
            var nonFungibleItem = attachment.itemUsable ?? (INonFungibleItem)attachment.costume;
            var popup           = Find <CombinationResultPopup>();
            var materialItems   = attachment.materials
                                  .Select(pair => new { pair, item = pair.Key })
                                  .Select(t => new CombinationMaterial(
                                              t.item,
                                              t.pair.Value,
                                              t.pair.Value,
                                              t.pair.Value))
                                  .ToList();
            var model = new UI.Model.CombinationResultPopup(new CountableItem(itemBase, 1))
            {
                isSuccess     = true,
                materialItems = materialItems
            };

            model.OnClickSubmit.Subscribe(_ =>
            {
                LocalLayerModifier.AddItem(avatarAddress, nonFungibleItem.ItemId, false);
                LocalLayerModifier.RemoveNewAttachmentMail(avatarAddress, mail.id, false);
                LocalLayerModifier.RemoveAttachmentResult(avatarAddress, mail.id);
                LocalLayerModifier.ModifyAvatarItemRequiredIndex(
                    avatarAddress,
                    nonFungibleItem.ItemId,
                    Game.Game.instance.Agent.BlockIndex);
            });
            popup.Pop(model);
        }
Exemple #2
0
        public void AvatarNewAttachmentMailSetter()
        {
            var equipment         = GetFirstEquipment();
            var combinationResult = new CombinationConsumable.ResultModel
            {
                itemUsable = equipment
            };
            var attachmentMail = new CombinationMail(combinationResult, 0, new Guid(), 0);

            Assert.False(attachmentMail.New);
            _avatarState.mailBox.Add(attachmentMail);
            var modifier =
                JsonTest(new AvatarAttachmentMailNewSetter(attachmentMail.id));

            _avatarState = modifier.Modify(_avatarState);
            Assert.True(attachmentMail.New);
        }
Exemple #3
0
        public static bool TryGetMail(
            this CombinationSlotState state,
            long blockIndex,
            long requiredBlockIndex,
            out CombinationMail combinationMail,
            out ItemEnhanceMail itemEnhanceMail)
        {
            combinationMail = null;
            itemEnhanceMail = null;

            if (!state.TryGetResultId(out var resultId))
            {
                return(false);
            }

            switch (state.Result)
            {
            case ItemEnhancement.ResultModel r:
                itemEnhanceMail = new ItemEnhanceMail(
                    r,
                    blockIndex,
                    resultId,
                    requiredBlockIndex);
                return(true);

            case ItemEnhancement7.ResultModel r:
                itemEnhanceMail = new ItemEnhanceMail(
                    r,
                    blockIndex,
                    resultId,
                    requiredBlockIndex);
                return(true);

            case CombinationConsumable5.ResultModel r:
                combinationMail = new CombinationMail(
                    r,
                    blockIndex,
                    resultId,
                    requiredBlockIndex);
                return(true);

            default:
                return(false);
            }
        }
Exemple #4
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));
            }

            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.TryGetFungibleItem(materialId, out var inventoryItem);
                    var material = (Material)inventoryItem.item;
                    materials[material] = count;
                    avatarState.inventory.RemoveFungibleItem(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 CombinationConsumable.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.UpdateV2(mail);
            avatarState.UpdateFromCombination(itemUsable);
            sw.Stop();
            Log.Verbose("{AddressesHex}Combination Update AvatarState: {Elapsed}", addressesHex, sw.Elapsed);
            sw.Restart();

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

            avatarState.UpdateQuestRewards(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()));
        }
        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()));
        }
        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));
            }

            if (!states.TryGetAgentAvatarStates(ctx.Signer, AvatarAddress, out var agentState,
                                                out var avatarState))
            {
                throw new FailedLoadStateException("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("Aborted as the slot state is failed to load");
            }

            if (!slotState.Validate(avatarState, ctx.BlockIndex))
            {
                throw new CombinationSlotUnlockException(
                          $"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(nameof(EquipmentItemRecipeSheet), RecipeId);
            }

            if (!(SubRecipeId is null))
            {
                if (!recipe.SubRecipeIds.Contains((int)SubRecipeId))
                {
                    throw new SheetRowColumnException(
                              $"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(recipe.UnlockStage, current);
            }

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

            if (!avatarState.inventory.RemoveMaterial(material.ItemId, recipe.MaterialCount))
            {
                throw new NotEnoughMaterialException(
                          $"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(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(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(nameof(MaterialItemSheet), materialInfo.Id);
                    }

                    if (!avatarState.inventory.RemoveMaterial(subMaterialRow.ItemId,
                                                              materialInfo.Count))
                    {
                        throw new NotEnoughMaterialException(
                                  $"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,
                          $"Aborted as the agent ({ctx.Signer}) has no sufficient gold: {agentBalance} < {requiredGold}"
                          );
            }

            if (avatarState.actionPoint < requiredActionPoint)
            {
                throw new NotEnoughActionPointException(
                          $"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 CombinationConsumable.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.UpdateV2(mail);
            avatarState.questList.UpdateCombinationEquipmentQuest(RecipeId);
            avatarState.UpdateFromCombination(equipment);
            avatarState.UpdateQuestRewards(materialSheet);
            return(states
                   .SetState(AvatarAddress, avatarState.Serialize())
                   .SetState(slotAddress, slotState.Serialize())
                   .SetState(ctx.Signer, agentState.Serialize()));
        }
Exemple #7
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));
            }

            var sw = new Stopwatch();

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

            Log.Debug("Combination exec started.");

            if (!states.TryGetAgentAvatarStates(ctx.Signer, AvatarAddress, out AgentState agentState,
                                                out AvatarState avatarState))
            {
                return(LogError(context, "Aborted as the avatar state of the signer was failed to load."));
            }

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

            if (!avatarState.worldInformation.TryGetUnlockedWorldByStageClearedBlockIndex(out var world))
            {
                return(LogError(context, "Aborted as the WorldInformation was failed to load."));
            }

            if (world.StageClearedId < GameConfig.RequireClearedStageLevel.CombinationEquipmentAction)
            {
                // 스테이지 클리어 부족 에러.
                return(LogError(
                           context,
                           "Aborted as the signer is not cleared the minimum stage level required to combine consumables yet: {ClearedLevel} < {RequiredLevel}.",
                           world.StageClearedId,
                           GameConfig.RequireClearedStageLevel.CombinationEquipmentAction
                           ));
            }

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

            if (slotState is null || !(slotState.Validate(avatarState, ctx.BlockIndex)))
            {
                return(LogError(
                           context,
                           "Aborted as the slot state is failed to load or invalid: {@SlotState} @ {SlotIndex}",
                           slotState,
                           slotIndex
                           ));
            }

            var tableSheets = TableSheets.FromActionContext(ctx);

            sw.Stop();
            Log.Debug("Combination Get TableSheetsState: {Elapsed}", sw.Elapsed);
            sw.Restart();

            Log.Debug("Execute Combination; player: {Player}", AvatarAddress);
            var consumableItemSheet = tableSheets.ConsumableItemSheet;
            var recipeRow           = tableSheets.ConsumableItemRecipeSheet.Values.FirstOrDefault(r => r.Id == recipeId);

            if (recipeRow is null)
            {
                return(LogError(context, "Aborted as the recipe was failed to load."));
            }
            var materials = new Dictionary <Material, int>();

            foreach (var materialInfo in recipeRow.Materials)
            {
                var materialId = materialInfo.Id;
                var count      = materialInfo.Count;
                if (avatarState.inventory.HasItem(materialId, count))
                {
                    avatarState.inventory.TryGetFungibleItem(materialId, out var inventoryItem);
                    var material = (Material)inventoryItem.item;
                    materials[material] = count;
                    avatarState.inventory.RemoveFungibleItem(material, count);
                }
                else
                {
                    return(LogError(
                               context,
                               "Aborted as the player has no enough material ({Material} * {Quantity})",
                               materialId,
                               count
                               ));
                }
            }

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

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

            var costAP = recipeRow.RequiredActionPoint;

            if (avatarState.actionPoint < costAP)
            {
                // ap 부족 에러.
                return(LogError(
                           context,
                           "Aborted due to insufficient action point: {ActionPointBalance} < {ActionCost}",
                           avatarState.actionPoint,
                           costAP
                           ));
            }

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

            var resultConsumableItemId = recipeRow.ResultConsumableItemId;

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

            if (!consumableItemSheet.TryGetValue(resultConsumableItemId, out var consumableItemRow))
            {
                // 소모품 테이블 값 가져오기 실패.
                return(LogError(
                           context,
                           "Aborted as the consumable item ({ItemId} was failed to load from the data table.",
                           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.UpdateFromCombination(itemUsable);
            sw.Stop();
            Log.Debug("Combination Update AvatarState: {Elapsed}", sw.Elapsed);
            sw.Restart();

            avatarState.UpdateQuestRewards(ctx);

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

            Log.Debug("Combination Total Executed Time: {Elapsed}", ended - started);
            return(states
                   .SetState(ctx.Signer, agentState.Serialize())
                   .SetState(slotAddress, slotState.Serialize()));
        }
Exemple #8
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(slotAddress, MarkChanged)
                       .SetState(ctx.Signer, MarkChanged)
                       .MarkBalanceChanged(GoldCurrencyMock, ctx.Signer, BlacksmithAddress));
            }

            if (!states.TryGetAgentAvatarStates(ctx.Signer, AvatarAddress, out var agentState,
                                                out var avatarState))
            {
                return(LogError(context, "Aborted as the avatar state of the signer was failed to load."));
            }

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

            if (slotState is null || !(slotState.Validate(avatarState, ctx.BlockIndex)))
            {
                return(LogError(
                           context,
                           "Aborted as the slot state is failed to load or invalid: {@SlotState} @ {SlotIndex}",
                           slotState,
                           SlotIndex
                           ));
            }

            var tableSheets   = TableSheets.FromActionContext(ctx);
            var recipeSheet   = tableSheets.EquipmentItemRecipeSheet;
            var materialSheet = tableSheets.MaterialItemSheet;
            var materials     = new Dictionary <Material, int>();

            // 레시피 검증
            if (!recipeSheet.TryGetValue(RecipeId, out var recipe))
            {
                return(LogError(
                           context,
                           "Aborted as the recipe {RecipeId} was failed to load from the sheet.",
                           RecipeId
                           ));
            }

            if (!(SubRecipeId is null))
            {
                if (!recipe.SubRecipeIds.Contains((int)SubRecipeId))
                {
                    return(LogError(
                               context,
                               "Aborted as the subrecipe {SubRecipeId} was failed to load from the sheet.",
                               SubRecipeId
                               ));
                }
            }

            // 메인 레시피 해금 검사.
            if (!avatarState.worldInformation.IsStageCleared(recipe.UnlockStage))
            {
                return(LogError(
                           context,
                           "Aborted as the signer is not cleared the minimum stage level required to use the recipe {@Recipe} yet.",
                           recipe
                           ));
            }

            if (!materialSheet.TryGetValue(recipe.MaterialId, out var material))
            {
                return(LogError(
                           context,
                           "Aborted as the material {MaterialId} was failed to load from the sheet.",
                           recipe.MaterialId
                           ));
            }

            if (!avatarState.inventory.RemoveMaterial(material.ItemId, recipe.MaterialCount))
            {
                return(LogError(
                           context,
                           "Aborted as the player has no enough material ({Material} * {Quantity})",
                           material,
                           recipe.MaterialCount
                           ));
            }

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

            materials[equipmentMaterial] = recipe.MaterialCount;

            BigInteger requiredGold        = recipe.RequiredGold;
            var        requiredActionPoint = recipe.RequiredActionPoint;

            // 장비 제작
            if (!tableSheets.EquipmentItemSheet.TryGetValue(recipe.ResultEquipmentId, out var equipRow))
            {
                return(LogError(
                           context,
                           "Aborted as the equipment item {EquipmentId} was failed to load from the sheet.",
                           recipe.ResultEquipmentId
                           ));
            }

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

            // 서브 레시피 검증
            HashSet <int> optionIds = null;

            if (SubRecipeId.HasValue)
            {
                var subSheet = tableSheets.EquipmentItemSubRecipeSheet;
                if (!subSheet.TryGetValue((int)SubRecipeId, out var subRecipe))
                {
                    return(LogError(
                               context,
                               "Aborted as the subrecipe {SubRecipeId} was failed to load from the subsheet.",
                               SubRecipeId
                               ));
                }

                // 서브 레시피 해금 검사.
                if (!avatarState.worldInformation.IsStageCleared(subRecipe.UnlockStage))
                {
                    return(LogError(
                               context,
                               "Aborted as the signer is not cleared the minimum stage level required to use the subrecipe {@SubRecipe} yet.",
                               subRecipe
                               ));
                }

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

                foreach (var materialInfo in subRecipe.Materials)
                {
                    if (!materialSheet.TryGetValue(materialInfo.Id, out var subMaterialRow))
                    {
                        return(LogError(
                                   context,
                                   "Aborted as the meterial info {MaterialInfoId} was failed to load from the submaterial sheet.",
                                   materialInfo.Id
                                   ));
                    }

                    if (!avatarState.inventory.RemoveMaterial(subMaterialRow.ItemId,
                                                              materialInfo.Count))
                    {
                        return(LogError(
                                   context,
                                   "Aborted as the player has no enough material ({Material} * {Quantity})",
                                   subMaterialRow,
                                   materialInfo.Count
                                   ));
                    }

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

                optionIds = SelectOption(tableSheets, subRecipe, ctx.Random, equipment);
                equipment.Update(requiredBlockIndex);
            }

            // 자원 검증
            BigInteger agentBalance = states.GetBalance(ctx.Signer, states.GetGoldCurrency());

            if (agentBalance < requiredGold || avatarState.actionPoint < requiredActionPoint)
            {
                return(LogError(
                           context,
                           "Aborted due to insufficient action point: {ActionPointBalance} < {ActionCost}",
                           avatarState.actionPoint,
                           requiredActionPoint
                           ));
            }

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

            // FIXME: BlacksmithAddress 계좌로 돈이 쌓이기만 하는데 이걸 어떻게 순환시킬지 기획이 필요.
            if (requiredGold > 0)
            {
                states = states.TransferAsset(ctx.Signer, BlacksmithAddress, states.GetGoldCurrency(), requiredGold);
            }

            var result = new CombinationConsumable.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, SubRecipeId);
            avatarState.UpdateFromCombination(equipment);
            avatarState.UpdateQuestRewards(ctx);
            return(states
                   .SetState(AvatarAddress, avatarState.Serialize())
                   .SetState(slotAddress, slotState.Serialize())
                   .SetState(ctx.Signer, agentState.Serialize()));
        }
        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>();

            // 레시피 검증
            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."
                              );
                }
            }

            // 메인 레시피 해금 검사.
            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>();

            // 장비 제작
            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
                );

            // 서브 레시피 검증
            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);
            }

            // 자원 검증
            FungibleAssetValue agentBalance = states.GetBalance(ctx.Signer, states.GetGoldCurrency());

            if (agentBalance < (states.GetGoldCurrency() * requiredGold) || 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 계좌로 돈이 쌓이기만 하는데 이걸 어떻게 순환시킬지 기획이 필요.
            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.Update2(mail);
            avatarState.questList.UpdateCombinationEquipmentQuest(RecipeId);
            avatarState.UpdateFromCombination(equipment);
            avatarState.UpdateQuestRewards(materialSheet);

            //Avoid InvalidBlockStateRootHashException to 50000 index.
            if (avatarState.questList.Any(q => q.Complete && !q.IsPaidInAction))
            {
                var prevIds = avatarState.questList.completedQuestIds;
                avatarState.UpdateQuestRewards(materialSheet);
                avatarState.questList.completedQuestIds = prevIds;
            }

            return(states
                   .SetState(AvatarAddress, avatarState.Serialize())
                   .SetState(slotAddress, slotState.Serialize())
                   .SetState(ctx.Signer, agentState.Serialize()));
        }
        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()));
        }