mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-17 23:22:27 +00:00
Refactor serialization system (#702)
* Initial work for serialization refactor * Experiment with new naming conventions * Mostly implement user and slot serialization. Still needs to be fine tuned to match original implementation Many things are left in a broken state like website features/api endpoints/lbp3 categories * Fix release building * Migrate scores, reviews, and more to new serialization system. Many things are still broken but progress is steadily being made * Fix Api responses and migrate serialization for most types * Make serialization better and fix bugs Fix recursive PrepareSerialization when recursive item is set during root item's PrepareSerialization, items, should be properly indexed in order but it's only tested to 1 level of recursion * Fix review serialization * Fix user serialization producing malformed SQL query * Remove DefaultIfEmpty query * MariaDB doesn't like double nested queries * Fix LBP1 tag counter * Implement lbp3 categories and add better deserialization handling * Implement expression tree caching to speed up reflection and write new serializer tests * Remove Game column from UserEntity and rename DatabaseContextModelSnapshot.cs back to DatabaseModelSnapshot.cs * Make UserEntity username not required * Fix recursive serialization of lists and add relevant unit tests * Actually commit the migration * Fix LocationTests to use new deserialization class * Fix comments not serializing the right author username * Replace all occurrences of StatusCode with their respective ASP.NET named result instead of StatusCode(403) everything is now in the form of Forbid() * Fix SlotBase.ConvertToEntity and LocationTests * Fix compilation error * Give Location a default value in GameUserSlot and GameUser * Reimplement stubbed website functions * Convert grief reports to new serialization system * Update DatabaseModelSnapshot and bump dotnet tool version * Remove unused directives * Fix broken type reference * Fix rated comments on website * Don't include banned users in website comments * Optimize score submission * Fix slot id calculating in in-game comment posting * Move serialization interfaces to types folder and add more documentation * Allow uploading of versus scores
This commit is contained in:
parent
307b2135a3
commit
329ab66043
248 changed files with 4993 additions and 2896 deletions
|
@ -4,12 +4,12 @@ using LBPUnion.ProjectLighthouse.Database;
|
|||
using LBPUnion.ProjectLighthouse.Extensions;
|
||||
using LBPUnion.ProjectLighthouse.Helpers;
|
||||
using LBPUnion.ProjectLighthouse.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Serialization;
|
||||
using LBPUnion.ProjectLighthouse.StorableLists.Stores;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Level;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Token;
|
||||
using LBPUnion.ProjectLighthouse.Types.Levels;
|
||||
using LBPUnion.ProjectLighthouse.Types.Logging;
|
||||
using LBPUnion.ProjectLighthouse.Types.Serialization;
|
||||
using LBPUnion.ProjectLighthouse.Types.Users;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -34,7 +34,7 @@ public class ScoreController : ControllerBase
|
|||
[HttpPost("scoreboard/{slotType}/{id:int}/{childId:int}")]
|
||||
public async Task<IActionResult> SubmitScore(string slotType, int id, int childId)
|
||||
{
|
||||
GameToken token = this.GetToken();
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
string username = await this.database.UsernameFromGameToken(token);
|
||||
|
||||
|
@ -44,16 +44,15 @@ public class ScoreController : ControllerBase
|
|||
return this.BadRequest();
|
||||
}
|
||||
|
||||
Score? score = await this.DeserializeBody<Score>();
|
||||
GameScore? score = await this.DeserializeBody<GameScore>();
|
||||
if (score == null)
|
||||
{
|
||||
Logger.Warn($"Rejecting score upload, score is null (slotType={slotType}, slotId={id}, user={username})", LogArea.Score);
|
||||
return this.BadRequest();
|
||||
}
|
||||
|
||||
// This only seems to happens on lbp2 versus levels, not sure why
|
||||
if (score.PlayerIdCollection.Contains(':'))
|
||||
score.PlayerIdCollection = score.PlayerIdCollection.Replace(':', ',');
|
||||
// Workaround for parsing player ids of versus levels
|
||||
if (score.PlayerIds.Length == 1 && score.PlayerIds[0].Contains(':')) score.PlayerIds = score.PlayerIds[0].Split(":");
|
||||
|
||||
if (score.PlayerIds.Length == 0)
|
||||
{
|
||||
|
@ -89,15 +88,14 @@ public class ScoreController : ControllerBase
|
|||
|
||||
SanitizationHelper.SanitizeStringsInClass(score);
|
||||
|
||||
if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer);
|
||||
int slotId = id;
|
||||
|
||||
score.SlotId = id;
|
||||
score.ChildSlotId = childId;
|
||||
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
|
||||
|
||||
Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId);
|
||||
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == id);
|
||||
if (slot == null)
|
||||
{
|
||||
Logger.Warn($"Rejecting score upload, slot is null (slotId={score.SlotId}, slotType={slotType}, reqId={id}, user={username})", LogArea.Score);
|
||||
Logger.Warn($"Rejecting score upload, slot is null (slotId={slotId}, slotType={slotType}, reqId={id}, user={username})", LogArea.Score);
|
||||
return this.BadRequest();
|
||||
}
|
||||
|
||||
|
@ -115,46 +113,56 @@ public class ScoreController : ControllerBase
|
|||
break;
|
||||
case GameVersion.LittleBigPlanetPSP:
|
||||
case GameVersion.Unknown:
|
||||
default: throw new ArgumentOutOfRangeException();
|
||||
default:
|
||||
return this.BadRequest();
|
||||
}
|
||||
|
||||
Score playerScore = new()
|
||||
{
|
||||
PlayerIdCollection = string.Join(',', score.PlayerIds),
|
||||
Type = score.Type,
|
||||
Points = score.Points,
|
||||
SlotId = score.SlotId,
|
||||
ChildSlotId = score.ChildSlotId,
|
||||
};
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
IQueryable<Score> existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId)
|
||||
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 == playerScore.PlayerIdCollection)
|
||||
.Where(s => s.Type == playerScore.Type);
|
||||
if (existingScore.Any())
|
||||
.Where(s => s.PlayerIdCollection == playerIdCollection)
|
||||
.Where(s => s.Type == score.Type)
|
||||
.FirstOrDefaultAsync();
|
||||
if (existingScore != null)
|
||||
{
|
||||
Score first = existingScore.First(s => s.SlotId == playerScore.SlotId);
|
||||
playerScore.ScoreId = first.ScoreId;
|
||||
playerScore.Points = Math.Max(first.Points, playerScore.Points);
|
||||
this.database.Entry(first).CurrentValues.SetValues(playerScore);
|
||||
existingScore.Points = Math.Max(existingScore.Points, score.Points);
|
||||
}
|
||||
else
|
||||
{
|
||||
ScoreEntity playerScore = new()
|
||||
{
|
||||
PlayerIdCollection = playerIdCollection,
|
||||
Type = score.Type,
|
||||
Points = score.Points,
|
||||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
};
|
||||
this.database.Scores.Add(playerScore);
|
||||
}
|
||||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment", childId: score.ChildSlotId);
|
||||
|
||||
return this.Ok(myRanking);
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scoreboardSegment",
|
||||
PageSize = 5,
|
||||
PageStart = -1,
|
||||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = score.Type,
|
||||
TargetUsername = username,
|
||||
TargetPlayerIds = null,
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet("friendscores/{slotType}/{slotId:int}/{type:int}")]
|
||||
[HttpGet("friendscores/{slotType}/{slotId:int}/{childId:int}/{type:int}")]
|
||||
public async Task<IActionResult> FriendScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
|
||||
{
|
||||
GameToken token = this.GetToken();
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
if (pageSize <= 0) return this.BadRequest();
|
||||
|
||||
|
@ -180,7 +188,17 @@ public class ScoreController : ControllerBase
|
|||
if (friendUsername != null) friendNames.Add(friendUsername);
|
||||
}
|
||||
|
||||
return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, "scores", friendNames.ToArray(), childId));
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scores",
|
||||
PageSize = pageSize,
|
||||
PageStart = pageStart,
|
||||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = type,
|
||||
TargetUsername = username,
|
||||
TargetPlayerIds = friendNames.ToArray(),
|
||||
}));
|
||||
}
|
||||
|
||||
[HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")]
|
||||
|
@ -188,7 +206,7 @@ public class ScoreController : ControllerBase
|
|||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
public async Task<IActionResult> TopScores(string slotType, int slotId, int? childId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5)
|
||||
{
|
||||
GameToken token = this.GetToken();
|
||||
GameTokenEntity token = this.GetToken();
|
||||
|
||||
if (pageSize <= 0) return this.BadRequest();
|
||||
|
||||
|
@ -198,79 +216,60 @@ public class ScoreController : ControllerBase
|
|||
|
||||
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
|
||||
|
||||
return this.Ok(this.getScores(slotId, type, username, pageStart, pageSize, childId: childId));
|
||||
return this.Ok(this.getScores(new LeaderboardOptions
|
||||
{
|
||||
RootName = "scores",
|
||||
PageSize = pageSize,
|
||||
PageStart = pageStart,
|
||||
SlotId = slotId,
|
||||
ChildSlotId = childId,
|
||||
ScoreType = type,
|
||||
TargetUsername = username,
|
||||
TargetPlayerIds = null,
|
||||
}));
|
||||
}
|
||||
|
||||
[SuppressMessage("ReSharper", "PossibleMultipleEnumeration")]
|
||||
private string getScores
|
||||
(
|
||||
int slotId,
|
||||
int type,
|
||||
string username,
|
||||
int pageStart = -1,
|
||||
int pageSize = 5,
|
||||
string rootName = "scores",
|
||||
string[]? playerIds = null,
|
||||
int? childId = 0
|
||||
)
|
||||
private class LeaderboardOptions
|
||||
{
|
||||
public int SlotId { get; set; }
|
||||
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? ChildSlotId;
|
||||
}
|
||||
|
||||
private 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 == slotId && s.Type == type)
|
||||
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == childId)
|
||||
var rankedScores = this.database.Scores.Where(s => s.SlotId == options.SlotId && s.Type == options.ScoreType)
|
||||
.Where(s => s.ChildSlotId == 0 || s.ChildSlotId == options.ChildSlotId)
|
||||
.AsEnumerable()
|
||||
.Where(s => playerIds == null || playerIds.Any(id => s.PlayerIdCollection.Split(",").Contains(id)))
|
||||
.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
|
||||
{
|
||||
Score = s,
|
||||
Rank = rank + 1,
|
||||
}
|
||||
);
|
||||
.Select((s, rank) => new
|
||||
{
|
||||
Score = s,
|
||||
Rank = rank + 1,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
|
||||
// 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(username)).MaxBy(rs => rs.Score.Points);
|
||||
var myScore = rankedScores.Where(rs => rs.Score.PlayerIdCollection.Split(",").Contains(options.TargetUsername)).MaxBy(rs => rs.Score.Points);
|
||||
|
||||
// Paginated viewing: if not requesting pageStart, get results around user
|
||||
var pagedScores = rankedScores.Skip(pageStart != -1 || myScore == null ? pageStart - 1 : myScore.Rank - 3).Take(Math.Min(pageSize, 30));
|
||||
var pagedScores = rankedScores.Skip(options.PageStart != -1 || myScore == null ? options.PageStart - 1 : myScore.Rank - 3).Take(Math.Min(options.PageSize, 30));
|
||||
|
||||
string serializedScores = pagedScores.Aggregate
|
||||
(
|
||||
string.Empty,
|
||||
(current, rs) =>
|
||||
{
|
||||
rs.Score.Rank = rs.Rank;
|
||||
return current + rs.Score.Serialize();
|
||||
}
|
||||
);
|
||||
List<GameScore> gameScores = pagedScores.Select(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank)).ToList();
|
||||
|
||||
string res;
|
||||
if (myScore == null) res = LbpSerializer.StringElement(rootName, serializedScores);
|
||||
else
|
||||
res = LbpSerializer.TaggedStringElement
|
||||
(
|
||||
rootName,
|
||||
serializedScores,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
"yourScore", myScore.Score.Points
|
||||
},
|
||||
{
|
||||
"yourRank", myScore.Rank
|
||||
}, //This is the numerator of your position globally in the side menu.
|
||||
{
|
||||
"totalNumScores", rankedScores.Count()
|
||||
}, // This is the denominator of your position globally in the side menu.
|
||||
}
|
||||
);
|
||||
|
||||
return res;
|
||||
return new ScoreboardResponse(options.RootName, gameScores, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0, rankedScores.Count);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue