JoinMatchedAsync has empty presences on some clients

  1. Versions: Nakama 3.21.0, (Docker), Godot 4 SDK
  2. Server Framework Runtime language: TS/JS

I need some help understanding how the match join works, as I observe some behavior that feels weird to me. I have this very bare bones server code in TypeScript:

const matchInit = function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    params: { [key: string]: string }
): { state: nkruntime.MatchState; tickRate: number; label: string } {
    logger.debug('Lobby match created');

    return {
        state: { Debug: true, presences: {} },
        tickRate: 1,
        label: ''
    };
};

const matchJoinAttempt = function (
    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('%q attempted to join Lobby match', ctx.userId);

    return {
        state,
        accept: true
    };
};

const matchJoin = function (
    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('Presence %s', JSON.stringify(presences));

    for (const presence of presences) {
        state.presences[presence.userId] = presence;
        logger.debug('%q joined Lobby match', presence.userId);
    }

    logger.debug('State %s', JSON.stringify(state));

    return { state };
};

const matchLeave = function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    dispatcher: nkruntime.MatchDispatcher,
    tick: number,
    state: nkruntime.MatchState,
    presences: nkruntime.Presence[]
): { state: nkruntime.MatchState } | null {
    presences.forEach(function (presence) {
        state.presences[presence.userId] = presence;
        logger.debug('%q left Lobby match', presence.userId);
    });

    return {
        state
    };
};

const matchLoop = function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    dispatcher: nkruntime.MatchDispatcher,
    tick: number,
    state: nkruntime.MatchState,
    messages: nkruntime.MatchMessage[]
): { state: nkruntime.MatchState } | null {
    logger.debug('Lobby match loop executed');
    logger.debug('State %s', JSON.stringify(state));

    return { state };
};

const matchSignal = function (
    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 {
    logger.debug('Lobby match signal received: ' + data);

    return {
        state,
        data: 'Lobby match signal received: ' + data
    };
};

const matchTerminate = function (
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    dispatcher: nkruntime.MatchDispatcher,
    tick: number,
    state: nkruntime.MatchState,
    graceSeconds: number
): { state: nkruntime.MatchState } | null {
    logger.debug('Lobby match terminated');

    return {
        state
    };
};

function matchmakerMatched(
    context: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    matches: nkruntime.MatchmakerResult[]
): string {
    matches.forEach(function (match) {
        logger.info("Custom Matched user '%s' named '%s'", match.presence.userId, match.presence.username);

        Object.keys(match.properties).forEach(function (key) {
            logger.info("Matched on '%s' value '%s'", key, match.properties[key]);
        });
    });

    try {
        const matchId = nk.matchCreate('lobby', { invited: 'test' });
        return matchId;
    } catch (err) {
        logger.error(err as any);
        throw err;
    }
}

When I launch two game clients and join a match I receive the following objects on the client side from joinMatchedAsync:

Client 1:

Match<authoritative=true, match_id=40edce82-70e5-44f2-8e50-1966ebd2b44d.nakama1, label=, presences=[], size=0, self=UserPresence<persistence=false, session_id=5436d264-1ffa-11ef-91b8-7106fdcb5b46, status=, username=Shamshiel, user_id=ea1401d5-ab1c-4ad9-9ae2-e0410c7b87b3>>

Client 2:

Match<authoritative=true, match_id=40edce82-70e5-44f2-8e50-1966ebd2b44d.nakama1, label=, presences=[UserPresence<persistence=false, session_id=5436d264-1ffa-11ef-91b8-7106fdcb5b46, status=, username=Shamshiel, user_id=ea1401d5-ab1c-4ad9-9ae2-e0410c7b87b3>], size=1, self=UserPresence<persistence=false, session_id=56b40b80-1ffa-11ef-91b8-7106fdcb5b46, status=, username=WtpzVJhcyQ, user_id=731c67bf-4794-4fc2-b604-2be4cf1168e5>>

I would have assumed that both clients receive a response with the presences in the match but for some reason only one gets both and the other one zero,

Is that normal behaviour? If yes, what would be the preferred way for both clients to get the information who is in the match so I can display details about them?

Hello @Shamshiel,

Please make sure that in the client code you’re setting up the event hooks to receive presence joins and leaves before you call MatchJoinAsync. Also, the call itself of MatchJoinAsync returns the current presences at the time of the call, whilst the event will receive further delta updates of the joins and leaves.

Hope this helps,
Best.

1 Like

Thank you for your insights. I played aorund with it more and I also came to understand it how you just described it.