diff --git a/Refresh.Core/Extensions/GameAssetExtensions.cs b/Refresh.Core/Extensions/GameAssetExtensions.cs index 407a8a7c5..12cefc149 100644 --- a/Refresh.Core/Extensions/GameAssetExtensions.cs +++ b/Refresh.Core/Extensions/GameAssetExtensions.cs @@ -15,21 +15,6 @@ namespace Refresh.Core.Extensions; public static class GameAssetExtensions { - public static void TraverseDependenciesRecursively(this GameAsset asset, GameDatabaseContext database, Action callback) - { - callback(asset.AssetHash, asset); - foreach (string internalAssetHash in database.GetAssetDependencies(asset).ToArray()) - { - GameAsset? internalAsset = database.GetAssetFromHash(internalAssetHash); - - // Only run this if this is null, since the next recursion will trigger its own callback - if(internalAsset == null) - callback(internalAssetHash, internalAsset); - - internalAsset?.TraverseDependenciesRecursively(database, callback); - } - } - /// /// Automatically crops and resizes an image into its corresponding icon form. /// diff --git a/Refresh.Core/Services/CommandService.cs b/Refresh.Core/Services/CommandService.cs index a36146b59..8236dbfe2 100644 --- a/Refresh.Core/Services/CommandService.cs +++ b/Refresh.Core/Services/CommandService.cs @@ -100,6 +100,20 @@ public void HandleCommand(CommandInvocation command, GameDatabaseContext databas database.SetUnescapeXmlSequences(user, false); break; } + case "showmodp": + case "showmoddedplanet": + case "showmoddedplanets": + { + database.SetShowModdedPlanets(user, true); + break; + } + case "hidemodp": + case "hidemoddedplanet": + case "hidemoddedplanets": + { + database.SetShowModdedPlanets(user, false); + break; + } case "showmods": { database.SetShowModdedContent(user, true); diff --git a/Refresh.Database/Extensions/GameAssetExtensions.cs b/Refresh.Database/Extensions/GameAssetExtensions.cs new file mode 100644 index 000000000..0a4a1ec66 --- /dev/null +++ b/Refresh.Database/Extensions/GameAssetExtensions.cs @@ -0,0 +1,21 @@ +using Refresh.Database.Models.Assets; + +namespace Refresh.Database.Extensions; + +public static class GameAssetExtensions +{ + public static void TraverseDependenciesRecursively(this GameAsset asset, GameDatabaseContext database, Action callback) + { + callback(asset.AssetHash, asset); + foreach (string internalAssetHash in database.GetAssetDependencies(asset).ToArray()) + { + GameAsset? internalAsset = database.GetAssetFromHash(internalAssetHash); + + // Only run this if this is null, since the next recursion will trigger its own callback + if(internalAsset == null) + callback(internalAssetHash, internalAsset); + + internalAsset?.TraverseDependenciesRecursively(database, callback); + } + } +} \ No newline at end of file diff --git a/Refresh.Database/GameDatabaseContext.Users.cs b/Refresh.Database/GameDatabaseContext.Users.cs index 6ab2755e2..c9de39e20 100644 --- a/Refresh.Database/GameDatabaseContext.Users.cs +++ b/Refresh.Database/GameDatabaseContext.Users.cs @@ -8,6 +8,7 @@ using Refresh.Database.Models.Levels.Scores; using Refresh.Database.Models.Levels; using Refresh.Database.Models.Photos; +using Refresh.Database.Models.Assets; namespace Refresh.Database; @@ -111,16 +112,19 @@ public void UpdateUserData(GameUser user, ISerializedEditUser data, TokenGame ga { case TokenGame.LittleBigPlanet2: user.Lbp2PlanetsHash = data.PlanetsHash; - user.Lbp3PlanetsHash = data.PlanetsHash; + user.AreLbp2PlanetsModded = this.GetPlanetModdedStatus(data.PlanetsHash); break; case TokenGame.LittleBigPlanet3: user.Lbp3PlanetsHash = data.PlanetsHash; + user.AreLbp3PlanetsModded = this.GetPlanetModdedStatus(data.PlanetsHash); break; case TokenGame.LittleBigPlanetVita: user.VitaPlanetsHash = data.PlanetsHash; + user.AreVitaPlanetsModded = this.GetPlanetModdedStatus(data.PlanetsHash); break; case TokenGame.BetaBuild: user.BetaPlanetsHash = data.PlanetsHash; + user.AreBetaPlanetsModded = this.GetPlanetModdedStatus(data.PlanetsHash); break; } @@ -128,7 +132,6 @@ public void UpdateUserData(GameUser user, ISerializedEditUser data, TokenGame ga if (data.IconHash != null) switch (game) { - case TokenGame.LittleBigPlanet1: case TokenGame.LittleBigPlanet2: case TokenGame.LittleBigPlanet3: @@ -214,6 +217,9 @@ public void UpdateUserData(GameUser user, IApiEditUserRequest data) if (data.ProfileVisibility != null) user.ProfileVisibility = data.ProfileVisibility.Value; + + if (data.ShowModdedPlanets != null) + user.ShowModdedPlanets = data.ShowModdedPlanets.Value; if (data.ShowModdedContent != null) user.ShowModdedContent = data.ShowModdedContent.Value; @@ -226,6 +232,28 @@ public void UpdateUserData(GameUser user, IApiEditUserRequest data) }); } + public void UpdatePlanetModdedStatus(GameUser user) + { + user.AreLbp2PlanetsModded = this.GetPlanetModdedStatus(user.Lbp2PlanetsHash); + user.AreLbp3PlanetsModded = this.GetPlanetModdedStatus(user.Lbp3PlanetsHash); + user.AreVitaPlanetsModded = this.GetPlanetModdedStatus(user.VitaPlanetsHash); + user.AreBetaPlanetsModded = this.GetPlanetModdedStatus(user.BetaPlanetsHash); + } + + private bool GetPlanetModdedStatus(string rootAssetHash) + { + bool modded = false; + + GameAsset? rootAsset = this.GetAssetFromHash(rootAssetHash); + rootAsset?.TraverseDependenciesRecursively(this, (_, asset) => + { + if (asset != null && (asset.AssetFlags & (AssetFlags.Modded | AssetFlags.ModdedOnPlanets)) != 0) + modded = true; + }); + + return modded; + } + [Pure] public int GetTotalUserCount() => this.GameUsers.Count(); @@ -370,12 +398,15 @@ public void FullyDeleteUser(GameUser user) public void ResetUserPlanets(GameUser user) { - this.Write(() => - { - user.Lbp2PlanetsHash = "0"; - user.Lbp3PlanetsHash = "0"; - user.VitaPlanetsHash = "0"; - }); + user.Lbp2PlanetsHash = "0"; + user.Lbp3PlanetsHash = "0"; + user.VitaPlanetsHash = "0"; + user.BetaPlanetsHash = "0"; + user.AreLbp2PlanetsModded = false; + user.AreLbp3PlanetsModded = false; + user.AreVitaPlanetsModded = false; + user.AreBetaPlanetsModded = false; + this.SaveChanges(); } public void SetUnescapeXmlSequences(GameUser user, bool value) @@ -386,6 +417,12 @@ public void SetUnescapeXmlSequences(GameUser user, bool value) }); } + public void SetShowModdedPlanets(GameUser user, bool value) + { + user.ShowModdedPlanets = value; + this.SaveChanges(); + } + public void SetShowModdedContent(GameUser user, bool value) { this.Write(() => diff --git a/Refresh.Database/Migrations/20251020102449_TrackModdedPlanets.cs b/Refresh.Database/Migrations/20251020102449_TrackModdedPlanets.cs new file mode 100644 index 000000000..773cf9f74 --- /dev/null +++ b/Refresh.Database/Migrations/20251020102449_TrackModdedPlanets.cs @@ -0,0 +1,76 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Refresh.Database.Migrations +{ + /// + [DbContext(typeof(GameDatabaseContext))] + [Migration("20251020102449_TrackModdedPlanets")] + public partial class TrackModdedPlanets : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "AreBetaPlanetsModded", + table: "GameUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "AreLbp2PlanetsModded", + table: "GameUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "AreLbp3PlanetsModded", + table: "GameUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "AreVitaPlanetsModded", + table: "GameUsers", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "ShowModdedPlanets", + table: "GameUsers", + type: "boolean", + nullable: false, + defaultValue: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "AreBetaPlanetsModded", + table: "GameUsers"); + + migrationBuilder.DropColumn( + name: "AreLbp2PlanetsModded", + table: "GameUsers"); + + migrationBuilder.DropColumn( + name: "AreLbp3PlanetsModded", + table: "GameUsers"); + + migrationBuilder.DropColumn( + name: "AreVitaPlanetsModded", + table: "GameUsers"); + + migrationBuilder.DropColumn( + name: "ShowModdedPlanets", + table: "GameUsers"); + } + } +} diff --git a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs index 67bf12bf8..88a67cdbc 100644 --- a/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs +++ b/Refresh.Database/Migrations/GameDatabaseContextModelSnapshot.cs @@ -1488,6 +1488,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("AllowIpAuthentication") .HasColumnType("boolean"); + b.Property("AreBetaPlanetsModded") + .HasColumnType("boolean"); + + b.Property("AreLbp2PlanetsModded") + .HasColumnType("boolean"); + + b.Property("AreLbp3PlanetsModded") + .HasColumnType("boolean"); + + b.Property("AreVitaPlanetsModded") + .HasColumnType("boolean"); + b.Property("BanExpiryDate") .HasColumnType("timestamp with time zone"); @@ -1584,6 +1596,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ShowModdedContent") .HasColumnType("boolean"); + b.Property("ShowModdedPlanets") + .HasColumnType("boolean"); + b.Property("ShowReuploadedContent") .HasColumnType("boolean"); diff --git a/Refresh.Database/Models/Assets/AssetFlags.cs b/Refresh.Database/Models/Assets/AssetFlags.cs index c568479e0..fa4b1a9b9 100644 --- a/Refresh.Database/Models/Assets/AssetFlags.cs +++ b/Refresh.Database/Models/Assets/AssetFlags.cs @@ -16,6 +16,10 @@ public enum AssetFlags /// This asset will only ever be created by mods. /// Modded = 1 << 2, + /// + /// A planet is considered modded if it depends on this asset, or if the asset already has the Modded flag above. + /// + ModdedOnPlanets = 1 << 3, } public static class AssetSafetyLevelExtensions @@ -26,29 +30,30 @@ public static AssetFlags FromAssetType(GameAssetType type, GameAssetFormat? meth { // Common asset types created by the game GameAssetType.Level => AssetFlags.None, - GameAssetType.StreamingLevelChunk => AssetFlags.None, + GameAssetType.StreamingLevelChunk => AssetFlags.None | AssetFlags.ModdedOnPlanets, GameAssetType.Plan => AssetFlags.None, - GameAssetType.ThingRecording => AssetFlags.None, - GameAssetType.SyncedProfile => AssetFlags.None, - GameAssetType.GriefSongState => AssetFlags.None, - GameAssetType.Quest => AssetFlags.None, - GameAssetType.AdventureSharedData => AssetFlags.None, - GameAssetType.AdventureCreateProfile => AssetFlags.None, - GameAssetType.ChallengeGhost => AssetFlags.None, + GameAssetType.ThingRecording => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.SyncedProfile => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.GriefSongState => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.Quest => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.AdventureSharedData => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.AdventureCreateProfile => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.ChallengeGhost => AssetFlags.None | AssetFlags.ModdedOnPlanets, // Common media types created by the game - GameAssetType.VoiceRecording => AssetFlags.Media, + GameAssetType.VoiceRecording => AssetFlags.Media | AssetFlags.ModdedOnPlanets, GameAssetType.Painting => AssetFlags.Media, GameAssetType.Texture => AssetFlags.Media, GameAssetType.Jpeg => AssetFlags.Media, GameAssetType.Png => AssetFlags.Media, - GameAssetType.Tga => AssetFlags.Media, - GameAssetType.Mip => AssetFlags.Media, + GameAssetType.Tga => AssetFlags.Media | AssetFlags.ModdedOnPlanets, + GameAssetType.Mip => AssetFlags.Media | AssetFlags.ModdedOnPlanets, - // Uncommon, but still vanilla assets created by the game in niche scenarios - GameAssetType.GfxMaterial => AssetFlags.Media, // while not image/audio data like the other media types, this is marked as media because this file can contain full PS3 shaders - GameAssetType.Material => AssetFlags.None, - GameAssetType.Bevel => AssetFlags.None, + // Uncommon, but still vanilla assets created by the game in niche scenarios. + // While not image/audio data like the other media types, GfxMaterial is marked as media because this file can contain full PS3 shaders. + GameAssetType.GfxMaterial => AssetFlags.Media | AssetFlags.ModdedOnPlanets, + GameAssetType.Material => AssetFlags.None | AssetFlags.ModdedOnPlanets, + GameAssetType.Bevel => AssetFlags.None | AssetFlags.ModdedOnPlanets, // Modded media types GameAssetType.GameDataTexture => AssetFlags.Media | AssetFlags.Modded, diff --git a/Refresh.Database/Models/Users/GameUser.cs b/Refresh.Database/Models/Users/GameUser.cs index 2ccc18d25..e2ffb1314 100644 --- a/Refresh.Database/Models/Users/GameUser.cs +++ b/Refresh.Database/Models/Users/GameUser.cs @@ -78,6 +78,11 @@ public partial class GameUser : IRateLimitUser public string Lbp3PlanetsHash { get; set; } = "0"; public string VitaPlanetsHash { get; set; } = "0"; + public bool AreBetaPlanetsModded { get; set; } + public bool AreLbp2PlanetsModded { get; set; } + public bool AreLbp3PlanetsModded { get; set; } + public bool AreVitaPlanetsModded { get; set; } + public string YayFaceHash { get; set; } = "0"; public string BooFaceHash { get; set; } = "0"; public string MehFaceHash { get; set; } = "0"; @@ -119,6 +124,11 @@ public partial class GameUser : IRateLimitUser public GameUserRole Role { get; set; } + /// + /// Whether planets containing mods or VoiceRecordings should be shown in-game + /// + public bool ShowModdedPlanets { get; set; } = true; + /// /// Whether modded content should be shown in level listings /// diff --git a/Refresh.Database/Query/IApiEditUserRequest.cs b/Refresh.Database/Query/IApiEditUserRequest.cs index aef959497..21ff0ca2b 100644 --- a/Refresh.Database/Query/IApiEditUserRequest.cs +++ b/Refresh.Database/Query/IApiEditUserRequest.cs @@ -14,6 +14,7 @@ public interface IApiEditUserRequest bool? RedirectGriefReportsToPhotos { get; set; } bool? UnescapeXmlSequences { get; set; } string? EmailAddress { get; set; } + bool? ShowModdedPlanets { get; set; } bool? ShowModdedContent { get; set; } bool? ShowReuploadedContent { get; set; } Visibility? LevelVisibility { get; set; } diff --git a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs index 01370ef50..0bfb0b2a3 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/Admin/AdminUserApiEndpoints.cs @@ -107,6 +107,11 @@ public ApiResponse GetUserPlanetsByUuid(RequestCont Lbp2PlanetsHash = user.Lbp2PlanetsHash, Lbp3PlanetsHash = user.Lbp3PlanetsHash, VitaPlanetsHash = user.VitaPlanetsHash, + BetaPlanetsHash = user.BetaPlanetsHash, + AreLbp2PlanetsModded = user.AreLbp2PlanetsModded, + AreLbp3PlanetsModded = user.AreLbp3PlanetsModded, + AreVitaPlanetsModded = user.AreVitaPlanetsModded, + AreBetaPlanetsModded = user.AreBetaPlanetsModded, }; } @@ -123,6 +128,11 @@ public ApiResponse GetUserPlanetsByUsername(Request Lbp2PlanetsHash = user.Lbp2PlanetsHash, Lbp3PlanetsHash = user.Lbp3PlanetsHash, VitaPlanetsHash = user.VitaPlanetsHash, + BetaPlanetsHash = user.BetaPlanetsHash, + AreLbp2PlanetsModded = user.AreLbp2PlanetsModded, + AreLbp3PlanetsModded = user.AreLbp3PlanetsModded, + AreVitaPlanetsModded = user.AreVitaPlanetsModded, + AreBetaPlanetsModded = user.AreBetaPlanetsModded, }; } diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiUpdateUserRequest.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiUpdateUserRequest.cs index dbe7b821e..7d1f7a5de 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiUpdateUserRequest.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Request/ApiUpdateUserRequest.cs @@ -21,6 +21,7 @@ public class ApiUpdateUserRequest : IApiEditUserRequest public string? EmailAddress { get; set; } + public bool? ShowModdedPlanets { get; set; } public bool? ShowModdedContent { get; set; } public bool? ShowReuploadedContent { get; set; } diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Admin/ApiAdminUserPlanetsResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Admin/ApiAdminUserPlanetsResponse.cs index 11eb9e796..53c93bc5d 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Admin/ApiAdminUserPlanetsResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Admin/ApiAdminUserPlanetsResponse.cs @@ -6,4 +6,10 @@ public class ApiAdminUserPlanetsResponse : IApiResponse public string Lbp2PlanetsHash { get; set; } = "0"; public string Lbp3PlanetsHash { get; set; } = "0"; public string VitaPlanetsHash { get; set; } = "0"; + public string BetaPlanetsHash { get; set; } = "0"; + + public bool AreLbp2PlanetsModded { get; set; } + public bool AreLbp3PlanetsModded { get; set; } + public bool AreVitaPlanetsModded { get; set; } + public bool AreBetaPlanetsModded { get; set; } } \ No newline at end of file diff --git a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs index f0cfcf71c..dbad276a0 100644 --- a/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs +++ b/Refresh.Interfaces.APIv3/Endpoints/DataTypes/Response/Users/ApiExtendedGameUserResponse.cs @@ -27,6 +27,7 @@ public class ApiExtendedGameUserResponse : ApiGameUserResponse, IApiResponse, ID public required bool ShouldResetPassword { get; set; } public required bool AllowIpAuthentication { get; set; } + public required bool ShowModdedPlanets { get; set; } public required bool ShowModdedContent { get; set; } public required bool ShowReuploadedContent { get; set; } @@ -71,6 +72,7 @@ public class ApiExtendedGameUserResponse : ApiGameUserResponse, IApiResponse, ID ActiveRoom = ApiGameRoomResponse.FromOld(dataContext.Match.RoomAccessor.GetRoomByUser(user), dataContext), LevelVisibility = user.LevelVisibility, ProfileVisibility = user.ProfileVisibility, + ShowModdedPlanets = user.ShowModdedPlanets, ShowModdedContent = user.ShowModdedContent, ShowReuploadedContent = user.ShowReuploadedContent, ConnectedToPresenceServer = user.PresenceServerAuthToken != null, diff --git a/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameUserResponse.cs b/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameUserResponse.cs index 3ee6695a7..66542903f 100644 --- a/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameUserResponse.cs +++ b/Refresh.Interfaces.Game/Endpoints/DataTypes/Response/GameUserResponse.cs @@ -127,14 +127,12 @@ public class GameUserResponse : IDataConvertableFrom response.PlanetsHash = dataContext.Game switch { - TokenGame.LittleBigPlanet1 => "0", - TokenGame.LittleBigPlanet2 => old.Lbp2PlanetsHash, - TokenGame.LittleBigPlanet3 => old.Lbp3PlanetsHash, - TokenGame.LittleBigPlanetVita => old.VitaPlanetsHash, - TokenGame.LittleBigPlanetPSP => "0", - TokenGame.Website => "0", - TokenGame.BetaBuild => old.BetaPlanetsHash, - _ => throw new ArgumentOutOfRangeException(nameof(dataContext.Game), dataContext.Game, null), + // Hide modded planets if the requesting user doesn't want them to be shown, and the requested user is not the requesting user + TokenGame.LittleBigPlanet2 => !old.AreLbp2PlanetsModded || ShowModdedPlanet(dataContext.User!, old) ? old.Lbp2PlanetsHash : "0" , + TokenGame.LittleBigPlanet3 => !old.AreLbp3PlanetsModded || ShowModdedPlanet(dataContext.User!, old) ? old.Lbp3PlanetsHash : "0" , + TokenGame.LittleBigPlanetVita => !old.AreVitaPlanetsModded || ShowModdedPlanet(dataContext.User!, old) ? old.VitaPlanetsHash : "0", + TokenGame.BetaBuild => !old.AreBetaPlanetsModded || ShowModdedPlanet(dataContext.User!, old) ? old.BetaPlanetsHash : "0", + _ => "0", }; // Fill out slot usage information @@ -212,5 +210,8 @@ public class GameUserResponse : IDataConvertableFrom return response; } + private static bool ShowModdedPlanet(GameUser requestingUser, GameUser requestedUser) + => requestingUser.ShowModdedPlanets || requestingUser.UserId == requestedUser.UserId; + public static IEnumerable FromOldList(IEnumerable oldList, DataContext dataContext) => oldList.Select(old => FromOld(old, dataContext)).ToList()!; } \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs b/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs new file mode 100644 index 000000000..cb95d8d1d --- /dev/null +++ b/Refresh.Interfaces.Workers/Migrations/BackfillModdedPlanetFlagsMigration.cs @@ -0,0 +1,28 @@ +using Refresh.Database.Extensions; +using Refresh.Database.Models.Users; +using Refresh.Workers; + +namespace Refresh.Interfaces.Workers.Migrations; + +public class BackfillModdedPlanetFlagsMigration : MigrationJob +{ + protected override IQueryable SortAndFilter(IQueryable query) + { + return query + .OrderBy(u => u.UserId) + .Where(u => u.Lbp2PlanetsHash != "0" + || u.Lbp3PlanetsHash != "0" + || u.VitaPlanetsHash != "0" + || u.BetaPlanetsHash != "0"); + } + + protected override void Migrate(WorkContext context, GameUser[] batch) + { + foreach (GameUser user in batch) + { + context.Database.UpdatePlanetModdedStatus(user); + } + + context.Database.SaveChanges(); + } +} \ No newline at end of file diff --git a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs index 3c0407185..cf181524e 100644 --- a/Refresh.Interfaces.Workers/RefreshWorkerManager.cs +++ b/Refresh.Interfaces.Workers/RefreshWorkerManager.cs @@ -24,6 +24,7 @@ public static WorkerManager Create(Logger logger, IDataStore dataStore, GameData manager.AddJob(); manager.AddJob(); manager.AddJob(); + manager.AddJob(); return manager; } diff --git a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs index 3df913625..1b4ee33af 100644 --- a/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs +++ b/RefreshTests.GameServer/Tests/ApiV3/UserApiTests.cs @@ -163,6 +163,35 @@ public void CanPatchOwnUser() Assert.That(user.Description, Is.EqualTo(description)); } + [Test] + public void UpdateShowModdedPlanets() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Api, user); + + // Disable and ensure the option is set to hide + ApiUpdateUserRequest payload = new() + { + ShowModdedPlanets = false, + }; + ApiResponse? response = client.PatchData("/api/v3/users/me", payload); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Data!.ShowModdedPlanets, Is.False); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByObjectId(user.UserId)!.ShowModdedPlanets, Is.False); + + // Enable again and ensure the option is set to show + payload.ShowModdedPlanets = true; + response = client.PatchData("/api/v3/users/me", payload); + Assert.That(response, Is.Not.Null); + Assert.That(response!.Data!.ShowModdedPlanets, Is.True); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByObjectId(user.UserId)!.ShowModdedPlanets, Is.True); + } + [Test] public void UserDescriptionGetsTrimmed() { diff --git a/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs b/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs new file mode 100644 index 000000000..351fa696c --- /dev/null +++ b/RefreshTests.GameServer/Tests/Planets/ModdedPlanetsTests.cs @@ -0,0 +1,255 @@ +using Refresh.Core.Configuration; +using Refresh.Database.Models.Assets; +using Refresh.Database.Models.Authentication; +using Refresh.Database.Models.Users; +using Refresh.Interfaces.Game.Endpoints.DataTypes.Response; +using Refresh.Interfaces.Game.Types.UserData; +using RefreshTests.GameServer.Extensions; + +namespace RefreshTests.GameServer.Tests.Planets; + +public class ModdedPlanetsTests : GameServerTest +{ + private const string TEST_LEVEL_HASH = "acddf3f9251c1ddb675ad81ba34ba16135b54aca"; + private const string TEST_LEVEL_HASH_2 = "59b250969f6b0b05ea352e4b7efa597efa0f7d21"; + private const string TEST_VOIP_HASH = "148c07876f15ef9ab90cc93e4900daa003214ae7"; + private const string TEST_TEXTURE_HASH = "9488801db61c5313db3bb15db7d66fd26df7e789"; + private const string TEST_MESH_HASH = "ca238f89938fe835049eaa72e79157dc9e292ad8"; + + private void UploadUnmoddedPlanet(TestContext context, HttpClient client, string rootAssetHash) + { + // Upload planet dependency and make the planet level depend on the texture + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_TEXTURE_HASH}", new ReadOnlyMemoryContent("TEX "u8.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + context.Database.AddOrOverwriteAssetDependencyRelations(rootAssetHash, [TEST_TEXTURE_HASH]); + + // Update user in-game + SerializedUpdateDataProfile request = new() + { + PlanetsHash = rootAssetHash, + }; + + message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + } + + private void UploadLightlyModdedPlanet(TestContext context, HttpClient client, string rootAssetHash) + { + // Upload planet dependency and make the planet level depend on the voice recording + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_VOIP_HASH}", new ReadOnlyMemoryContent("VOPb"u8.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + context.Database.AddOrOverwriteAssetDependencyRelations(rootAssetHash, [TEST_VOIP_HASH]); + + // Update user in-game + SerializedUpdateDataProfile request = new() + { + PlanetsHash = rootAssetHash, + }; + + message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + } + + private void UploadHeavilyModdedPlanet(TestContext context, HttpClient client, string rootAssetHash) + { + // Prepare config so normal users may upload modded assets + GameServerConfig config = context.Server.Value.GameServerConfig; + config.BlockedAssetFlags = new(AssetFlags.Dangerous); + + // Upload planet dependency and make the planet level depend on the voice recording + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_MESH_HASH}", new ReadOnlyMemoryContent("MSHb"u8.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + context.Database.AddOrOverwriteAssetDependencyRelations(rootAssetHash, [TEST_MESH_HASH]); + + context.Database.Refresh(); + + // Update user in-game + SerializedUpdateDataProfile request = new() + { + PlanetsHash = rootAssetHash, + }; + + message = client.PostAsync($"/lbp/updateUser", new StringContent(request.AsXML())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ShowUnmoddedPlanets(bool showModdedPlanets) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.SetShowModdedPlanets(user, showModdedPlanets); + using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + + GameUser publisher = context.CreateUser(); + using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, publisher); + + // Upload root planet asset + HttpResponseMessage message = client2.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result; + this.UploadUnmoddedPlanet(context, client2, TEST_LEVEL_HASH); + context.Database.Refresh(); + + // Ensure the planet has been correctly flagged as unmodded + Assert.That(context.Database.GetUserByObjectId(publisher.UserId)!.AreLbp2PlanetsModded, Is.False); + + // Now get the publisher's user data as the other user + message = client1.GetAsync($"/lbp/user/{publisher.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + GameUserResponse response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo(TEST_LEVEL_HASH)); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ShowOrHideLightlyModdedPlanets(bool showModdedPlanets) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.SetShowModdedPlanets(user, showModdedPlanets); + using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + + GameUser publisher = context.CreateUser(); + using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, publisher); + + // Upload root planet asset + HttpResponseMessage message = client2.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result; + this.UploadLightlyModdedPlanet(context, client2, TEST_LEVEL_HASH); + context.Database.Refresh(); + + // Ensure the planet has been correctly flagged as modded + Assert.That(context.Database.GetUserByObjectId(publisher.UserId)!.AreLbp2PlanetsModded, Is.True); + + // Now get the publisher's user data as the other user + message = client1.GetAsync($"/lbp/user/{publisher.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + GameUserResponse response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo(showModdedPlanets ? TEST_LEVEL_HASH : "0")); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ShowOrHideHeavilyModdedPlanets(bool showModdedPlanets) + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.SetShowModdedPlanets(user, showModdedPlanets); + using HttpClient client1 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + + GameUser publisher = context.CreateUser(); + using HttpClient client2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, publisher); + + // Upload root planet asset + HttpResponseMessage message = client2.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result; + this.UploadHeavilyModdedPlanet(context, client2, TEST_LEVEL_HASH); + context.Database.Refresh(); + + // Ensure the planet has been correctly flagged as modded + Assert.That(context.Database.GetUserByObjectId(publisher.UserId)!.AreLbp2PlanetsModded, Is.True); + + // Now get the publisher's user data as the other user + message = client1.GetAsync($"/lbp/user/{publisher.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + GameUserResponse response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo(showModdedPlanets ? TEST_LEVEL_HASH : "0")); + } + + [Test] + public void ModdedLbp2PlanetDoesntHideUnmoddedVitaPlanet() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.SetShowModdedPlanets(user, false); + using HttpClient client1Lbp2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + using HttpClient client1Vita = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetVita, TokenPlatform.Vita, out string _, user); + + // Prepare config so normal users may upload modded assets + GameServerConfig config = context.Server.Value.GameServerConfig; + config.BlockedAssetFlags = new(AssetFlags.Dangerous); + + GameUser publisher = context.CreateUser(); + using HttpClient client2LBP2 = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, publisher); + using HttpClient client2Vita = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanetVita, TokenPlatform.Vita, out string _, publisher); + + // Upload root planet assets + HttpResponseMessage message = client2Vita.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + message = client2LBP2.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH_2}", new ReadOnlyMemoryContent("LVLblol"u8.ToArray())).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + this.UploadUnmoddedPlanet(context, client2Vita, TEST_LEVEL_HASH); + this.UploadLightlyModdedPlanet(context, client2LBP2, TEST_LEVEL_HASH_2); + context.Database.Refresh(); + + // Ensure the LBP2 planet has been correctly flagged as modded, and Vita as unmodded + GameUser updated = context.Database.GetUserByObjectId(publisher.UserId)!; + Assert.That(updated.AreLbp2PlanetsModded, Is.True); + Assert.That(updated.AreVitaPlanetsModded, Is.False); + + // Now get the publisher's user data from various games + message = client1Lbp2.GetAsync($"/lbp/user/{publisher.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + GameUserResponse response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo("0")); + + message = client1Vita.GetAsync($"/lbp/user/{publisher.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo(TEST_LEVEL_HASH)); + } + + [Test] + public void UpdateShowModdedPlanetsIngame() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.VerifyUserEmail(user); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + + // Disable and ensure the option is set to hide + HttpResponseMessage message = client.PostAsync("/lbp/filter", new StringContent("/hidemodp")).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByObjectId(user.UserId)!.ShowModdedPlanets, Is.False); + + // Enable again and ensure the option is set to show + message = client.PostAsync("/lbp/filter", new StringContent("/showmodp")).Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + context.Database.Refresh(); + Assert.That(context.Database.GetUserByObjectId(user.UserId)!.ShowModdedPlanets, Is.True); + } + + [Test] + public void DontHideOwnModdedPlanets() + { + using TestContext context = this.GetServer(); + GameUser user = context.CreateUser(); + context.Database.SetShowModdedPlanets(user, false); + using HttpClient client = context.GetAuthenticatedClient(TokenType.Game, TokenGame.LittleBigPlanet2, TokenPlatform.PS3, out string _, user); + + // Upload root planet asset + HttpResponseMessage message = client.PostAsync($"/lbp/upload/{TEST_LEVEL_HASH}", new ReadOnlyMemoryContent("LVLb"u8.ToArray())).Result; + this.UploadHeavilyModdedPlanet(context, client, TEST_LEVEL_HASH); + context.Database.Refresh(); + + // Ensure the planet has been correctly flagged as modded + Assert.That(context.Database.GetUserByObjectId(user.UserId)!.AreLbp2PlanetsModded, Is.True); + + // Now get the publisher's user data as the other user + message = client.GetAsync($"/lbp/user/{user.Username}").Result; + Assert.That(message.StatusCode, Is.EqualTo(OK)); + + // Ensure the planet hash attribute is set to the modded planet independently of the user's preference + GameUserResponse response = message.Content.ReadAsXML(); + Assert.That(response.PlanetsHash, Is.EqualTo(TEST_LEVEL_HASH)); + } +} \ No newline at end of file