Skip to content

Commit

Permalink
- Implement CLI commands
Browse files Browse the repository at this point in the history
- Implement campaign levels from a game_data / patch_data Sound Shapes folder
- Refactor community level import
- Fix bug where Level Analysis would fail making it impossible to upload a level under certain circumstances
- Refactor level publishing
- Replace unix dates in API requests and responses with DateTimeOffsets

Fixes #200
  • Loading branch information
turecross321 committed Nov 6, 2023
1 parent 2d0cecc commit 80fca83
Show file tree
Hide file tree
Showing 31 changed files with 413 additions and 342 deletions.
43 changes: 11 additions & 32 deletions SoundShapesServer/Database/GameDatabaseContext.Levels.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using Bunkum.Core.Storage;
using SoundShapesServer.Extensions;
using SoundShapesServer.Extensions.Queryable;
using SoundShapesServer.Requests.Game;
using SoundShapesServer.Responses.Api.Framework;
using SoundShapesServer.Responses.Api.Framework.Errors;
using SoundShapesServer.Types;
Expand All @@ -17,42 +16,30 @@ namespace SoundShapesServer.Database;

public partial class GameDatabaseContext
{
public GameLevel CreateLevel(GameUser user, PublishLevelRequest request, PlatformType uploadPlatform,
bool createEvent = true, string? levelId = null, bool campaignLevel = false)
public void AddLevel(GameLevel level, bool createEvent)
{
levelId ??= GenerateLevelId();
GameLevel level = new()
{
Id = levelId,
Author = user,
Name = request.Name,
Language = request.Language,
CreationDate = request.CreationDate,
ModificationDate = request.CreationDate,
FileSize = request.FileSize,
Visibility = LevelVisibility.Public,
UploadPlatform = uploadPlatform
};
GameUser author = level.Author;

_realm.Write(() =>
{
_realm.Add(level);
user.LevelsCount = user.Levels.Count();
author.LevelsCount = author.Levels.Count();
});

if (createEvent)
CreateEvent(user, EventType.LevelPublish, uploadPlatform, EventDataType.Level, level.Id);

return level;
CreateEvent(author, EventType.LevelPublish, level.UploadPlatform, EventDataType.Level, level.Id);
}

public GameLevel EditLevel(PublishLevelRequest updatedPublishLevel, GameLevel level)
public GameLevel EditLevel(GameLevel level, string name, int? language = null, LevelVisibility? visibility = null,
DateTimeOffset? creationDate = null)
{
_realm.Write(() =>
{
level.Name = AdhereToLevelNameCharacterLimit(updatedPublishLevel.Name);
level.Visibility = updatedPublishLevel.Visibility;
level.ModificationDate = DateTimeOffset.UtcNow;
level.Name = AdhereToLevelNameCharacterLimit(name);
level.Language = language ?? level.Language;
level.Visibility = visibility ?? level.Visibility;
level.CreationDate = creationDate ?? level.CreationDate;
level.ModificationDate = creationDate ?? DateTimeOffset.UtcNow;
});

return level;
Expand All @@ -65,14 +52,6 @@ private void SetLevelPlayTime(GameLevel level)
_realm.Write(() => { level.TotalPlayTime = totalPlayTime; });
}

public void UploadLevelResources(IDataStore dataStore, GameLevel level, byte[] levelFile, byte[] thumbnailFile,
byte[] soundFile)
{
UploadLevelResource(dataStore, level, levelFile, FileType.Level);
UploadLevelResource(dataStore, level, thumbnailFile, FileType.Image);
UploadLevelResource(dataStore, level, soundFile, FileType.Sound);
}

public bool AnalyzeLevel(GameLevel level, byte[] levelFile)
{
SSLevel? ssLevel = SSLevel.FromLevelFile(levelFile);
Expand Down
30 changes: 15 additions & 15 deletions SoundShapesServer/Database/GameDatabaseContext.Punishments.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,33 @@ public partial class GameDatabaseContext
public Punishment CreatePunishment(GameUser author, GameUser recipient, ApiPunishRequest request)
{
DateTimeOffset now = DateTimeOffset.UtcNow;

Punishment newPunishment = new()
{
PunishmentType = request.PunishmentType,
Recipient = recipient,
Reason = request.Reason,
ExpiryDate = DateTimeOffset.FromUnixTimeSeconds(request.ExpiryDate),
ExpiryDate = request.ExpiryDate,
CreationDate = now,
ModificationDate = now,
Author = author
};

_realm.Write(() =>
{
_realm.Add(newPunishment);
});

_realm.Write(() => { _realm.Add(newPunishment); });

return newPunishment;
}

public Punishment EditPunishment(GameUser author, Punishment punishment, GameUser recipient, ApiPunishRequest request)
public Punishment EditPunishment(GameUser author, Punishment punishment, GameUser recipient,
ApiPunishRequest request)
{
_realm.Write(() =>
{
punishment.Author = author;
punishment.Recipient = recipient;
punishment.PunishmentType = request.PunishmentType;
punishment.Reason = request.Reason;
punishment.ExpiryDate = DateTimeOffset.FromUnixTimeSeconds(request.ExpiryDate);
punishment.ExpiryDate = request.ExpiryDate;
punishment.ModificationDate = DateTimeOffset.UtcNow;
});

Expand All @@ -55,17 +53,19 @@ public void RevokePunishment(Punishment punishment)
punishment.RevokeDate = DateTimeOffset.UtcNow;
});
}

public Punishment? GetPunishmentWithId(string id)
{
if (!ObjectId.TryParse(id, out ObjectId objectId))
if (!ObjectId.TryParse(id, out ObjectId objectId))
return null;

return _realm.All<Punishment>().FirstOrDefault(p => p.Id == objectId);
}

public PaginatedList<Punishment> GetPaginatedPunishments(PunishmentOrderType order, bool descending, PunishmentFilters filters, int from, int count)

public PaginatedList<Punishment> GetPaginatedPunishments(PunishmentOrderType order, bool descending,
PunishmentFilters filters, int from, int count)
{
return new PaginatedList<Punishment>(_realm.All<Punishment>().FilterPunishments(filters).OrderPunishments(order, descending), from, count);
return new PaginatedList<Punishment>(
_realm.All<Punishment>().FilterPunishments(filters).OrderPunishments(order, descending), from, count);
}
}
5 changes: 4 additions & 1 deletion SoundShapesServer/Database/GameDatabaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,10 @@ protected override void Migrate(Migration migration, ulong oldVersion)
Author = adminUser,
Id = levelId,
Name = levelId,
Visibility = LevelVisibility.Private
Visibility = LevelVisibility.Private,
UploadPlatform = PlatformType.Unknown,
CreationDate = default,
ModificationDate = default
};
migration.NewRealm.Add(level);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using SoundShapesServer.Extensions;
using SoundShapesServer.Responses.Api.Framework;
using SoundShapesServer.Responses.Api.Framework.Errors;
using SoundShapesServer.Responses.Api.Responses;
using SoundShapesServer.Responses.Api.Responses.Leaderboard;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Leaderboard;
using SoundShapesServer.Types.Levels;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public ApiResponse<ApiDailyLevelResponse> AddDailyLevel(RequestContext context,
return ApiNotFoundError.LevelNotFound;

DailyLevel createdDailyLevel =
database.CreateDailyLevel(user, level, DateTimeOffset.FromUnixTimeSeconds(body.Date));
database.CreateDailyLevel(user, level, body.Date);
return ApiDailyLevelResponse.FromOld(createdDailyLevel);
}

Expand All @@ -49,7 +49,7 @@ public ApiResponse<ApiDailyLevelResponse> EditDailyLevel(RequestContext context,
if (level == null) return ApiNotFoundError.LevelNotFound;

DailyLevel createdDailyLevel =
database.EditDailyLevel(dailyLevel, user, level, DateTimeOffset.FromUnixTimeSeconds(body.Date));
database.EditDailyLevel(dailyLevel, user, level, body.Date);
return ApiDailyLevelResponse.FromOld(createdDailyLevel);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
using Bunkum.Protocols.Http;
using SoundShapesServer.Attributes;
using SoundShapesServer.Database;
using SoundShapesServer.Helpers;
using SoundShapesServer.Requests.Api;
using SoundShapesServer.Requests.Game;
using SoundShapesServer.Responses.Api.Framework;
using SoundShapesServer.Responses.Api.Framework.Errors;
using SoundShapesServer.Responses.Api.Responses.Levels;
Expand All @@ -25,9 +25,21 @@ public class ApiLevelManagementEndpoints : EndpointGroup
public ApiResponse<ApiLevelFullResponse> CreateLevel(RequestContext context, GameDatabaseContext database,
GameUser user, ApiCreateLevelRequest body, ProfanityService profanity)
{
body.Name = profanity.CensorSentence(body.Name); // Censor any potential profanity
GameLevel publishedLevel = database.CreateLevel(user, new PublishLevelRequest(body), PlatformType.Unknown);
return ApiLevelFullResponse.FromOld(publishedLevel);
DateTimeOffset now = DateTimeOffset.UtcNow;
GameLevel level = new()
{
Id = IdHelper.GenerateLevelId(),
Name = profanity.CensorSentence(body.Name),
Language = body.Language,
CreationDate = body.CreationDate ?? now,
ModificationDate = body.CreationDate ?? now,
Author = user,
Visibility = body.Visibility,
UploadPlatform = PlatformType.Unknown
};

database.AddLevel(level, true);
return ApiLevelFullResponse.FromOld(level);
}

[ApiEndpoint("levels/id/{id}/setLevel", HttpMethods.Post)]
Expand Down Expand Up @@ -95,9 +107,9 @@ public ApiResponse<ApiLevelFullResponse> EditLevel(RequestContext context, GameD
if (user.PermissionsType < PermissionsType.Moderator)
return ApiUnauthorizedError.NoEditPermission;

body.Name = profanity.CensorSentence(body.Name); // Censor any potential profanity
GameLevel publishedLevel = database.EditLevel(new PublishLevelRequest(body), level);
return ApiLevelFullResponse.FromOld(publishedLevel);

level = database.EditLevel(level, profanity.CensorSentence(body.Name), body.Language, body.Visibility);
return ApiLevelFullResponse.FromOld(level);
}

[ApiEndpoint("levels/id/{id}", HttpMethods.Delete)]
Expand Down
4 changes: 2 additions & 2 deletions SoundShapesServer/Endpoints/Game/LeaderboardEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ public Response SubmitScore(RequestContext context, GameDatabaseContext database

LeaderboardSubmissionRequest deSerializedRequest = LeaderboardSubmissionRequest.DeSerializeSubmission(body);


if (deSerializedRequest.Completed) database.AddCompletionToLevel(user, level);
if (deSerializedRequest.Completed)
database.AddCompletionToLevel(user, level);
database.CreatePlay(user, level);
database.AddDeathsToLevel(user, level, deSerializedRequest.Deaths);
database.SetLevelDifficulty(level);
Expand Down
89 changes: 55 additions & 34 deletions SoundShapesServer/Endpoints/Game/Levels/LevelManagementEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
using SoundShapesServer.Configuration;
using SoundShapesServer.Database;
using SoundShapesServer.Extensions;
using SoundShapesServer.Requests.Game;
using SoundShapesServer.Helpers;
using SoundShapesServer.Responses.Game.Levels;
using SoundShapesServer.Types;
using SoundShapesServer.Types.Authentication;
Expand All @@ -22,52 +22,68 @@ namespace SoundShapesServer.Endpoints.Game.Levels;
public class LevelManagementEndpoints : EndpointGroup
{
// Gets called by Endpoints.cs
public static Response CreateLevel(RequestContext context, GameServerConfig config, IDataStore dataStore, ProfanityService profanity, MultipartFormDataParser parser, GameDatabaseContext database, GameUser user, GameToken token)
public static Response CreateLevel(RequestContext context, GameServerConfig config, IDataStore dataStore,
ProfanityService profanity, MultipartFormDataParser parser, GameDatabaseContext database, GameUser user,
GameToken token)
{
if (user.Levels.Count() >= config.LevelPublishLimit) return HttpStatusCode.Forbidden;
if (user.Levels.Count() >= config.LevelPublishLimit)
return HttpStatusCode.Forbidden;

PublishLevelRequest publishLevelRequest = new (
parser.GetParameterValue("title"),
int.Parse(parser.GetParameterValue("sce_np_language")));

publishLevelRequest.Name = profanity.CensorSentence(publishLevelRequest.Name); // Censor any potential profanity
GameLevel publishedLevel = database.CreateLevel(user, publishLevelRequest, token.PlatformType);

Response uploadedResources = UploadLevelResources(context, database, dataStore, parser, publishedLevel, user);
if (uploadedResources.StatusCode != HttpStatusCode.Created) return uploadedResources;

return new Response(new LevelPublishResponse(publishedLevel), ContentType.Json, HttpStatusCode.Created);
string name = parser.GetParameterValue("title");
int language = int.Parse(parser.GetParameterValue("sce_np_language"));

DateTimeOffset now = DateTimeOffset.UtcNow;
GameLevel level = new()
{
Id = IdHelper.GenerateLevelId(),
Author = user,
Name = profanity.CensorSentence(name),
Language = language,
Visibility = LevelVisibility.Public,
UploadPlatform = token.PlatformType,
CreationDate = now,
ModificationDate = now
};

database.AddLevel(level, true);

Response uploadedResources = UploadLevelResources(context, database, dataStore, parser, level, user);
if (uploadedResources.StatusCode != HttpStatusCode.Created)
return uploadedResources;

return new Response(new LevelPublishResponse(level), ContentType.Json, HttpStatusCode.Created);
}

// Gets called by Endpoints.cs
public static Response UpdateLevel(RequestContext context, IDataStore dataStore, ProfanityService profanity, MultipartFormDataParser parser, GameDatabaseContext database, GameUser user, string levelId)
public static Response UpdateLevel(RequestContext context, IDataStore dataStore, ProfanityService profanity,
MultipartFormDataParser parser, GameDatabaseContext database, GameUser user, string levelId)
{
GameLevel? level = database.GetLevelWithId(levelId);

if (level == null) return HttpStatusCode.NotFound;
if (level.Author.Id != user.Id) return HttpStatusCode.Forbidden;

PublishLevelRequest publishLevelRequest = new (
parser.GetParameterValue("title"),
int.Parse(parser.GetParameterValue("sce_np_language")));

publishLevelRequest.Name = profanity.CensorSentence(publishLevelRequest.Name); // Censor any potential profanity
GameLevel publishedLevel = database.EditLevel(publishLevelRequest, level);

Response uploadedResources = UploadLevelResources(context, database, dataStore, parser, publishedLevel, user);
if (uploadedResources.StatusCode != HttpStatusCode.Created) return uploadedResources;
string name = parser.GetParameterValue("title");
int language = int.Parse(parser.GetParameterValue("sce_np_language"));

level = database.EditLevel(level, name, language);

return new Response(new LevelPublishResponse(publishedLevel), ContentType.Json, HttpStatusCode.Created);
Response uploadedResources = UploadLevelResources(context, database, dataStore, parser, level, user);
if (uploadedResources.StatusCode != HttpStatusCode.Created)
return uploadedResources;

return new Response(new LevelPublishResponse(level), ContentType.Json, HttpStatusCode.Created);
}


private static Response UploadLevelResources(RequestContext context, GameDatabaseContext database, IDataStore dataStore,
private static Response UploadLevelResources(RequestContext context, GameDatabaseContext database,
IDataStore dataStore,
IMultipartFormDataParser parser, GameLevel level, GameUser user)
{
byte[]? levelFile = null;
byte[]? thumbnailFile = null;
byte[]? soundFile = null;

foreach (FilePart? file in parser.Files)
{
byte[] bytes = file.ToByteArray();
Expand All @@ -86,7 +102,8 @@ private static Response UploadLevelResources(RequestContext context, GameDatabas
break;
case FileType.Unknown:
default:
context.Logger.LogInfo(BunkumCategory.Filter, user.Username + " attempted to upload an illegal file: " + file.ContentType);
context.Logger.LogInfo(BunkumCategory.Filter,
user.Username + " attempted to upload an illegal file: " + file.ContentType);
return HttpStatusCode.BadRequest;
}
}
Expand All @@ -96,17 +113,21 @@ private static Response UploadLevelResources(RequestContext context, GameDatabas
context.Logger.LogInfo(BunkumCategory.Filter, user.Username + " did not upload all the required files.");
return HttpStatusCode.BadRequest;
}

database.UploadLevelResources(dataStore, level, levelFile, thumbnailFile, soundFile);


database.UploadLevelResource(dataStore, level, levelFile, FileType.Level);
database.UploadLevelResource(dataStore, level, thumbnailFile, FileType.Image);
database.UploadLevelResource(dataStore, level, soundFile, FileType.Sound);

return HttpStatusCode.Created;
}

// Gets called by Endpoints.cs
public static Response RemoveLevel(IDataStore dataStore, GameDatabaseContext database, GameUser user, GameLevel level)
public static Response RemoveLevel(IDataStore dataStore, GameDatabaseContext database, GameUser user,
GameLevel level)
{
if (level.Author.Id != user.Id) return new Response(HttpStatusCode.Forbidden);

database.RemoveLevel(level, dataStore);

return HttpStatusCode.OK;
Expand Down
Loading

0 comments on commit 80fca83

Please sign in to comment.