diff --git a/.Lib9c.Tests/Action/ClaimPatrolRewardTest.cs b/.Lib9c.Tests/Action/ClaimPatrolRewardTest.cs new file mode 100644 index 0000000000..17920e4d96 --- /dev/null +++ b/.Lib9c.Tests/Action/ClaimPatrolRewardTest.cs @@ -0,0 +1,111 @@ +namespace Lib9c.Tests.Action +{ + using System.Linq; + using Lib9c.Tests.Util; + using Libplanet.Action.State; + using Libplanet.Crypto; + using Libplanet.Mocks; + using Nekoyume; + using Nekoyume.Action; + using Nekoyume.Model.Mail; + using Nekoyume.Model.State; + using Nekoyume.Module; + using Serilog; + using Xunit; + using Xunit.Abstractions; + + public class ClaimPatrolRewardTest + { + private readonly IWorld _initialState; + private readonly TableSheets _tableSheets; + + public ClaimPatrolRewardTest(ITestOutputHelper outputHelper) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.TestOutput(outputHelper) + .CreateLogger(); + + _initialState = new World(MockUtil.MockModernWorldState); + + var sheets = TableSheetsImporter.ImportSheets(); + foreach (var (key, value) in sheets) + { + _initialState = _initialState + .SetLegacyState(Addresses.TableSheet.Derive(key), value.Serialize()); + } + + _tableSheets = new TableSheets(sheets); + } + + [Fact] + public void Execute() + { + var privateKey = new PrivateKey(); + var agentAddress = privateKey.Address; + var row = _tableSheets.PatrolRewardSheet.Values.First(); + var (state, avatar, _) = InitializeUtil.AddAvatar(_initialState, _tableSheets.GetAvatarSheets(), agentAddress); + var avatarAddress = avatar.address; + var action = new ClaimPatrolReward(avatar.address); + + var nextState = action.Execute(new ActionContext + { + Signer = agentAddress, + BlockIndex = 1L, + PreviousState = state, + RandomSeed = 0, + }); + + var avatarState = nextState.GetAvatarState(avatarAddress); + var itemSheet = _tableSheets.ItemSheet; + var mail = Assert.IsType(avatarState.mailBox.Single()); + + foreach (var reward in row.Rewards) + { + var ticker = reward.Ticker; + if (string.IsNullOrEmpty(ticker)) + { + var itemId = reward.ItemId; + var rowId = itemSheet[itemId].Id; + Assert.True(avatarState.inventory.HasItem(rowId, reward.Count)); + var item = mail.Items.First(i => i.id == itemId); + Assert.Equal(item.count, reward.Count); + } + else + { + var currency = Currencies.GetMinterlessCurrency(ticker); + var recipient = Currencies.PickAddress(currency, agentAddress, avatarAddress); + var fav = nextState.GetBalance(recipient, currency); + Assert.Equal(currency * reward.Count, fav); + Assert.Contains(fav, mail.FungibleAssetValues); + } + } + + Assert.Equal(1L, nextState.GetPatrolRewardClaimedBlockIndex(avatarAddress)); + Assert.True(row.Interval > 1L); + + // Throw RequiredBlockIndex by reward interval + Assert.Throws(() => action.Execute(new ActionContext + { + Signer = agentAddress, + BlockIndex = 2L, + PreviousState = nextState, + RandomSeed = 0, + })); + } + + [Fact] + public void Execute_Throw_InvalidAddressException() + { + var signer = new PrivateKey().Address; + var action = new ClaimPatrolReward(signer); + + Assert.Throws(() => action.Execute(new ActionContext + { + Signer = signer, + BlockIndex = 0, + PreviousState = _initialState, + })); + } + } +} diff --git a/Lib9c/Action/ClaimPatrolReward.cs b/Lib9c/Action/ClaimPatrolReward.cs new file mode 100644 index 0000000000..61f9a63f22 --- /dev/null +++ b/Lib9c/Action/ClaimPatrolReward.cs @@ -0,0 +1,113 @@ +using System; +using System.Collections.Generic; +using Bencodex.Types; +using Lib9c; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Libplanet.Types.Assets; +using Nekoyume.Extensions; +using Nekoyume.Helper; +using Nekoyume.Model.Item; +using Nekoyume.Model.Mail; +using Nekoyume.Model.State; +using Nekoyume.Module; +using Nekoyume.TableData; +using Nekoyume.TableData.Event; + +namespace Nekoyume.Action +{ + /// + /// Claim patrol reward + /// + [Serializable] + [ActionType(TypeIdentifier)] + public class ClaimPatrolReward : ActionBase + { + public const string TypeIdentifier = "claim_patrol_reward"; + public Address AvatarAddress; + + public ClaimPatrolReward() + { + } + + public ClaimPatrolReward(Address avatarAddress) + { + AvatarAddress = avatarAddress; + } + public override IWorld Execute(IActionContext context) + { + GasTracer.UseGas(1); + var signer = context.Signer; + var states = context.PreviousState; + if (!Addresses.CheckAvatarAddrIsContainedInAgent(signer, AvatarAddress)) + { + throw new InvalidAddressException(); + } + + // avatar + var avatarState = states.GetAvatarState(AvatarAddress, true, false, false); + var avatarLevel = avatarState.level; + var inventory = avatarState.inventory; + + // sheets + var sheets = states.GetSheets(containItemSheet: true, sheetTypes: new[] + { + typeof(PatrolRewardSheet) + }); + var patrolRewardSheet = sheets.GetSheet(); + var itemSheet = sheets.GetItemSheet(); + + // validate + states.TryGetPatrolRewardClaimedBlockIndex(AvatarAddress, out var claimedBlockIndex); + var row = patrolRewardSheet.FindByLevel(avatarLevel, context.BlockIndex); + if (claimedBlockIndex > 0L && claimedBlockIndex + row.Interval > context.BlockIndex) + { + throw new RequiredBlockIndexException(); + } + + // mit rewards + var random = context.GetRandom(); + var favs = new List(); + var items = new List<(int id, int count)>(); + foreach (var reward in row.Rewards) + { + var ticker = reward.Ticker; + if (string.IsNullOrEmpty(ticker)) + { + var itemRow = itemSheet[reward.ItemId]; + inventory.MintItem(itemRow, reward.Count, false, random); + items.Add(new (reward.ItemId, reward.Count)); + } + else + { + var currency = Currencies.GetMinterlessCurrency(ticker); + var recipient = Currencies.PickAddress(currency, signer, AvatarAddress); + var fav = currency * reward.Count; + states = states.MintAsset(context, recipient, fav); + favs.Add(fav); + } + } + + var mailBox = avatarState.mailBox; + var mail = new PatrolRewardMail(context.BlockIndex, random.GenerateRandomGuid(), context.BlockIndex, favs, items); + mailBox.Add(mail); + mailBox.CleanUp(); + avatarState.mailBox = mailBox; + + // set states + states = states + .SetAvatarState(AvatarAddress, avatarState, setAvatar: true, setInventory: true, setWorldInformation: false, setQuestList: false) + .SetPatrolRewardClaimedBlockIndex(AvatarAddress, context.BlockIndex); + return states; + } + + public override IValue PlainValue => Dictionary.Empty + .Add("type_id", TypeIdentifier) + .Add("values", AvatarAddress.Serialize()); + public override void LoadPlainValue(IValue plainValue) + { + AvatarAddress = ((Dictionary)plainValue)["values"].ToAddress(); + } + } +} diff --git a/Lib9c/Addresses.cs b/Lib9c/Addresses.cs index 18097cdec2..5b4871a147 100644 --- a/Lib9c/Addresses.cs +++ b/Lib9c/Addresses.cs @@ -54,6 +54,7 @@ public static class Addresses public static readonly Address CombinationSlot = new("0000000000000000000000000000000000000024"); public static readonly Address ClaimedGiftIds = new("0000000000000000000000000000000000000025"); + public static readonly Address PatrolReward = new("0000000000000000000000000000000000000026"); // Adventure Boss public static readonly Address AdventureBoss = new("0000000000000000000000000000000000000100"); @@ -106,7 +107,7 @@ public static Address GetGuildBanAccountAddress(Address guildAddress) => /// /// An address of an account having . - /// + /// public static readonly Address GuildDelegateeMetadata = new Address("0000000000000000000000000000000000000210"); diff --git a/Lib9c/Module/PatrolRewardModule.cs b/Lib9c/Module/PatrolRewardModule.cs new file mode 100644 index 0000000000..37d0c9a0e5 --- /dev/null +++ b/Lib9c/Module/PatrolRewardModule.cs @@ -0,0 +1,43 @@ +using Bencodex.Types; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Action; + +namespace Nekoyume.Module +{ + public static class PatrolRewardModule + { + public static long GetPatrolRewardClaimedBlockIndex(this IWorldState worldState, Address avatarAddress) + { + var value = worldState.GetAccountState(Addresses.PatrolReward).GetState(avatarAddress); + if (value is Integer integer) + { + return integer; + } + + throw new FailedLoadStateException(""); + } + + public static bool TryGetPatrolRewardClaimedBlockIndex(this IWorldState worldState, Address avatarAddress, out long blockIndex) + { + blockIndex = 0L; + try + { + var temp = GetPatrolRewardClaimedBlockIndex(worldState, avatarAddress); + blockIndex = temp; + return true; + } + catch (FailedLoadStateException) + { + return false; + } + } + + public static IWorld SetPatrolRewardClaimedBlockIndex(this IWorld world, Address avatarAddress, long blockIndex) + { + var account = world.GetAccount(Addresses.PatrolReward); + account = account.SetState(avatarAddress, (Integer)blockIndex); + return world.SetAccount(Addresses.PatrolReward, account); + } + } +}