Skip to content

Commit

Permalink
Matching: Heavily refactor our dive-in code.
Browse files Browse the repository at this point in the history
This commit makes the following changes
 - Don't match players into rooms which won't fit them
 - Don't create 3 copies of the room list worst-case, small optimization
 - Select rooms with a weighted distribution, instead of a uniform distribution. This makes "better" rooms (e.g. higher mood) more likely to be picked.
 - Return a proper 404 status code when no rooms are found.
  • Loading branch information
Beyley committed Apr 20, 2024
1 parent 9ef6a86 commit d46b612
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 22 deletions.
62 changes: 42 additions & 20 deletions Refresh.GameServer/Types/Matching/MatchMethods/FindRoomMethod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,31 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
levelId = body.Slots[1];
}

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

List<GameRoom> rooms = service.RoomAccessor.GetRoomsByGameAndPlatform(token.TokenGame, token.TokenPlatform)
//TODO: Add user option to filter rooms by language
//TODO: Deprioritize rooms which have PassedNoJoinPoint set
//TODO: Filter by BuildVersion

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 &&
(levelId == null || r.LevelId == levelId))
.OrderByDescending(r => r.RoomMood)
.ToList();
// 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
(levelId == null || levelId == 0 || r.LevelId == 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)
// 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())
// Order by descending room mood, so that rooms with higher mood (e.g. allowing more people) get selected more often
// This is a stable sort, which is why the order needs to be shuffled above
.ThenByDescending(r => r.RoomMood);

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

ObjectId? forceMatch = user.ForceMatch;
Expand All @@ -56,36 +68,46 @@ public Response Execute(MatchService service, Logger logger, GameDatabaseContext
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();
rooms = rooms.Where(r => r.PlayerIds.Any(player => player.Id != null && player.Id == forceMatch.Value));
}

if (rooms.Count <= 0)
// 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();

if (roomList.Count <= 0)
{
return NotFound; // TODO: update this response, shouldn't be 404
//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);
}

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

GameRoom room = rooms[Random.Shared.Next(0, rooms.Count)];
// Generate a weighted random number, this is weighted relatively strongly towards lower numbers,
// which makes it more likely to pick rooms with a higher mood, since those are sorted near the start of the array
// Graph: https://www.desmos.com/calculator/aagcmlbb08
double weightedRandom = 1 - Math.Cbrt(1 - Random.Shared.NextDouble());

// 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))];

SerializedRoomMatchResponse roomMatch = new()
{
HostMood = (byte)room.RoomMood,
RoomState = (byte)room.RoomState,
Players = new List<SerializedRoomPlayer>(),
Slots = new List<List<int>>(1)
{
new(1)
{
Players = [],
Slots =
[
[
(byte)room.LevelType,
room.LevelId,
},
},
],
],
};

foreach (GameUser? roomUser in room.GetPlayers(database))
Expand Down
23 changes: 21 additions & 2 deletions RefreshTests.GameServer/Tests/Matching/MatchingTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System.Diagnostics;
using System.Text;
using Bunkum.Core.Responses;
using Newtonsoft.Json;
using Refresh.GameServer.Authentication;
using Refresh.GameServer.Services;
using Refresh.GameServer.Types.Matching;
using Refresh.GameServer.Types.Matching.Responses;
using Refresh.GameServer.Types.UserData;

namespace RefreshTests.GameServer.Tests.Matching;
Expand Down Expand Up @@ -80,7 +83,15 @@ public void DoesntMatchIfNoRooms()
NatType.Open,
},
}, context.Database, user1, token1);
Assert.That(response.StatusCode, Is.EqualTo(NotFound));

// Deserialize the result
List<SerializedStatusCodeMatchResponse> responseObjects =
JsonConvert.DeserializeObject<List<SerializedStatusCodeMatchResponse>>(Encoding.UTF8.GetString(response.Data))!;

//Make sure the only result is a 404 object
Assert.That(responseObjects, Has.Count.EqualTo(1));
Assert.That(response.StatusCode, Is.EqualTo(OK));
Assert.That(responseObjects[0].StatusCode, Is.EqualTo(404));
}

[Test]
Expand Down Expand Up @@ -116,7 +127,15 @@ public void StrictNatCantJoinStrict()
NatType.Strict,
},
}, context.Database, user2, token2);
Assert.That(response.StatusCode, Is.EqualTo(NotFound));

//Deserialize the result
List<SerializedStatusCodeMatchResponse> responseObjects =
JsonConvert.DeserializeObject<List<SerializedStatusCodeMatchResponse>>(Encoding.UTF8.GetString(response.Data))!;

//Make sure the only result is a 404 object
Assert.That(responseObjects, Has.Count.EqualTo(1));
Assert.That(response.StatusCode, Is.EqualTo(OK));
Assert.That(responseObjects[0].StatusCode, Is.EqualTo(404));
}

[Test]
Expand Down

0 comments on commit d46b612

Please sign in to comment.