Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PoC: use mimir in arena #5759

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
19 changes: 18 additions & 1 deletion nekoyume/Assets/_Scripts/ApiClient/ApiClients.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using Nekoyume.GraphQL;
using Nekoyume.Helper;
using Nekoyume.Multiplanetary;
using Nekoyume.Multiplanetary.Extensions;
using NineChronicles.ExternalServices.IAPService.Runtime;
using NineChronicles.ExternalServices.IAPService.Runtime.Models;

Expand All @@ -21,6 +25,8 @@ private static class Singleton
private static readonly string DccUrlJsonPath =
Platform.GetStreamingAssetsPath("dccUrl.json");

public GraphQLHttpClient Mimir { get; private set; }

public NineChroniclesAPIClient WorldBossClient { get; private set; }

public NineChroniclesAPIClient RpcGraphQlClient { get; private set; }
Expand All @@ -46,7 +52,7 @@ public void SetDccUrl()
}

// TODO: 중복코드 정리, 초기화 안 된 경우 로직 정리
public void Initialize(CommandLineOptions clo)
public void Initialize(CommandLineOptions clo, PlanetId? planetId)
{
if (clo == null)
{
Expand All @@ -56,6 +62,17 @@ public void Initialize(CommandLineOptions clo)

// NOTE: planetContext.CommandLineOptions and _commandLineOptions are same.
// NOTE: Initialize several services after Agent initialized.
Mimir = planetId.HasValue
? planetId.Value.Is("odin")
? new GraphQLHttpClient(
"https://mimir.nine-chronicles.dev/odin/graphql/",
new NewtonsoftJsonSerializer())
: planetId.Value.Is("heimdall")
? new GraphQLHttpClient(
"https://mimir.nine-chronicles.dev/heimdall/graphql/",
new NewtonsoftJsonSerializer())
: null
: null;
WorldBossClient = new NineChroniclesAPIClient(clo.ApiServerHost);
RpcGraphQlClient = string.IsNullOrEmpty(clo.RpcServerHost) ?
new NineChroniclesAPIClient(string.Empty) :
Expand Down
3 changes: 3 additions & 0 deletions nekoyume/Assets/_Scripts/ApiClient/GraphQL/Queries.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

155 changes: 155 additions & 0 deletions nekoyume/Assets/_Scripts/ApiClient/GraphQL/Queries/MimirQuery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.Client.Http;
using Libplanet.Crypto;
using Nekoyume.Arena;
using Nekoyume.Model.Arena;
using Nekoyume.UI.Model;
using Newtonsoft.Json.Linq;

namespace Nekoyume.GraphQL.Queries
{
public static class MimirQuery
{
private static async Task<GraphQLResponse<JObject>> Query(
GraphQLHttpClient mimirClient,
string query)
{
try
{
var request = new GraphQLHttpRequest(query);
var response = await mimirClient.SendQueryAsync<JObject>(request);
if (response.Data is not null ||
response.Errors is not { Length: > 0 })
{
return response;
}

var sb = new StringBuilder();
sb.AppendLine("GraphQL response data is null and has errors:");
foreach (var error in response.Errors)
{
sb.AppendLine($" {error.Message}");
}

NcDebug.LogError(sb.ToString());
return null;
}
catch (Exception e)
{
NcDebug.LogError($"Failed to execute GraphQL query.\n{e}");
return null;
}
}

public static async Task<long?> GetMetadataBlockIndexAsync(
GraphQLHttpClient mimirClient,
string collectionName)
{
if (mimirClient is null)
{
return null;
}

var query = @$"query {{
metadata(collectionName: ""{collectionName}"") {{
latestBlockIndex
}}
}}";
var response = await Query(mimirClient, query);
var data = response?.Data;
if (data is null)
{
return null;
}

try
{
return data["metadata"]["latestBlockIndex"].Value<long>();
}
catch (Exception e)
{
NcDebug.LogError($"Failed to parse latestBlockIndex from metadata.\n{e}");
return null;
}
}

public static async Task<List<ArenaParticipantModel>> GetArenaParticipantsAsync(
GraphQLHttpClient mimirClient,
Address avatarAddress)
{
if (mimirClient is null)
{
return null;
}

var query = @$"query {{
arena {{
leaderboardByAvatarAddress(avatarAddress: ""{avatarAddress}"") {{
address
simpleAvatar {{
name
level
}}
rank
arenaScore {{
score
}}
}}
}}
}}";
var response = await Query(mimirClient, query);
var data = response?.Data;
if (data is null)
{
return null;
}

try
{
var children = data["arena"]["leaderboardByAvatarAddress"].Children<JObject>();
var participants = children
.Select(e =>
{
var address = new Address(e["address"].Value<string>());
var name = e["simpleAvatar"]["name"].Value<string>();
var nameWithHash = $"{name} <size=80%><color=#A68F7E>#{address.ToHex()[..4]}</color></size>";
return new ArenaParticipantModel
{
AvatarAddr = address,
NameWithHash = nameWithHash,
PortraitId = GameConfig.DefaultAvatarArmorId,
Level = e["simpleAvatar"]["level"].Value<int>(),
Cp = 999_999_999,
GuildName = "Guild Name Here",
Score = e["arenaScore"]["score"].Value<int>(),
Rank = e["rank"].Value<int>(),
WinScore = 0,
LoseScore = 0,
};
})
.ToList();
var me = participants.FirstOrDefault(p => p.AvatarAddr.Equals(avatarAddress));
var myScore = me?.Score ?? ArenaScore.ArenaScoreDefault;
foreach (var participant in participants)
{
var (myWinScore, myDefeatScore, _) =
ArenaHelper.GetScores(myScore, participant.Score);
participant.WinScore = myWinScore;
participant.LoseScore = myDefeatScore;
}

return participants;
}
catch (Exception e)
{
NcDebug.LogError($"Failed to parse arena participants.\n{e}");
return null;
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nekoyume/Assets/_Scripts/Game/Scene/LoginScene.cs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ CommandLineOptions.PrivateKey is null
}

game.SetActionManager();
ApiClients.Instance.Initialize(CommandLineOptions);
ApiClients.Instance.Initialize(CommandLineOptions, game.CurrentPlanetId);

StartCoroutine(game.InitializeIAP());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ namespace Nekoyume.Multiplanetary.Extensions
{
public static class PlanetIdExtensions
{
public static bool Is(this PlanetId planetId, string planeName)
{
planeName = planeName.ToLowerInvariant();
if (planetId.Equals(PlanetId.Odin))
{
return planeName == "odin";
}

if (planetId.Equals(PlanetId.Heimdall))
{
return planeName == "heimdall";
}

return false;
}

public static string ToLocalizedPlanetName(
this PlanetId planetId,
bool containsPlanetId)
Expand Down
87 changes: 67 additions & 20 deletions nekoyume/Assets/_Scripts/State/RxProps.Arena.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Bencodex.Types;
Expand All @@ -8,16 +9,14 @@
using Libplanet.Crypto;
using Nekoyume.Action;
using Nekoyume.ApiClient;
using Nekoyume.Arena;
using Nekoyume.Game.LiveAsset;
using Nekoyume.GraphQL;
using Nekoyume.GraphQL.Queries;
using Nekoyume.Helper;
using Nekoyume.Model.Arena;
using Nekoyume.Model.EnumType;
using Nekoyume.Model.State;
using Nekoyume.UI.Model;
using UnityEngine;
using static Lib9c.SerializeKeys;
using Random = UnityEngine.Random;

namespace Nekoyume.State
{
Expand Down Expand Up @@ -253,22 +252,7 @@ private static async Task<List<ArenaParticipantModel>>
? new ArenaAvatarState(iValue2)
: null;
var lastBattleBlockIndex = arenaAvatarState?.LastBattleBlockIndex ?? 0L;
try
{
var response = await ApiClients.Instance.ArenaServiceClient.QueryArenaInfoAsync(currentAvatarAddr);
// Arrange my information so that it comes first when it's the same score.
arenaInfo = response.StateQuery.ArenaParticipants.ToList();
}
catch (Exception e)
{
NcDebug.LogException(e);
// TODO: this is temporary code for local testing.
arenaInfo.AddRange(_states.AvatarStates.Values.Select(avatar => new ArenaParticipantModel
{
AvatarAddr = avatar.address,
NameWithHash = avatar.NameWithHash
}));
}
arenaInfo = await GetArenaParticipantsAsync(currentAvatarAddr);

string playerGuildName = null;
if (Game.Game.instance.GuildModels.Any())
Expand Down Expand Up @@ -367,5 +351,68 @@ private static async UniTask SetArenaInfoOnMainThreadAsync(ArenaParticipantModel
await UniTask.SwitchToMainThread();
_playerArenaInfo.SetValueAndForceNotify(playerArenaInfo);
}

private static async Task<List<ArenaParticipantModel>> GetArenaParticipantsAsync(Address avatarAddr)
{
var mimirClient = ApiClients.Instance.Mimir;
if (mimirClient is null)
{
return await GetFromHeadlessAsync();
}

try
{
const long confirmedBlockGap = 2;
var mimirArenaBlockIndex = await MimirQuery.GetMetadataBlockIndexAsync(
mimirClient,
"arena");
if (_agent.BlockIndex > mimirArenaBlockIndex + confirmedBlockGap)
{
return await GetFromHeadlessAsync();
}

try
{
return await MimirQuery.GetArenaParticipantsAsync(
mimirClient,
avatarAddr);
}
catch (Exception e)
{
NcDebug.Log("Failed to get arena participants from Mimir. Falling back to headless." +
$" Exception: {e.Message}");
return await GetFromHeadlessAsync();
}
}
catch (Exception e)
{
NcDebug.LogException(e);
// TODO: this is temporary code for local testing.
var result = new List<ArenaParticipantModel>();
result.AddRange(_states.AvatarStates.Values.Select(avatar => new ArenaParticipantModel
{
AvatarAddr = avatar.address,
NameWithHash = avatar.NameWithHash
}));

return result;
}

async Task<List<ArenaParticipantModel>> GetFromHeadlessAsync()
{
try
{
// fallback to Headless
var response = await ApiClients.Instance.ArenaServiceClient.QueryArenaInfoAsync(avatarAddr);
// Arrange my information so that it comes first when it's the same score.
return response.StateQuery.ArenaParticipants.ToList();
}
catch (Exception e)
{
NcDebug.LogError($"[Arena]failed to get participants from Headless. {e.Message}");
return new List<ArenaParticipantModel>();
}
}
}
}
}
1 change: 1 addition & 0 deletions nekoyume/nekoyume.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=Equippable/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fenrir/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ginnungagap/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=heimdall/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=helheim/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Jotunheim/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Libplanet/@EntryIndexedValue">True</s:Boolean>
Expand Down