Conventions
The rules the API follows and expects you to follow: how things are named, what a call hands back, what stays stable between releases, and how a log line is put together. None of it is new machinery - it is the shape of what the other pages describe, collected in one place.
- Wire events are
<domain>:<verb>, lowercase, one colon:chat:send,race:start. The server’s own traffic uses the domainschat,vehicle,player,session,worldandmodules; a resource picks a domain of its own, usually its name.node:is reserved for the framework. Event names carry no version: a renamed event does not fail, it goes quiet - so keep names once published and add new ones beside them. - Engine hooks are camelCase and never cross the wire:
playerJoined,vehicleSpawned. A notification is<subject><Verb-ed>- past tense, after the subject (vehicleEdited,playerSeatChanged,serverShutdown); a stream is…Changed; a request a handler can deny is<subject><Action>Request(vehicleSpawnRequest,relayRequest) - noonprefix,node.onalready says it. Two spellings, two kinds - a reader can tell a network message from a server hook at a glance. The spellings servers before 1.2.0 used (playerJoin,onVehicleSpawnRequest,canRelay) are deprecated aliases that still work and warn once per resource; see Events → Naming. - Bus messages between resources follow the wire rule:
chat:say,chat:command,dimensions:changed. - Resource names use letters, digits,
_,-and., starting with a letter or digit. The name is the folder, the log tag, thenode.storagestore (storage/<name>.json, at most 64 characters) and the name the client mod files the streamed scripts under (at most 128). - Storage keys are
kind:idstrings up to 256 characters:playtime:42,lastSeen:42. Key byplayer.accountIdorplayer.name, never byplayer.id. - Module channels are
u32ids you choose; the id space is shared by every module and resource on a server, so publish yours.0x44494D53(DIMS) belongs todimensions. - Console tags of your own (
node.log.custom, Clog_custom) stay within six characters so the console columns line up.
Objects and ids
Section titled “Objects and ids”The server thinks in ids: a player id (small, assigned at connect, reused after a disconnect) and a
vehicle’s global id (unique for the life of the server). The node face wraps them in objects;
node.raw and the C ABI keep the ids.
- A
PlayerorVehicleis a table withidand a metatable. Reading any other field fetches the record once and caches it on the object;refresh()drops the cache; methods act by id, so the object outlives the record (player:isConnected(),vehicle:exists()tell you whether it still refers to something). Two objects are==when their ids match;tostringgivesPlayer#3 Alice,Vehicle#12 Alice. - Ids inside a record that name another entity come as objects with the raw id beside them:
vehicle.driveris aPlayer,vehicle.driverIdthe number;player.vehicleaVehicle,player.vehicleIdthe number (-1on foot). - Every
nodecall that takes a player or a vehicle accepts the object or the id:node.send(target, ...),node.players.get(id),vehicle:seat(player),node.bans.add(who). - Where speed matters the ids stay:
node.raw.onhands raw arguments, and the relay filter (relayRequest) receives(fromPid, toPid, category, subtype, globalId)because it runs per packet. - A raw name is the C name in camelCase:
kick_playerisnode.raw.kickPlayer,get_vehicle_transform_jsonisnode.raw.getVehicleTransformreturning the decoded table.
Return shapes
Section titled “Return shapes”The Lua reference marks each shape in the signature; these are the shapes it uses.
| Shape | Used by | Examples |
|---|---|---|
boolean |
actions: true when done, false when the target does not exist or the request was not accepted |
player:kick, node.send, node.storage.set, vehicle:setTag, node.resources.reload |
value or nil (marked ?) |
lookups and reads of things that may not exist yet | node.players.find, vehicle:transform(), node.fs.read, node.json.decode (nil on a parse error) |
| array, possibly empty | lists | node.players.all(), player:vehicles(), node.bans.all() |
number |
counts and ids | node.off (handlers removed), node.after (a timer id), player:resync() (bundles sent) |
result, err |
one background call | node.await(workFn, args) gives the result, or nil and an error string |
status, body, headers |
one coroutine HTTP call | node.http.fetch: 0, "request not queued", {} when it could not start; -1 and the error text in body when the transport failed |
| callback arguments | asynchronous forms | cb(status, body, headers) for HTTP, cb(ok) for node.fs.writeAsync, doneFn(result, err) for node.job |
| a default | node.storage.get(key, default) |
returns default when the key is absent |
| an error (throws) | misuse, not runtime failure | node.on with a non-string name or a non-function handler, node.commands.add with a bad signature, node.sleep outside node.async |
Nothing in node returns an error object: a failed action is false, a missing thing is nil, a
programming mistake throws. node.on(name, fn) with the same fn twice is one subscription: the
second call replaces the first, the handler runs once per event, and node.off(name, fn) removes
it and returns 1 (Events → Naming says the same for the deprecated
spellings). Two different functions are two handlers. Subscribe at load; a reload starts from a
clean state.
In C the shapes are integers: an action returns 0 on success and -1 on failure, a question
1 or 0, a buffer fill the number of bytes written or -1 when the buffer is too small, and a
size query (buf = NULL) the length it would write. Native modules
has the buffer loop.
Payloads
Section titled “Payloads”- A table you hand to
player:send,node.broadcast,node.bus.emit,node.storage.setornode.http.postis JSON-encoded for you: an array when its keys are1..n, an object otherwise; functions becomenull; nesting stops at 32 levels. A string goes as it is. - A wire event’s
dataarrives on the server as the string the client sent - decode it withnode.json.decodeand check the type. A notification’s or request’s payload arrives decoded where it is JSON on the wire (a config, a coupler call) and as a string where it is a name (a seat role). - Bus
datais a string too; the module channel carries bytes and parses nothing. - On the client,
node.emitServer(name, data)sendstostring(data): encode a table withjsonEncodethere and decode on the server.NodeMP.events.triggerServerencodes for you.
Versions and compatibility
Section titled “Versions and compatibility”| Surface | Version | What is promised |
|---|---|---|
| Wire protocol | v18 (Wire::ProtoVersion) |
Exact match. Launcher, client mod and server ship together; a mismatch is refused at the handshake with Protocol version mismatch: launcher speaks v17, server speaks v18 - update the outdated side. |
| C ABI | 1.12 (NODE_ABI_VERSION_MAJOR 1, MINOR 12) |
The major is the layout: nothing moves within it, new entries are appended and bump the minor, a retired entry becomes a stub that keeps its slot. A module built against an older 1.x keeps working; a different major is refused by the loader. |
Lua node and node.raw |
server 1.2.1 |
Generated from one schema, sdk/api.toml, together with node.h and the reference pages; apigen.py docs --check fails when they drift, so the reference says what the server does. node.raw is the one-to-one mirror of the C entries. |
Client node table |
client mod 1.4.0 |
The ten functions on Client scripting. |
NodeMP.* |
client mod 1.4.0 (NodeMP.VERSION) |
One stable global; the original flat helpers stay as aliases of the namespaced calls. NodeMP.internal and the dotted module names underneath may move between versions. |
The wire changelog lives in server/include/net/Protocol.h, and the ABI history in the entry
docs of sdk/node.h (ABI 1.8, ABI 1.9, …). Neither is duplicated here.
The Lua environment
Section titled “The Lua environment”Each resource runs in its own Lua 5.4 state with the standard libraries open - string, table,
math, os, io, coroutine, utf8, debug - and only the C module loaders removed
(Native modules). node adds the
server; it does not duplicate the standard library, so several “how do I” questions have a
standard Lua answer, and a few have none yet:
| I want to | Use | Notes |
|---|---|---|
A random float in [0, 1) |
math.random() |
Lua 5.4 seeds the generator randomly when the state is created; no math.randomseed call is needed. |
A random integer in [a, b] |
math.random(a, b) |
|
A random float in [a, b) |
a + (b - a) * math.random() |
There is no node helper for it. |
| Random bytes for a token or a key | node.crypto.randomBytes(n), node.crypto.randomHex(n?) |
Cryptographic; math.random is not. |
| How long something took | node.server.uptime() before and after: monotonic, fractional seconds |
os.clock() is the process’s CPU time over every thread, so it measures CPU-bound code on the worker and little else. There is no per-handler statistics call like BeamMP’s Util.DebugExecutionTime; the server itself logs every handler slice over 250 ms (Concurrency). |
| The wall clock | node.server.time() (unix, fractional), node.server.unixTime() (whole seconds), os.date, os.time |
|
| Memory used by this resource | collectgarbage("count") * 1024 - bytes of this Lua state |
The current state only: there is no figure for all states together, nor for the process, and node.server.metrics() carries counts (players, vehicles, queue depth), not bytes. |
| The operating system | not in node |
package.config:sub(1, 1) is "\\" on Windows and "/" elsewhere, which is what a path needs; the OS name and version are not exposed. node.server.version() is the server’s own version. |
| JSON | node.json.encode(value), node.json.decode(text) |
encode writes compact JSON and takes no options: no pretty-printing, no minify, no flatten (RFC 6901), no diff or patch (RFC 6902) - BeamMP’s Util.JsonPrettify, JsonFlatten, JsonDiff and JsonDiffApply have no equivalent. Pretty-print with a few lines of Lua when a file is for humans; a diff is a table comparison you write. |
local t0 = node.server.uptime()local sum = 0for _ = 1, 100000 do sum = sum + math.random(1, 6) endnode.log("100000 dice rolls in %.2f ms, average %.3f", (node.server.uptime() - t0) * 1000, sum / 100000)
node.log("float %.3f · int %d · float in [10, 20) %.3f · token %s", math.random(), math.random(1, 6), 10 + 10 * math.random(), node.crypto.randomHex(4))node.log("this Lua state uses %d KB · path separator %s", math.floor(collectgarbage("count")), package.config:sub(1, 1))
-- expect: 100000 dice rolls in \d+\.\d\d ms, average 3\.\d+-- expect: this Lua state uses \d+ KBLogging
Section titled “Logging”Every console line is HH:MM:SS Tag › message: the time, a tag padded to six characters, a
› and the message; logs/server.log receives the same line without colours. With
[General] Debug = true the timestamp gains milliseconds and a column with the writing thread’s
name is inserted after the › (Res › PluginFramework race · …), so a parser should not
assume the message starts right after it. Where your output lands:
node.log(msg, ...)prints under theRestag with the resource’s name in front:race · Player#0 Alice is ready. Extra arguments arestring.formatarguments.node.log.warnandnode.log.erroruse theWarnandErrortags, same prefix. Errors the server reports on your behalf readrace · error in event 'race:ready': ...with a stack trace (error in resource event '…'for a bus handler,error in timer callback,error in async task), and a handler that holds the worker for more than 250 ms is reported as a stall, with the resource and the kind in parentheses. Not every line the server prints about your code carries the prefix:emitClient: invalid player ID '0'(anode.sendto a player that is not there) andnode.sleep called outside a node.async taskname no resource.node.log.tag(tag, msg)writes under one of the server’s own tags -Core,Net,Res,Mods,Module,Join,Leave,Kick,Veh,Warn,Error,Debug; an unknown tag falls back toModule.node.log.custom(tag, rgb, msg)uses a tag of your own in an0xRRGGBBcolour,node.log.raw(text)skips the prefix,node.log.sink(fn)observes every line,node.log.titlesets the console title.- A native module’s
log_infolands underModule;log_customunder its own tag; a C log sink may suppress lines, a Lua one only observes.
On the client, beamng.log tags the mod’s lines: node.res for delivery and activation of your
files, node.events for node.log and handler errors (Error in event handler for "race:start" from source "node.res/race": ...), node.net and node.session for the link and the roster, and
nodemp.events for errors inside NodeMP.events.on handlers (Handler for 'race:start' errored: ...).
- Lua API reference - every signature with its shape marked.
- Events - the naming rule applied to every event kind.
- Native modules - the C conventions in full.
- Wire protocol - where
v18is defined.
