Commit 109fe069 authored by Andrei Mihu's avatar Andrei Mihu
Browse files

Add authoritative match callback for clients completing the join process. (#215)

parent 26087f4b
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ The format is based on [keep a changelog](http://keepachangelog.com) and this pr
### Added
- Config option to adjust authoritative match data input queue size. 
- Config option to adjust authoritative match call queue size.
- Authoritative match modules now allow a `match_join` callback that triggers when users have completed their join process.

### Changed
- Presence list in match join responses no longer contains the user's own presence. 
+1 −1
Original line number Diff line number Diff line
@@ -66,7 +66,7 @@ end
nk.register_rpc(send_stream_data, "clientrpc.send_stream_data")

local function create_authoritative_match(_context, _payload)
  local match_id = nk.match_create("match", {})
  local match_id = nk.match_create("match", { debug = true })
  return nk.json_encode({ match_id = match_id })
end
nk.register_rpc(create_authoritative_match, "clientrpc.create_authoritative_match")
+56 −3
Original line number Diff line number Diff line
@@ -72,7 +72,7 @@ Dispatcher exposes useful functions to the match. Format:
}

Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with
calls to match_join_attempt or match_leave.
calls to match_join_attempt, match_join, or match_leave.

State is the current in-memory match state, may be any Lua term except nil.

@@ -95,6 +95,58 @@ local function match_join_attempt(context, dispatcher, tick, state, presence)
  return state, true
end

--[[
Called when one or more users have successfully completed the match join process after their match_join_attempt returns
`true`. When their presences are sent to this function the users are ready to receive match data messages and can be
targets for the dispatcher's `broadcast_message` function.

Context represents information about the match and server, for information purposes. Format:
{
  env = {}, -- key-value data set in the runtime.env server configuration.
  execution_mode = "Match",
  match_id = "client-friendly match ID, can be shared with clients and used in match join operations",
  match_node = "name of the Nakama node hosting this match",
  match_label = "the label string returned from match_init",
  match_tick_rate = 1 -- the tick rate returned by match_init
}

Dispatcher exposes useful functions to the match. Format:
{
  broadcast_message = function(op_code, data, presences, sender),
    -- numeric message op code
    -- a data payload string, or nil
    -- list of presences (a subset of match participants) to use as message targets, or nil to send to the whole match
    -- a presence to tag on the message as the 'sender', or nil
  match_kick = function(presences)
    -- a list of presences to remove from the match
}

Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with
calls to match_join_attempt, match_join, or match_leave.

State is the current in-memory match state, may be any Lua term except nil.

Presences is a list of users that have joined the match. 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"
  },
  ...
}

Expected return these values (all required) in order:
1. An (optionally) updated state. May be any non-nil Lua term, or nil to end the match.
--]]
local function match_join(context, dispatcher, tick, state, presences)
  if state.debug then
    print("match join:\n" .. du.print_r(presences))
  end
  return state
end

--[[
Called when one or more users have left the match for any reason, including connection loss.

@@ -120,7 +172,7 @@ Dispatcher exposes useful functions to the match. Format:
}

Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with
calls to match_join_attempt or match_leave.
calls to match_join_attempt, match_join, or match_leave.

State is the current in-memory match state, may be any Lua term except nil.

@@ -170,7 +222,7 @@ Dispatcher exposes useful functions to the match. Format:
}

Tick is the current match tick number, starts at 0 and increments after every match_loop call. Does not increment with
calls to match_join_attempt or match_leave.
calls to match_join_attempt, match_join, or match_leave.

State is the current in-memory match state, may be any Lua term except nil.

@@ -206,6 +258,7 @@ end
return {
  match_init = match_init,
  match_join_attempt = match_join_attempt,
  match_join = match_join,
  match_leave = match_leave,
  match_loop = match_loop
}
+1 −0
Original line number Diff line number Diff line
@@ -109,6 +109,7 @@ func main() {
	}
	leaderboardCache := server.NewLocalLeaderboardCache(logger, startupLogger, db)
	matchRegistry := server.NewLocalMatchRegistry(logger, db, config, socialClient, leaderboardCache, sessionRegistry, tracker, router, stdLibs, once, config.GetName())
	tracker.SetMatchJoinListener(matchRegistry.Join)
	tracker.SetMatchLeaveListener(matchRegistry.Leave)
	// Separate module evaluation/validation from module loading.
	// We need the match registry to be available to wire all functions exposed to the runtime, which in turn needs the modules at least cached first.
+66 −0
Original line number Diff line number Diff line
@@ -56,6 +56,7 @@ type MatchHandler struct {
	vm            *lua.LState
	initFn        lua.LValue
	joinAttemptFn lua.LValue
	joinFn        lua.LValue
	leaveFn       lua.LValue
	loopFn        lua.LValue
	ctx           *lua.LTable
@@ -121,6 +122,10 @@ func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient
	if joinAttemptFn.Type() != lua.LTFunction {
		return nil, errors.New("match_join_attempt not found or not a function")
	}
	joinFn := tab.RawGet(lua.LString("match_join"))
	if joinFn == nil || joinFn.Type() != lua.LTFunction {
		joinFn = nil
	}
	leaveFn := tab.RawGet(lua.LString("match_leave"))
	if leaveFn.Type() != lua.LTFunction {
		return nil, errors.New("match_leave not found or not a function")
@@ -210,6 +215,7 @@ func NewMatchHandler(logger *zap.Logger, db *sql.DB, config Config, socialClient
		vm:            vm,
		initFn:        initFn,
		joinAttemptFn: joinAttemptFn,
		joinFn:        joinFn,
		leaveFn:       leaveFn,
		loopFn:        loopFn,
		ctx:           ctx,
@@ -471,6 +477,66 @@ func JoinAttempt(resultCh chan *MatchJoinResult, userID, sessionID uuid.UUID, us
	}
}

func Join(joins []*MatchPresence) func(mh *MatchHandler) {
	return func(mh *MatchHandler) {
		if mh.joinFn == nil {
			return
		}

		mh.Lock()
		if mh.stopped {
			mh.Unlock()
			return
		}
		mh.Unlock()

		presences := mh.vm.CreateTable(len(joins), 0)
		for i, p := range joins {
			presence := mh.vm.CreateTable(0, 4)
			presence.RawSetString("user_id", lua.LString(p.UserID.String()))
			presence.RawSetString("session_id", lua.LString(p.SessionID.String()))
			presence.RawSetString("username", lua.LString(p.Username))
			presence.RawSetString("node", lua.LString(p.Node))

			presences.RawSetInt(i+1, presence)
		}

		// Execute the match_leave call.
		mh.vm.Push(LSentinel)
		mh.vm.Push(mh.joinFn)
		mh.vm.Push(mh.ctx)
		mh.vm.Push(mh.dispatcher)
		mh.vm.Push(mh.tick)
		mh.vm.Push(mh.state)
		mh.vm.Push(presences)

		err := mh.vm.PCall(5, lua.MultRet, nil)
		if err != nil {
			mh.Stop()
			mh.logger.Warn("Stopping match after error from match_join execution", zap.Int("tick", int(mh.tick)), zap.Error(err))
			return
		}

		// Extract the resulting state.
		state := mh.vm.Get(-1)
		if state.Type() == lua.LTNil || state.Type() == LTSentinel {
			mh.logger.Info("Match join returned nil or no state, stopping match")
			mh.Stop()
			return
		}
		mh.vm.Pop(1)
		// Check for and remove the sentinel value, will fail if there are any extra return values.
		if sentinel := mh.vm.Get(-1); sentinel.Type() != LTSentinel {
			mh.logger.Warn("Match join returned too many values, stopping match")
			mh.Stop()
			return
		}
		mh.vm.Pop(1)

		mh.state = state
	}
}

func Leave(leaves []*MatchPresence) func(mh *MatchHandler) {
	return func(mh *MatchHandler) {
		mh.Lock()
Loading