From 0b9903151639b2d010ebfea7e0783aabed87e113 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:18:59 +0200 Subject: [PATCH 01/21] Reports -> GameGriefReports --- .../GameDatabaseContext.Reports.cs | 32 +++++++++---------- Refresh.Database/GameDatabaseContext.cs | 2 +- .../Models/Relations/ReportPlayerRelation.cs | 2 +- .../Models/Reports/GriefReport.cs | 2 +- .../Endpoints/ReportingEndpoints.cs | 2 +- .../Reporting/ReportingEndpointsTests.cs | 6 ++-- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Reports.cs b/Refresh.Database/GameDatabaseContext.Reports.cs index 59dca13cd..8b24743c8 100644 --- a/Refresh.Database/GameDatabaseContext.Reports.cs +++ b/Refresh.Database/GameDatabaseContext.Reports.cs @@ -7,7 +7,7 @@ namespace Refresh.Database; public partial class GameDatabaseContext // Reports { - private IQueryable ReportsIncluded => this.Reports + private IQueryable GameGriefReportsIncluded => this.GameGriefReports .Include(r => r.Reporter) .Include(r => r.Reporter.Statistics) .Include(r => r.Level) @@ -20,16 +20,16 @@ public partial class GameDatabaseContext // Reports .Include(r => r.Players) .ThenInclude(p => p.User.Statistics); - public void CreateReport(GriefReport griefReport) + public void CreateReport(GameGriefReport griefReport) { this.Write(() => { griefReport.ReportDate = this._time.Now; - this.Reports.Add(griefReport); + this.GameGriefReports.Add(griefReport); }); } - public void UpdateReportStatus(GriefReport griefReport, GriefReportStatus status, GameUser? reviewer = null, string? notes = null) + public void UpdateReportStatus(GameGriefReport griefReport, GriefReportStatus status, GameUser? reviewer = null, string? notes = null) { this.Write(() => { @@ -40,41 +40,41 @@ public void UpdateReportStatus(GriefReport griefReport, GriefReportStatus status }); } - public void RemoveReport(GriefReport griefReport) + public void RemoveReport(GameGriefReport griefReport) { this.Write(() => { - this.Reports.Remove(griefReport); + this.GameGriefReports.Remove(griefReport); }); } [Pure] - public GriefReport? GetReportById(int id) => - this.ReportsIncluded.FirstOrDefault(r => r.ReportId == id); + public GameGriefReport? GetReportById(int id) => + this.GameGriefReportsIncluded.FirstOrDefault(r => r.ReportId == id); [Pure] - public DatabaseList GetReportsByStatus(GriefReportStatus status, int count, int skip) => - new(this.ReportsIncluded + public DatabaseList GetReportsByStatus(GriefReportStatus status, int count, int skip) => + new(this.GameGriefReportsIncluded .Where(r => r.Status == status) .OrderByDescending(r => r.ReportDate), skip, count); [Pure] - public DatabaseList GetReportsByReporter(GameUser reporter, int count, int skip) => - new(this.ReportsIncluded + public DatabaseList GetReportsByReporter(GameUser reporter, int count, int skip) => + new(this.GameGriefReportsIncluded .Where(r => r.Reporter == reporter) .OrderByDescending(r => r.ReportDate), skip, count); [Pure] - public DatabaseList GetReportsForLevel(GameLevel level, int count, int skip) => - new(this.ReportsIncluded + public DatabaseList GetReportsForLevel(GameLevel level, int count, int skip) => + new(this.GameGriefReportsIncluded .Where(r => r.Level == level) .OrderByDescending(r => r.ReportDate), skip, count); [Pure] - public DatabaseList GetStaleReports(TimeSpan maxAge, int count, int skip) + public DatabaseList GetStaleReports(TimeSpan maxAge, int count, int skip) { DateTimeOffset cutoff = this._time.Now - maxAge; - return new(this.ReportsIncluded + return new(this.GameGriefReportsIncluded .Where(r => r.Status == GriefReportStatus.Pending && r.ReportDate < cutoff) .OrderBy(r => r.ReportDate), skip, count); } diff --git a/Refresh.Database/GameDatabaseContext.cs b/Refresh.Database/GameDatabaseContext.cs index c8c5e69b9..a611bb4e8 100644 --- a/Refresh.Database/GameDatabaseContext.cs +++ b/Refresh.Database/GameDatabaseContext.cs @@ -80,7 +80,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext internal DbSet PinProgressRelations { get; set; } internal DbSet ProfilePinRelations { get; set; } internal DbSet GameSkillRewards { get; set; } - internal DbSet Reports { get; set; } + internal DbSet GameGriefReports { get; set; } internal DbSet ReportPlayersRelations { get; set; } internal DbSet Workers { get; set; } internal DbSet JobStates { get; set; } diff --git a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs index bf09357fa..adb8d5ece 100644 --- a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs +++ b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs @@ -10,7 +10,7 @@ public partial class ReportPlayerRelation { [Required] [ForeignKey(nameof(ReportId))] - public GriefReport GriefReport { get; set; } + public GameGriefReport GameGriefReport { get; set; } [Required] [ForeignKey(nameof(UserId))] diff --git a/Refresh.Database/Models/Reports/GriefReport.cs b/Refresh.Database/Models/Reports/GriefReport.cs index d2a737cdc..d881fce5f 100644 --- a/Refresh.Database/Models/Reports/GriefReport.cs +++ b/Refresh.Database/Models/Reports/GriefReport.cs @@ -7,7 +7,7 @@ namespace Refresh.Database.Models.Reports; -public class GriefReport +public class GameGriefReport { [Key] public int ReportId { get; set; } diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 01180d52e..f94ba2ac1 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -118,7 +118,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas if (body.Description != null && body.Description.Length > UgcLimits.DescriptionLimit) body.Description = body.Description[..UgcLimits.DescriptionLimit]; - GriefReport griefReport = new() + GameGriefReport griefReport = new() { Reporter = user, GameVersion = token.TokenGame, diff --git a/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs b/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs index 1cf9aa1ff..d3c61028a 100644 --- a/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs @@ -87,10 +87,10 @@ public void UploadReport() context.Database.Refresh(); - DatabaseList reports = context.Database.GetReportsByReporter(user, 10, 0); + DatabaseList reports = context.Database.GetReportsByReporter(user, 10, 0); Assert.That(reports.TotalItems, Is.EqualTo(1)); - GriefReport createdReport = reports.Items.First(); + GameGriefReport createdReport = reports.Items.First(); Assert.That(createdReport.Level?.LevelId, Is.EqualTo(level.LevelId)); Assert.That(createdReport.Type, Is.EqualTo(GriefReportType.TermsOfService)); Assert.That(createdReport.Description, Is.EqualTo("This is a description, sent by LBP3")); @@ -122,7 +122,7 @@ public void CanUploadReportWithBadLevel() context.Database.Refresh(); - DatabaseList reports = context.Database.GetReportsByReporter(user, 10, 0); + DatabaseList reports = context.Database.GetReportsByReporter(user, 10, 0); Assert.That(reports.TotalItems, Is.EqualTo(1)); Assert.That(reports.Items.First().Level, Is.Null); From 2ad92e5e83032dd1d898e66da6a0c7c21f1bd2f0 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:19:36 +0200 Subject: [PATCH 02/21] Forgot to rename the file itself --- .../Models/Reports/{GriefReport.cs => GameGriefReport.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Refresh.Database/Models/Reports/{GriefReport.cs => GameGriefReport.cs} (100%) diff --git a/Refresh.Database/Models/Reports/GriefReport.cs b/Refresh.Database/Models/Reports/GameGriefReport.cs similarity index 100% rename from Refresh.Database/Models/Reports/GriefReport.cs rename to Refresh.Database/Models/Reports/GameGriefReport.cs From 6d0fbf02e636716f14fec5671fbf352c2f146fe4 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:27:31 +0200 Subject: [PATCH 03/21] Make grief report struct classes used for uploads follow naming rules --- .../Endpoints/ReportingEndpoints.cs | 4 +- ...GameReport.cs => SerializedGriefReport.cs} | 18 ++--- ...ubble.cs => SerializedReportInfoBubble.cs} | 4 +- .../{Marqee.cs => SerializedReportMarqee.cs} | 4 +- .../{Player.cs => SerializedReportPlayer.cs} | 6 +- .../{Rect.cs => SerializedReportRect.cs} | 2 +- ...s.cs => SerializedReportScreenElements.cs} | 14 ++-- ...nRect.cs => SerializedReportScreenRect.cs} | 4 +- .../{Slot.cs => SerializedReportSlot.cs} | 4 +- .../Reporting/ReportingEndpointsTests.cs | 80 +++++++++---------- 10 files changed, 70 insertions(+), 70 deletions(-) rename Refresh.Interfaces.Game/Types/Report/{GameReport.cs => SerializedGriefReport.cs} (75%) rename Refresh.Interfaces.Game/Types/Report/{InfoBubble.cs => SerializedReportInfoBubble.cs} (82%) rename Refresh.Interfaces.Game/Types/Report/{Marqee.cs => SerializedReportMarqee.cs} (63%) rename Refresh.Interfaces.Game/Types/Report/{Player.cs => SerializedReportPlayer.cs} (77%) rename Refresh.Interfaces.Game/Types/Report/{Rect.cs => SerializedReportRect.cs} (92%) rename Refresh.Interfaces.Game/Types/Report/{ScreenElements.cs => SerializedReportScreenElements.cs} (66%) rename Refresh.Interfaces.Game/Types/Report/{ScreenRect.cs => SerializedReportScreenRect.cs} (62%) rename Refresh.Interfaces.Game/Types/Report/{Slot.cs => SerializedReportSlot.cs} (84%) diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index f94ba2ac1..1cf92fcc7 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -26,7 +26,7 @@ public class ReportingEndpoints : EndpointGroup [GameEndpoint("grief", HttpMethods.Post, ContentType.Xml)] [RequireEmailVerified] [EndpointRateLimit(EndpointBucketId.GameUploadGriefReport)] - public Response UploadReport(RequestContext context, GameDatabaseContext database, GameReport body, GameUser user, + public Response UploadReport(RequestContext context, GameDatabaseContext database, SerializedGriefReport body, GameUser user, IDateTimeProvider time, Token token, GameServerConfig config) { if (user.IsWriteBlocked(config)) @@ -138,7 +138,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas return OK; } - private static string NormalizeRectangle(Rect rect, Size imageSize) + private static string NormalizeRectangle(SerializedReportRect rect, Size imageSize) { float halfWidth = imageSize.Width / 2f; float halfHeight = imageSize.Height / 2f; diff --git a/Refresh.Interfaces.Game/Types/Report/GameReport.cs b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs similarity index 75% rename from Refresh.Interfaces.Game/Types/Report/GameReport.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs index 7e9991cb8..6bccedfff 100644 --- a/Refresh.Interfaces.Game/Types/Report/GameReport.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs @@ -6,13 +6,13 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("griefReport")] -public class GameReport +public class SerializedGriefReport { [XmlIgnore] - private List InternalInfoBubble { get; } = []; + private List InternalInfoBubble { get; } = []; [XmlElement("infoBubble")] - public InfoBubble[] InfoBubble + public SerializedReportInfoBubble[] InfoBubble { get => this.InternalInfoBubble.ToArray(); set @@ -24,7 +24,7 @@ public InfoBubble[] InfoBubble return; } - foreach (InfoBubble infoBubble in value) + foreach (SerializedReportInfoBubble infoBubble in value) this.InternalInfoBubble.Add(infoBubble); } } @@ -36,7 +36,7 @@ public InfoBubble[] InfoBubble public int InternalType { get; set; } [XmlElement("marqee")] - public Marqee Marqee { get; set; } + public SerializedReportMarqee Marqee { get; set; } [XmlElement("levelOwner")] public string LevelOwner { get; set; } @@ -60,10 +60,10 @@ public InfoBubble[] InfoBubble public string JpegHash { get; set; } [XmlIgnore] - private List InternalPlayers { get; } = []; + private List InternalPlayers { get; } = []; [XmlElement("player")] - public Player[] Players + public SerializedReportPlayer[] Players { get => this.InternalPlayers.ToArray(); set @@ -75,11 +75,11 @@ public Player[] Players return; } - foreach (Player player in value) + foreach (SerializedReportPlayer player in value) this.InternalPlayers.Add(player); } } [XmlElement("screenElements")] - public ScreenElements ScreenElements { get; set; } + public SerializedReportScreenElements ScreenElements { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Types/Report/InfoBubble.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportInfoBubble.cs similarity index 82% rename from Refresh.Interfaces.Game/Types/Report/InfoBubble.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportInfoBubble.cs index 818130286..6f0b22de1 100644 --- a/Refresh.Interfaces.Game/Types/Report/InfoBubble.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportInfoBubble.cs @@ -5,10 +5,10 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("infoBubble")] -public class InfoBubble +public class SerializedReportInfoBubble { [XmlElement("slot")] - public Slot Slot { get; set; } + public SerializedReportSlot Slot { get; set; } [XmlElement("name")] public string Name { get; set; } diff --git a/Refresh.Interfaces.Game/Types/Report/Marqee.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportMarqee.cs similarity index 63% rename from Refresh.Interfaces.Game/Types/Report/Marqee.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportMarqee.cs index df014d7cc..1336c6461 100644 --- a/Refresh.Interfaces.Game/Types/Report/Marqee.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportMarqee.cs @@ -5,8 +5,8 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("marqee")] -public class Marqee +public class SerializedReportMarqee { [XmlElement("rect")] - public Rect Rect { get; set; } + public SerializedReportRect Rect { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Types/Report/Player.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportPlayer.cs similarity index 77% rename from Refresh.Interfaces.Game/Types/Report/Player.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportPlayer.cs index 90a4ae69d..cfaa3b1f4 100644 --- a/Refresh.Interfaces.Game/Types/Report/Player.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportPlayer.cs @@ -5,13 +5,13 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("player")] -public class Player +public class SerializedReportPlayer { [XmlElement("id")] public string Username { get; set; } [XmlElement("rect")] - public Rect Rectangle { get; set; } + public SerializedReportRect Rectangle { get; set; } [XmlAttribute("reporter")] public bool Reporter { get; set; } @@ -26,5 +26,5 @@ public class Player public string Text { get; set; } [XmlElement("screenRect")] - public ScreenRect ScreenRect { get; set; } + public SerializedReportScreenRect ScreenRect { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Types/Report/Rect.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportRect.cs similarity index 92% rename from Refresh.Interfaces.Game/Types/Report/Rect.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportRect.cs index 84b73fa8f..9a8ec0ff9 100644 --- a/Refresh.Interfaces.Game/Types/Report/Rect.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportRect.cs @@ -3,7 +3,7 @@ namespace Refresh.Interfaces.Game.Types.Report; [XmlRoot("rect")] -public class Rect +public class SerializedReportRect { [XmlAttribute(AttributeName="t")] public long Top { get; set; } diff --git a/Refresh.Interfaces.Game/Types/Report/ScreenElements.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportScreenElements.cs similarity index 66% rename from Refresh.Interfaces.Game/Types/Report/ScreenElements.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportScreenElements.cs index c53e0dab1..eb46e7005 100644 --- a/Refresh.Interfaces.Game/Types/Report/ScreenElements.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportScreenElements.cs @@ -5,13 +5,13 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("screenElements")] -public class ScreenElements +public class SerializedReportScreenElements { [XmlIgnore] - private List InternalSlot { get; } = []; + private List InternalSlot { get; } = []; [XmlElement("slot")] - public Slot[] Slot + public SerializedReportSlot[] Slot { get => this.InternalSlot.ToArray(); set @@ -23,16 +23,16 @@ public Slot[] Slot return; } - foreach (Slot slot in value) + foreach (SerializedReportSlot slot in value) this.InternalSlot.Add(slot); } } [XmlIgnore] - private List InternalPlayer { get; } = []; + private List InternalPlayer { get; } = []; [XmlElement("player")] - public Player[] Player + public SerializedReportPlayer[] Player { get => this.InternalPlayer.ToArray(); set @@ -44,7 +44,7 @@ public Player[] Player return; } - foreach (Player player in value) + foreach (SerializedReportPlayer player in value) this.InternalPlayer.Add(player); } } diff --git a/Refresh.Interfaces.Game/Types/Report/ScreenRect.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportScreenRect.cs similarity index 62% rename from Refresh.Interfaces.Game/Types/Report/ScreenRect.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportScreenRect.cs index 97dbf59f4..5880dac7a 100644 --- a/Refresh.Interfaces.Game/Types/Report/ScreenRect.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportScreenRect.cs @@ -5,8 +5,8 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("screenRect")] -public class ScreenRect +public class SerializedReportScreenRect { [XmlElement("rect")] - public Rect Rect { get; set; } + public SerializedReportRect Rect { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.Game/Types/Report/Slot.cs b/Refresh.Interfaces.Game/Types/Report/SerializedReportSlot.cs similarity index 84% rename from Refresh.Interfaces.Game/Types/Report/Slot.cs rename to Refresh.Interfaces.Game/Types/Report/SerializedReportSlot.cs index fbc0d75e6..d0d5e371e 100644 --- a/Refresh.Interfaces.Game/Types/Report/Slot.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedReportSlot.cs @@ -5,7 +5,7 @@ namespace Refresh.Interfaces.Game.Types.Report; #nullable disable [XmlRoot("slot")] -public class Slot +public class SerializedReportSlot { [XmlElement("id")] public int Id { get; set; } @@ -17,7 +17,7 @@ public class Slot public string Text { get; set; } [XmlElement("screenRect")] - public ScreenRect ScreenRect { get; set; } + public SerializedReportScreenRect ScreenRect { get; set; } [XmlElement("slotId")] public string SlotId { get; set; } diff --git a/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs b/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs index d3c61028a..327d77e88 100644 --- a/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Reporting/ReportingEndpointsTests.cs @@ -29,14 +29,14 @@ public void UploadReport() HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - GameReport report = new() + SerializedGriefReport report = new() { - InfoBubble = new InfoBubble[] + InfoBubble = new SerializedReportInfoBubble[] {}, Type = GriefReportType.TermsOfService, - Marqee = new Marqee + Marqee = new SerializedReportMarqee { - Rect = new Rect + Rect = new SerializedReportRect { Top = 10, Left = 10, @@ -51,12 +51,12 @@ public void UploadReport() Description = "This is a description, sent by LBP3", GriefStateHash = null, JpegHash = TEST_ASSET_HASH, - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, @@ -67,9 +67,9 @@ public void UploadReport() IngameNow = true, PlayerNumber = 0, Text = "Some text", - ScreenRect = new ScreenRect + ScreenRect = new SerializedReportScreenRect { - Rect = new Rect + Rect = new SerializedReportRect { Top = 10, Left = 10, @@ -79,7 +79,7 @@ public void UploadReport() }, }, }, - ScreenElements = new ScreenElements(), + ScreenElements = new SerializedReportScreenElements(), }; HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(report.AsXML())).Result; @@ -111,7 +111,7 @@ public void CanUploadReportWithBadLevel() HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - GameReport report = new() + SerializedGriefReport report = new() { LevelId = int.MaxValue, JpegHash = TEST_ASSET_HASH, @@ -142,36 +142,36 @@ public void CantUploadReportWithBadPlayerCount(bool psp) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - GameReport report = new() + SerializedGriefReport report = new() { LevelId = 0, JpegHash = TEST_ASSET_HASH, - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = "hee", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "hawk", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "2uh", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "burger", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "er", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, }, }; @@ -194,38 +194,38 @@ public void CantUploadReportWithBadScreenElementPlayerCount(bool psp) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - GameReport report = new() + SerializedGriefReport report = new() { LevelId = 0, JpegHash = TEST_ASSET_HASH, - ScreenElements = new ScreenElements + ScreenElements = new SerializedReportScreenElements { - Player = new Player[] + Player = new SerializedReportPlayer[] { new() { Username = "hee", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "haw", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "ham", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "burg", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, new() { Username = "er", - Rectangle = new Rect(), + Rectangle = new SerializedReportRect(), }, }, }, @@ -255,14 +255,14 @@ public void RedirectGriefReportToPhoto(TokenGame game) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new GameReport + HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new SerializedGriefReport { - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, @@ -310,14 +310,14 @@ public void RedirectGriefReportToPhotoDeveloperLevel(TokenGame game) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new GameReport + HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new SerializedGriefReport { - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, @@ -364,14 +364,14 @@ public void RedirectGriefReportToPhotoWhenNotUserOrDeveloperSlot(TokenGame game) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new GameReport + HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new SerializedGriefReport { - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, @@ -418,14 +418,14 @@ public void RedirectGriefReportToPhotoWhenMissingLevel(TokenGame game) HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_ASSET_HASH}", new ReadOnlyMemoryContent(TestAsset)).Result; Assert.That(message.StatusCode, Is.EqualTo(OK)); - HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new GameReport + HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new SerializedGriefReport { - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, @@ -463,14 +463,14 @@ public void CantRedirectGriefReportToPhotoWhenWebsiteToken() using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.Website, TokenPlatform.PS3, user); - HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new GameReport + HttpResponseMessage response = client.PostAsync("/lbp/grief", new StringContent(new SerializedGriefReport { - Players = new Player[] + Players = new SerializedReportPlayer[] { new() { Username = user.Username, - Rectangle = new Rect + Rectangle = new SerializedReportRect { Top = 10, Left = 10, From 4e283ab900b1754dd637cf1cc895c4c18d57a3f3 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:32:22 +0200 Subject: [PATCH 04/21] Minor grouping issue --- Refresh.Database/Models/Relations/ReportPlayerRelation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs index adb8d5ece..eec7cd939 100644 --- a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs +++ b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs @@ -11,18 +11,18 @@ public partial class ReportPlayerRelation [Required] [ForeignKey(nameof(ReportId))] public GameGriefReport GameGriefReport { get; set; } + [Required] public int ReportId { get; set; } [Required] [ForeignKey(nameof(UserId))] public GameUser User { get; set; } - - [Required] public int ReportId { get; set; } [Required] public ObjectId UserId { get; set; } public bool IsReporter { get; set; } public bool IsInGameNow { get; set; } // i think something about whether the player left during the report, could rename it if confirmed public int PlayerNumber { get; set; } + // TODO this and photo subject bounds should be stored in a more atomized way public string PlayerRect { get; set; } = string.Empty; // normalized "l,t,r,b" public DateTimeOffset Timestamp { get; set; } From 21877f698fd0b3af7e15f9008bf9517d856ed1f6 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:37:22 +0200 Subject: [PATCH 05/21] Compare entities referenced by grief reports by FK --- Refresh.Database/GameDatabaseContext.Reports.cs | 4 ++-- Refresh.Database/Models/Reports/GameGriefReport.cs | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Reports.cs b/Refresh.Database/GameDatabaseContext.Reports.cs index 8b24743c8..467210c0f 100644 --- a/Refresh.Database/GameDatabaseContext.Reports.cs +++ b/Refresh.Database/GameDatabaseContext.Reports.cs @@ -61,13 +61,13 @@ public DatabaseList GetReportsByStatus(GriefReportStatus status [Pure] public DatabaseList GetReportsByReporter(GameUser reporter, int count, int skip) => new(this.GameGriefReportsIncluded - .Where(r => r.Reporter == reporter) + .Where(r => r.ReporterUserId == reporter.UserId) .OrderByDescending(r => r.ReportDate), skip, count); [Pure] public DatabaseList GetReportsForLevel(GameLevel level, int count, int skip) => new(this.GameGriefReportsIncluded - .Where(r => r.Level == level) + .Where(r => r.LevelId == level.LevelId) .OrderByDescending(r => r.ReportDate), skip, count); [Pure] diff --git a/Refresh.Database/Models/Reports/GameGriefReport.cs b/Refresh.Database/Models/Reports/GameGriefReport.cs index d881fce5f..6dcbf3d3d 100644 --- a/Refresh.Database/Models/Reports/GameGriefReport.cs +++ b/Refresh.Database/Models/Reports/GameGriefReport.cs @@ -1,7 +1,6 @@ -using System.Drawing; +using MongoDB.Bson; using Refresh.Database.Models.Authentication; using Refresh.Database.Models.Levels; -using Refresh.Database.Models.Photos; using Refresh.Database.Models.Relations; using Refresh.Database.Models.Users; @@ -11,14 +10,18 @@ public class GameGriefReport { [Key] public int ReportId { get; set; } + [Required, ForeignKey(nameof(ReporterUserId))] public GameUser Reporter { get; set; } = null!; + [Required] public ObjectId ReporterUserId { get; set; } + public DateTimeOffset ReportDate { get; set; } public TokenGame GameVersion { get; set; } // Level data public int? LevelId { get; set; } - [ForeignKey("LevelId")] + [ForeignKey(nameof(LevelId))] public GameLevel? Level { get; set; } // Level reference only provided if it's an uploaded or story level + public string? LevelType { get; set; } public string InitialStateHash { get; set; } = "0"; // Hash for asset of initial level public string GriefStateHash { get; set; } = "0"; // Hash for asset of griefed level @@ -33,7 +36,10 @@ public class GameGriefReport public string Description { get; set; } = string.Empty; // Moderation + [Required, ForeignKey(nameof(ReviewedByUserId))] public GameUser? ReviewedBy { get; set; } + [Required] public ObjectId? ReviewedByUserId { get; set; } + public DateTimeOffset? ReviewedDate { get; set; } public string? ModeratorNotes { get; set; } public GriefReportStatus Status { get; set; } = GriefReportStatus.Pending; From f3da274153d3aa85cbf640cf6948258488acdbf9 Mon Sep 17 00:00:00 2001 From: Toaster Date: Mon, 12 Oct 2026 18:42:30 +0200 Subject: [PATCH 06/21] WHY WAS THIS REQUIRED? --- Refresh.Database/GameDatabaseContext.Reports.cs | 5 ++--- Refresh.Database/Models/Relations/ReportPlayerRelation.cs | 8 +++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.Reports.cs b/Refresh.Database/GameDatabaseContext.Reports.cs index 467210c0f..b9efeb553 100644 --- a/Refresh.Database/GameDatabaseContext.Reports.cs +++ b/Refresh.Database/GameDatabaseContext.Reports.cs @@ -16,9 +16,8 @@ public partial class GameDatabaseContext // Reports .Include(r => r.ReviewedBy) .Include(r => r.ReviewedBy!.Statistics) .Include(r => r.Players) - .ThenInclude(p => p.User) - .Include(r => r.Players) - .ThenInclude(p => p.User.Statistics); + .ThenInclude(p => p.User!) + .ThenInclude(p => p.Statistics); public void CreateReport(GameGriefReport griefReport) { diff --git a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs index eec7cd939..aea7f1796 100644 --- a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs +++ b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs @@ -3,20 +3,18 @@ using Refresh.Database.Models.Users; namespace Refresh.Database.Models.Relations; -#nullable disable [PrimaryKey(nameof(ReportId), nameof(UserId))] public partial class ReportPlayerRelation { [Required] [ForeignKey(nameof(ReportId))] - public GameGriefReport GameGriefReport { get; set; } + public GameGriefReport GameGriefReport { get; set; } = null!; [Required] public int ReportId { get; set; } - [Required] [ForeignKey(nameof(UserId))] - public GameUser User { get; set; } - [Required] public ObjectId UserId { get; set; } + public GameUser? User { get; set; } + public ObjectId? UserId { get; set; } public bool IsReporter { get; set; } public bool IsInGameNow { get; set; } // i think something about whether the player left during the report, could rename it if confirmed From 1be64cd403658173911eba7c6fcfcb20da4c59b9 Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 12:16:29 +0200 Subject: [PATCH 07/21] Add username to ReportPlayerRelation, replace UserId with player number in PK --- Refresh.Database/Models/Relations/ReportPlayerRelation.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs index aea7f1796..73c2556a4 100644 --- a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs +++ b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs @@ -4,7 +4,7 @@ namespace Refresh.Database.Models.Relations; -[PrimaryKey(nameof(ReportId), nameof(UserId))] +[PrimaryKey(nameof(ReportId), nameof(PlayerNumber))] public partial class ReportPlayerRelation { [Required] @@ -15,7 +15,8 @@ public partial class ReportPlayerRelation [ForeignKey(nameof(UserId))] public GameUser? User { get; set; } public ObjectId? UserId { get; set; } - + public string Username { get; set; } = ""; + public bool IsReporter { get; set; } public bool IsInGameNow { get; set; } // i think something about whether the player left during the report, could rename it if confirmed public int PlayerNumber { get; set; } From 0d7f513432b84b6e37c767d22556d7a0e6abe29c Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 12:18:25 +0200 Subject: [PATCH 08/21] Grief report upload's "levelType" is actually slot type, rename props to reflect that --- .../Models/Reports/GameGriefReport.cs | 2 +- .../Endpoints/ReportingEndpoints.cs | 31 ++++++++++++------- .../Types/Report/SerializedGriefReport.cs | 3 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/Refresh.Database/Models/Reports/GameGriefReport.cs b/Refresh.Database/Models/Reports/GameGriefReport.cs index 6dcbf3d3d..81ca9b9ca 100644 --- a/Refresh.Database/Models/Reports/GameGriefReport.cs +++ b/Refresh.Database/Models/Reports/GameGriefReport.cs @@ -22,7 +22,7 @@ public class GameGriefReport [ForeignKey(nameof(LevelId))] public GameLevel? Level { get; set; } // Level reference only provided if it's an uploaded or story level - public string? LevelType { get; set; } + public string? SlotType { get; set; } public string InitialStateHash { get; set; } = "0"; // Hash for asset of initial level public string GriefStateHash { get; set; } = "0"; // Hash for asset of griefed level diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 1cf92fcc7..16f45a198 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -32,7 +32,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas if (user.IsWriteBlocked(config)) return Unauthorized; - GameLevel? level = database.GetLevelByIdAndType(body.LevelType, body.LevelId); + GameLevel? level = database.GetLevelByIdAndType(body.SlotType, body.LevelId); Size imageSize; switch (token.TokenGame) { @@ -85,7 +85,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas { LevelId = body.LevelId, Title = level?.Title ?? "", - Type = body.LevelType + Type = body.SlotType }, PhotoSubjects = subjects, }, subjects, user, level); @@ -100,18 +100,27 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas List players = new(); if (body.Players != null) { - players.AddRange(body.Players.Select(player => + // We don't preserve player numbers since bugged or forged requests could cause issues otherwise, + // but we will still use them to preserve the player order, for our own player numbers. + int newPlayerNumber = 1; + foreach (SerializedReportPlayer player in body.Players.OrderBy(p => p.PlayerNumber)) { - GameUser? playerUser = database.GetUserByUsername(player.Username); // what if user doesnt exist - return new ReportPlayerRelation + // Even if the user doesn't exist (local guest or user from another server), + // we should save their presence in this report anyway, just like with photos. + GameUser? playerUser = database.GetUserByUsername(player.Username); + + players.Add(new ReportPlayerRelation { - User = playerUser, + User = playerUser, + Username = player.Username, + PlayerNumber = newPlayerNumber, IsReporter = player.Reporter, - IsInGameNow = player.IngameNow, - PlayerNumber = player.PlayerNumber, + IsInGameNow = player.IngameNow, PlayerRect = player.Rectangle == null ? string.Empty : NormalizeRectangle(player.Rectangle, imageSize) - }; - })); + }); + + newPlayerNumber++; + } } // Trim description @@ -123,7 +132,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas Reporter = user, GameVersion = token.TokenGame, Level = level, - LevelType = body.LevelType, + SlotType = body.SlotType, InitialStateHash = body.InitialStateHash ?? "0", GriefStateHash = body.GriefStateHash ?? "0", PhotoAssetHash = jpegHash ?? "0", diff --git a/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs index 6bccedfff..7783079b7 100644 --- a/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs @@ -44,8 +44,9 @@ public SerializedReportInfoBubble[] InfoBubble [XmlElement("initialStateHash")] public string InitialStateHash { get; set; } + // Actually the slot type, but they named it this anyway... [XmlElement("levelType")] - public string LevelType { get; set; } + public string SlotType { get; set; } [XmlElement("levelId")] public int LevelId { get; set; } From bdfe3bde41715ae32c18a4cff7ae07a1184b63cc Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 13:17:09 +0200 Subject: [PATCH 09/21] Revert GameGriefReport.LevelType -> SlotType, it's also called LevelType on GamePhoto and ApiGamePhotoResponse --- Refresh.Database/Models/Reports/GameGriefReport.cs | 2 +- Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs | 6 +++--- .../Types/Report/SerializedGriefReport.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Refresh.Database/Models/Reports/GameGriefReport.cs b/Refresh.Database/Models/Reports/GameGriefReport.cs index 81ca9b9ca..6dcbf3d3d 100644 --- a/Refresh.Database/Models/Reports/GameGriefReport.cs +++ b/Refresh.Database/Models/Reports/GameGriefReport.cs @@ -22,7 +22,7 @@ public class GameGriefReport [ForeignKey(nameof(LevelId))] public GameLevel? Level { get; set; } // Level reference only provided if it's an uploaded or story level - public string? SlotType { get; set; } + public string? LevelType { get; set; } public string InitialStateHash { get; set; } = "0"; // Hash for asset of initial level public string GriefStateHash { get; set; } = "0"; // Hash for asset of griefed level diff --git a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs index 16f45a198..8dce50c58 100644 --- a/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs +++ b/Refresh.Interfaces.Game/Endpoints/ReportingEndpoints.cs @@ -32,7 +32,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas if (user.IsWriteBlocked(config)) return Unauthorized; - GameLevel? level = database.GetLevelByIdAndType(body.SlotType, body.LevelId); + GameLevel? level = database.GetLevelByIdAndType(body.LevelType, body.LevelId); Size imageSize; switch (token.TokenGame) { @@ -85,7 +85,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas { LevelId = body.LevelId, Title = level?.Title ?? "", - Type = body.SlotType + Type = body.LevelType }, PhotoSubjects = subjects, }, subjects, user, level); @@ -132,7 +132,7 @@ public Response UploadReport(RequestContext context, GameDatabaseContext databas Reporter = user, GameVersion = token.TokenGame, Level = level, - SlotType = body.SlotType, + LevelType = body.LevelType, InitialStateHash = body.InitialStateHash ?? "0", GriefStateHash = body.GriefStateHash ?? "0", PhotoAssetHash = jpegHash ?? "0", diff --git a/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs index 7783079b7..1cc0d2b9e 100644 --- a/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs +++ b/Refresh.Interfaces.Game/Types/Report/SerializedGriefReport.cs @@ -46,7 +46,7 @@ public SerializedReportInfoBubble[] InfoBubble // Actually the slot type, but they named it this anyway... [XmlElement("levelType")] - public string SlotType { get; set; } + public string LevelType { get; set; } [XmlElement("levelId")] public int LevelId { get; set; } From fc620f9e59f1e8f320547fe03dd05d49aa2daa4a Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 13:18:30 +0200 Subject: [PATCH 10/21] Add DB migration for grief report stuff --- ...013102558_MiscGriefReportQualityUpdates.cs | 119 ++++++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 38 +++--- 2 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs new file mode 100644 index 000000000..2320c517d --- /dev/null +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -0,0 +1,119 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20261013102558_MiscGriefReportQualityUpdates")] + public partial class MiscGriefReportQualityUpdates : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable( + name: "Reports", + newName: "GameGriefReports"); + + migrationBuilder.AlterColumn( + name: "PlayerRect", + table: "ReportPlayersRelations", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "ReportPlayersRelations", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddColumn( + name: "Username", + table: "ReportPlayersRelations", + type: "text", + nullable: false, + defaultValue: ""); + + // (Re)init the player number column so we can ensure it only has unique values per report ID, + // so we can safely use it to replace UserId in the primary key + // https://stackoverflow.com/questions/65231516/add-and-update-a-column-with-sequential-numbering-based-on-grouping + migrationBuilder.Sql + (""" + UPDATE rpr SET rpr."PlayerNumber" = newNum + FROM ( + SELECT "ReportPlayerRelations".*, row_number() + OVER (PARTITION BY "ReportId" ORDER BY "ReportId") newNum + FROM "ReportPlayerRelations" + ) rpr + """); + + migrationBuilder.DropPrimaryKey( + name: "PK_ReportPlayersRelations", + table: "ReportPlayersRelations"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ReportPlayersRelations", + table: "ReportPlayersRelations", + columns: new[] { "ReportId", "PlayerNumber" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameTable( + name: "GameGriefReports", + newName: "Reports"); + + migrationBuilder.AlterColumn( + name: "UserId", + table: "ReportPlayersRelations", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "PlayerRect", + table: "ReportPlayersRelations", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + // Just remove all with duplicate user IDs. Usually we won't have duplicate IDs anyway, + // and this should be enough for a rollback. Better than dropping the whole table or something. + // We can just use PlayerNumber to uniquely identify rows per ReportId, since we've already made sure + // they're actually unique in the update (which we're coming from if we're downgrading). + migrationBuilder.Sql + (""" + DELETE FROM "ReportPlayersRelations" + WHERE "PlayerNumber" NOT IN ( + SELECT "PlayerNumber" + FROM "DisallowedEmailDomains" + PARTITION BY "ReportId" + GROUP BY "UserId" + ORDER BY "PlayerNumber" + ) + """); + + migrationBuilder.DropPrimaryKey( + name: "PK_ReportPlayersRelations", + table: "ReportPlayersRelations"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ReportPlayersRelations", + table: "ReportPlayersRelations", + columns: new[] { "ReportId", "UserId" }); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 86774e7d0..21a78424c 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1200,8 +1200,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ReportId") .HasColumnType("integer"); - b.Property("UserId") - .HasColumnType("text"); + b.Property("PlayerNumber") + .HasColumnType("integer"); b.Property("IsInGameNow") .HasColumnType("boolean"); @@ -1209,16 +1209,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsReporter") .HasColumnType("boolean"); - b.Property("PlayerNumber") - .HasColumnType("integer"); - b.Property("PlayerRect") + .IsRequired() .HasColumnType("text"); b.Property("Timestamp") .HasColumnType("timestamp with time zone"); - b.HasKey("ReportId", "UserId"); + b.Property("UserId") + .HasColumnType("text"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("ReportId", "PlayerNumber"); b.HasIndex("UserId"); @@ -1284,7 +1289,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("UniquePlayLevelRelations"); }); - modelBuilder.Entity("Refresh.Database.Models.Reports.GriefReport", b => + modelBuilder.Entity("Refresh.Database.Models.Reports.GameGriefReport", b => { b.Property("ReportId") .ValueGeneratedOnAdd() @@ -1332,6 +1337,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("ReviewedByUserId") + .IsRequired() .HasColumnType("text"); b.Property("ReviewedDate") @@ -1351,7 +1357,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ReviewedByUserId"); - b.ToTable("Reports"); + b.ToTable("GameGriefReports"); }); modelBuilder.Entity("Refresh.Database.Models.Statistics.GameLevelStatistics", b => @@ -2426,7 +2432,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Refresh.Database.Models.Relations.ReportPlayerRelation", b => { - b.HasOne("Refresh.Database.Models.Reports.GriefReport", "GriefReport") + b.HasOne("Refresh.Database.Models.Reports.GameGriefReport", "GameGriefReport") .WithMany("Players") .HasForeignKey("ReportId") .OnDelete(DeleteBehavior.Cascade) @@ -2434,11 +2440,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Refresh.Database.Models.Users.GameUser", "User") .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("UserId"); - b.Navigation("GriefReport"); + b.Navigation("GameGriefReport"); b.Navigation("User"); }); @@ -2500,7 +2504,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); - modelBuilder.Entity("Refresh.Database.Models.Reports.GriefReport", b => + modelBuilder.Entity("Refresh.Database.Models.Reports.GameGriefReport", b => { b.HasOne("Refresh.Database.Models.Levels.GameLevel", "Level") .WithMany() @@ -2514,7 +2518,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Refresh.Database.Models.Users.GameUser", "ReviewedBy") .WithMany() - .HasForeignKey("ReviewedByUserId"); + .HasForeignKey("ReviewedByUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); b.Navigation("Level"); @@ -2581,7 +2587,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Subjects"); }); - modelBuilder.Entity("Refresh.Database.Models.Reports.GriefReport", b => + modelBuilder.Entity("Refresh.Database.Models.Reports.GameGriefReport", b => { b.Navigation("Players"); }); From 922b4d5765468fed72ced8505d0bf83c0dbe7582 Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 13:35:57 +0200 Subject: [PATCH 11/21] Fix update migration SQL --- Refresh.Database/GameDatabaseContext.cs | 2 +- .../20261013102558_MiscGriefReportQualityUpdates.cs | 11 ++++++++++- .../Migrations/GameDatabaseContextModelSnapshot.cs | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Refresh.Database/GameDatabaseContext.cs b/Refresh.Database/GameDatabaseContext.cs index a611bb4e8..ecd71569c 100644 --- a/Refresh.Database/GameDatabaseContext.cs +++ b/Refresh.Database/GameDatabaseContext.cs @@ -81,7 +81,7 @@ public partial class GameDatabaseContext : DbContext, IDatabaseContext internal DbSet ProfilePinRelations { get; set; } internal DbSet GameSkillRewards { get; set; } internal DbSet GameGriefReports { get; set; } - internal DbSet ReportPlayersRelations { get; set; } + internal DbSet ReportPlayerRelations { get; set; } internal DbSet Workers { get; set; } internal DbSet JobStates { get; set; } internal DbSet GameLevelRevisions { get; set; } diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 2320c517d..0aba2e4bd 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -17,6 +17,11 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "Reports", newName: "GameGriefReports"); + // thank you for this typo smh + migrationBuilder.RenameTable( + name: "ReportPlayersRelations", + newName: "ReportPlayerRelations"); + migrationBuilder.AlterColumn( name: "PlayerRect", table: "ReportPlayersRelations", @@ -71,6 +76,10 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.RenameTable( name: "GameGriefReports", newName: "Reports"); + + migrationBuilder.RenameTable( + name: "ReportPlayerRelations", + newName: "ReportPlayersRelations"); migrationBuilder.AlterColumn( name: "UserId", @@ -99,7 +108,7 @@ protected override void Down(MigrationBuilder migrationBuilder) DELETE FROM "ReportPlayersRelations" WHERE "PlayerNumber" NOT IN ( SELECT "PlayerNumber" - FROM "DisallowedEmailDomains" + FROM "ReportPlayersRelations" PARTITION BY "ReportId" GROUP BY "UserId" ORDER BY "PlayerNumber" diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 21a78424c..2cc11d0e2 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1227,7 +1227,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("UserId"); - b.ToTable("ReportPlayersRelations"); + b.ToTable("ReportPlayerRelations"); }); modelBuilder.Entity("Refresh.Database.Models.Relations.SubPlaylistRelation", b => From ce54c7f182a6909757b17fc983c076ba4167a636 Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 13:47:42 +0200 Subject: [PATCH 12/21] Minor update SQL improvement --- .../20261013102558_MiscGriefReportQualityUpdates.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 0aba2e4bd..4a7f8e84e 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -54,7 +54,7 @@ protected override void Up(MigrationBuilder migrationBuilder) (""" UPDATE rpr SET rpr."PlayerNumber" = newNum FROM ( - SELECT "ReportPlayerRelations".*, row_number() + SELECT *, row_number() OVER (PARTITION BY "ReportId" ORDER BY "ReportId") newNum FROM "ReportPlayerRelations" ) rpr @@ -76,7 +76,7 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.RenameTable( name: "GameGriefReports", newName: "Reports"); - + migrationBuilder.RenameTable( name: "ReportPlayerRelations", newName: "ReportPlayersRelations"); From 0bbff60ebafdf03b3ea1bd82f82219d64d524669 Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 13:50:20 +0200 Subject: [PATCH 13/21] Forgot to fix the typo in the other migration operations --- ...20261013102558_MiscGriefReportQualityUpdates.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 4a7f8e84e..e16dfb923 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -24,7 +24,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.AlterColumn( name: "PlayerRect", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", type: "text", nullable: false, defaultValue: "", @@ -34,7 +34,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.AlterColumn( name: "UserId", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", type: "text", nullable: true, oldClrType: typeof(string), @@ -42,7 +42,7 @@ protected override void Up(MigrationBuilder migrationBuilder) migrationBuilder.AddColumn( name: "Username", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", type: "text", nullable: false, defaultValue: ""); @@ -61,12 +61,12 @@ protected override void Up(MigrationBuilder migrationBuilder) """); migrationBuilder.DropPrimaryKey( - name: "PK_ReportPlayersRelations", - table: "ReportPlayersRelations"); + name: "PK_ReportPlayerRelations", + table: "ReportPlayerRelations"); migrationBuilder.AddPrimaryKey( - name: "PK_ReportPlayersRelations", - table: "ReportPlayersRelations", + name: "PK_ReportPlayerRelations", + table: "ReportPlayerRelations", columns: new[] { "ReportId", "PlayerNumber" }); } From ce2487a20592d986a891396cb7e1bd970557f37d Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 14:17:04 +0200 Subject: [PATCH 14/21] Fix all ReportPlayerRelations.Timestamps being -infinity --- ...1013102558_MiscGriefReportQualityUpdates.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index e16dfb923..06dbb7a28 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -52,12 +52,12 @@ protected override void Up(MigrationBuilder migrationBuilder) // https://stackoverflow.com/questions/65231516/add-and-update-a-column-with-sequential-numbering-based-on-grouping migrationBuilder.Sql (""" - UPDATE rpr SET rpr."PlayerNumber" = newNum + UPDATE r SET r."PlayerNumber" = newNum FROM ( SELECT *, row_number() OVER (PARTITION BY "ReportId" ORDER BY "ReportId") newNum FROM "ReportPlayerRelations" - ) rpr + ) r """); migrationBuilder.DropPrimaryKey( @@ -68,6 +68,20 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "PK_ReportPlayerRelations", table: "ReportPlayerRelations", columns: new[] { "ReportId", "PlayerNumber" }); + + // For some reason, all relations timestamps were set to -infinity on production, + // so just set them to when the reports they belong to were created. + migrationBuilder.Sql + (""" + UPDATE "ReportPlayerRelations" SET "Timestamp" = u."ReportDate" + FROM ( + SELECT g."ReportDate", g."ReportId" + FROM "ReportPlayerRelations" as r + INNER JOIN "GameGriefReports" as g + ON r."ReportId" = g."ReportId" + ) AS u + WHERE "ReportPlayerRelations"."ReportId" = u."ReportId" AND u.Timestamp = '-infinity' + """); } /// From 2d106f5435e13891678acd9f7e4c289afabf542c Mon Sep 17 00:00:00 2001 From: Toaster Date: Tue, 13 Oct 2026 14:32:31 +0200 Subject: [PATCH 15/21] Drop ReportPlayerRelation.Timestamp because unused and never set --- ...013102558_MiscGriefReportQualityUpdates.cs | 14 -------- ...22831_DropReportPlayerRelationTimestamp.cs | 36 +++++++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 3 -- .../Models/Relations/ReportPlayerRelation.cs | 4 +-- 4 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 Refresh.Database/Migrations/20261013122831_DropReportPlayerRelationTimestamp.cs diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 06dbb7a28..9e5ab4d56 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -68,20 +68,6 @@ protected override void Up(MigrationBuilder migrationBuilder) name: "PK_ReportPlayerRelations", table: "ReportPlayerRelations", columns: new[] { "ReportId", "PlayerNumber" }); - - // For some reason, all relations timestamps were set to -infinity on production, - // so just set them to when the reports they belong to were created. - migrationBuilder.Sql - (""" - UPDATE "ReportPlayerRelations" SET "Timestamp" = u."ReportDate" - FROM ( - SELECT g."ReportDate", g."ReportId" - FROM "ReportPlayerRelations" as r - INNER JOIN "GameGriefReports" as g - ON r."ReportId" = g."ReportId" - ) AS u - WHERE "ReportPlayerRelations"."ReportId" = u."ReportId" AND u.Timestamp = '-infinity' - """); } /// diff --git a/Refresh.Database/Migrations/20261013122831_DropReportPlayerRelationTimestamp.cs b/Refresh.Database/Migrations/20261013122831_DropReportPlayerRelationTimestamp.cs new file mode 100644 index 000000000..bdc02d426 --- /dev/null +++ b/Refresh.Database/Migrations/20261013122831_DropReportPlayerRelationTimestamp.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20261013122831_DropReportPlayerRelationTimestamp")] + public partial class DropReportPlayerRelationTimestamp : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // This column had no setters at all, meaning all rows had this set to -infinity. + // The report uploads also don't actually contain any player-specific timestamps, + // so this is safe to drop. + migrationBuilder.DropColumn( + name: "Timestamp", + table: "ReportPlayerRelations"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Timestamp", + table: "ReportPlayerRelations", + type: "timestamp with time zone", + nullable: false, + defaultValue: new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero)); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 2cc11d0e2..22f116d0a 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1213,9 +1213,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired() .HasColumnType("text"); - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - b.Property("UserId") .HasColumnType("text"); diff --git a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs index 73c2556a4..bc3fa9e59 100644 --- a/Refresh.Database/Models/Relations/ReportPlayerRelation.cs +++ b/Refresh.Database/Models/Relations/ReportPlayerRelation.cs @@ -21,8 +21,6 @@ public partial class ReportPlayerRelation public bool IsInGameNow { get; set; } // i think something about whether the player left during the report, could rename it if confirmed public int PlayerNumber { get; set; } - // TODO this and photo subject bounds should be stored in a more atomized way + // TODO this and photo subject bounds should be stored in a more atomic way public string PlayerRect { get; set; } = string.Empty; // normalized "l,t,r,b" - - public DateTimeOffset Timestamp { get; set; } } \ No newline at end of file From 33c73a319a70967161fd3c18251d9ccbe7046a86 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 11:42:53 +0200 Subject: [PATCH 16/21] Accidentally made GameGriefReport.ReviewedByUserId required, undo --- ...18_MakeReviewedByUserIdNotRequiredAgain.cs | 62 +++++++++++++++++++ .../GameDatabaseContextModelSnapshot.cs | 5 +- .../Models/Reports/GameGriefReport.cs | 4 +- 3 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs diff --git a/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs new file mode 100644 index 000000000..3c829f8ce --- /dev/null +++ b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20261017093718_MakeReviewedByUserIdNotRequiredAgain")] + public partial class MakeReviewedByUserIdNotRequiredAgain : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", + table: "GameGriefReports"); + + migrationBuilder.AlterColumn( + name: "ReviewedByUserId", + table: "GameGriefReports", + type: "text", + nullable: true, + oldClrType: typeof(string), + oldType: "text"); + + migrationBuilder.AddForeignKey( + name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", + table: "GameGriefReports", + column: "ReviewedByUserId", + principalTable: "GameUsers", + principalColumn: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", + table: "GameGriefReports"); + + migrationBuilder.AlterColumn( + name: "ReviewedByUserId", + table: "GameGriefReports", + type: "text", + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "text", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", + table: "GameGriefReports", + column: "ReviewedByUserId", + principalTable: "GameUsers", + principalColumn: "UserId", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 22f116d0a..4ff17febb 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1334,7 +1334,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text"); b.Property("ReviewedByUserId") - .IsRequired() .HasColumnType("text"); b.Property("ReviewedDate") @@ -2515,9 +2514,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Refresh.Database.Models.Users.GameUser", "ReviewedBy") .WithMany() - .HasForeignKey("ReviewedByUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("ReviewedByUserId"); b.Navigation("Level"); diff --git a/Refresh.Database/Models/Reports/GameGriefReport.cs b/Refresh.Database/Models/Reports/GameGriefReport.cs index 6dcbf3d3d..1ca9b9ecb 100644 --- a/Refresh.Database/Models/Reports/GameGriefReport.cs +++ b/Refresh.Database/Models/Reports/GameGriefReport.cs @@ -36,9 +36,9 @@ public class GameGriefReport public string Description { get; set; } = string.Empty; // Moderation - [Required, ForeignKey(nameof(ReviewedByUserId))] + [ForeignKey(nameof(ReviewedByUserId))] public GameUser? ReviewedBy { get; set; } - [Required] public ObjectId? ReviewedByUserId { get; set; } + public ObjectId? ReviewedByUserId { get; set; } public DateTimeOffset? ReviewedDate { get; set; } public string? ModeratorNotes { get; set; } From 5bf8a723880f3d948f74354467c054af0dd74999 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 12:45:09 +0200 Subject: [PATCH 17/21] No unnecessary index bs, move table renames to end, drop PK before all UserId modifications, add PK after --- ...013102558_MiscGriefReportQualityUpdates.cs | 86 ++++++++++--------- ...18_MakeReviewedByUserIdNotRequiredAgain.cs | 23 ----- 2 files changed, 46 insertions(+), 63 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 9e5ab4d56..159b021e0 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -13,18 +13,9 @@ public partial class MiscGriefReportQualityUpdates : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.RenameTable( - name: "Reports", - newName: "GameGriefReports"); - - // thank you for this typo smh - migrationBuilder.RenameTable( - name: "ReportPlayersRelations", - newName: "ReportPlayerRelations"); - migrationBuilder.AlterColumn( name: "PlayerRect", - table: "ReportPlayerRelations", + table: "ReportPlayersRelations", type: "text", nullable: false, defaultValue: "", @@ -32,58 +23,65 @@ protected override void Up(MigrationBuilder migrationBuilder) oldType: "text", oldNullable: true); + migrationBuilder.AddColumn( + name: "Username", + table: "ReportPlayersRelations", + type: "text", + nullable: false, + defaultValue: ""); + + // ReportPlayerRelations PK changes (since we're altering UserId, which is part of PK at this point) + + migrationBuilder.DropPrimaryKey( + name: "PK_ReportPlayersRelations", + table: "ReportPlayersRelations"); + migrationBuilder.AlterColumn( name: "UserId", - table: "ReportPlayerRelations", + table: "ReportPlayersRelations", type: "text", nullable: true, oldClrType: typeof(string), oldType: "text"); - migrationBuilder.AddColumn( - name: "Username", - table: "ReportPlayerRelations", - type: "text", - nullable: false, - defaultValue: ""); - // (Re)init the player number column so we can ensure it only has unique values per report ID, // so we can safely use it to replace UserId in the primary key // https://stackoverflow.com/questions/65231516/add-and-update-a-column-with-sequential-numbering-based-on-grouping migrationBuilder.Sql (""" - UPDATE r SET r."PlayerNumber" = newNum + UPDATE "ReportPlayersRelations" SET "PlayerNumber" = newNum FROM ( SELECT *, row_number() OVER (PARTITION BY "ReportId" ORDER BY "ReportId") newNum - FROM "ReportPlayerRelations" - ) r + FROM "ReportPlayersRelations" + ) """); - migrationBuilder.DropPrimaryKey( - name: "PK_ReportPlayerRelations", - table: "ReportPlayerRelations"); - migrationBuilder.AddPrimaryKey( name: "PK_ReportPlayerRelations", - table: "ReportPlayerRelations", + table: "ReportPlayersRelations", columns: new[] { "ReportId", "PlayerNumber" }); + + // Doing this in the middle caused random issues with the following DB commands, + // so rename the tables after everything. + + migrationBuilder.RenameTable( + name: "Reports", + newName: "GameGriefReports"); + + // thank you for this typo smh + migrationBuilder.RenameTable( + name: "ReportPlayersRelations", + newName: "ReportPlayerRelations"); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.RenameTable( - name: "GameGriefReports", - newName: "Reports"); - - migrationBuilder.RenameTable( - name: "ReportPlayerRelations", - newName: "ReportPlayersRelations"); migrationBuilder.AlterColumn( name: "UserId", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", type: "text", nullable: false, defaultValue: "", @@ -93,7 +91,7 @@ protected override void Down(MigrationBuilder migrationBuilder) migrationBuilder.AlterColumn( name: "PlayerRect", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", type: "text", nullable: true, oldClrType: typeof(string), @@ -105,10 +103,10 @@ protected override void Down(MigrationBuilder migrationBuilder) // they're actually unique in the update (which we're coming from if we're downgrading). migrationBuilder.Sql (""" - DELETE FROM "ReportPlayersRelations" + DELETE FROM "ReportPlayerRelations" WHERE "PlayerNumber" NOT IN ( SELECT "PlayerNumber" - FROM "ReportPlayersRelations" + FROM "ReportPlayerRelations" PARTITION BY "ReportId" GROUP BY "UserId" ORDER BY "PlayerNumber" @@ -116,13 +114,21 @@ ORDER BY "PlayerNumber" """); migrationBuilder.DropPrimaryKey( - name: "PK_ReportPlayersRelations", - table: "ReportPlayersRelations"); + name: "PK_ReportPlayerRelations", + table: "ReportPlayerRelations"); migrationBuilder.AddPrimaryKey( name: "PK_ReportPlayersRelations", - table: "ReportPlayersRelations", + table: "ReportPlayerRelations", columns: new[] { "ReportId", "UserId" }); + + migrationBuilder.RenameTable( + name: "GameGriefReports", + newName: "Reports"); + + migrationBuilder.RenameTable( + name: "ReportPlayerRelations", + newName: "ReportPlayersRelations"); } } } diff --git a/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs index 3c829f8ce..c327e13b5 100644 --- a/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs +++ b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs @@ -13,10 +13,6 @@ public partial class MakeReviewedByUserIdNotRequiredAgain : Migration /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", - table: "GameGriefReports"); - migrationBuilder.AlterColumn( name: "ReviewedByUserId", table: "GameGriefReports", @@ -24,22 +20,11 @@ protected override void Up(MigrationBuilder migrationBuilder) nullable: true, oldClrType: typeof(string), oldType: "text"); - - migrationBuilder.AddForeignKey( - name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", - table: "GameGriefReports", - column: "ReviewedByUserId", - principalTable: "GameUsers", - principalColumn: "UserId"); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropForeignKey( - name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", - table: "GameGriefReports"); - migrationBuilder.AlterColumn( name: "ReviewedByUserId", table: "GameGriefReports", @@ -49,14 +34,6 @@ protected override void Down(MigrationBuilder migrationBuilder) oldClrType: typeof(string), oldType: "text", oldNullable: true); - - migrationBuilder.AddForeignKey( - name: "FK_GameGriefReports_GameUsers_ReviewedByUserId", - table: "GameGriefReports", - column: "ReviewedByUserId", - principalTable: "GameUsers", - principalColumn: "UserId", - onDelete: ReferentialAction.Cascade); } } } From 2022cd3d0161af006909f70cc7f5b97939f4bdb7 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 13:11:33 +0200 Subject: [PATCH 18/21] Fix my update SQL to actually update player nums --- .../20261013102558_MiscGriefReportQualityUpdates.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 159b021e0..388a27f9e 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -49,12 +49,16 @@ protected override void Up(MigrationBuilder migrationBuilder) // https://stackoverflow.com/questions/65231516/add-and-update-a-column-with-sequential-numbering-based-on-grouping migrationBuilder.Sql (""" - UPDATE "ReportPlayersRelations" SET "PlayerNumber" = newNum + UPDATE "ReportPlayersRelations" SET "PlayerNumber" = u.newNum FROM ( - SELECT *, row_number() - OVER (PARTITION BY "ReportId" ORDER BY "ReportId") newNum + SELECT *, row_number() OVER ( + PARTITION BY "ReportId" + ORDER BY "ReportId" + ) AS newNum FROM "ReportPlayersRelations" - ) + ) AS u + WHERE "ReportPlayersRelations"."ReportId" = u."ReportId" + AND "ReportPlayersRelations"."UserId" = u."UserId" """); migrationBuilder.AddPrimaryKey( From 105df3db65b5b7782c7fe5449ba207b08db95679 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 15:20:36 +0200 Subject: [PATCH 19/21] Fix ReportPlayerRelations rollback SQL --- ...013102558_MiscGriefReportQualityUpdates.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 388a27f9e..0b087de36 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -102,19 +102,21 @@ protected override void Down(MigrationBuilder migrationBuilder) oldType: "text"); // Just remove all with duplicate user IDs. Usually we won't have duplicate IDs anyway, - // and this should be enough for a rollback. Better than dropping the whole table or something. - // We can just use PlayerNumber to uniquely identify rows per ReportId, since we've already made sure - // they're actually unique in the update (which we're coming from if we're downgrading). + // but we should ensure we don't anyway; also, this should be enough for a rollback on this, + // better than dropping the whole table or something. + // We can just use PlayerNumber to uniquely identify rows per ReportId, that and ReportId + // are the only PK attributes after the update above. migrationBuilder.Sql (""" DELETE FROM "ReportPlayerRelations" - WHERE "PlayerNumber" NOT IN ( - SELECT "PlayerNumber" + USING ( + SELECT min ("PlayerNumber") as keepNumber, "ReportId", "UserId" FROM "ReportPlayerRelations" - PARTITION BY "ReportId" - GROUP BY "UserId" - ORDER BY "PlayerNumber" - ) + GROUP BY "UserId", "ReportId" + ) AS d + WHERE "PlayerNumber" != d.keepNumber + AND "ReportPlayerRelations"."ReportId" = d."ReportId" + AND "ReportPlayerRelations"."UserId" = d."UserId" """); migrationBuilder.DropPrimaryKey( From d6a2d20d816eaf9b2efa9c2cb5fbaa41ee50d973 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 15:20:58 +0200 Subject: [PATCH 20/21] Don't make ReviewedByUserId non-nullable again in the rollback --- ...61017093718_MakeReviewedByUserIdNotRequiredAgain.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs index c327e13b5..ef3c761f3 100644 --- a/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs +++ b/Refresh.Database/Migrations/20261017093718_MakeReviewedByUserIdNotRequiredAgain.cs @@ -25,15 +25,7 @@ protected override void Up(MigrationBuilder migrationBuilder) /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.AlterColumn( - name: "ReviewedByUserId", - table: "GameGriefReports", - type: "text", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true); + // Nothing needed, just leave it at null since we already want it like that, else SQL exception } } } From 8c47d3186eeb6e1fe0ddeeb8f50526d9937c5ed0 Mon Sep 17 00:00:00 2001 From: Toaster Date: Sat, 17 Oct 2026 15:27:13 +0200 Subject: [PATCH 21/21] Forgot to drop username on rollback --- .../20261013102558_MiscGriefReportQualityUpdates.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs index 0b087de36..0e21c2cd4 100644 --- a/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs +++ b/Refresh.Database/Migrations/20261013102558_MiscGriefReportQualityUpdates.cs @@ -100,6 +100,10 @@ protected override void Down(MigrationBuilder migrationBuilder) nullable: true, oldClrType: typeof(string), oldType: "text"); + + migrationBuilder.DropColumn( + name: "Username", + table: "ReportPlayerRelations"); // Just remove all with duplicate user IDs. Usually we won't have duplicate IDs anyway, // but we should ensure we don't anyway; also, this should be enough for a rollback on this,