Getting started
A KCD:MP server runs one game mode: a Lua script that defines the rules of that server - where players spawn, what the
chat and the commands do, who may hurt whom, what stands in the world. The server calls the script’s
callbacks when something happens (OnPlayerConnect, OnPlayerDeath, OnTick …) and the script calls
the server back through the functions on the other pages (SendClientMessage, SetPlayerPos, CreateHorse …). A mode
that defines nothing is a valid mode: the server spawns everyone at its default point and answers its own chat commands.
Running a mode
Section titled “Running a mode”The script is named in server.toml or on the command line; the command line wins:
[gamemode]script = "gamemodes/freeroam.lua" # relative to the server's working directory; "" = the built-in freeroamwatch = false # true: the file is re-read when it changes on disk (a development server)KcdMp.Server --gamemode gamemodes/my_mode.luaThe server ships with server.toml and the example modes beside it (Setting up has
the folder and the first run); your own mode is a file next to them, named in your server.toml. require resolves
relative to the script’s directory, so a mode may be a folder of files. Three modes ship with the server and are the best
examples: gamemodes/freeroam.lua (spawns, greetings, a HUD clock, a greeting zone, the party commands, the NPC actor demo),
gamemodes/duel_arena.lua (a queue, two players at a time in an arena, the rest untouchable) and gamemodes/marker/marker.lua
(a checkpoint run with a client script that draws the marker in each player’s game).
A mode is reloaded without a restart by /reload (admins), by ReloadGameMode() from the
script itself or by [gamemode] watch = true.
The script
Section titled “The script”Plain Lua 5.4 with the standard libraries. The server registers the API as globals before the script runs; the script defines the callbacks it cares about as globals too:
SetGameModeText("hello")
local spawnX, spawnY, spawnZ, spawnYaw = GetDefaultSpawn()
function OnPlayerConnect(pid) SendClientMessageToAll(COLOUR_SERVER, GetPlayerName(pid) .. " joined")end
function OnPlayerRequestSpawn(pid) SetSpawnInfo(pid, spawnX + 1.5 * (pid % 8), spawnY, spawnZ, spawnYaw) -- a step per player, so nobody spawns inside anyone return trueend
function OnPlayerCommandText(pid, cmd, args) if cmd == "hello" then SendClientMessage(pid, COLOUR_SERVER, "Hello, " .. GetPlayerName(pid)) return true -- handled end return false -- the server's built-in commands, then "Unknown command"endScripts are trusted - the server admin wrote them - so the luanet table NLua adds is there too
(luanet.import_type("System.IO.File") gives a script the .NET class library: files, a database, HTTP).
A command with arguments
Section titled “A command with arguments”Every / line a player types reaches OnPlayerCommandText as the command word
and the rest of the line. sscanf turns that rest into typed values - one letter per argument -
and tells you what is wrong when it cannot, so a command is a handful of lines:
function OnPlayerCommandText(pid, cmd, args) if cmd == "pay" then local target, amount = sscanf(args, "ud") -- u = a player, d = a whole number if target == false then SendClientMessage(pid, COLOUR_RED, "usage: /pay <player> <amount> - " .. amount) -- amount holds the reason return true end if amount <= 0 then SendClientMessage(pid, COLOUR_RED, "the amount must be positive") return true end SendClientMessage(target, COLOUR_GREEN, GetPlayerName(pid) .. " paid you " .. amount) SendClientMessage(pid, COLOUR_SERVER, "you paid " .. GetPlayerName(target) .. " " .. amount) return true end return falseend/pay hans 50 gives Hans’s pid and 50 - hans, Hans, ha or his pid all name him, as long as only one player
matches (GetPlayerId does the matching). /pay hans abc gives false and
"argument 2 (abc) is not a whole number", /pay nobody 50 gives "no player called nobody", /pay hans gives
"argument 2 is missing". The letters: u a player, d a whole number, f a number, s one word (or a
"quoted string"), z the rest of the line; a ? after a letter makes it optional. So /whisper <player> <message>
is "uz", /kick <player> [reason] is "uz?", /setspawn <x> <y> <z> [yaw] is "fffd?". The shipped modes do it
this way: freeroam.lua’s /hurt [player] [amount] and duel_arena.lua’s /arena x y z [yaw].
The tick
Section titled “The tick”Everything a mode does happens on the server’s simulation thread, one tick at a time, 30 times a second
([rates] tick_hz). The callbacks run from inside a tick; the calls a callback makes take effect in that tick’s output.
Two consequences:
- a script never races with anything - no locks, no threads, a timer’s function runs where
OnTickwould; - a slow callback delays the whole tick for every player. The server log says so while it happens (
the slowest tick took 41 ms against a 33 ms budget); anOnTickthat does real work every tick is the first place to look when players stutter. Do the heavy things on a timer every second instead.
Ids and units
Section titled “Ids and units”pid |
a player: 0 to GetMaxPlayers() - 1, given at the handshake and reused after a disconnect. Keep the pid, not the name, while a player is on; check IsPlayerConnected(pid) before trusting a pid kept across ticks |
id |
a world entity - a horse, a pickup, a prop, an NPC actor, a dog: the protocol’s net id, unique while it lives, nil from every getter once it is gone |
| positions | metres in the level’s world space, as the game’s own console reports them (GetPlayerPos ↔ the game’s pos). z < 0 when spawning or teleporting means “on the terrain” |
| yaw | degrees, 0 = facing +Y, counter-clockwise (the game’s convention) |
| colours | 0xRRGGBBAA - 0xFF0000FF is opaque red; the constants name the usual ones |
| time | GetServerTime() in milliseconds; timers in milliseconds; the world clock in hours (13.5 = 13:30) |
| strings | UTF-8; chat lines and names as the players typed them |
| keys of game things | items, souls, buffs and meshes are named by id, name or GUID from the reference lists; a call given an unknown key fails quietly (nil, false) and logs why |
A function that reads a player who is not connected returns nil (positions, names) or a neutral value (-1 for a ping);
one that acts on them returns false. Nothing raises an error for a bad id.
When a script fails
Section titled “When a script fails”- A runtime error inside a callback is logged (
[lua] error in OnPlayerText: ..., the first fifty in full, then counted) and that callback’s effect is skipped - the server keeps running, the player keeps playing. - A syntax error or an error while the script loads stops the server at startup with the message, so a broken mode is
found before players join. On a reload the same error leaves the built-in freeroam in charge until the next reload
(
game mode: x.lua failed to load (...); the built-in freeroam runs until the next reload). Log(...)writes to the server log with a[lua]prefix;printis the same function.
Your editor
Section titled “Your editor”The server folder’s sdk/KcdMp.d.lua describes this whole API - every function, callback and constant, with its
documentation and types - for the Lua language server: completion as you type, the page’s text on hover, and a warning for a
name that does not exist or an argument of the wrong type, before the server ever runs the script. In VS Code install the
“Lua” extension (sumneko / LuaLS) and open the gamemodes folder - it comes with a .luarc.json that points the language
server at the file. For a mode in a folder of its own, put a .luarc.json next to the script:
{ "runtime.version": "Lua 5.4", "workspace.library": ["../../sdk"] }or copy KcdMp.d.lua into the folder. A coding assistant working in that folder reads the same file, so it knows the API
instead of guessing at it (AI assistants has a CLAUDE.md to start from);
sdk/KcdMp-client.d.lua is the same for a client script. Other editors and the C# side:
Setting up.
Where next
Section titled “Where next”The Server API index lists every callback and function by topic, each with its own page; the Combat guide explains the model behind the combat callbacks; Constants lists every constant. The keys that name game things - items, souls, buffs, meshes, presets, clips, doors - are in the reference.
