From 35f2448e0026f393434819a294606aedeecb2096 Mon Sep 17 00:00:00 2001 From: shiibe <82057235+shiibe@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:29:33 -0400 Subject: [PATCH] Add leaderboard API endpoint --- GameDatabase/Entities/SongBestDatum.cs | 7 +- .../Responses/SongLeaderboardResponse.cs | 6 ++ SharedProject/Models/SongBestData.cs | 1 - SharedProject/Models/SongLeaderboard.cs | 27 ++++++ .../Controllers/Api/SongLeaderboardData.cs | 43 ++++++++ .../Interfaces/ISongLeaderboardService.cs | 7 ++ .../Services/SongLeaderboardService.cs | 97 +++++++++++++++++++ .../Components/Song/SongLeaderboardCard.razor | 2 + 8 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 SharedProject/Models/Responses/SongLeaderboardResponse.cs create mode 100644 SharedProject/Models/SongLeaderboard.cs create mode 100644 TaikoLocalServer/Controllers/Api/SongLeaderboardData.cs create mode 100644 TaikoLocalServer/Services/Interfaces/ISongLeaderboardService.cs create mode 100644 TaikoLocalServer/Services/SongLeaderboardService.cs create mode 100644 TaikoWebUI/Components/Song/SongLeaderboardCard.razor diff --git a/GameDatabase/Entities/SongBestDatum.cs b/GameDatabase/Entities/SongBestDatum.cs index 8cf993a..b8c5c54 100644 --- a/GameDatabase/Entities/SongBestDatum.cs +++ b/GameDatabase/Entities/SongBestDatum.cs @@ -11,7 +11,12 @@ namespace GameDatabase.Entities public uint BestRate { get; set; } public CrownType BestCrown { get; set; } public ScoreRank BestScoreRank { get; set; } - + public uint GoodCount { get; set; } + public uint OkCount { get; set; } + public uint MissCount { get; set; } + public uint ComboCount { get; set; } + public uint HitCount { get; set; } + public uint DrumrollCount { get; set; } public virtual UserDatum? Ba { get; set; } } } \ No newline at end of file diff --git a/SharedProject/Models/Responses/SongLeaderboardResponse.cs b/SharedProject/Models/Responses/SongLeaderboardResponse.cs new file mode 100644 index 0000000..0075c34 --- /dev/null +++ b/SharedProject/Models/Responses/SongLeaderboardResponse.cs @@ -0,0 +1,6 @@ +namespace SharedProject.Models.Responses; + +public class SongLeaderboardResponse +{ + public List Leaderboard { get; set; } = new(); +} \ No newline at end of file diff --git a/SharedProject/Models/SongBestData.cs b/SharedProject/Models/SongBestData.cs index f721dca..544ee39 100644 --- a/SharedProject/Models/SongBestData.cs +++ b/SharedProject/Models/SongBestData.cs @@ -46,6 +46,5 @@ public class SongBestData public List AiSectionBestData { get; set; } = new(); public bool ShowAiData { get; set; } - public List RecentPlayData { get; set; } = new(); } \ No newline at end of file diff --git a/SharedProject/Models/SongLeaderboard.cs b/SharedProject/Models/SongLeaderboard.cs new file mode 100644 index 0000000..f61acfc --- /dev/null +++ b/SharedProject/Models/SongLeaderboard.cs @@ -0,0 +1,27 @@ +using SharedProject.Enums; + +namespace SharedProject.Models; + +public class SongLeaderboard +{ + public uint Rank { get; set; } + + public uint Baid { get; set; } + + public uint BestScore { get; set; } + + public uint BestRate { get; set; } + + public CrownType BestCrown { get; set; } + + public ScoreRank BestScoreRank { get; set; } + + public uint GoodCount { get; set; } + public uint OkCount { get; set; } + public uint MissCount { get; set; } + public uint ComboCount { get; set; } + public uint HitCount { get; set; } + public uint DrumrollCount { get; set; } + + public string? UserName { get; set; } +} \ No newline at end of file diff --git a/TaikoLocalServer/Controllers/Api/SongLeaderboardData.cs b/TaikoLocalServer/Controllers/Api/SongLeaderboardData.cs new file mode 100644 index 0000000..905ef91 --- /dev/null +++ b/TaikoLocalServer/Controllers/Api/SongLeaderboardData.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Options; +using SharedProject.Models; +using SharedProject.Models.Responses; +using TaikoLocalServer.Filters; +using TaikoLocalServer.Settings; + +namespace TaikoLocalServer.Controllers.Api; + +[ApiController] +[Route("api/[controller]")] + +public class SongLeaderboardData(ISongLeaderboardService songLeaderboardService,IAuthService authService, IOptions settings) + : BaseController +{ + private readonly AuthSettings authSettings = settings.Value; + + [HttpGet("{baid}/{songId}/{difficulty}")] + [ServiceFilter(typeof(AuthorizeIfRequiredAttribute))] + + public async Task> GetSongLeaderboard(uint baid, uint songId, uint difficulty) + { + if (authSettings.AuthenticationRequired) + { + var tokenInfo = authService.ExtractTokenInfo(HttpContext); + if (tokenInfo is null) + { + return Unauthorized(); + } + + if (tokenInfo.Value.baid != baid && !tokenInfo.Value.isAdmin) + { + return Forbid(); + } + } + + var leaderboard = await songLeaderboardService.GetSongLeaderboard(songId, (Difficulty)difficulty, (int)baid); + + return Ok(new SongLeaderboardResponse + { + Leaderboard = leaderboard + }); + } +} \ No newline at end of file diff --git a/TaikoLocalServer/Services/Interfaces/ISongLeaderboardService.cs b/TaikoLocalServer/Services/Interfaces/ISongLeaderboardService.cs new file mode 100644 index 0000000..1e7eeee --- /dev/null +++ b/TaikoLocalServer/Services/Interfaces/ISongLeaderboardService.cs @@ -0,0 +1,7 @@ +namespace TaikoLocalServer.Services.Interfaces; + +using SharedProject.Models; +public interface ISongLeaderboardService +{ + public Task> GetSongLeaderboard(uint songId, Difficulty difficulty, int baid, int limit = 10); +} \ No newline at end of file diff --git a/TaikoLocalServer/Services/SongLeaderboardService.cs b/TaikoLocalServer/Services/SongLeaderboardService.cs new file mode 100644 index 0000000..4234193 --- /dev/null +++ b/TaikoLocalServer/Services/SongLeaderboardService.cs @@ -0,0 +1,97 @@ +using GameDatabase.Context; +using SharedProject.Models; + +namespace TaikoLocalServer.Services; + +public class SongLeaderboardService : ISongLeaderboardService +{ + private readonly TaikoDbContext context; + + public SongLeaderboardService(TaikoDbContext context) + { + this.context = context; + } + + public async Task> GetSongLeaderboard(uint songId, Difficulty difficulty, int baid, + int limit = 10) + { + if (baid == 0) + { + throw new ArgumentNullException(nameof(baid)); + } + + // get all scores for the song from every user + var scores = await context.SongBestData + .Where(x => x.SongId == songId && x.Difficulty == difficulty) + .OrderByDescending(x => x.BestScore) + .ThenByDescending(x => x.BestRate) + .Take(limit) + .ToListAsync(); + + // get the user data for each score + var leaderboard = new List(); + + // get the user data for each score + foreach (var score in scores) + { + var user = await context.UserData + .Where(x => x.Baid == score.Baid) + .FirstOrDefaultAsync(); + + leaderboard.Add(new SongLeaderboard + { + Rank = (uint)leaderboard.Count + 1, + Baid = score.Baid, + UserName = user?.MyDonName, + BestScore = score.BestScore, + BestRate = score.BestRate, + BestCrown = score.BestCrown, + BestScoreRank = score.BestScoreRank, + GoodCount = score.GoodCount, + OkCount = score.OkCount, + MissCount = score.MissCount, + ComboCount = score.ComboCount, + HitCount = score.HitCount, + DrumrollCount = score.DrumrollCount + }); + } + + // get current user score if it exists + var currentUserScore = await context.SongBestData + .Where(x => x.SongId == songId && x.Difficulty == difficulty && x.Baid == baid) + .FirstOrDefaultAsync(); + + if (currentUserScore != null) + { + // check if they are in the limit + var userRank = leaderboard.FindIndex(x => x.Baid == baid); + + if (userRank >= limit) + { + // get the user data for the current user + var user = await context.UserData + .Where(x => x.Baid == baid) + .FirstOrDefaultAsync(); + + leaderboard.Add(new SongLeaderboard + { + Rank = (uint)userRank + 1, + Baid = currentUserScore.Baid, + UserName = user?.MyDonName, + BestScore = currentUserScore.BestScore, + BestRate = currentUserScore.BestRate, + BestCrown = currentUserScore.BestCrown, + BestScoreRank = currentUserScore.BestScoreRank, + GoodCount = currentUserScore.GoodCount, + OkCount = currentUserScore.OkCount, + MissCount = currentUserScore.MissCount, + ComboCount = currentUserScore.ComboCount, + HitCount = currentUserScore.HitCount, + DrumrollCount = currentUserScore.DrumrollCount + }); + } + } + + return leaderboard; + } +} \ No newline at end of file diff --git a/TaikoWebUI/Components/Song/SongLeaderboardCard.razor b/TaikoWebUI/Components/Song/SongLeaderboardCard.razor new file mode 100644 index 0000000..1587ea8 --- /dev/null +++ b/TaikoWebUI/Components/Song/SongLeaderboardCard.razor @@ -0,0 +1,2 @@ +@using TaikoWebUI.Utilities; +