← Back to posts

Reverse-Engineering the BeamNG Lua API (So You Don't Have To)

A couple of weeks ago over my Christmas break, a few mates and I got really into BeamNG/BeamMP. One of the first maps we loved was a drag racing server, but it was hosted overseas and the lag was awful. So we did what any reasonable group of programmers with too much free time would do: we decided to spin up our own server and find a drag racing mod.

Couldn't find anything we liked. So we decided to write our own.

This was probably a mistake.

The documentation problem

After a few iterations on the mod, I started getting frustrated. There's basically no documentation for BeamNG's Lua API. No official reference, barely any blog posts, nothing. I found a stub project on GitHub but it was missing a ton of stuff: no param types, no return types, no descriptions.

My first approach was just dumping keys from the in-game Lua console:

local function dumpKeys(name, obj)
  log('I', 'stubgen', '== ' .. name .. ' ==')
  if type(obj) == 'table' then
    for k,_ in pairs(obj) do log('I','stubgen', name .. '.' .. tostring(k)) end
  else
    local mt = getmetatable(obj)
    if mt then
      local idx = rawget(mt, "__index")
      if type(idx) == "table" then
        for k,_ in pairs(idx) do log('I','stubgen', name .. ':' .. tostring(k)) end
      end
      for k,_ in pairs(mt) do
        log('I','stubgen', name .. '  ' .. tostring(k))
      end
    end
  end
end

dumpKeys("be", be)

This technically works but it's pretty useless. You get function names and nothing else. No idea what they do or what they expect.

Trying to automate it

I got Claude Code to help me write a Go CLI tool that scans the BeamNG Lua files for globally defined functions. This worked better—we got stubs generated that looked like:

-- BeamNG.drive Lua API Stubs
-- Module: core
-- Auto-generated by beamng-api-extractor (Go)

---@meta

---@class core
core = {}

--- compression
---@param arg1 any
---@return any
function core:compression(arg1) end

Better than nothing, but everything is typed as any and there's no descriptions. The bigger problem was that huge chunks of the API were still missing. I expanded the tool to search for functions called on objects across multiple files, which helped, but the definitions themselves weren't in the Lua code anywhere.

That's when I realised a lot of these functions must be defined in the game engine itself, not in Lua. Native exports.

The Ghidra rabbit hole

So I did what any sane person would do and opened the BeamNG binary in Ghidra.

I have never really touched assembly before. It was immediately obvious I was out of my depth.

I let Ghidra analyse the binary, searched for getPlayerVehicle, and found this massive function that seems to handle Lua exports. Here's a bit of the pseudo-C that Ghidra reconstructed:

undefined8 *FUN_140236560(undefined8 *param_1,undefined8 param_2,longlong param_3,undefined8 param_4)
{
  FUN_140236950(param_1);
  param_1[5] = param_2;
  *param_1 = engine::EngineExport::vftable;
  param_1[9] = 0;
  *(undefined4 *)(param_1 + 6) = 0;
  param_1[7] = param_3;
  param_1[8] = param_4;
  param_1[9] = *(undefined8 *)(param_3 + 0x50);
  *(undefined8 **)(param_3 + 0x50) = param_1;
  *param_1 = engine::EngineExportScope::vftable;
  return param_1;
}

Yeah, I have no idea what this means. All the function names and parameters are randomly generated since you can't see the original source. The decompiled code was over 100MB so I couldn't just throw it at an LLM either.

I even set up a Ghidra MCP server to let Claude poke around in there, but it was painfully slow-burning through tokens just renaming one parameter at a time. Would've taken days. Mad respect to people who actually know how to do reverse engineering, because I certainly don't.

I gave up on that approach.

Back to Lua

I went back to the Lua stubs and found another project that, while incomplete, gave me a better sense of what objects and functions I was missing. I combined that with the stubs i already had to get a more complete version.

I wrote a script that walked through every stub and flagged anything missing a description, examples, param types, or return types, basically a big TODO list.

Then I fed that TODO list into Claude Code and asked it to pick a function, search for usages of it throughout the BeamNG Lua codebase, and construct a proper stub entry from the context. This took a while but it actually worked pretty well. We ended up with decent coverage.

-- Gets the vehicle for a player
-- example:  local veh = be:getPlayerVehicle(0)
--- @occurrences 26
---@param player integer -- Player index (usually 0)
---@return BeamNGVehicle|nil -- Player's vehicle
function be:getPlayerVehicle(player) end

Are the stubs perfect?

No. Not even close.

LLMs are great tools but they hallucinate. I've already found multiple functions that are missing or incorrectly documented while working on my mod. This is going to be a long process to sort out.

But it's way better than what existed before.

The mod itself

After spending days on this side quest I finally got back to actually working on the mod. It's coming along well. I know people have opinions about LLM assisted coding, but I'm a software dev in my day job, I know what to look for and what to be skeptical of. After reviewing all the code over many iterations, I've picked up Lua (which is honestly pretty simple) and learned enough of the APIs that I can add features and fix bugs without LLM help now. I still ask ChatGPT for pointers sometimes, especially for anything involving math, but less than I used to.

There's still some rough spots in the codebase. We'll work it out.

The stubs

If you're getting into BeamNG or BeamMP modding and want a starting point for the Lua API, you can find the stubs here: github.com/acm-gaming/beammp-lua-stubs

They're not complete and they're not always correct, but they might save you some time. If nothing else, hopefully this post helps you feel less alone if you've been banging your head against the BeamNG API. The documentation situation really is that bad—it's not just you.