-- Single source of truth for game modes and tuning constants. -- -- The client mirrors this table by calling rpc_get_modes, so a mode can be -- retuned (or added) here without shipping a new client build. local nk = require("nakama") local M = {} -- Seed once at load. Without this Lua's generator starts from a fixed state, so -- a multi-environment pool would deal out the same map order after every restart. math.randomseed(nk.time()) -- Storage collections. Kept in one place so a typo can't silently create a -- second, empty collection. M.COLLECTION_GAMESERVERS = "gameservers" M.COLLECTION_TICKETS = "match_tickets" M.COLLECTION_PROFILE = "profile" M.COLLECTION_ASSIGNMENT = "match_assignment" M.KEY_PROFILE = "public" M.KEY_MMR = "mmr" M.KEY_GS_INDEX = "index" -- list of known server_ids M.DEFAULT_MMR = 1000 -- team_size : players per team -- players : total players the matchmaker must gather -- max_conn : what the headless Mirror server sets NetworkManager.maxConnections to -- scenes : pool of Unity environments this mode can be played in. One is -- chosen per match (see M.resolve_scene). Adding an environment is -- a one-line change here - no client or game-server code involved. -- -- Every name listed must be a scene in the Unity build (Build Settings), or the -- game server will fail to load it and the match will hang at the loading screen. M.MODES = { ["1v1"] = { key = "1v1", team_size = 1, players = 2, max_conn = 2, scenes = { "Map_2" } }, ["2v2"] = { key = "2v2", team_size = 2, players = 4, max_conn = 4, scenes = { "Map_2" } }, ["5v5"] = { key = "5v5", team_size = 5, players = 10, max_conn = 10, scenes = { "Map_2" } }, } -- Overrides every mode's pool with a single scene. Set MATCH_SCENE in server/.env -- to point development at one environment without editing this file - that is how -- the lightweight CharacterControl scene is used instead of the ~14 MB Map_2 when -- running several Multiplayer Play Mode clients on one machine. -- -- Read from context.env: Nakama's Lua runtime exposes runtime env per call, NOT -- as a module-level global, so this cannot be resolved when the module loads. M.ENV_MATCH_SCENE = "MATCH_SCENE" --- The environments a mode can actually use right now, honouring MATCH_SCENE. function M.scene_pool(context, mode) local forced = ((context or {}).env or {})[M.ENV_MATCH_SCENE] if forced ~= nil and forced ~= "" then return { forced } end return (mode and mode.scenes) or {} end --- Picks the environment for one match. `context` may be nil (no override then). function M.resolve_scene(context, mode) local pool = M.scene_pool(context, mode) if #pool == 0 then return nil end if #pool == 1 then return pool[1] end return pool[math.random(#pool)] end -- Maps the Unity GameMode enum name to a mode key, so the client can send -- either form and Lua stays authoritative about what is valid. M.MODE_ALIASES = { OneVsOne = "1v1", TwoVsTwo = "2v2", FiveVsFive = "5v5", } function M.mode(key) if key == nil then return nil end return M.MODES[key] or M.MODES[M.MODE_ALIASES[key] or ""] end -- A game server is considered dead if it has not heartbeat within this window. -- Must be comfortably larger than the client's heartbeat interval (15s) so a -- single dropped packet does not evict a healthy server. M.GS_HEARTBEAT_TIMEOUT_SEC = 45 -- How long a minted join ticket stays redeemable. Long enough to cover scene -- load and a slow phone, short enough that a leaked ticket is worthless. M.TICKET_TTL_SEC = 120 -- How long the matchsession waits for every matched player to actually join -- before it gives up and cancels the match. M.SESSION_JOIN_TIMEOUT_SEC = 20 -- Ticks per second for the signalling match. It only runs a timeout counter. M.SESSION_TICK_RATE = 2 -- Opcodes broadcast over the matchsession. M.OP_MATCH_READY = 1 -- server -> client: { host, port, ticket, team, slot, mode, matchId } M.OP_MATCH_CANCELLED = 2 -- server -> client: { reason } M.OP_ROSTER = 3 -- server -> client: { players = [{ userId, username, team, slot }] } return M