Skip to content

Web interface SDK

A server’s web interface is a set of pages - HTML, CSS and JavaScript - that players’ games draw over the game: a menu, a shop, a login screen, a scoreboard, a new chat. Each page is a frame named in the interface’s ui.toml, and the game gives every page one global object, KcdMp, before the page’s own scripts run. It is how the page learns where it runs, hears the game mode and the client scripts, answers them, reads the live values the game keeps for it, and asks for the keyboard and the mouse cursor. Nothing else of the game is reachable from a page: what it may receive and send is what its frame’s entry in ui.toml allows (events_in, events_out, feeds, focus).

KcdMp.onServer("shop.items", (value) => render(value.items)); // the game mode sent a "shop.items" event
document.querySelector("#buy").onclick = () => KcdMp.emitServer("shop.buy", { item: "bread" });

The page also runs in a plain browser while it is being written: KcdMp.env.mock is then true, every call answers on the spot and KcdMp.mock plays the game’s part. The types of the whole object are in KcdMp-web.d.ts, for TypeScript and for editors that read it. The guide Make a web interface puts a first frame on the screen from start to finish.

Topic
The page 9 properties, 1 function
Messages and events 3 functions, 2 handlers
Feeds 1 property, 2 handlers
Focus and visibility 2 functions, 3 handlers
Drawing a component 1 handler
Logging and errors 3 functions, 1 handler
Mock mode 1 property, 7 functions

What a page knows about itself: its frame’s name, the version of the page API, where it runs and how big its screen is - and KcdMp.ready(), the call that tells the game the page is listening. A page needs no set-up: KcdMp is there before the page’s own scripts run.

Property What it holds
KcdMp.frame The name of this page’s frame.
KcdMp.api The version of the page API the game runs.
KcdMp.contract The contract this frame implements, when it draws one of the game’s interface components.
KcdMp.env What the page knows about where it runs.
KcdMp.env.mock Whether the page runs outside the game.
KcdMp.env.screen The size of the page and its display’s pixel ratio.
KcdMp.env.locale The player’s language, as a language tag.
KcdMp.env.dev Whether the player’s game runs in developer mode.
KcdMp.env.component The game’s interface component this frame draws, if any.
Function What it does
KcdMp.ready Tells the game the page is loaded and listening.

The size of the page and of the display it is drawn on.

Field Type
w number the page’s width in CSS pixels
h number the page’s height in CSS pixels
scale number the display’s pixel ratio (2 on a display that draws two device pixels per CSS pixel)

A page talks to two others: the client scripts that run in the player’s own game, and the game mode on the server.

The page wants to With The other side
hear a client script, or a message the mode sent to this frame KcdMp.onMessage a client script’s SendWebMessage(frame, data); the mode’s SendPlayerWebMessage(pid, frame, data)
ask a client script something and wait for the answer KcdMp.post the script’s OnWebEvent(frame, name, data), whose return value is the answer
hear an event of the game mode KcdMp.onServer the mode’s SendClientEvent(pid, name, payload)
send the game mode an event KcdMp.emitServer the mode’s OnClientEvent(pid, name, payload, source), with the frame’s name as source
use an action of the interface component it draws KcdMp.call the game itself

What a page may receive and send is decided by its frame’s entry in ui.toml: events_in lists the event names it may hear and events_out the names it may send. Without them a frame may use the names that start with its own name and a dot (shop.open for a frame called shop). Names are 1 to 64 characters of a-z 0-9 _ . : -, and those that start with kcdmp: belong to the game itself and are never delivered or accepted.

Everything a page sends is JSON - at most 16 KB of it, nested at most 16 levels deep - and everything it receives is at most 64 KB. A page may make about 100 calls and send 64 KB a second; more is refused with an error until the budget fills again. Sends to the game mode also share the player’s budget of events with their client scripts: 30 events and 64 KB a second in all.

Function What it does
KcdMp.post Asks a client script something and waits for its answer.
KcdMp.emitServer Sends the game mode an event.
KcdMp.call Calls an action of the game’s interface component this frame draws.
Handler When it fires
KcdMp.onMessage Registers a function that receives what a client script or the game mode sent this frame with a message.
KcdMp.onServer Registers a function that receives the game mode’s events with a given name.

A feed is a live value the game keeps for a page and only reads to it: who the player is, whether the interface is on screen. A page subscribes to a feed and is called at once with the latest value and again at every change. A frame gets only the feeds its ui.toml entry lists (feeds = ["session", "visibility"]); subscribing to any other name is allowed and logs a warning, but no value ever arrives.

Two feeds exist: session, the connection and the player, and visibility, whether the interface is drawn. A subscription before KcdMp.ready is fine: the latest values are delivered ahead of the queued messages when the page is ready.

Property What it holds
KcdMp.feed The feeds a page may read.
Handler When it fires
KcdMp.feed.session.subscribe Registers a function that receives the connection and the player.
KcdMp.feed.visibility.subscribe Registers a function that is told when the interface is drawn or hidden.

The connection and the player, as the session feed carries them.

Field Type
server string the server’s name
address string host:port as the player connected to it
level string the level the server runs
playerId number the player’s id on the server - the game mode’s pid
name string the player’s name
rtt number the round-trip time to the server in milliseconds, 0 until it has been measured

Whether the interface is drawn, and if not, why.

Field Type
visible boolean true while the interface is drawn over the game
reason "loading" | "menu" | "game_screen" | "panic" | "hidden" | "" why it is not drawn: "loading" (a level is loading), "menu" (a game menu is open), "game_screen" (one of the game’s own screens is up), "panic" (the player switched the interface off), "hidden" (it is turned off), and "" while it is drawn

While a page is on screen the game keeps playing: keys and the mouse go to the game unless the page asks for them. A page asks with KcdMp.focus and gives them back with KcdMp.blur; the game mode and the client scripts can hand a frame the focus as well. The player has the last word. The frame’s focus setting in ui.toml decides what a request gets:

  • "never" - the frame gets no focus at all;
  • "input" (the default) - the cursor at once, the keyboard only within five seconds of the player’s own click or key press in that frame, so a page can take the keyboard from a click handler but not 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.

Asking for the keyboard more than six times a minute without any input from the player is refused. Esc gives the focus back unless the page handles the key itself. KcdMp.onVisibility tells a page when the interface stops being drawn - a game menu opened, a level loading - and KcdMp.onKeyBinding hears the keys a server has declared for its interface.

Function What it does
KcdMp.focus Asks the game to give this frame the keyboard and the mouse cursor.
KcdMp.blur Gives the keyboard and the mouse cursor back to the game.
Handler When it fires
KcdMp.onVisibility Registers a function that is told when the interface is drawn or hidden.
KcdMp.onPlayerCursor Registers a function that is told when the player brings up their cursor or takes it away.
KcdMp.onKeyBinding Registers a function for a key binding the server declared.

What a page asks for with KcdMp.focus.

Field Type
keyboard boolean true to receive the keys (typed text goes to the page’s text fields) (optional)
cursor boolean true to receive the mouse cursor (optional)
keepInput boolean true to let the game keep the player’s keys while no text field of the page has the focus, so the player can walk with the page open (optional)

Twelve of the game’s interface components can be drawn by a page of the server’s interface instead of KCD:MP’s own: the chat, the scoreboard, the mode’s HUD texts and GameText (hudtext), the sleep fade, the notices, the death screen, the party frames with the invitation, the voice indicator, the emote wheel, a mode’s shop, the dialogs (a mode’s dialogue page, the game’s own questions) and the loading screen. The game mode gives a component to a frame - SetPlayerInterface(pid, "chat", "mychat"), or [components.chat] drawer = "mychat" in ui.toml (the loading screen’s only there) - and from then on that frame gets the component’s events through KcdMp.onComponent and calls its actions through KcdMp.call; the game’s own piece steps aside. Until the page is ready, and whenever it cannot draw (its renderer stopped, the player’s web layer is off), the game’s own piece draws the component, so a player never has none. A frame draws one component at a time; KcdMp.env.component and KcdMp.contract say which ("chat", "chat@1").

Each component’s contract is what such a page serves: the events it hears, the actions it may call and the keys the game hands it. Every text in an event is the server’s or another player’s: show it with textContent, never as HTML.

The chat (chat@1). Enter opens the chat: the game gives the frame the keyboard and the mouse cursor and sends chat:open; the page shows its box, and sends what the player typed with chat.send, then chat.close (Esc in the page should do the same: call event.preventDefault() in its handler and then chat.close). While the box is closed the scroll keys (Page Up, Page Down unless the player rebound them) arrive as chat:scroll.

Event Data When
chat:config { maxChars, scrollStep, layout: { anchor, marginX, marginY, width, lines, backingAlpha, backingAlphaOpen, fontScale }, channels: [{ name, label, color, canSend }], suggestions: [{ name, help, params }], secretCommands: [names] } at the start and whenever any of it changes (the mode’s chat options, channels, commands)
chat:history { lines: [line] } - the last 200 at the start
chat:message line = { seq, id, ts, kind: "player"/"mode"/"server"/"local", authorId, author, color, text, spans: [{ text, color, bold, italic }], channel, channelLabel, channelColor, template, params } each line
chat:open { prefill } the player opened the chat
chat:close {} the game closed the box (a menu opened, the chat was hidden, the frame lost the keyboard)
chat:clear {} the mode cleared the chat
chat:scroll { lines } - up when positive a scroll key while the box is closed
Action Data What it does
chat.send { text, channel } says the line (a / command too) on a channel the player may write in - "" is the default one; only while the box is open, at most 4 a second
chat.close {} the box is closed: the keyboard and the cursor go back to the game
chat.layout { x, y, w, h } where the page’s chat is, in pixels of a screen 1080 tall: the game’s own panels that share the corner make room for it
chat.typing { active } reserved

The scoreboard (scoreboard@1). Tab shows the board while it is held, or opens and closes it (the scoreboard’s mode option). In toggle mode the frame gets the mouse cursor while the board is open - its own clicks and mouse wheel. Page Up and Page Down arrive as scoreboard:page while it is open, and so does the mouse wheel in hold mode.

Event Data When
server:info { name, address, title } - title is the scoreboard’s title option, "" for the usual one at the start and on a change
scoreboard:columns { columns: [{ key, label, width, align: "left"/"center"/"right", format: ""/"number"/"time"/"percent", color }] } - the mode’s SetScoreboardColumns at the start and on a change
roster:snapshot { players: [entry] } with entry = { id, name, label, color, team, ping, inMenu, talking, muted, party, self, cells: { key: text } } at the start
roster:update { players: [entry] } - the entries that changed a few times a second while the board is open, once a second otherwise
roster:remove { ids: [id] } a player left
scoreboard:show { open, mode: "hold"/"toggle" } Tab
scoreboard:page { delta } - 1 a page down, -1 up Page Down / Page Up, the wheel in hold mode
Action Data What it does
roster.mute { id, muted } the player stops (or starts again) hearing that player’s voice - on their own game
scoreboard.close {} closes a board the player opened in toggle mode

The HUD texts (hudtext@1). What the mode puts on the screen with CreateHudText and GameText. No actions, no keys: the frame never gets the keyboard or the cursor for it.

Event Data When
hudtext:snapshot { elements: [element] } with element = { id, x, y, text, color, opacity, scale, align: "left"/"center"/"right" } - x and y are fractions of the screen from the top left (y is the line’s top, x its left edge, centre or right edge by align), scale 1 is the HUD’s own size at the start (several messages when there are many: the rest come as hudtext:update)
hudtext:update { elements: [element] } - the elements that changed or came the mode changed them
hudtext:remove { ids: [id] } the mode hid or destroyed them
gametext:show { text, style: "center"/"top"/"lower", durationMs, remainingMs, until } - shown for remainingMs, "" = none; until is when it is gone, in Date.now() each GameText (the newest replaces the last), and at the start while one runs

The sleep fade (sleep@1). A bed’s sleep or a book’s reading darkens the screen a moment with the hours it took (the clock is the server’s: the game’s own time skip is stopped). It comes from the player’s own game - no mode sends it. No actions, no keys.

Event Data When
sleep:slept { hours, reading, elapsedMs, inMs, holdMs, outMs, until } - the fade started elapsedMs ago: dark over inMs, dark until holdMs from its start, then light again over outMs; reading for a book’s a sleep or a reading ends, and at the start while one fades

The notices (notices@1). The mode’s ShowInfoText, ShowNotification, ShowGameLog and ShowObjectiveEvent (and the client’s own short notices). The tutorial box - a mode’s tips, the party’s invitation, the death box - stays the game’s own. A notice is an event of the moment: a frame is never told of one that came before it. No actions, no keys.

Event Data When
notice:show { id, kind, text, raw, durationMs, local } and by kind: "info" + { priority, background } (one line at a time; a higher priority goes before the ones waiting), "notification", "log" + { type, level } (the game’s log kinds; 0, 0 is a plain line), "objective" + { title, status: "new"/"done"/"failed" } - text is the line without the game’s markup, raw as the mode sent it; durationMs 0 = the page’s own time for the kind; local = the player’s own game said it each notice
notice:hide { kind: "info" } HideInfoText: the info line shown now goes
notice:clear {} every notice goes, shown or waiting

The death screen (death@1). What the server’s death box says, in fields: who killed the player and when they are back on their feet. The game’s own box does not show while a frame draws the death. No actions yet, no keys.

Event Data When
death:show { killerId, killerName, cause, timer, respawnMs, until } - killerId a player’s id (-1 none), killerName that player’s name or an NPC’s label ("" = no one), cause the server’s words when no one killed them ("You fell to your death."), timer = the respawn comes by itself, in respawnMs (0 = due), at until (Date.now()); timer false = the game mode decides the player died, and at the start while they are dead
death:hide {} the player is on their feet again

The party (party@1). The party frames - the other members with their health and stamina - and the party’s invitation. The invitation’s keys (Y and N unless the player changed them) stay the game’s own: they answer it whoever draws the box. The frame takes no keys; it may get the cursor (its own KcdMp.focus), and then its own Accept / Decline buttons answer with party.answer.

Event Data When
party:config { layout: { anchor, marginX, marginY, showStamina } } - the party component’s options at the start and on a change
party:state { id, leader, name, frames, members: [{ id, name, label, leader, self }] } - id 0 = in no party, frames = the mode lets the frames show; every member in the order they joined, the player’s own entry among them at the start and when the party changes
party:vitals { members: [{ id, known, health, maxHealth, stamina, maxStamina, injuries, dead, bleeding, loading, pos }] } - the others (the game’s HUD shows the player’s own); known false until the first; pos [x, y, z] in metres, absent while that member is not in the world at the start, then at most twice a second
party:invite { fromId, fromName, seconds, until, acceptKey, declineKey } an invitation: it runs out in seconds
party:invite-end {} it was answered, withdrawn or ran out
Action Data What it does
party.answer { accepted } answers the invitation - only right after the player’s own click or key in the page, never a script’s

The voice indicator (voice@1). Who is talking, and the player’s own microphone. The game has no piece of its own for it (the talking marks are in the scoreboard and the name plates): a web drawer adds one. All of it is the player’s own game’s state.

Event Data When
voice:self { mode: "off"/"push-to-talk"/"voice", micOpen, transmitting, level, key } - level 0..1 while it sends, key the push-to-talk key’s name at the start, on a change, several a second while the player talks
voice:talking { ids: [id], players: [{ id, name }] } - the players heard talking now at the start and on a change
voice:muted { ids: [id], players: [{ id, name }] } - the players this player muted at the start and on a change
Action Data What it does
voice.mute { id, muted } stop (or start again) hearing that player - on this player’s game alone
voice.mode { mode } the microphone’s mode; "push-to-talk" and "voice" only right after the player’s own click or key in the page

The emote wheel (emotes@1). The ring G opens. It stays the game’s in everything but its pixels: G held or tapped, the number keys, Esc, the mouse’s pointer and the pick are the game’s own, and the page is told what to show (the mouse moves the pointer, never a cursor of the page).

Event Data When
emotes:list { entries: [{ name, label }] } - the server’s wheel, clockwise from the top ([] = none) at the start and on a change
emotes:open { entries: [{ name, label }], sticky } - the wheel as it opened; sticky: a tap left it open, it waits for a click, a number key, G again or Esc when it opens, and again when it turns sticky
emotes:pointer { dx, dy, index } - the pointer, -1..1 of the ring’s reach (y down); index the entry pointed at, -1 none while it is open, as the mouse moves
emotes:close { picked } - the emote chosen, "" none when it closes
Action Data What it does
emotes.pick { name } plays that emote of the wheel and closes it - only while the wheel is open
emotes.close {} closes the wheel - only while it is open
emotes.stop {} the emote playing ends - only while the wheel is open

The shop (shop@1). A mode’s shop on the list panel (OpenShop, a shop that keeps the panel); a vendor’s own trade screen stays the game’s. The keys stay the game’s (W / S, A / D the quantity, Tab, Enter, Esc) and move what the page is told; the page holds the pointer while the shop is open, so its rows and buttons can be clicked.

Event Data When
shop:open { shopId, title, vendorId, sellTab, bottomless, vendorMoney, rowsPerPage } - vendorId -1 = none; bottomless: the vendor’s purse is no limit when a shop opens
shop:list { tab, entries: [{ itemClass, name, price, amount }], vendorMoney } - tab 0 Buy, 1 Sell; price in tenths of a Groschen a piece; amount the stock (-1 unlimited) on Buy, what the player carries on Sell at the opening and when a tab’s rows change
shop:state { tab, index, page, quantity } - the row under the cursor (-1 = an empty list), the first row shown, the quantity (1-99) at the opening and on every move
player:purse { money } - the player’s purse, tenths of a Groschen at the opening and when it changes
shop:close {} when the shop closes
Action Data What it does
shop.select { tab, index } puts the cursor on that row (and that tab)
shop.quantity { amount } the quantity, 1-99
shop.buy, shop.sell { itemClass, amount } buys or sells that row (the amount kept to the stock or what is carried) - only right after the player’s own click or key in the page
shop.close {} closes the shop

The dialogs (dialog@1). A mode’s dialogue page (ShowPlayerDialogue) and a question of the player’s own game (a body change asking to restart), one at a time - the question first. The keys stay the game’s (W / S, 1-8, E / Enter, Esc on a page; Y / Enter, N on a question); the page holds the pointer while a dialog is open. A kick’s or a refusal’s notice stays the game’s own box.

Event Data When
dialog:open { id, kind, title, text, options: [{ text, enabled, key }], cursor, actorId } - kind "dialogue" (title the speaker, options the choices with key "1".."8", cursor the highlighted one) or "question" (options Accept "y" and Decline "n", cursor -1); a new page is a new id when a dialog opens or the page changes
dialog:cursor { id, index } - the page’s highlight moved when the keys move it
dialog:close { id } when it closes
Action Data What it does
dialog.answer { id, choice } a page’s choice 1-8 (0 leaves the conversation), a question’s 1 (yes) or 0 (no) - only right after the player’s own click or key in the page
dialog.select { id, index } moves a page’s highlight (as the pointer moves over the choices)

The loading screen (loading@1). What covers the game while the player connects and the level loads, until they stand in the world - and, when ui.toml asks for it, after that. Its drawer is ui.toml’s alone: [components.loading] drawer is "default" (KCD:MP’s own, with the interface’s theme) or the name of a frame; without the section a frame called loading is the screen (the launcher fetches its files before the game starts). A mode’s SetPlayerInterface cannot change it: the screen shows while the level loads, before anything the mode sends reaches the game. The page shows over everything - the rest of the interface is not drawn while it covers the game, so give it an opaque background - and the game’s own screen draws until the page has drawn its first picture. A loading frame does not wait for the whole interface: it draws as soon as its own files are on the player’s PC - its page’s folder and the files at the top of ui/, which the launcher fetches before the game starts (on a server without a password) and the game otherwise asks for first - while the rest still comes. Until everything has arrived, a file outside those answers not found, and a page that asked for one is loaded again once the interface is complete: keep a loading page’s files in its folder. manual_shutdown = true under [components.loading] keeps it past the spawn until the mode’s ClosePlayerLoadingScreen, the page’s loading.done or 240 seconds; cursor = true gives the page the mouse cursor while it is held (there is none during the level load itself). No keys. (The visibility feed says "loading" while the screen is up - to the loading page too, which is drawn all the same.)

Event Data When
loading:state { phase, line, server: { name, motd, host, port, level, levelName }, fraction, manual, cursor, ending, tips, version } - phase "connecting", "handshaking", "loading" (the level loads), "ready" (loaded, the spawn awaited), "spawned" or "ended" (refused, kicked, the server gone: line says why); line what the game’s own screen says; fraction 0..1, or null while it cannot be measured; manual = the screen is held past the spawn; tips the game’s own lines at the start and on every change
loading:handover { data } - the mode’s SetPlayerLoadingHandover, as it wrote it (null = none any more) at the start and whenever it changes - it may come during the level load
loading:end {} - the screen fades out now (1.2 seconds), then it is gone the player stands in the world and nothing holds the screen
Action Data What it does
loading.done {} ends a screen held past the spawn (manual_shutdown) - as the mode’s ClosePlayerLoadingScreen; before the spawn it waits for it

(The page’s KcdMp tells the game itself when the page has drawn - the action loading.shown; a page never calls it.)

Colours are "#rrggbb" texts ("" = none of its own), id and authorId are player ids (-1 = none), ts the game’s own clock in milliseconds. A cell is the player’s state value under the column’s key, as text - the page formats it.

Handler When it fires
KcdMp.onComponent Registers a function that receives the events of the game component this frame draws.

A page has no console the player can see, so it writes to the interface log, a file of the player’s game next to its other logs, and it can hear its own errors. The log takes about twenty lines a second per player; the rest are counted and dropped with one note. What a page logs stays on the player’s computer: it is never sent to the server, and a script error reaches the game mode only when the server asked for those ([client] ui_report_errors in the server’s configuration, which the mode hears as OnPlayerWebError).

Uncaught errors and unhandled promise rejections are logged as errors by themselves and passed to KcdMp.onError. In a browser, in mock mode, these calls write nothing: the browser’s own console is the place to look there.

Function What it does
KcdMp.log.info Writes a line to the interface log.
KcdMp.log.warn Writes a warning to the interface log.
KcdMp.log.error Writes an error to the interface log.
Handler When it fires
KcdMp.onError Registers a function that is told of the page’s errors and of the game’s complaints about the page.

A page can be written and tried in a plain browser, without the game. Outside the game KcdMp.env.mock is true and every call answers on the spot: KcdMp.ready, KcdMp.emitServer, KcdMp.focus, KcdMp.blur and KcdMp.call settle with { ok: true, mock: true }, and KcdMp.post does the same unless KcdMp.mock.reply says what it should answer. Nothing arrives by itself: no events, no messages and no feeds until the page plays the game’s part with KcdMp.mock. The object exists only in mock mode - inside the game KcdMp.mock is undefined.

Property What it holds
KcdMp.mock The controls that play the game’s part when the page runs in a browser.
Function What it does
KcdMp.mock.emit Fires the page’s event handlers as if the game mode had sent the event.
KcdMp.mock.message Fires the page’s message handlers as if a client script or the game mode had sent a message.
KcdMp.mock.feed Publishes a feed value as if the game had.
KcdMp.mock.reply Sets what KcdMp.post answers for a name.
KcdMp.mock.key Fires a key binding’s handlers as if the player had pressed - or let go of - the key.
KcdMp.mock.playerCursor Fires the player-cursor handlers as if the player had brought up - or taken away - their cursor.
KcdMp.mock.component Plays an event of the component the page draws - or its assignment - as the game would.