Skip to content

Commit

Permalink
Implement friend leaderboards (#589)
Browse files Browse the repository at this point in the history
The query here is extraordinarily slow (~10s in Debug mode on my
machine, with a warmed up JIT). But leaving it as a 404 causes the game
to wait a full 30 seconds between requests, so even though these
requests are serial in LBP, its still faster for users in the end to
have it implemented.

We can optimize the query after Postgres gets in.
  • Loading branch information
jvyden authored Jul 28, 2024
2 parents 812f271 + 1439413 commit a2d8ff1
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 4 deletions.
23 changes: 19 additions & 4 deletions Refresh.GameServer/Database/GameDatabaseContext.Leaderboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ public GameSubmittedScore SubmitScore(SerializedScore score, GameUser user, Game
return newScore;
}

[UsedImplicitly] private record ScoreLevelWithPlayer(GameLevel Level, GameUser Player);

public DatabaseList<GameSubmittedScore> GetTopScoresForLevel(GameLevel level, int count, int skip, byte type, bool showDuplicates = false)
{
IEnumerable<GameSubmittedScore> scores = this.GameSubmittedScores
Expand All @@ -70,7 +68,7 @@ public DatabaseList<GameSubmittedScore> GetTopScoresForLevel(GameLevel level, in
.AsEnumerable();

if (!showDuplicates)
scores = scores.DistinctBy(s => new ScoreLevelWithPlayer(s.Level, s.Players[0]));
scores = scores.DistinctBy(s => s.Players[0]);

return new DatabaseList<GameSubmittedScore>(scores, skip, count);
}
Expand All @@ -86,13 +84,30 @@ public IEnumerable<ScoreWithRank> GetRankedScoresAroundScore(GameSubmittedScore
.AsEnumerable()
.ToList();

scores = scores.DistinctBy(s => new ScoreLevelWithPlayer(s.Level, s.Players[0]))
scores = scores.DistinctBy(s => s.Players[0])
.ToList();

return scores.Select((s, i) => new ScoreWithRank(s, i + 1))
.Skip(Math.Min(scores.Count, scores.IndexOf(score) - count / 2)) // center user's score around other scores
.Take(count);
}

public IEnumerable<ScoreWithRank> GetLevelTopScoresByFriends(GameUser user, GameLevel level, int count, byte type)
{
IEnumerable<GameUser> mutuals = this.GetUsersMutuals(user);

IEnumerable<GameSubmittedScore> scores = this.GameSubmittedScores
.Where(s => s.ScoreType == type && s.Level == level)
.OrderByDescending(s => s.Score)
.AsEnumerable()
.DistinctBy(s => s.Players[0].UserId)
//TODO: THIS CALL IS EXTREMELY INEFFECIENT!!! once we are in postgres land, figure out a way to do this effeciently
.Where(s => s.Players.Any(p => p.UserId == user.UserId || mutuals.Contains(p)))
.Take(10)
.ToList();

return scores.Select((s, i) => new ScoreWithRank(s, i + 1));
}

[Pure]
[ContractAnnotation("null => null; notnull => canbenull")]
Expand Down
19 changes: 19 additions & 0 deletions Refresh.GameServer/Endpoints/Game/Levels/LeaderboardEndpoints.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using Bunkum.Core;
using Bunkum.Core.Endpoints;
using Bunkum.Core.Endpoints.Debugging;
using Bunkum.Core.RateLimit;
using Bunkum.Core.Responses;
using Bunkum.Listener.Protocol;
Expand All @@ -11,6 +12,7 @@
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Lists;
using Refresh.GameServer.Types.Roles;
using Refresh.GameServer.Types.Scores;
using Refresh.GameServer.Types.UserData;
using Refresh.GameServer.Types.UserData.Leaderboard;

Expand Down Expand Up @@ -66,6 +68,23 @@ public Response GetUserScores(RequestContext context, GameUser user, GameDatabas

return new Response(SerializedMultiLeaderboardResponse.FromOld(multiLeaderboard), ContentType.Xml);
}

[GameEndpoint("scoreboard/friends/{slotType}/{id}", HttpMethods.Post, ContentType.Xml)]
[RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, BucketName)]
[NullStatusCode(NotFound)]
public SerializedScoreLeaderboardList? GetLevelFriendLeaderboard(
RequestContext context,
GameUser user,
GameDatabaseContext database,
string slotType,
int id,
FriendScoresRequest body)
{
GameLevel? level = database.GetLevelByIdAndType(slotType, id);
if (level == null) return null;

return SerializedScoreLeaderboardList.FromSubmittedEnumerable(database.GetLevelTopScoresByFriends(user, level, 10, body.Type));
}

[GameEndpoint("scoreboard/{slotType}/{id}", ContentType.Xml, HttpMethods.Post)]
[RateLimitSettings(RequestTimeoutDuration, MaxRequestAmount, RequestBlockDuration, BucketName)]
Expand Down
13 changes: 13 additions & 0 deletions Refresh.GameServer/Types/Scores/FriendScoresRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Xml.Serialization;

namespace Refresh.GameServer.Types.Scores;

[XmlRoot("playRecord")]
public class FriendScoresRequest
{
[XmlElement("playerIds")]
public List<string> Usernames { get; set; }

Check warning on line 9 in Refresh.GameServer/Types/Scores/FriendScoresRequest.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Usernames' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

[XmlElement("type")]
public byte Type { get; set; }
}

0 comments on commit a2d8ff1

Please sign in to comment.