diff --git a/backend/app/Savor22b/Constants/Addresses.cs b/backend/app/Savor22b/Constants/Addresses.cs index 0b85ca56..63415344 100644 --- a/backend/app/Savor22b/Constants/Addresses.cs +++ b/backend/app/Savor22b/Constants/Addresses.cs @@ -4,6 +4,13 @@ namespace Savor22b.Constants; public static class Addresses { - public static readonly Address ShopVaultAddress = new Address("0000000000000000000000000000000000000000"); - public static readonly Address UserHouseDataAddress = new Address("0000000000000000000000000000000000000001"); + public static readonly Address ShopVaultAddress = new Address( + "0000000000000000000000000000000000000000" + ); + public static readonly Address UserHouseDataAddress = new Address( + "0000000000000000000000000000000000000001" + ); + public static readonly Address DungeonDataAddress = new Address( + "0000000000000000000000000000000000000002" + ); } diff --git a/backend/app/Savor22b/GraphTypes/Query/Query.cs b/backend/app/Savor22b/GraphTypes/Query/Query.cs index 1c77ff6b..f22001c2 100644 --- a/backend/app/Savor22b/GraphTypes/Query/Query.cs +++ b/backend/app/Savor22b/GraphTypes/Query/Query.cs @@ -529,7 +529,6 @@ swarm is null ); AddField(new CalculateRelocationCostQuery()); - AddField(new VillagesQuery(blockChain)); AddField(new ShopQuery()); } diff --git a/backend/app/Savor22b/GraphTypes/Subscription/Subscription.cs b/backend/app/Savor22b/GraphTypes/Subscription/Subscription.cs index 08059557..ca6e041b 100644 --- a/backend/app/Savor22b/GraphTypes/Subscription/Subscription.cs +++ b/backend/app/Savor22b/GraphTypes/Subscription/Subscription.cs @@ -44,6 +44,7 @@ public Subscription( _subject = new Subject(); AddField(new UserStateField(_blockChain, _subject)); + AddField(new VillageField(_blockChain, _subject)); AddField( new FieldType() diff --git a/backend/app/Savor22b/GraphTypes/Query/VillagesQuery.cs b/backend/app/Savor22b/GraphTypes/Subscription/VillageField.cs similarity index 68% rename from backend/app/Savor22b/GraphTypes/Query/VillagesQuery.cs rename to backend/app/Savor22b/GraphTypes/Subscription/VillageField.cs index dcaee855..2a8379cc 100644 --- a/backend/app/Savor22b/GraphTypes/Query/VillagesQuery.cs +++ b/backend/app/Savor22b/GraphTypes/Subscription/VillageField.cs @@ -1,7 +1,9 @@ -namespace Savor22b.GraphTypes.Query; +namespace Savor22b.GraphTypes.Subscription; using System.Collections.Immutable; using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; using Bencodex.Types; using GraphQL; using GraphQL.Resolvers; @@ -12,12 +14,18 @@ namespace Savor22b.GraphTypes.Query; using Savor22b.Model; using Savor22b.States; -public class VillagesQuery : FieldType +public class VillageField : FieldType { - public VillagesQuery(BlockChain blockChain) + private readonly BlockChain _blockChain; + private readonly Subject _subject; + + public VillageField(BlockChain blockChain, Subject subject) : base() { - Name = "villages"; + _blockChain = blockChain; + _subject = subject; + + Name = "Village"; Type = typeof(NonNullGraphType>); Description = "Get all villages"; Resolver = new FuncFieldResolver>(context => @@ -38,6 +46,25 @@ public VillagesQuery(BlockChain blockChain) throw new ExecutionError(e.Message); } }); + StreamResolver = new SourceStreamResolver>( + (context) => + { + try + { + var villages = CsvDataHelper.GetVillageCSVData().ToArray(); + + return _subject + .DistinctUntilChanged() + .Select( + _ => GetVillageDetails(villages.ToImmutableList(), context, blockChain) + ); + } + catch (Exception e) + { + throw new ExecutionError(e.Message); + } + } + ); } public static ImmutableList GetVillageDetails( diff --git a/backend/app/Savor22b/GraphTypes/Types/DungeonStateType.cs b/backend/app/Savor22b/GraphTypes/Types/DungeonStateType.cs index 9346f6c3..1bfc93d0 100644 --- a/backend/app/Savor22b/GraphTypes/Types/DungeonStateType.cs +++ b/backend/app/Savor22b/GraphTypes/Types/DungeonStateType.cs @@ -1,11 +1,14 @@ namespace Savor22b.GraphTypes.Types; using GraphQL.Types; +using Libplanet; +using Libplanet.Blockchain; +using Libplanet.Explorer.GraphTypes; using Savor22b.Model; public class DungeonStateType : ObjectGraphType { - public DungeonStateType() + public DungeonStateType(BlockChain blockChain) { Field>( "name", @@ -36,5 +39,30 @@ public DungeonStateType() description: "던전이 위치하는 마을의 id 입니다.", resolve: context => context.Source.VillageId ); + + Field>>( + "seeds", + description: "던전에서 획득 가능한 씨앗 목록입니다.", + resolve: context => + { + var seeds = context.Source.RewardSeedIdList.Select( + seedId => CsvDataHelper.GetSeedById(seedId)! + ); + + return seeds; + } + ); + + Field>( + "isConquest", + description: "현재 던전이 점령 되었는지의 여부입니다.", + resolve: context => context.Source.IsConquest(blockChain) + ); + + Field( + "conquestUserAddress", + description: "현재 던전을 점령하고 있는 유저의 Address입니다.", + resolve: context => context.Source.CurrentConquestUserAddress(blockChain) + ); } } diff --git a/backend/app/Savor22b/GraphTypes/Types/StaticSeedStateType.cs b/backend/app/Savor22b/GraphTypes/Types/StaticSeedStateType.cs new file mode 100644 index 00000000..8308ee16 --- /dev/null +++ b/backend/app/Savor22b/GraphTypes/Types/StaticSeedStateType.cs @@ -0,0 +1,26 @@ +using GraphQL.Types; +using Savor22b.Model; + +namespace Savor22b.GraphTypes.Types; + +public class StaticSeedStateType : ObjectGraphType +{ + public StaticSeedStateType() + { + Field( + name: "seedId", + description: "The ID of the seed.", + resolve: context => context.Source.Id + ); + + Field( + name: "name", + description: "The name of the seed.", + resolve: context => + { + Seed seed = CsvDataHelper.GetSeedById(context.Source.Id)!; + return seed.Name; + } + ); + } +} diff --git a/backend/app/Savor22b/Model/Dungeon.cs b/backend/app/Savor22b/Model/Dungeon.cs index b60055dd..ad985629 100644 --- a/backend/app/Savor22b/Model/Dungeon.cs +++ b/backend/app/Savor22b/Model/Dungeon.cs @@ -1,3 +1,10 @@ +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet; +using Libplanet.Blockchain; +using Savor22b.Constants; +using Savor22b.States; + namespace Savor22b.Model; public class Dungeon @@ -7,4 +14,31 @@ public class Dungeon public int Y { get; set; } public int ID { get; set; } public int VillageId { get; set; } + public ImmutableList RewardSeedIdList { get; set; } + + private static GlobalDungeonState GetGlobalDungeonState(BlockChain blockChain) + { + GlobalDungeonState globalDungeonState = blockChain.GetState(Addresses.DungeonDataAddress) + is Dictionary stateEncoded + ? new GlobalDungeonState(stateEncoded) + : new GlobalDungeonState(); + + return globalDungeonState; + } + + public bool IsConquest(BlockChain blockChain) + { + GlobalDungeonState globalDungeonState = GetGlobalDungeonState(blockChain); + + return globalDungeonState.DungeonStatus.ContainsKey(ID.ToString()); + } + + public Address? CurrentConquestUserAddress(BlockChain blockChain) + { + GlobalDungeonState globalDungeonState = GetGlobalDungeonState(blockChain); + + return globalDungeonState.DungeonStatus.TryGetValue(ID.ToString(), out Address address) + ? address + : null; + } } diff --git a/backend/app/Savor22b/States/DungeonConquestHistoryState.cs b/backend/app/Savor22b/States/DungeonConquestHistoryState.cs new file mode 100644 index 00000000..da150e2e --- /dev/null +++ b/backend/app/Savor22b/States/DungeonConquestHistoryState.cs @@ -0,0 +1,53 @@ +namespace Savor22b.States; + +using Bencodex.Types; +using Libplanet; +using Libplanet.Headless.Extensions; + +public class DungeonConquestHistoryState : State +{ + public long BlockIndex { get; private set; } + public int DungeonId { get; private set; } + public Address TargetUserAddress { get; private set; } + public int DungeonConquestStatus { get; private set; } + + public DungeonConquestHistoryState( + long blockIndex, + int dungeonId, + Address targetUserAddress, + int dungeonConquestStatus + ) + { + BlockIndex = blockIndex; + DungeonId = dungeonId; + TargetUserAddress = targetUserAddress; + DungeonConquestStatus = dungeonConquestStatus; + } + + public DungeonConquestHistoryState(Dictionary encoded) + { + BlockIndex = encoded[nameof(BlockIndex)].ToLong(); + DungeonId = encoded[nameof(DungeonId)].ToInteger(); + TargetUserAddress = encoded[nameof(TargetUserAddress)].ToAddress(); + DungeonConquestStatus = encoded[nameof(DungeonConquestStatus)].ToInteger(); + } + + public IValue Serialize() + { + var pairs = new[] + { + new KeyValuePair((Text)nameof(BlockIndex), BlockIndex.Serialize()), + new KeyValuePair((Text)nameof(DungeonId), DungeonId.Serialize()), + new KeyValuePair( + (Text)nameof(TargetUserAddress), + TargetUserAddress.ToBencodex() + ), + new KeyValuePair( + (Text)nameof(DungeonConquestStatus), + DungeonConquestStatus.Serialize() + ), + }; + + return new Dictionary(pairs); + } +} diff --git a/backend/app/Savor22b/States/DungeonHistoryState.cs b/backend/app/Savor22b/States/DungeonHistoryState.cs new file mode 100644 index 00000000..2e481b29 --- /dev/null +++ b/backend/app/Savor22b/States/DungeonHistoryState.cs @@ -0,0 +1,57 @@ +namespace Savor22b.States; + +using System.Collections.Immutable; +using Bencodex.Types; +using Libplanet.Headless.Extensions; + +public class DungeonHistoryState : State +{ + public long BlockIndex { get; private set; } + public int DungeonId { get; private set; } + public int DungeonClearStatus { get; private set; } + public ImmutableList DungeonClearRewardSeedIdList { get; private set; } + + public DungeonHistoryState( + long blockIndex, + int dungeonId, + int dungeonClearStatus, + ImmutableList dungeonClearRewardSeedIdList + ) + { + BlockIndex = blockIndex; + DungeonId = dungeonId; + DungeonClearStatus = dungeonClearStatus; + DungeonClearRewardSeedIdList = dungeonClearRewardSeedIdList; + } + + public DungeonHistoryState(Dictionary encoded) + { + BlockIndex = encoded[nameof(BlockIndex)].ToLong(); + DungeonId = encoded[nameof(DungeonId)].ToInteger(); + DungeonClearStatus = encoded[nameof(DungeonClearStatus)].ToInteger(); + DungeonClearRewardSeedIdList = ( + (Bencodex.Types.List)encoded[nameof(DungeonClearRewardSeedIdList)] + ) + .Select(e => e.ToInteger()) + .ToImmutableList(); + } + + public IValue Serialize() + { + var pairs = new[] + { + new KeyValuePair((Text)nameof(BlockIndex), BlockIndex.Serialize()), + new KeyValuePair((Text)nameof(DungeonId), DungeonId.Serialize()), + new KeyValuePair( + (Text)nameof(DungeonClearStatus), + DungeonClearStatus.Serialize() + ), + new KeyValuePair( + (Text)nameof(DungeonClearRewardSeedIdList), + new List(DungeonClearRewardSeedIdList.Select(e => e.Serialize())) + ), + }; + + return new Dictionary(pairs); + } +} diff --git a/backend/app/Savor22b/States/DungeonKeyHistory.cs b/backend/app/Savor22b/States/DungeonKeyHistory.cs deleted file mode 100644 index 4fed82fa..00000000 --- a/backend/app/Savor22b/States/DungeonKeyHistory.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Savor22b.States; - -using Bencodex.Types; -using Libplanet.Headless.Extensions; - -public class DungeonKeyHistory : State -{ - public long BlockIndex { get; private set; } - public int DungeonId { get; private set; } - - public DungeonKeyHistory(long blockIndex, int dungeonId) - { - BlockIndex = blockIndex; - DungeonId = dungeonId; - } - - public DungeonKeyHistory(Dictionary encoded) - { - BlockIndex = encoded[nameof(BlockIndex)].ToLong(); - DungeonId = encoded[nameof(DungeonId)].ToInteger(); - } - - public IValue Serialize() - { - var pairs = new[] - { - new KeyValuePair((Text)nameof(BlockIndex), BlockIndex.Serialize()), - new KeyValuePair((Text)nameof(DungeonId), DungeonId.Serialize()), - }; - - return new Dictionary(pairs); - } -} diff --git a/backend/app/Savor22b/States/GlobalDungeonState.cs b/backend/app/Savor22b/States/GlobalDungeonState.cs new file mode 100644 index 00000000..e8341187 --- /dev/null +++ b/backend/app/Savor22b/States/GlobalDungeonState.cs @@ -0,0 +1,53 @@ +namespace Savor22b.States; + +using Libplanet.Headless.Extensions; + +using Bencodex.Types; +using Libplanet; + +public class GlobalDungeonState : State +{ + public Dictionary DungeonStatus { get; private set; } + + public GlobalDungeonState() + { + DungeonStatus = new Dictionary(); + } + + public GlobalDungeonState(Dictionary dungeonStatus) + { + DungeonStatus = dungeonStatus; + } + + public GlobalDungeonState(Bencodex.Types.Dictionary encoded) + { + if (encoded.TryGetValue((Text)nameof(DungeonStatus), out var dungeonStatus)) + { + DungeonStatus = ((Bencodex.Types.Dictionary)dungeonStatus).ToDictionary( + pair => ((Bencodex.Types.Text)pair.Key).Value, + pair => pair.Value.ToAddress() + ); + } + else + { + DungeonStatus = new Dictionary(); + } + } + + public IValue Serialize() + { + var pairs = new[] + { + new KeyValuePair( + (Text)nameof(DungeonStatus), + new Dictionary( + DungeonStatus.Select( + e => new KeyValuePair((Text)e.Key, e.Value.ToBencodex()) + ) + ) + ), + }; + + return new Dictionary(pairs); + } +} diff --git a/backend/app/Savor22b/States/UserDungeonState.cs b/backend/app/Savor22b/States/UserDungeonState.cs index 04f7f90a..9c68e4ea 100644 --- a/backend/app/Savor22b/States/UserDungeonState.cs +++ b/backend/app/Savor22b/States/UserDungeonState.cs @@ -8,30 +8,40 @@ public class UserDungeonState : State public static readonly int MaxDungeonKey = 5; public static readonly int DungeonKeyChargeIntervalBlock = 12; - public ImmutableList DungeonKeyHistories { get; private set; } + public ImmutableList DungeonHistories { get; private set; } + public ImmutableList DungeonConquestHistories { get; private set; } public UserDungeonState() { - DungeonKeyHistories = ImmutableList.Empty; + DungeonHistories = ImmutableList.Empty; + DungeonConquestHistories = ImmutableList.Empty; } - public UserDungeonState(ImmutableList dungeonKeyHistories) + public UserDungeonState( + ImmutableList dungeonKeyHistories, + ImmutableList dungeonConquestHistories + ) { - DungeonKeyHistories = dungeonKeyHistories; + DungeonHistories = dungeonKeyHistories; + DungeonConquestHistories = dungeonConquestHistories; } public UserDungeonState(Dictionary encoded) { - if (encoded.TryGetValue((Text)nameof(DungeonKeyHistories), out var dungeonKeyHistories)) + if (encoded.TryGetValue((Text)nameof(DungeonHistories), out var dungeonKeyHistories)) { - DungeonKeyHistories = ((List)dungeonKeyHistories) - .Select(element => new DungeonKeyHistory((Dictionary)element)) + DungeonHistories = ((List)dungeonKeyHistories) + .Select(element => new DungeonHistoryState((Dictionary)element)) .ToImmutableList(); } else { - DungeonKeyHistories = ImmutableList.Empty; + DungeonHistories = ImmutableList.Empty; } + + DungeonConquestHistories = ((List)encoded[nameof(DungeonConquestHistories)]) + .Select(element => new DungeonConquestHistoryState((Dictionary)element)) + .ToImmutableList(); } public IValue Serialize() @@ -40,8 +50,12 @@ public IValue Serialize() new[] { new KeyValuePair( - (Text)nameof(DungeonKeyHistories), - new List(DungeonKeyHistories.Select(element => element.Serialize())) + (Text)nameof(DungeonHistories), + new List(DungeonHistories.Select(element => element.Serialize())) + ), + new KeyValuePair( + (Text)nameof(DungeonConquestHistories), + new List(DungeonConquestHistories.Select(element => element.Serialize())) ), } ); @@ -49,17 +63,17 @@ public IValue Serialize() public int GetDungeonKeyCount(long blockIndex) { - return MaxDungeonKey - GetCurrentDungeonKeyHistories(blockIndex).Count; + return MaxDungeonKey - GetCurrentDungeonHistories(blockIndex).Count; } - public ImmutableList GetCurrentDungeonKeyHistories(long blockIndex) + public ImmutableList GetCurrentDungeonHistories(long blockIndex) { var lowerBoundIndex = blockIndex - (MaxDungeonKey * DungeonKeyChargeIntervalBlock); - var result = new List(); + var result = new List(); - for (int i = DungeonKeyHistories.Count - 1; i >= 0; i--) + for (int i = DungeonHistories.Count - 1; i >= 0; i--) { - var history = DungeonKeyHistories[i]; + var history = DungeonHistories[i]; if (history.BlockIndex > lowerBoundIndex && history.BlockIndex <= blockIndex) { diff --git a/resources/savor22b/tabledata/dungeon.csv b/resources/savor22b/tabledata/dungeon.csv index c7a9f9b5..896361ac 100644 --- a/resources/savor22b/tabledata/dungeon.csv +++ b/resources/savor22b/tabledata/dungeon.csv @@ -1,4 +1,4 @@ -Name,X,Y,ID,VillageId -땅콩 던전,0,1,0,1 -빵 던전,2,3,1,2 -케이크 던전,2,5,2,3 \ No newline at end of file +Name,X,Y,ID,VillageId,RewardSeedIdList +땅콩 던전,0,1,0,1,1;2;3;4;5 +빵 던전,2,3,1,2,5;6;7;8;9 +케이크 던전,2,5,2,3,1;2;3;6;7;8 \ No newline at end of file