Skip to content

Commit

Permalink
Merge pull request #2982 from planetarium/feature/claim-one-time-gift
Browse files Browse the repository at this point in the history
Introduce `ClaimGifts`
  • Loading branch information
tyrosine1153 authored Nov 12, 2024
2 parents 4f90c1a + 49680cb commit 0adc4ea
Show file tree
Hide file tree
Showing 11 changed files with 504 additions and 0 deletions.
181 changes: 181 additions & 0 deletions .Lib9c.Tests/Action/ClaimGiftsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
namespace Lib9c.Tests.Action
{
using System;
using System.Linq;
using Libplanet.Action.State;
using Libplanet.Crypto;
using Libplanet.Mocks;
using Nekoyume;
using Nekoyume.Action;
using Nekoyume.Model.Item;
using Nekoyume.Model.State;
using Nekoyume.Module;
using Xunit;

public class ClaimGiftsTest
{
private readonly TableSheets _tableSheets;
private readonly IWorld _state;

public ClaimGiftsTest()
{
_state = new World(MockUtil.MockModernWorldState);

var tableCsv = TableSheetsImporter.ImportSheets();
foreach (var (key, value) in tableCsv)
{
_state = _state.SetLegacyState(Addresses.GetSheetAddress(key), value.Serialize());
}

_tableSheets = new TableSheets(tableCsv);
}

[Theory]
[InlineData(1)]
[InlineData(300)]
[InlineData(600)]
[InlineData(1200)]
public void Execute_Success(long blockIndex)
{
var agentAddress = new PrivateKey().Address;
var avatarAddress = Addresses.GetAvatarAddress(agentAddress, 0);

var avatarState = AvatarState.Create(
avatarAddress,
agentAddress,
0,
_tableSheets.GetAvatarSheets(),
default);
var state = _state.SetAvatarState(avatarAddress, avatarState);

if (!_tableSheets.ClaimableGiftsSheet.TryFindRowByBlockIndex(blockIndex, out var row))
{
throw new Exception();
}

Execute(
state,
avatarAddress,
agentAddress,
row.Id,
blockIndex,
row.Items.ToArray()
);
}

[Fact]
public void Execute_ClaimableGiftsNotAvailableException()
{
var agentAddress = new PrivateKey().Address;
var avatarAddress = Addresses.GetAvatarAddress(agentAddress, 0);

var avatarState = AvatarState.Create(
avatarAddress,
agentAddress,
0,
_tableSheets.GetAvatarSheets(),
default);
var state = _state.SetAvatarState(avatarAddress, avatarState);
var sheet = _tableSheets.ClaimableGiftsSheet;

Assert.Throws<ClaimableGiftsNotAvailableException>(() =>
{
var row = sheet.Values.OrderBy(row => row.StartedBlockIndex).First();
Execute(
state,
avatarAddress,
agentAddress,
row.Id,
row.StartedBlockIndex - 1,
row.Items.ToArray()
);
});
Assert.Throws<ClaimableGiftsNotAvailableException>(() =>
{
var row = sheet.Values.OrderByDescending(row => row.EndedBlockIndex).First();
Execute(
state,
avatarAddress,
agentAddress,
row.Id,
row.EndedBlockIndex + 1,
row.Items.ToArray()
);
});
}

[Fact]
public void Execute_AlreadyClaimedGiftsException()
{
var agentAddress = new PrivateKey().Address;
var avatarAddress = Addresses.GetAvatarAddress(agentAddress, 0);

var avatarState = AvatarState.Create(
avatarAddress,
agentAddress,
0,
_tableSheets.GetAvatarSheets(),
default);
var state = _state.SetAvatarState(avatarAddress, avatarState);

var row = _tableSheets.ClaimableGiftsSheet.Values.First();
var blockIndex = row.StartedBlockIndex;

var nextState = Execute(
state,
avatarAddress,
agentAddress,
row.Id,
blockIndex,
row.Items.ToArray()
);
Assert.Throws<AlreadyClaimedGiftsException>(() =>
{
Execute(
nextState,
avatarAddress,
agentAddress,
row.Id,
blockIndex + 1,
row.Items.ToArray()
);
});
}

private IWorld Execute(
IWorld previousState,
Address avatarAddress,
Address agentAddress,
int giftId,
long blockIndex,
(int itemId, int quantity, bool tradable)[] expected)
{
var prevClaimedGifts = _state.GetClaimedGifts(avatarAddress);

var action = new ClaimGifts(avatarAddress, giftId);
var actionContext = new ActionContext
{
PreviousState = previousState,
Signer = agentAddress,
BlockIndex = blockIndex,
};

var nextState = action.Execute(actionContext);

// Check claimed gifts.
var nextClaimedGifts = nextState.GetClaimedGifts(avatarAddress);
Assert.Equal(prevClaimedGifts.Count + 1, nextClaimedGifts.Count);

// Check Inventory.
var inventory = nextState.GetInventoryV2(avatarAddress);
foreach (var (itemId, quantity, tradable) in expected)
{
Assert.True(inventory.TryGetItem(itemId, out var inventoryItem));
Assert.Equal(quantity, inventoryItem.count);
Assert.Equal(tradable, inventoryItem.item is ITradableItem);
}

return nextState;
}
}
}
33 changes: 33 additions & 0 deletions .Lib9c.Tests/TableData/Event/ClaimableGiftsSheetTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace Lib9c.Tests.TableData.Event
{
using Nekoyume.TableData;
using Xunit;

public class ClaimableGiftsSheetTest
{
[Fact]
public void Set()
{
const string csv = @"id,started_block_index,ended_block_index,item_1_id,item_1_quantity,item_1_tradable,item_2_id,item_2_quantity,item_2_tradable,item_3_id,item_3_quantity,item_3_tradable,item_4_id,item_4_quantity,item_4_tradable,item_5_id,item_5_quantity,item_5_tradable
1,1,250,600402,5,true,,,,,,,,,,,,
2,251,500,40100030,1,true,,,,,,,,,,,,
3,501,1000,49900022,1,true,,,,,,,,,,,,
4,1001,1500,40100028,1,true,,,,,,,,,,,,
5,1501,2000,400000,5,false,,,,,,,,,,,,";

var sheet = new ClaimableGiftsSheet();
sheet.Set(csv);
Assert.Equal(5, sheet.Count);
Assert.NotNull(sheet.First);
Assert.NotNull(sheet.Last);
var row = sheet.First;
Assert.Equal(1, row.Id);
Assert.Equal(1, row.StartedBlockIndex);
Assert.Equal(250, row.EndedBlockIndex);
Assert.Single(row.Items);
Assert.Equal(600402, row.Items[0].itemId);
Assert.Equal(5, row.Items[0].quantity);
Assert.True(row.Items[0].tradable);
}
}
}
2 changes: 2 additions & 0 deletions .Lib9c.Tests/TableSheets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,8 @@ public TableSheets(Dictionary<string, string> sheets, bool ignoreFailedGetProper

/* Custom Craft */

public ClaimableGiftsSheet ClaimableGiftsSheet { get; private set; }

public void ItemSheetInitialize()
{
ItemSheet ??= new ItemSheet();
Expand Down
21 changes: 21 additions & 0 deletions Lib9c/Action/AlreadyClaimedGiftsException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Runtime.Serialization;

namespace Nekoyume.Action
{
[Serializable]
public class AlreadyClaimedGiftsException : Exception
{
public AlreadyClaimedGiftsException()
{
}

public AlreadyClaimedGiftsException(string msg) : base(msg)
{
}

public AlreadyClaimedGiftsException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
}
150 changes: 150 additions & 0 deletions Lib9c/Action/ClaimGifts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
using System.Collections.Immutable;
using System.Linq;
using Bencodex.Types;
using Libplanet.Action;
using Libplanet.Action.State;
using Libplanet.Crypto;
using Nekoyume.Exceptions;
using Nekoyume.Extensions;
using Nekoyume.Model.Item;
using Nekoyume.Model.State;
using Nekoyume.Module;
using Nekoyume.TableData;
using static Lib9c.SerializeKeys;

namespace Nekoyume.Action
{
/// <summary>
/// An action to claim gifts using the Gift Id. <br/>
/// Refer <see cref="ClaimableGiftsSheet"/> to find gifts data.<br/>
/// It can only claim once at the specified block index.
/// </summary>
[ActionType(ActionTypeText)]
public class ClaimGifts : GameAction
{
private const string ActionTypeText = "claim_gifts";

/// <summary>
/// The address of the avatar claiming the gift.
/// </summary>
public Address AvatarAddress { get; private set; }
/// <summary>
/// The ID of the gift to be claimed. This ID is used in the <see cref="ClaimableGiftsSheet"/>.
/// </summary>
public int GiftId { get; private set; }
private const string GiftIdKey = "gi";

public ClaimGifts()
{
}

public ClaimGifts(Address avatarAddress, int giftId)
{
AvatarAddress = avatarAddress;
GiftId = giftId;
}

protected override IImmutableDictionary<string, IValue> PlainValueInternal =>
ImmutableDictionary<string, IValue>.Empty
.Add(AvatarAddressKey, AvatarAddress.Serialize())
.Add(GiftIdKey, GiftId.Serialize());

protected override void LoadPlainValueInternal(IImmutableDictionary<string, IValue> plainValue)
{
AvatarAddress = plainValue[AvatarAddressKey].ToAddress();
GiftId = plainValue[GiftIdKey].ToInteger();
}

/// <exception cref="FailedLoadStateException">Thrown when the inventory could not be loaded.</exception>
/// <exception cref="SheetRowNotFoundException">Thrown when the gift ID is not found in the <see cref="ClaimableGiftsSheet"/>.</exception>
/// <exception cref="ClaimableGiftsNotAvailableException">Thrown when the gift is not available at the current block index.</exception>
/// <exception cref="AlreadyClaimedGiftsException">Thrown when the gift has already been claimed.</exception>
public override IWorld Execute(IActionContext context)
{
context.UseGas(1);
var states = context.PreviousState;
var random = context.GetRandom();
var addressesHex = GetSignerAndOtherAddressesHex(context, AvatarAddress);

// NOTE: The `AvatarAddress` must contained in `Signer`'s `AgentState.avatarAddresses`.
if (!Addresses.CheckAvatarAddrIsContainedInAgent(context.Signer, AvatarAddress))
{
throw new InvalidActionFieldException(
ActionTypeText,
addressesHex,
nameof(AvatarAddress),
$"Signer({context.Signer}) is not contained in" +
$" AvatarAddress({AvatarAddress}).");
}

var inventory = states.GetInventoryV2(AvatarAddress);
if (inventory is null)
{
throw new FailedLoadStateException(
ActionTypeText,
addressesHex,
typeof(Inventory),
AvatarAddress);
}

var sheetTypes = new []
{
typeof(ClaimableGiftsSheet),
};
var sheets = states.GetSheets(
containItemSheet: true,
sheetTypes: sheetTypes);

var claimableGiftsSheet = sheets.GetSheet<ClaimableGiftsSheet>();
if (!claimableGiftsSheet.TryGetValue(GiftId, out var giftRow))
{
throw new SheetRowNotFoundException(
addressesHex,
nameof(claimableGiftsSheet),
GiftId);
}

if (!giftRow.Validate(context.BlockIndex))
{
throw new ClaimableGiftsNotAvailableException(
$"[{addressesHex}] Claimable gift is not available at block index: {context.BlockIndex}"
);
}

var claimedGiftIds = states.GetClaimedGifts(AvatarAddress);
if (claimedGiftIds.Contains(giftRow.Id))
{
throw new AlreadyClaimedGiftsException(
$"[{addressesHex}] Already claimed gift. You can only claim gift once : {giftRow.Id}"
);
}

var itemSheet = sheets.GetItemSheet();
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);
}
}
}

claimedGiftIds.Add(giftRow.Id);

return states
.SetClaimedGifts(AvatarAddress, claimedGiftIds)
.SetInventory(AvatarAddress, inventory);
}
}
}
Loading

0 comments on commit 0adc4ea

Please sign in to comment.