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

Add chat commands #148

Merged
merged 11 commits into from
Sep 4, 2023
11 changes: 9 additions & 2 deletions Refresh.GameServer/Endpoints/Game/Levels/PublishEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.DataTypes.Request;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.UserData;

Expand Down Expand Up @@ -44,7 +45,7 @@ private static bool VerifyLevel(GameLevelRequest body, GameUser user, LoggerCont

[GameEndpoint("startPublish", ContentType.Xml, Method.Post)]
[NullStatusCode(BadRequest)]
public SerializedLevelResources? StartPublish(RequestContext context, GameUser user, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
public SerializedLevelResources? StartPublish(RequestContext context, GameUser user, GameDatabaseContext database, GameLevelRequest body, CommandService command, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
{
//If verifying the request fails, return null
if (!VerifyLevel(body, user, logger)) return null;
Expand All @@ -58,14 +59,17 @@ private static bool VerifyLevel(GameLevelRequest body, GameUser user, LoggerCont

if (hashes.Any(hash => hash.Length != 40)) return null;

//Mark the user as publishing
command.StartPublishing(user.UserId);

return new SerializedLevelResources
{
Resources = hashes.Where(r => !dataStore.ExistsInStore(r)).ToArray(),
};
}

[GameEndpoint("publish", ContentType.Xml, Method.Post)]
public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelRequest body, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
public Response PublishLevel(RequestContext context, GameUser user, Token token, GameDatabaseContext database, GameLevelRequest body, CommandService command, IDataStore dataStore, LoggerContainer<BunkumContext> logger)
{
//If verifying the request fails, return null
if (!VerifyLevel(body, user, logger)) return BadRequest;
Expand Down Expand Up @@ -93,6 +97,9 @@ public Response PublishLevel(RequestContext context, GameUser user, Token token,
database.AddPublishFailNotification("You may not republish another user's level.", level, user);
return BadRequest;
}

//Mark the user as no longer publishing
command.StopPublishing(user.UserId);

level.Publisher = user;

Expand Down
27 changes: 24 additions & 3 deletions Refresh.GameServer/Endpoints/Game/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Refresh.GameServer.Database;
using Refresh.GameServer.Endpoints.Game.DataTypes.Response;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Commands;
using Refresh.GameServer.Types.Lists;
using Refresh.GameServer.Types.Roles;
using Refresh.GameServer.Types.UserData;
Expand Down Expand Up @@ -168,15 +169,35 @@ public SerializedUserList GetMultipleUsers(RequestContext context, GameDatabaseC
/// <returns>The string shown in-game.</returns>
[GameEndpoint("filter", Method.Post)]
[AllowEmptyBody]
public string Filter(RequestContext context, string body, GameUser user)
public string Filter(RequestContext context, CommandService commandService, string body, GameUser user, GameDatabaseContext database)
{
Debug.Assert(user != null);
Debug.Assert(body != null);

context.Logger.LogInfo(BunkumContext.Filter, $"<{user}>: {body}");

//TODO: Add filtering

if (commandService.IsPublishing(user.UserId))
{
context.Logger.LogInfo(BunkumContext.Filter, $"Publish filter {body}");
jvyden marked this conversation as resolved.
Show resolved Hide resolved
}
else
{
context.Logger.LogInfo(BunkumContext.Filter, $"<{user}>: {body}");

try
{
Command command = commandService.ParseCommand(body);

context.Logger.LogInfo(BunkumContext.Commands, $"User used command \"{command.Name}\" with args \"{command.Arguments}\"");

commandService.HandleCommand(command, database, user);
}
catch
{
//do nothing
}
}

return body;
}
}
2 changes: 1 addition & 1 deletion Refresh.GameServer/Refresh.GameServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
</ItemGroup>

<ItemGroup Condition="'$(Configuration)'!='DebugLocalBunkum'">
<PackageReference Include="Bunkum" Version="3.3.17" />
<PackageReference Include="Bunkum" Version="3.3.18" />
<PackageReference Include="Bunkum.RealmDatabase" Version="3.1.5" />
</ItemGroup>

Expand Down
1 change: 1 addition & 0 deletions Refresh.GameServer/RefreshGameServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ protected virtual void SetupServices()
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: "Refresh",
Expand Down
103 changes: 103 additions & 0 deletions Refresh.GameServer/Services/CommandService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using Bunkum.HttpServer;
using Bunkum.HttpServer.Services;
using JetBrains.Annotations;
using MongoDB.Bson;
using NotEnoughLogs;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.Commands;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Services;

public class CommandService : EndpointService
{
private readonly MatchService _match;

public CommandService(LoggerContainer<BunkumContext> logger, MatchService match) : base(logger) {
this._match = match;
}

private readonly HashSet<ObjectId> _usersPublishing = new();

/// <summary>
/// Start tracking the user, eg. they started publishing
/// </summary>
/// <param name="id">The user ID</param>
public void StartPublishing(ObjectId id)
{
//Unconditionally add the user to the set
_ = this._usersPublishing.Add(id);
Beyley marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
/// Stop tracking the user, eg. they stopped publishing
/// </summary>
/// <param name="id">The user ID</param>
public void StopPublishing(ObjectId id)
{
//Unconditionally remove the user from the set
_ = this._usersPublishing.Remove(id);
Beyley marked this conversation as resolved.
Show resolved Hide resolved
}

public bool IsPublishing(ObjectId id) => this._usersPublishing.Contains(id);

/// <summary>
/// Parse a command string into a command object
/// </summary>
/// <param name="str">Command string</param>
/// <returns>Parsed command</returns>
/// <exception cref="FormatException">When the command is in an invalid format</exception>
[Pure]
public Command ParseCommand(string str)
jvyden marked this conversation as resolved.
Show resolved Hide resolved
{
//Ensure the command string starts with a slash
if (str[0] != '/')
{
throw new FormatException("Commands must start with `/`");
}

int idx = str.IndexOf(" ", StringComparison.Ordinal);

//If idx is 1, the command name is blank
// ReSharper disable once ConvertIfStatementToSwitchStatement
if (idx == 1)
{
throw new FormatException("Blank command name");
}

//If theres no space after, or if the space is the last character, then there are no arguments
if (idx == -1 || idx == str.Length - 1)
{
return new Command(idx == str.Length - 1 ? str[1..idx] : str[1..], null);
}

return new Command(str[1..idx], str[(idx + 1)..]);
}

public void HandleCommand(Command command, GameDatabaseContext database, GameUser user)
{
switch (command.Name)
{
case "forcematch": {
if (command.Arguments == null)
{
throw new Exception("User not provided for force match command");
}

GameUser? target = database.GetUserByUsername(command.Arguments);

if (target != null)
{
this._match.SetForceMatch(user.UserId, target.UserId);
}

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

break;
}
}
}
}
23 changes: 23 additions & 0 deletions Refresh.GameServer/Services/MatchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using NotEnoughLogs;
using System.Reflection;
using Bunkum.HttpServer.Responses;
using MongoDB.Bson;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Types.Matching;
Expand All @@ -16,6 +17,8 @@ public partial class MatchService : EndpointService
private readonly List<IMatchMethod> _matchMethods = new();

private readonly List<GameRoom> _rooms = new();

private readonly Dictionary<ObjectId, ObjectId> _forceMatches = new();

public IEnumerable<GameRoom> Rooms
{
Expand Down Expand Up @@ -122,4 +125,24 @@ public Response ExecuteMethod(string methodStr, SerializedRoomData roomData, Gam

return method.Execute(this, this.Logger, database, user, token, roomData);
}

public void SetForceMatch(ObjectId user, ObjectId target)
{
this._forceMatches[user] = target;
}

public ObjectId? GetForceMatch(ObjectId user)
{
if (this._forceMatches.TryGetValue(user, out ObjectId target))
{
return target;
}

return null;
}

public void ClearForceMatch(ObjectId user)
{
this._forceMatches.Remove(user);
}
}
3 changes: 3 additions & 0 deletions Refresh.GameServer/Types/Commands/Command.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace Refresh.GameServer.Types.Commands;

public record Command(string Name, string? Arguments);
jvyden marked this conversation as resolved.
Show resolved Hide resolved
26 changes: 19 additions & 7 deletions Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Bunkum.CustomHttpListener.Parsing;
using Bunkum.HttpServer;
using Bunkum.HttpServer.Responses;
using MongoDB.Bson;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
Expand All @@ -20,11 +21,6 @@ public Response Execute(MatchService service, LoggerContainer<BunkumContext> log
Token token,
SerializedRoomData body)
{
if (body.NatType is not { Count: 1 })
{
return BadRequest;
}

GameRoom? usersRoom = service.GetRoomByPlayer(user, token.TokenPlatform, token.TokenGame);
if (usersRoom == null) return BadRequest; // user should already have a room.

Expand All @@ -47,17 +43,33 @@ public Response Execute(MatchService service, LoggerContainer<BunkumContext> log
(levelId == null || r.LevelId == levelId))
.OrderByDescending(r => r.RoomMood)
.ToList();

//When a user is behind a Strict NAT layer, we can only connect them to players with Open NAT types
if (body.NatType[0] == NatType.Strict)
if (body.NatType != null && body.NatType[0] == NatType.Strict)
{
rooms = rooms.Where(r => r.NatType == NatType.Open).ToList();
}

ObjectId? forceMatch = service.GetForceMatch(user.UserId);

//If the user has a forced match
if (forceMatch != null)
{
//Filter the rooms to only the rooms that contain the player we are wanting to force match to
rooms = rooms.Where(r => r.PlayerIds.Any(player => player.Id != null && player.Id == forceMatch.Value)).ToList();
}

if (rooms.Count <= 0)
{
return NotFound; // TODO: update this response, shouldn't be 404
}

//If the user has a forced match and we found a room
if (forceMatch != null)
{
//Clear the user's force match
service.ClearForceMatch(user.UserId);
}

GameRoom room = rooms[Random.Shared.Next(0, rooms.Count)];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,8 @@ public Response Execute(MatchService service, LoggerContainer<BunkumContext> log
Token token,
SerializedRoomData body)
{
if (body.NatType is not { Count: 1 })
{
return BadRequest;
}

if (body.Players == null) return BadRequest;
GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType[0]);
GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType == null ? NatType.Open : body.NatType[0]);

room.LastContact = DateTimeOffset.Now;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,7 @@ public class UpdateRoomDataMethod : IMatchMethod
public Response Execute(MatchService service, LoggerContainer<BunkumContext> logger,
GameDatabaseContext database, GameUser user, Token token, SerializedRoomData body)
{
if (body.NatType is not { Count: 1 })
{
return BadRequest;
}

GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType[0]);
GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType == null ? NatType.Open : body.NatType[0]);
if (room.HostId.Id != user.UserId) return Unauthorized;

room.LastContact = DateTimeOffset.Now;
Expand Down
38 changes: 38 additions & 0 deletions RefreshTests.GameServer/Tests/Commands/CommandParseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Bunkum.HttpServer;
using NotEnoughLogs;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Commands;

namespace RefreshTests.GameServer.Tests.Commands;

public class CommandParseTests : GameServerTest
{
[Test]
public void ParsingTest()
{
LoggerContainer<BunkumContext> logger = new();
CommandService service = new(logger, new MatchService(logger));

Assert.That(service.ParseCommand("/parse test"), Is.EqualTo(new Command("parse", "test")));
Assert.That(service.ParseCommand("/noargs"), Is.EqualTo(new Command("noargs", null)));
Assert.That(service.ParseCommand("/noargs "), Is.EqualTo(new Command("noargs", null)));
}

[Test]
public void NoSlashThrows()
{
LoggerContainer<BunkumContext> logger = new();
CommandService service = new(logger, new MatchService(logger));

Assert.That(() => service.ParseCommand("parse test"), Throws.Exception);
}

[Test]
public void BlankCommandThrows()
{
LoggerContainer<BunkumContext> logger = new();
CommandService service = new(logger, new MatchService(logger));

Assert.That(() => service.ParseCommand("/ test"), Throws.Exception);
}
}
Loading