User interfaces
Menus, shops, forms and HUD panels - built in the gamemode, shown in the player's game.
A gamemode can show players its own windows: a menu, a shop, a form, a scoreboard. It builds them from building blocks (ui.*) and shows them with a UiView. The player's game draws them with the same components as the Open Road MP menu - they look like part of the game.
const welcome = new UiView({ title: "Welcome!" })
welcome.add(
ui.text("Have a good trip on our server."),
ui.button({ label: "Let's go", close: true }),
)
mp.events.on("playerJoin", (player) => welcome.show(player))The server only sends a description of the window - no code of yours runs in the player's game. Clicks and inputs come back to your gamemode as function calls.
The previews on this page are real: the docs run each example with the gamemode host and draw what it shows with the game's own renderer. Click around - instead of your gamemode, the preview tells you what your handler would get.
Notices
For a short message there is no need for a window: mp.ui.notify shows a notice at the top of the HUD, in the style of the game's own messages.
mp.ui.notify(player, { title: "Checkpoint 3/10", text: "+30 s" })
mp.ui.notify(mp.players.all(), { title: "Server restart in 5 minutes", kind: "warning", seconds: 10 })| Field | |
|---|---|
title | at most 120 characters |
text | optional, at most 300 characters |
kind | "info" (default), "warning" or "alert" - colour and icon |
seconds | how long it stays, default 5; 0 = until the player leaves the world |
Windows and HUD panels
new UiView({ title: "Shop", size: "lg" }) // a window (default)
new UiView({ mode: "hud", position: "top-right", title: "Race" }) // a panel over the game- A window appears in the middle of the screen. The player gets the mouse, and the game gets no input until the window is closed: with its X, with Escape, or with a button that has
close: true.closable: falsekeeps the window open until you close it withhide(). - A HUD panel sits at the edge of the screen and does not take the mouse - for scoreboards, timers, race positions.
Building blocks
| Kind | Blocks |
|---|---|
| Layout | ui.stack (one below the other), ui.row (side by side), ui.grid (columns), ui.card, ui.tabs, ui.accordion, ui.separator |
| Text | ui.heading, ui.text, ui.badge, ui.alert |
| Data | ui.table, ui.progress, ui.spinner |
| Input | ui.button, ui.input, ui.textarea, ui.select, ui.checkbox, ui.switch, ui.radio, ui.slider, ui.logoPicker |
Containers take their children after the options. Text can be passed directly, and false, null or undefined are skipped - handy for conditions:
const onSale = true
const buy = (player: OpenRoadMP.Player) => mp.log(`${player.name} buys a Scania S`)
const offer = new UiView({ title: "Truck dealer", size: "sm" })
offer.add(
ui.card({ title: "Scania S", footer: [ui.button({ label: "Buy", onClick: buy })] },
"Our best seller",
onSale && ui.badge("-20 %", { variant: "destructive" }),
),
)All blocks with their options: API reference → ui and UiView.
A complete example: a shop
Tabs, a grid of cards, a table, inputs - put together the way a real shop would be:
const trucks = [
{ name: "Scania S", price: 148000, power: "770 hp" },
{ name: "Volvo FH16", price: 139000, power: "750 hp" },
{ name: "DAF XG+", price: 131000, power: "530 hp" },
{ name: "MAN TGX", price: 127000, power: "640 hp" },
]
function buyTruck(player: OpenRoadMP.Player, truck: string) {
mp.chat.send(player, `You bought a ${truck}.`)
}
const shop = new UiView({ title: "Truck shop", description: "Everything for the road", size: "lg" })
shop.add(
ui.tabs({
tabs: [
{
title: "Trucks",
content: [
ui.grid({ columns: 2 },
...trucks.map((t) =>
ui.card({ title: t.name, description: t.power,
footer: [ui.button({ label: `Buy for € ${t.price.toLocaleString("en")}`, onClick: (p) => buyTruck(p, t.name) })] }),
),
),
],
},
{
title: "Prices",
content: [
ui.table({ columns: ["Truck", "Power", "Price"],
rows: trucks.map((t) => [t.name, t.power, `€ ${t.price.toLocaleString("en")}`]) }),
],
},
{
title: "Order",
content: [
ui.select({ id: "truck", label: "Truck", options: trucks.map((t) => t.name), value: trucks[0].name }),
ui.radio({ id: "colour", label: "Colour", options: ["White", "Black", "Red"], value: "White" }),
ui.slider({ id: "tank", label: "Fuel tank (l)", min: 400, max: 1400, step: 100, value: 800 }),
ui.switch({ id: "delivery", label: "Deliver to my garage", checked: true }),
ui.textarea({ id: "note", label: "Note for the dealer", placeholder: "Anything else?" }),
ui.button({ label: "Order", close: true,
onClick: (player, values) => buyTruck(player, String(values.truck)) }),
],
},
],
}),
)
mp.commands.add("shop", "open the truck shop", (player) => shop.show(player))And a HUD panel - no mouse, at the edge of the screen:
const delivery = new UiView({ mode: "hud", position: "top-left", title: "Delivery" })
delivery.add(
ui.text("Apples · Düsseldorf → Hamburg", { tone: "muted", size: "sm" }),
ui.progress({ label: "Distance", value: 62 }),
ui.row({ justify: "between" },
ui.badge("€ 12,000"),
ui.text("1 h 20 min left", { size: "sm" }),
),
)Clicks and inputs
Buttons have onClick, selects, checkboxes, switches, radios and sliders onChange, a text field onSubmit (Enter). The function runs in your gamemode and gets the player and the current values of all inputs of the window, by their id:
const plateForm = new UiView({ title: "Licence plate" })
plateForm.add(
ui.input({ id: "plate", label: "Text", placeholder: "OR MP 42", maxLength: 12 }),
ui.select({ id: "country", label: "Design", options: ["germany", "france", "poland"], value: "germany" }),
ui.button({
label: "Apply",
close: true,
onClick: (player, values) => {
mp.players.setLicensePlate(player, String(values.plate), String(values.country))
},
}),
)
mp.commands.add("plate", "pick your licence plate", (player) => plateForm.show(player))Values are text for input (a number with type: "number"), textarea, select and radio, true/false for checkbox and switch, and a number for slider and logoPicker.
ui.logoPicker shows the game's 16 company logos as pictures - the player clicks one. Its value is the logo number (0-15), ready for mp.companies.create({ name, logo }) or company.logo:
ui.logoPicker({ id: "logo", label: "Logo", value: company.logo, onChange: (_p, values) => (company.logo = Number(values.logo)) })One view, many players
A view can be open for many players at once - the handlers get the player who clicked. For content that differs per player (their money, their truck), build a view for that player when it is opened:
mp.commands.add("account", "your account", (player) => {
new UiView({ title: "Account" })
.add(
ui.text(`Truck: ${player.truck}`),
ui.progress({ label: "Engine wear", value: player.damage.engine * 100 }),
)
.show(player)
})Changing what is shown
Change the blocks or options and call update() - everyone who has the view open sees the new version. hide(player) closes it for one player, hide() for everyone.
const race = new UiView({ mode: "hud", title: "Race", position: "top-right" })
race.show(mp.players.all())
function showStandings(order: OpenRoadMP.Player[]) {
race.set(ui.table({ columns: ["#", "Driver"], rows: order.map((p, i) => [String(i + 1), p.name]) }))
race.update()
}onClose tells you when a player closed a window (or left the server):
shop.onClose((player) => mp.log(`${player.name} closed the shop`))Limits
- A view may be at most 32 KB as JSON (a few hundred blocks). For long lists, use pages or tabs.
- Images and icons from the internet are not possible - a server cannot load anything into the player's game.
- The example gamemode
freeroamhas a complete window:/garage(a truck for every brand in tabs).