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