#nullable enable using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Serialization; using Microsoft.AspNetCore.Mvc; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers.Slots; [ApiController] [Route("LITTLEBIGPLANETPS3_XML/")] [Produces("text/xml")] public class ScoreController : ControllerBase { private readonly Database database; public ScoreController(Database database) { this.database = database; } [HttpPost("scoreboard/{slotType}/{id:int}")] public async Task SubmitScore(string slotType, int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false) { GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); string username = await this.database.UsernameFromGameToken(token); if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest(); this.Request.Body.Position = 0; string bodyString = await new StreamReader(this.Request.Body).ReadToEndAsync(); XmlSerializer serializer = new(typeof(Score)); Score? score = (Score?)serializer.Deserialize(new StringReader(bodyString)); if (score == null) return this.BadRequest(); if (score.PlayerIds.Length == 0) return this.BadRequest(); if (score.Points < 0) return this.BadRequest(); if (score.Type is > 4 or < 1) return this.BadRequest(); SanitizationHelper.SanitizeStringsInClass(score); if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); score.SlotId = id; Slot? slot = this.database.Slots.FirstOrDefault(s => s.SlotId == score.SlotId); if (slot == null) 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: break; case GameVersion.Unknown: break; default: throw new ArgumentOutOfRangeException(); } // Submit scores from all players in lobby foreach (string player in score.PlayerIds) { List players = new(); players.Add(player); // make sure this player is first players.AddRange(score.PlayerIds.Where(p => p != player)); Score playerScore = new() { PlayerIdCollection = string.Join(',', players), Type = score.Type, Points = score.Points, SlotId = score.SlotId, }; IQueryable existingScore = this.database.Scores.Where(s => s.SlotId == playerScore.SlotId) .Where(s => s.PlayerIdCollection == playerScore.PlayerIdCollection) .Where(s => s.Type == playerScore.Type); if (existingScore.Any()) { 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); } else { this.database.Scores.Add(playerScore); } } await this.database.SaveChangesAsync(); string myRanking = this.getScores(score.SlotId, score.Type, username, -1, 5, "scoreboardSegment"); return this.Ok(myRanking); } [HttpGet("friendscores/user/{slotId:int}/{type:int}")] public IActionResult FriendScores(int slotId, int type) //=> await TopScores(slotId, type); => this.Ok(LbpSerializer.BlankElement("scores")); [HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] public async Task TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) { GameToken? token = await this.database.GameTokenFromRequest(this.Request); if (token == null) return this.StatusCode(403, ""); 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(slotId, type, username, pageStart, pageSize)); } [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] private string getScores ( int slotId, int type, string username, int pageStart = -1, int pageSize = 5, string rootName = "scores" ) { // 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) .OrderByDescending(s => s.Points) .ThenBy(s => s.ScoreId) .ToList() .Select ( (s, rank) => new { Score = s, Rank = rank + 1, } ); // 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.Contains(username)).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)); string serializedScores = pagedScores.Aggregate ( string.Empty, (current, rs) => { rs.Score.Rank = rs.Rank; return current + rs.Score.Serialize(); } ); string res; if (myScore == null) res = LbpSerializer.StringElement(rootName, serializedScores); else res = LbpSerializer.TaggedStringElement ( rootName, serializedScores, new Dictionary { { "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; } }