Skip to content

Getting started

A client script is a Lua file that runs inside a player’s game, in the game’s own Lua state, next to the KCD:MP client. It is the game mode’s half on the player’s screen: a marker in the world, a sound, a bit of UI, a value read from the game that the server cannot see. A game mode works without one - the server’s chat lines, GameTexts and HUD texts need nothing on the client - and most modes need none. A mode that wants more puts its client files next to itself, and the server sends them to every player who joins.

A client script is written the way a game mode is: it defines callbacks by name (OnServerEvent, OnFrame, OnPartyChange …) and calls functions (ShowInfoText, CreateUiText, SendServerEvent …) - the server’s names without the pid, because on this side the player is always yourself.

The server sends it. A mode with a client half lives in a folder of its own, the way a C# plugin does, with the client files in client/ next to it:

gamemodes/arena/arena.lua the server half - [gamemode] script names it
gamemodes/arena/client/*.lua the client half - sent to every joining player, run in name order

Nothing is asked of the players: the files arrive with the join, before the roster and the mode’s first events, and run once the level is ready - so a callback a file defines sees the event the mode sends from OnPlayerSpawn. The launcher and the game say so when a server does this (“runs 2 client scripts in your game, sandboxed to the game”). [client] scripts in the server’s configuration names another folder; a mode in a single file keeps its client half in a folder named after it (freeroam.lua + freeroam/client/). The limits: 64 files, 256 KB each, 2 MB together. /reload on the server sends the folder again - the running scripts are dropped first (their callbacks, their timers, their UI), then the new ones run; the watch setting does the same by itself when a client file changes on disk.

A script runs once, when it arrives: its top-level code is its init. What it defines stays for the session and is gone with the game or replaced by the next /reload. Several files of one folder share one environment: a function the first file defines, the second can call - and one OnFrame serves the set, the last definition winning.

For development the loop is the server’s: edit the file in the mode’s client/ folder and type /reload on the server - or set watch in the server’s [gamemode] section and the running scripts are replaced by themselves whenever a file changes on disk. The script runs in your own game in the sandbox, exactly as it will for a player.

A script the server sent runs in a sandbox: the boundary is the game. It has everything it needs to draw, read the world and talk to the server, and nothing that reaches the player’s computer beyond the game - no files, no other programs, no browser, no console, no saves. What is there is listed below; a name that is not listed is not there (nil), and a script that wants one more should say so - the list grows on request.

Every callback, function and constant on its own page:

  • script events to and from the game mode - OnServerEvent, SendServerEvent;
  • the state bags the mode set - GetGlobalState, GetPlayerState, GetEntityState, OnStateChange;
  • the player, the level and the frame - OnFrame, GetPlayerId, GetPlayerPos, GetLevelName;
  • the party - GetParty, OnPartyChange, for a script that draws its own frames;
  • the game’s HUD - ShowInfoText, ShowNotification, ShowObjectiveEvent, ShowTutorial: the game’s own toasts;
  • the game’s UI for a script - CreateUiClip, CreateUiText, CreateUiRect, the SetUi* family, OnKey: panels and lines in the game’s own typeface, placed in a space that fits every screen (the shipped freeroam draws its clock that way);
  • the constants - the server’s COLOR_* without the alpha byte, the UI alignments, the objective kinds;
  • the console commands of the client.

The game’s own Lua, as the sandbox gives it

Section titled “The game’s own Lua, as the sandbox gives it”

The script runs in the game’s Lua state, in an environment of its own. What it holds:

the base library assert, error, ipairs, next, pairs, pcall, select, tonumber, tostring, type, unpack, xpcall, rawequal, rawget, rawset, setmetatable, getmetatable (not on strings), collectgarbage, loadstring (its chunks run in the sandbox too), print (into the client log)
string, math, table, coroutine copies of the game’s - a script may change its own copy, not the game’s
os.clock(), os.time() the two the game has
System the log (LogAlways, Log, Warning, Error), the drawing for this frame (DrawLabel(pos, size, text, r, g, b, a), DrawLine(a, b, r, g, b, a), DrawText, Draw2DLine), the entities (GetEntity, GetEntityByName, GetEntities, GetEntitiesByClass, GetEntitiesInSphere, GetNearestEntityByClass, GetPhysicalEntitiesInBox, SpawnEntity, RemoveEntity …), the world (RayTraceCheck, RayWorldIntersection, GetTerrainElevation({x=, y=, z=}), IsPointIndoors, IsPointVisible, ProjectToScreen, the view camera’s GetViewCameraPos / Dir / Fov, GetViewport), the clocks (GetCurrTime, GetFrameTime, GetFrameID, GetLocalOSTime), the look of the world (SetPostProcessFxParam, SetWind, the ambient color, the sky highlight, ActivateLight)
Script.SetTimer(ms, fn), Script.SetTimerForFunction(ms, fn), Script.KillTimer(id) the game’s own timers - one shot; call again for a loop. A /reload kills the ones a script left running
Game the game’s own table without its saves, loads and the recording
Calendar the game’s clock - GetWorldHourOfDay(), GetWorldTime() (game seconds since day zero)
UIAction the game’s own UI elements from Lua - ShowElement, HideElement, CallFunction, SetVariable, the listeners: the road to UI in the game’s own look
Particle SpawnEffect, CreateDecal
player the local player entity: player:GetWorldPos(), player.soul (GetStatLevel, GetSkillLevel, HasPerk …), player.actor, player.human (IsMounted, IsWeaponDrawn …), player.inventory (GetInventoryTable, HasItem …)
the API every callback, function and constant

What is not there, on purpose: dofile, loadfile, require (any file on the disk), io, package, debug, getfenv / setfenv, System.ExecuteCommand and the console, System.BrowseURL, System.Quit, System.LoadTextFile, the saves, Script.LoadScript. The entity tables (player, what System.GetEntity returns) are the game’s own with every function of theirs - its scripts (Scripts.pak) show the names and signatures, and a list of them is planned for this reference. Two warnings: inside the game the script has the game’s power over the player’s own client, so it can still break their game; and a script that changes the player’s own health, items or position is cheating in the server’s eyes - the server judges what the client reports, and the audit is one of the judges: an item a client script makes with the game’s own calls is one the server cannot explain, and it is taken out of the game under enforce. Values, combat, health and items stay the server’s: a client script gets no say in them.

An entity’s tables last as long as the entity. An NPC actor, an animal, a horse or another player’s body leaves the player’s game whenever it leaves their view - the mode’s DestroyEntity, the area of interest, invisibility - so keep its id, not its tables: look it up again with System.GetEntity(id) each time you need it (nil means it is gone) and read .inventory, .soul and the rest from what that returns. A table kept from a body that has gone answers nothing; on older clients a kept inventory asked after its body went closed the game.

The environment also holds a KcdMp table with the older spellings below and nothing else: the client’s own machinery is out of a script’s reach. A name a script writes into it that is not one of those stays in the script’s own copy.

Scripts of the first releases used the KcdMp table with lower-case names. Those still work in this release and go in the next; the page of each new name says what it does.

Older Now
KcdMp.on_event(name, fn), KcdMp.on_any_event function OnServerEvent(name, payload)
KcdMp.send_event(name, payload) SendServerEvent(name, payload)
KcdMp.on_frame(fn) function OnFrame(dt)
KcdMp.state.global[key], KcdMp.state.player[pid][key], KcdMp.state.entity[id][key] GetGlobalState(key), GetPlayerState(pid, key), GetEntityState(id, key)
KcdMp.on_state(key, fn), KcdMp.on_state_change function OnStateChange(scope, id, key, value)
KcdMp.player_id, KcdMp.level(), KcdMp.player_pos() GetPlayerId(), GetLevelName(), GetPlayerPos() (three numbers)
KcdMp.party, KcdMp.on_party_change GetParty(), function OnPartyChange(party)
KcdMp.hud.info_text, notification, game_log, objective, tutorial, hide_*, clear_queue ShowInfoText, ShowNotification, ShowGameLog, ShowObjectiveEvent, ShowTutorial, Hide*, ClearNotificationQueue
KcdMp.ui.clip, text, rect, set, remove, clear, screen, project, on_key CreateUiClip, CreateUiText, CreateUiRect, the SetUi* family, DestroyUiElement, ClearUi, GetScreenSize, WorldToScreen, function OnKey(key, pressed)
KcdMp.count, KcdMp.census, KcdMp.label_occlusion CountEntities, GetEntityCensus, SetLabelOcclusion

Colors stay 0xRRGGBB with the alpha apart (0 .. 100) on this side: the game’s Lua keeps its numbers as 32-bit floats, and the server’s 0xRRGGBBAA would lose its last byte.

The marker round trip: the mode sends a point, the script draws a label there until the player is close, then tells the mode. The server folder ships it whole as the marker example mode (gamemodes/marker/marker.lua with client/marker.lua: a checkpoint run around the spawn, laps counted by the server).

-- gamemodes/marker/client/marker.lua
local marker -- {x=, y=, z=} or nil
function OnServerEvent(name, payload)
if name ~= "marker" then return end
local x, y, z = payload:match("^([^,]+),([^,]+),(.+)$")
marker = {x = tonumber(x), y = tonumber(y), z = tonumber(z)}
print("marker at " .. payload)
end
-- the drawing functions show for one frame: draw from OnFrame, not a timer
function OnFrame(dt)
if marker and player then
local p = player:GetWorldPos()
local d = math.sqrt((p.x - marker.x) ^ 2 + (p.y - marker.y) ^ 2)
System.DrawLabel({x = marker.x, y = marker.y, z = marker.z + 1.5}, 1.4, string.format("%.0f m", d), 1, 0.85, 0.3, 1)
if d < 2 then
SendServerEvent("marker_reached", "")
marker = nil
end
end
end
-- gamemodes/marker/marker.lua, the game mode's side
function OnPlayerSpawn(pid)
SendClientEvent(pid, "marker", "1290.0,1095.0,26.3")
end
function OnClientEvent(pid, name, payload)
-- the client says it is there; the mode checks GetPlayerPos before it counts
if name == "marker_reached" then GameText(pid, "Checkpoint!", 1500) end
end
  • A script may send at most 30 events a second to the server; more is dropped. An event with a name over 64 bytes or a payload over 4096 bytes is refused whole (never cut) and logged.
  • A client half is at most 64 files, 256 KB each, 2 MB together; a file over the limit is left out with a line in the server’s log.
  • An error inside OnServerEvent, OnStateChange, OnPartyChange or OnKey is logged ([KcdMp] OnServerEvent: ...) and the callback stays; an OnFrame that fails is logged and switched off; a syntax or load-time error in a file is logged with the file’s name and line ([KcdMp] client script marker.lua: marker.lua:12: ...) and the other files still run.
  • print(...) and System.LogAlways("[KcdMp] ...") land in the KCD:MP client log, %LOCALAPPDATA%\KcdMp\client.log; the F9 window, where the server allows it, shows the console’s own output.
  • The game’s console splits a line on ; and treats a=b as an assignment: a text passed through a console command must avoid both. The script events encode their payload, so they carry anything.