Skip to content

Commit

Permalink
feat: Add communication with leaderboards api
Browse files Browse the repository at this point in the history
  • Loading branch information
Patryk Chojnicki authored and Dawid Sygocki committed May 4, 2023
1 parent 8b76193 commit f42a1a3
Show file tree
Hide file tree
Showing 25 changed files with 2,408 additions and 18 deletions.
8 changes: 8 additions & 0 deletions Runtime/Communication/Leaderboards.meta

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

125 changes: 125 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Elympics
{
/// <summary>
/// Create leaderboard client with desired parameters and use its fetch methods with custom callbacks
/// </summary>
public class LeaderboardClient
{
private const string LeaderboardsUrl = "https://api.elympics.cc/v2/leaderboardservice/leaderboard";
private const string LeaderboardsUserCenteredUrl = "https://api.elympics.cc/v2/leaderboardservice/leaderboard/user-centred";

private readonly Dictionary<string, string> _queryValues;

private bool _isBusy;
private int _currentPage = 1;

/// <param name="pageSize">Must be in range 1 - 100 (inclusive)</param>
/// <param name="queueName">No filtering by queue if null provided</param>
public LeaderboardClient(int pageSize, LeaderboardTimeScope timeScope, string queueName = null, LeaderboardGameVersion gameVersion = LeaderboardGameVersion.All)
{
if (pageSize < 1 || pageSize > 100)
throw new ArgumentOutOfRangeException(nameof(pageSize), "Must be in range 1 - 100 (inclusive)");

if (timeScope == null)
throw new ArgumentNullException(nameof(timeScope));

if (!Enum.IsDefined(typeof(LeaderboardGameVersion), gameVersion))
throw new ArgumentOutOfRangeException(nameof(gameVersion));

var gameConfig = ElympicsConfig.Load().GetCurrentGameConfig();
if (gameConfig == null)
throw new ElympicsException("Provide ElympicsGameConfig before proceeding");

_queryValues = new Dictionary<string, string>
{
{ "PageNumber", _currentPage.ToString() },
{ "PageSize", pageSize.ToString() },
{ "GameId", gameConfig.GameId },
{ "GameVersion", gameVersion == LeaderboardGameVersion.All ? null : gameConfig.GameVersion },
{ "QueueName", queueName },
{ "TimeScope", timeScope.LeaderboardTimeScopeType.ToString() },
};

if (timeScope.LeaderboardTimeScopeType == LeaderboardTimeScopeType.Custom)
{
_queryValues.Add("DateFrom", timeScope.DateFrom.ToString("o"));
_queryValues.Add("DateTo", timeScope.DateTo.ToString("o"));
}
}


public void FetchFirstPage(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure = null) => SendLeaderboardRequest(LeaderboardsUrl, 1, onSuccess, onFailure);
public void FetchPageWithUser(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure = null) => SendLeaderboardRequest(LeaderboardsUserCenteredUrl, _currentPage, onSuccess, onFailure);
public void FetchNextPage(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure = null) => SendLeaderboardRequest(LeaderboardsUrl, _currentPage + 1, onSuccess, onFailure);
public void FetchPreviousPage(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure = null) => SendLeaderboardRequest(LeaderboardsUrl, _currentPage - 1, onSuccess, onFailure);
public void FetchRefreshedCurrentPage(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure = null) => SendLeaderboardRequest(LeaderboardsUrl, _currentPage, onSuccess, onFailure);

private void SendLeaderboardRequest(string url, int pageNumber, Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure)
{
onFailure = onFailure ?? DefaultFailure;

if (_isBusy)
onFailure(LeaderboardFetchError.RequestAlreadyInProgress);
else if (!ElympicsLobbyClient.Instance.IsAuthenticated)
onFailure(LeaderboardFetchError.NotAuthenticated);
else if (pageNumber < 1)
onFailure(LeaderboardFetchError.PageLessThanOne);
else
{
_isBusy = true;
_queryValues["PageNumber"] = pageNumber.ToString();
var authorization = ElympicsLobbyClient.Instance.AuthData.BearerAuthorization;
ElympicsWebClient.SendGetRequest(url, _queryValues, authorization, HandleCallback(onSuccess, onFailure));
}
}


private Action<Result<LeaderboardResponse, Exception>> HandleCallback(Action<LeaderboardFetchResult> onSuccess, Action<LeaderboardFetchError> onFailure)
{
return result =>
{
_isBusy = false;
if (result.IsSuccess)
{
if (result.Value?.data?.Any() is true)
{
_currentPage = result.Value.pageNumber;
onSuccess(new LeaderboardFetchResult(result.Value));
}
else if (result.Value?.pageNumber == 1)
onFailure(LeaderboardFetchError.NoRecords);
else
onFailure(LeaderboardFetchError.PageGreaterThanMax);
}
else
{
var errorMessage = result.Error.ToString();
if (errorMessage.Contains("There is no score of this user in the leaderboard."))
onFailure(LeaderboardFetchError.NoScoresForUser);
else if (errorMessage.Contains("401"))
onFailure(LeaderboardFetchError.NotAuthenticated);
else
{
onFailure(LeaderboardFetchError.UnknownError);
Debug.LogError(errorMessage);
}
}
};
}

private static void DefaultFailure(LeaderboardFetchError error) => Debug.LogError(error);
}

public enum LeaderboardGameVersion
{
All,
Current,
}
}
11 changes: 11 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardClient.cs.meta

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

24 changes: 24 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardEntry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System;

namespace Elympics
{
public class LeaderboardEntry
{
/// <summary>
/// Raw user ID.
/// It is recommended to pair it with nicknames using your own external backend.
/// </summary>
public string UserId { get; }
public int Position { get; }
public float Score { get; }
public DateTimeOffset ScoredAt { get; }

internal LeaderboardEntry(LeaderboardResponseEntry entry)
{
UserId = entry.userId;
Position = entry.position;
Score = entry.points;
ScoredAt = DateTimeOffset.Parse(entry.endedAt);
}
}
}
11 changes: 11 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardEntry.cs.meta

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

16 changes: 16 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardFetchError.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Elympics
{
public enum LeaderboardFetchError
{
UnknownError = 0,

NoRecords = 101,
PageLessThanOne = 102,
PageGreaterThanMax = 103,
NoScoresForUser = 104,

RequestAlreadyInProgress = 201,

NotAuthenticated = 301,
}
}
11 changes: 11 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardFetchError.cs.meta

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

22 changes: 22 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardFetchResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using System.Linq;

namespace Elympics
{
public class LeaderboardFetchResult
{
public List<LeaderboardEntry> Entries { get; }
public int TotalRecords { get; }
public int PageNumber { get; }

internal LeaderboardFetchResult(LeaderboardResponse response)
{
if (response == null)
return;

Entries = response.data?.Select(x => new LeaderboardEntry(x)).ToList();
TotalRecords = response.totalRecords;
PageNumber = response.pageNumber;
}
}
}
11 changes: 11 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardFetchResult.cs.meta

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

48 changes: 48 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardTimeScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;

namespace Elympics
{
public class LeaderboardTimeScope
{
public LeaderboardTimeScopeType LeaderboardTimeScopeType { get; }
public DateTimeOffset DateFrom { get; }
public DateTimeOffset DateTo { get; }

private const int MaxDaysSpan = 31;

public LeaderboardTimeScope(LeaderboardTimeScopeType leaderboardTimeScopeType)
{
if (!Enum.IsDefined(typeof(LeaderboardTimeScopeType), leaderboardTimeScopeType))
throw new ArgumentOutOfRangeException(nameof(leaderboardTimeScopeType));

if (leaderboardTimeScopeType == LeaderboardTimeScopeType.Custom)
throw new ArgumentException("To declare custom time scope use constructor with DateTime parameters");

LeaderboardTimeScopeType = leaderboardTimeScopeType;
}

public LeaderboardTimeScope(DateTimeOffset dateFrom, DateTimeOffset dateTo)
{
if (dateFrom >= dateTo)
throw new ArgumentException($"{nameof(dateFrom)} must be before {nameof(dateTo)}");

if ((dateTo - dateFrom).TotalDays > MaxDaysSpan)
throw new ArgumentException($"Maximal accepted date difference is {MaxDaysSpan} days");

LeaderboardTimeScopeType = LeaderboardTimeScopeType.Custom;
DateFrom = dateFrom;
DateTo = dateTo;
}

public LeaderboardTimeScope(DateTimeOffset dateStart, TimeSpan timeSpan) : this(dateStart, dateStart + timeSpan)
{ }
}

public enum LeaderboardTimeScopeType
{
AllTime = 0,
Month = 1,
Day = 2,
Custom = 3,
}
}
11 changes: 11 additions & 0 deletions Runtime/Communication/Leaderboards/LeaderboardTimeScope.cs.meta

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

8 changes: 8 additions & 0 deletions Runtime/Communication/Leaderboards/Models.meta

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

16 changes: 16 additions & 0 deletions Runtime/Communication/Leaderboards/Models/LeaderboardResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace Elympics
{
internal class LeaderboardResponse : PaginatedResponseModel<LeaderboardResponseEntry> { }

[Serializable]
internal class LeaderboardResponseEntry
{
public string userId;
public string matchId;
public int position;
public float points;
public string endedAt;
};
}

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;

namespace Elympics
{
[Serializable]
internal class PaginatedResponseModel<T>
{
public List<T> data;
public int pageNumber;
public int pageSize;
public int totalPages;
public int totalRecords;

public string firstPage;
public string lastPage;
public string nextPage;
public string previousPage;
}
}

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

0 comments on commit f42a1a3

Please sign in to comment.