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