Skip to content

Commit

Permalink
FindRoomMethod: Fix level ID search & always print room state (#548)
Browse files Browse the repository at this point in the history
Printing the room state out always helps a lot with debugging when
people state that "dive in doesn't work".
Often by the time someone responds "can you take a capture of the API
room output", the room state has changed enough where it's not as
helpful, the API also doesn't expose info like NAT type or build
version. This change makes sure that we can easily see exactly what the
server was seeing when it filtered down the rooms, and can narrow down
why no rooms were found.

If the extra noise does become a problem in the future when we grow and
have many rooms, I have made the dump a config option, so that it can be
easily disabled/re-enabled when needed.

The level ID search changes should also fix some issues people have been
having with dive in.
  • Loading branch information
jvyden authored Jul 9, 2024
2 parents bfddeed + 571829a commit f6083da
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 113 deletions.
6 changes: 5 additions & 1 deletion Refresh.GameServer/Configuration/GameServerConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Refresh.GameServer.Configuration;
[SuppressMessage("ReSharper", "RedundantDefaultMemberInitializer")]
public class GameServerConfig : Config
{
public override int CurrentConfigVersion => 15;
public override int CurrentConfigVersion => 16;
public override int Version { get; set; } = 0;

protected override void Migrate(int oldVer, dynamic oldConfig) {}
Expand Down Expand Up @@ -45,4 +45,8 @@ protected override void Migrate(int oldVer, dynamic oldConfig) {}
/// The amount of data the user is allowed to upload before all resource uploads get blocked, defaults to 100mb.
/// </summary>
public int UserFilesizeQuota { get; set; } = 100 * 1_048_576;
/// <summary>
/// Whether to print the room state whenever a `FindBestRoom` match returns no results
/// </summary>
public bool PrintRoomStateWhenNoFoundRooms { get; set; } = true;
}
12 changes: 8 additions & 4 deletions Refresh.GameServer/Endpoints/Game/MatchingEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
using Bunkum.Listener.Protocol;
using Bunkum.Protocols.Http;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Data;
using Refresh.GameServer.Types.Matching;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Endpoints.Game;

Expand All @@ -17,7 +17,11 @@ public class MatchingEndpoints : EndpointGroup
// [FindBestRoom,["Players":["VitaGamer128"],"Reservations":["0"],"NAT":[2],"Slots":[[5,0]],"Location":[0x17257bc9,0x17257bf2],"Language":1,"BuildVersion":289,"Search":"","RoomState":3]]
[GameEndpoint("match", HttpMethods.Post, ContentType.Json)]
[DebugRequestBody, DebugResponseBody]
public Response Match(RequestContext context, GameDatabaseContext database, GameUser user, Token token, MatchService service, string body)
public Response Match(
RequestContext context,
string body,
DataContext dataContext,
GameServerConfig gameServerConfig)
{
(string method, string jsonBody) = MatchService.ExtractMethodAndBodyFromJson(body);
context.Logger.LogInfo(BunkumCategory.Matching, $"Received {method} match request, data: {jsonBody}");
Expand All @@ -35,7 +39,7 @@ public Response Match(RequestContext context, GameDatabaseContext database, Game
return BadRequest;
}

return service.ExecuteMethod(method, roomData, database, user, token);
return dataContext.Match.ExecuteMethod(method, roomData, dataContext, gameServerConfig);
}

// Sent by LBP1 to notify the server it has entered a level.
Expand Down
7 changes: 4 additions & 3 deletions Refresh.GameServer/Services/MatchService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
using Bunkum.Core.Responses;
using Bunkum.Listener.Protocol;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Types.Data;
using Refresh.GameServer.Types.Matching;
using Refresh.GameServer.Types.Matching.MatchMethods;
using Refresh.GameServer.Types.Matching.Responses;
Expand Down Expand Up @@ -131,12 +132,12 @@ public override void Initialize()
private IMatchMethod? TryGetMatchMethod(string method)
=> this._matchMethods.FirstOrDefault(m => m.MethodNames.Contains(method));

public Response ExecuteMethod(string methodStr, SerializedRoomData roomData, GameDatabaseContext database, GameUser user, Token token)
public Response ExecuteMethod(string methodStr, SerializedRoomData roomData, DataContext dataContext, GameServerConfig gameServerConfig)
{
IMatchMethod? method = this.TryGetMatchMethod(methodStr);
if (method == null) return BadRequest;

Response response = method.Execute(this, this.Logger, database, user, token, roomData);
Response response = method.Execute(dataContext, roomData, gameServerConfig);

// If there's a response data specified, then there's nothing more we need to do
if (response.Data.Length != 0)
Expand Down
22 changes: 9 additions & 13 deletions Refresh.GameServer/Types/Matching/MatchMethods/CreateRoomMethod.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
using Bunkum.Core;
using Bunkum.Core.Responses;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.UserData;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Types.Data;

namespace Refresh.GameServer.Types.Matching.MatchMethods;

public class CreateRoomMethod : IMatchMethod
{
public IEnumerable<string> MethodNames => new[] { "CreateRoom" };

public Response Execute(MatchService service, Logger logger, GameDatabaseContext database, GameUser user, Token token,
SerializedRoomData body)
public Response Execute(DataContext dataContext, SerializedRoomData body, GameServerConfig gameServerConfig)
{
NatType natType = body.NatType == null ? NatType.Open : body.NatType[0];
GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, natType, body.BuildVersion, body.PassedNoJoinPoint);
if (room.HostId.Id != user.UserId)
GameRoom room = dataContext.Match.GetOrCreateRoomByPlayer(dataContext.User!, dataContext.Platform, dataContext.Game, natType, body.BuildVersion, body.PassedNoJoinPoint);
if (room.HostId.Id != dataContext.User!.UserId)
{
room = service.SplitUserIntoNewRoom(user, token.TokenPlatform, token.TokenGame, natType, body.BuildVersion, body.PassedNoJoinPoint);
room = dataContext.Match.SplitUserIntoNewRoom(dataContext.User, dataContext.Platform, dataContext.Game, natType, body.BuildVersion, body.PassedNoJoinPoint);
}

if (body.RoomState != null) room.RoomState = body.RoomState.Value;

if (body.Slots.Count > 1)
{
logger.LogWarning(BunkumCategory.Matching, "Received create room request with multiple slots, rejecting");
dataContext.Logger.LogWarning(BunkumCategory.Matching, "Received create room request with multiple slots, rejecting");
return BadRequest;
}

foreach(List<int> slot in body.Slots)
{
if (slot.Count != 2)
{
logger.LogWarning(BunkumCategory.Matching, "Received request with invalid slot, rejecting.");
dataContext.Logger.LogWarning(BunkumCategory.Matching, "Received request with invalid slot, rejecting.");
return BadRequest;
}

Expand All @@ -48,7 +44,7 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
room.RoomMood = (RoomMood)mood;
}

service.RoomAccessor.UpdateRoom(room);
dataContext.Match.RoomAccessor.UpdateRoom(room);

return OK;
}
Expand Down
94 changes: 58 additions & 36 deletions Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
using MongoDB.Bson;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Database;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Data;
using Refresh.GameServer.Types.Levels;
using Refresh.GameServer.Types.Matching.Responses;
using Refresh.GameServer.Types.UserData;

Expand All @@ -15,50 +18,60 @@ public class FindRoomMethod : IMatchMethod
{
public IEnumerable<string> MethodNames => new[] { "FindBestRoom" };

public Response Execute(MatchService service, Logger logger, GameDatabaseContext database,
GameUser user,
Token token,
SerializedRoomData body)
public Response Execute(DataContext dataContext, SerializedRoomData body, GameServerConfig gameServerConfig)
{
GameRoom? usersRoom = service.RoomAccessor.GetRoomByUser(user, token.TokenPlatform, token.TokenGame);
GameRoom? usersRoom = dataContext.Match.RoomAccessor.GetRoomByUser(dataContext.User!, dataContext.Platform, dataContext.Game);
if (usersRoom == null) return BadRequest; // user should already have a room.

List<int> levelIds = new(body.Slots.Count);
// We only really need to match level IDs for user or developer levels
List<int> userLevelIds = [];
List<int> developerLevelIds = [];
// Iterate over all sent slots and append their IDs to the list of level IDs to check
foreach(List<int> slot in body.Slots)
{
if (slot.Count != 2)
{
logger.LogWarning(BunkumCategory.Matching, "Received request with invalid slot, rejecting.");
dataContext.Logger.LogWarning(BunkumCategory.Matching, "Received request with invalid slot, rejecting.");
return BadRequest;
}

RoomSlotType slotType = (RoomSlotType)slot[0];
int slotId = slot[1];

// 0 means "no level specified"
if (slot[1] == 0)
if (slotId == 0)
continue;

levelIds.Add(slot[1]);
switch (slotType)
{
case RoomSlotType.Online:
userLevelIds.Add(slotId);
break;
case RoomSlotType.Story:
developerLevelIds.Add(slotId);
break;
}
}

// If we are on vita and the game specified more than one level ID, then its trying to do dive in only to players in a certain category of levels
// This is not how most people expect dive in to work, so let's pretend that the game didn't specify any level IDs whatsoever, so they will get matched with all players
if (token.TokenGame == TokenGame.LittleBigPlanetVita && levelIds.Count > 1)
levelIds = [];
if (dataContext.Game == TokenGame.LittleBigPlanetVita && userLevelIds.Count > 1)
userLevelIds = [];

//TODO: Add user option to filter rooms by language

List<GameRoom> allRooms = dataContext.Match.RoomAccessor
.GetRoomsByGameAndPlatform(dataContext.Game, dataContext.Platform)
.Where(r => r.RoomId != usersRoom.RoomId).ToList();

IEnumerable<GameRoom> rooms = service.RoomAccessor
// Get all the available rooms
.GetRoomsByGameAndPlatform(token.TokenGame, token.TokenPlatform)
.Where(r =>
// Make sure we don't match the user into their own room
r.RoomId != usersRoom.RoomId &&
IEnumerable<GameRoom> rooms = allRooms.Where(r =>
// If the level id isn't specified, or is 0, then we don't want to try to match against level IDs, else only match the user to people who are playing that level
(levelIds.Count == 0 || levelIds.Contains(r.LevelId)) &&
(userLevelIds.Count == 0 || r.LevelType != RoomSlotType.Online || userLevelIds.Contains(r.LevelId)) &&
(developerLevelIds.Count == 0 || r.LevelType != RoomSlotType.Story || developerLevelIds.Contains(r.LevelId)) &&
// Make sure that we don't try to match the player into a full room, or a room which won't fit the user's current room
usersRoom.PlayerIds.Count + r.PlayerIds.Count <= 4 &&
// Match the build version of the rooms
(r.BuildVersion ?? 0) == body.BuildVersion)
// Match the build version of the rooms, or dont match build versions if the game doesnt specify it
(body.BuildVersion == null || (r.BuildVersion ?? 0) == body.BuildVersion))
// Shuffle the rooms around before sorting, this is because the selection is based on a weighted average towards the top of the range,
// so there would be a bias towards longer lasting rooms without this shuffle
.OrderBy(r => Random.Shared.Next())
Expand All @@ -74,7 +87,7 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
rooms = rooms.Where(r => r.NatType == NatType.Open);
}

ObjectId? forceMatch = user.ForceMatch;
ObjectId? forceMatch = dataContext.User!.ForceMatch;

//If the user has a forced match
if (forceMatch != null)
Expand All @@ -84,18 +97,27 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
}

// Now that we've done all our filtering, lets convert it to a list, so we can index it quickly.
List<GameRoom> roomList = rooms.ToList();
List<GameRoom> foundRooms = rooms.ToList();

if (roomList.Count <= 0)
// If there's no rooms, dump an "overview" of the global room state, to help debug matching issues
if (foundRooms.Count <= 0 && gameServerConfig.PrintRoomStateWhenNoFoundRooms)
{
#if DEBUG
logger.LogDebug(BunkumCategory.Matching, "Room search by {0} on {1} ({2}) returned no results, dumping list of available rooms.", user.Username, token.TokenGame, token.TokenPlatform);

// Dump an "overview" of the global room state, to help debug matching issues
rooms = service.RoomAccessor.GetRoomsByGameAndPlatform(token.TokenGame, token.TokenPlatform);
foreach (GameRoom logRoom in rooms)
logger.LogDebug(BunkumCategory.Matching,"Room {0} has NAT type {1} and is on level {2}", logRoom.RoomId, logRoom.NatType, logRoom.LevelId);
#endif
if (allRooms.Count == 0)
{
dataContext.Logger.LogDebug(BunkumCategory.Matching,
"Room search by {0} on {1} ({2}) returned no results due to there being no open rooms on the game/platform.",
dataContext.User.Username, dataContext.Game, dataContext.Platform);
}
else
{
dataContext.Logger.LogDebug(BunkumCategory.Matching,
"Room search by {0} on {1} ({2}) returned no results, dumping list of possible rooms.",
dataContext.User.Username, dataContext.Game, dataContext.Platform);

foreach (GameRoom logRoom in allRooms)
dataContext.Logger.LogDebug(BunkumCategory.Matching, "Room {0}: Nat Type {1}, Level {2} ({3}), Build Version {4}",
logRoom.RoomId, logRoom.NatType, logRoom.LevelId, logRoom.LevelType, logRoom.BuildVersion ?? 0);
}

// Return a 404 status code if there's no rooms to match them to
return new Response(new List<object> { new SerializedStatusCodeMatchResponse(404), }, ContentType.Json);
Expand All @@ -105,7 +127,7 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
if (forceMatch != null)
{
// Clear the user's force match
database.ClearForceMatch(user);
dataContext.Database.ClearForceMatch(dataContext.User);
}

// Generate a weighted random number, this is weighted relatively strongly towards lower numbers,
Expand All @@ -116,9 +138,9 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext

// Even though NextDouble guarantees the result to be < 1.0, and this mathematically always will check out,
// rounding errors may cause this to become roomList.Count (which would crash), so we use a Math.Min to make sure it doesn't
GameRoom room = roomList[Math.Min(roomList.Count - 1, (int)Math.Floor(weightedRandom * roomList.Count))];
GameRoom room = foundRooms[Math.Min(foundRooms.Count - 1, (int)Math.Floor(weightedRandom * foundRooms.Count))];

logger.LogInfo(BunkumCategory.Matching, "Matched user {0} into {1}'s room (id: {2})", user.Username, room.HostId.Username, room.RoomId);
dataContext.Logger.LogInfo(BunkumCategory.Matching, "Matched user {0} into {1}'s room (id: {2})", dataContext.User.Username, room.HostId.Username, room.RoomId);

SerializedRoomMatchResponse roomMatch = new()
{
Expand All @@ -134,13 +156,13 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
],
};

foreach (GameUser? roomUser in room.GetPlayers(database))
foreach (GameUser? roomUser in room.GetPlayers(dataContext.Database))
{
if(roomUser == null) continue;
roomMatch.Players.Add(new SerializedRoomPlayer(roomUser.Username, 0));
}

foreach (GameUser? roomUser in usersRoom.GetPlayers(database))
foreach (GameUser? roomUser in usersRoom.GetPlayers(dataContext.Database))
{
if(roomUser == null) continue;
roomMatch.Players.Add(new SerializedRoomPlayer(roomUser.Username, 1));
Expand Down
10 changes: 3 additions & 7 deletions Refresh.GameServer/Types/Matching/MatchMethods/IMatchMethod.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
using Bunkum.Core.Responses;
using JetBrains.Annotations;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.UserData;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Types.Data;

namespace Refresh.GameServer.Types.Matching.MatchMethods;

Expand All @@ -13,6 +10,5 @@ public interface IMatchMethod
{
IEnumerable<string> MethodNames { get; }

Response Execute(MatchService service, Logger logger,
GameDatabaseContext database, GameUser user, Token token, SerializedRoomData body);
Response Execute(DataContext dataContext, SerializedRoomData body, GameServerConfig gameServerConfig);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using Bunkum.Core.Responses;
using NotEnoughLogs;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Database;
using Refresh.GameServer.Services;
using Refresh.GameServer.Configuration;
using Refresh.GameServer.Types.Data;
using Refresh.GameServer.Types.UserData;

namespace Refresh.GameServer.Types.Matching.MatchMethods;
Expand All @@ -11,22 +9,19 @@ public class UpdatePlayersInRoomMethod : IMatchMethod
{
public IEnumerable<string> MethodNames => new[] { "UpdatePlayersInRoom" };

public Response Execute(MatchService service, Logger logger, GameDatabaseContext database,
GameUser user,
Token token,
SerializedRoomData body)
public Response Execute(DataContext dataContext, SerializedRoomData body, GameServerConfig gameServerConfig)
{
if (body.Players == null) return BadRequest;
GameRoom room = service.GetOrCreateRoomByPlayer(user, token.TokenPlatform, token.TokenGame, body.NatType == null ? NatType.Open : body.NatType[0], body.BuildVersion, body.PassedNoJoinPoint);
GameRoom room = dataContext.Match.GetOrCreateRoomByPlayer(dataContext.User!, dataContext.Platform, dataContext.Game, body.NatType == null ? NatType.Open : body.NatType[0], body.BuildVersion, body.PassedNoJoinPoint);

foreach (string playerUsername in body.Players)
{
GameUser? player = database.GetUserByUsername(playerUsername);
GameUser? player = dataContext.Database.GetUserByUsername(playerUsername);

if (player != null)
service.AddPlayerToRoom(player, room, token.TokenPlatform, token.TokenGame);
dataContext.Match.AddPlayerToRoom(player, room, dataContext.Platform, dataContext.Game);
else
service.AddPlayerToRoom(playerUsername, room);
dataContext.Match.AddPlayerToRoom(playerUsername, room);
}

return OK;
Expand Down
Loading

0 comments on commit f6083da

Please sign in to comment.