From fdf1988a34ef63baa30732270110e65e2064eaa9 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 1 Aug 2022 14:46:29 -0500 Subject: [PATCH] Implement online story features and photos taken in levels (#389) * Initial commit to support developer slots * Remove hearting story levels, prevent race condition in adding dev slots, and remove LastContactHelper local db object. * Fix photos taken in pod showing wrong level. * Add support for pod and create mode photos * Add time display to photos and added photo display to level page * Add pagination to in game photos * Update in pod description * Fix migration * Adjust wording of photos taken on local slots * Set slot default type to User Fixes old slots being set to developer slots * Apply suggestions * Add player count to developer slots Co-authored-by: Jayden --- .../Controllers/CommentController.cs | 40 +++++-- .../Controllers/Matching/MatchController.cs | 3 +- .../Controllers/Resources/PhotosController.cs | 58 ++++++++-- .../Controllers/Slots/ListController.cs | 2 +- .../Controllers/Slots/ScoreController.cs | 17 ++- .../Controllers/Slots/SlotsController.cs | 46 +++++++- .../Startup/GameServerStartup.cs | 2 +- .../Pages/LandingPage.cshtml.cs | 5 +- .../Pages/Partials/PhotoPartial.cshtml | 26 ++++- .../Pages/PhotosPage.cshtml.cs | 1 + .../Pages/SlotPage.cshtml | 17 ++- .../Pages/SlotPage.cshtml.cs | 12 +- .../Pages/SlotsPage.cshtml.cs | 2 + .../Pages/UserPage.cshtml.cs | 2 +- .../Extensions/DatabaseExtensions.cs | 3 +- ProjectLighthouse/Helpers/SlotHelper.cs | 106 ++++++++++++++++++ ProjectLighthouse/Helpers/StatisticsHelper.cs | 3 +- .../Levels/Categories/NewestLevelsCategory.cs | 4 +- ProjectLighthouse/Levels/Slot.cs | 26 ++++- ProjectLighthouse/Levels/SlotType.cs | 8 ++ .../20220729002704_DeveloperSlots.cs | 72 ++++++++++++ .../Migrations/DatabaseModelSnapshot.cs | 18 +++ .../PlayerData/LastContactHelper.cs | 3 +- ProjectLighthouse/PlayerData/Photo.cs | 34 +++++- ProjectLighthouse/PlayerData/PhotoSlot.cs | 20 ++++ 25 files changed, 483 insertions(+), 47 deletions(-) create mode 100644 ProjectLighthouse/Helpers/SlotHelper.cs create mode 100644 ProjectLighthouse/Migrations/20220729002704_DeveloperSlots.cs create mode 100644 ProjectLighthouse/PlayerData/PhotoSlot.cs diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index 46c239f5..b2c501ba 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -23,32 +23,40 @@ public class CommentController : ControllerBase } [HttpPost("rateUserComment/{username}")] - [HttpPost("rateComment/user/{slotId:int}")] - public async Task RateComment([FromQuery] int commentId, [FromQuery] int rating, string? username, int? slotId) + [HttpPost("rateComment/{slotType}/{slotId:int}")] + public async Task RateComment([FromQuery] int commentId, [FromQuery] int rating, string? username, string? slotType, int slotId) { User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); + if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest(); + bool success = await this.database.RateComment(user, commentId, rating); if (!success) return this.BadRequest(); return this.Ok(); } - [HttpGet("comments/user/{slotId:int}")] + [HttpGet("comments/{slotType}/{slotId:int}")] [HttpGet("userComments/{username}")] - public async Task GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, int? slotId) + public async Task GetComments([FromQuery] int pageStart, [FromQuery] int pageSize, string? username, string? slotType, int slotId) { User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); - int targetId = slotId.GetValueOrDefault(); + int targetId = slotId; CommentType type = CommentType.Level; if (!string.IsNullOrWhiteSpace(username)) { targetId = this.database.Users.First(u => u.Username.Equals(username)).UserId; type = CommentType.Profile; } + else + { + if (SlotHelper.IsTypeInvalid(slotType) || slotId == 0) return this.BadRequest(); + } + + if (type == CommentType.Level && slotType == "developer") targetId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); List comments = await this.database.Comments.Include (c => c.Poster) @@ -72,8 +80,8 @@ public class CommentController : ControllerBase } [HttpPost("postUserComment/{username}")] - [HttpPost("postComment/user/{slotId:int}")] - public async Task PostComment(string? username, int? slotId) + [HttpPost("postComment/{slotType}/{slotId:int}")] + public async Task PostComment(string? username, string? slotType, int slotId) { User? poster = await this.database.UserFromGameRequest(this.Request); if (poster == null) return this.StatusCode(403, ""); @@ -86,14 +94,18 @@ public class CommentController : ControllerBase SanitizationHelper.SanitizeStringsInClass(comment); - CommentType type = (slotId.GetValueOrDefault() == 0 ? CommentType.Profile : CommentType.Level); + CommentType type = (slotId == 0 ? CommentType.Profile : CommentType.Level); + + if (type == CommentType.Level && (SlotHelper.IsTypeInvalid(slotType) || slotId == 0)) return this.BadRequest(); if (comment == null) return this.BadRequest(); - int targetId = slotId.GetValueOrDefault(); + int targetId = slotId; if (type == CommentType.Profile) targetId = this.database.Users.First(u => u.Username == username).UserId; + if (slotType == "developer") targetId = await SlotHelper.GetPlaceholderSlotId(this.database, targetId, SlotType.Developer); + bool success = await this.database.PostComment(poster, targetId, type, comment.Message); if (success) return this.Ok(); @@ -101,8 +113,8 @@ public class CommentController : ControllerBase } [HttpPost("deleteUserComment/{username}")] - [HttpPost("deleteComment/user/{slotId:int}")] - public async Task DeleteComment([FromQuery] int commentId, string? username, int? slotId) + [HttpPost("deleteComment/{slotType}/{slotId:int}")] + public async Task DeleteComment([FromQuery] int commentId, string? username, string? slotType, int slotId) { User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); @@ -110,6 +122,10 @@ public class CommentController : ControllerBase Comment? comment = await this.database.Comments.FirstOrDefaultAsync(c => c.CommentId == commentId); if (comment == null) return this.NotFound(); + if (comment.Type == CommentType.Level && (SlotHelper.IsTypeInvalid(slotType) || slotId == 0)) return this.BadRequest(); + + if (slotType == "developer") slotId = await SlotHelper.GetPlaceholderSlotId(this.database, slotId, SlotType.Developer); + // if you are not the poster if (comment.PosterUserId != user.UserId) { @@ -125,7 +141,7 @@ public class CommentController : ControllerBase { Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.SlotId == comment.TargetId); // if you aren't the creator of the level - if (slot == null || slot.CreatorId != user.UserId || slotId.GetValueOrDefault() != slot.SlotId) + if (slot == null || slot.CreatorId != user.UserId || slotId != slot.SlotId) { return this.StatusCode(403, ""); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs index 23a195a5..c6c8097a 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Matching/MatchController.cs @@ -8,7 +8,6 @@ using LBPUnion.ProjectLighthouse.Match.MatchCommands; using LBPUnion.ProjectLighthouse.Match.Rooms; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -71,7 +70,7 @@ public class MatchController : ControllerBase #endregion - await LastContactHelper.SetLastContact(user, gameToken.GameVersion, gameToken.Platform); + await LastContactHelper.SetLastContact(this.database, user, gameToken.GameVersion, gameToken.Platform); #region Process match data diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs index f9461c8d..7452c77c 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Resources/PhotosController.cs @@ -3,11 +3,11 @@ using System.Xml.Serialization; using Discord; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Serialization; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -53,6 +53,38 @@ public class PhotosController : ControllerBase photo.CreatorId = user.UserId; photo.Creator = user; + if (photo.XmlLevelInfo != null) + { + bool validLevel = false; + PhotoSlot photoSlot = photo.XmlLevelInfo; + if (photoSlot.SlotType is SlotType.Pod or SlotType.Local) photoSlot.SlotId = 0; + switch (photoSlot.SlotType) + { + case SlotType.User: + { + Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == SlotType.User && s.SlotId == photoSlot.SlotId); + if (slot != null) validLevel = slot.RootLevel == photoSlot.RootLevel; + break; + } + case SlotType.Pod: + case SlotType.Local: + case SlotType.Developer: + { + Slot? slot = await this.database.Slots.FirstOrDefaultAsync(s => s.Type == photoSlot.SlotType && s.InternalSlotId == photoSlot.SlotId); + if (slot != null) + photoSlot.SlotId = slot.SlotId; + else + photoSlot.SlotId = await SlotHelper.GetPlaceholderSlotId(this.database, photoSlot.SlotId, photoSlot.SlotType); + validLevel = true; + break; + } + default: Logger.Warn($"Invalid photo level type: {photoSlot.SlotType}", LogArea.Photos); + break; + } + + if (validLevel) photo.SlotId = photo.XmlLevelInfo.SlotId; + } + if (photo.Subjects.Count > 4) return this.BadRequest(); if (photo.Timestamp > TimeHelper.Timestamp) photo.Timestamp = TimeHelper.Timestamp; @@ -104,11 +136,23 @@ public class PhotosController : ControllerBase return this.Ok(); } - [HttpGet("photos/user/{id:int}")] - public async Task SlotPhotos(int id) + [HttpGet("photos/{slotType}/{id:int}")] + public async Task SlotPhotos([FromQuery] int pageStart, [FromQuery] int pageSize, string slotType, int id) { - List photos = await this.database.Photos.Include(p => p.Creator).Take(10).ToListAsync(); - string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(id)); + User? user = await this.database.UserFromGameRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest(); + + if (slotType == "developer") id = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); + + List photos = await this.database.Photos.Include(p => p.Creator) + .Where(p => p.SlotId == id) + .OrderByDescending(s => s.Timestamp) + .Skip(pageStart - 1) + .Take(Math.Min(pageSize, 30)) + .ToListAsync(); + string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(id, SlotHelper.ParseType(slotType))); return this.Ok(LbpSerializer.StringElement("photos", response)); } @@ -126,7 +170,7 @@ public class PhotosController : ControllerBase .Skip(pageStart - 1) .Take(Math.Min(pageSize, 30)) .ToListAsync(); - string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize(0)); + string response = photos.Aggregate(string.Empty, (s, photo) => s + photo.Serialize()); return this.Ok(LbpSerializer.StringElement("photos", response)); } @@ -145,7 +189,7 @@ public class PhotosController : ControllerBase (s => s.Timestamp) .Skip(pageStart - 1) .Take(Math.Min(pageSize, 30)) - .Aggregate(string.Empty, (s, photo) => s + photo.Serialize(0)); + .Aggregate(string.Empty, (s, photo) => s + photo.Serialize()); return this.Ok(LbpSerializer.StringElement("photos", response)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs index 0a680a9e..d7980bee 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ListController.cs @@ -1,10 +1,10 @@ #nullable enable +using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Serialization; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs index cc61df46..34696cf3 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/ScoreController.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics.CodeAnalysis; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; @@ -23,13 +24,15 @@ public class ScoreController : ControllerBase this.database = database; } - [HttpPost("scoreboard/user/{id:int}")] - public async Task SubmitScore(int id, [FromQuery] bool lbp1 = false, [FromQuery] bool lbp2 = false, [FromQuery] bool lbp3 = false) + [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) { (User, GameToken)? userAndToken = await this.database.UserAndGameTokenFromRequest(this.Request); if (userAndToken == null) return this.StatusCode(403, ""); + if (SlotHelper.IsTypeInvalid(slotType)) return this.BadRequest(); + // ReSharper disable once PossibleInvalidOperationException User user = userAndToken.Value.Item1; GameToken gameToken = userAndToken.Value.Item2; @@ -43,6 +46,8 @@ public class ScoreController : ControllerBase 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); @@ -92,15 +97,19 @@ public class ScoreController : ControllerBase //=> await TopScores(slotId, type); => this.Ok(LbpSerializer.BlankElement("scores")); - [HttpGet("topscores/user/{slotId:int}/{type:int}")] + [HttpGet("topscores/{slotType}/{slotId:int}/{type:int}")] [SuppressMessage("ReSharper", "PossibleMultipleEnumeration")] - public async Task TopScores(int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) + public async Task TopScores(string slotType, int slotId, int type, [FromQuery] int pageStart = -1, [FromQuery] int pageSize = 5) { // Get username User? user = await this.database.UserFromGameRequest(this.Request); if (user == null) return this.StatusCode(403, ""); + 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, user, pageStart, pageSize)); } diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs index 4cbad3f8..54e105e4 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/Slots/SlotsController.cs @@ -8,7 +8,6 @@ using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.PlayerData.Reviews; using LBPUnion.ProjectLighthouse.Serialization; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -65,6 +64,47 @@ public class SlotsController : ControllerBase ); } + [HttpGet("slotList")] + public async Task GetSlotListAlt([FromQuery] int[] s) + { + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + List serializedSlots = new(); + foreach (int slotId in s) + { + Slot? slot = await this.database.Slots.Include(t => t.Creator).Include(t => t.Location).Where(t => t.SlotId == slotId && t.Type == SlotType.User).FirstOrDefaultAsync(); + if (slot == null) + { + slot = await this.database.Slots.Where(t => t.InternalSlotId == slotId && t.Type == SlotType.Developer).FirstOrDefaultAsync(); + if (slot == null) + { + serializedSlots.Add($"{slotId}"); + continue; + } + } + serializedSlots.Add(slot.Serialize()); + } + string serialized = serializedSlots.Aggregate(string.Empty, (current, slot) => slot == null ? current : current + slot); + + return this.Ok(LbpSerializer.TaggedStringElement("slots", serialized, "total", serializedSlots.Count)); + } + + [HttpGet("s/developer/{id:int}")] + public async Task SDev(int id) + { + User? user = await this.database.UserFromGameRequest(this.Request); + if (user == null) return this.StatusCode(403, ""); + + GameToken? token = await this.database.GameTokenFromRequest(this.Request); + if (token == null) return this.StatusCode(403, ""); + + int slotId = await SlotHelper.GetPlaceholderSlotId(this.database, id, SlotType.Developer); + Slot slot = await this.database.Slots.FirstAsync(s => s.SlotId == slotId); + + return this.Ok(slot.SerializeDevSlot()); + } + [HttpGet("s/user/{id:int}")] public async Task SUser(int id) { @@ -458,10 +498,10 @@ public class SlotsController : ControllerBase if (gameFilterType == "both") // Get game versions less than the current version // Needs support for LBP3 ("both" = LBP1+2) - whereSlots = this.database.Slots.Where(s => s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime); + whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && s.GameVersion <= gameVersion && s.FirstUploaded >= oldestTime); else // Get game versions exactly equal to gamefiltertype - whereSlots = this.database.Slots.Where(s => s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime); + whereSlots = this.database.Slots.Where(s => s.Type == SlotType.User && s.GameVersion == gameVersion && s.FirstUploaded >= oldestTime); return whereSlots.Include(s => s.Creator).Include(s => s.Location); } diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index 2c316e72..495bf66f 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -164,7 +164,7 @@ public class GameServerStartup if (gameToken != null && gameToken.GameVersion == GameVersion.LittleBigPlanet1) // Ignore UserFromGameToken null because user must exist for a token to exist await LastContactHelper.SetLastContact - ((await database.UserFromGameToken(gameToken))!, GameVersion.LittleBigPlanet1, gameToken.Platform); + (database, (await database.UserFromGameToken(gameToken))!, GameVersion.LittleBigPlanet1, gameToken.Platform); } #nullable disable diff --git a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs index fecee91b..bd7c1707 100644 --- a/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/LandingPage.cshtml.cs @@ -43,13 +43,14 @@ public class LandingPage : BaseLayout const int maxShownLevels = 5; this.LatestTeamPicks = await this.Database.Slots.Where - (s => s.TeamPick) + (s => s.Type == SlotType.User) + .Where(s => s.TeamPick) .OrderByDescending(s => s.FirstUploaded) .Take(maxShownLevels) .Include(s => s.Creator) .ToListAsync(); - this.NewestLevels = await this.Database.Slots.OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync(); + this.NewestLevels = await this.Database.Slots.Where(s => s.Type == SlotType.User).OrderByDescending(s => s.FirstUploaded).Take(maxShownLevels).Include(s => s.Creator).ToListAsync(); return this.Page(); } diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml index 66c8b0fa..da1a46b2 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/PhotoPartial.cshtml @@ -1,5 +1,6 @@ +@using System.Globalization +@using LBPUnion.ProjectLighthouse.Levels @using LBPUnion.ProjectLighthouse.PlayerData -@using LBPUnion.ProjectLighthouse.Types @model LBPUnion.ProjectLighthouse.PlayerData.Photo @@ -18,6 +19,27 @@ @Model.Creator?.Username + @if (Model.Slot != null) + { + switch (Model.Slot.Type) + { + case SlotType.User: + + in level @Model.Slot.Name + + break; + case SlotType.Developer: + in a story mode level + break; + case SlotType.Pod: + in the pod + break; + case SlotType.Local: + in a level on the moon + break; + } + } + at @DateTime.UnixEpoch.AddSeconds(Model.Timestamp).ToString(CultureInfo.CurrentCulture)

@@ -124,4 +146,4 @@ context.setTransform(1, 0, 0, 1, 0, 0); }) }, false); - \ No newline at end of file + diff --git a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs index b636205e..f2ce8fb0 100644 --- a/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/PhotosPage.cshtml.cs @@ -40,6 +40,7 @@ public class PhotosPage : BaseLayout this.Photos = await this.Database.Photos.Include (p => p.Creator) + .Include(p => p.Slot) .Where(p => p.Creator!.Username.Contains(this.SearchValue) || p.PhotoSubjectCollection.Contains(this.SearchValue)) .OrderByDescending(p => p.Timestamp) .Skip(pageNumber * ServerStatics.PageSize) diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index 55313d80..e25f8144 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -3,6 +3,7 @@ @using LBPUnion.ProjectLighthouse.Administration @using LBPUnion.ProjectLighthouse.Configuration @using LBPUnion.ProjectLighthouse.Extensions +@using LBPUnion.ProjectLighthouse.PlayerData @using LBPUnion.ProjectLighthouse.PlayerData.Reviews @model LBPUnion.ProjectLighthouse.Servers.Website.Pages.SlotPage @@ -162,7 +163,21 @@ - + @if (Model.Photos.Count != 0) + { +
+

Most recent photos

+ +
+ @foreach (Photo photo in Model.Photos) + { +
+ @await Html.PartialAsync("Partials/PhotoPartial", photo) +
+ } +
+
+ } @if (Model.User != null && Model.User.IsAdmin) {
diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs index 47d8f265..08397d10 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml.cs @@ -15,6 +15,7 @@ public class SlotPage : BaseLayout { public List Comments = new(); public List Reviews = new(); + public List Photos = new(); public readonly bool CommentsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelCommentsEnabled; public readonly bool ReviewsEnabled = ServerConfiguration.Instance.UserGeneratedContentLimits.LevelReviewsEnabled; @@ -25,7 +26,10 @@ public class SlotPage : BaseLayout public async Task OnGet([FromRoute] int id) { - Slot? slot = await this.Database.Slots.Include(s => s.Creator).FirstOrDefaultAsync(s => s.SlotId == id); + Slot? slot = await this.Database.Slots.Include + (s => s.Creator) + .Where(s => s.Type == SlotType.User) + .FirstOrDefaultAsync(s => s.SlotId == id); if (slot == null) return this.NotFound(); this.Slot = slot; @@ -57,6 +61,12 @@ public class SlotPage : BaseLayout this.Reviews = new List(); } + this.Photos = await this.Database.Photos.Include(p => p.Creator) + .OrderByDescending(p => p.Timestamp) + .Where(r => r.SlotId == id) + .Take(10) + .ToListAsync(); + if (this.User == null) return this.Page(); foreach (Comment c in this.Comments) diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs index 2bc11efa..a7089eb5 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/SlotsPage.cshtml.cs @@ -55,6 +55,7 @@ public class SlotsPage : BaseLayout this.SearchValue = name.Trim(); this.SlotCount = await this.Database.Slots.Include(p => p.Creator) + .Where(p => p.Type == SlotType.User) .Where(p => p.Name.Contains(finalSearch.ToString())) .Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower()))) .Where(p => targetGame == null || p.GameVersion == targetGame) @@ -66,6 +67,7 @@ public class SlotsPage : BaseLayout if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount) return this.Redirect($"/slots/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}"); this.Slots = await this.Database.Slots.Include(p => p.Creator) + .Where(p => p.Type == SlotType.User) .Where(p => p.Name.Contains(finalSearch.ToString())) .Where(p => p.Creator != null && (targetAuthor == null || string.Equals(p.Creator.Username.ToLower(), targetAuthor.ToLower()))) .Where(p => targetGame == null || p.GameVersion == targetGame) diff --git a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs index 067ee0a2..6a1dd155 100644 --- a/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/UserPage.cshtml.cs @@ -28,7 +28,7 @@ public class UserPage : BaseLayout this.ProfileUser = await this.Database.Users.FirstOrDefaultAsync(u => u.UserId == userId); if (this.ProfileUser == null) return this.NotFound(); - this.Photos = await this.Database.Photos.OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync(); + this.Photos = await this.Database.Photos.Include(p => p.Slot).OrderByDescending(p => p.Timestamp).Where(p => p.CreatorId == userId).Take(6).ToListAsync(); if (this.CommentsEnabled) { this.Comments = await this.Database.Comments.Include(p => p.Poster) diff --git a/ProjectLighthouse/Extensions/DatabaseExtensions.cs b/ProjectLighthouse/Extensions/DatabaseExtensions.cs index 67748eff..b1335ca1 100644 --- a/ProjectLighthouse/Extensions/DatabaseExtensions.cs +++ b/ProjectLighthouse/Extensions/DatabaseExtensions.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.PlayerData.Reviews; -using LBPUnion.ProjectLighthouse.Types; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Extensions; @@ -19,6 +18,8 @@ public static class DatabaseExtensions public static IQueryable ByGameVersion (this IQueryable query, GameVersion gameVersion, bool includeSublevels = false, bool includeCreatorAndLocation = false) { + query = query.Where(s => s.Type == SlotType.User); + if (includeCreatorAndLocation) { query = query.Include(s => s.Creator).Include(s => s.Location); diff --git a/ProjectLighthouse/Helpers/SlotHelper.cs b/ProjectLighthouse/Helpers/SlotHelper.cs new file mode 100644 index 00000000..f2b8abb3 --- /dev/null +++ b/ProjectLighthouse/Helpers/SlotHelper.cs @@ -0,0 +1,106 @@ +#nullable enable +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Levels; +using LBPUnion.ProjectLighthouse.PlayerData.Profiles; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Helpers; + +public static class SlotHelper +{ + + public static SlotType ParseType(string? slotType) + { + if (slotType == null) return SlotType.Unknown; + return slotType switch + { + "developer" => SlotType.Developer, + "user" => SlotType.User, + "moon" => SlotType.Moon, + "pod" => SlotType.Pod, + "local" => SlotType.Local, + _ => SlotType.Unknown, + }; + + } + + public static bool IsTypeInvalid(string? slotType) + { + if (slotType == null) return true; + return slotType switch + { + "developer" => false, + "user" => false, + _ => true, + }; + } + + private static readonly SemaphoreSlim semaphore = new(1, 1); + + public static async Task GetPlaceholderSlotId(Database database, int guid, SlotType slotType) + { + int slotId = await database.Slots.Where(s => s.Type == slotType && s.InternalSlotId == guid).Select(s => s.SlotId).FirstOrDefaultAsync(); + if (slotId != 0) return slotId; + + await semaphore.WaitAsync(TimeSpan.FromSeconds(5)); + try + { + // if two requests come in at the same time for the same story level which hasn't been generated + // one will wait for the lock to be released and the second will be caught by this second check + slotId = await database.Slots + .Where(s => s.Type == slotType && s.InternalSlotId == guid) + .Select(s => s.SlotId) + .FirstOrDefaultAsync(); + + if (slotId != 0) return slotId; + + Location? devLocation = await database.Locations.FirstOrDefaultAsync(l => l.Id == 1); + if (devLocation == null) + { + devLocation = new Location + { + Id = 1, + }; + database.Locations.Add(devLocation); + } + + int devCreatorId = await database.Users.Where(u => u.Username.Length == 0).Select(u => u.UserId).FirstOrDefaultAsync(); + if (devCreatorId == 0) + { + User devCreator = new() + { + Username = "", + Banned = true, + Biography = "Placeholder author of story levels", + BannedReason = "Banned to not show in users list", + LocationId = devLocation.Id, + }; + database.Users.Add(devCreator); + await database.SaveChangesAsync(); + devCreatorId = devCreator.UserId; + } + + Slot slot = new() + { + Name = $"{slotType} slot {guid}", + Description = $"Placeholder for {slotType} type level", + CreatorId = devCreatorId, + InternalSlotId = guid, + LocationId = devLocation.Id, + Type = slotType, + }; + + database.Slots.Add(slot); + await database.SaveChangesAsync(); + return slot.SlotId; + } + finally + { + semaphore.Release(); + } + } + +} \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/StatisticsHelper.cs b/ProjectLighthouse/Helpers/StatisticsHelper.cs index f5531d49..8410f535 100644 --- a/ProjectLighthouse/Helpers/StatisticsHelper.cs +++ b/ProjectLighthouse/Helpers/StatisticsHelper.cs @@ -1,5 +1,6 @@ using System.Linq; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData; using LBPUnion.ProjectLighthouse.Types; using Microsoft.EntityFrameworkCore; @@ -16,7 +17,7 @@ public static class StatisticsHelper (GameVersion gameVersion) => await database.LastContacts.Where(l => TimeHelper.Timestamp - l.Timestamp < 300 && l.GameVersion == gameVersion).CountAsync(); - public static async Task SlotCount() => await database.Slots.CountAsync(); + public static async Task SlotCount() => await database.Slots.Where(s => s.Type == SlotType.User).CountAsync(); public static async Task UserCount() => await database.Users.CountAsync(u => !u.Banned); diff --git a/ProjectLighthouse/Levels/Categories/NewestLevelsCategory.cs b/ProjectLighthouse/Levels/Categories/NewestLevelsCategory.cs index a5d41b16..9ba8a23d 100644 --- a/ProjectLighthouse/Levels/Categories/NewestLevelsCategory.cs +++ b/ProjectLighthouse/Levels/Categories/NewestLevelsCategory.cs @@ -13,12 +13,12 @@ public class NewestLevelsCategory : Category public override string Description { get; set; } = "Levels recently published"; public override string IconHash { get; set; } = "g820623"; public override string Endpoint { get; set; } = "newest"; - public override Slot? GetPreviewSlot(Database database) => database.Slots.OrderByDescending(s => s.FirstUploaded).FirstOrDefault(); + public override Slot? GetPreviewSlot(Database database) => database.Slots.Where(s => s.Type == SlotType.User).OrderByDescending(s => s.FirstUploaded).FirstOrDefault(); public override IEnumerable GetSlots (Database database, int pageStart, int pageSize) => database.Slots.ByGameVersion(GameVersion.LittleBigPlanet3, false, true) .OrderByDescending(s => s.FirstUploaded) .Skip(pageStart - 1) .Take(Math.Min(pageSize, 20)); - public override int GetTotalSlots(Database database) => database.Slots.Count(); + public override int GetTotalSlots(Database database) => database.Slots.Count(s => s.Type == SlotType.User); } \ No newline at end of file diff --git a/ProjectLighthouse/Levels/Slot.cs b/ProjectLighthouse/Levels/Slot.cs index 098d7760..c9491858 100644 --- a/ProjectLighthouse/Levels/Slot.cs +++ b/ProjectLighthouse/Levels/Slot.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; @@ -40,14 +41,15 @@ public class Slot } [XmlAttribute("type")] - [NotMapped] [JsonIgnore] - public string Type { get; set; } = "user"; + public SlotType Type { get; set; } = SlotType.User; [Key] [XmlElement("id")] public int SlotId { get; set; } + public int InternalSlotId { get; set; } + [XmlElement("name")] public string Name { get; set; } = ""; @@ -240,6 +242,24 @@ public class Slot LbpSerializer.StringElement("sizeOfResources", this.Resources.Sum(FileHelper.ResourceSize)); } + public string SerializeDevSlot() + { + int comments = this.database.Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId); + + int photos = this.database.Photos.Count(c => c.SlotId == this.SlotId); + + int players = RoomHelper.Rooms + .Where(r => r.Slot.SlotType == SlotType.Developer && r.Slot.SlotId == this.InternalSlotId) + .Sum(r => r.PlayerIds.Count); + + string slotData = LbpSerializer.StringElement("id", this.InternalSlotId) + + LbpSerializer.StringElement("playerCount", players) + + LbpSerializer.StringElement("commentCount", comments) + + LbpSerializer.StringElement("photoCount", photos); + + return LbpSerializer.TaggedStringElement("slot", slotData, "type", "developer"); + } + public string Serialize ( GameVersion gameVersion = GameVersion.LittleBigPlanet1, @@ -248,6 +268,8 @@ public class Slot Review? yourReview = null ) { + if (this.Type == SlotType.Developer) return this.SerializeDevSlot(); + int playerCount = RoomHelper.Rooms.Count(r => r.Slot.SlotType == SlotType.User && r.Slot.SlotId == this.SlotId); string slotData = LbpSerializer.StringElement("name", this.Name) + diff --git a/ProjectLighthouse/Levels/SlotType.cs b/ProjectLighthouse/Levels/SlotType.cs index a34876c9..90e7ea76 100644 --- a/ProjectLighthouse/Levels/SlotType.cs +++ b/ProjectLighthouse/Levels/SlotType.cs @@ -1,12 +1,20 @@ +using System.Xml.Serialization; + namespace LBPUnion.ProjectLighthouse.Levels; public enum SlotType { + [XmlEnum("developer")] Developer = 0, + [XmlEnum("user")] User = 1, + [XmlEnum("moon")] Moon = 2, Unknown = 3, Unknown2 = 4, + [XmlEnum("pod")] Pod = 5, + [XmlEnum("local")] + Local = 6, DLC = 8, } \ No newline at end of file diff --git a/ProjectLighthouse/Migrations/20220729002704_DeveloperSlots.cs b/ProjectLighthouse/Migrations/20220729002704_DeveloperSlots.cs new file mode 100644 index 00000000..d05747f6 --- /dev/null +++ b/ProjectLighthouse/Migrations/20220729002704_DeveloperSlots.cs @@ -0,0 +1,72 @@ +using LBPUnion.ProjectLighthouse; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20220729002704_DeveloperSlots")] + public partial class DeveloperSlots : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "InternalSlotId", + table: "Slots", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddColumn( + name: "Type", + table: "Slots", + type: "int", + defaultValue: 1, + nullable: false); + + migrationBuilder.AddColumn( + name: "SlotId", + table: "Photos", + type: "int", + nullable: true); + + migrationBuilder.CreateIndex( + name: "IX_Photos_SlotId", + table: "Photos", + column: "SlotId"); + + migrationBuilder.AddForeignKey( + name: "FK_Photos_Slots_SlotId", + table: "Photos", + column: "SlotId", + principalTable: "Slots", + principalColumn: "SlotId", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Photos_Slots_SlotId", + table: "Photos"); + + migrationBuilder.DropIndex( + name: "IX_Photos_SlotId", + table: "Photos"); + + migrationBuilder.DropColumn( + name: "InternalSlotId", + table: "Slots"); + + migrationBuilder.DropColumn( + name: "Type", + table: "Slots"); + + migrationBuilder.DropColumn( + name: "SlotId", + table: "Photos"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs index 9a2d1729..494435fd 100644 --- a/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseModelSnapshot.cs @@ -210,6 +210,9 @@ namespace ProjectLighthouse.Migrations b.Property("InitiallyLocked") .HasColumnType("tinyint(1)"); + b.Property("InternalSlotId") + .HasColumnType("int"); + b.Property("LastUpdated") .HasColumnType("bigint"); @@ -289,6 +292,10 @@ namespace ProjectLighthouse.Migrations b.Property("TeamPick") .HasColumnType("tinyint(1)"); + b.Property("Type") + .IsRequired() + .HasColumnType("int"); + b.HasKey("SlotId"); b.HasIndex("CreatorId"); @@ -461,6 +468,9 @@ namespace ProjectLighthouse.Migrations .IsRequired() .HasColumnType("longtext"); + b.Property("SlotId") + .HasColumnType("int"); + b.Property("SmallHash") .IsRequired() .HasColumnType("longtext"); @@ -472,6 +482,8 @@ namespace ProjectLighthouse.Migrations b.HasIndex("CreatorId"); + b.HasIndex("SlotId"); + b.ToTable("Photos"); }); @@ -1006,7 +1018,13 @@ namespace ProjectLighthouse.Migrations .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.HasOne("LBPUnion.ProjectLighthouse.Levels.Slot", "Slot") + .WithMany() + .HasForeignKey("SlotId"); + b.Navigation("Creator"); + + b.Navigation("Slot"); }); modelBuilder.Entity("LBPUnion.ProjectLighthouse.PlayerData.PhotoSubject", b => diff --git a/ProjectLighthouse/PlayerData/LastContactHelper.cs b/ProjectLighthouse/PlayerData/LastContactHelper.cs index 28c9ae6c..78a31531 100644 --- a/ProjectLighthouse/PlayerData/LastContactHelper.cs +++ b/ProjectLighthouse/PlayerData/LastContactHelper.cs @@ -9,9 +9,8 @@ namespace LBPUnion.ProjectLighthouse.PlayerData; public static class LastContactHelper { - private static readonly Database database = new(); - public static async Task SetLastContact(User user, GameVersion gameVersion, Platform platform) + public static async Task SetLastContact(Database database, User user, GameVersion gameVersion, Platform platform) { LastContact? lastContact = await database.LastContacts.Where(l => l.UserId == user.UserId).FirstOrDefaultAsync(); diff --git a/ProjectLighthouse/PlayerData/Photo.cs b/ProjectLighthouse/PlayerData/Photo.cs index 8a8ca224..302d5366 100644 --- a/ProjectLighthouse/PlayerData/Photo.cs +++ b/ProjectLighthouse/PlayerData/Photo.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Levels; using LBPUnion.ProjectLighthouse.PlayerData.Profiles; using LBPUnion.ProjectLighthouse.Serialization; using Microsoft.EntityFrameworkCore; @@ -19,6 +20,10 @@ public class Photo [NotMapped] private List? _subjects; + [NotMapped] + [XmlElement("slot")] + public PhotoSlot? XmlLevelInfo; + [NotMapped] [XmlArray("subjects")] [XmlArrayItem("subject")] @@ -81,9 +86,34 @@ public class Photo [ForeignKey(nameof(CreatorId))] public User? Creator { get; set; } - public string Serialize(int slotId) + public int? SlotId { get; set; } + + [ForeignKey(nameof(SlotId))] + public Slot? Slot { get; set; } + + public string Serialize() { - string slot = LbpSerializer.TaggedStringElement("slot", LbpSerializer.StringElement("id", slotId), "type", "user"); + using Database database = new(); + var partialSlot = database.Slots.Where(s => s.SlotId == this.SlotId.GetValueOrDefault()) + .Select(s => new + { + s.InternalSlotId, + s.Type, + }) + .FirstOrDefault(); + if (partialSlot == null) return this.Serialize(0, SlotType.User); + + int serializedSlotId = partialSlot.InternalSlotId; + if (serializedSlotId == 0) serializedSlotId = this.SlotId.GetValueOrDefault(); + + return this.Serialize(serializedSlotId, partialSlot.Type); + } + + public string Serialize(int slotId, SlotType slotType) + { + + string slot = LbpSerializer.TaggedStringElement("slot", LbpSerializer.StringElement("id", slotId), "type", slotType.ToString().ToLower()); + if (slotId == 0) slot = ""; string subjectsAggregate = this.Subjects.Aggregate(string.Empty, (s, subject) => s + subject.Serialize()); diff --git a/ProjectLighthouse/PlayerData/PhotoSlot.cs b/ProjectLighthouse/PlayerData/PhotoSlot.cs new file mode 100644 index 00000000..2726a932 --- /dev/null +++ b/ProjectLighthouse/PlayerData/PhotoSlot.cs @@ -0,0 +1,20 @@ +using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Levels; + +namespace LBPUnion.ProjectLighthouse.PlayerData; + +[XmlRoot("slot")] +public class PhotoSlot +{ + [XmlAttribute("type")] + public SlotType SlotType { get; set; } + + [XmlElement("id")] + public int SlotId { get; set; } + + [XmlElement("rootLevel")] + public string RootLevel { get; set; } + + [XmlElement("name")] + public string LevelName { get; set; } +} \ No newline at end of file