local nk = require('nakama') local function tsize(T) local count = 0 for _ in pairs(T) do count = count + 1 end return count end 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 function find_match(search_query) local matches = nk.match_list(search_limit, isAuthoritative, nil, min_size, max_size, search_query or '*') if (#matches > 0) then table.sort( matches, function(a, b) return a.size > b.size end ) return matches[1].match_id else return nil end end local default_search_query = ' +label.visibility:public +label.total_joined:<2' local function quick_find_match(context, payload) local found_match_id = find_match(((payload and nk.json_decode(payload).search_label) or '') .. default_search_query) print( ('Quick find game result for %s is %s'):format( (((payload and nk.json_decode(payload).search_label) or '') .. default_search_query), found_match_id ) ) return nk.json_encode({['match_id'] = found_match_id}) end nk.register_rpc(quick_find_match, 'quick_find_match_rpc') local function makematch(context, matched_users) -- ONLY for matchmaking -- print matched users for _, user in ipairs(matched_users) do local presence = user.presence nk.logger_info(("Matched user '%s' named '%s'"):format(presence.user_id, presence.username)) for k, v in pairs(user.properties) do nk.logger_info(("Matched on '%s' value '%s'"):format(k, v)) end end local modulename = 'match_handler' local setupstate = {invited = matched_users} local matchid = nk.match_create(modulename, setupstate) return matchid end nk.register_matchmaker_matched(makematch) local function update_player_match_storage(context, state) local write_items = {} for _, presence in pairs(state.registered_users) do local saved = {} local state_op = 3 if state.complete then state_op = 4 elseif presence.user_id == state.due_guesser then state_op = 2 elseif presence.user_id == state.due_bluffer then state_op = 1 end saved.op = state_op local value = { collection = 'joinedgames', key = context.match_id, user_id = presence.user_id, value = saved, permission_read = 1, permission_write = 0 } write_items[#write_items + 1] = value end print('Write items ' .. nk.json_encode(write_items)) nk.storage_write(write_items) end local function check_match_complete(guesses_list) for _, guesses in pairs(guesses_list) do if #guesses < 5 then return false end end return true end -- local function state_update_msg(dispatcher, presence, state) -- local resume_state = { -- ['due_bluffer'] = state.due_bluffer == presence.user_id, -- ['due_guess'] = (state.latest_bluff and state.latest_bluff.fake), -- ['registered_users'] = state.registered_users, -- ['personal_bluffs'] = state.bluff_list and state.bluff_list[presence.user_id], -- ['personal_guesses'] = state.guesses_list and state.guesses_list[presence.user_id] -- } -- dispatcher.broadcast_message(220, nk.json_encode({['data'] = resume_state}), {presence}) -- end local M = {} 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, presences = {}, registered_users = {}, message_queue = {}, guesses_list = {}, bluffs_list = {}, due_bluffer = nil, due_guesser = nil } return gamestate, tickrate, nk.json_encode(g_label) end function M.match_join_attempt(context, dispatcher, tick, state, presence, metadata) local acceptuser = true local reason = '' if (state.complete) then -- Do not allow users to join complete matches? acceptuser = false reason = 'Match is over.' elseif (tsize(state.registered_users) >= max_size and not state.registered_users[presence.user_id]) then -- Allow users to reconnect as long as they had legitimately registered. -- Do not allow users to join after max size is reached. acceptuser = false reason = 'Match is full.' end return state, acceptuser, reason 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 dispatcher.match_label_update(nk.json_encode(state.label)) if state and not state['due_bluffer'] then -- and #state.registered_users >= max_size -- WARNING: Should enable this ^ after solo testing state['due_bluffer'] = presence.user_id end if tsize(state.registered_users) < 1 then state.registered_users[presence.user_id] = presence dispatcher.broadcast_message(200, nk.json_encode({['message'] = 'First user has joined the match.'})) elseif not state.registered_users[presence.user_id] then state.registered_users[presence.user_id] = presence if (state.latest_bluff and state.latest_bluff.fake) then state.due_bluffer = presence.user_id state.due_guesser = presence.user_id end dispatcher.broadcast_message(200, nk.json_encode({['message'] = 'Another user has joined the match.'})) else state.registered_users[presence.user_id] = presence dispatcher.broadcast_message(200, nk.json_encode({['message'] = 'Someone has rejoined the match.'})) end -- if state.registered_users and state.registered_users[presence.user_id] then -- -- Return the state to the player to display last updates -- -- The `state` must hold enough data to recreate events sequentially local resume_state = { ['due_bluffer'] = state.due_bluffer == presence.user_id, ['due_guesser'] = state.due_guesser == presence.user_id, ['due_guess'] = (state.latest_bluff and state.latest_bluff.fake), ['registered_users'] = state.registered_users, ['user_bluffs'] = state.bluff_list and state.bluff_list[presence.user_id], ['user_guesses'] = state.guesses_list and state.guesses_list[presence.user_id] } dispatcher.broadcast_message(210, nk.json_encode({['data'] = resume_state}), {presence}) -- end end update_player_match_storage(context, state) return state end function M.match_leave(context, dispatcher, tick, state, presences) for _, presence in ipairs(presences) do state.presences[presence.user_id] = nil end -- This includes player leaving due to connection loss, -- so it should pause the match temporarily instead of forfeiting a player -- if not state.complete then -- for _, presence in ipairs(state.presences) do -- dispatcher.broadcast_message(3, 'Opponent has forfeit.') -- end -- end dispatcher.broadcast_message(300, nk.json_encode({['message'] = 'Opponent has disconnected.'})) return state end function M.match_loop(context, dispatcher, tick, state, messages) for _, message in ipairs(messages) do if message.op_code == 1 then if message.sender.user_id == state.due_bluffer and not state.complete then -- Player made a bluff state.latest_bluff = nk.json_decode(message.data).bluff if state.latest_bluff then dispatcher.broadcast_message(11, message.data, {message.sender}) -- The data is already json encoded for reg_id, reg_pres in pairs(state.registered_users) do if reg_id == message.sender.user_id then if state.bluffs_list then -- Update bluffs list if state.bluffs_list[reg_id] then state.bluffs_list[reg_id][#state.bluffs_list[reg_id] + 1] = state.latest_bluff print('Updated bluffs_list ') else state.bluffs_list[reg_id] = {state.latest_bluff} print('Created bluffs_list partition') end else state.bluffs_list = { [reg_id] = state.latest_bluff } print('Created bluffs_list') end else print('Forwarding bluff to ' .. reg_id) state.due_bluffer = reg_id state.due_guesser = reg_id local new_data = { ['latest_bluff'] = state.latest_bluff.fake } dispatcher.broadcast_message(12, nk.json_encode({['data'] = new_data}), {reg_id}) break end end update_player_match_storage(context, state) else dispatcher.broadcast_message( 666, nk.json_encode({['message'] = 'Invalid bluff data.', ['data'] = message}), {message.sender} ) end if state.due_bluffer == message.sender.user_id then dispatcher.broadcast_message( 666, nk.json_encode({['message'] = 'Error: Should have sent message to guesser now.'}) ) end else dispatcher.broadcast_message( 666, nk.json_encode({['message'] = 'Not your turn to bluff.'}), {message.sender} ) end elseif message.op_code == 2 then if message.sender.user_id == state.due_guesser and not state.complete then -- Player tried to block local msg if nk.json_decode(message.data).guess == state.latest_bluff.real then print('Correct guess.') msg = {result = true, guess = nk.json_decode(message.data).guess, real = state.latest_bluff.real} else print('Wrong guess.') msg = {result = false, guess = nk.json_decode(message.data).guess, real = state.latest_bluff.real} end state.due_guesser = nil -- Update guesses list if state.guesses_list then if state.guesses_list[message.sender.user_id] then state.guesses_list[message.sender.user_id][#state.guesses_list[message.sender.user_id] + 1] = msg print('Updated guesses_list ') else state.guesses_list[message.sender.user_id] = {msg} print('Created guesses_list partition') end else state.guesses_list = { [message.sender.user_id] = msg } print('Created guesses_list') end state.complete = check_match_complete(state.guesses_list) local code_sender = 21 local code_rec = 22 if state.complete then code_sender = 31 code_rec = 32 end for reg_id, reg_pres in pairs(state.presences) do if reg_id == message.sender.user_id then dispatcher.broadcast_message(code_sender, nk.json_encode({['data'] = msg}), {message.sender}) else dispatcher.broadcast_message(code_rec, nk.json_encode({['data'] = msg}), {reg_pres}) break end end else dispatcher.broadcast_message( 666, nk.json_encode({['message'] = 'Not your turn to block.'}), {message.sender} ) end else dispatcher.broadcast_message( 666, nk.json_encode({['message'] = 'Unknown opcode recieved.'}), {message.sender} ) end end return state end function M.match_terminate(context, dispatcher, tick, state, grace_seconds) local message = 'Server shutting down in ' .. grace_seconds .. ' seconds' -- This only happens when the server is doing a graceful shutdown, such as during maintenance. -- WARNING: Should probably add code here to save the match to some storage to resume later. dispatcher.broadcast_message(400, nk.json_encode({['data'] = message})) return nil end return M