Make a web interface
A web interface is a set of pages - HTML, CSS and JavaScript - that every player’s game draws over the game while they are on your server: a menu, a shop window, a login screen, a map, a chat of your own. KCD:MP carries the browser engine that draws them (Chromium, through CEF) and starts it only on a server that has a web interface. A server without one looks exactly as it always did, and a player whose game cannot draw a page keeps the standard interface.
The interface is a folder of files next to the game mode. Each page in it is a frame: a layer of its own over the game,
named in the folder’s ui.toml, which also says what the frame may do. The game mode shows and hides the frames, sends them
data and hears what they send back. A page talks to the game through one object, KcdMp, that the game gives every page;
the web interface SDK documents it.
This guide builds one frame from start to finish, then covers the keyboard and the mouse, replacing a piece of KCD:MP’s own interface, the look, big files and the players who have no web layer.
The folder
Section titled “The folder”The interface lives in a folder called ui, next to the game mode’s client folder:
gamemodes/ mymode.lua the game mode mymode/ client/ its client scripts, if it has any ui/ ui.toml what the interface holds and what each frame may do panel/ index.html panel.css panel.js[client] ui in server.toml points somewhere else: another folder, or a .zip of one, which panel file managers handle
better than the hundreds of files a build tool writes (server configuration).
At start the server checks the folder and prints what it found, with every mistake in ui.toml named by its key. An
interface with errors is not sent, and players keep the standard interface. A player’s game downloads the interface when they
join and keeps it, so a later join fetches only the files that changed.
A first frame
Section titled “A first frame”ui.toml
Section titled “ui.toml”api = 1name = "My server's interface"version = "1"
[frames.panel]page = "panel/index.html"focus = "input"events_in = ["panel.*"]events_out = ["panel.*"]feeds = ["session"]One [frames.<name>] section per frame. This one shows panel/index.html, may receive and send events whose names start with
panel., reads the session feed (the server’s name, the player’s name, the ping) and may ask for the keyboard and the mouse
cursor (below).
The page
Section titled “The page”panel/index.html:
<!doctype html><html lang="en"><head><meta charset="utf-8"><link rel="stylesheet" href="panel.css"></head><body> <div id="box"> <p id="hello">Welcome</p> <button id="buy">Buy bread</button> </div> <script src="panel.js"></script></body></html>panel/panel.js:
// the game mode's "panel.hello" event: the value is the table it sent, as JSON parsedKcdMp.onServer("panel.hello", (value) => { document.getElementById("hello").textContent = "Welcome, " + value.name;});
// a click sends the game mode an event of the page's owndocument.getElementById("buy").addEventListener("click", () => { KcdMp.emitServer("panel.buy", { item: "bread" });});panel.css is any stylesheet. A page is transparent where it draws nothing, so the game shows through around the box.
The game mode
Section titled “The game mode”-- the player's web layer is up (also called once right after OnPlayerConnect with the state their game reported)function OnPlayerWebStatus(pid, status, reason) if status == "ready" then ShowPlayerWebFrame(pid, "panel") endend
-- the panel's page has loaded and listensfunction OnPlayerWebReady(pid, frame) if frame == "panel" then SendClientEvent(pid, "panel.hello", { name = GetPlayerName(pid) }) endend
-- what a page sends: the frame's name comes as the sourcefunction OnClientEvent(pid, name, payload, source) if source == "panel" and name == "panel.buy" then local order = JsonDecode(payload) if type(order) == "table" and order.item == "bread" then -- check it as any other input from a player, then sell the bread end endendShowPlayerWebFrame and the other calls answer true when the request was
handed to the player’s game, or false and the reason in words: "no such player", "no web interface", "the player has no web layer", "no such frame". HidePlayerWebFrame hides a frame and keeps its
page alive, so showing it again finds it as it was.
ui.toml
Section titled “ui.toml”| Key | Default | What it is |
|---|---|---|
api |
- | 1, the version of the web interface the folder is written for (required) |
name, version |
"" |
the interface’s own name and version |
[frames.<name>] |
- | one frame; the name is lower-case letters, digits and - (root, ui, engine, kcdmp, theme, native, default, hidden, lab and lua are taken); at most 32 frames |
page |
- | the frame’s .html file, a path inside the folder (required) |
preload |
false |
true loads the page at the join, hidden, so it shows at once when the mode asks |
z |
300 |
the frame’s layer, 0 to 999: a higher frame is drawn over a lower one |
focus |
"input" |
what the frame may take when it asks: "never" (nothing), "input" (the cursor at once, the keyboard only just after the player’s own click or key in the frame), "open" (the cursor at any time, the keyboard after the player’s first click in the frame) - below |
events_in |
[] |
the names of the mode’s events the page may receive; * at the end matches any rest: "panel.*" |
events_out |
[] |
the names the page may send with emitServer |
feeds |
[] |
the live values the page may read: "session" (the server, the level, the player, the ping) and "visibility" (whether the interface is drawn, and why not) |
[components.<name>] |
- | a piece of KCD:MP’s interface the folder draws or styles (below) |
[theme] |
- | the look of KCD:MP’s own web pieces (below) |
[files] |
include = ["**"] |
include and exclude, lists of patterns, choose the files that are sent; every frame’s page and the theme’s stylesheet always are |
[permissions] |
nothing | what the pages may reach outside the folder: connect and img (hosts), embeds ("youtube"), capabilities ("webgl", "wasm", "audio", "workers") |
A page can reach nothing outside its folder unless [permissions] names it: no other site, no other frame, nothing of the
game but what KcdMp gives it. The files are the kinds a page uses: html, css, js, mjs, json, svg, txt, the
images (png, jpg, jpeg, webp, avif, gif), the fonts (woff, woff2, ttf, otf) and the sounds and videos (ogg,
opus, webm, wav).
Talking to a page
Section titled “Talking to a page”| From | To | How | Arrives as |
|---|---|---|---|
| the game mode | a page | SendClientEvent / SendClientEventToAll |
KcdMp.onServer, in every page whose events_in takes the name (the client scripts get the same event) |
| the game mode | one frame | SendPlayerWebMessage |
KcdMp.onMessage, exactly the value sent |
| a page | the game mode | KcdMp.emitServer |
OnClientEvent, the frame’s name as its source |
| a client script | a frame | SendWebMessage |
KcdMp.onMessage |
| a page | the client scripts | KcdMp.post |
OnWebEvent, whose answer the page awaits |
The values travel as JSON: a Lua table becomes an object, and JsonEncode and
JsonDecode do it by hand on the server’s side. A message a frame cannot take yet waits
until its page is ready. What comes back from a page is the player’s game’s word: a page can be changed on the player’s own
PC, so check every request as you would a chat command.
A client script can drive frames too, with ShowWebFrame,
HideWebFrame and SetWebFocus: a mode with
client scripts keeps its screen logic there, a mode without any drives everything from the server.
The keyboard and the mouse
Section titled “The keyboard and the mouse”A frame starts without either: the player keeps playing while it is drawn. It gets the cursor, the keyboard or both when the mode
asks with SetPlayerWebFocus or the page asks with
KcdMp.focus, and gives them back with SetPlayerWebFocus(pid, frame, false, false) or
KcdMp.blur. The frame’s focus setting has the last word on the player’s side:
"never"- the frame gets no focus at all: an overlay the player only looks at."input"(the default) - the cursor at once; the keyboard only within five seconds of the player’s own click or key in the frame, so a page can take the keyboard from a click handler but never out of the blue."open"- the cursor at any time; the keyboard after the player’s first click in the frame since it was shown.
keepInput (the fifth argument of SetPlayerWebFocus) keeps the game’s walking keys working while no text field of the page
has the focus: a menu the player can walk with. Esc gives the focus back unless the page handles the key itself, and asking for
the keyboard more than six times a minute without any input from the player is refused. The keys a mode declares with
RegisterKeyBinding reach the pages too, through
KcdMp.onKeyBinding.
Replacing a piece of KCD:MP’s interface
Section titled “Replacing a piece of KCD:MP’s interface”The chat, the scoreboard, the notices, the party frames, the voice indicator, the emote wheel, the HUD texts, the sleep fade,
the death screen, the shop and the dialogs each have two more drawers than native and hidden
(Change the interface):
default- KCD:MP’s own web version of the piece, which your theme styles: a chat with channel tabs and command suggestions as the player types, a scoreboard with the mode’s own columns (SetScoreboardColumns).- the name of one of your frames - your page draws the piece. It receives the piece’s lines, roster or notices, its keys
and its settings, and calls the piece’s actions: the piece’s contract (
chat@1,scoreboard@1,notices@1…) is documented under Drawing a component. A frame draws one piece at a time.
[components.<name>] in ui.toml sets what every player starts with:
[components.chat]drawer = "default"
[components.scoreboard]drawer = "board" # the frame [frames.board] draws itcontract = "scoreboard@1"mode = "toggle" # the piece's options, as SetInterfaceOptionsForAll takes themand the mode changes it for one player or everyone with
SetPlayerInterface and
SetInterfaceForAll. While a player’s game cannot draw the web drawer - no web
layer, a page not ready yet, a page that stopped - the native piece draws instead, and the web one takes over the moment it can;
GetPlayerInterface says which draws now.
The loading screen has a web version too, but only ui.toml chooses it: [components.loading] drawer is "default" or a
frame’s name, and without the section a frame called loading draws it. It shows while the level loads, before anything the mode
sends can arrive, so its data comes from SetPlayerLoadingHandover, and
manual_shutdown = true keeps it up until the mode calls
ClosePlayerLoadingScreen.
The look
Section titled “The look”The default pieces take a theme from ui.toml:
[theme]css = "theme.css" # a stylesheet of the folder, applied to KCD:MP's web pieces
[theme.vars]"--kcdmp-accent" = "#F6E890""--kcdmp-text" = "#F3EEDF""--kcdmp-backing" = "rgba(20, 16, 12, 0.6)"
[theme.chat.templates] # chat lines laid out by the mode's templatesme = "<i>* {0} {1}</i>"The variables are --kcdmp- names: accent, text, text-dim, backing, font, radius and the pieces’ own. A chat line
sent with SendChatMessage and a template name fills the template’s {0} to {15}
with its values, always as text. The game’s own typefaces are there for every page: "KcdMp Regular", "KcdMp Display" and
"KcdMp Manuscript" in a font-family.
Size and big files
Section titled “Size and big files”A player downloads the interface over the game’s connection when they first join, so keep it small: [client] ui_max_mb caps it
(8 MB by default, 64 at most) and the server log warns from 4 MB up. Pictures, fonts, sounds and videos over 1 MB come from a
host of your own instead: set [client] ui_cdn to its address, run the server once with --pack-ui <folder> to write the files
to upload there, and list the host in [permissions] connect. Without ui_cdn a file that big is an error.
Players without the web layer
Section titled “Players without the web layer”OnPlayerWebStatus says how a player’s web layer stands: "starting",
"ready", "failed" (with the reason) or "off" - an older game, or a player who turned Server interfaces off in the
launcher. Those players keep the standard interface, and IsPlayerWebReady
answers false for them. A mode whose screens matter has a second way for them:
RegisterUiFallback sends a frame’s events to a client script’s box instead,
player by player.
A reload of the mode leaves the frames on the players’ screens, and the new mode
hears OnPlayerWebStatus and OnPlayerWebReady again.
Writing and testing pages
Section titled “Writing and testing pages”- Any build tool works - the folder only needs the files it writes. Vite needs
base: './'. Source maps,node_modulesand the sources stay out with[files] exclude. - In a browser. A page opened outside the game runs in mock mode: copy
web/sdk.jsfrom your KCD:MP folder next to your pages and load it first with<script src="sdk.js"></script>(in the game the copy steps aside for the game’s own). ThenKcdMp.mockplays the game’s part: events, messages, feeds and keys. - In the game. Turn on UI developer mode in the launcher’s settings: the F9 window gets a Web tab with every frame’s
state, the pages’ console, a line to run JavaScript in a frame and a reload button, and the game argument
-KcdMp_web_dev_url <frame>=http://127.0.0.1:<port>/(the launcher’s Game arguments) loads a frame from a development server on your own PC. In this mode the launcher joins only servers on your own list (a favourite, a line of your server list, this PC). - Script errors stay in the player’s own log unless
[client] ui_report_errors = true: then the mode hears each one inOnPlayerWebError. - The types of the whole
KcdMpobject are inKcdMp-web.d.ts, for TypeScript and for editors that read it.
The same in C#
Section titled “The same in C#”A C# mode has every call above on IServerApi and every callback on IGameMode, with the same names and the same rules:
ShowPlayerWebFrame(player, "panel", out var reason), OnPlayerWebStatus(IPlayer, string status, string reason) and the rest -
the C# page lists them under Web interface.
