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 queryable columns are ordinary custom properties, so they can be seeded
   at creation and the key list published afterwards. */
rt_blob props = {0};
rt_blob keys  = {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: */
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);

/* Publishing to a Named SQL Lobby Needs C++: realtime_create_room has no
   lobbyName or lobbyType parameter, so a room a C client creates is always
   listed in the default lobby of the default type. The queryable C0 to C9
   values and the LobbyProperties key list can be set as shown above, but
   the room does not reach a SQL lobby, so no SQL filter will find it.
   Create rooms for a SQL lobby from C++, and use C for the query side. */

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

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 == RT_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

char filter[128];
snprintf(filter, sizeof(filter), "C1 BETWEEN %d AND %d", elo - 200, elo + 200);

realtime_join_random_room(client,
                          0,        /* maxPlayers: match any     */
                          0,        /* matchmakingMode: FillRoom */
                          "ranked", /* lobbyName                 */
                          2,        /* lobbyType: SqlLobby       */
                          filter);

/* On NoMatchFound, widen the filter and search again. */
#define RT_ERROR_NO_MATCH_FOUND 24

if (e->type == RT_JoinRandomRoomResult && e->mode == RT_ERROR_NO_MATCH_FOUND)
{
    snprintf(filter, sizeof(filter), "C1 BETWEEN %d AND %d", elo - 600, elo + 600);
    realtime_join_random_room(client, 0, 0, "ranked", 2, filter);
}

/* Widening the filter is the fallback a C client has, because the room it
   would create instead cannot be published to the SQL lobby. Chaining
   ordered fallbacks with ; inside one filter string does the same thing in
   a single round trip. */
Back to top