diff --git a/.github/workflows/push_docker_image.yml b/.github/workflows/push_docker_image.yml index d603c9d2b..c43327a3c 100644 --- a/.github/workflows/push_docker_image.yml +++ b/.github/workflows/push_docker_image.yml @@ -13,6 +13,7 @@ on: - release/* # This branch is for testing only. Use until the next(v200080) release. - test/action-evaluation-publisher-elapse-metric + - exp/arena-list tags: - "*" workflow_dispatch: diff --git a/NineChronicles.Headless.Executable/Program.cs b/NineChronicles.Headless.Executable/Program.cs index 9828513a3..1e4762e4c 100644 --- a/NineChronicles.Headless.Executable/Program.cs +++ b/NineChronicles.Headless.Executable/Program.cs @@ -459,6 +459,10 @@ IActionLoader MakeSingleActionLoader() .AddRuntimeInstrumentation() .AddAspNetCoreInstrumentation() .AddPrometheusExporter()); + + // worker + services.AddHostedService(); + services.AddSingleton(); }); NineChroniclesNodeService service = diff --git a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs index 67e8eb60a..93b5c4e4e 100644 --- a/NineChronicles.Headless.Tests/GraphQLTestUtils.cs +++ b/NineChronicles.Headless.Tests/GraphQLTestUtils.cs @@ -41,6 +41,7 @@ public static Task ExecuteQueryAsync( } services.AddLibplanetExplorer(); + services.AddSingleton(); var serviceProvider = services.BuildServiceProvider(); return ExecuteQueryAsync( diff --git a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs index 4643f7e9c..dc6a8cee8 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/GraphQLTestBase.cs @@ -128,6 +128,7 @@ public GraphQLTestBase(ITestOutputHelper output) services.AddLibplanetExplorer(); services.AddSingleton(ncService); services.AddSingleton(ncService.Store); + services.AddSingleton(); ServiceProvider serviceProvider = services.BuildServiceProvider(); Schema = new StandaloneSchema(serviceProvider); diff --git a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs index 841050cff..56d17fba8 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/StateQueryTest.cs @@ -143,7 +143,7 @@ public async Task Garage( sb.ToString(), source: new StateContext( mockState, - 0L)); + 0L, new ArenaMemoryCache())); Assert.Null(queryResult.Errors); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; var garages = (Dictionary)data["garages"]; diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AgentStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AgentStateTypeTest.cs index beeb0bb3b..cfd2ad37d 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AgentStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AgentStateTypeTest.cs @@ -72,7 +72,7 @@ public async Task Query(int goldBalance, string goldDecimalString, int crystalBa var queryResult = await ExecuteQueryAsync( query, - source: new AgentStateType.AgentStateContext(agentState, mockState, 0) + source: new AgentStateType.AgentStateContext(agentState, mockState, 0, new ArenaMemoryCache()) ); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; var expected = new Dictionary() diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs index f89a50484..d7ad90992 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/AvatarStateTypeTest.cs @@ -33,7 +33,7 @@ public async Task Query(AvatarState avatarState, Dictionary expe source: new AvatarStateType.AvatarStateContext( avatarState, mockState, - 0)); + 0, new ArenaMemoryCache())); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; Assert.Equal(expected, data); } @@ -71,7 +71,7 @@ public async Task QueryWithCombinationSlotState(AvatarState avatarState, Diction source: new AvatarStateType.AvatarStateContext( avatarState, mockState, - 0)); + 0, new ArenaMemoryCache())); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; Assert.Equal(expected, data); } diff --git a/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs b/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs index d2f91c95d..4861e0bd2 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/States/Models/StakeStateTypeTest.cs @@ -44,7 +44,7 @@ public async Task Query(StakeStateV2 stakeState, Address stakeStateAddress, long stakeState, stakeStateAddress, mockState, - blockIndex)); + blockIndex, new ArenaMemoryCache())); var data = (Dictionary)((ExecutionNode)queryResult.Data!).ToValue()!; Assert.Equal(expected, data); } diff --git a/NineChronicles.Headless.Tests/GraphTypes/WorldBossScenarioTest.cs b/NineChronicles.Headless.Tests/GraphTypes/WorldBossScenarioTest.cs index 9bcc284e8..ae3bb6567 100644 --- a/NineChronicles.Headless.Tests/GraphTypes/WorldBossScenarioTest.cs +++ b/NineChronicles.Headless.Tests/GraphTypes/WorldBossScenarioTest.cs @@ -62,7 +62,7 @@ public WorldBossScenarioTest() [1] = true, [2] = false, }; - _stateContext = new StateContext(GetMockState(), 1L); + _stateContext = new StateContext(GetMockState(), 1L, new ArenaMemoryCache()); var minerPrivateKey = new PrivateKey(); var initializeStates = new InitializeStates( rankingState: new RankingState0(), diff --git a/NineChronicles.Headless/ArenaMemoryCache.cs b/NineChronicles.Headless/ArenaMemoryCache.cs new file mode 100644 index 000000000..eb7f4fb12 --- /dev/null +++ b/NineChronicles.Headless/ArenaMemoryCache.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace NineChronicles.Headless; + +public class ArenaMemoryCache +{ + public MemoryCache Cache { get; } = new(new OptionsWrapper(new MemoryCacheOptions + { + SizeLimit = null + })); +} diff --git a/NineChronicles.Headless/ArenaParticipant.cs b/NineChronicles.Headless/ArenaParticipant.cs new file mode 100644 index 000000000..b241ad9ef --- /dev/null +++ b/NineChronicles.Headless/ArenaParticipant.cs @@ -0,0 +1,38 @@ +using Libplanet.Crypto; +using Nekoyume.Model.State; + +namespace NineChronicles.Headless; + +public class ArenaParticipant +{ + public readonly Address AvatarAddr; + public readonly int Score; + public readonly int Rank; + public int WinScore; + public int LoseScore; + public readonly int Cp; + public readonly int PortraitId; + public readonly string NameWithHash; + public readonly int Level; + + public ArenaParticipant( + Address avatarAddr, + int score, + int rank, + AvatarState avatarState, + int portraitId, + int winScore, + int loseScore, + int cp) + { + AvatarAddr = avatarAddr; + Score = score; + Rank = rank; + WinScore = winScore; + LoseScore = loseScore; + Cp = cp; + PortraitId = portraitId; + NameWithHash = avatarState.NameWithHash; + Level = avatarState.level; + } +} diff --git a/NineChronicles.Headless/ArenaParticipantsWorker.cs b/NineChronicles.Headless/ArenaParticipantsWorker.cs new file mode 100644 index 000000000..5dd43ec1a --- /dev/null +++ b/NineChronicles.Headless/ArenaParticipantsWorker.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Bencodex.Types; +using Libplanet.Crypto; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Hosting; +using Nekoyume; +using Nekoyume.Action; +using Nekoyume.Arena; +using Nekoyume.Battle; +using Nekoyume.Model.Arena; +using Nekoyume.Model.EnumType; +using Nekoyume.Model.Item; +using Nekoyume.Model.State; +using Nekoyume.TableData; +using NineChronicles.Headless.GraphTypes; +using Serilog; +using static Lib9c.SerializeKeys; + +namespace NineChronicles.Headless; + +public class ArenaParticipantsWorker : BackgroundService +{ + private ILogger _logger; + private ArenaMemoryCache _cache; + private StandaloneContext _context; + + public ArenaParticipantsWorker(ArenaMemoryCache memoryCache, StandaloneContext context) + { + _cache = memoryCache; + _context = context; + _logger = Log.Logger.ForContext(); + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + try + { + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(1000, stoppingToken); + GetArenaParticipants(); + } + } + catch (OperationCanceledException) + { + //pass + } + catch (Exception) + { + _logger.Warning("Stopping ArenaParticipantsWorker"); + await StopAsync(stoppingToken); + } + } + + public void GetArenaParticipants() + { + var sw = new Stopwatch(); + sw.Start(); + // Copy from NineChronicles RxProps.Arena + // https://github.com/planetarium/NineChronicles/blob/80.0.1/nekoyume/Assets/_Scripts/State/RxProps.Arena.cs#L279 + var blockChain = _context.BlockChain; + if (blockChain is null) + { + throw new Exception(); + } + + var tip = blockChain.Tip; + var blockIndex = blockChain.Tip.Index; + var accountState = blockChain.GetAccountState(tip.Hash); + var currentRoundData = accountState.GetSheet().GetRoundByBlockIndex(blockIndex); + var participantsAddr = ArenaParticipants.DeriveAddress( + currentRoundData.ChampionshipId, + currentRoundData.Round); + var participants = accountState.GetState(participantsAddr) is List participantsList + ? new ArenaParticipants(participantsList) + : null; + var cacheKey = $"{currentRoundData.ChampionshipId}_{currentRoundData.Round}"; + if (participants is null) + { + _cache.Cache.Set(cacheKey, new List()); + return; + } + + var avatarAddrList = participants.AvatarAddresses; + var avatarAndScoreAddrList = avatarAddrList + .Select(avatarAddr => ( + avatarAddr, + ArenaScore.DeriveAddress( + avatarAddr, + currentRoundData.ChampionshipId, + currentRoundData.Round))) + .ToArray(); + // NOTE: If addresses is too large, and split and get separately. + var scores = accountState.GetStates( + avatarAndScoreAddrList.Select(tuple => tuple.Item2).ToList()); + var avatarAddrAndScores = new List<(Address avatarAddr, int score)>(); + for (int i = 0; i < avatarAddrList.Count; i++) + { + var tuple = avatarAndScoreAddrList[i]; + var score = scores[i] is List scoreList ? (int)(Integer)scoreList[1] : ArenaScore.ArenaScoreDefault; + avatarAddrAndScores.Add((tuple.avatarAddr, score)); + } + List<(Address avatarAddr, int score, int rank)> orderedTuples = avatarAddrAndScores + .OrderByDescending(tuple => tuple.score) + .ThenBy(tuple => tuple.avatarAddr) + .Select(tuple => (tuple.avatarAddr, tuple.score, 0)) + .ToList(); + int? currentScore = null; + var currentRank = 1; + var avatarAddrAndScoresWithRank = new List<(Address avatarAddr, int score, int rank)>(); + var trunk = new List<(Address avatarAddr, int score, int rank)>(); + for (var i = 0; i < orderedTuples.Count; i++) + { + var tuple = orderedTuples[i]; + if (!currentScore.HasValue) + { + currentScore = tuple.score; + trunk.Add(tuple); + continue; + } + + if (currentScore.Value == tuple.score) + { + trunk.Add(tuple); + currentRank++; + if (i < orderedTuples.Count - 1) + { + continue; + } + + foreach (var tupleInTrunk in trunk) + { + avatarAddrAndScoresWithRank.Add(( + tupleInTrunk.avatarAddr, + tupleInTrunk.score, + currentRank)); + } + + trunk.Clear(); + + continue; + } + + foreach (var tupleInTrunk in trunk) + { + avatarAddrAndScoresWithRank.Add(( + tupleInTrunk.avatarAddr, + tupleInTrunk.score, + currentRank)); + } + + trunk.Clear(); + if (i < orderedTuples.Count - 1) + { + trunk.Add(tuple); + currentScore = tuple.score; + currentRank++; + continue; + } + + avatarAddrAndScoresWithRank.Add(( + tuple.avatarAddr, + tuple.score, + currentRank + 1)); + } + + var runeListSheet = accountState.GetSheet(); + var costumeSheet = accountState.GetSheet(); + var characterSheet = accountState.GetSheet(); + var runeOptionSheet = accountState.GetSheet(); + var runeIds = runeListSheet.Values.Select(x => x.Id).ToList(); + var row = characterSheet[GameConfig.DefaultAvatarCharacterId]; + var addrBulk = avatarAddrAndScoresWithRank + .SelectMany(tuple => new[] + { + tuple.avatarAddr, + tuple.avatarAddr.Derive(LegacyInventoryKey), + ItemSlotState.DeriveAddress(tuple.avatarAddr, BattleType.Arena), + RuneSlotState.DeriveAddress(tuple.avatarAddr, BattleType.Arena), + }) + .ToList(); + + foreach (var tuple in avatarAddrAndScoresWithRank) + { + addrBulk.AddRange(runeIds.Select(x => RuneState.DeriveAddress(tuple.avatarAddr, x))); + } + + var states = accountState.GetStates(addrBulk); + var stateBulk = new Dictionary(); + for (int i = 0; i < addrBulk.Count; i++) + { + var address = addrBulk[i]; + var value = states[i]; + stateBulk.TryAdd(address, value ?? Null.Value); + } + var runeStates = new List(); + var result = avatarAddrAndScoresWithRank.Select(tuple => + { + var (avatarAddr, score, rank) = tuple; + var avatar = new AvatarState((Dictionary) stateBulk[avatarAddr]); + if (stateBulk[avatarAddr.Derive(LegacyInventoryKey)] is List inventoryList) + { + var inventory = new Inventory(inventoryList); + avatar.inventory = inventory; + } + + var itemSlotState = + stateBulk[ItemSlotState.DeriveAddress(avatarAddr, BattleType.Arena)] is + List itemSlotList + ? new ItemSlotState(itemSlotList) + : new ItemSlotState(BattleType.Arena); + + var runeSlotState = + stateBulk[RuneSlotState.DeriveAddress(avatarAddr, BattleType.Arena)] is + List runeSlotList + ? new RuneSlotState(runeSlotList) + : new RuneSlotState(BattleType.Arena); + + runeStates.Clear(); + foreach (var id in runeIds) + { + var address = RuneState.DeriveAddress(avatarAddr, id); + if (stateBulk[address] is List runeStateList) + { + runeStates.Add(new RuneState(runeStateList)); + } + } + + var equippedRuneStates = new List(); + foreach (var runeId in runeSlotState.GetRuneSlot().Select(slot => slot.RuneId)) + { + if (!runeId.HasValue) + { + continue; + } + + var runeState = runeStates.FirstOrDefault(x => x.RuneId == runeId); + if (runeState != null) + { + equippedRuneStates.Add(runeState); + } + } + + var equipments = itemSlotState.Equipments + .Select(guid => + avatar.inventory.Equipments.FirstOrDefault(x => x.ItemId == guid)) + .Where(item => item != null).ToList(); + var costumes = itemSlotState.Costumes + .Select(guid => + avatar.inventory.Costumes.FirstOrDefault(x => x.ItemId == guid)) + .Where(item => item != null).ToList(); + var runeOptions = StateQuery.GetRuneOptions(equippedRuneStates, runeOptionSheet); + var cp = CPHelper.TotalCP(equipments, costumes, runeOptions, avatar.level, row, costumeSheet); + var portraitId = StateQuery.GetPortraitId(equipments, costumes); + return new ArenaParticipant( + avatarAddr, + score, + rank, + avatar, + portraitId, + 0, + 0, + cp + ); + }).ToList(); + _cache.Cache.Set(cacheKey, result, TimeSpan.FromHours(1)); + sw.Stop(); + _logger.Information("Set Arena Cache[{CacheKey}]: {Elapsed}", cacheKey, sw.Elapsed); + } +} diff --git a/NineChronicles.Headless/GraphTypes/ArenaParticipantType.cs b/NineChronicles.Headless/GraphTypes/ArenaParticipantType.cs new file mode 100644 index 000000000..553416e60 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/ArenaParticipantType.cs @@ -0,0 +1,47 @@ +using GraphQL.Types; +using Libplanet.Explorer.GraphTypes; + +namespace NineChronicles.Headless.GraphTypes; + +public class ArenaParticipantType : ObjectGraphType +{ + public ArenaParticipantType() + { + Field>( + nameof(ArenaParticipant.AvatarAddr), + description: "Address of avatar.", + resolve: context => context.Source.AvatarAddr); + Field>( + nameof(ArenaParticipant.Score), + description: "Arena score of avatar.", + resolve: context => context.Source.Score); + Field>( + nameof(ArenaParticipant.Rank), + description: "Arena rank of avatar.", + resolve: context => context.Source.Rank); + Field>( + nameof(ArenaParticipant.WinScore), + description: "Score for victory.", + resolve: context => context.Source.WinScore); + Field>( + nameof(ArenaParticipant.LoseScore), + description: "Score for defeat.", + resolve: context => context.Source.LoseScore); + Field>( + nameof(ArenaParticipant.Cp), + description: "Cp of avatar.", + resolve: context => context.Source.Cp); + Field>( + nameof(ArenaParticipant.PortraitId), + description: "Portrait icon id.", + resolve: context => context.Source.PortraitId); + Field>( + nameof(ArenaParticipant.Level), + description: "Level of avatar.", + resolve: context => context.Source.Level); + Field>( + nameof(ArenaParticipant.NameWithHash), + description: "Name of avatar.", + resolve: context => context.Source.NameWithHash); + } +} diff --git a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs index ae7394372..fdf69e95e 100644 --- a/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StandaloneQuery.cs @@ -28,7 +28,7 @@ namespace NineChronicles.Headless.GraphTypes { public class StandaloneQuery : ObjectGraphType { - public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration configuration, ActionEvaluationPublisher publisher) + public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration configuration, ActionEvaluationPublisher publisher, ArenaMemoryCache arenaMemoryCache) { bool useSecretToken = configuration[GraphQLService.SecretTokenKey] is { }; @@ -57,7 +57,8 @@ public StandaloneQuery(StandaloneContext standaloneContext, IConfiguration confi { BlockHash bh => chain[bh].Index, null => chain.Tip!.Index, - } + }, + arenaMemoryCache ); } ); diff --git a/NineChronicles.Headless/GraphTypes/StateQuery.cs b/NineChronicles.Headless/GraphTypes/StateQuery.cs index 8ce7407b8..ff96d99dc 100644 --- a/NineChronicles.Headless/GraphTypes/StateQuery.cs +++ b/NineChronicles.Headless/GraphTypes/StateQuery.cs @@ -10,8 +10,11 @@ using Libplanet.Explorer.GraphTypes; using Nekoyume; using Nekoyume.Action; +using Nekoyume.Arena; +using Nekoyume.Battle; using Nekoyume.Extensions; using Nekoyume.Model.Arena; +using Nekoyume.Model.EnumType; using Nekoyume.Model.Item; using Nekoyume.Model.Stake; using Nekoyume.Model.State; @@ -40,7 +43,7 @@ public StateQuery() return new AvatarStateType.AvatarStateContext( context.AccountState.GetAvatarState(address), context.AccountState, - context.BlockIndex); + context.BlockIndex, context.ArenaMemoryCache); } catch (InvalidAddressException) { @@ -231,7 +234,8 @@ public StateQuery() return new AgentStateType.AgentStateContext( new AgentState(state), context.Source.AccountState, - context.Source.BlockIndex + context.Source.BlockIndex, + context.Source.ArenaMemoryCache ); } @@ -248,7 +252,8 @@ public StateQuery() stakeStateV2, stakeStateAddress, ctx.AccountState, - ctx.BlockIndex + ctx.BlockIndex, + ctx.ArenaMemoryCache ); } @@ -643,6 +648,107 @@ public StateQuery() ); RegisterGarages(); + + Field>>( + "arenaParticipants", + arguments: new QueryArguments( + new QueryArgument> + { + Name = "avatarAddress" + }, + new QueryArgument> + { + Name = "filterBounds", + DefaultValue = true, + } + ), + resolve: context => + { + // Copy from NineChronicles RxProps.Arena + // https://github.com/planetarium/NineChronicles/blob/80.0.1/nekoyume/Assets/_Scripts/State/RxProps.Arena.cs#L279 + var blockIndex = context.Source.BlockIndex; + var currentAvatarAddr = context.GetArgument
("avatarAddress"); + var filterBounds = context.GetArgument("filterBounds"); + var currentRoundData = context.Source.AccountState.GetSheet().GetRoundByBlockIndex(blockIndex); + int playerScore = ArenaScore.ArenaScoreDefault; + var cacheKey = $"{currentRoundData.ChampionshipId}_{currentRoundData.Round}"; + List result = new(); + var scoreAddr = ArenaScore.DeriveAddress(currentAvatarAddr, currentRoundData.ChampionshipId, currentRoundData.Round); + var scoreState = context.Source.GetState(scoreAddr); + if (scoreState is List scores) + { + playerScore = (Integer)scores[1]; + } + if (context.Source.ArenaMemoryCache.Cache.TryGetValue(cacheKey, + out var cachedResult)) + { + result = (cachedResult as List)!; + foreach (var arenaParticipant in result) + { + var (win, lose, _) = ArenaHelper.GetScores(playerScore, arenaParticipant.Score); + arenaParticipant.WinScore = win; + arenaParticipant.LoseScore = lose; + } + } + + if (filterBounds) + { + result = GetBoundsWithPlayerScore(result, currentRoundData.ArenaType, playerScore); + } + + return result; + } + ); + } + + public static List GetRuneOptions( + List runeStates, + RuneOptionSheet sheet) + { + var result = new List(); + foreach (var runeState in runeStates) + { + if (!sheet.TryGetValue(runeState.RuneId, out var row)) + { + continue; + } + + if (!row.LevelOptionMap.TryGetValue(runeState.Level, out var statInfo)) + { + continue; + } + + result.Add(statInfo); + } + + return result; + } + + public static List GetBoundsWithPlayerScore( + List arenaInformation, + ArenaType arenaType, + int playerScore) + { + var bounds = ArenaHelper.ScoreLimits.ContainsKey(arenaType) + ? ArenaHelper.ScoreLimits[arenaType] + : ArenaHelper.ScoreLimits.First().Value; + + bounds = (bounds.upper + playerScore, bounds.lower + playerScore); + return arenaInformation + .Where(a => a.Score <= bounds.upper && a.Score >= bounds.lower) + .ToList(); + } + + public static int GetPortraitId(List equipments, List costumes) + { + var fullCostume = costumes.FirstOrDefault(x => x?.ItemSubType == ItemSubType.FullCostume); + if (fullCostume != null) + { + return fullCostume.Id; + } + + var armor = equipments.FirstOrDefault(x => x?.ItemSubType == ItemSubType.Armor); + return armor?.Id ?? GameConfig.DefaultAvatarArmorId; } } } diff --git a/NineChronicles.Headless/GraphTypes/States/AgentStateType.cs b/NineChronicles.Headless/GraphTypes/States/AgentStateType.cs index 8a95deae2..8684439e8 100644 --- a/NineChronicles.Headless/GraphTypes/States/AgentStateType.cs +++ b/NineChronicles.Headless/GraphTypes/States/AgentStateType.cs @@ -19,8 +19,8 @@ public class AgentStateType : ObjectGraphType { public class AgentStateContext : StateContext { - public AgentStateContext(AgentState agentState, IAccountState accountState, long blockIndex) - : base(accountState, blockIndex) + public AgentStateContext(AgentState agentState, IAccountState accountState, long blockIndex, ArenaMemoryCache arenaMemoryCache) + : base(accountState, blockIndex, arenaMemoryCache) { AgentState = agentState; } @@ -49,7 +49,8 @@ public AgentStateType() x => new AvatarStateType.AvatarStateContext( x, context.Source.AccountState, - context.Source.BlockIndex)); + context.Source.BlockIndex, + context.Source.ArenaMemoryCache)); }); Field>( "gold", diff --git a/NineChronicles.Headless/GraphTypes/States/ArenaInformationObjectType.cs b/NineChronicles.Headless/GraphTypes/States/ArenaInformationObjectType.cs new file mode 100644 index 000000000..45b44c156 --- /dev/null +++ b/NineChronicles.Headless/GraphTypes/States/ArenaInformationObjectType.cs @@ -0,0 +1,36 @@ +using GraphQL.Types; +using Libplanet.Explorer.GraphTypes; +using Nekoyume.Model.Arena; + +namespace NineChronicles.Headless.GraphTypes.States; + +public class ArenaInformationObjectType : ObjectGraphType +{ + public ArenaInformationObjectType() + { + Field>( + nameof(ArenaInformation.Address), + resolve: context => context.Source.Address + ); + Field>( + nameof(ArenaInformation.Win), + resolve: context => context.Source.Win + ); + Field>( + nameof(ArenaInformation.Lose), + resolve: context => context.Source.Lose + ); + Field>( + nameof(ArenaInformation.Ticket), + resolve: context => context.Source.Ticket + ); + Field>( + nameof(ArenaInformation.TicketResetCount), + resolve: context => context.Source.TicketResetCount + ); + Field>( + nameof(ArenaInformation.PurchasedTicketCount), + resolve: context => context.Source.PurchasedTicketCount + ); + } +} diff --git a/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs b/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs index e3f0c09e7..7714d5315 100644 --- a/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs +++ b/NineChronicles.Headless/GraphTypes/States/AvatarStateType.cs @@ -21,8 +21,8 @@ public class AvatarStateType : ObjectGraphType { public class StakeStateContext : StateContext { - public StakeStateContext(StakeStateV2 stakeState, Address address, IAccountState accountState, long blockIndex) - : base(accountState, blockIndex) + public StakeStateContext(StakeStateV2 stakeState, Address address, IAccountState accountState, long blockIndex, ArenaMemoryCache arenaMemoryCache) + : base(accountState, blockIndex, arenaMemoryCache) { StakeState = stakeState; Address = address; diff --git a/NineChronicles.Headless/GraphTypes/States/StateContext.cs b/NineChronicles.Headless/GraphTypes/States/StateContext.cs index ab420f116..168570aac 100644 --- a/NineChronicles.Headless/GraphTypes/States/StateContext.cs +++ b/NineChronicles.Headless/GraphTypes/States/StateContext.cs @@ -13,12 +13,14 @@ public class StateContext { public StateContext( IAccountState accountState, - long blockIndex) + long blockIndex, + ArenaMemoryCache arenaMemoryCache) { AccountState = accountState; BlockIndex = blockIndex; CurrencyFactory = new CurrencyFactory(() => accountState); FungibleAssetValueFactory = new FungibleAssetValueFactory(CurrencyFactory); + ArenaMemoryCache = arenaMemoryCache; } public IAccountState AccountState { get; } @@ -34,5 +36,7 @@ public StateContext( public IReadOnlyList GetStates(IReadOnlyList
addresses) => AccountState.GetStates(addresses); public FungibleAssetValue GetBalance(Address address, Currency currency) => AccountState.GetBalance(address, currency); + + public ArenaMemoryCache ArenaMemoryCache { get; } } }