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",