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.
How a script reaches the player
Section titled “How a script reaches the player”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 itgamemodes/arena/client/*.lua the client half - sent to every joining player, run in name orderNothing 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.
What the script has
Section titled “What the script has”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.
The API
Section titled “The API”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, theSetUi*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.
Not an API
Section titled “Not an API”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.
Older names
Section titled “Older names”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.
A first script
Section titled “A first script”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.lualocal 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 timerfunction 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 endend-- gamemodes/marker/marker.lua, the game mode's sidefunction 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) endendLimits and errors
Section titled “Limits and errors”- 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,OnPartyChangeorOnKeyis logged ([KcdMp] OnServerEvent: ...) and the callback stays; anOnFramethat 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(...)andSystem.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 treatsa=bas an assignment: a text passed through a console command must avoid both. The script events encode their payload, so they carry anything.
