Hi, I bumped into this requirement and came up with this idea after not finding a solution in the forum. I have a simple game which doesn’t require authoritative game state (I can do a server side RPC validation at the end of each round). But it does require match discovery. My solution was for the host to create 2 matches: one client relay (created first) and one authoritative. When the authoritative match is created, include the match ID of the client relay match in the label and have the host join the match straight away. When the player is looking for matches, if they find one they like, then their device can connect to the client relay match via the ID in the label. I also configure the authoritative match not to accept more than 1 connection and to terminate when the single host player leaves. As a downside, this means the host runs 2 socket connections for the duration of the matching, but it does mean that the server isn’t tied up with game state management for a simple requirement. Hope it’s useful for someone.
My label looks like this (idm is the client relay match ID): {"minRank":0,"maxWordLength":10,"minPlayers":5,"roundLength":60,"maxRank":7,"minWordLength":3,"idm":"00536156-4ae1-4e17-9d48-f613376fb0c7.","maxPlayers":5,"numRounds":10}
My match handling code:
export function matchInitHandler(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, params: {[key: string]: string}): {state: nkruntime.MatchState, tickRate: number, label: string} {
const newLabel = JSON.stringify(params);
logger.info('Starting match, parameters: %s', newLabel);
return {
state: { presences: {}, shouldTerminate: false, emptyTicks: 0 },
tickRate: 1, // 1 tick per second = 1 MatchLoop func invocations per second
label: newLabel
};
};
export function matchJoinHandler (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
logger.debug('matchJoinHandler(): Match presences: %s, match state: %s', JSON.stringify(presences), JSON.stringify(state));
presences.forEach(function (p) {
state.presences[p.sessionId] = p;
});
return {
state
};
}
export function matchJoinAttemptHandler (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presence: nkruntime.Presence, metadata: {[key: string]: any }) : {state: nkruntime.MatchState, accept: boolean, rejectMessage?: string | undefined } | null {
logger.debug('matchJoinAttemptHandler(): Match presence: %s, match state: %s', JSON.stringify(presence), Object.keys(state.presences).length);
return {
state,
// We accept the match creator only
accept: Object.keys(state.presences).length == 0
};
}
export function matchLeaveHandler(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, presences: nkruntime.Presence[]) : { state: nkruntime.MatchState } | null {
Object.keys(presences).forEach(function(key: any) {
delete(state.presences[presences[key].sessionId]);
});
// We terminate the match when the host player leaves - this is just for match advertising purposes
state.shouldTerminate = true
return { state };
}
export function matchSignalHandler(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, data: string) : { state: nkruntime.MatchState, data?: string } | null {
return { state };
}
export function matchLoopHandler (ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, messages: nkruntime.MatchMessage[]) : { state: nkruntime.MatchState} | null {
// If we have no presences in the match according to the match state, then terminate it
// The authoritative matches are only used for match advertising, so shouldn't be active for very long
logger.debug('!!!!GAME LOOP!!!!: Match state: %s, %s', JSON.stringify(state), state.presences.length);
if (Object.keys(state.presences).length === 0) {
state.emptyTicks++;
} else {
state.emptyTicks = 0;
}
if (state.shouldTerminate || state.emptyTicks >= 10) {
logger.info('Termination requested or match empty for >= 10 ticks, requesting termination, id: %s, label: %s', state.matchId, state.label);
return null;
}
return {
state
};
}
export function matchTerminate(ctx: nkruntime.Context, logger: nkruntime.Logger, nk: nkruntime.Nakama, dispatcher: nkruntime.MatchDispatcher, tick: number, state: nkruntime.MatchState, graceSeconds: number) : { state: nkruntime.MatchState} | null {
logger.info('Terminating match, id: %s, label: %s', state.matchId, state.label);
return {
state
};
}