-- Entry point. Nakama loads every .lua in the modules dir, but only this file -- registers things - so the full surface area of the backend is visible here. local nk = require("nakama") local config = require("config") local util = require("util") local gameservers = require("gameservers") local tickets = require("tickets") local profile = require("profile") local matchmaking = require("matchmaking") -- --------------------------------------------------------------------------- -- Health -- --------------------------------------------------------------------------- local function rpc_healthcheck(_context, _payload) return util.ok({ service = "jd-nakama", time = util.now(), modes = { "1v1", "2v2", "5v5" }, }) end -- --------------------------------------------------------------------------- -- Game server: match results -- --------------------------------------------------------------------------- -- Reported by the Mirror server when a match finishes. Releases the server back -- to the pool and writes progression. -- -- Payload: { secret, server_id, match_id, winning_team, -- players = [{ user_id, team, rank_points }] } local function rpc_gs_match_ended(context, payload) local input = util.decode(payload) util.assert_gameserver(context, input) local winning_team = input.winning_team -- 0, 1, or nil for a draw/abort for _, player in ipairs(input.players or {}) do if player.user_id ~= nil and player.user_id ~= "" then local won = (winning_team ~= nil) and (player.team == winning_team) -- Flat +/-20 for now. Swap for Elo/Glicko once there is enough match data -- to calibrate against; the storage shape does not need to change. local mmr_delta = 0 if winning_team ~= nil then mmr_delta = won and 20 or -20 end local ok, err = pcall(profile.apply_result, player.user_id, won, tonumber(player.rank_points) or 0, mmr_delta) if not ok then -- One bad player record must not stop the others from being written, -- and must never stop the server from being released. nk.logger_error(("failed to apply result for %s: %s") :format(tostring(player.user_id), tostring(err))) end end end gameservers.release(input.server_id, "match ended") nk.logger_info(("match %s ended on %s, winning team %s") :format(tostring(input.match_id), tostring(input.server_id), tostring(winning_team))) return util.ok({}) end -- Reported when the Mirror server has every expected player connected. Purely -- observational today, but it is the hook an ops dashboard will want. local function rpc_gs_match_ready(context, payload) local input = util.decode(payload) util.assert_gameserver(context, input) nk.logger_info(("match %s ready on %s with %s players") :format(tostring(input.match_id), tostring(input.server_id), tostring(input.player_count))) return util.ok({}) end -- --------------------------------------------------------------------------- -- Registration -- --------------------------------------------------------------------------- -- Client-facing (session-authenticated) nk.register_rpc(rpc_healthcheck, "rpc_healthcheck") nk.register_rpc(profile.rpc_get_modes, "rpc_get_modes") nk.register_rpc(profile.rpc_get_profile, "rpc_get_profile") nk.register_rpc(profile.rpc_set_loadout, "rpc_set_loadout") -- Game-server-facing (http_key + GS_SHARED_SECRET; see util.assert_gameserver) nk.register_rpc(gameservers.register, "rpc_gs_register") nk.register_rpc(gameservers.heartbeat, "rpc_gs_heartbeat") nk.register_rpc(gameservers.rpc_list, "rpc_gs_list") nk.register_rpc(tickets.redeem, "rpc_gs_validate_ticket") nk.register_rpc(rpc_gs_match_ready, "rpc_gs_match_ready") nk.register_rpc(rpc_gs_match_ended, "rpc_gs_match_ended") -- The signalling match. Registered by name so matchmaking.lua can create it. nk.register_matchmaker_matched(matchmaking.matchmaker_matched) -- Give every new account a profile immediately, so the first matchmaking query -- always has an MMR to sort on. -- -- Lua after-hooks are called as (context, outgoing_payload, incoming_payload). -- We only need the authenticated user, which lands on the context. local function after_authenticate(context, _outgoing, _incoming) local user_id = context.user_id if user_id == nil or user_id == "" then return end local ok, err = pcall(profile.get, user_id) if not ok then nk.logger_error(("profile bootstrap failed for %s: %s"):format(user_id, tostring(err))) end end nk.register_req_after(after_authenticate, "AuthenticateDevice") nk.register_req_after(after_authenticate, "AuthenticateCustom") nk.logger_info(("jd-nakama modules loaded (%d modes)"):format( (function() local n = 0 for _ in pairs(config.MODES) do n = n + 1 end return n end)()))