Skip to content

Commit

Permalink
Merge pull request #3088 from planetarium/feature/issue-3006
Browse files Browse the repository at this point in the history
�Introduce ClaimPatrolReward
  • Loading branch information
ipdae authored Dec 27, 2024
2 parents 5a221f1 + 219c40d commit cca5b2a
Show file tree
Hide file tree
Showing 14 changed files with 652 additions and 32 deletions.
111 changes: 111 additions & 0 deletions .Lib9c.Tests/Action/ClaimPatrolRewardTest.cs
Original file line number Diff line number Diff line change
@@ -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<PatrolRewardMail>(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<RequiredBlockIndexException>(() => 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<InvalidAddressException>(() => action.Execute(new ActionContext
{
Signer = signer,
BlockIndex = 0,
PreviousState = _initialState,
}));
}
}
}
88 changes: 88 additions & 0 deletions .Lib9c.Tests/TableData/Event/PatrolRewardSheetTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
namespace Lib9c.Tests.TableData.Event
{
using System.Linq;
using Nekoyume.TableData.Event;
using Xunit;

public class PatrolRewardSheetTest
{
private readonly PatrolRewardSheet _sheet = new ();

public PatrolRewardSheetTest()
{
const string csv =
"id,start,end,interval,min_level,max_level,reward_count,reward_item_id,reward_ticker,reward_count,reward_item_id,reward_ticker,reward_count,reward_item_id,reward_ticker,reward_count,reward_item_id,reward_ticker\n1,0,100,8400,1,200,1,500000,,1,600201,\n2,100,200,8400,201,350,1,500000,,2,600201,,100000,,CRYSTAL\n3,200,300,8400,351,,2,500000,,3,600201,,200000,,CRYSTAL,1,600202,\n";
_sheet.Set(csv);
}

[Fact]
public void Set()
{
var row = _sheet[1];
Assert.Equal(0L, row.StartedBlockIndex);
Assert.Equal(100L, row.EndedBlockIndex);
Assert.Equal(8400L, row.Interval);
Assert.Equal(1, row.MinimumLevel);
Assert.Equal(200, row.MaxLevel);
var apReward = row.Rewards.First();
Assert.Equal(1, apReward.Count);
Assert.Equal(500000, apReward.ItemId);
Assert.True(string.IsNullOrEmpty(apReward.Ticker));
var gdReward = row.Rewards.Last();
Assert.Equal(1, gdReward.Count);
Assert.Equal(600201, gdReward.ItemId);
Assert.True(string.IsNullOrEmpty(gdReward.Ticker));

row = _sheet[2];
Assert.Equal(100L, row.StartedBlockIndex);
Assert.Equal(200L, row.EndedBlockIndex);
Assert.Equal(8400L, row.Interval);
Assert.Equal(201, row.MinimumLevel);
Assert.Equal(350, row.MaxLevel);
apReward = row.Rewards.First();
Assert.Equal(1, apReward.Count);
Assert.Equal(500000, apReward.ItemId);
Assert.True(string.IsNullOrEmpty(apReward.Ticker));
gdReward = row.Rewards[1];
Assert.Equal(2, gdReward.Count);
Assert.Equal(600201, gdReward.ItemId);
Assert.True(string.IsNullOrEmpty(gdReward.Ticker));
var crystalReward = row.Rewards.Last();
Assert.Equal(100000, crystalReward.Count);
Assert.Equal(0, crystalReward.ItemId);
Assert.Equal("CRYSTAL", crystalReward.Ticker);

row = _sheet[3];
Assert.Equal(200L, row.StartedBlockIndex);
Assert.Equal(300L, row.EndedBlockIndex);
Assert.Equal(8400L, row.Interval);
Assert.Equal(351, row.MinimumLevel);
Assert.Null(row.MaxLevel);
apReward = row.Rewards.First();
Assert.Equal(2, apReward.Count);
Assert.Equal(500000, apReward.ItemId);
Assert.True(string.IsNullOrEmpty(apReward.Ticker));
gdReward = row.Rewards[1];
Assert.Equal(3, gdReward.Count);
Assert.Equal(600201, gdReward.ItemId);
Assert.True(string.IsNullOrEmpty(gdReward.Ticker));
crystalReward = row.Rewards[2];
Assert.Equal(200000, crystalReward.Count);
Assert.Equal(0, crystalReward.ItemId);
Assert.Equal("CRYSTAL", crystalReward.Ticker);
var rdReward = row.Rewards.Last();
Assert.Equal(1, rdReward.Count);
Assert.Equal(600202, rdReward.ItemId);
Assert.True(string.IsNullOrEmpty(rdReward.Ticker));
}

[Theory]
[InlineData(2, 1, 0)]
[InlineData(350, 2, 150)]
[InlineData(500, 3, 300)]
public void FindByLevel(int level, int id, long blockIndex)
{
Assert.Equal(id, _sheet.FindByLevel(level, blockIndex).Id);
}
}
}
2 changes: 2 additions & 0 deletions .Lib9c.Tests/TableSheets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ public TableSheets(Dictionary<string, string> sheets, bool ignoreFailedGetProper

public SynthesizeWeightSheet SynthesizeWeightSheet { get; private set; }

public PatrolRewardSheet PatrolRewardSheet { get; private set; }

public void ItemSheetInitialize()
{
ItemSheet ??= new ItemSheet();
Expand Down
17 changes: 2 additions & 15 deletions Lib9c/Action/ClaimGifts.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Libplanet.Crypto;
using Nekoyume.Exceptions;
using Nekoyume.Extensions;
using Nekoyume.Helper;
using Nekoyume.Model.Item;
using Nekoyume.Model.State;
using Nekoyume.Module;
Expand Down Expand Up @@ -123,21 +124,7 @@ public override IWorld Execute(IActionContext context)
foreach (var (itemId, quantity, tradable) in giftRow.Items)
{
var itemRow = itemSheet[itemId];
if (itemRow is MaterialItemSheet.Row materialRow)
{
var item = tradable
? ItemFactory.CreateTradableMaterial(materialRow)
: ItemFactory.CreateMaterial(materialRow);
inventory.AddItem(item, quantity);
}
else
{
foreach (var _ in Enumerable.Range(0, quantity))
{
var item = ItemFactory.CreateItem(itemRow, random);
inventory.AddItem(item);
}
}
inventory.MintItem(itemRow, quantity, tradable, random);
}

claimedGiftIds.Add(giftRow.Id);
Expand Down
18 changes: 2 additions & 16 deletions Lib9c/Action/ClaimItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -134,22 +135,7 @@ public override IWorld Execute(IActionContext context)
// it's only right that this is fixed in Inventory.
var itemRow = itemSheet[itemId];
var itemCount = (int)fungibleAssetValue.RawValue;
if (itemRow is MaterialItemSheet.Row materialRow)
{
var item = tradable
? ItemFactory.CreateTradableMaterial(materialRow)
: ItemFactory.CreateMaterial(materialRow);
avatarState.inventory.AddItem(item, itemCount);
}
else
{
foreach (var _ in Enumerable.Range(0, itemCount))
{
var item = ItemFactory.CreateItem(itemRow, random);
avatarState.inventory.AddItem(item);
}
}

avatarState.inventory.MintItem(itemRow, itemCount, tradable, random);
items.Add((itemRow.Id, itemCount));
}
}
Expand Down
120 changes: 120 additions & 0 deletions Lib9c/Action/ClaimPatrolReward.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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.Mail;
using Nekoyume.Model.State;
using Nekoyume.Module;
using Nekoyume.TableData.Event;

namespace Nekoyume.Action
{
/// <summary>
/// Claim patrol reward
/// </summary>
[Serializable]
[ActionType(TypeIdentifier)]
public class ClaimPatrolReward : ActionBase
{
public const string TypeIdentifier = "claim_patrol_reward";

/// <summary>
/// The address of the avatar to receive the patrol reward.
/// </summary>
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;

// Validate that the avatar address belongs to the signer.
// This ensures that only the owner of the avatar can claim the patrol reward for it.
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<PatrolRewardSheet>();
var itemSheet = sheets.GetItemSheet();

// validate
states.TryGetPatrolRewardClaimedBlockIndex(AvatarAddress, out var claimedBlockIndex);
var row = patrolRewardSheet.FindByLevel(avatarLevel, context.BlockIndex);

// Ensure rewards cannot be claimed too frequently.
// If the last claimed block index is set and the current block index is less than the allowed interval, throw an exception.
if (claimedBlockIndex > 0L && claimedBlockIndex + row.Interval > context.BlockIndex)
{
throw new RequiredBlockIndexException();
}

// mint rewards
var random = context.GetRandom();
var fav = new List<FungibleAssetValue>();
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 asset = currency * reward.Count;
states = states.MintAsset(context, recipient, asset);
fav.Add(asset);
}
}

var mailBox = avatarState.mailBox;
var mail = new PatrolRewardMail(context.BlockIndex, random.GenerateRandomGuid(), context.BlockIndex, fav, items);
mailBox.Add(mail);
mailBox.CleanUp();
avatarState.mailBox = mailBox;

// set states
return states
.SetAvatarState(AvatarAddress, avatarState, setAvatar: true, setInventory: true, setWorldInformation: false, setQuestList: false)
.SetPatrolRewardClaimedBlockIndex(AvatarAddress, context.BlockIndex);
}

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

0 comments on commit cca5b2a

Please sign in to comment.