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
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.
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.
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.
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.
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.
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.
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
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), scale1 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; durationMs0 = 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 }] } - id0 = 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
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 } - level0..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 } - tab0 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; fraction0..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.
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.
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.