← Back to posts

How to Load a Custom UI Layout in BeamNG and Beam-MP (The Undocumented Way)

If you've ever tried to create a custom UI for a BeamNG or Beam-MP mod and wanted it to load automatically for players, you've probably discovered what I did: there's almost no documentation on how to do this. After weeks of frustration, dead ends, and a lucky Discord find, I finally figured it out. Here's the whole journey, so hopefully you don't have to suffer like I did.

The Problem

I run a gaming community, and over the Christmas break we had too much time on our hands. We built a drag racing mod for Beam-MP with a custom UI, a light tree and leaderboard that we were genuinely proud of.

Screenshot of our drag racing mod UI

One problem: players had to manually add the UI elements to their screen. There was no obvious way to make our layout load by default when someone joined the server. We searched everywhere for documentation. We asked LLMs. Nothing useful came up.

Attempt #1: The ui_apps Extension

We dug through another mod that seemed to have this working and reverse-engineered something like this:

local M = {}

local APP_DEFS = {
    {
        name = "Drag Light Tree",
        domElement = "<light-tree></light-tree>",
        x = 20,
        y = 20,
        w = 140,
        h = 420,
        visible = true,
        preserveAspectRatio = true,
        sizeLocked = false,
        category = "ui.apps.categories.multiplayer",
    },
    {
        name = "Drag Leaderboard",
        domElement = "<leaderboard-widget></leaderboard-widget>",
        x = 200,
        y = 100,
        w = 380,
        h = 520,
        visible = true,
        preserveAspectRatio = false,
        sizeLocked = false,
        category = "ui.apps.categories.multiplayer",
    },
}

local function api()
    local ex = rawget(_G, "extensions")
    return ex and (ex.uiapps or ex.ui_apps) or nil
end

local function addAppSpecific(def)
    local a = api()
    if not a then return false end
    
    local ok = pcall(a.addAppToCurrentLayout, def)
    if ok and a.saveCurrentLayout then
        pcall(a.saveCurrentLayout, "drag_timer_layout")
    end
    return ok
end

The idea was to hook into the ui_apps or uiapps extension when the world loads, create a layout, and add our app definitions to it.

It never worked.

I spent hours debugging with print statements, stepping through the code line by line. Turns out ui_apps wasn't a valid extension anymore, probably an old API from a previous version of the game. The uiapps extension existed, but its functions were completely different and not useful for what we needed.

Attempt #2: Custom Layout Files

We found some documentation (I genuinely can't remember where) suggesting you could drop your own layout file into settings/ui_apps/layouts/default. Name it anything ending in .uilayout.json, copy the structure from Beam-MP's multiplayer.uilayout.json, and it should appear in the layout selector.

It was default for some of us. Not others.

I ran dump(uiapps.getLayouts()) in the console and noticed something odd: on my machine, multiplayer.uilayout.json was the last entry in the list. On my friend's machine, our custom layout was last. His loaded our layout by default. Mine didn't.

After another few hours of experimentation, I discovered that naming our layout exactly multiplayer.uilayout.json with identical contents to Beam-MP's version plus our UI apps—worked for me.

So we shipped it.

The Server Problem

For a while, this approach seemed fine. Then we launched a public server.

Players would join, see no UI, have no idea what to do, and leave. We watched it happen over and over. Worried we were permanently turning people off from ever coming back, we shut the server down.

Work resumed after the holidays and BeamNG went on the back burner. But the problem nagged at me. I'd spend an hour after work most days refining the mod, fixing bugs, polishing the UI. The "UI doesn't load" issue remained unsolved.

Two days ago I turned the server back on. Raced with some mates. The UI worked for them. Looking good. Then some strangers dropped in and immediately started asking how the game works - clearly they couldn't see the UI either.

Server off. Break time.

Attempt #3: GUI Hooks

Yesterday I decided to properly solve this. I ages digging through BeamNG's UI code in the installation folder and found an event called appContainer:loadLayoutByType. This looked promising, something I could trigger from my mod via guihooks.

Tested it in the game console. It worked.

Put it in my mod. Didn't work.

Of course.

The Solution (Finally)

I checked online again and found a fresh post on the Beam-MP forums, someone asking the exact same question. I replied with my multiplayer.uilayout.json workaround, noting it was unreliable. A Beam-MP maintainer responded: "Don't do it that way."

Great. But how should I do it?

Then I discovered: Beam-MP has a Discord. Of course it does.

I joined and scrolled through the #scripting channel. Someone had asked my exact question: how do I load my layout when a player joins?

The answer, from a helpful community member:

core_gamestate.setGameState('multiplayer', 'mylayout', 'multiplayer')

That's it. That's the whole thing.

I ran it in the console. It worked. I added it to my mod, uploaded it, rejoined the server, and... it worked.

The Frustration

This function isn't documented anywhere I could find. No forum posts. No wiki entries. No official guides. The knowledge exists only in the heads of a few people and, if you're lucky, a recent Discord message.

I happened to join at the right time and scroll to the right message. If that conversation had been a month older, I might never have found it.

For Future Modders

If you're trying to load a custom UI layout in BeamNG or Beam-MP, here's what actually works:

  1. Create your .uilayout.json file with your app definitions
  2. Place it in settings/ui_apps/layouts/default/
  3. Call this when appropriate (e.g., when the player joins multiplayer):
core_gamestate.setGameState('multiplayer', 'your_layout_name', 'multiplayer')

Replace 'your_layout_name' with whatever you set the type field as in your uilayout.json file.


I'm trying to document the BeamNG API and make this knowledge more accessible. If you're interested in more BeamNG modding deep-dives, check out my post on reverse-engineering the BeamNG Lua API where I document my process for creating API stubs.

Hopefully this saves someone else the weeks of frustration it cost me.