Matchmaker roles

Hello, I’m having the same problem as this topic: Matchmaker Roles Query, as in I need my games to have a specific amount of players per role, instead of simply having X players in a match.

I tried using the Lua runtime to implement this logic but I can’t find the right spot to insert my code. What I mean is that I tried using a Match handler, using the Match.match_join_attempt function to block players whose role is already full from entering. This however still starts the match, giving errors to other players.

It also means (from what I understood) that other players that could join (aka whose role is still open) cannot do it since the match is not in the matchmaker pool anymore. I might be wrong here since I didn’t understand it correctly.

In short my question is: how do I implement this kind of logic using the runtime lua? Is it even possible?

Sounds like you’re using authoritative multiplayer in the Lua runtime, which is a good solution for your problem so you’re on the right track. :+1:

There are two parts you’ll need to adjust to make this work.

(1) Create the concept of a waiting/playing mode in your match handlers. Just because the match handler has started doesn’t have to mean gameplay is in progress. Players might be connected to the match but just sitting on a lobby/waiting screen until enough players join to actually start playing. You would represent this in your match state, coordinate with clients, and start playing when appropriate.

(2) Use the match label and updates to it to “advertise” how many players are still needed and in which roles.

Let’s say you need 2 more “dps” players to start your match, your match label might look like this: {"waiting":1,"dps":2,"tank":0,"heal":0}. In this example the “waiting” flag is your waiting/playing mode indicator I described above, and the other 3 represent spots available for each player role.

A DPS player would then search for a match using the listing operation and a query that looks like this: +label.waiting:1 +label.dps:>0. This query is only for matches that are still waiting (assuming you don’t want to join matches in progress) and that have 1+ DPS spots available.

In your match handler’s join attempt and join hooks you check that there is still room for the player attempting to join and their role, and update the match label to subtract 1 spot for that role. If the match is now full you should also run your logic to transition from the waiting state to playing the game.

Hope this helps!

1 Like

yes thanks a lot, that’s a good direction. My code is like this as of now

local nk = require("nakama")

--utils function
function mysplit (inputstr, sep)
    local t={}
    for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
            table.insert(t, str)
    end
    return t
end

function table_from_string(str)
    local tab = {}

    local split = mysplit(str," ")
    for k,v in pairs(split) do
        local row = mysplit(v,":")
        label_table[row[1]] = row[2]
    end
    
    return tab
end

--create authority match via matchmaker
local function makematch(context, matched_users)
    local modulename = "roles"
    local setupstate = { invited = matched_users }
    local matchid = nk.match_create(modulename, setupstate)
    return matchid
end
nk.register_matchmaker_matched(makematch)

--match handler
local M = {}

function M.match_init(context, setupstate)
    local gamestate = {}
    local tickrate = 10
    --I used the label instead of the gamestate since I wanna filter aviable matches via label a
    --anyway, keeping track of the filled roles in the gamestate as well felt redundant
    local label = "waiting:0 dps:2 tank:2 heal:2"
    return gamestate, tickrate, label
end

function M.match_join_attempt(context, dispatcher, tick, state, presence, metadata)
    local acceptuser = true

    --let's make a table out of the lable string so it's easier to work with
    local label_table = table_from_string(context.match_label)
    --remove 1 from the role the joining player selected, we assume at least one spot was open
    --since we are searching games with at least one spot of the selected role aviable
    label_table[metadata.role] = label_table[metadata.role] - 1

    local full = true

    --we check if every role is filled
    for k,v in pairs(label_table) do
    if k ~= "waiting" and v ~= 0 then
        full = false
        end
    end

    --if it is we say the match isn't waiting anymore
    if full then
        label.waiting = 0
    end

    return state, acceptuser
end

return M

I don’t nofity players when the match is full because I figured I can just use the number of opponents that connect to figure out when the game is full and thus ready to start.

The problems I’m facing and don’t really understand are two:

  1. I’m getting attempt to modify read only table error when calling match create. Absolutly no idea what could be causing this

  2. When searching for games I put +label.waiting:1 +label.dps:>0 in the client “query” parameter of my client’s (Gogot) add_matchmaker_async function. This however doesn’t work, players do not find games that they should be able to join.

You’re on the right track.

  1. Don’t try to declare unnecessary global functions, instead make them local to the module they’re declared in. i.e. change function mysplit (inputstr, sep) to local function mysplit (inputstr, sep), same for table_from_string.
  2. Your match label must be a JSON string for the query to work. Note in my example the label is valid JSON. Let me know if you still run into issues after you’ve made this change. :+1:

Thanks a lot for your advices, however I ran into another error that I really cannot comprehend. The problem is the matchmaker doesn’t actually create a match before having the right amount of players to connect. However without any match online no players can search for the +label.waiting:1.

As far as I understand normally the matchmaker would create a match as soon as two players (or however many you select) are search for a match, this however doesn’t work for me because I need them to join a match only when they actually can (based on their role).

A simple solution I think is to simply create an empty match with labelled waiting with all roles available. This way players can join using the normal matchmaker.

I pieced together code from the docs to make this:

nk.run_once(function(context)

local modulename = "roles"
local setupstate = { label = '{"waiting":1,"judge":1,"compete":1}' }
local matchid = nk.match_create(modulename, setupstate)
return matchid end)

however doing this the runtime straight up can’t start. I get no error but my pc starts to lag. Any idea what could possible be doing this? Or maybe creating a single match like this isn’t possible/advisable.

Sorry for the length of this thread, but I really want this role selection to work.

No problem, thread length isn’t an issue as long as the discussion is useful and helps both you and others. :+1:

I think the main confusion is the use of the matchmaker. The matchmaker is a feature to find other players, and once a suitable set of players is found they are given a match. What I’ve proposed in this thread is not to use the matchmaker, but just match listings. So it matchmaker is “find suitable players” then match listings is “find suitable matches”.

You should create a “find or create match” RPC function to do the heavy lifting for the client. You can see a good example of this in the TypeScript template example - the Lua equivalent would be the same pattern:

  1. Client sends an RPC request to server with a payload containing preferences/parameters. In your case the player’s selected role might be a good parameter.
  2. The RPC function performs an appropriate match listing.
  3. If a match is found, return it in the RPC response.
  4. If no match is found, create one, then return it in the RPC response.

You won’t need to create empty matches at server startup with this pattern, and you won’t be using the matchmaker. This RPC function would replace both.

Ok, I got it to work. The main problem I was encountering is that I was using an incomplete match_handler, one that only implemented two of the six functions required for a working match_handler.

The logic is pretty much what you described:

  1. Player calls RPC
  2. RPC searches for suitable matches
  3. RPC returns a match_id, either of one suitable match or one that it has just created

Meanwhile, the match updates its label based on which roles are available.

I put the code in a GitHub public repo, if anyone has my same needs. There I explain in more detail how to use it.

Thanks a lot for all the help and clarification :slightly_smiling_face:

Awesome!

As you’ve noticed, all match handler functions are required. Even if your functions do nothing except return hardcoded values (such as the match join attempt function allowing everyone to join) the server will still attempt to call them - it can’t know ahead of time that you intend to do nothing in the function. :+1: