mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-05-16 22:52:27 +00:00
* 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
275 lines
No EOL
11 KiB
C#
275 lines
No EOL
11 KiB
C#
#nullable enable
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using LBPUnion.ProjectLighthouse.Database;
|
|
using LBPUnion.ProjectLighthouse.Extensions;
|
|
using LBPUnion.ProjectLighthouse.Helpers;
|
|
using LBPUnion.ProjectLighthouse.Logging;
|
|
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;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots;
|
|
|
|
[ApiController]
|
|
[Authorize]
|
|
[Route("LITTLEBIGPLANETPS3_XML/")]
|
|
[Produces("text/xml")]
|
|
public class ScoreController : ControllerBase
|
|
{
|
|
private readonly DatabaseContext database;
|
|
|
|
public ScoreController(DatabaseContext database)
|
|
{
|
|
this.database = database;
|
|
}
|
|
|
|
[HttpPost("scoreboard/{slotType}/{id:int}")]
|
|
[HttpPost("scoreboard/{slotType}/{id:int}/{childId:int}")]
|
|
public async Task<IActionResult> SubmitScore(string slotType, int id, int childId)
|
|
{
|
|
GameTokenEntity token = this.GetToken();
|
|
|
|
string username = await this.database.UsernameFromGameToken(token);
|
|
|
|
if (SlotHelper.IsTypeInvalid(slotType))
|
|
{
|
|
Logger.Warn($"Rejecting score upload, slot type is invalid (slotType={slotType}, user={username})", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
Logger.Warn($"Rejecting score upload, there are 0 playerIds (slotType={slotType}, slotId={id}, user={username})", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
if (score.Points < 0)
|
|
{
|
|
Logger.Warn($"Rejecting score upload, points value is less than 0 (points={score.Points}, user={username})", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
// Score types:
|
|
// 1-4: Co-op with the number representing the number of players
|
|
// 5: leaderboard filtered by day (never uploaded with this id)
|
|
// 6: leaderboard filtered by week (never uploaded either)
|
|
// 7: Versus levels & leaderboard filtered by all time
|
|
if (score.Type is > 4 or < 1 && score.Type != 7)
|
|
{
|
|
Logger.Warn($"Rejecting score upload, score type is out of bounds (type={score.Type}, user={username})", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
if (!score.PlayerIds.Contains(username))
|
|
{
|
|
string bodyString = await this.ReadBodyAsync();
|
|
Logger.Warn("Rejecting score upload, requester username is not present in playerIds" +
|
|
$" (user={username}, playerIds={string.Join(",", score.PlayerIds)}, " +
|
|
$"gameVersion={token.GameVersion.ToPrettyString()}, type={score.Type}, id={id}, slotType={slotType}, body='{bodyString}')", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
SanitizationHelper.SanitizeStringsInClass(score);
|
|
|
|
int slotId = id;
|
|
|
|
if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer);
|
|
|
|
SlotEntity? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == id);
|
|
if (slot == null)
|
|
{
|
|
Logger.Warn($"Rejecting score upload, slot is null (slotId={slotId}, slotType={slotType}, reqId={id}, user={username})", LogArea.Score);
|
|
return this.BadRequest();
|
|
}
|
|
|
|
switch (token.GameVersion)
|
|
{
|
|
case GameVersion.LittleBigPlanet1:
|
|
slot.PlaysLBP1Complete++;
|
|
break;
|
|
case GameVersion.LittleBigPlanet2:
|
|
case GameVersion.LittleBigPlanetVita:
|
|
slot.PlaysLBP2Complete++;
|
|
break;
|
|
case GameVersion.LittleBigPlanet3:
|
|
slot.PlaysLBP3Complete++;
|
|
break;
|
|
case GameVersion.LittleBigPlanetPSP:
|
|
case GameVersion.Unknown:
|
|
default:
|
|
return this.BadRequest();
|
|
}
|
|
|
|
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.Type == score.Type)
|
|
.FirstOrDefaultAsync();
|
|
if (existingScore != null)
|
|
{
|
|
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();
|
|
|
|
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)
|
|
{
|
|
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);
|
|
|
|
UserFriendData? store = UserFriendStore.GetUserFriendData(token.UserId);
|
|
if (store == null) return this.Ok();
|
|
|
|
List<string> friendNames = new()
|
|
{
|
|
username,
|
|
};
|
|
|
|
foreach (int friendId in store.FriendIds)
|
|
{
|
|
string? friendUsername = await this.database.Users.Where(u => u.UserId == friendId)
|
|
.Select(u => u.Username)
|
|
.FirstOrDefaultAsync();
|
|
if (friendUsername != null) friendNames.Add(friendUsername);
|
|
}
|
|
|
|
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}")]
|
|
[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
|
|
{
|
|
RootName = "scores",
|
|
PageSize = pageSize,
|
|
PageStart = pageStart,
|
|
SlotId = slotId,
|
|
ChildSlotId = childId,
|
|
ScoreType = type,
|
|
TargetUsername = username,
|
|
TargetPlayerIds = null,
|
|
}));
|
|
}
|
|
|
|
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 == options.SlotId && 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
|
|
{
|
|
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(options.TargetUsername)).MaxBy(rs => rs.Score.Points);
|
|
|
|
// 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.Select(ps => GameScore.CreateFromEntity(ps.Score, ps.Rank)).ToList();
|
|
|
|
return new ScoreboardResponse(options.RootName, gameScores, myScore?.Score.Points ?? 0, myScore?.Rank ?? 0, rankedScores.Count);
|
|
}
|
|
} |