Skip to content

Your first game mode

A mode is a script that defines the callbacks it cares about and calls the server back. This one greets players, answers /hello, keeps a clock in the corner of every screen and puts a subtitle up when someone walks into the spawn. Save it, point [gamemode] script at it, start the server.

hello.lua
SetGameModeText("hello")
local spawnX, spawnY, spawnZ, spawnYaw = GetDefaultSpawn()
local clock -- a HUD text every player sees
local spawnZone -- a circle around the spawn
function OnGameModeInit()
Log("hello on " .. GetLevel() .. ", " .. GetMaxPlayers() .. " slots")
clock = CreateHudText(0.99, 0.02, FormatWorldTime(), COLOUR_YELLOW, 1.0, HUD_ALIGN_RIGHT)
SetTimer(function() SetHudText(clock, FormatWorldTime()) end, 2000, true)
spawnZone = CreateCircleZone(spawnX, spawnY, 12)
end
function OnPlayerConnect(pid)
SendClientMessage(pid, COLOUR_SERVER, "Welcome to " .. GetLevel() .. ", " .. GetPlayerName(pid) .. ". /hello says hello.")
SendClientMessageToAll(COLOUR_SERVER, GetPlayerName(pid) .. " joined")
end
function OnPlayerRequestSpawn(pid)
-- a step of 1.5 m per player id, so nobody spawns inside anyone
SetSpawnInfo(pid, spawnX + 1.5 * (pid % 16), spawnY + 1.5 * (pid // 16), spawnZ, spawnYaw)
return true
end
function OnPlayerSpawn(pid)
GameText(pid, GetLevel(), 3000, GAMETEXT_CENTRE)
ShowHudText(clock, pid)
end
function OnPlayerEnterZone(pid, zone)
if zone == spawnZone then GameText(pid, "The spawn", 1500, GAMETEXT_LOWER) end
end
function OnPlayerCommandText(pid, cmd, args)
if cmd == "hello" then
SendClientMessage(pid, COLOUR_GREEN, "Hello, " .. GetPlayerName(pid) .. "!")
return true -- handled; the server's built-ins stay out of it
end
return false -- not ours: /pos, /players, /tp ... answer as usual
end
function OnPlayerDisconnect(pid, reason)
SendClientMessageToAll(COLOUR_SERVER, GetPlayerName(pid) .. " left (" .. reason .. ")")
end
  1. Load. The script runs top to bottom: SetGameModeText names the mode in the log, GetDefaultSpawn reads [spawn] from server.toml. Then OnGameModeInit runs — the place for anything that exists for the whole session: the HUD text, its timer, the zone.

  2. A player connects. The handshake is done and their client is loading the level; OnPlayerConnect may talk to them already. They are not in the world yet — no position, no health.

  3. The level is ready. OnPlayerRequestSpawn decides where: SetSpawnInfo sets the point, return true lets the spawn happen now (false would hold the player until the mode calls SpawnPlayer itself — a lobby, a team pick).

  4. In the world. OnPlayerSpawn: the player is replicated to everyone. Now GetPlayerPos, GameText, ShowHudText mean something.

  5. Every tick the server tests every player against every zone and calls OnPlayerEnterZone / OnPlayerLeaveZone on the edges; timers fire from the same slot.

  6. Chat. A /hello line arrives as OnPlayerCommandText(pid, "hello", ""). Returning true ends it there; false hands the line on to the server’s built-in commands, then to Unknown command.

  7. Leaving. OnPlayerDisconnect still knows the name; the id is free for the next player after it returns.

  • Every callback the world can call, with its arguments and what a return value does: Callbacks.
  • Moving, healing, teleporting and reading players: Players.
  • Something on the client’s screen the HUD cannot do? Ship a client script and talk to it with script events; share values with every client through state bags.
  • [gamemode] watch = true reloads the script on every save — see hot reload.