diff --git a/.Lib9c.Tests/Action/SynthesizeTest.cs b/.Lib9c.Tests/Action/SynthesizeTest.cs index 20f8872269..cda7f63720 100644 --- a/.Lib9c.Tests/Action/SynthesizeTest.cs +++ b/.Lib9c.Tests/Action/SynthesizeTest.cs @@ -103,11 +103,13 @@ public void ExecuteSingle(Grade grade, ItemSubType itemSubType) (state, var items) = UpdateItemsFromSubType(grade, itemSubTypes, state, avatarAddress); state = state.SetActionPoint(avatarAddress, 120); - var previousAvatarState = state.GetAvatarState(avatarAddress); var action = new Synthesize() { AvatarAddress = avatarAddress, MaterialIds = SynthesizeSimulator.GetItemGuids(items), + ChargeAp = false, + MaterialGradeId = (int)grade, + MaterialItemSubTypeId = (int)itemSubType, }; var ctx = new ActionContext @@ -153,17 +155,11 @@ public void ExecuteSingle(Grade grade, ItemSubType itemSubType) break; } - var gradeDict = SynthesizeSimulator.GetGradeDict( - action.MaterialIds, - previousAvatarState, - blockIndex, - avatarAddress.ToHex(), - out _, - out _ - ); - var inputData = new SynthesizeSimulator.InputData() { + Grade = grade, + ItemSubType = itemSubType, + MaterialCount = itemSubTypes.Length, SynthesizeSheet = TableSheets.SynthesizeSheet, SynthesizeWeightSheet = TableSheets.SynthesizeWeightSheet, CostumeItemSheet = TableSheets.CostumeItemSheet, @@ -173,7 +169,6 @@ out _ EquipmentItemOptionSheet = TableSheets.EquipmentItemOptionSheet, SkillSheet = TableSheets.SkillSheet, RandomObject = new TestRandom(), - GradeDict = gradeDict, }; var result = SynthesizeSimulator.Simulate(inputData)[0]; @@ -220,11 +215,13 @@ public void ExecuteMultiple(Grade grade, ItemSubType itemSubType) (state, var items) = UpdateItemsFromSubType(grade, itemSubTypes, state, avatarAddress); state = state.SetActionPoint(avatarAddress, 120); - var previousAvatarState = state.GetAvatarState(avatarAddress); var action = new Synthesize() { AvatarAddress = avatarAddress, MaterialIds = SynthesizeSimulator.GetItemGuids(items), + ChargeAp = false, + MaterialGradeId = (int)grade, + MaterialItemSubTypeId = (int)itemSubType, }; var ctx = new ActionContext @@ -235,19 +232,12 @@ public void ExecuteMultiple(Grade grade, ItemSubType itemSubType) Signer = agentAddress, }; - state = action.Execute(ctx); - - var gradeDict = SynthesizeSimulator.GetGradeDict( - action.MaterialIds, - previousAvatarState, - blockIndex, - avatarAddress.ToHex(), - out _, - out _ - ); - + action.Execute(ctx); var inputData = new SynthesizeSimulator.InputData() { + Grade = grade, + ItemSubType = itemSubType, + MaterialCount = itemSubTypes.Length, SynthesizeSheet = TableSheets.SynthesizeSheet, SynthesizeWeightSheet = TableSheets.SynthesizeWeightSheet, CostumeItemSheet = TableSheets.CostumeItemSheet, @@ -257,7 +247,6 @@ out _ EquipmentItemOptionSheet = TableSheets.EquipmentItemOptionSheet, SkillSheet = TableSheets.SkillSheet, RandomObject = new TestRandom(), - GradeDict = gradeDict, }; var resultList = SynthesizeSimulator.Simulate(inputData); @@ -299,6 +288,9 @@ public void ExecuteNotEnoughActionPoint() { AvatarAddress = avatarAddress, MaterialIds = SynthesizeSimulator.GetItemGuids(items), + ChargeAp = false, + MaterialGradeId = (int)grade, + MaterialItemSubTypeId = (int)itemSubType, }; var ctx = new ActionContext @@ -336,6 +328,9 @@ public void ExecuteMultipleSameType(int testCount) { AvatarAddress = avatarAddress, MaterialIds = SynthesizeSimulator.GetItemGuids(items), + ChargeAp = false, + MaterialGradeId = (int)grade, + MaterialItemSubTypeId = (int)itemSubType, }; var ctx = new ActionContext @@ -380,6 +375,9 @@ public void ExecuteInvalidMaterial(Grade grade, ItemSubType[] itemSubTypes) { AvatarAddress = avatarAddress, MaterialIds = SynthesizeSimulator.GetItemGuids(items), + ChargeAp = false, + MaterialGradeId = (int)grade, + MaterialItemSubTypeId = (int)itemSubTypes[0], }; var ctx = new ActionContext diff --git a/Lib9c/Action/Synthesize.cs b/Lib9c/Action/Synthesize.cs index d0035cf080..1f5438af8e 100644 --- a/Lib9c/Action/Synthesize.cs +++ b/Lib9c/Action/Synthesize.cs @@ -11,6 +11,7 @@ using Nekoyume.Model.Item; using Nekoyume.Model.State; using Nekoyume.Helper; +using Nekoyume.Model.EnumType; using Nekoyume.Module; using Nekoyume.TableData; @@ -34,11 +35,30 @@ public class Synthesize : GameAction private const string MaterialsKey = "m"; private const string ChargeApKey = "c"; private const string AvatarAddressKey = "a"; + private const string GradeKey = "g"; + private const string ItemSubTypeKey = "i"; #region Fields + /// + /// Id list of items as material. + /// public List MaterialIds = new(); + /// + /// Whether to charge action points with action execution. + /// public bool ChargeAp; + /// + /// AvatarAddress of the signer. + /// public Address AvatarAddress; + /// + /// MaterialGrade of the material item. + /// + public int MaterialGradeId; + /// + /// ItemSubType of the material item. + /// + public int MaterialItemSubTypeId; #endregion Fields /// @@ -50,6 +70,8 @@ public override IWorld Execute(IActionContext context) { GasTracer.UseGas(1); var states = context.PreviousState; + var materialGrade = (Grade)MaterialGradeId; + var materialItemSubType = (ItemSubType)MaterialItemSubTypeId; // Collect addresses var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress); @@ -85,19 +107,34 @@ public override IWorld Execute(IActionContext context) // Calculate action point var actionPoint = CalculateActionPoint(states, avatarState, sheets, context); - - // Initialize variables - var gradeDict = SynthesizeSimulator.GetGradeDict(MaterialIds, avatarState, context.BlockIndex, - addressesHex, out var materialEquipments, out var materialCostumes); + var materialItems = SynthesizeSimulator.GetMaterialList( + MaterialIds, + avatarState, + context.BlockIndex, + materialGrade, + materialItemSubType, + addressesHex + ); // Unequip items (if necessary) - foreach (var materialEquipment in materialEquipments) + foreach (var materialItem in materialItems) { - materialEquipment.Unequip(); + switch (materialItem) + { + case Equipment equipment: + equipment.Unequip(); + break; + case Costume costume: + costume.Unequip(); + break; + } } - foreach (var materialCostume in materialCostumes) + + if (materialItems.Count == 0 || materialItems.Count != MaterialIds.Count) { - materialCostume.Unequip(); + throw new InvalidMaterialException( + $"{addressesHex} Aborted as the material item is not valid." + ); } // Remove materials from inventory @@ -113,6 +150,9 @@ public override IWorld Execute(IActionContext context) var synthesizedItems = SynthesizeSimulator.Simulate(new SynthesizeSimulator.InputData() { + Grade = materialGrade, + ItemSubType = materialItemSubType, + MaterialCount = materialItems.Count, SynthesizeSheet = sheets.GetSheet(), SynthesizeWeightSheet = sheets.GetSheet(), CostumeItemSheet = sheets.GetSheet(), @@ -123,7 +163,6 @@ public override IWorld Execute(IActionContext context) SkillSheet = sheets.GetSheet(), BlockIndex = context.BlockIndex, RandomObject = context.GetRandom(), - GradeDict = gradeDict, }); // Add synthesized items to inventory @@ -184,6 +223,8 @@ private long CalculateActionPoint(IWorld states, AvatarState avatarState, Sheets [MaterialsKey] = new List(MaterialIds.OrderBy(i => i).Select(i => i.Serialize())), [ChargeApKey] = ChargeAp.Serialize(), [AvatarAddressKey] = AvatarAddress.Serialize(), + [GradeKey] = (Integer)MaterialGradeId, + [ItemSubTypeKey] = (Integer)MaterialItemSubTypeId, } .ToImmutableDictionary(); @@ -192,6 +233,8 @@ protected override void LoadPlainValueInternal(IImmutableDictionary>; - /// /// Represents the result of the synthesis. /// @@ -33,25 +31,23 @@ public struct SynthesizeResult /// public static class SynthesizeSimulator { - private static readonly ItemType[] ValidItemType = - { - ItemType.Costume, - ItemType.Equipment, - }; - - private static readonly ItemSubType[] ValidItemSubType = - { - ItemSubType.FullCostume, - ItemSubType.Title, - ItemSubType.Grimoire, - ItemSubType.Aura, - }; - /// /// Simulate the synthesis of items. /// public struct InputData { + /// + /// The grade of the material item. + /// + public Grade Grade; + /// + /// The subtype of the material item. + /// + public ItemSubType ItemSubType; + /// + /// The number of materials. + /// + public int MaterialCount; /// /// The sheet that contains the synthesis information. /// @@ -101,10 +97,6 @@ public struct InputData /// Caution: Must have the same seed as when the action is executed /// public IRandom RandomObject; - /// - /// The grade dictionary of the material items. - /// - public GradeDict GradeDict; } private struct EquipmentData @@ -132,159 +124,131 @@ public static List Simulate(InputData inputData) var synthesizeSheet = inputData.SynthesizeSheet; var random = inputData.RandomObject; - var gradeDict = inputData.GradeDict; // Calculate the number of items to be synthesized based on materials - foreach (var gradeItem in gradeDict) + var gradeId = (int)inputData.Grade; + + if (!synthesizeSheet.TryGetValue(gradeId, out var synthesizeRow)) { - var gradeId = gradeItem.Key; - var subTypeDict = gradeItem.Value; + throw new SheetRowNotFoundException( + $"Aborted as the synthesize row for grade ({gradeId}) was failed to load in {nameof(SynthesizeSheet)}", gradeId + ); + } - if (!synthesizeSheet.TryGetValue(gradeId, out var synthesizeRow)) + var itemSubType = inputData.ItemSubType; + var materialCount = inputData.MaterialCount; + + var requiredCount = synthesizeRow.RequiredCountDict[itemSubType].RequiredCount; + var succeedRate = synthesizeRow.RequiredCountDict[itemSubType].SucceedRate; + var synthesizeCount = materialCount / requiredCount; + var remainder = materialCount % requiredCount; + + if (synthesizeCount <= 0 || remainder != 0) + { + throw new NotEnoughMaterialException( + $"Aborted as the number of materials for grade {gradeId} and subtype {itemSubType} is not enough." + ); + } + + // Calculate success for each synthesis + for (var i = 0; i < synthesizeCount; i++) + { + // random value range is 0 ~ 9999 + // If the SucceedRate of the table is 0, use '<' for always to fail. + // and SucceedRate of the table is 10000, always success(because left value range is 0~9999) + var isSuccess = random.Next(SynthesizeSheet.SucceedRateMax) < succeedRate; + + var grade = (Grade)gradeId; + // Decide the item to add to inventory based on SynthesizeWeightSheet + var synthesizedItem = GetSynthesizedItem( + grade, + isSuccess, + inputData.SynthesizeWeightSheet, + inputData.CostumeItemSheet, + inputData.EquipmentItemSheet, + inputData.EquipmentItemRecipeSheet, + inputData.EquipmentItemSubRecipeSheetV2, + inputData.EquipmentItemOptionSheet, + inputData.SkillSheet, + inputData.BlockIndex, + random, + itemSubType, + out var equipmentData); + + if (isSuccess && grade == (Grade)synthesizedItem.Grade) { - throw new SheetRowNotFoundException( - $"Aborted as the synthesize row for grade ({gradeId}) was failed to load in {nameof(SynthesizeSheet)}", gradeId - ); + // If there are no items in the data that are one above the current grade, they cannot succeed. + isSuccess = false; } - foreach (var subTypeItem in subTypeDict) + synthesizeResults.Add(new SynthesizeResult { - var itemSubType = subTypeItem.Key; - var materialCount = subTypeItem.Value; - - // TODO: subType별로 필요한 아이템 개수가 다를 수 있음 - var requiredCount = synthesizeRow.RequiredCountDict[itemSubType].RequiredCount; - var succeedRate = synthesizeRow.RequiredCountDict[itemSubType].SucceedRate; - var synthesizeCount = materialCount / requiredCount; - var remainder = materialCount % requiredCount; - - if (synthesizeCount <= 0 || remainder != 0) - { - throw new NotEnoughMaterialException( - $"Aborted as the number of materials for grade {gradeId} and subtype {itemSubType} is not enough." - ); - } - - // Calculate success for each synthesis - for (var i = 0; i < synthesizeCount; i++) - { - // random value range is 0 ~ 9999 - // If the SucceedRate of the table is 0, use '<' for always to fail. - // and SucceedRate of the table is 10000, always success(because left value range is 0~9999) - var isSuccess = random.Next(SynthesizeSheet.SucceedRateMax) < succeedRate; - - var grade = (Grade)gradeId; - // Decide the item to add to inventory based on SynthesizeWeightSheet - var synthesizedItem = GetSynthesizedItem( - grade, - isSuccess, - inputData.SynthesizeWeightSheet, - inputData.CostumeItemSheet, - inputData.EquipmentItemSheet, - inputData.EquipmentItemRecipeSheet, - inputData.EquipmentItemSubRecipeSheetV2, - inputData.EquipmentItemOptionSheet, - inputData.SkillSheet, - inputData.BlockIndex, - random, - itemSubType, - out var equipmentData); - - if (isSuccess && grade == (Grade)synthesizedItem.Grade) - { - // If there are no items in the data that are one above the current grade, they cannot succeed. - isSuccess = false; - } - - synthesizeResults.Add(new SynthesizeResult - { - ItemBase = synthesizedItem, - IsSuccess = isSuccess, - RecipeId = equipmentData.RecipeId, - SubRecipeId = equipmentData.SubRecipeId, - }); - } - } + ItemBase = synthesizedItem, + IsSuccess = isSuccess, + RecipeId = equipmentData.RecipeId, + SubRecipeId = equipmentData.SubRecipeId, + }); } return synthesizeResults; } - /// - /// Get the grade of the material items and return the dictionary of the grade and the number of items. - /// - /// material item ids - /// target avatar state - /// current block index - /// addresses in hex - /// material equipment list - /// material costume list - /// - /// - /// - public static GradeDict GetGradeDict(List materialIds, AvatarState avatarState, long blockIndex, - string addressesHex, out List materialEquipments, out List materialCostumes) + public static List GetMaterialList( + List materialIds, AvatarState avatarState, long blockIndex, + Grade grade, ItemSubType itemSubType, + string addressesHex) { - ItemSubType? cachedItemSubType = null; - var gradeDict = new GradeDict(); - materialEquipments = new List(); - materialCostumes = new List(); + return itemSubType switch + { + ItemSubType.FullCostume or ItemSubType.Title => GetCostumeMaterialList(materialIds, avatarState, grade, itemSubType, addressesHex), + ItemSubType.Aura or ItemSubType.Grimoire => GetEquipmentMaterialList(materialIds, avatarState, blockIndex, grade, itemSubType, addressesHex), + _ => throw new ArgumentException($"Invalid item sub type: {itemSubType}", nameof(itemSubType)), + }; + } + public static List GetEquipmentMaterialList( + List materialIds, AvatarState avatarState, long blockIndex, + Grade grade, ItemSubType itemSubType, + string addressesHex) + { + var materialEquipments = new List(); foreach (var materialId in materialIds) { - var materialEquipment = GetEquipmentFromId(materialId, avatarState, blockIndex, addressesHex, ref cachedItemSubType); - var materialCostume = GetCostumeFromId(materialId, avatarState, addressesHex, ref cachedItemSubType); - if (materialEquipment == null && materialCostume == null) + var materialEquipment = GetEquipmentFromId(materialId, avatarState, grade, itemSubType, blockIndex, addressesHex); + if (materialEquipment == null) { throw new InvalidMaterialException( - $"{addressesHex} Aborted as the material item is not a valid item type." + $"{addressesHex} Aborted as the material item is not a equipment item type." ); } - if (materialEquipment != null) - { - materialEquipments.Add(materialEquipment); - SetGradeDict(ref gradeDict, materialEquipment.Grade, materialEquipment.ItemSubType); - } - - if (materialCostume != null) - { - materialCostumes.Add(materialCostume); - SetGradeDict(ref gradeDict, materialCostume.Grade, materialCostume.ItemSubType); - } - } - - if (cachedItemSubType == null) - { - throw new InvalidOperationException("ItemSubType is not set."); + materialEquipments.Add(materialEquipment); } - return gradeDict; + return materialEquipments; } - private static void SetGradeDict(ref GradeDict gradeDict, int grade, ItemSubType itemSubType) + public static List GetCostumeMaterialList( + List materialIds, AvatarState avatarState, Grade grade, ItemSubType itemSubType, string addressesHex) { - if (gradeDict.ContainsKey(grade)) + var materialCostumes = new List(); + foreach (var materialId in materialIds) { - if (gradeDict[grade].ContainsKey(itemSubType)) + var materialEquipment = GetCostumeFromId(materialId, avatarState, grade, itemSubType, addressesHex); + if (materialEquipment == null) { - gradeDict[grade][itemSubType]++; - } - else - { - gradeDict[grade][itemSubType] = 1; + throw new InvalidMaterialException( + $"{addressesHex} Aborted as the material item is not a equipment item type." + ); } + + materialCostumes.Add(materialEquipment); } - else - { - gradeDict[grade] = new Dictionary - { - { itemSubType, 1 }, - }; - } + + return materialCostumes; } - private static Equipment? GetEquipmentFromId(Guid materialId, AvatarState avatarState, long blockIndex, string addressesHex, ref ItemSubType? cachedItemSubType) + private static Equipment? GetEquipmentFromId(Guid materialId, AvatarState avatarState, Grade grade, ItemSubType itemSubType, long blockIndex, string addressesHex) { if (!avatarState.inventory.TryGetNonFungibleItem(materialId, out Equipment materialEquipment)) { @@ -300,32 +264,31 @@ private static void SetGradeDict(ref GradeDict gradeDict, int grade, ItemSubType } // Validate item type - if (!ValidItemType.Contains(materialEquipment.ItemType)) + if (materialEquipment.ItemType != ItemType.Equipment) { throw new InvalidMaterialException( $"{addressesHex} Aborted as the material item is not a valid item type: {materialEquipment.ItemType}." ); } - if (!ValidItemSubType.Contains(materialEquipment.ItemSubType)) + if (materialEquipment.Grade != (int)grade) { throw new InvalidMaterialException( - $"{addressesHex} Aborted as the material item is not a valid item sub type: {materialEquipment.ItemSubType}." + $"{addressesHex} Aborted as the material item is not a valid grade: {materialEquipment.Grade}." ); } - cachedItemSubType ??= materialEquipment.ItemSubType; - if (materialEquipment.ItemSubType != cachedItemSubType) + if (materialEquipment.ItemSubType != itemSubType) { throw new InvalidMaterialException( - $"{addressesHex} Aborted as the material item is not a {cachedItemSubType}, but {materialEquipment.ItemSubType}." - ); + $"{addressesHex} Aborted as the material item is not a valid item sub type: {materialEquipment.ItemSubType}." + ); } return materialEquipment; } - private static Costume? GetCostumeFromId(Guid materialId, AvatarState avatarState, string addressesHex, ref ItemSubType? cachedItemSubType) + private static Costume? GetCostumeFromId(Guid materialId, AvatarState avatarState, Grade grade, ItemSubType itemSubType, string addressesHex) { if (!avatarState.inventory.TryGetNonFungibleItem(materialId, out Costume costumeItem)) { @@ -333,26 +296,25 @@ private static void SetGradeDict(ref GradeDict gradeDict, int grade, ItemSubType } // Validate item type - if (!ValidItemType.Contains(costumeItem.ItemType)) + if (costumeItem.ItemType != ItemType.Costume) { throw new InvalidMaterialException( $"{addressesHex} Aborted as the material item is not a valid item type: {costumeItem.ItemType}." ); } - if (!ValidItemSubType.Contains(costumeItem.ItemSubType)) + if (costumeItem.Grade != (int)grade) { throw new InvalidMaterialException( - $"{addressesHex} Aborted as the material item is not a valid item sub type: {costumeItem.ItemSubType}." + $"{addressesHex} Aborted as the material item is not a valid grade: {costumeItem.Grade}." ); } - cachedItemSubType ??= costumeItem.ItemSubType; - if (costumeItem.ItemSubType != cachedItemSubType) + if (costumeItem.ItemSubType != itemSubType) { throw new InvalidMaterialException( - $"{addressesHex} Aborted as the material item is not a {cachedItemSubType}, but {costumeItem.ItemSubType}." - ); + $"{addressesHex} Aborted as the material item is not a valid item sub type: {costumeItem.ItemSubType}." + ); } return costumeItem; @@ -403,7 +365,7 @@ private static ItemBase GetSynthesizedItem( private static ItemBase GetRandomCostume(Grade grade, bool isSuccess, ItemSubType itemSubType, SynthesizeWeightSheet weightSheet, CostumeItemSheet costumeItemSheet, IRandom random) { - HashSet? synthesizeResultPool = null; + HashSet? synthesizeResultPool; if (isSuccess) { synthesizeResultPool = GetSynthesizeResultPool(GetTargetGrade(grade), itemSubType, costumeItemSheet); @@ -459,7 +421,7 @@ private static ItemBase GetRandomEquipment( IRandom random, ref EquipmentData equipmentData) { - HashSet? synthesizeResultPool = null; + HashSet? synthesizeResultPool; if (isSuccess) { synthesizeResultPool = GetSynthesizeResultPool(GetTargetGrade(grade), itemSubType, equipmentItemSheet); @@ -602,15 +564,15 @@ public static HashSet GetSynthesizeResultPool(Grade sourceGrade, ItemSubTyp /// grades of material items /// excepted FullCostume,Title /// CostumeItemSheet to use - /// list of items key(int) - public static List GetSynthesizeResultPool(List sourceGrades, ItemSubType subType, CostumeItemSheet sheet) + /// list of items key(int), grade(Grade) tuple + public static HashSet<(int, Grade)> GetSynthesizeResultPool(HashSet sourceGrades, ItemSubType subType, CostumeItemSheet sheet) { return sheet .Values .Where(r => r.ItemSubType == subType) - .Where(r => sourceGrades.Any(grade => (Grade)r.Grade == GetUpgradeGrade(grade, subType, sheet))) - .Select(r => r.Id) - .ToList(); + .Where(r => sourceGrades.Any(grade => (Grade)r.Grade == grade)) + .Select(r => (r.Id, (Grade)r.Grade)) + .ToHashSet(); } /// @@ -619,15 +581,15 @@ public static List GetSynthesizeResultPool(List sourceGrades, ItemSu /// grades of material items /// excepted Grimoire,Aura /// EquipmentItemSheet to use - /// list of items key(int) - public static List GetSynthesizeResultPool(List sourceGrades, ItemSubType subType, EquipmentItemSheet sheet) + /// list of items key(int), grade(Grade) tuple + public static HashSet<(int, Grade)> GetSynthesizeResultPool(HashSet sourceGrades, ItemSubType subType, EquipmentItemSheet sheet) { return sheet .Values .Where(r => r.ItemSubType == subType) - .Where(r => sourceGrades.Any(grade => (Grade)r.Grade == GetUpgradeGrade(grade, subType, sheet))) - .Select(r => r.Id) - .ToList(); + .Where(r => sourceGrades.Any(grade => (Grade)r.Grade == grade)) + .Select(r => (r.Id, (Grade)r.Grade)) + .ToHashSet(); } /// @@ -707,7 +669,14 @@ public static List GetItemGuids(IEnumerable itemBases) => itemBa }; }).ToList(); - private static Grade GetTargetGrade(Grade grade) => grade switch + /// + /// Get the target grade of the item. + /// max grade is Divinity + /// + /// grade of the item + /// target grade + /// + public static Grade GetTargetGrade(Grade grade) => grade switch { Grade.Normal => Grade.Rare, Grade.Rare => Grade.Epic,