diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs index 7301bc52c..f10a407e5 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/UserController.cs @@ -40,7 +40,15 @@ public async Task GetUser(string username) UserEntity? user = await this.database.Users.FirstOrDefaultAsync(u => u.Username == username); if (user == null) return this.NotFound(); - return this.Ok(GameUser.CreateFromEntity(user, this.GetToken().GameVersion)); + GameVersion gameVersion = this.GetToken().GameVersion; + + UserProfilePinsEntity? profilePins = await this.database.UserProfilePins.FirstOrDefaultAsync(p => p.UserId == user.UserId && p.GameVersion == gameVersion); + + GameUser profile = GameUser.CreateFromEntity(user, gameVersion); + + profile.ProfilePins = profilePins?.Pins ?? user.Pins; + + return this.Ok(profile); } [HttpGet("users")] @@ -173,24 +181,103 @@ public async Task UpdateUser() [Produces("text/json")] public async Task UpdateMyPins() { - UserEntity? user = await this.database.UserFromGameToken(this.GetToken()); + GameTokenEntity token = this.GetToken(); + + UserEntity? user = await this.database.UserFromGameToken(token); if (user == null) return this.Forbid(); + if (token.GameVersion is not (GameVersion.LittleBigPlanet2 or GameVersion.LittleBigPlanet3 or GameVersion.LittleBigPlanetVita)) + { + return this.BadRequest(); + } + string bodyString = await this.ReadBodyAsync(); - Pins? pinJson = JsonSerializer.Deserialize(bodyString); - if (pinJson?.ProfilePins == null) return this.BadRequest(); + Pins? pinJson; + + try + { + pinJson = JsonSerializer.Deserialize(bodyString); + } + catch (JsonException) + { + return this.BadRequest(); + } + + if (pinJson == null) + return this.BadRequest(); + + if (!PinUploadParser.TryParse(pinJson, out PinUploadParser.ParsedPinUpload parsed)) + return this.BadRequest(); + + PinSet pinSet = token.GameVersion.ToPinSet(); + + Dictionary storedProgress = await this.database.UserPinProgress + .Where(p => p.UserId == user.UserId && p.PinSet == pinSet) + .ToDictionaryAsync(p => p.ProgressType); - // Sometimes the update gets called periodically as pin progress updates via playing, - // may not affect equipped profile pins however, so check before setting it. - string currentPins = user.Pins; - string newPins = string.Join(",", pinJson.ProfilePins.Distinct()); + foreach (KeyValuePair uploaded in parsed.Progress) + { + if (storedProgress.TryGetValue(uploaded.Key, out UserPinProgressEntity? stored)) + { + stored.Value = PinProgressRules.Merge(uploaded.Key, stored.Value, uploaded.Value); + + continue; + } + + UserPinProgressEntity entity = new() + { + UserId = user.UserId, + PinSet = pinSet, + ProgressType = uploaded.Key, + Value = uploaded.Value, + }; + + this.database.UserPinProgress.Add(entity); + storedProgress.Add(uploaded.Key, entity); + } + + if (parsed.ProfilePins != null) + { + string newPins = string.Join(",", parsed.ProfilePins); - if (string.Equals(currentPins, newPins)) return this.Ok("[{\"StatusCode\":200}]"); + UserProfilePinsEntity? profilePins = await this.database.UserProfilePins + .FirstOrDefaultAsync(p => p.UserId == user.UserId && p.GameVersion == token.GameVersion); + + if (profilePins == null) + { + profilePins = new UserProfilePinsEntity + { + UserId = user.UserId, + GameVersion = token.GameVersion, + Pins = newPins, + }; + + this.database.UserProfilePins.Add(profilePins); + } + else if (!string.Equals(profilePins.Pins, newPins)) + { + profilePins.Pins = newPins; + } + } - user.Pins = newPins; await this.database.SaveChangesAsync(); - return this.Ok("[{\"StatusCode\":200}]"); + List responsePins = []; + + foreach (UserPinProgressEntity progress in storedProgress.Values.OrderBy(p => p.ProgressType)) + { + responsePins.Add(progress.ProgressType); + responsePins.Add(progress.Value); + } + + return new JsonResult(new + { + progress = responsePins, + awards = responsePins, + }) + { + ContentType = "text/json", + }; } -} \ No newline at end of file +} diff --git a/ProjectLighthouse.Servers.GameServer/Types/Users/PinUploadParser.cs b/ProjectLighthouse.Servers.GameServer/Types/Users/PinUploadParser.cs new file mode 100644 index 000000000..6d4a0a927 --- /dev/null +++ b/ProjectLighthouse.Servers.GameServer/Types/Users/PinUploadParser.cs @@ -0,0 +1,131 @@ +#nullable enable +using System.Text.Json; + +namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users; + +public static class PinUploadParser +{ + public sealed class ParsedPinUpload + { + public Dictionary Progress { get; } = []; + + // Null means profile_pins was omitted + public List? ProfilePins { get; internal set; } + } + + public static bool TryParse(Pins upload, out ParsedPinUpload result) + { + result = new ParsedPinUpload(); + + if (!TryParseProgress(upload.Progress, result)) + return false; + + if (!TryParseAwards(upload.Awards, result)) + return false; + + if (!TryParseProfilePins(upload.ProfilePins, result)) + return false; + + return true; + } + + private static bool TryParseProgress(JsonElement[]? values, ParsedPinUpload result) + { + if (values == null) + return true; + + if ((values.Length & 1) != 0) + return false; + + for (int i = 0; i < values.Length; i += 2) + { + if (values[i].ValueKind != JsonValueKind.Number || !values[i].TryGetUInt32(out uint progressType)) + { + return false; + } + + if (values[i + 1].ValueKind != JsonValueKind.Number || !values[i + 1].TryGetDouble(out double value)) + { + return false; + } + + if (!double.IsFinite(value)) + return false; + + if (!result.Progress.TryAdd(progressType, value)) + return false; + } + + return true; + } + + private static bool TryParseAwards(JsonElement[]? values, ParsedPinUpload result) + { + if (values == null) + return true; + + if ((values.Length & 1) != 0) + return false; + + HashSet seenProgressTypes = []; + + for (int i = 0; i < values.Length; i += 2) + { + if (values[i].ValueKind != JsonValueKind.Number || !values[i].TryGetUInt32(out uint progressType)) + { + return false; + } + + if (values[i + 1].ValueKind != JsonValueKind.Number || !values[i + 1].TryGetInt64(out long count)) + { + return false; + } + + if (count < 0) + return false; + + if (!seenProgressTypes.Add(progressType)) + return false; + + if (result.Progress.TryGetValue(progressType, out double existingValue)) + { + result.Progress[progressType] = Math.Max(existingValue, count); + } + else + { + result.Progress.Add(progressType, count); + } + } + + return true; + } + + private static bool TryParseProfilePins(JsonElement[]? values, ParsedPinUpload result) + { + if (values == null) + return true; + + if (values.Length > 3) + return false; + + List profilePins = new(values.Length); + HashSet seenPinIds = []; + + foreach (JsonElement value in values) + { + if (value.ValueKind != JsonValueKind.Number || !value.TryGetUInt32(out uint pinId)) + { + return false; + } + + if (!seenPinIds.Add(pinId)) + return false; + + profilePins.Add(pinId); + } + + result.ProfilePins = profilePins; + + return true; + } +} diff --git a/ProjectLighthouse.Servers.GameServer/Types/Users/Pins.cs b/ProjectLighthouse.Servers.GameServer/Types/Users/Pins.cs index acc52f659..0514103a9 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Users/Pins.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Users/Pins.cs @@ -1,3 +1,5 @@ +#nullable enable +using System.Text.Json; using System.Text.Json.Serialization; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users; @@ -5,11 +7,11 @@ namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Types.Users; public class Pins { [JsonPropertyName("progress")] - public long[]? Progress { get; set; } + public JsonElement[]? Progress { get; set; } [JsonPropertyName("awards")] - public long[]? Awards { get; set; } + public JsonElement[]? Awards { get; set; } [JsonPropertyName("profile_pins")] - public long[]? ProfilePins { get; set; } -} \ No newline at end of file + public JsonElement[]? ProfilePins { get; set; } +} diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs index 3347db1e6..9de82da13 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -1,11 +1,14 @@ using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Mvc; using Xunit; @@ -14,6 +17,27 @@ namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; [Trait("Category", "Unit")] public class UserControllerTests { + private static GameTokenEntity GetLbp2Token() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet2; + return token; + } + + private static GameTokenEntity GetLbp3Token() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanet3; + return token; + } + + private static GameTokenEntity GetVitaToken() + { + GameTokenEntity token = MockHelper.GetUnitTestToken(); + token.GameVersion = GameVersion.LittleBigPlanetVita; + return token; + } + [Fact] public async Task GetUser_WithValidUser_ShouldReturnUser() { @@ -127,8 +151,7 @@ public async Task UpdateMyPins_ShouldReturnBadRequest_WhenBodyIsInvalid() await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); UserController userController = new(dbMock); - userController.SetupTestController("{}"); - + userController.SetupTestController(GetLbp2Token(), "{"); IActionResult result = await userController.UpdateMyPins(); @@ -141,20 +164,23 @@ public async Task UpdateMyPins_ShouldUpdatePins() await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); UserController userController = new(dbMock); - userController.SetupTestController("{\"profile_pins\": [1234]}"); - - const string expectedPins = "1234"; - const string expectedResponse = "[{\"StatusCode\":200}]"; + userController.SetupTestController(GetLbp2Token(), "{\"profile_pins\": [1234]}"); IActionResult result = await userController.UpdateMyPins(); - string pinsResponse = result.CastTo(); - Assert.Equal(expectedPins, dbMock.Users.First().Pins); - Assert.Equal(expectedResponse, pinsResponse); + JsonResult jsonResult = Assert.IsType(result); + + UserProfilePinsEntity profilePins = dbMock.UserProfilePins.Single(); + + Assert.Equal(1, profilePins.UserId); + Assert.Equal(GameVersion.LittleBigPlanet2, profilePins.GameVersion); + Assert.Equal("1234", profilePins.Pins); + + Assert.Equal("text/json", jsonResult.ContentType); } [Fact] - public async Task UpdateMyPins_ShouldNotSave_WhenPinsAreEqual() + public async Task UpdateMyPins_ShouldLeaveLegacyPinsUnchanged() { UserEntity entity = MockHelper.GetUnitTestUser(); entity.Pins = "1234"; @@ -165,21 +191,22 @@ public async Task UpdateMyPins_ShouldNotSave_WhenPinsAreEqual() await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); UserController userController = new(dbMock); - userController.SetupTestController("{\"profile_pins\": [1234]}"); - - const string expectedPins = "1234"; - const string expectedResponse = "[{\"StatusCode\":200}]"; + userController.SetupTestController(GetLbp2Token(), "{\"profile_pins\": [5678]}"); IActionResult result = await userController.UpdateMyPins(); - string pinsResponse = result.CastTo(); + Assert.IsType(result); + + UserProfilePinsEntity profilePins = dbMock.UserProfilePins.Single(); - Assert.Equal(expectedPins, dbMock.Users.First().Pins); - Assert.Equal(expectedResponse, pinsResponse); + Assert.Equal(GameVersion.LittleBigPlanet2, profilePins.GameVersion); + Assert.Equal("5678", profilePins.Pins); + + Assert.Equal("1234", dbMock.Users.First().Pins); } [Fact] - public async Task UpdateMyPins_ShouldRemove_DuplicatePins() + public async Task UpdateMyPins_ShouldRejectDuplicateProfilePins() { UserEntity entity = MockHelper.GetUnitTestUser(); entity.Pins = "1234"; @@ -190,16 +217,493 @@ public async Task UpdateMyPins_ShouldRemove_DuplicatePins() await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); UserController userController = new(dbMock); - userController.SetupTestController("{\"profile_pins\": [1234, 1234]}"); + userController.SetupTestController(GetLbp2Token(), "{\"profile_pins\": [1234, 1234]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + Assert.Empty(dbMock.UserProfilePins); + + Assert.Equal("1234", dbMock.Users.First().Pins); + } + + [Fact] + public async Task UpdateMyPins_ShouldNotDowngradeStoredProgress() + { + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, + PinSet = PinSet.LittleBigPlanet, + ProgressType = 1234, + Value = 10, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(1234u, storedProgress.ProgressType); + Assert.Equal(10, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldAcceptLowerValue_WhenLowerProgressIsBetter() + { + const uint progressType = 191183438u; + + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, + PinSet = PinSet.LittleBigPlanet, + ProgressType = progressType, + Value = 10, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [191183438, 5]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(progressType, storedProgress.ProgressType); + Assert.Equal(5, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldNotReplaceBetterLowerProgressWithWorseValue() + { + const uint progressType = 191183438u; + + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, + PinSet = PinSet.LittleBigPlanet, + ProgressType = progressType, + Value = 5, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [191183438, 10]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(progressType, storedProgress.ProgressType); + Assert.Equal(5, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldReturnStoredProgress_WhenClientUploadsWorseValue() + { + const uint progressType = 1234u; + + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, + PinSet = PinSet.LittleBigPlanet, + ProgressType = progressType, + Value = 10, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5]}"); + + IActionResult result = await userController.UpdateMyPins(); + + JsonResult jsonResult = Assert.IsType(result); + + string json = JsonSerializer.Serialize(jsonResult.Value); + + using JsonDocument document = JsonDocument.Parse(json); + + JsonElement responseProgress = document.RootElement.GetProperty("progress"); + + JsonElement responseAwards = document.RootElement.GetProperty("awards"); + + Assert.Equal(2, responseProgress.GetArrayLength()); + Assert.Equal(progressType, responseProgress[0].GetUInt32()); + Assert.Equal(10, responseProgress[1].GetDouble()); + + Assert.Equal(2, responseAwards.GetArrayLength()); + Assert.Equal(progressType, responseAwards[0].GetUInt32()); + Assert.Equal(10, responseAwards[1].GetDouble()); + } + + [Fact] + public async Task UpdateMyPins_ShouldUseHigherValue_WhenProgressAndAwardsOverlap() + { + const uint progressType = 1234u; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5], \"awards\": [1234, 9]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(progressType, storedProgress.ProgressType); + Assert.Equal(9, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldPersistFractionalProgress() + { + const uint progressType = 1234u; + const double expectedValue = 5.5; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5.5]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(progressType, storedProgress.ProgressType); + Assert.Equal(expectedValue, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldReturnBadRequest_WhenProgressArrayHasOddLength() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5, 5678]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + Assert.Empty(dbMock.UserPinProgress); + } + + [Theory] + [InlineData("{\"awards\": [1234]}")] + [InlineData("{\"progress\": [1234, 1, 1234, 2]}")] + [InlineData("{\"awards\": [1234, 1, 1234, 2]}")] + [InlineData("{\"progress\": [-1, 5]}")] + [InlineData("{\"progress\": [4294967296, 5]}")] + [InlineData("{\"progress\": [1234, \"5\"]}")] + [InlineData("{\"awards\": [1234, -1]}")] + [InlineData("{\"awards\": [1234, 1.5]}")] + public async Task UpdateMyPins_ShouldReturnBadRequest_WhenPinDataIsMalformed(string body) + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), body); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + Assert.Empty(dbMock.UserPinProgress); + Assert.Empty(dbMock.UserProfilePins); + } + + [Fact] + public async Task UpdateMyPins_ShouldReturnBadRequest_WhenMoreThanThreeProfilePinsAreUploaded() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"profile_pins\": [1, 2, 3, 4]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + Assert.Empty(dbMock.UserProfilePins); + } + + [Fact] + public async Task UpdateMyPins_ShouldKeepProfilePins_WhenProfilePinsAreOmitted() + { + List profilePins = new() + { + new UserProfilePinsEntity + { + UserId = 1, GameVersion = GameVersion.LittleBigPlanet2, Pins = "111,222,333", + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(profilePins); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 5]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserProfilePinsEntity storedPins = dbMock.UserProfilePins.Single(); + + Assert.Equal("111,222,333", storedPins.Pins); + } + + [Fact] + public async Task UpdateMyPins_ShouldClearProfilePins_WhenEmptyArrayIsUploaded() + { + List profilePins = new() + { + new UserProfilePinsEntity + { + UserId = 1, GameVersion = GameVersion.LittleBigPlanet2, Pins = "111,222,333", + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(profilePins); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"profile_pins\": []}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + UserProfilePinsEntity storedPins = dbMock.UserProfilePins.Single(); + + Assert.Equal(string.Empty, storedPins.Pins); + } + + [Fact] + public async Task UpdateMyPins_ShouldPreserveProgressMissingFromSparseUpload() + { + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, PinSet = PinSet.LittleBigPlanet, ProgressType = 1234, Value = 10, + }, + new UserPinProgressEntity + { + UserId = 1, PinSet = PinSet.LittleBigPlanet, ProgressType = 5678, Value = 20, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 15]}"); + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + + List storedProgress = dbMock.UserPinProgress + .OrderBy(p => p.ProgressType) + .ToList(); + + Assert.Equal(2, storedProgress.Count); + + Assert.Equal(1234u, storedProgress[0].ProgressType); + Assert.Equal(15, storedProgress[0].Value); + + Assert.Equal(5678u, storedProgress[1].ProgressType); + Assert.Equal(20, storedProgress[1].Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldShareProgressBetweenLbp2AndLbp3() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController lbp2Controller = new(dbMock); + lbp2Controller.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 10]}"); + + IActionResult lbp2Result = await lbp2Controller.UpdateMyPins(); + + Assert.IsType(lbp2Result); + + UserController lbp3Controller = new(dbMock); + lbp3Controller.SetupTestController(GetLbp3Token(), "{\"progress\": [1234, 5]}"); + + IActionResult lbp3Result = await lbp3Controller.UpdateMyPins(); + + Assert.IsType(lbp3Result); + + List storedProgress = dbMock.UserPinProgress.ToList(); + + Assert.Single(storedProgress); + Assert.Equal(PinSet.LittleBigPlanet, storedProgress[0].PinSet); + Assert.Equal(1234u, storedProgress[0].ProgressType); + Assert.Equal(10, storedProgress[0].Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldKeepVitaProgressSeparateFromLbp2AndLbp3() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController lbp2Controller = new(dbMock); + lbp2Controller.SetupTestController(GetLbp2Token(), "{\"progress\": [1234, 10]}"); + + IActionResult lbp2Result = await lbp2Controller.UpdateMyPins(); + + Assert.IsType(lbp2Result); + + UserController vitaController = new(dbMock); + vitaController.SetupTestController(GetVitaToken(), "{\"progress\": [1234, 3]}"); + + IActionResult vitaResult = await vitaController.UpdateMyPins(); + + Assert.IsType(vitaResult); + + List storedProgress = dbMock.UserPinProgress.ToList(); + + Assert.Equal(2, storedProgress.Count); + + UserPinProgressEntity lbpProgress = storedProgress.Single(p => p.PinSet == PinSet.LittleBigPlanet); + + UserPinProgressEntity vitaProgress = storedProgress.Single(p => p.PinSet == PinSet.Vita); + + Assert.Equal(10, lbpProgress.Value); + Assert.Equal(3, vitaProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldUseLowerValue_ForCommunityLowerIsBetterProgressType() + { + const uint progressType = 2033315234u; - const string expectedPins = "1234"; - const string expectedResponse = "[{\"StatusCode\":200}]"; + List progress = new() + { + new UserPinProgressEntity + { + UserId = 1, PinSet = PinSet.LittleBigPlanet, ProgressType = progressType, Value = 50, + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(progress); + + UserController userController = new(dbMock); + userController.SetupTestController( + GetLbp2Token(), + "{\"progress\": [2033315234, 25]}"); IActionResult result = await userController.UpdateMyPins(); - string pinsResponse = result.CastTo(); + Assert.IsType(result); + + UserPinProgressEntity storedProgress = dbMock.UserPinProgress.Single(); + + Assert.Equal(progressType, storedProgress.ProgressType); + Assert.Equal(25, storedProgress.Value); + } + + [Fact] + public async Task UpdateMyPins_ShouldStoreProfilePinsSeparatelyForLbp2AndLbp3() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController lbp2Controller = new(dbMock); + lbp2Controller.SetupTestController(GetLbp2Token(), "{\"profile_pins\": [111]}"); + + IActionResult lbp2Result = await lbp2Controller.UpdateMyPins(); + + Assert.IsType(lbp2Result); + + UserController lbp3Controller = new(dbMock); + lbp3Controller.SetupTestController(GetLbp3Token(), "{\"profile_pins\": [222]}"); + + IActionResult lbp3Result = await lbp3Controller.UpdateMyPins(); + + Assert.IsType(lbp3Result); + + List profilePins = dbMock.UserProfilePins.ToList(); + + Assert.Equal(2, profilePins.Count); + + UserProfilePinsEntity lbp2Pins = profilePins.Single(p => p.GameVersion == GameVersion.LittleBigPlanet2); + + UserProfilePinsEntity lbp3Pins = profilePins.Single(p => p.GameVersion == GameVersion.LittleBigPlanet3); + + Assert.Equal("111", lbp2Pins.Pins); + Assert.Equal("222", lbp3Pins.Pins); + } + + [Fact] + public async Task GetUser_ShouldReturnGameSpecificProfilePins() + { + UserEntity user = MockHelper.GetUnitTestUser(); + user.Pins = "111"; + + List users = [user]; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); + + dbMock.UserProfilePins.Add(new UserProfilePinsEntity + { + UserId = user.UserId, + GameVersion = GameVersion.LittleBigPlanet2, + Pins = "222,333", + }); + + await dbMock.SaveChangesAsync(); + + UserController controller = new(dbMock); + controller.SetupTestController(GetLbp2Token()); + + IActionResult result = await controller.GetUser(user.Username); + + GameUser gameUser = result.CastTo(); + + Assert.Equal("222,333", gameUser.ProfilePins); + } + + [Fact] + public async Task GetUser_ShouldFallBackToLegacyProfilePins() + { + UserEntity user = MockHelper.GetUnitTestUser(); + user.Pins = "111,222,333"; + + List users = [user]; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); + + UserController controller = new(dbMock); + controller.SetupTestController(GetLbp2Token()); + + IActionResult result = await controller.GetUser(user.Username); + + GameUser gameUser = result.CastTo(); - Assert.Equal(expectedPins, dbMock.Users.First().Pins); - Assert.Equal(expectedResponse, pinsResponse); + Assert.Equal("111,222,333", gameUser.ProfilePins); } -} \ No newline at end of file +} diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 0a2e09e15..5f2480faa 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -33,6 +33,8 @@ public partial class DatabaseContext : DbContext public DbSet PhotoSubjects { get; set; } public DbSet PlatformLinkAttempts { get; set; } public DbSet Users { get; set; } + public DbSet UserPinProgress { get; set; } + public DbSet UserProfilePins { get; set; } #endregion #region Levels @@ -88,4 +90,4 @@ public static DatabaseContext CreateNewInstance() MySqlServerVersion.LatestSupportedServerVersion); return new DatabaseContext(builder.Options); } -} \ No newline at end of file +} diff --git a/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.Designer.cs b/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.Designer.cs new file mode 100644 index 000000000..f787eea38 --- /dev/null +++ b/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.Designer.cs @@ -0,0 +1,1630 @@ +// +using System; +using LBPUnion.ProjectLighthouse.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20260830001802_AddUserPinState")] + partial class AddUserPinState + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.18") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b => + { + b.Property("HeartedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedLevelId")); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedPlaylistEntity", b => + { + b.Property("HeartedPlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedPlaylistId")); + + b.Property("PlaylistId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedPlaylistId"); + + b.HasIndex("PlaylistId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedPlaylists"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedProfileEntity", b => + { + b.Property("HeartedProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("HeartedProfileId")); + + b.Property("HeartedUserId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("HeartedProfileId"); + + b.HasIndex("HeartedUserId"); + + b.HasIndex("UserId"); + + b.ToTable("HeartedProfiles"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.QueuedLevelEntity", b => + { + b.Property("QueuedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("QueuedLevelId")); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("QueuedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("QueuedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedCommentEntity", b => + { + b.Property("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatingId")); + + b.Property("CommentId") + .HasColumnType("int"); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatingId"); + + b.HasIndex("CommentId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedComments"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedLevelEntity", b => + { + b.Property("RatedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedLevelId")); + + b.Property("Rating") + .HasColumnType("int"); + + b.Property("RatingLBP1") + .HasColumnType("double"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("TagLBP1") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedReviewEntity", b => + { + b.Property("RatedReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("RatedReviewId")); + + b.Property("ReviewId") + .HasColumnType("int"); + + b.Property("Thumb") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("RatedReviewId"); + + b.HasIndex("ReviewId"); + + b.HasIndex("UserId"); + + b.ToTable("RatedReviews"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.VisitedLevelEntity", b => + { + b.Property("VisitedLevelId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("VisitedLevelId")); + + b.Property("PlaysLBP1") + .HasColumnType("int"); + + b.Property("PlaysLBP2") + .HasColumnType("int"); + + b.Property("PlaysLBP3") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("VisitedLevelId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("VisitedLevels"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.DatabaseCategoryEntity", b => + { + b.Property("CategoryId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CategoryId")); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Endpoint") + .HasColumnType("longtext"); + + b.Property("IconHash") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("SlotIdsCollection") + .HasColumnType("longtext"); + + b.HasKey("CategoryId"); + + b.ToTable("CustomCategories"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", b => + { + b.Property("PlaylistId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlaylistId")); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SlotCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("PlaylistId"); + + b.HasIndex("CreatorId"); + + b.ToTable("Playlists"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.ReviewEntity", b => + { + b.Property("ReviewId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReviewId")); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("DeletedBy") + .HasColumnType("int"); + + b.Property("LabelCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReviewerId") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("Text") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Thumb") + .HasColumnType("int"); + + b.Property("ThumbsDown") + .HasColumnType("int"); + + b.Property("ThumbsUp") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("ReviewId"); + + b.HasIndex("ReviewerId"); + + b.HasIndex("SlotId"); + + b.ToTable("Reviews"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.ScoreEntity", b => + { + b.Property("ScoreId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ScoreId")); + + b.Property("ChildSlotId") + .HasColumnType("int"); + + b.Property("Points") + .HasColumnType("int"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("ScoreId"); + + b.HasIndex("SlotId"); + + b.HasIndex("UserId"); + + b.ToTable("Scores"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", b => + { + b.Property("SlotId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("SlotId")); + + b.Property("AuthorLabels") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("BackgroundHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CommentsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("CrossControllerRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Description") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("FirstUploaded") + .HasColumnType("bigint"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("Hidden") + .HasColumnType("tinyint(1)"); + + b.Property("HiddenReason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("IconHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("InitiallyLocked") + .HasColumnType("tinyint(1)"); + + b.Property("InternalSlotId") + .HasColumnType("int"); + + b.Property("IsAdventurePlanet") + .HasColumnType("tinyint(1)"); + + b.Property("LastUpdated") + .HasColumnType("bigint"); + + b.Property("Lbp1Only") + .HasColumnType("tinyint(1)"); + + b.Property("LevelType") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("LocationPacked") + .HasColumnType("bigint unsigned"); + + b.Property("LockedByModerator") + .HasColumnType("tinyint(1)"); + + b.Property("LockedReason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MaximumPlayers") + .HasColumnType("int"); + + b.Property("MinimumPlayers") + .HasColumnType("int"); + + b.Property("MoveRequired") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlaysLBP1") + .HasColumnType("int"); + + b.Property("PlaysLBP1Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP1Unique") + .HasColumnType("int"); + + b.Property("PlaysLBP2") + .HasColumnType("int"); + + b.Property("PlaysLBP2Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP2Unique") + .HasColumnType("int"); + + b.Property("PlaysLBP3") + .HasColumnType("int"); + + b.Property("PlaysLBP3Complete") + .HasColumnType("int"); + + b.Property("PlaysLBP3Unique") + .HasColumnType("int"); + + b.Property("ResourceCollection") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("RootLevel") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Shareable") + .HasColumnType("int"); + + b.Property("SubLevel") + .HasColumnType("tinyint(1)"); + + b.Property("TeamPickTime") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("SlotId"); + + b.HasIndex("CreatorId"); + + b.ToTable("Slots"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Maintenance.CompletedMigrationEntity", b => + { + b.Property("MigrationName") + .HasColumnType("varchar(255)"); + + b.Property("RanAt") + .HasColumnType("datetime(6)"); + + b.HasKey("MigrationName"); + + b.ToTable("CompletedMigrations"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Moderation.GriefReportEntity", b => + { + b.Property("ReportId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("ReportId")); + + b.Property("Bounds") + .HasColumnType("longtext"); + + b.Property("GriefStateHash") + .HasColumnType("longtext"); + + b.Property("InitialStateHash") + .HasColumnType("longtext"); + + b.Property("JpegHash") + .HasColumnType("longtext"); + + b.Property("LevelId") + .HasColumnType("int"); + + b.Property("LevelOwner") + .HasColumnType("longtext"); + + b.Property("LevelType") + .HasColumnType("longtext"); + + b.Property("Players") + .HasColumnType("longtext"); + + b.Property("ReportingPlayerId") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("ReportId"); + + b.HasIndex("ReportingPlayerId"); + + b.ToTable("Reports"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Moderation.ModerationCaseEntity", b => + { + b.Property("CaseId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CaseId")); + + b.Property("AffectedId") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime(6)"); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("CreatorUsername") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DismissedAt") + .HasColumnType("datetime(6)"); + + b.Property("DismisserId") + .HasColumnType("int"); + + b.Property("DismisserUsername") + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("ModeratorNotes") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Processed") + .HasColumnType("tinyint(1)"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("CaseId"); + + b.HasIndex("CreatorId"); + + b.HasIndex("DismisserId"); + + b.ToTable("Cases"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Notifications.NotificationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("IsDismissed") + .HasColumnType("tinyint(1)"); + + b.Property("Text") + .HasColumnType("longtext"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.BlockedProfileEntity", b => + { + b.Property("BlockedProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("BlockedProfileId")); + + b.Property("BlockedUserId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("BlockedProfileId"); + + b.HasIndex("BlockedUserId"); + + b.HasIndex("UserId"); + + b.ToTable("BlockedProfiles"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", b => + { + b.Property("CommentId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("CommentId")); + + b.Property("Deleted") + .HasColumnType("tinyint(1)"); + + b.Property("DeletedBy") + .HasColumnType("longtext"); + + b.Property("DeletedType") + .HasColumnType("longtext"); + + b.Property("Message") + .HasColumnType("longtext"); + + b.Property("PosterUserId") + .HasColumnType("int"); + + b.Property("TargetSlotId") + .HasColumnType("int"); + + b.Property("TargetUserId") + .HasColumnType("int"); + + b.Property("ThumbsDown") + .HasColumnType("int"); + + b.Property("ThumbsUp") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("CommentId"); + + b.HasIndex("PosterUserId"); + + b.HasIndex("TargetSlotId"); + + b.HasIndex("TargetUserId"); + + b.ToTable("Comments"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.LastContactEntity", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("UserId"); + + b.ToTable("LastContacts"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b => + { + b.Property("PhotoId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoId")); + + b.Property("CreatorId") + .HasColumnType("int"); + + b.Property("LargeHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("MediumHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("PlanHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("SlotId") + .HasColumnType("int"); + + b.Property("SmallHash") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.HasKey("PhotoId"); + + b.HasIndex("CreatorId"); + + b.HasIndex("SlotId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoSubjectEntity", b => + { + b.Property("PhotoSubjectId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PhotoSubjectId")); + + b.Property("Bounds") + .HasColumnType("longtext"); + + b.Property("PhotoId") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("PhotoSubjectId"); + + b.HasIndex("PhotoId"); + + b.HasIndex("UserId"); + + b.ToTable("PhotoSubjects"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PlatformLinkAttemptEntity", b => + { + b.Property("PlatformLinkAttemptId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("PlatformLinkAttemptId")); + + b.Property("IPAddress") + .HasColumnType("longtext"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("PlatformId") + .HasColumnType("bigint unsigned"); + + b.Property("Timestamp") + .HasColumnType("bigint"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("PlatformLinkAttemptId"); + + b.HasIndex("UserId"); + + b.ToTable("PlatformLinkAttempts"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("UserId")); + + b.Property("AdminGrantedSlots") + .HasColumnType("int"); + + b.Property("BannedReason") + .HasColumnType("longtext"); + + b.Property("Biography") + .HasColumnType("longtext"); + + b.Property("BooHash") + .HasColumnType("longtext"); + + b.Property("CommentsEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("EmailAddress") + .HasColumnType("longtext"); + + b.Property("EmailAddressVerified") + .HasColumnType("tinyint(1)"); + + b.Property("IconHash") + .HasColumnType("longtext"); + + b.Property("Language") + .HasColumnType("longtext"); + + b.Property("LastLogin") + .HasColumnType("bigint"); + + b.Property("LastLogout") + .HasColumnType("bigint"); + + b.Property("LevelVisibility") + .HasColumnType("int"); + + b.Property("LinkedPsnId") + .HasColumnType("bigint unsigned"); + + b.Property("LinkedRpcnId") + .HasColumnType("bigint unsigned"); + + b.Property("LocationPacked") + .HasColumnType("bigint unsigned"); + + b.Property("MehHash") + .HasColumnType("longtext"); + + b.Property("Password") + .HasColumnType("longtext"); + + b.Property("PasswordResetRequired") + .HasColumnType("tinyint(1)"); + + b.Property("PermissionLevel") + .HasColumnType("int"); + + b.Property("Pins") + .HasColumnType("longtext"); + + b.Property("PlanetHashLBP2") + .HasColumnType("longtext"); + + b.Property("PlanetHashLBP2CC") + .HasColumnType("longtext"); + + b.Property("PlanetHashLBP3") + .HasColumnType("longtext"); + + b.Property("PlanetHashLBPVita") + .HasColumnType("longtext"); + + b.Property("ProfileTag") + .HasColumnType("longtext"); + + b.Property("ProfileVisibility") + .HasColumnType("int"); + + b.Property("TimeZone") + .HasColumnType("longtext"); + + b.Property("TwoFactorBackup") + .HasColumnType("longtext"); + + b.Property("TwoFactorSecret") + .HasColumnType("longtext"); + + b.Property("Username") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("YayHash") + .HasColumnType("longtext"); + + b.HasKey("UserId"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserPinProgressEntity", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("PinSet") + .HasColumnType("int"); + + b.Property("ProgressType") + .HasColumnType("int unsigned"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("UserId", "PinSet", "ProgressType"); + + b.ToTable("UserPinProgress"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserProfilePinsEntity", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("Pins") + .HasColumnType("longtext"); + + b.HasKey("UserId", "GameVersion"); + + b.ToTable("UserProfilePins"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.ApiKeyEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.ToTable("APIKeys"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.EmailSetTokenEntity", b => + { + b.Property("EmailSetTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailSetTokenId")); + + b.Property("EmailToken") + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("EmailSetTokenId"); + + b.HasIndex("UserId"); + + b.ToTable("EmailSetTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.EmailVerificationTokenEntity", b => + { + b.Property("EmailVerificationTokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("EmailVerificationTokenId")); + + b.Property("EmailToken") + .HasColumnType("longtext"); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("EmailVerificationTokenId"); + + b.HasIndex("UserId"); + + b.ToTable("EmailVerificationTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.GameTokenEntity", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("LocationHash") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("PatchworkJoinKeyEnabled") + .HasColumnType("tinyint(1)"); + + b.Property("PatchworkMajor") + .HasColumnType("int"); + + b.Property("PatchworkMinor") + .HasColumnType("int"); + + b.Property("Platform") + .HasColumnType("int"); + + b.Property("TicketHash") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("UserToken") + .HasColumnType("longtext"); + + b.HasKey("TokenId"); + + b.HasIndex("UserId"); + + b.ToTable("GameTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.PasswordResetTokenEntity", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("ResetToken") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("int"); + + b.HasKey("TokenId"); + + b.ToTable("PasswordResetTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.RegistrationTokenEntity", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("Token") + .HasColumnType("longtext"); + + b.Property("Username") + .HasColumnType("longtext"); + + b.HasKey("TokenId"); + + b.ToTable("RegistrationTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.WebTokenEntity", b => + { + b.Property("TokenId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("TokenId")); + + b.Property("ExpiresAt") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("int"); + + b.Property("UserToken") + .HasColumnType("longtext"); + + b.Property("Verified") + .HasColumnType("tinyint(1)"); + + b.HasKey("TokenId"); + + b.ToTable("WebTokens"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", b => + { + b.Property("AnnouncementId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("AnnouncementId")); + + b.Property("Content") + .HasColumnType("longtext"); + + b.Property("PublisherId") + .HasColumnType("int"); + + b.Property("Title") + .HasColumnType("longtext"); + + b.HasKey("AnnouncementId"); + + b.HasIndex("PublisherId"); + + b.ToTable("WebsiteAnnouncements"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedLevelEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedPlaylistEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", "Playlist") + .WithMany() + .HasForeignKey("PlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Playlist"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.HeartedProfileEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "HeartedUser") + .WithMany() + .HasForeignKey("HeartedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("HeartedUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.QueuedLevelEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedCommentEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", "Comment") + .WithMany() + .HasForeignKey("CommentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Comment"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedLevelEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.RatedReviewEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.ReviewEntity", "Review") + .WithMany() + .HasForeignKey("ReviewId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Review"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Interaction.VisitedLevelEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.PlaylistEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.ReviewEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Reviewer") + .WithMany() + .HasForeignKey("ReviewerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Reviewer"); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.ScoreEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Slot"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Moderation.GriefReportEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "ReportingPlayer") + .WithMany() + .HasForeignKey("ReportingPlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ReportingPlayer"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Moderation.ModerationCaseEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Dismisser") + .WithMany() + .HasForeignKey("DismisserId"); + + b.Navigation("Creator"); + + b.Navigation("Dismisser"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Notifications.NotificationEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.BlockedProfileEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "BlockedUser") + .WithMany() + .HasForeignKey("BlockedUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BlockedUser"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.CommentEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Poster") + .WithMany() + .HasForeignKey("PosterUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "TargetSlot") + .WithMany() + .HasForeignKey("TargetSlotId"); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "TargetUser") + .WithMany() + .HasForeignKey("TargetUserId"); + + b.Navigation("Poster"); + + b.Navigation("TargetSlot"); + + b.Navigation("TargetUser"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.LastContactEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Level.SlotEntity", "Slot") + .WithMany() + .HasForeignKey("SlotId"); + + b.Navigation("Creator"); + + b.Navigation("Slot"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoSubjectEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", "Photo") + .WithMany("PhotoSubjects") + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Photo"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PlatformLinkAttemptEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserPinProgressEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserProfilePinsEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.EmailSetTokenEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.EmailVerificationTokenEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.GameTokenEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Website.WebsiteAnnouncementEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "Publisher") + .WithMany() + .HasForeignKey("PublisherId"); + + b.Navigation("Publisher"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.PhotoEntity", b => + { + b.Navigation("PhotoSubjects"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.cs b/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.cs new file mode 100644 index 000000000..8f8bd811f --- /dev/null +++ b/ProjectLighthouse/Migrations/20260830001802_AddUserPinState.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ProjectLighthouse.Migrations +{ + /// + public partial class AddUserPinState : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserPinProgress", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + PinSet = table.Column(type: "int", nullable: false), + ProgressType = table.Column(type: "int unsigned", nullable: false), + Value = table.Column(type: "double", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserPinProgress", x => new { x.UserId, x.PinSet, x.ProgressType }); + table.ForeignKey( + name: "FK_UserPinProgress_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateTable( + name: "UserProfilePins", + columns: table => new + { + UserId = table.Column(type: "int", nullable: false), + GameVersion = table.Column(type: "int", nullable: false), + Pins = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_UserProfilePins", x => new { x.UserId, x.GameVersion }); + table.ForeignKey( + name: "FK_UserProfilePins_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserPinProgress"); + + migrationBuilder.DropTable( + name: "UserProfilePins"); + } + } +} diff --git a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs index 817a93916..be9358f2c 100644 --- a/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs +++ b/ProjectLighthouse/Migrations/DatabaseContextModelSnapshot.cs @@ -957,6 +957,41 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("Users"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserPinProgressEntity", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("PinSet") + .HasColumnType("int"); + + b.Property("ProgressType") + .HasColumnType("int unsigned"); + + b.Property("Value") + .HasColumnType("double"); + + b.HasKey("UserId", "PinSet", "ProgressType"); + + b.ToTable("UserPinProgress"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserProfilePinsEntity", b => + { + b.Property("UserId") + .HasColumnType("int"); + + b.Property("GameVersion") + .HasColumnType("int"); + + b.Property("Pins") + .HasColumnType("longtext"); + + b.HasKey("UserId", "GameVersion"); + + b.ToTable("UserProfilePins"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.ApiKeyEntity", b => { b.Property("Id") @@ -1518,6 +1553,28 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserPinProgressEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserProfilePinsEntity", b => + { + b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + modelBuilder.Entity("LBPUnion.ProjectLighthouse.Types.Entities.Token.EmailSetTokenEntity", b => { b.HasOne("LBPUnion.ProjectLighthouse.Types.Entities.Profile.UserEntity", "User") diff --git a/ProjectLighthouse/Types/Entities/Profile/UserPinProgressEntity.cs b/ProjectLighthouse/Types/Entities/Profile/UserPinProgressEntity.cs new file mode 100644 index 000000000..2a09e527c --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Profile/UserPinProgressEntity.cs @@ -0,0 +1,18 @@ +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +[PrimaryKey(nameof(UserId), nameof(PinSet), nameof(ProgressType))] +public class UserPinProgressEntity +{ + public int UserId { get; set; } + + public UserEntity User { get; set; } = null!; + + public PinSet PinSet { get; set; } + + public uint ProgressType { get; set; } + + public double Value { get; set; } +} diff --git a/ProjectLighthouse/Types/Entities/Profile/UserProfilePinsEntity.cs b/ProjectLighthouse/Types/Entities/Profile/UserProfilePinsEntity.cs new file mode 100644 index 000000000..21e102584 --- /dev/null +++ b/ProjectLighthouse/Types/Entities/Profile/UserProfilePinsEntity.cs @@ -0,0 +1,13 @@ +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Types.Entities.Profile; + +[PrimaryKey(nameof(UserId), nameof(GameVersion))] +public class UserProfilePinsEntity +{ + public int UserId { get; set; } + public UserEntity User { get; set; } = null!; + public GameVersion GameVersion { get; set; } + public string Pins { get; set; } = ""; +} diff --git a/ProjectLighthouse/Types/Users/PinProgressRules.cs b/ProjectLighthouse/Types/Users/PinProgressRules.cs new file mode 100644 index 000000000..61e813ff5 --- /dev/null +++ b/ProjectLighthouse/Types/Users/PinProgressRules.cs @@ -0,0 +1,18 @@ +using System; + +namespace LBPUnion.ProjectLighthouse.Types.Users; + +public static class PinProgressRules +{ + private const uint StoryScoreboardBestPercentage = 191183438u; + private const uint CommunityScoreboardBestPercentage = 2033315234u; + + public static bool IsLowerProgressBetter(uint progressType) => progressType is StoryScoreboardBestPercentage or CommunityScoreboardBestPercentage; + + public static double Merge(uint progressType, double storedValue, double uploadedValue) + { + return IsLowerProgressBetter(progressType) + ? Math.Min(storedValue, uploadedValue) + : Math.Max(storedValue, uploadedValue); + } +} diff --git a/ProjectLighthouse/Types/Users/PinSet.cs b/ProjectLighthouse/Types/Users/PinSet.cs new file mode 100644 index 000000000..3b5f5e7ed --- /dev/null +++ b/ProjectLighthouse/Types/Users/PinSet.cs @@ -0,0 +1,20 @@ +using System; + +namespace LBPUnion.ProjectLighthouse.Types.Users; + +public enum PinSet +{ + LittleBigPlanet = 0, + Vita = 1, +} + +public static class PinSetExtensions +{ + public static PinSet ToPinSet(this GameVersion gameVersion) => gameVersion switch + { + GameVersion.LittleBigPlanet2 => PinSet.LittleBigPlanet, + GameVersion.LittleBigPlanet3 => PinSet.LittleBigPlanet, + GameVersion.LittleBigPlanetVita => PinSet.Vita, + _ => throw new ArgumentOutOfRangeException(nameof(gameVersion), gameVersion, null), + }; +}