Nk.match_list() not working with search queries

I’m having trouble with match listing, specifically with search queries as listed here.
https://heroiclabs.com/docs/gameplay-multiplayer-server-multiplayer/#search-query

With an empty search query it works and returns matches, however when I add a query it finds nothing.

I know the match label and search query have the correct parameters since:

  1. The logger says that Created match has label visibility=public,
    which is the value of the label argument at the end of M.match_init that is returned after init. So the match inits with the correct label, "visibility=public".

  2. And also from the log Recieved query is +label.visibility:public,
    which is the value of the query being searched for.
    I also also tried force-entering a search query in case there was an encoding issue, but there isn’t.
    The query is "+label.visibility:public".

The exact method call is nk.match_list(search_limit, isAuthoritative, '', min_size, max_size, s_query)

I also tried replacing the empty label with nill instead of empty quotes with no difference.

Don’t know what could have caused that issue.
But here’s an example from the code we use and it works fine -
inside match_init we return the label like this. Updated the label to your use case -

function m_h.match_init(context, state)
    nk.logger_info("[match_init] : inside new match init. Pre-state = " .. nk.json_encode(state))
    nk.logger_info("[match_init] : new match id = [" .. context.match_id .. "]")

    local label = {
        visibility = "public"
    }
    local match_label = nk.json_encode(label)

    local tickrate = 1 -- per sec

    return state, tickrate, match_label
end

Then for searching matches you could use -

function s_m.list_matches(context, payload)
    local json = nk.json_decode(payload)

    --[[
        ------------------ fetching from the live tables ---------------------------
        List at most 10 matches, authoritative true, and that
        have between min_size and max_size players currently participating.
    --]]
    local limit = 10
    local authoritative = true
    local min_size = 0
    local max_size = 12

    local visibility = json.visibilty -- this will have value "public"
   
    local filter = "+label.visibility:".. tostring(visibility)
    
    --Searching matches with filter
    local matches_list = nk.match_list(limit, authoritative, nil, min_size, max_size, filter)
    local response = {}

    if(matches_list) then
         local no_of_matches = #matches_list
         nk.logger_info("[search_matches/list_matches] : Found matches Count : " .. no_of_matches)
        --nk.logger_info(nk.json_encode(matches_list))
        
        if(no_of_matches == 0) then
            nk.logger_info("no matches found")
            response["matches"] = nil
        else
            nk.logger_info("matches found")
            response["matches"] = matches_list
        end
    else
        nk.logger_info("no matches found")
        response["matches"] = nil
    end

    return nk.json_encode(response)
end

I hope this helps.

1 Like

Thank you, it seemed I had misunderstood how the label should be formatted.

The problem was that I was sending a string with the value of literally “visibility=public” since I did not know it was supposed to be an object looking at the docs.

To fix it, now I send a json object from the client with value {label: {visiblity : "public"}} as the setup payload for the match, and then on the server I use nk.json_decode() in match_create_rpc() to save it to the match’s setup state, and then I assign in M.match_init() normally by reading it from the match’s setup state.

Here’s some of the code to clarify what is working:

Client Side:

    async create_public_match(payload = null) {
        var data = payload || { label: { visibility: "public" } };

        var match_id = await multiplayer._create_match(data);

        return match_id;
    }

    async _create_match(match_payload: object) {
        var match_id = await backendManager.send_rpc("create_match_rpc", match_payload);

        if (match_id && match_id.payload && match_id.payload.match_id) {
            console.log("Succesfully created new match.");
            return match_id.payload.match_id;
        } else {
            console.log("Error creating new match.");
            return null;
        }


    }

Server Side:

local nk = require('nakama')

local function create_match(context, payload)
    local modulename = 'match_handler'
    local setupstate = {initialstate = nk.json_decode(payload)}

    local matchid = nk.match_create(modulename, setupstate)

    -- Send notification of some kind
    return nk.json_encode({['match_id'] = matchid, ['setupstate'] = setupstate})
end
nk.register_rpc(create_match, 'create_match_rpc')

local min_size = 0
local max_size = 2
local search_limit = 100
local isAuthoritative = true
local default_search_query = '+label.visibility:public'
local function find_match(search_query)
    print('Searching with recieved query: ' .. search_query)
    local s_query = search_query or default_search_query

    local matches = nk.match_list(search_limit, isAuthoritative, nil, min_size, max_size, s_query)
    if (#matches > 0) then
        table.sort(
            matches,
            function(a, b)
                return a.size > b.size
            end
        )
        return matches[1].match_id
    else
        print('Could not find matching matches.')
        return nil
    end
end

local function quick_find_match(context, payload)
    local found_match_id = find_match(payload and nk.json_decode(payload).search_label)
    nk.logger_info(('Quick find game result is %s'):format(found_match_id))
    return nk.json_encode({['match_id'] = found_match_id})
end

nk.register_rpc(quick_find_match, 'quick_find_match_rpc')

Also if I make ask, what is the purpose of min_size and max_size in this method call?

@GroovyAntoidDev There’s a bit of history to our match labels. If a label is formatted in the way you’d done in your first example it will not be indexed by the matchmaker. Instead it would be used to exact match the input string from game clients. This is why your query was ignored.

You should make the inputs you want to query in a JSON object for it to be indexed by the matchmaker. Thanks to @hdjay0129 for his example on how to do it.

Also if I make ask, what is the purpose of min_size and max_size in this method call?

The min and max size fields are used to filter to matches returned which contain the range of players between those two values. i.e. Find me matches which have at least 1 player in and no more than 12 players. This would be represented with a min_size of 1 and a max_size of 12.

1 Like

@novabyte It seems the size is based on active presences, is it possible to get the total number of players who joined even if they’re offline, since for turn-based games most of the time the players are not both online and they join and disconnect regularly.

If that’s not possible, is it possible to edit the label after creation to indicate whether the match is full or not, or perhaps use the returned data in the match list to get the match state?

Edit: To clarify, what I am trying to do is that games have a 2 player limit, and I register the joined players in the match state and the match will reject joins when it reaches 2 registered players unless they are one of the two who had registered before.

These 2 players have the match id recorded and they can currently seamlessly and perfectly switch from realtime to turn based play depending on whether the opponent is online.

However, I need other players to be able to filter the lobby list to get matches they can join i.e. registered users < 2, which requires accessing the match state or if possible filtering the match list in a better way.

I think you can store the total no of joined players inside the match label and update it according whenever a player join or leave and then call this -
"label.total_joined:>" .. min_joined .. " AND label.total_joined:<" .. max_joined

@hdjay0129 Thank you, that would indeed fix it, but I could not find info in the docs about where the label is stored after initialization, is it inside the match state or somewhere else?

You can use this to update label in game -
dispatcher.match_label_update("updatedlabel")
more info - https://heroiclabs.com/docs/gameplay-multiplayer-server-multiplayer/#match-runtime-api

1 Like

Also, if you only want two players to join the match. Then you can track the no of joined players inside the state and then check for total joined inside match_joined_attempt callback to check for your conditions.
Like this -

local function match_join_attempt(context, dispatcher, tick, state, presence, metadata)
  -- Presence format:
  -- {
  --   user_id = "user unique ID",
  --   session_id = "session ID of the user's current connection",
  --   username = "user's unique username",
  --   node = "name of the Nakama node the user is connected to"
  -- }
  local accept_user = true
  local reason = ""
  if(state.total_joined == 2) then
    accept_user = false
    reason = "match full"
  end
  return state, accept_user, reason
end

EDIT : I think you are already doing this. But just in case.
Also, I think, it would be better to open another thread for this :smile:

1 Like

Thanks for that dispatcher.match_label_update method! I swear I literally just read the runtime api but I didn’t go past that section.

And also thank you for the second snippet, I am doing exactly that in the match_join_attempt() and it works perfectly.

1 Like

Great :+1:
Glad to know it helped

Isn’t the label an object now, how does this work with a string?

Edit: Ah yeah I guess I should open another thread, but I guess technically we’re still talking about match labels :smiley:

I think, we are using label as object on top level but internally it’s still being used as string for backward compatibility.
@novabyte might know more.

Yes. It’s as @hdjay0129 mentioned. Internally the game server still respects the match label as a string literal but you would store within it a JSON object for it to be indexed by the matchmaker.

Edit: I managed to get it to work, I will post some code here in a few moments and then close this thread :b

Thanks everyone!

Some server-side snippets:
(Note that these are tiny fragments that are not functional on their own, look at earlier comments in this post and at the docs for a full example of the match handler).

local function find_match(search_query)
    local s_query = (search_query or default_search_query) .. ' +label.total_joined:<2'
    print('Searching with the query: ' .. s_query)

    local matches = nk.match_list(search_limit, isAuthoritative, nil, min_size, max_size, s_query)

local default_new_match_label = {visibility = 'private'}
function M.match_init(context, setupstate)
    local tickrate = 1 -- per sec
    local g_label = setupstate.initialstate.label or default_new_match_label
    g_label.total_joined = 0

    local gamestate = {
        label = g_label,
        other =properties,
etc...
    }

    return gamestate, tickrate, nk.json_encode(g_label)
end
function M.match_join(context, dispatcher, tick, state, presences)
    for _, presence in ipairs(presences) do
        state.presences[presence.user_id] = presence

        state.label.total_joined = state.label.total_joined + 1
        local new_label = nk.json_encode(state.label)
        print('Match label is now ' .. new_label)
        dispatcher.match_label_update(new_label)