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. Set CreateRoomOptions.Lobby = LobbyType::SqlLobby and a LobbyName, list the queryable keys in LobbyProperties and supply their values in CustomProperties. 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++
C

C++

Task<Result<MutableRoomView>> 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[PHOTON_STR("C0")] = PHOTON_STR("arena");        // game mode
    options.CustomProperties[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);

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

GetRoomList(lobbyName, sqlFilter) returns the rooms matching the filter as RoomListing entries, the building block for a room browser. Each listing carries the room name, player counts and lobby-visible custom properties; see Data Types for all fields.

Query the lobby and evaluate the listings:

C++
C

C++

Task<Result<int>> CountOpenArenaRooms(RealtimeClient& client)
{
    Result<std::vector<RoomListing>> 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. */

Joining with a Filter

JoinRandomRoom runs the same filters for matchmaking. Set MatchmakingOptions.Lobby = LobbyType::SqlLobby, the LobbyName and the SqlFilter, and the server picks a random room satisfying the filter.

When no room satisfies the filter the Task resolves to ErrorCode::NoMatchFound. 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; JoinRandomOrCreateRoom combines both steps in one operation.

Search near the player's skill and create a matching room when nothing is found:

C++
C

C++

Task<Result<MutableRoomView>> 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<MutableRoomView> joined = co_await client.JoinRandomRoom(matchmaking);
    if (joined.IsOk() || joined.GetErrorCode() != ErrorCode::NoMatchFound)
    {
        co_return joined;
    }

    CreateRoomOptions create;
    create.Lobby                               = LobbyType::SqlLobby;
    create.LobbyName                           = PHOTON_STR("ranked");
    create.LobbyProperties                     = {PHOTON_STR("C1")};
    create.CustomProperties[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. */
#define ERROR_NO_MATCH_FOUND 24

if (e->type == RealtimeEventType_JoinRandomRoomResult && e->mode == ERROR_NO_MATCH_FOUND)
{
    /* 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. */

Last updated on

Back to top