mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-14 09:11:28 +00:00
Migrate scores to use proper relationships (#830)
* Initial work for score migration * Finish score migration * Implement suggested changes from code review * Make Score Timestamp default the current time * Chunk insertions to reduce packet size and give all scores the same Timestamp * Fix serialization of GameScore * Break score ties by time then scoreId * Make lighthouse score migration not dependent on current score implementation
This commit is contained in:
parent
a7d5095c67
commit
70a66e6034
17 changed files with 549 additions and 210 deletions
|
@ -1,5 +1,4 @@
|
|||
#nullable enable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using LBPUnion.ProjectLighthouse.Database;
|
||||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
|
@ -30,26 +29,14 @@ public class ScoreController : ControllerBase
|
|||
this.database = database;
|
||||
}
|
||||
|
||||
private string[] getFriendUsernames(int userId, string username)
|
||||
private static int[] GetFriendIds(int userId)
|
||||
{
|
||||
UserFriendData? store = UserFriendStore.GetUserFriendData(userId);
|
||||
if (store == null) return new[] { username, };
|
||||
List<int>? friendIds = store?.FriendIds;
|
||||
friendIds ??= new List<int>();
|
||||
friendIds.Add(userId);
|
||||
|
||||
List<string> friendNames = new()
|
||||
{
|
||||
username,
|
||||
};
|
||||
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (int friendId in store.FriendIds)
|
||||
{
|
||||
string? friendUsername = this.database.Users.Where(u => u.UserId == friendId)
|
||||
.Select(u => u.Username)
|
||||
.FirstOrDefault();
|
||||
if (friendUsername != null) friendNames.Add(friendUsername);
|
||||
}
|
||||
|
||||
return friendNames.ToArray();
|
||||
return friendIds.Distinct().ToArray();
|
||||
}
|
||||
|
||||
[HttpPost("scoreboard/{slotType}/{id:int}")]
|
||||
|
@ -144,33 +131,33 @@ public class ScoreController : ControllerBase
|
|||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
string playerIdCollection = string.Join(',', score.PlayerIds);
|
||||
|
||||
ScoreEntity? existingScore = await this.database.Scores.Where(s => s.SlotId == slot.SlotId)
|
||||
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId)
|
||||
.Where(s => s.PlayerIdCollection == playerIdCollection)
|
||||
.Where(s => s.UserId == token.UserId)
|
||||
.Where(s => s.Type == score.Type)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingScore != null)
|
||||
{
|
||||
existingScore.Points = Math.Max(existingScore.Points, score.Points);
|
||||
existingScore.Timestamp = TimeHelper.TimestampMillis;
|
||||
}
|
||||
else
|
||||
{
|
||||
ScoreEntity playerScore = new()
|
||||
{
|
||||
PlayerIdCollection = playerIdCollection,
|
||||
UserId = token.UserId,
|
||||
Type = score.Type,
|
||||
Points = score.Points,
|
||||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
Timestamp = TimeHelper.TimestampMillis,
|
||||
};
|
||||
this.database.Scores.Add(playerScore);
|
||||
}
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
return this.Ok(await this.GetScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scoreboardSegment",
|
||||
PageSize = 5,
|
||||
|
@ -178,7 +165,7 @@ public class ScoreController : ControllerBase
|
|||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = score.Type,
|
||||
TargetUsername = username,
|
||||
TargetUser = token.UserId,
|
||||
TargetPlayerIds = null,
|
||||
}));
|
||||
}
|
||||
|
@ -189,8 +176,6 @@ public class ScoreController : ControllerBase
|
|||
{
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
|
||||
|
||||
LeaderboardOptions options = new()
|
||||
|
@ -199,7 +184,7 @@ public class ScoreController : ControllerBase
|
|||
PageStart = 1,
|
||||
ScoreType = -1,
|
||||
SlotId = id,
|
||||
TargetUsername = username,
|
||||
TargetUser = token.UserId,
|
||||
RootName = "scoreboardSegment",
|
||||
};
|
||||
if (!HttpMethods.IsPost(this.Request.Method))
|
||||
|
@ -208,7 +193,7 @@ public class ScoreController : ControllerBase
|
|||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
options.ScoreType = i;
|
||||
ScoreboardResponse response = this.getScores(options);
|
||||
ScoreboardResponse response = await this.GetScores(options);
|
||||
scoreboardResponses.Add(new PlayerScoreboardResponse(response.Scores, i));
|
||||
}
|
||||
return this.Ok(new MultiScoreboardResponse(scoreboardResponses));
|
||||
|
@ -217,9 +202,9 @@ public class ScoreController : ControllerBase
|
|||
GameScore? score = await this.DeserializeBody<GameScore>();
|
||||
if (score == null) return this.BadRequest();
|
||||
options.ScoreType = score.Type;
|
||||
options.TargetPlayerIds = this.getFriendUsernames(token.UserId, username);
|
||||
options.TargetPlayerIds = GetFriendIds(token.UserId);
|
||||
|
||||
return this.Ok(this.getScores(options));
|
||||
return this.Ok(await this.GetScores(options));
|
||||
}
|
||||
|
||||
[HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")]
|
||||
|
@ -230,15 +215,13 @@ public class ScoreController : ControllerBase
|
|||
|
||||
if (pageSize <= 0) return this.BadRequest();
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
|
||||
|
||||
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
|
||||
|
||||
string[] friendIds = this.getFriendUsernames(token.UserId, username);
|
||||
int[] friendIds = GetFriendIds(token.UserId);
|
||||
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
return this.Ok(await this.GetScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scores",
|
||||
PageSize = pageSize,
|
||||
|
@ -246,27 +229,24 @@ public class ScoreController : ControllerBase
|
|||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = type,
|
||||
TargetUsername = username,
|
||||
TargetUser = token.UserId,
|
||||
TargetPlayerIds = friendIds,
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")]
|
||||
[HttpGet("topscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
|
||||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
public async Task<IActionResult> TopScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
|
||||
{
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
if (pageSize <= 0) return this.BadRequest();
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest();
|
||||
|
||||
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
|
||||
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
return this.Ok(await this.GetScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scores",
|
||||
PageSize = pageSize,
|
||||
|
@ -274,52 +254,56 @@ public class ScoreController : ControllerBase
|
|||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = type,
|
||||
TargetUsername = username,
|
||||
TargetUser = token.UserId,
|
||||
TargetPlayerIds = null,
|
||||
}));
|
||||
}
|
||||
|
||||
private class LeaderboardOptions
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
public int SlotId { get; init; }
|
||||
public int ScoreType { get; set; }
|
||||
public string TargetUsername { get; set; } = "";
|
||||
public int PageStart { get; set; } = -1;
|
||||
public int PageSize { get; set; } = 5;
|
||||
public string RootName { get; set; } = "scores";
|
||||
public string[]? TargetPlayerIds;
|
||||
public int TargetUser { get; init; }
|
||||
public int PageStart { get; init; } = -1;
|
||||
public int PageSize { get; init; } = 5;
|
||||
public string RootName { get; init; } = "scores";
|
||||
public int[]? TargetPlayerIds;
|
||||
public int? ChildSlotId;
|
||||
}
|
||||
|
||||
private ScoreboardResponse getScores(LeaderboardOptions options)
|
||||
private async Task<ScoreboardResponse> GetScores(LeaderboardOptions options)
|
||||
{
|
||||
// This is hella ugly but it technically assigns the proper rank to a score
|
||||
// var needed for Anonymous type returned from SELECT
|
||||
var rankedScores = this.database.Scores.Where(s => s.SlotId == options.SlotId)
|
||||
IQueryable<ScoreEntity> scoreQuery = this.database.Scores.Where(s => s.SlotId == options.SlotId)
|
||||
.Where(s => options.ScoreType == -1 || s.Type == options.ScoreType)
|
||||
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == options.ChildSlotId)
|
||||
.AsEnumerable()
|
||||
.Where(s => options.TargetPlayerIds == null ||
|
||||
options.TargetPlayerIds.Any(id => s.PlayerIdCollection.Split(",").Contains(id)))
|
||||
.OrderByDescending(s => s.Points)
|
||||
.ThenBy(s => s.ScoreId)
|
||||
.ToList()
|
||||
.Select((s, rank) => new
|
||||
.Where(s => options.TargetPlayerIds == null || options.TargetPlayerIds.Contains(s.UserId));
|
||||
|
||||
// First find if you have a score on a level to find scores around it
|
||||
var myScore = await scoreQuery.Where(s => s.UserId == options.TargetUser)
|
||||
.Select(s => new
|
||||
{
|
||||
Score = s,
|
||||
Rank = rank + 1,
|
||||
Rank = scoreQuery.Count(s2 => s2.Points > s.Points) + 1,
|
||||
}).FirstOrDefaultAsync();
|
||||
|
||||
int skipAmt = options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3;
|
||||
|
||||
var rankedScores = scoreQuery.OrderByDescending(s => s.Points)
|
||||
.ThenBy(s => s.Timestamp)
|
||||
.ThenBy(s => s.ScoreId)
|
||||
.Skip(Math.Max(0, skipAmt))
|
||||
.Take(Math.Min(options.PageSize, 30))
|
||||
.Select(s => new
|
||||
{
|
||||
Score = s,
|
||||
Rank = scoreQuery.Count(s2 => s2.Points > s.Points) + 1,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
int totalScores = scoreQuery.Count();
|
||||
|
||||
// Find your score, since even if you aren't in the top list your score is pinned
|
||||
var myScore = rankedScores.Where(rs => rs.Score.PlayerIdCollection.Split(",").Contains(options.TargetUsername)).MaxBy(rs => rs.Score.Points);
|
||||
List<GameScore> gameScores = rankedScores.ToSerializableList(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank));
|
||||
|
||||
// Paginated viewing: if not requesting pageStart, get results around user
|
||||
var pagedScores = rankedScores.Skip(options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3).Take(Math.Min(options.PageSize, 30));
|
||||
|
||||
List<GameScore> gameScores = pagedScores.ToSerializableList(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank));
|
||||
|
||||
return new ScoreboardResponse(options.RootName, gameScores, rankedScores.Count, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0);
|
||||
return new ScoreboardResponse(options.RootName, gameScores, totalScores, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue