Skip to content

Commit fc7278a

Browse files
authored
Implement level labels (#942)
This implements: - An enum containing hopefully all labels, aswell as some methods to (de)serialize them for the game - The ability for publishers to set up to 5 labels for their levels - The ability to filter levels by publisher labels in-game - Migrates review labels to be stored using `List<Label>` instead of `string` - Adds some label sanitization - When level statistics get recalculated, the 5 most recurring labels among a level's reviews get saved in its statistics to have them show under levels in-game aswell (the game differenciates between publisher and non-publisher labels there) - Deduplicates code used by both tags and labels - Fixes tags to show up on levels in-game again
2 parents 005e5a7 + 7f986a4 commit fc7278a

26 files changed

Lines changed: 746 additions & 41 deletions

Refresh.Common/Constants/UgcLimits.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ public static class UgcLimits
44
{
55
// Object limits
66
public const int MaximumLevels = 9_999;
7+
public const int MaximumLabels = 5;
78

89
// String limits
910
public const int TitleLimit = 64;

Refresh.Database/Extensions/LevelEnumerableExtensions.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ public static IQueryable<GameLevel> FilterByLevelFilterSettings(this IQueryable<
8888
// Filter out sub levels that weren't published by self
8989
levels = levels.Where(l => !l.IsSubLevel || l.Publisher == user);
9090

91+
// Filter by labels
92+
if (levelFilterSettings.Labels.Any())
93+
{
94+
levels = levels.Where(lvl => lvl.Labels.Any(lab => levelFilterSettings.Labels.Contains(lab)));
95+
}
96+
9197
return levels;
9298
}
9399

@@ -146,6 +152,12 @@ public static IEnumerable<GameLevel> FilterByLevelFilterSettings(this IEnumerabl
146152
// Filter out sub levels that weren't published by self
147153
levels = levels.Where(l => !l.IsSubLevel || l.Publisher == user);
148154

155+
// Filter by labels
156+
if (levelFilterSettings.Labels.Any())
157+
{
158+
levels = levels.Where(lvl => lvl.Labels.Any(lab => levelFilterSettings.Labels.Contains(lab)));
159+
}
160+
149161
return levels;
150162
}
151163
}

Refresh.Database/GameDatabaseContext.Levels.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public GameLevel AddLevel(ISerializedPublishLevel createInfo, TokenGame game, Ga
3838
IconHash = createInfo.IconHash,
3939
LocationX = createInfo.Location.X,
4040
LocationY = createInfo.Location.Y,
41+
Labels = createInfo.FinalPublisherLabels.ToList() ?? [],
4142
RootResource = createInfo.RootResource,
4243
IsLocked = createInfo.IsLocked,
4344
IsCopyable = createInfo.IsCopyable == 1,
@@ -191,6 +192,12 @@ public GameLevel UpdateLevel(ISerializedPublishLevel updateInfo, GameLevel level
191192
level.SameScreenGame = updateInfo.SameScreenGame;
192193
level.BackgroundGuid = updateInfo.BackgroundGuid;
193194

195+
// Only update labels if this level is updated in a game which supports them, to not lose the labels
196+
if (game is not TokenGame.LittleBigPlanet1 or TokenGame.LittleBigPlanetPSP)
197+
{
198+
level.Labels = updateInfo.FinalPublisherLabels.ToList();
199+
}
200+
194201
// If we're changing the actual contents of the level, update the game version and update date aswell
195202
if (updateInfo.RootResource != level.RootResource)
196203
{

Refresh.Database/GameDatabaseContext.Relations.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,20 @@ public void AddReviewToLevel(GameReview review, GameLevel level)
460460
review.Publisher.Statistics!.ReviewCount++;
461461
});
462462
}
463+
464+
public void MigrateReviewLabels(IEnumerable<GameReview> reviews)
465+
{
466+
foreach (GameReview review in reviews)
467+
{
468+
#pragma warning disable CS0618 // LabelsString is obsolete
469+
if (string.IsNullOrWhiteSpace(review.LabelsString)) continue;
470+
471+
review.Labels = LabelExtensions.FromLbpCommaList(review.LabelsString).ToList();
472+
#pragma warning restore CS0618
473+
}
474+
475+
this.SaveChanges();
476+
}
463477

464478
public void DeleteReviewsPostedByUser(GameUser user)
465479
{

Refresh.Database/GameDatabaseContext.Statistics.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Diagnostics;
22
using System.Diagnostics.CodeAnalysis;
3+
using Refresh.Common.Constants;
34
using Refresh.Database.Models.Comments;
45
using Refresh.Database.Models.Levels;
56
using Refresh.Database.Models.Playlists;
@@ -85,7 +86,7 @@ private void WriteEnsuringStatistics(GamePlaylist parent, GamePlaylist child, Ac
8586
}
8687

8788
#region Levels
88-
internal const int LevelStatisticsVersion = 3;
89+
internal const int LevelStatisticsVersion = 4;
8990

9091
public IEnumerable<GameLevel> GetLevelsWithStatisticsNeedingUpdates()
9192
{
@@ -168,6 +169,7 @@ private void RecalculateLevelStatisticsInternal(GameLevel level)
168169
level.Statistics.PhotoByPublisherCount = this.GetTotalPhotosInLevelByUser(level, level.Publisher);
169170
level.Statistics.ParentPlaylistCount = this.GetTotalPlaylistsContainingLevel(level);
170171
this.RecalculateLevelRatingStatisticsInternal(level);
172+
this.RecalculateLevelRecurringLabels(level);
171173

172174
level.Statistics.RecalculateAt = null;
173175
level.Statistics.Version = LevelStatisticsVersion;
@@ -186,6 +188,22 @@ private void RecalculateLevelRatingStatisticsInternal(GameLevel level)
186188
level.Statistics.Karma = level.Statistics.YayCount - level.Statistics.BooCount;
187189
}
188190

191+
private void RecalculateLevelRecurringLabels(GameLevel level)
192+
{
193+
Debug.Assert(level.Statistics != null);
194+
195+
// Take the most recurring labels among the reviews for the level
196+
level.Statistics.RecurringLabels = this.GameReviews
197+
.Where(r => r.LevelId == level.LevelId)
198+
.SelectMany(r => r.Labels)
199+
.GroupBy(r => r)
200+
.Select(g => new { Label = g.Key, Count = g.Count() })
201+
.OrderByDescending(g => g.Count)
202+
.Take(UgcLimits.MaximumLabels)
203+
.Select(g => g.Label)
204+
.ToList();
205+
}
206+
189207
private void MarkLevelStatisticsDirty(GameLevel level)
190208
{
191209
Debug.Assert(this.ChangeTracker.HasChanges(), "should be called in write (no changes detected)");
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20250831164448_AddLevelLabels")]
11+
public partial class AddLevelLabels : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
migrationBuilder.AddColumn<byte[]>(
17+
name: "RecurringLabels",
18+
table: "GameLevelStatistics",
19+
type: "smallint[]",
20+
nullable: false,
21+
defaultValue: new byte[0]);
22+
23+
migrationBuilder.AddColumn<byte[]>(
24+
name: "Labels",
25+
table: "GameLevels",
26+
type: "smallint[]",
27+
nullable: false,
28+
defaultValue: new byte[0]);
29+
}
30+
31+
/// <inheritdoc />
32+
protected override void Down(MigrationBuilder migrationBuilder)
33+
{
34+
migrationBuilder.DropColumn(
35+
name: "RecurringLabels",
36+
table: "GameLevelStatistics");
37+
38+
migrationBuilder.DropColumn(
39+
name: "Labels",
40+
table: "GameLevels");
41+
}
42+
}
43+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Microsoft.EntityFrameworkCore.Infrastructure;
2+
using Microsoft.EntityFrameworkCore.Migrations;
3+
4+
#nullable disable
5+
6+
namespace Refresh.Database.Migrations
7+
{
8+
/// <inheritdoc />
9+
[DbContext(typeof(GameDatabaseContext))]
10+
[Migration("20250902151307_AddReviewLabelList")]
11+
public partial class AddReviewLabelList : Migration
12+
{
13+
/// <inheritdoc />
14+
protected override void Up(MigrationBuilder migrationBuilder)
15+
{
16+
migrationBuilder.RenameColumn(
17+
name: "Labels",
18+
table: "GameReviews",
19+
newName: "LabelsString");
20+
21+
migrationBuilder.AddColumn<byte[]>(
22+
name: "Labels",
23+
table: "GameReviews",
24+
type: "smallint[]",
25+
nullable: false,
26+
defaultValue: new byte[0]);
27+
}
28+
29+
/// <inheritdoc />
30+
protected override void Down(MigrationBuilder migrationBuilder)
31+
{
32+
migrationBuilder.DropColumn(
33+
name: "Labels",
34+
table: "GameReviews");
35+
36+
migrationBuilder.RenameColumn(
37+
name: "LabelsString",
38+
table: "GameReviews",
39+
newName: "Labels");
40+
}
41+
}
42+
}

Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
1818
{
1919
#pragma warning disable 612, 618
2020
modelBuilder
21-
.HasAnnotation("ProductVersion", "9.0.7")
21+
.HasAnnotation("ProductVersion", "9.0.8")
2222
.HasAnnotation("Relational:MaxIdentifierLength", 63);
2323

2424
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -212,9 +212,14 @@ protected override void BuildModel(ModelBuilder modelBuilder)
212212
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("ReviewId"));
213213

214214
b.Property<string>("Content")
215+
.IsRequired()
215216
.HasColumnType("text");
216217

217-
b.Property<string>("Labels")
218+
b.PrimitiveCollection<byte[]>("Labels")
219+
.IsRequired()
220+
.HasColumnType("smallint[]");
221+
222+
b.Property<string>("LabelsString")
218223
.HasColumnType("text");
219224

220225
b.Property<int>("LevelId")
@@ -414,6 +419,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
414419
b.Property<bool>("IsSubLevel")
415420
.HasColumnType("boolean");
416421

422+
b.PrimitiveCollection<byte[]>("Labels")
423+
.IsRequired()
424+
.HasColumnType("smallint[]");
425+
417426
b.Property<byte>("LevelType")
418427
.HasColumnType("smallint");
419428

@@ -1294,6 +1303,10 @@ protected override void BuildModel(ModelBuilder modelBuilder)
12941303
b.Property<DateTimeOffset?>("RecalculateAt")
12951304
.HasColumnType("timestamp with time zone");
12961305

1306+
b.PrimitiveCollection<byte[]>("RecurringLabels")
1307+
.IsRequired()
1308+
.HasColumnType("smallint[]");
1309+
12971310
b.Property<int>("ReviewCount")
12981311
.HasColumnType("integer");
12991312

Refresh.Database/Models/Comments/GameReview.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ public partial class GameReview : ISequentialId
99
{
1010
[Key] public int ReviewId { get; set; }
1111

12-
[Required]
13-
public GameLevel Level { get; set; }
12+
[Required, ForeignKey(nameof(LevelId))] public GameLevel Level { get; set; }
13+
[Required] public int LevelId { get; set; }
1414

1515
[Required]
1616
public GameUser Publisher { get; set; }
17+
18+
#nullable enable
1719

1820
public DateTimeOffset PostedAt { get; set; }
1921

20-
public string Labels { get; set; }
21-
22-
public string Content { get; set; }
23-
22+
[Obsolete("Deprecated. This attribute only exists so BackfillReviewLabelsMigration could properly migrate labels at runtime.")]
23+
public string? LabelsString { get; set; }
24+
[Required] public List<Label> Labels { get; set; } = [];
25+
26+
public string Content { get; set; } = "";
27+
2428
[NotMapped] public int SequentialId
2529
{
2630
get => this.ReviewId;

Refresh.Database/Models/Levels/GameLevel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.Diagnostics;
2-
using System.Xml.Serialization;
32
using Refresh.Database.Models.Authentication;
4-
using Refresh.Database.Models.Comments;
53
using Refresh.Database.Models.Statistics;
64
using Refresh.Database.Models.Users;
75

@@ -22,6 +20,8 @@ public partial class GameLevel : ISequentialId
2220
public int LocationX { get; set; }
2321
public int LocationY { get; set; }
2422

23+
public List<Label> Labels { get; set; } = [];
24+
2525
public string RootResource { get; set; } = string.Empty;
2626

2727
/// <summary>

0 commit comments

Comments
 (0)