SQL Lobby Matchmaking
When to Use the SQL Lobby
The default lobby matches rooms by exact property equality, which cannot express "close to my skill" or "any of these maps". The SQL lobby replaces equality matching with query-style filters, so clients can match on ranges, alternatives and combinations of room properties.
Typical cases are skill or ELO ranges, map-and-mode combinations and versioned or regional room buckets, anywhere a single equality comparison is not enough.
Publishing Rooms to a SQL Lobby
A room enters a SQL lobby at creation time.
In the room's create options, set the lobby type to SQL lobby and give the lobby a name, list the queryable keys as lobby properties and supply their values as custom properties.
The queryable keys use the reserved column names C0 to C9.
Only the properties named C0 to C9 are queryable, and only with string or integer values.
Every other custom property stays invisible to filters, no matter what it contains.
Publish the game mode and the room's ELO rating as queryable columns:
C++
Task<Result<Room>> CreateRankedRoom(RealtimeClient& client)
{
CreateRoomOptions options;
options.MaxPlayers = 8;
options.Lobby = LobbyType::SqlLobby;
options.LobbyName = PHOTON_STR("ranked");
options.LobbyProperties = {PHOTON_STR("C0"), PHOTON_STR("C1")};
options.CustomProperties.Set(PHOTON_STR("C0"), PHOTON_STR("arena")); // game mode
options.CustomProperties.Set(PHOTON_STR("C1"), static_cast<int32_t>(1450)); // room ELO
co_return co_await client.CreateRoom({}, options);
}
C
/* The rt_* blob helpers, copied from "Blob Helpers" on the C API page. */
#include "photon_blob.h"
/* realtime_create_room has no lobby parameters: a room is listed in the lobby
its creator is in, so join the SQL lobby first. One join switches lobbies. */
realtime_join_lobby(client, "ranked", 2); /* lobbyType 2 = SqlLobby */
/* ... in the JoinLobbyResult handler: */
rt_blob props = {0};
rt_begin(&props);
rt_put_str(&props, "C0", "arena"); /* game mode */
rt_put_i32(&props, "C1", 1450); /* room ELO */
rt_end(&props);
realtime_create_room(client, NULL, 8, 1, 1, 0, 0, NULL, props.data, props.len);
rt_free(&props);
/* ... in the CreateRoomResult handler: LobbyProperties are applied after the
creation, once in the room, and are what makes C0 and C1 queryable. */
rt_blob keys = {0};
rt_begin(&keys);
rt_add_str(&keys, "C0");
rt_add_str(&keys, "C1");
rt_end(&keys);
realtime_room_set_lobby_properties(client, keys.data, keys.len);
rt_free(&keys);
JavaScript
const room = await client.createRoom('', {
maxPlayers: 8,
lobby: 'sqlLobby',
lobbyName: 'ranked',
lobbyProperties: ['C0', 'C1'],
customProperties: {
C0: 'arena', // game mode
C1: 1450, // room ELO, an integer, so it is sent as an int32
},
});
An empty room name lets the server pick one.
The room stays in the lobby it was created in for its whole lifetime, so the creator is free to leave or switch lobbies afterwards.
Filter Syntax
A filter is a SQL-like WHERE clause over the columns C0 to C9.
It supports the comparison operators, AND and OR combinations, BETWEEN for ranges and IN for alternatives.
| Operator | Sample Filter |
|---|---|
= / != |
C0 = 'arena' |
< / <= / > / >= |
C1 > 1000 |
BETWEEN |
C1 BETWEEN 100 AND 200 |
IN |
C0 IN ('ctf', 'tdm') |
AND / OR |
C0 = 'arena' AND C1 > 1000 |
Several filters can be chained with ; as ordered fallbacks.
The server evaluates them left to right and the first filter that matches a room wins, so a strict query can carry progressively wider alternatives in a single call.
Querying the Room List
A room list query takes a lobby name and a filter and returns the rooms matching the filter as room listings, the building block for a room browser. Each listing carries the room name, player counts and lobby-visible custom properties; see Room Lists for the fields and Data Types for the C++ types.
| C++ | C | JavaScript |
|---|---|---|
GetRoomList() |
realtime_get_room_list() |
getRoomList() |
Query the lobby and evaluate the listings:
C++
Task<Result<int>> CountOpenArenaRooms(RealtimeClient& client)
{
Result<std::vector<RoomListing>, RoomListError> rooms = co_await client.GetRoomList(
PHOTON_STR("ranked"), PHOTON_STR("C0 = 'arena' AND C1 BETWEEN 1000 AND 2000"));
if (rooms.IsErr())
{
co_return Result<int>::Err(rooms.GetError());
}
int openRooms = 0;
for (const RoomListing& listing : rooms.GetValue())
{
if (listing.IsOpen)
{
++openRooms;
}
}
co_return Result<int>::Ok(openRooms);
}
C
/* The rt_* blob helpers, copied from "Blob Helpers" on the C API page. */
#include "photon_blob.h"
realtime_get_room_list(client, "ranked", "C0 = 'arena' AND C1 BETWEEN 1000 AND 2000");
/* GetRoomListResult carries [int32 count] then, per room,
[string name][int32 playerCount][u8 maxPlayers][u8 isOpen]
[int32 directMessaging][map customProperties] */
if (e->type == RealtimeEventType_GetRoomListResult && e->mode == 0 && e->blobOffset >= 0)
{
rt_reader r = rt_read(blob + e->blobOffset, e->blobLength);
int32_t rooms = rt_get_i32(&r);
int openRooms = 0;
for (int32_t i = 0; i < rooms; ++i)
{
int32_t nameLen = 0;
(void)rt_get_str(&r, &nameLen);
(void)rt_get_i32(&r); /* playerCount */
(void)rt_get_u8(&r); /* maxPlayers */
if (rt_get_u8(&r) != 0) /* isOpen */
{
++openRooms;
}
(void)rt_get_i32(&r); /* directMessaging */
rt_skip_map(&r); /* customProperties */
}
printf("%d open arena rooms\n", openRooms);
}
/* The filter string is passed through unchanged, so every operator in the
filter-syntax table works from C. realtime_get_cached_room_list returns
the same layout for the last list the server pushed, and RoomListUpdated
(103) delivers it as an event while the client is in a lobby. */
JavaScript
const rooms = await client.getRoomList('ranked', "C0 = 'arena' AND C1 BETWEEN 1000 AND 2000");
const openRooms = rooms.filter((room) => room.isOpen).length;
console.log(`${openRooms} open arena rooms`);
Joining with a Filter
A random join runs the same filters for matchmaking. In the matchmaking options, set the lobby type to SQL lobby, the lobby name and the SQL filter, and the server picks a random room satisfying the filter.
When no room satisfies the filter, the join fails with the no-match-found error (JoinRoomError::NoMatchFound in C++, RealtimeJoinRoomError_NoMatchFound in C, 'noMatchFound' in JavaScript).
Handle it by widening the filter in a follow-up attempt or by creating a room that satisfies the original query, so the next searcher finds it; join-random-or-create combines both steps in one operation.
Search near the player's skill and create a matching room when nothing is found:
C++
Task<Result<Room>> JoinNearSkill(RealtimeClient& client, int32_t elo)
{
MatchmakingOptions matchmaking;
matchmaking.Lobby = LobbyType::SqlLobby;
matchmaking.LobbyName = PHOTON_STR("ranked");
matchmaking.SqlFilter = PHOTON_STR("C1 BETWEEN ") + RealtimeCore::Common::ToStringType(elo - 200) + PHOTON_STR(" AND ") + RealtimeCore::Common::ToStringType(elo + 200);
Result<Room, JoinRoomError> joined = co_await client.JoinRandomRoom(matchmaking);
if (joined.IsOk() || joined.GetErrorCode() != JoinRoomError::NoMatchFound)
{
co_return joined;
}
CreateRoomOptions create;
create.Lobby = LobbyType::SqlLobby;
create.LobbyName = PHOTON_STR("ranked");
create.LobbyProperties = {PHOTON_STR("C1")};
create.CustomProperties.Set(PHOTON_STR("C1"), elo);
co_return co_await client.CreateRoom({}, create);
}
C
/* The rt_* blob helpers, copied from "Blob Helpers" on the C API page. */
#include "photon_blob.h"
/* The lobby to search and the filter are parameters of the search itself. */
realtime_join_random_room(client,
0, /* maxPlayers: any */
0, /* mode: FillRoom */
"ranked", /* lobbyName */
2, /* lobbyType: SqlLobby */
"C1 BETWEEN 1250 AND 1650");
/* Fall back to creating a matching room when nothing was found. */
if (e->type == RealtimeEventType_JoinRandomRoomResult && e->mode == RealtimeJoinRoomError_NoMatchFound)
{
/* The search addressed the lobby but did not join it, so join it now: the
room is listed in whichever lobby its creator is in. */
realtime_join_lobby(client, "ranked", 2); /* lobbyType 2 = SqlLobby */
}
/* ... in the JoinLobbyResult handler, create the room the search did not find: */
rt_blob props = {0};
rt_begin(&props);
rt_put_i32(&props, "C1", 1450);
rt_end(&props);
realtime_create_room(client, NULL, 8, 1, 1, 0, 0, NULL, props.data, props.len);
rt_free(&props);
/* ... in the CreateRoomResult handler, publish C1 so the next searcher's
filter can see it: */
rt_blob keys = {0};
rt_begin(&keys);
rt_add_str(&keys, "C1");
rt_end(&keys);
realtime_room_set_lobby_properties(client, keys.data, keys.len);
rt_free(&keys);
/* realtime_join_random_or_create_room takes no matchmaking parameters at all -
no lobby, no filter - so it cannot stand in for the search above. A filtered
search plus fallback has to be written as the two-step flow here, which
leaves the race JoinRandomOrCreateRoom closes: another client can create a
matching room between the NoMatchFound and the create. */
JavaScript
import { isRealtimeError } from '@photonengine/realtime-core';
const elo = 1450;
let room;
try {
room = await client.joinRandomRoom({
lobby: 'sqlLobby',
lobbyName: 'ranked',
sqlFilter: `C1 BETWEEN ${elo - 200} AND ${elo + 200}`,
});
} catch (error) {
if (!isRealtimeError(error) || error.code !== 'noMatchFound') {
throw error;
}
room = await client.createRoom('', {
lobby: 'sqlLobby',
lobbyName: 'ranked',
lobbyProperties: ['C1'],
customProperties: { C1: elo },
});
}
joinRandomOrCreateRoom() takes the same matchmaking options as its second argument and does both steps in one operation.
Last updated on
Back to top