diff --git a/Refresh.Common/Refresh.Common.csproj b/Refresh.Common/Refresh.Common.csproj index ce073fe9..40fd66e7 100644 --- a/Refresh.Common/Refresh.Common.csproj +++ b/Refresh.Common/Refresh.Common.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs b/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs index b9df97f8..be7540e3 100644 --- a/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs +++ b/Refresh.GameServer/Database/GameDatabaseContext.Levels.cs @@ -351,6 +351,12 @@ public DatabaseList GetCoolLevels(int count, int skip, GameUser? user .Where(l => l.Score > 0) .OrderByDescending(l => l.Score), skip, count); + [Pure] + public DatabaseList GetAdventureLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings) => + new(this.GetLevelsByGameVersion(levelFilterSettings.GameVersion) + .FilterByLevelFilterSettings(user, levelFilterSettings) + .Where(l => l.IsAdventure), skip, count); + [Pure] public DatabaseList SearchForLevels(int count, int skip, GameUser? user, LevelFilterSettings levelFilterSettings, string query) { diff --git a/Refresh.GameServer/Database/GameDatabaseProvider.cs b/Refresh.GameServer/Database/GameDatabaseProvider.cs index deebbdc5..336023c2 100644 --- a/Refresh.GameServer/Database/GameDatabaseProvider.cs +++ b/Refresh.GameServer/Database/GameDatabaseProvider.cs @@ -33,7 +33,7 @@ protected GameDatabaseProvider(IDateTimeProvider time) this._time = time; } - protected override ulong SchemaVersion => 137; + protected override ulong SchemaVersion => 138; protected override string Filename => "refreshGameServer.realm"; @@ -310,6 +310,12 @@ protected override void Migrate(Migration migration, ulong oldVersion) else newLevel.DateTeamPicked = null; } + + // In version 138 we added support for Adventures in LBP3. Set their status to false by default. + if (oldVersion < 138) + { + newLevel.IsAdventure = false; + } } // In version 22, tokens added expiry and types so just wipe them all diff --git a/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Levels/ApiGameLevelResponse.cs b/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Levels/ApiGameLevelResponse.cs index ca9fd94e..84339da2 100644 --- a/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Levels/ApiGameLevelResponse.cs +++ b/Refresh.GameServer/Endpoints/ApiV3/DataTypes/Response/Levels/ApiGameLevelResponse.cs @@ -16,6 +16,7 @@ public class ApiGameLevelResponse : IApiResponse, IDataConvertableFrom new() { LevelId = this.LevelId, + IsAdventure = this.IsAdventure, Title = this.Title, IconHash = this.IconHash, Description = this.Description, diff --git a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs index cb55ffd4..8177bdd5 100644 --- a/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs +++ b/Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameLevelResponse.cs @@ -19,6 +19,8 @@ namespace Refresh.GameServer.Endpoints.Game.DataTypes.Response; public class GameLevelResponse : IDataConvertableFrom { [XmlElement("id")] public required int LevelId { get; set; } + + [XmlElement("isAdventurePlanet")] public required bool IsAdventure { get; set; } [XmlElement("name")] public required string Title { get; set; } [XmlElement("icon")] public required string IconHash { get; set; } @@ -96,6 +98,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext) return new GameLevelResponse { LevelId = dataContext.Game == TokenGame.LittleBigPlanet3 ? LevelIdFromHash(hash) : int.MaxValue, + IsAdventure = false, Title = $"Hashed Level - {hash}", IconHash = "0", GameVersion = 0, @@ -143,6 +146,7 @@ public static GameLevelResponse FromHash(string hash, DataContext dataContext) GameLevelResponse response = new() { LevelId = old.LevelId, + IsAdventure = old.IsAdventure, Title = old.Title, IconHash = old.IconHash, Description = old.Description, diff --git a/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs index b55b0bf8..3f31becc 100644 --- a/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs +++ b/Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs @@ -1,14 +1,17 @@ using Bunkum.Core; using Bunkum.Core.Endpoints; +using Bunkum.Core.Endpoints.Debugging; using Bunkum.Core.Responses; using Bunkum.Listener.Protocol; using Bunkum.Protocols.Http; using Refresh.Common.Constants; +using Refresh.GameServer.Authentication; using Refresh.GameServer.Database; using Refresh.GameServer.Endpoints.Game.DataTypes.Request; using Refresh.GameServer.Endpoints.Game.DataTypes.Response; using Refresh.GameServer.Extensions; using Refresh.GameServer.Services; +using Refresh.GameServer.Types.Assets; using Refresh.GameServer.Types.Data; using Refresh.GameServer.Types.Levels; using Refresh.GameServer.Types.UserData; @@ -33,14 +36,15 @@ private static bool VerifyLevel(GameLevelRequest body, DataContext dataContext) body.Description = body.Description[..UgcConstantLimits.DescriptionLimit]; if (body.MaxPlayers is > 4 or < 0 || body.MinPlayers is > 4 or < 0) - { return false; - } - //If the icon hash is a GUID hash, verify its a valid texture GUID + //If the icon hash is a GUID hash, verify that its a valid texture GUID if (body.IconHash.StartsWith('g') && !dataContext.GuidChecker.IsTextureGuid(dataContext.Game, long.Parse(body.IconHash.AsSpan()[1..]))) return false; + if (body.IsAdventure && dataContext.Game != TokenGame.LittleBigPlanet3) + return false; + GameLevel? existingLevel = dataContext.Database.GetLevelByRootResource(body.RootResource); // If there is an existing level with this root hash, and this isn't an update request, block the upload if (existingLevel != null && body.LevelId != existingLevel.LevelId) @@ -62,13 +66,28 @@ private static bool VerifyLevel(GameLevelRequest body, DataContext dataContext) DataContext dataContext) { //If verifying the request fails, return null - if (!VerifyLevel(body, dataContext)) return null; + if (!VerifyLevel(body, dataContext)) + { + context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify root level"); + return null; + } + + if (body.Slots != null) + { + foreach (GameLevelRequest innerLevel in body.Slots) + { + if (VerifyLevel(innerLevel, dataContext)) continue; + + context.Logger.LogInfo(RefreshContext.Publishing, "Failed to verify inner level {0}", innerLevel.LevelId); + return null; + } + } List hashes = [ - .. body.XmlResources, + ..body.XmlResources, body.RootResource, - body.IconHash + body.IconHash, ]; //Remove all invalid or GUID assets @@ -111,6 +130,23 @@ public Response PublishLevel(RequestContext context, //Make sure the root resource exists in the data store if (!dataContext.DataStore.ExistsInStore(rootResourcePath)) return NotFound; + GameAsset? asset = dataContext.Database.GetAssetFromHash(level.RootResource); + if (asset != null) + { + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (level.IsAdventure && asset.AssetType != GameAssetType.AdventureCreateProfile) + { + dataContext.Database.AddPublishFailNotification("The uploaded adventure data was corrupted.", level, dataContext.User!); + return BadRequest; + } + + if (!level.IsAdventure && asset.AssetType != GameAssetType.Level) + { + dataContext.Database.AddPublishFailNotification("The uploaded level data was corrupted.", level, dataContext.User!); + return BadRequest; + } + } + if (level.LevelId != default) // Republish requests contain the id of the old level { context.Logger.LogInfo(BunkumCategory.UserContent, "Republishing level id {0}", level.LevelId); @@ -122,7 +158,7 @@ public Response PublishLevel(RequestContext context, return new Response(GameLevelResponse.FromOld(newBody, dataContext)!, ContentType.Xml); } - dataContext.Database.AddPublishFailNotification("You may not republish another user's level.", level, dataContext.User); + dataContext.Database.AddPublishFailNotification("You may not republish another user's level.", level, dataContext.User!); return BadRequest; } diff --git a/Refresh.GameServer/Refresh.GameServer.csproj b/Refresh.GameServer/Refresh.GameServer.csproj index acd7c9d7..9b12a4b4 100644 --- a/Refresh.GameServer/Refresh.GameServer.csproj +++ b/Refresh.GameServer/Refresh.GameServer.csproj @@ -52,12 +52,12 @@ - - - - - - + + + + + + diff --git a/Refresh.GameServer/RefreshContext.cs b/Refresh.GameServer/RefreshContext.cs index 65aacac2..9c51a10f 100644 --- a/Refresh.GameServer/RefreshContext.cs +++ b/Refresh.GameServer/RefreshContext.cs @@ -8,4 +8,5 @@ public enum RefreshContext PasswordReset, LevelListOverride, CoolLevels, + Publishing, } \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/AdventureCategory.cs b/Refresh.GameServer/Types/Levels/Categories/AdventureCategory.cs new file mode 100644 index 00000000..d9938812 --- /dev/null +++ b/Refresh.GameServer/Types/Levels/Categories/AdventureCategory.cs @@ -0,0 +1,24 @@ +using Bunkum.Core; +using Refresh.GameServer.Database; +using Refresh.GameServer.Endpoints.Game.Levels.FilterSettings; +using Refresh.GameServer.Types.Data; +using Refresh.GameServer.Types.UserData; + +namespace Refresh.GameServer.Types.Levels.Categories; + +public class AdventureCategory : LevelCategory +{ + public AdventureCategory() : base("adventure", Array.Empty(), false) + { + this.Name = "Adventures"; + this.Description = "Storylines and other big projects by the community."; + this.FontAwesomeIcon = "book-bookmark"; + this.IconHash = "g820625"; + } + + public override DatabaseList? Fetch(RequestContext context, int skip, int count, DataContext dataContext, + LevelFilterSettings levelFilterSettings, GameUser? _) + { + return dataContext.Database.GetAdventureLevels(count, skip, dataContext.User, levelFilterSettings); + } +} \ No newline at end of file diff --git a/Refresh.GameServer/Types/Levels/Categories/CategoryService.cs b/Refresh.GameServer/Types/Levels/Categories/CategoryService.cs index 52e694eb..f4715920 100644 --- a/Refresh.GameServer/Types/Levels/Categories/CategoryService.cs +++ b/Refresh.GameServer/Types/Levels/Categories/CategoryService.cs @@ -31,6 +31,7 @@ public class CategoryService : EndpointService new ByTagCategory(), new DeveloperLevelsCategory(), new ContestCategory(), + new AdventureCategory(), ]; internal CategoryService(Logger logger) : base(logger) diff --git a/Refresh.GameServer/Types/Levels/GameLevel.cs b/Refresh.GameServer/Types/Levels/GameLevel.cs index 48ec1c13..af994f11 100644 --- a/Refresh.GameServer/Types/Levels/GameLevel.cs +++ b/Refresh.GameServer/Types/Levels/GameLevel.cs @@ -16,6 +16,8 @@ namespace Refresh.GameServer.Types.Levels; public partial class GameLevel : IRealmObject, ISequentialId { [PrimaryKey] public int LevelId { get; set; } + + public bool IsAdventure { get; set; } [Indexed(IndexType.FullText)] public string Title { get; set; } = ""; diff --git a/Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs b/Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs index f1e8ce52..70bd39fe 100644 --- a/Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs +++ b/Refresh.GameServer/Types/Levels/GameMinimalLevelResponse.cs @@ -17,6 +17,7 @@ public class GameMinimalLevelResponse : IDataConvertableFrom - + diff --git a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs index 70c5b8a3..a72ef602 100644 --- a/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs +++ b/RefreshTests.GameServer/Tests/Levels/PublishEndpointsTests.cs @@ -28,6 +28,7 @@ public void PublishLevel() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "TEST LEVEL", IconHash = "g719", Description = "DESCRIPTION", @@ -91,6 +92,7 @@ public void LevelWithLongTitleGetsTruncated() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = new string('*', UgcConstantLimits.TitleLimit * 2), IconHash = "g0", Description = "Normal length", @@ -129,6 +131,7 @@ public void LevelWithLongDescriptionGetsTruncated() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = new string('=', UgcConstantLimits.DescriptionLimit * 2), @@ -167,6 +170,7 @@ public void CantPublishLevelWithInvalidMaxPlayers() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = "Normal Description", @@ -200,6 +204,7 @@ public void CantPublishLevelWithInvalidMinPlayers() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = "Normal Description", @@ -233,6 +238,7 @@ public void CantPublishLevelWithInvalidRootResource() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = "Normal Description", @@ -269,6 +275,7 @@ public void CantPublishLevelWithInvalidIconGuid(TokenGame game) GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = "Normal Description", @@ -301,6 +308,7 @@ public void CanPublishLevelWithInvalidIconGuidPsp() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g0", Description = "Normal Description", @@ -334,6 +342,7 @@ public void CantPublishLevelWithMissingRootResource() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "Normal Title!", IconHash = "g719", Description = "Normal Description", @@ -369,6 +378,7 @@ public void CantRepublishOtherUsersLevel() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "TEST LEVEL", IconHash = "g719", Description = "DESCRIPTION", @@ -425,6 +435,7 @@ public void CantPublishSameRootLevelHashTwice() GameLevelRequest level = new() { LevelId = 0, + IsAdventure = false, Title = "TEST LEVEL", IconHash = "g719", Description = "DESCRIPTION",