Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability for server to override level pages #210

Merged
merged 12 commits into from
Nov 3, 2023
14 changes: 14 additions & 0 deletions Refresh.GameServer/Endpoints/ApiV3/LevelApiEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,18 @@ public ApiOkResponse DeleteLevelById(RequestContext context, GameDatabaseContext

return new ApiOkResponse();
}

[ApiV3Endpoint("levels/id/{id}/setAsOverride", HttpMethods.Post)]
[DocSummary("Marks the level to show in the next slot list gotten from the game")]
[DocError(typeof(ApiNotFoundError), ApiNotFoundError.LevelMissingErrorWhen)]
public ApiOkResponse SetLevelAsOverrideById(RequestContext context, GameDatabaseContext database, GameUser user, LevelListOverrideService service,
[DocSummary("The ID of the level")] int id)
{
GameLevel? level = database.GetLevelById(id);
if (level == null) return ApiNotFoundError.LevelMissingError;

service.AddOverridesForUser(user, level);

return new ApiOkResponse();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
[XmlElement("location")] public required GameLocation Location { get; set; }
[XmlElement("planets")] public required string PlanetsHash { get; set; }

[XmlElement("npHandle")] public SerializedUserHandle Handle { get; set; }

Check warning on line 23 in Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Handle' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.

Check warning on line 23 in Refresh.GameServer/Endpoints/Game/DataTypes/Response/GameUserResponse.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

Non-nullable property 'Handle' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
[XmlElement("commentCount")] public int CommentCount { get; set; }
[XmlElement("commentsEnabled")] public bool CommentsEnabled { get; set; }
[XmlElement("favouriteSlotCount")] public int FavouriteLevelCount { get; set; }
Expand Down Expand Up @@ -75,12 +75,12 @@
Handle = SerializedUserHandle.FromUser(old),
CommentCount = old.ProfileComments.Count,
CommentsEnabled = true,
FavouriteLevelCount = old.FavouriteLevelRelations.Count(),
FavouriteUserCount = old.UsersFavourited.Count(),
QueuedLevelCount = old.QueueLevelRelations.Count(),
HeartCount = old.UsersFavouritingMe.Count(),
PhotosByMeCount = old.PhotosByMe.Count(),
PhotosWithMeCount = old.PhotosWithMe.Count(),
FavouriteLevelCount = old.IsManaged ? old.FavouriteLevelRelations.Count() : 0,
FavouriteUserCount = old.IsManaged ? old.UsersFavourited.Count() : 0,
QueuedLevelCount = old.IsManaged ? old.QueueLevelRelations.Count() : 0,
HeartCount = old.IsManaged ? old.UsersFavouritingMe.Count() : 0,
PhotosByMeCount = old.IsManaged ? old.PhotosByMe.Count() : 0,
PhotosWithMeCount = old.IsManaged ? old.PhotosWithMe.Count() : 0,

EntitledSlots = MaximumLevels,
EntitledSlotsLBP2 = MaximumLevels,
Expand All @@ -102,6 +102,14 @@

private void FillInExtraData(GameUser old, TokenGame gameVersion, GameDatabaseContext database)
{
if (!old.IsManaged)
{
this.PlanetsHash = "0";
this.IconHash = "0";

return;
}

this.PlanetsHash = gameVersion switch
{
TokenGame.LittleBigPlanet1 => "0",
Expand Down
41 changes: 35 additions & 6 deletions Refresh.GameServer/Endpoints/Game/Levels/LevelEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,20 @@ public class LevelEndpoints : EndpointGroup
GameDatabaseContext database,
CategoryService categoryService,
MatchService matchService,
LevelListOverrideService overrideService,
GameUser user,
Token token,
string route)
{
if (overrideService.UserHasOverrides(user))
{
List<GameMinimalLevelResponse> overrides = overrideService.GetOverridesForUser(user, database)
.Select(l => GameMinimalLevelResponse.FromOldWithExtraData(l, matchService))
.ToList()!;

return new SerializedMinimalLevelList(overrides, overrides.Count);
}

(int skip, int count) = context.GetPageData();

DatabaseList<GameLevel>? levels = categoryService.Categories
Expand All @@ -43,12 +53,19 @@ public class LevelEndpoints : EndpointGroup
[GameEndpoint("slots/{route}/{username}", ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
[NullStatusCode(NotFound)]
public SerializedMinimalLevelList? GetLevelsWithPlayer(RequestContext context, GameDatabaseContext database, CategoryService categories, MatchService matchService, Token token, string route, string username)
public SerializedMinimalLevelList? GetLevelsWithPlayer(RequestContext context,
GameDatabaseContext database,
CategoryService categories,
MatchService matchService,
LevelListOverrideService overrideService,
Token token,
string route,
string username)
{
GameUser? user = database.GetUserByUsername(username);
if (user == null) return null;

return this.GetLevels(context, database, categories, matchService, user, token, route);
return this.GetLevels(context, database, categories, matchService, overrideService, user, token, route);
}

[GameEndpoint("s/user/{id}", ContentType.Xml)]
Expand Down Expand Up @@ -118,18 +135,30 @@ public SerializedMinimalLevelResultsList GetLevelsFromCategory(RequestContext co

[GameEndpoint("slots", ContentType.Xml)]
[MinimumRole(GameUserRole.Restricted)]
public SerializedMinimalLevelList? NewestLevels(RequestContext context, GameDatabaseContext database, CategoryService categories, MatchService matchService, GameUser user, Token token)
=> this.GetLevels(context, database, categories, matchService, user, token, "newest");
public SerializedMinimalLevelList? NewestLevels(RequestContext context,
GameDatabaseContext database,
CategoryService categories,
MatchService matchService,
LevelListOverrideService overrideService,
GameUser user,
Token token)
=> this.GetLevels(context, database, categories, matchService, overrideService, user, token, "newest");

[GameEndpoint("favouriteSlots/{username}", ContentType.Xml)]
[NullStatusCode(NotFound)]
[MinimumRole(GameUserRole.Restricted)]
public SerializedMinimalFavouriteLevelList? FavouriteLevels(RequestContext context, GameDatabaseContext database, CategoryService categories, MatchService matchService, Token token, string username)
public SerializedMinimalFavouriteLevelList? FavouriteLevels(RequestContext context,
GameDatabaseContext database,
CategoryService categories,
MatchService matchService,
LevelListOverrideService overrideService,
Token token,
string username)
{
GameUser? user = database.GetUserByUsername(username);
if (user == null) return null;

SerializedMinimalLevelList? levels = this.GetLevels(context, database, categories, matchService, user, token, "favouriteSlots");
SerializedMinimalLevelList? levels = this.GetLevels(context, database, categories, matchService, overrideService, user, token, "favouriteSlots");

return new SerializedMinimalFavouriteLevelList(levels);
}
Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/Endpoints/Game/ModerationEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public string Filter(RequestContext context, CommandService commandService, stri
context.Logger.LogInfo(BunkumCategory.Commands, $"User used command '{command.Name.ToString()}' with args '{command.Arguments.ToString()}'");

commandService.HandleCommand(command, database, user, token);
return "(Command)";
}
catch
{
Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/RefreshContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ public enum RefreshContext
Worker,
Discord,
PasswordReset,
LevelListOverride,
}
6 changes: 4 additions & 2 deletions Refresh.GameServer/RefreshGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using Bunkum.HealthChecks;
using Bunkum.HealthChecks.RealmDatabase;
using Bunkum.Protocols.Http;
using Bunkum.RealmDatabase;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
using NotEnoughLogs.Sinks;
Expand Down Expand Up @@ -60,7 +59,7 @@
this._databaseProvider = databaseProvider.Invoke();
this._dataStore = dataStore;

this._server = listener == null ? new BunkumHttpServer(logConfig, sinks) : new BunkumHttpServer(listener, configuration: logConfig, sinks);

Check warning on line 62 in Refresh.GameServer/RefreshGameServer.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

'BunkumHttpServer.BunkumHttpServer(BunkumListener, LoggerConfiguration?, List<ILoggerSink>?)' is obsolete: 'This constructor is obsolete, `UseListener` is preferred instead!'

Check warning on line 62 in Refresh.GameServer/RefreshGameServer.cs

View workflow job for this annotation

GitHub Actions / Build, Test, and Upload Builds

'BunkumHttpServer.BunkumHttpServer(BunkumListener, LoggerConfiguration?, List<ILoggerSink>?)' is obsolete: 'This constructor is obsolete, `UseListener` is preferred instead!'
this.Logger.LogDebug(RefreshContext.Startup, "Successfully initialized " + this.GetType().Name);

this._server.Initialize = _ =>
Expand Down Expand Up @@ -126,7 +125,6 @@
this._server.AddService<CategoryService>();
this._server.AddService<FriendStorageService>();
this._server.AddService<MatchService>();
this._server.AddService<CommandService>();
this._server.AddService<ImportService>();
this._server.AddService<DocumentationService>();
this._server.AddAutoDiscover(serverBrand: $"{this._config!.InstanceName} (Refresh)",
Expand All @@ -146,6 +144,10 @@
if (this._config!.TrackRequestStatistics)
this._server.AddService<RequestStatisticTrackingService>();

this._server.AddService<LevelListOverrideService>();

this._server.AddService<CommandService>();

#if DEBUG
this._server.AddService<DebugService>();
#endif
Expand Down
29 changes: 21 additions & 8 deletions Refresh.GameServer/Services/CommandService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.Commands;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Services;

public class CommandService : EndpointService
{
private readonly MatchService _match;
private readonly LevelListOverrideService _levelListService;

public CommandService(Logger logger, MatchService match) : base(logger) {
public CommandService(Logger logger, MatchService match, LevelListOverrideService levelListService) : base(logger) {
this._match = match;
this._levelListService = levelListService;
}

private readonly HashSet<ObjectId> _usersPublishing = new();
Expand Down Expand Up @@ -80,7 +83,8 @@ public void HandleCommand(CommandInvocation command, GameDatabaseContext databas
{
switch (command.Name)
{
case "forcematch": {
case "forcematch":
{
if (command.Arguments == null)
{
throw new Exception("User not provided for force match command");
Expand All @@ -95,19 +99,28 @@ public void HandleCommand(CommandInvocation command, GameDatabaseContext databas

break;
}
case "clearforcematch": {
case "clearforcematch":
{
this._match.ClearForceMatch(user.UserId);

break;
}
case "griefphotoson": {
case "griefphotoson":
{
user.RedirectGriefReportsToPhotos = true;

break;
}
case "griefphotosoff": {
case "griefphotosoff":
{
user.RedirectGriefReportsToPhotos = false;

break;
}
case "play":
{
GameLevel? level = database.GetLevelById(int.Parse(command.Arguments));
if (level != null)
{
this._levelListService.AddOverridesForUser(user, level);
}
break;
}
#if DEBUG
Expand Down
50 changes: 50 additions & 0 deletions Refresh.GameServer/Services/LevelListOverrideService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Diagnostics;
using Bunkum.Core.Services;
using MongoDB.Bson;
using NotEnoughLogs;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Services;

public class LevelListOverrideService : EndpointService
{
public LevelListOverrideService(Logger logger) : base(logger)
{}

private readonly Dictionary<ObjectId, List<int>> _userIdsToLevelList = new(1);

public bool UserHasOverrides(GameUser user)
{
bool result = this._userIdsToLevelList.ContainsKey(user.UserId);

this.Logger.LogTrace(RefreshContext.LevelListOverride, "{0} has overrides: {1}", user.Username, result);
return result;
}

public void AddOverridesForUser(GameUser user, GameLevel level)
{
this.AddOverridesForUser(user, new[] { level });
}

public void AddOverridesForUser(GameUser user, IEnumerable<GameLevel> levels)
{
Debug.Assert(!this.UserHasOverrides(user), "User already has overrides");

List<int> ids = levels.Select(l => l.LevelId).ToList();
this.Logger.LogDebug(RefreshContext.LevelListOverride, "Adding level override for {0}: [{1}]", user.Username, string.Join(", ", ids));
this._userIdsToLevelList.Add(user.UserId, ids);
}

public IEnumerable<GameLevel> GetOverridesForUser(GameUser user, GameDatabaseContext database)
{
Debug.Assert(this.UserHasOverrides(user), "User does not have overrides, should be checked first");

List<int> overrides = this._userIdsToLevelList[user.UserId];
this.Logger.LogDebug(RefreshContext.LevelListOverride, "Getting level override for {0}: [{1}]", user.Username, string.Join(", ", overrides));
this._userIdsToLevelList.Remove(user.UserId);

return overrides.Select(database.GetLevelById)!;
}
}
16 changes: 15 additions & 1 deletion RefreshTests.GameServer/GameServer/TestRefreshGameServer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
using System.Reflection;
using Bunkum.Core;
using Bunkum.Core.Services;
using Bunkum.Core.Storage;
using Bunkum.Protocols.Http;
using JetBrains.Annotations;
using NotEnoughLogs;
using NotEnoughLogs.Behaviour;
using NotEnoughLogs.Sinks;
Expand Down Expand Up @@ -64,7 +68,17 @@ protected override void SetupServices()
this._server.AddService<TimeProviderService>(this.DateTimeProvider);
this._server.AddService<CategoryService>();
this._server.AddService<MatchService>();
this._server.AddService<CommandService>();
this._server.AddService<ImportService>();
this._server.AddService<LevelListOverrideService>();
this._server.AddService<CommandService>();
}

[Pure]
public TService GetService<TService>() where TService : Service
{
List<Service> services = (List<Service>)typeof(BunkumServer).GetField("_services", BindingFlags.Instance | BindingFlags.NonPublic)!
.GetValue(this._server)!;

return (TService)services.First(s => typeof(TService) == s.GetType());
}
Comment on lines +77 to 83
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im not sure how i feel about this, this should be something exposed in Bunkum, but since its non-critical test code it should be fine, non-blocking for merge but i'd like to see this more cleanly resolved at some point

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the reasoning behind that behind choice in Bunkum was I wanted to discourage handling of Bunkum's objects, but as Bunkum grew that use-case became more acceptable (think database providers outside of the main BunkumServer for instance). I'll definitely implement this in Bunkum.

}
5 changes: 5 additions & 0 deletions RefreshTests.GameServer/TestContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Bunkum.Core.Services;
using Bunkum.Protocols.Http.Direct;
using JetBrains.Annotations;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types;
Expand Down Expand Up @@ -120,6 +122,9 @@ public GameSubmittedScore SubmitScore(int score, byte type, GameLevel level, Gam
return submittedScore;
}

[Pure]
public TService GetService<TService>() where TService : Service => this.Server.Value.GetService<TService>();

public void Dispose()
{
this.Database.Dispose();
Expand Down
6 changes: 3 additions & 3 deletions RefreshTests.GameServer/Tests/Commands/CommandParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ private void ParseTest(CommandService service, ReadOnlySpan<char> input, ReadOnl
public void ParsingTest()
{
using Logger logger = new(new []{ new NUnitSink() });
CommandService service = new(logger, new MatchService(logger));
CommandService service = new(logger, new MatchService(logger), new LevelListOverrideService(logger));

ParseTest(service, "/parse test", "parse", "test");
ParseTest(service, "/noargs", "noargs", "");
Expand All @@ -31,7 +31,7 @@ public void ParsingTest()
public void NoSlashThrows()
{
using Logger logger = new(new []{ new NUnitSink() });
CommandService service = new(logger, new MatchService(logger));
CommandService service = new(logger, new MatchService(logger), new LevelListOverrideService(logger));

Assert.That(() => _ = service.ParseCommand("parse test"), Throws.InstanceOf<FormatException>());
}
Expand All @@ -40,7 +40,7 @@ public void NoSlashThrows()
public void BlankCommandThrows()
{
using Logger logger = new(new []{ new NUnitSink() });
CommandService service = new(logger, new MatchService(logger));
CommandService service = new(logger, new MatchService(logger), new LevelListOverrideService(logger));

Assert.That(() => _ = service.ParseCommand("/ test"), Throws.InstanceOf<FormatException>());
}
Expand Down
Loading
Loading