diff --git a/.Lib9c.Tests/Action/Scenario/WorldUnlockScenarioTest.cs b/.Lib9c.Tests/Action/Scenario/WorldUnlockScenarioTest.cs index c158ccce4c..5ef3fcdb83 100644 --- a/.Lib9c.Tests/Action/Scenario/WorldUnlockScenarioTest.cs +++ b/.Lib9c.Tests/Action/Scenario/WorldUnlockScenarioTest.cs @@ -3,9 +3,9 @@ namespace Lib9c.Tests.Action.Scenario using System; using System.Collections.Generic; using System.Linq; + using System.Text; using Libplanet.Action.State; using Libplanet.Crypto; - using Libplanet.Types.Assets; using Nekoyume; using Nekoyume.Action; using Nekoyume.Model; @@ -35,13 +35,12 @@ public WorldUnlockScenarioTest() _avatarAddress = _agentAddress.Derive("avatar"); _rankingMapAddress = _avatarAddress.Derive("ranking_map"); - var gameConfigState = new GameConfigState(sheets[nameof(GameConfigSheet)]); var avatarState = new AvatarState( _avatarAddress, _agentAddress, 0, _tableSheets.GetAvatarSheets(), - gameConfigState, + new GameConfigState(sheets[nameof(GameConfigSheet)]), _rankingMapAddress ) { @@ -51,18 +50,11 @@ public WorldUnlockScenarioTest() _weeklyArenaState = new WeeklyArenaState(0); -#pragma warning disable CS0618 - // Use of obsolete method Currency.Legacy(): https://github.com/planetarium/lib9c/discussions/1319 - var currency = Currency.Legacy("NCG", 2, null); -#pragma warning restore CS0618 - var goldCurrencyState = new GoldCurrencyState(currency); _initialState = new World(new MockWorldState()) - .SetLegacyState(Addresses.GoldCurrency, goldCurrencyState.Serialize()) .SetLegacyState(_weeklyArenaState.address, _weeklyArenaState.Serialize()) .SetAgentState(_agentAddress, agentState) .SetAvatarState(_avatarAddress, avatarState) - .SetLegacyState(_rankingMapAddress, new RankingMapState(_rankingMapAddress).Serialize()) - .SetLegacyState(gameConfigState.address, gameConfigState.Serialize()); + .SetLegacyState(_rankingMapAddress, new RankingMapState(_rankingMapAddress).Serialize()); foreach (var (key, value) in sheets) { @@ -98,20 +90,17 @@ public void UnlockWorldByHackAndSlashAfterPatchTableWithAddRow( var doomfist = Doomfist.GetOne(_tableSheets, avatarState.level, ItemSubType.Weapon); avatarState.inventory.AddItem(doomfist); - var nextState = _initialState - .SetAvatarState(_avatarAddress, avatarState) - .SetLegacyState( - _avatarAddress.Derive("world_ids"), - new Bencodex.Types.List(Enumerable.Range(1, worldIdToClear).Select(i => i.Serialize()))); - var hackAndSlash = new HackAndSlash + var nextState = _initialState.SetAvatarState(_avatarAddress, avatarState); + var hackAndSlash = new HackAndSlash3 { - WorldId = worldIdToClear, - StageId = stageIdToClear, - AvatarAddress = _avatarAddress, - Costumes = new List(), - Equipments = new List { doomfist.NonFungibleId }, - Foods = new List(), - RuneInfos = new List(), + worldId = worldIdToClear, + stageId = stageIdToClear, + avatarAddress = _avatarAddress, + costumes = new List(), + equipments = new List { doomfist.NonFungibleId }, + foods = new List(), + WeeklyArenaAddress = _weeklyArenaState.address, + RankingMapAddress = _rankingMapAddress, }; nextState = hackAndSlash.Execute(new ActionContext { @@ -119,6 +108,7 @@ public void UnlockWorldByHackAndSlashAfterPatchTableWithAddRow( Signer = _agentAddress, RandomSeed = 0, }); + Assert.True(hackAndSlash.Result.IsClear); avatarState = nextState.GetAvatarState(_avatarAddress); Assert.True(avatarState.worldInformation.IsStageCleared(stageIdToClear)); @@ -159,6 +149,7 @@ public void UnlockWorldByHackAndSlashAfterPatchTableWithAddRow( Signer = _agentAddress, RandomSeed = 0, }); + Assert.True(hackAndSlash.Result.IsClear); avatarState = nextState.GetAvatarState(_avatarAddress); Assert.True(avatarState.worldInformation.IsWorldUnlocked(worldIdToUnlock)); diff --git a/Lib9c/Action/HackAndSlash.cs b/Lib9c/Action/HackAndSlash.cs index e1ef713d78..a2b05c2592 100644 --- a/Lib9c/Action/HackAndSlash.cs +++ b/Lib9c/Action/HackAndSlash.cs @@ -508,14 +508,17 @@ public IWorld Execute( sw.Restart(); if (simulator.Log.IsClear) { - avatarState.worldInformation.ClearStage( - WorldId, - StageId, - blockIndex, - worldSheet, - worldUnlockSheet - ); - stageCleared = true; + if (!stageCleared) + { + avatarState.worldInformation.ClearStage( + WorldId, + StageId, + blockIndex, + worldSheet, + worldUnlockSheet + ); + stageCleared = true; + } sw.Stop(); Log.Verbose("{AddressesHex} {Source} HAS {Process} from #{BlockIndex}: {Elapsed}", addressesHex, source, "ClearStage", blockIndex, sw.Elapsed.TotalMilliseconds); diff --git a/Lib9c/Action/HackAndSlash3.cs b/Lib9c/Action/HackAndSlash3.cs new file mode 100644 index 0000000000..bf122cd283 --- /dev/null +++ b/Lib9c/Action/HackAndSlash3.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Bencodex.Types; +using Lib9c.Abstractions; +using Libplanet.Action; +using Libplanet.Action.State; +using Libplanet.Crypto; +using Nekoyume.Battle; +using Nekoyume.Model.BattleStatus; +using Nekoyume.Model.State; +using Nekoyume.Module; +using Nekoyume.TableData; +using Serilog; + +namespace Nekoyume.Action +{ + [Serializable] + [ActionObsolete(ActionObsoleteConfig.V200020AccidentObsoleteIndex)] + [ActionType("hack_and_slash3")] + public class HackAndSlash3 : GameAction, IHackAndSlashV1 + { + public List costumes; + public List equipments; + public List foods; + public int worldId; + public int stageId; + public Address avatarAddress; + public Address WeeklyArenaAddress; + public Address RankingMapAddress; + public BattleLog Result { get; private set; } + + IEnumerable IHackAndSlashV1.Costumes => costumes; + IEnumerable IHackAndSlashV1.Equipments => equipments; + IEnumerable IHackAndSlashV1.Foods => foods; + int IHackAndSlashV1.WorldId => worldId; + int IHackAndSlashV1.StageId => stageId; + Address IHackAndSlashV1.AvatarAddress => avatarAddress; + Address IHackAndSlashV1.WeeklyArenaAddress => WeeklyArenaAddress; + Address IHackAndSlashV1.RankingMapAddress => RankingMapAddress; + + protected override IImmutableDictionary PlainValueInternal => + new Dictionary + { + ["costumes"] = new List(costumes.OrderBy(i => i).Select(e => e.Serialize())), + ["equipments"] = new List(equipments.OrderBy(i => i).Select(e => e.Serialize())), + ["foods"] = new List(foods.OrderBy(i => i).Select(e => e.Serialize())), + ["worldId"] = worldId.Serialize(), + ["stageId"] = stageId.Serialize(), + ["avatarAddress"] = avatarAddress.Serialize(), + ["weeklyArenaAddress"] = WeeklyArenaAddress.Serialize(), + ["rankingMapAddress"] = RankingMapAddress.Serialize(), + }.ToImmutableDictionary(); + + + protected override void LoadPlainValueInternal( + IImmutableDictionary plainValue) + { + costumes = ((List) plainValue["costumes"]).Select(e => e.ToInteger()).ToList(); + equipments = ((List) plainValue["equipments"]).Select(e => e.ToGuid()).ToList(); + foods = ((List) plainValue["foods"]).Select(e => e.ToGuid()).ToList(); + worldId = plainValue["worldId"].ToInteger(); + stageId = plainValue["stageId"].ToInteger(); + avatarAddress = plainValue["avatarAddress"].ToAddress(); + WeeklyArenaAddress = plainValue["weeklyArenaAddress"].ToAddress(); + RankingMapAddress = plainValue["rankingMapAddress"].ToAddress(); + } + + public override IWorld Execute(IActionContext context) + { + context.UseGas(1); + IActionContext ctx = context; + var states = ctx.PreviousState; + + CheckObsolete(ActionObsoleteConfig.V100080ObsoleteIndex, context); + + var addressesHex = GetSignerAndOtherAddressesHex(context, avatarAddress); + + var sw = new Stopwatch(); + sw.Start(); + var started = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex}HAS 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}HAS Get AgentAvatarStates: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + + if (avatarState.RankingMapAddress != RankingMapAddress) + { + throw new InvalidAddressException($"{addressesHex}Invalid ranking map address"); + } + + // worldId와 stageId가 유효한지 확인합니다. + var worldSheet = states.GetSheet(); + + if (!worldSheet.TryGetValue(worldId, out var worldRow, false)) + { + throw new SheetRowNotFoundException(addressesHex, nameof(WorldSheet), worldId); + } + + if (stageId < worldRow.StageBegin || + stageId > worldRow.StageEnd) + { + throw new SheetRowColumnException( + $"{addressesHex}{worldId} world is not contains {worldRow.Id} stage: " + + $"{worldRow.StageBegin}-{worldRow.StageEnd}"); + } + + var stageSheet = states.GetSheet(); + if (!stageSheet.TryGetValue(stageId, out var stageRow)) + { + throw new SheetRowNotFoundException(addressesHex, nameof(StageSheet), stageId); + } + + var worldInformation = avatarState.worldInformation; + if (!worldInformation.TryGetWorld(worldId, out var world)) + { + // NOTE: Add new World from WorldSheet + worldInformation.AddAndUnlockNewWorld(worldRow, ctx.BlockIndex, worldSheet); + } + + if (!world.IsUnlocked) + { + throw new InvalidWorldException($"{addressesHex}{worldId} is locked."); + } + + if (world.StageBegin != worldRow.StageBegin || + world.StageEnd != worldRow.StageEnd) + { + worldInformation.UpdateWorld(worldRow); + } + + if (world.IsStageCleared && stageId > world.StageClearedId + 1 || + !world.IsStageCleared && stageId != world.StageBegin) + { + throw new InvalidStageException( + $"{addressesHex}Aborted as the stage ({worldId}/{stageId}) is not cleared; " + + $"cleared stage: {world.StageClearedId}" + ); + } + + avatarState.ValidateEquipments(equipments, context.BlockIndex); + avatarState.ValidateConsumable(foods, context.BlockIndex); + avatarState.ValidateCostume(new HashSet(costumes)); + + var costumeStatSheet = states.GetSheet(); + + sw.Restart(); + if (avatarState.actionPoint < stageRow.CostAP) + { + throw new NotEnoughActionPointException( + $"{addressesHex}Aborted due to insufficient action point: " + + $"{avatarState.actionPoint} < {stageRow.CostAP}" + ); + } + + avatarState.actionPoint -= stageRow.CostAP; + + avatarState.EquipCostumes(new HashSet(costumes)); + + avatarState.EquipEquipments(equipments); + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Unequip items: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + var characterSheet = states.GetSheet(); + var random = ctx.GetRandom(); + var simulator = new StageSimulatorV1( + random, + avatarState, + foods, + worldId, + stageId, + states.GetStageSimulatorSheetsV1(), + costumeStatSheet + ); + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Initialize Simulator: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + simulator.SimulateV1(); + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Simulator.Simulate(): {Elapsed}", addressesHex, sw.Elapsed); + + Log.Verbose( + "{AddressesHex}Execute HackAndSlash({AvatarAddress}); worldId: {WorldId}, stageId: {StageId}, result: {Result}, " + + "clearWave: {ClearWave}, totalWave: {TotalWave}", + addressesHex, + avatarAddress, + worldId, + stageId, + simulator.Log.result, + simulator.Log.clearedWaveNumber, + simulator.Log.waveCount + ); + + sw.Restart(); + if (simulator.Log.IsClear) + { + var worldUnlockSheet = states.GetSheet(); + simulator.Player.worldInformation.ClearStage( + worldId, + stageId, + ctx.BlockIndex, + worldSheet, + worldUnlockSheet + ); + } + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS ClearStage: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + avatarState.Update(simulator); + + var materialSheet = states.GetSheet(); + avatarState.UpdateQuestRewards2(materialSheet); + + avatarState.updatedAt = ctx.BlockIndex; + avatarState.mailBox.CleanUpV1(); + states = states.SetAvatarState(avatarAddress, avatarState); + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Set AvatarState: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + if (states.TryGetLegacyState(RankingMapAddress, out Dictionary d) && simulator.Log.IsClear) + { + var ranking = new RankingMapState(d); + ranking.Update(avatarState); + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Update RankingState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + + var serialized = ranking.Serialize(); + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Serialize RankingState: {Elapsed}", addressesHex, sw.Elapsed); + sw.Restart(); + states = states.SetLegacyState(RankingMapAddress, serialized); + } + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Set RankingState: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + if (simulator.Log.stageId >= GameConfig.RequireClearedStageLevel.ActionsInRankingBoard && + simulator.Log.IsClear && + states.TryGetLegacyState(WeeklyArenaAddress, out Dictionary weeklyDict)) + { + var weekly = new WeeklyArenaState(weeklyDict); + if (!weekly.Ended) + { + if (weekly.ContainsKey(avatarAddress)) + { + var info = weekly[avatarAddress]; + info.UpdateV2(avatarState, characterSheet, costumeStatSheet); + weekly.Update(info); + } + else + { + weekly.SetV2(avatarState, characterSheet, costumeStatSheet); + } + + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Update WeeklyArenaState: {Elapsed}", addressesHex, sw.Elapsed); + + sw.Restart(); + var weeklySerialized = weekly.Serialize(); + sw.Stop(); + Log.Verbose("{AddressesHex}HAS Serialize RankingState: {Elapsed}", addressesHex, sw.Elapsed); + + states = states.SetLegacyState(weekly.address, weeklySerialized); + } + } + + Result = simulator.Log; + + var ended = DateTimeOffset.UtcNow; + Log.Verbose("{AddressesHex}HAS Total Executed Time: {Elapsed}", addressesHex, ended - started); + return states; + } + } +}