Lua API reference (node)
Generated from sdk/api.toml by sdk/tools/apigen.py docs. Do not edit; edit the schema.
This is the API a resource is written against. It lives in sdk/lua/prelude.lua, which the server runs into every resource’s Lua state on top of node.raw – the 1:1 mirror of the C ABI (Raw Lua reference (node.raw.*)), still there as the escape hatch. Signatures read node.ns.name(params) -> result; Player/Vehicle are objects (methods with :); a ? marks an optional parameter or a result that can be nil. A data argument that accepts a table is JSON-encoded for you; a payload documented as a table was JSON-decoded for you.
Guides: Getting started · Events · Concurrency · Wire events and clients · Recipes · Conventions
Contents
Section titled “Contents”- Top level: events, sending, timers, coroutines –
on,off,send,broadcast,after,every,cancel,defer,async,sleep,wait,yield,await,job,log - node.players – the roster –
get,all,count,ids,find - Player objects –
isConnected,refresh,kick,ban,verify,setStrict,send,sendModule,setRole,setGroup,vehicles,position,focus,headPose,camera,inputs,seat,unseat,swapWith,resync,tell - node.vehicles – the registry –
get,all,count,spawn,deleteAll,byTag,byTags,transforms - Vehicle objects –
exists,refresh,delete,transform,controls,inputs,electrics,powertrain,engine,controllers,couplers,nodes,breakGroups,paints,config,damage,sync,occupants,tag,setTag,removeTag,lock,unlock,lockState,setCoupler,trigger,resync,seat,setGroup - node.server – identity, limits, time, metrics –
name,setName,map,version,port,maxPlayers,setMaxPlayers,maxCars,setMaxCars,uptime,time,unixTime,metrics,snapshot,nodeGrabEnabled - node.bans –
add,remove,has,all - node.storage – persistent key/value –
get,set,delete - node.fs – files inside the resource folder –
read,write,writeAsync,list - node.http –
get,post,request,put,patch,delete,head,fetch - node.pg – PostgreSQL –
enabled,ready,query,exec,tx - node.json –
encode,decode - node.crypto –
sha256,hmac,randomBytes,randomHex - node.log –
warn,error,tag,custom,raw,sink,title - node.bus – between resources –
emit,on,off - node.modules – the binary channel –
on,off,send - node.relay – who sees what –
filter,unfilter,invalidate - node.resources and node.config –
reload,manifest - node.chat –
say,tell - node.commands –
add,remove
Top level: events, sending, timers, coroutines
Section titled “Top level: events, sending, timers, coroutines”What every resource uses every day sits directly on node: subscribing to events, sending to clients, timers, and the coroutine helpers. Handlers receive OBJECTS – a Player, a Vehicle – where the raw API has ids, and decoded tables where a payload is documented as JSON.
node.on(name, fn)
Section titled “node.on(name, fn)”Raw: node.raw.on
Subscribes fn to an event. The name decides what fn receives: engine events pass a Player or a Vehicle (or nothing for serverTick/serverShutdown); notifications and cancellable requests pass (player, vehicle, payload) with the payload decoded when it is JSON on the wire (the vehicle is left out where none is involved: playerConnectRequest, playerVerifyReported); any other name is a wire event from the client half and passes (player, data) with data as the string the client sent. A cancellable handler returns false[, reason] to deny. The events reference lists every name with its exact arguments. Several handlers per name are fine, across resources too; subscribing the same function twice replaces the first. A deprecated name (playerJoin, onPlayerConnectRequest, canRelay, …; the Formerly line of each event) subscribes to the same event as its canonical name and logs one warning per resource per old name (once per load: the once-table lives in the resource’s state, so a reload warns again) – so the same function under both spellings is one subscription, two different functions are two handlers.
node.off(name, fn?) -> number
Section titled “node.off(name, fn?) -> number”Raw: node.raw.off
Unsubscribes this resource’s handlers for the name – all of them, or only fn – and returns how many were removed. fn is the function that was given to node.on, matched by identity from any context, including coroutines (node.async, pg transaction bodies). A deprecated name is mapped like in node.on, so either spelling unsubscribes what either spelling subscribed.
node.send(target, event, data?) -> boolean
Section titled “node.send(target, event, data?) -> boolean”Raw: node.raw.emitClient
Sends a wire event to one player. target is a Player or an id; data is a table (JSON-encoded for you) or a string. false when the player is not connected. player:send is the same call.
node.broadcast(event, data?, except?) -> boolean
Section titled “node.broadcast(event, data?, except?) -> boolean”Raw: node.raw.emitAll, emitOthers
Sends a wire event to every connected player – or to everyone except one (a Player or an id), which is the shape of a relay: the excepted player counts as the sender for the relay filter and visibility groups. data is a table or a string.
node.after(ms, fn) -> number
Section titled “node.after(ms, fn) -> number”Raw: node.raw.setTimeout
Runs fn once after ms milliseconds on the worker thread; returns a timer id for node.cancel.
node.every(ms, fn) -> number
Section titled “node.every(ms, fn) -> number”Raw: node.raw.setInterval
Runs fn every ms milliseconds on the worker thread until cancelled; returns a timer id. A slow fn delays the next run; runs do not stack.
node.cancel(timerId)
Section titled “node.cancel(timerId)”Raw: node.raw.clearTimer
Cancels a timer from after or every. Safe for an id that already fired.
node.defer(fn)
Section titled “node.defer(fn)”Raw: node.raw.setImmediate
Runs fn after the current batch of handlers has finished – the way to act once every other handler of the event you are in has seen it.
node.async(fn, ...) -> number
Section titled “node.async(fn, ...) -> number”Raw: node.raw.async
Runs fn(…) as a cooperative coroutine on the worker and returns its task id. Inside it node.sleep, node.wait, node.await, node.yield and node.http.fetch suspend the coroutine while everything else keeps running.
node.sleep(ms)
Section titled “node.sleep(ms)”Raw: node.raw.sleep
Suspends the current coroutine for ms milliseconds. Only inside node.async.
node.wait(msOrPredicate, intervalMs?)
Section titled “node.wait(msOrPredicate, intervalMs?)”Raw: node.raw.wait
A number sleeps; a function suspends the coroutine until it returns truthy, polled every intervalMs (default 50). Only inside node.async.
node.yield()
Section titled “node.yield()”Raw: node.raw.yield
Gives the worker back for one turn inside a long loop that must run on it. Fairness, not parallelism. Only inside node.async.
node.await(workFn, args?) -> any, string?
Section titled “node.await(workFn, args?) -> any, string?”Raw: node.raw.await
Runs a SELF-CONTAINED function (no upvalues, no node, JSON-serialisable args and result) on a background pool thread and suspends the coroutine until it returns; gives back the result, or nil plus an error. Only inside node.async.
node.job(workFn, args?, doneFn) -> boolean
Section titled “node.job(workFn, args?, doneFn) -> boolean”Raw: node.raw.job
The callback form of node.await: workFn runs on the pool, doneFn(result, err) on the worker. false when workFn could not be dumped.
node.log(msg, ...)
Section titled “node.log(msg, ...)”Raw: node.raw.log
Prints msg to the server console under this resource’s name. Extra arguments are string.format arguments: node.log("%s joined", player.name). The table also carries the levels: node.log.warn, node.log.error, and the raw forms below.
node.config : table
Section titled “node.config : table”The [config] table of this resource’s resource.toml, as written (strings, numbers, booleans, arrays, nested tables); an empty table when there is none. Read once at load; node.resources.manifest().config re-reads the file.
node.raw : table
Section titled “node.raw : table”The 1:1 mirror of the C ABI: one function per C entry, ids in, tables out (Raw Lua reference (node.raw.*)). Everything above is built on it; reach for it when you need exactly the C behaviour.
node.players – the roster
Section titled “node.players – the roster”Look players up and iterate them. get returns a Player object for any id (cheap: nothing is fetched until a field is read), so it is fine to call in a 60 Hz handler.
node.players.get(id) -> Player?
Section titled “node.players.get(id) -> Player?”The Player with this id – a Player or a number-like table. Cheap: nothing is fetched until a field is read, so it is fine inside a 60 Hz handler. nil for a negative or non-numeric id; an unknown id gives a Player whose isConnected() is false.
node.players.all() -> array<Player>
Section titled “node.players.all() -> array<Player>”Raw: node.raw.getPlayers
Every connected player, as Players. Ascending by id.
node.players.count() -> number
Section titled “node.players.count() -> number”Raw: node.raw.getPlayerCount
How many players are connected.
node.players.ids() -> array<number>
Section titled “node.players.ids() -> array<number>”Raw: node.raw.getPlayers
The connected player ids.
node.players.find(name) -> Player?
Section titled “node.players.find(name) -> Player?”The connected player with this display name, case-insensitively; nil when none.
Player objects
Section titled “Player objects”A Player is a table with id and a metatable. Reading any other field fetches the player’s record once and caches it on the object (refresh() drops the cache); methods act by id, so an object outlives the record it was made from – player:isConnected() tells you whether it still refers to someone. Two Players are == when their ids match; tostring(player) is Player#<id> <name>.
player.id : number
Section titled “player.id : number”The player id – small, assigned at connect and REUSED after a disconnect. Key durable data by name or ip, not by id.
player.name : string?
Section titled “player.name : string?”The display name. Still known inside a playerLeft handler (the prelude remembers it), nil for an id that was never seen.
player.ip : string?
Section titled “player.ip : string?”The remote IP as text; what an IP ban keys on. For a verified account, accountId is the better key – it does not change with the player’s network.
player.port : number?
Section titled “player.port : number?”The remote TCP port.
player.connectedSeconds : number?
Section titled “player.connectedSeconds : number?”Seconds since the connection was accepted.
player.synced : boolean?
Section titled “player.synced : boolean?”true once the player finished joining (received the world).
player.udpConnected : boolean?
Section titled “player.udpConnected : boolean?”true while the UDP channel is bound; false means positions fall back to TCP.
player.pingSeconds : number?
Section titled “player.pingSeconds : number?”Seconds since the last keepalive: a freshness metric (0-1 healthy), not a round-trip time.
player.role : string?
Section titled “player.role : string?”The role a plugin assigned with setRole (“” when none). Per session.
player.group : number?
Section titled “player.group : number?”The visibility group; 0 is the shared world.
player.vehicle : Vehicle?
Section titled “player.vehicle : Vehicle?”The vehicle the player occupies in any seat, or nil on foot. vehicleId is the same as a number (-1 on foot).
player.seatRole : string?
Section titled “player.seatRole : string?”“driver”, “passenger” or “none”.
player.vehicleCount : number?
Section titled “player.vehicleCount : number?”How many vehicles the player drives or spawned.
player.unicycle : number?
Section titled “player.unicycle : number?”The global id of the walking avatar, -1 when seated.
player.verified : boolean?
Section titled “player.verified : boolean?”true when the join ticket was redeemed with the directory (wire v17): the name is then the account’s username or the guest name the directory minted, and somebody other than the player vouched for it. false on a server with no [Directory], for a join that brought no ticket, and for one admitted through RedeemFailOpen.
player.guest : boolean?
Section titled “player.guest : boolean?”true when there is no account behind the name: a Test Drive session, or nothing verified at all. verified and not guest is “a signed-in account”.
player.accountId : number?
Section titled “player.accountId : number?”The directory’s account id, nil for a guest. Stable across sessions and addresses – the key to persist anything about a player by, and what node.bans.add(accountId) bans.
player.accountRoles : string?
Section titled “player.accountRoles : string?”The directory’s role string for the account (“ADM” for a directory admin; “” for everyone else and for guests). Distinct from role, which is this server’s own per-session role.
player.identifiers : array<string>?
Section titled “player.identifiers : array<string>?”What the directory knows the player by: “nodemp:<id>”, “discord:<id>”, “ip:<addr>”. Empty for an unverified player.
player:isConnected() -> boolean
Section titled “player:isConnected() -> boolean”Raw: node.raw.isPlayerConnected
true while a player with this id is connected. Ids are reused, so this cannot say it is the SAME player you saw earlier.
player:refresh() -> Player
Section titled “player:refresh() -> Player”Drops the cached record so the next field read fetches it again. Returns the player for chaining.
player:kick(reason?) -> boolean
Section titled “player:kick(reason?) -> boolean”Raw: node.raw.kickPlayer
Disconnects the player, showing the reason. false for an unknown id.
player:ban(reason?) -> boolean
Section titled “player:ban(reason?) -> boolean”Raw: node.raw.banPlayer
Bans the player and kicks it with the reason: its account when one was verified (the ban follows the account to any address) and its IP always. Persisted in bans.json.
player:verify(level) -> boolean?, string?
Section titled “player:verify(level) -> boolean?, string?”Raw: node.raw.verifyPlayer
Asks the player’s launcher helper to check the game install again, now, at level "size" (file lengths), "scripts" (plus a hash of the game’s lua/ and ui/), "full" (hash everything – slow, an audit) or "strict" (the archive tables of contents against the server’s reference manifest, the whole game root, the user folder). The answer arrives as the playerVerifyReported event, after the server applied its own verdict (a “differs” or “failed” report kicks the player when [General] VerifyGame is not off). Returns true when the request went out, or nil and a reason: "unknown player"; "unsupported" – that level is not available for this player (its launcher is too old for it, or strict is not on offer because this server has no reference manifest); "pending" – a request to this player is still counted as in flight (the server accepts one per player per 60 s; the check every join runs does not count, so a player:verify from a playerJoined handler goes out at once). Any other level raises Player:verify: unknown level '...' at the caller’s line.
player:setStrict(on) -> boolean?, string?
Section titled “player:setStrict(on) -> boolean?, string?”Raw: node.raw.setPlayerStrict
Marks the session strict (on true) or lifts the mark (false or nil; any value other than false/nil counts as on – including 0). The server then refuses Vehicle::Camera frames FROM the player that target a vehicle it does not occupy – they are not relayed, so a strict client cannot follow other people’s cars with its camera; frames from other players to it are unaffected. A refused report still fires playerCameraChanged, with denied = true in player:camera(). Server-side only: tell the client about its strict mode yourself (the session config). Per session, cleared at disconnect. Returns true, or nil and "unknown player".
player:send(event, data?) -> boolean
Section titled “player:send(event, data?) -> boolean”Raw: node.raw.emitClient
Sends a wire event to this player; data is a table (JSON-encoded) or a string.
player:sendModule(channel, data) -> boolean
Section titled “player:sendModule(channel, data) -> boolean”Raw: node.raw.sendModule
Sends raw bytes to this player on a u32 module channel.
player:setRole(role) -> boolean
Section titled “player:setRole(role) -> boolean”Raw: node.raw.setPlayerRole
Assigns the per-session role string (“” clears). The client shows it as a tag beside the name.
player:setGroup(group)
Section titled “player:setGroup(group)”Raw: node.raw.setPlayerGroup
Puts the player in a visibility group; 0 is the shared world. Move its vehicles too (vehicle:setGroup).
player:vehicles() -> array<Vehicle>
Section titled “player:vehicles() -> array<Vehicle>”Raw: node.raw.getPlayerVehicles
The vehicles the player drives or spawned, as Vehicles.
player:position() -> record{x,y,z}?
Section titled “player:position() -> record{x,y,z}?”Raw: node.raw.getPlayerPosition
The last known position, from the occupied vehicle or the walking avatar; nil before the first snapshot.
player:focus() -> record{pos,ageSeconds}?
Section titled “player:focus() -> record{pos,ageSeconds}?”Raw: node.raw.getPlayerFocus
The point the relay’s distance filter uses for this player: the camera when a head pose streams, else the driven vehicle.
player:headPose() -> record{pos,rot,freeCamera,ageSeconds}?
Section titled “player:headPose() -> record{pos,rot,freeCamera,ageSeconds}?”Raw: node.raw.getPlayerHeadPose
The last camera/head pose (about 10 Hz); nil until one arrived.
player:camera() -> record{vehicle,ageSeconds,denied?}?
Section titled “player:camera() -> record{vehicle,ageSeconds,denied?}?”Raw: node.raw.getPlayerCamera
Which vehicle the camera is on – its own or the one spectated; vehicle is -1 for none. In a strict session (setStrict) a report the server refused to relay – its target is a vehicle the player does not occupy – carries denied = true; the field is absent otherwise.
player:inputs() -> record{vehicle,controls,controlsAgeSeconds,axes,axesAgeSeconds}?
Section titled “player:inputs() -> record{vehicle,controls,controlsAgeSeconds,axes,axesAgeSeconds}?”Raw: node.raw.getPlayerInputs
Everything the player is pressing: the driven vehicle, the five controls and gear from the 60 Hz snapshot, the extra axes, each with its age. nil unless the player drives.
player:seat(vehicle, role?) -> boolean
Section titled “player:seat(vehicle, role?) -> boolean”Raw: node.raw.seatPlayer
Seats the player in the vehicle (a Vehicle or a gid) as “driver” (default) or “passenger”, bypassing locks; a previous driver becomes a passenger.
player:unseat() -> boolean
Section titled “player:unseat() -> boolean”Raw: node.raw.unseatPlayer
Puts the player on foot. false when it already is.
player:swapWith(other) -> boolean
Section titled “player:swapWith(other) -> boolean”Raw: node.raw.swapPlayers
Exchanges seats – role and vehicle – with another Player (or id), across the whole driver/passenger/on-foot matrix.
player:resync() -> number
Section titled “player:resync() -> number”Raw: node.raw.resyncPlayer
Re-sends every vehicle’s spawn and cached state to this player; returns how many bundles went out.
player:tell(text, ...)
Section titled “player:tell(text, ...)”Says a system line in this player’s chat (via the chat resource; silent without it). Extra arguments are format arguments.
node.vehicles – the registry
Section titled “node.vehicles – the registry”Look vehicles up and iterate them. Unicycles – the walking avatars – are vehicles to the server; all() leaves them out unless asked, get returns them like any other.
node.vehicles.get(id) -> Vehicle?
Section titled “node.vehicles.get(id) -> Vehicle?”The Vehicle with this global id, cheap like players.get. nil for a negative or non-numeric id; an unknown id gives a Vehicle whose exists() is false.
node.vehicles.all(includeUnicycles?) -> array<Vehicle>
Section titled “node.vehicles.all(includeUnicycles?) -> array<Vehicle>”Raw: node.raw.getVehicles
Every vehicle in the registry as Vehicles with their records already loaded. Walking avatars (unicycles) are left out unless includeUnicycles is true.
node.vehicles.count() -> number
Section titled “node.vehicles.count() -> number”Raw: node.raw.getVehicleCount
How many vehicles the registry holds, unicycles included.
node.vehicles.spawn(config, ownerName?) -> Vehicle?
Section titled “node.vehicles.spawn(config, ownerName?) -> Vehicle?”Raw: node.raw.spawnVehicle
Spawns an ownerless vehicle from a config (a table or JSON text in the client SpawnReq schema: jbm, vcf, pos, rot, …) and hands its simulation to the best available client. nil when nobody can host it.
node.vehicles.deleteAll() -> number
Section titled “node.vehicles.deleteAll() -> number”Raw: node.raw.deleteAllVehicles
Deletes every vehicle; returns how many.
node.vehicles.byTag(key, value?) -> array<Vehicle>
Section titled “node.vehicles.byTag(key, value?) -> array<Vehicle>”Raw: node.raw.getVehiclesByTag
Vehicles carrying the tag key – any value, or exactly value.
node.vehicles.byTags(pairs) -> array<Vehicle>
Section titled “node.vehicles.byTags(pairs) -> array<Vehicle>”Raw: node.raw.getVehiclesByTags
Vehicles whose tags match EVERY key = value pair of the table (AND).
node.vehicles.transforms() -> array<record{id,pos,rot,vel,angVel,time,ping,seq,paused,transitioning,teleport,velocityStep,hasControls,ageSeconds}>
Section titled “node.vehicles.transforms() -> array<record{id,pos,rot,vel,angVel,time,ping,seq,paused,transitioning,teleport,velocityStep,hasControls,ageSeconds}>”Raw: node.raw.getVehicleTransforms
The positions table: every vehicle’s last snapshot in one array under one lock – the read for a radar polling every tick.
Vehicle objects
Section titled “Vehicle objects”A Vehicle is a table with id and a metatable, lazy like a Player: the registry record is fetched on the first field read and cached until refresh(). Ids that name players come as Players (driver, spawner, authority, passengers) with the raw ids beside them (driverId, …). Everything that streams – transform, controls, electrics – is a method, because it is a fresh read each time.
vehicle.id : number
Section titled “vehicle.id : number”The global id, unique for the life of the server.
vehicle.name : string?
Section titled “vehicle.name : string?”The owner label the spawn carried (the spawner’s display name for a player’s car, the ownerName given to vehicles.spawn).
vehicle.spawner : Player?
Section titled “vehicle.spawner : Player?”The Player that spawned it, nil for a server-spawned one. spawnerId is the id (-1).
vehicle.driver : Player?
Section titled “vehicle.driver : Player?”The Player at the wheel, nil when empty. driverId is the id (-1).
vehicle.authority : Player?
Section titled “vehicle.authority : Player?”The Player simulating it (not necessarily the spawner). authorityId is the id.
vehicle.passengers : array<Player>
Section titled “vehicle.passengers : array<Player>”The passengers as Players. passengerIds are the ids.
vehicle.isUnicycle : boolean?
Section titled “vehicle.isUnicycle : boolean?”true for a walking avatar.
vehicle.idleSeconds : number?
Section titled “vehicle.idleSeconds : number?”How long nobody has driven it; 0 while driven.
vehicle.spawnerOnline : boolean?
Section titled “vehicle.spawnerOnline : boolean?”Whether the spawning player is still connected.
vehicle.lockMode : string?
Section titled “vehicle.lockMode : string?”“unlocked”, “locked” or “driver-only”. lockWhitelist are the player ids always allowed in.
vehicle.configHash : number?
Section titled “vehicle.configHash : number?”FNV-1a-32 of the cached config – what clients echo so a desynced config is detectable.
vehicle.damageCounter : number?
Section titled “vehicle.damageCounter : number?”The authority-reported damage version. damageBlobCounter and damageBlobSize describe the stored blob (0 = pristine).
vehicle.tags : table?
Section titled “vehicle.tags : table?”The tags as key -> value.
vehicle.group : number?
Section titled “vehicle.group : number?”The visibility group; 0 is the shared world.
vehicle:exists() -> boolean
Section titled “vehicle:exists() -> boolean”true while the registry has this id.
vehicle:refresh() -> Vehicle
Section titled “vehicle:refresh() -> Vehicle”Drops the cached record. Returns the vehicle for chaining.
vehicle:delete() -> boolean
Section titled “vehicle:delete() -> boolean”Raw: node.raw.deleteVehicle
Deletes the vehicle for everyone; vehicleDeleted follows.
vehicle:transform() -> record{pos,rot,vel,angVel,time,ping,seq,paused,transitioning,teleport,velocityStep,hasControls,ageSeconds}?
Section titled “vehicle:transform() -> record{pos,rot,vel,angVel,time,ping,seq,paused,transitioning,teleport,velocityStep,hasControls,ageSeconds}?”Raw: node.raw.getVehicleTransform
The last kinematic snapshot; nil before the first. A few ticks old by construction.
vehicle:controls() -> record{steering,throttle,brake,clutch,parkingBrake,gear}?
Section titled “vehicle:controls() -> record{steering,throttle,brake,clutch,parkingBrake,gear}?”Raw: node.raw.getVehicleControls
The driving inputs from the last snapshot; nil while no driver has sent one.
vehicle:inputs() -> record{axes,ageSeconds}?
Section titled “vehicle:inputs() -> record{axes,ageSeconds}?”Raw: node.raw.getVehicleInputs
The driver’s extra (modded) input axes, merged from the delta stream.
vehicle:electrics() -> table?
Section titled “vehicle:electrics() -> table?”Raw: node.raw.getVehicleElectrics
The merged electrics (lights, signals, gauges).
vehicle:powertrain() -> table?
Section titled “vehicle:powertrain() -> table?”Raw: node.raw.getVehiclePowertrain
The merged powertrain device state.
vehicle:engine() -> table?
Section titled “vehicle:engine() -> table?”Raw: node.raw.getVehicleEngine
The merged engine state.
vehicle:controllers() -> table?
Section titled “vehicle:controllers() -> table?”Raw: node.raw.getVehicleControllers
The merged controller calls, “controller|function” -> call.
vehicle:couplers() -> array?
Section titled “vehicle:couplers() -> array?”Raw: node.raw.getVehicleCouplers
The merged coupler/door state.
vehicle:nodes() -> table?
Section titled “vehicle:nodes() -> table?”Raw: node.raw.getVehicleNodes
The last node-position (deformation) packet, as sent.
vehicle:breakGroups() -> array<string>?
Section titled “vehicle:breakGroups() -> array<string>?”Raw: node.raw.getVehicleBreakGroups
The break groups reported broken.
vehicle:paints() -> array?
Section titled “vehicle:paints() -> array?”Raw: node.raw.getVehiclePaints
The paint layers from the cached config.
vehicle:config() -> table?
Section titled “vehicle:config() -> table?”Raw: node.raw.getVehicleConfig
The cached spawn/edit config – what a joiner receives.
vehicle:damage() -> record{counter,blobCounter,size,blob}?
Section titled “vehicle:damage() -> record{counter,blobCounter,size,blob}?”Raw: node.raw.getVehicleDamage
Damage telemetry with the raw blob (a binary string, nil when pristine).
vehicle:sync() -> record{authority,authorityAgeSeconds,seatEpoch,spawnedAgeSeconds,idleSeconds,configChangedAgeSeconds,hasPos,posAgeSeconds,posSeq}?
Section titled “vehicle:sync() -> record{authority,authorityAgeSeconds,seatEpoch,spawnedAgeSeconds,idleSeconds,configChangedAgeSeconds,hasPos,posAgeSeconds,posSeq}?”Raw: node.raw.getVehicleSync
How alive the sync is: the authority and its heartbeat age, seat epoch, ages. The record for ‘why is this car frozen’.
vehicle:occupants() -> array<record{player,role}>
Section titled “vehicle:occupants() -> array<record{player,role}>”Raw: node.raw.getVehicleOccupants
Who is in it, driver first: { player = Player, role = "driver" | "passenger" }.
vehicle:tag(key) -> string?
Section titled “vehicle:tag(key) -> string?”Raw: node.raw.getVehicleTag
One tag’s value.
vehicle:setTag(key, value) -> boolean
Section titled “vehicle:setTag(key, value) -> boolean”Raw: node.raw.setVehicleTag
Sets a tag (key 1-64 bytes, value 1-256, 32 keys per vehicle; over-limit is rejected). Broadcast to clients; vehicleTagsChanged fires.
vehicle:removeTag(key) -> boolean
Section titled “vehicle:removeTag(key) -> boolean”Raw: node.raw.removeVehicleTag
Removes a tag; false when it was not there.
vehicle:lock(mode?, whitelist?) -> boolean
Section titled “vehicle:lock(mode?, whitelist?) -> boolean”Raw: node.raw.setVehicleLock
Locks the vehicle against NEW seat claims: mode “locked” (default), “driver-only” or “unlocked”; whitelist is an array of Players or ids always allowed (up to 32). Nobody seated is evicted; server-driven seating bypasses locks.
vehicle:unlock() -> boolean
Section titled “vehicle:unlock() -> boolean”Raw: node.raw.setVehicleLock
lock("unlocked").
vehicle:lockState() -> record{mode,whitelist}?
Section titled “vehicle:lockState() -> record{mode,whitelist}?”Raw: node.raw.getVehicleLock
The current lock.
vehicle:setCoupler(name, open?) -> boolean
Section titled “vehicle:setCoupler(name, open?) -> boolean”Raw: node.raw.setVehicleCoupler
Opens (default) or closes the named coupler group on every client and caches it.
vehicle:trigger(call) -> boolean
Section titled “vehicle:trigger(call) -> boolean”Raw: node.raw.triggerVehicle
Has the sync authority execute one controller call – a table or JSON {controllerName, functionName, ...}. Never asks vehicleTriggerRequest: the server does not veto itself.
vehicle:resync(to?) -> boolean
Section titled “vehicle:resync(to?) -> boolean”Raw: node.raw.resyncVehicle
Re-sends the spawn and cached state to one Player (or id), or to everyone.
vehicle:seat(player, role?) -> boolean
Section titled “vehicle:seat(player, role?) -> boolean”Raw: node.raw.seatPlayer
Seats a Player (or id) in this vehicle; player:seat(vehicle) is the same call.
vehicle:setGroup(group)
Section titled “vehicle:setGroup(group)”Raw: node.raw.setVehicleGroup
Puts the vehicle in a visibility group. A packet about a vehicle belongs to the vehicle’s group, not its driver’s.
node.server – identity, limits, time, metrics
Section titled “node.server – identity, limits, time, metrics”The server’s own facts and knobs.
node.server.name() -> string
Section titled “node.server.name() -> string”Raw: node.raw.getServerName
The server’s name.
node.server.setName(name) -> boolean
Section titled “node.server.setName(name) -> boolean”Raw: node.raw.setServerName
Changes the reported name.
node.server.map() -> string
Section titled “node.server.map() -> string”Raw: node.raw.getMap
The map, as the client-facing level path.
node.server.version() -> string
Section titled “node.server.version() -> string”Raw: node.raw.getServerVersion
The server’s version string.
node.server.port() -> number
Section titled “node.server.port() -> number”Raw: node.raw.getPort
The port (TCP and UDP).
node.server.maxPlayers() -> number
Section titled “node.server.maxPlayers() -> number”Raw: node.raw.getMaxPlayers
The player limit in force.
node.server.setMaxPlayers(n) -> boolean
Section titled “node.server.setMaxPlayers(n) -> boolean”Raw: node.raw.setMaxPlayers
Changes the player limit; enforced on the next connect, nobody is kicked.
node.server.maxCars() -> number
Section titled “node.server.maxCars() -> number”Raw: node.raw.getMaxCars
The per-player vehicle limit in force.
node.server.setMaxCars(n) -> boolean
Section titled “node.server.setMaxCars(n) -> boolean”Raw: node.raw.setMaxCars
Changes the per-player vehicle limit; enforced on the next spawn.
node.server.uptime() -> number
Section titled “node.server.uptime() -> number”Raw: node.raw.getServerUptime
Seconds since start (monotonic, fractional).
node.server.time() -> number
Section titled “node.server.time() -> number”Raw: node.raw.getTime
Unix time with fractions.
node.server.unixTime() -> number
Section titled “node.server.unixTime() -> number”Raw: node.raw.getUnixTime
Unix time in whole seconds.
node.server.metrics() -> table
Section titled “node.server.metrics() -> table”Raw: node.raw.getMetrics
Live metrics: players, vehicles, uptime, net counters, connection limiter, plugin queue.
node.server.snapshot() -> record{time,players,vehicles}
Section titled “node.server.snapshot() -> record{time,players,vehicles}”Raw: node.raw.getWorldSnapshot
One bulk read of the world: players with position and seat, vehicles with pos/rot/vel.
node.server.nodeGrabEnabled() -> boolean
Section titled “node.server.nodeGrabEnabled() -> boolean”Raw: node.raw.isNodeGrabEnabled
Whether the experimental node grabber is enabled in server.toml.
node.bans
Section titled “node.bans”IP bans, persisted in bans.json. Player:ban() is the one that also kicks.
node.bans.add(who, reason?) -> boolean
Section titled “node.bans.add(who, reason?) -> boolean”Raw: node.raw.banIp
Bans for future connects. who is an IP string (banIp), a directory account id number (banAccount, wire v17: the ban follows the account to any address) or a Player (banPlayer: account when verified, IP always, and the player is kicked). Persisted in bans.json. An existing session stays for the first two forms.
node.bans.remove(who) -> boolean
Section titled “node.bans.remove(who) -> boolean”Raw: node.raw.unbanIp
Lifts a ban: an IP string, or an account id number. False when it was not banned.
node.bans.has(who) -> boolean
Section titled “node.bans.has(who) -> boolean”Raw: node.raw.isBanned
Whether an IP string, an account id number, or a Player (its account when verified, else its IP) is banned.
node.bans.all() -> array<record{ip,account,reason,at,name}>
Section titled “node.bans.all() -> array<record{ip,account,reason,at,name}>”Raw: node.raw.getBans
Every ban: IP bans carry ip, account bans carry account (the directory’s id).
node.storage – persistent key/value
Section titled “node.storage – persistent key/value”A per-resource JSON store (storage/<resource>.json), flushed atomically on every write.
node.storage.get(key, default?) -> any
Section titled “node.storage.get(key, default?) -> any”Raw: node.raw.storageGet
The stored value, or default when the key is absent.
node.storage.set(key, value) -> boolean
Section titled “node.storage.set(key, value) -> boolean”Raw: node.raw.storageSet
Stores any JSON-serialisable value; flushed atomically.
node.storage.delete(key) -> boolean
Section titled “node.storage.delete(key) -> boolean”Raw: node.raw.storageDelete
Removes a key; false when absent.
node.fs – files inside the resource folder
Section titled “node.fs – files inside the resource folder”Files inside the resource’s own folder; paths that escape it are rejected.
node.fs.read(path) -> string?
Section titled “node.fs.read(path) -> string?”Raw: node.raw.readFile
Reads a file inside the resource folder; nil when missing or outside it.
node.fs.write(path, data) -> boolean
Section titled “node.fs.write(path, data) -> boolean”Raw: node.raw.writeFile
Writes a file inside the resource folder, creating folders. Synchronous.
node.fs.writeAsync(path, data, cb?) -> boolean
Section titled “node.fs.writeAsync(path, data, cb?) -> boolean”Raw: node.raw.writeFileAsync
Writes off the worker; cb(ok) runs on the worker when done. true means accepted.
node.fs.list(path?) -> array<record{name,dir,size}>?
Section titled “node.fs.list(path?) -> array<record{name,dir,size}>?”Raw: node.raw.listFiles
Lists a folder inside the resource folder (the root when omitted).
node.http
Section titled “node.http”Asynchronous HTTP. Callbacks run on the worker; fetch is the coroutine form for use inside node.async.
node.http.get(url, headers?, cb) -> boolean
Section titled “node.http.get(url, headers?, cb) -> boolean”Raw: node.raw.httpGet
GET; cb(status, body, headers) on the worker: status is the HTTP status code, or -1 with the error text in body when the request failed (could not resolve or connect, TLS handshake or certificate verification, timeout, malformed or oversized response). Returns false when the request could not be queued – cb then never runs. About 15 s timeout, 8 MB body. TLS peer verification is off unless the hoster set [Http] CaFile (then every https request is verified against that bundle and a failure is a -1).
node.http.post(url, body, headers?, cb) -> boolean
Section titled “node.http.post(url, body, headers?, cb) -> boolean”Raw: node.raw.httpPost
POST; body is a table (JSON-encoded) or a string. cb and the return value are as for get: status -1 with the error text in body when the request failed, false when it could not be queued.
node.http.request(method, url, opts?, cb) -> boolean
Section titled “node.http.request(method, url, opts?, cb) -> boolean”Raw: node.raw.httpRequest
Any method (server 1.2.0): node.http.request("PUT", url, { headers = h, body = t }, cb). method is a token of letters A-Z only, at most 16 – "GET", "POST", "PUT", "PATCH", "DELETE", "HEAD" or a custom one the service understands; the prelude upper-cases it for you, anything else comes back as status -1 with body “invalid HTTP method”; opts.headers is a table of header -> value, opts.body a table (JSON-encoded) or a string, both optional (opts itself may be nil). cb(status, body, headers) and the return value are exactly as for get: the HTTP status code, or -1 with the error text in body when the request failed; false when the request could not be queued (cb then never runs). A HEAD answer carries status and headers and an empty body. put, patch, delete and head are this with the method fixed.
node.http.put(url, body?, headers?, cb) -> boolean
Section titled “node.http.put(url, body?, headers?, cb) -> boolean”Raw: node.raw.httpRequest
PUT; body is a table (JSON-encoded), a string, or nil for none. cb and the return value are as for get.
node.http.patch(url, body?, headers?, cb) -> boolean
Section titled “node.http.patch(url, body?, headers?, cb) -> boolean”Raw: node.raw.httpRequest
PATCH; body is a table (JSON-encoded), a string, or nil for none. cb and the return value are as for get.
node.http.delete(url, body?, headers?, cb) -> boolean
Section titled “node.http.delete(url, body?, headers?, cb) -> boolean”Raw: node.raw.httpRequest
DELETE; the body is optional (most services want none: node.http.delete(url, nil, headers, cb) or node.http.delete(url, cb)). cb and the return value are as for get.
node.http.head(url, headers?, cb) -> boolean
Section titled “node.http.head(url, headers?, cb) -> boolean”Raw: node.raw.httpRequest
HEAD; cb(status, “”, headers) – the status and the response headers, never a body. The return value is as for get.
node.http.fetch(url, opts?) -> number, string, table
Section titled “node.http.fetch(url, opts?) -> number, string, table”The coroutine form, inside node.async only: local status, body, headers = node.http.fetch(url, { method = "DELETE", body = t, headers = h }). opts.method is any method request accepts (default GET; before 1.2.0 anything but POST was sent as GET). status is the HTTP status code, or -1 with the error text in body when the request failed; it is 0, with body “request not queued”, only when the request could not be queued at all (the background pool is full or shutting down).
node.pg – PostgreSQL
Section titled “node.pg – PostgreSQL”Asynchronous, pooled PostgreSQL over the connection string in [Database] Url (enabled() is false without one). Every call has two forms: with a callback, which runs on the worker when the statement completes, or without one inside node.async, where it suspends the coroutine and returns the result – local result, err = node.pg.query(sql, params). Statements are parameterised ($1..$n, node.pg.NULL for NULL), rows come back keyed by column name, errors are tables with the SQLSTATE or an internal code in code. tx runs a function on one reserved connection between BEGIN and COMMIT/ROLLBACK.
node.pg.enabled() -> boolean
Section titled “node.pg.enabled() -> boolean”Raw: node.raw.pgEnabled
true when the server was configured with a database ([Database] Url in server.toml, or NODE_DATABASE_URL). false means every node.pg call answers err.code == "pg_disabled"; check it once at load and degrade gracefully instead of assuming a database.
node.pg.ready() -> boolean
Section titled “node.pg.ready() -> boolean”Raw: node.raw.pgReady
true while at least one pool connection is established. The pool connects in the background and reconnects on its own (0.5 s to 30 s backoff); the server starts without the database, and until this turns true statements fail at once with err.code == "08001" rather than waiting.
node.pg.NULL : userdata
Section titled “node.pg.NULL : userdata”The NULL parameter. A Lua nil inside the params array ends the array – { 1, nil, "x" } sends one parameter – so write { 1, node.pg.NULL, "x" } for every NULL. One opaque value per server, recognised by identity; it belongs only inside a params array. Coming back the other way, a NULL cell is simply absent from its row (row.col == nil).
node.pg.query(sql, params?, cb?) -> record{rows,count,columns}?, record{code,message,detail,hint,constraint,table}?
Section titled “node.pg.query(sql, params?, cb?) -> record{rows,count,columns}?, record{code,message,detail,hint,constraint,table}?”Raw: node.raw.pgQuery
Runs one parameterised statement on the pool. sql uses $1..$n; params is the array of values, in order: node.pg.NULL -> NULL (a nil would end the array), boolean, integer, float, string as text (a NUL inside is not supported – send bytea as hex through decode($1, 'hex')), a table as JSON (for json/jsonb columns); the type is inferred by the server. A statement without parameters may leave params out: node.pg.query(sql, cb). Never concatenate values into sql. Two forms. With cb, the callback form: returns nothing and cb(result, err) runs on the worker when the statement completes. Without cb, inside node.async, the suspending form: the coroutine suspends and local result, err = node.pg.query(sql, params) gets the same two values; outside a coroutine it raises “node.pg.query: callback required outside node.async” (like node.sleep). result = { rows = { { col = value, ... }, ... }, count = <rows returned, or affected for a statement without rows>, columns = { "col", ... } }. Rows are keyed by column name (alias duplicate names; columns keeps the order); values follow the column type – bool -> boolean, int2/int4/int8 -> integer (64-bit), float4/float8 -> number, numeric -> string (exact; keep money in integer minor units), text/varchar/uuid/timestamp/date/time/interval -> string (Postgres’ text form), json/jsonb -> string (node.json.decode it), bytea -> string of raw bytes, arrays and anything else -> Postgres’ text form; a NULL cell is absent (nil). err = { code, message, detail?, hint?, constraint?, table? } with the SQLSTATE in code ("23505" unique violation, with constraint; "42601" syntax; "57014" statement timeout, [Database] QueryTimeoutMs; "08001" no connection; "08006" connection lost) or an internal code: pg_disabled (no [Database] Url), pg_queue_full (1000 statements pending), pg_result_cap (more than [Database] MaxRows rows; the result is dropped), pg_params (a value inside params the driver cannot send – a function, a coroutine, a userdata other than node.pg.NULL, a string with a NUL, a table JSON cannot encode – or more than 1000 of them), pg_reload (never delivered: a resource unloaded while a statement is in flight never sees its callback; the code exists so the list is complete). A params that is not a table at all – or a sql that is not a string, or a cb that is not a function – is a programming error and raises at the caller’s line instead of reaching cb. Every statement here is its own transaction; see tx for several on one connection – a node.pg.query inside a tx function is NOT part of that transaction.
node.pg.exec(sql, params?, cb?) -> number?, record{code,message,detail,hint,constraint,table}?
Section titled “node.pg.exec(sql, params?, cb?) -> number?, record{code,message,detail,hint,constraint,table}?”Raw: node.raw.pgQuery
query for statements whose rows you do not want: no row is materialised and the answer is the count – rows affected by an INSERT/UPDATE/DELETE, rows a SELECT would have returned. cb(count, err) in the callback form; local count, err = node.pg.exec(sql, params) inside node.async. Same params, errors and rules as query.
node.pg.tx(fn, cb?) -> any..., record{code,message,detail,hint,constraint,table}?
Section titled “node.pg.tx(fn, cb?) -> any..., record{code,message,detail,hint,constraint,table}?”A transaction. fn(tx) runs as a coroutine on ONE pool connection reserved for it – its own node.async task in the callback form, the caller’s coroutine in the suspending form: BEGIN, then everything fn does through tx, then COMMIT when fn returns normally, ROLLBACK when fn raises or calls tx:rollback(). Inside fn use the suspending forms only: local result, err = tx:query(sql, params?) and local count, err = tx:exec(sql, params?) – the same contract as node.pg.query/exec without a callback, on the reserved connection; tx:rollback(reason?) raises out of fn and rolls back. A plain node.pg.query inside fn goes to ANOTHER connection and is NOT part of the transaction. After a failed statement Postgres refuses every further statement of the transaction (25P02) until it is rolled back – check err and tx:rollback() or raise. Outcome: fn returned normally and COMMIT succeeded -> the return values of fn; COMMIT failed -> nil, err (a SQLSTATE; Postgres rolled back); fn raised -> ROLLBACK and nil, { code = "rollback", message = <the error text, or the .message of a raised table>, cause = <the value fn raised> } (so error(err) with the err of a failed tx:query keeps its SQLSTATE in cause.code); tx:rollback(reason) -> ROLLBACK and nil, { code = "rollback", message = reason }; the transaction outlived [Database] TxTimeoutMs (default 30 s) -> the server rolled it back already and the next tx call or the COMMIT answers { code = "tx_timeout" }; BEGIN itself failed -> nil, err (08001, pg_disabled, pg_queue_full). With cb, cb(…) receives exactly those values on the worker (cb(<returns of fn>) or cb(nil, err)); without cb, inside node.async, local a, b = node.pg.tx(fn) suspends the caller and returns them; outside a coroutine the suspending form raises “node.pg.tx: callback required outside node.async”. Keep transactions short: the connection is unavailable to everyone else while fn runs, and a node.sleep inside fn holds it too. Unloading the resource rolls back its open transactions and drops their callbacks.
node.json
Section titled “node.json”JSON in and out. Most of the API encodes for you (a table given to send, broadcast, bus.emit, storage.set is encoded); these are for everything else.
node.json.encode(value) -> string
Section titled “node.json.encode(value) -> string”Raw: node.raw.jsonEncode
Encodes a Lua value as JSON.
node.json.decode(text) -> any?
Section titled “node.json.decode(text) -> any?”Raw: node.raw.jsonDecode
Decodes JSON text; nil on a parse error.
node.crypto
Section titled “node.crypto”Digests and randomness.
node.crypto.sha256(data) -> string
Section titled “node.crypto.sha256(data) -> string”Raw: node.raw.sha256
SHA-256 as 64 hex characters.
node.crypto.hmac(key, data) -> string
Section titled “node.crypto.hmac(key, data) -> string”Raw: node.raw.hmac
HMAC-SHA256 as hex.
node.crypto.randomBytes(n) -> string?
Section titled “node.crypto.randomBytes(n) -> string?”Raw: node.raw.randomBytes
n cryptographically random bytes as a binary string (1..65536).
node.crypto.randomHex(n?) -> string?
Section titled “node.crypto.randomHex(n?) -> string?”n random bytes (default 16) as hex – a token.
node.log
Section titled “node.log”node.log(msg, ...) prints under the resource’s tag with string.format arguments; the table also carries the levels and the raw forms.
node.log.warn(msg, ...)
Section titled “node.log.warn(msg, ...)”Raw: node.raw.logWarn
Logs at the warning level under this resource’s name; format arguments accepted.
node.log.error(msg, ...)
Section titled “node.log.error(msg, ...)”Raw: node.raw.logError
Logs at the error level under this resource’s name.
node.log.tag(tag, msg, ...)
Section titled “node.log.tag(tag, msg, ...)”Raw: node.raw.logTag
Logs under one of the server’s own tags (Core, Net, Res, Mods, Module, Join, Leave, Kick, Veh, Warn, Error, Debug).
node.log.custom(tag, rgb, msg, ...)
Section titled “node.log.custom(tag, rgb, msg, ...)”Raw: node.raw.logCustom
Logs under a tag of your own in a 0xRRGGBB colour.
node.log.raw(text)
Section titled “node.log.raw(text)”Raw: node.raw.logRaw
Writes text to the console as-is, no timestamp or tag.
node.log.sink(fn)
Section titled “node.log.sink(fn)”Raw: node.raw.setLogSink
Observes every server log line as fn(tag, level, msg) on the worker; nil clears. Cannot suppress lines.
node.log.title(title)
Section titled “node.log.title(title)”Raw: node.raw.setConsoleTitle
Sets the console window title.
node.bus – between resources
Section titled “node.bus – between resources”Publish/subscribe between resources on this server (and native modules). Asynchronous, delivered to the sender too.
node.bus.emit(name, data?)
Section titled “node.bus.emit(name, data?)”Raw: node.raw.emitResource
Publishes to every resource and native module subscribed to name (the sender included), asynchronously on the worker. data is a table (JSON-encoded) or a string.
node.bus.on(name, fn)
Section titled “node.bus.on(name, fn)”Raw: node.raw.onResource
Subscribes fn(sourceResourceName, data) to bus messages under name; a native module appears as “native”.
node.bus.off(name, fn?) -> number
Section titled “node.bus.off(name, fn?) -> number”Raw: node.raw.offResource
Unsubscribes this resource’s bus handlers for name – all of them, or only fn, matched by identity from any context, including coroutines.
node.modules – the binary channel
Section titled “node.modules – the binary channel”Raw bytes on a numbered channel, for native modules and their client halves.
node.modules.on(channel, fn)
Section titled “node.modules.on(channel, fn)”Raw: node.raw.onModule
Subscribes fn(player, data) to raw bytes clients send on a u32 channel.
node.modules.off(channel, fn?) -> number
Section titled “node.modules.off(channel, fn?) -> number”Raw: node.raw.offModule
Unsubscribes this resource’s handlers for the channel – all of them, or only fn, matched by identity from any context, including coroutines.
node.modules.send(target, channel, data) -> boolean
Section titled “node.modules.send(target, channel, data) -> boolean”Raw: node.raw.sendModule
Sends raw bytes to a Player (or id), or to everyone with target “all” or nil.
node.relay – who sees what
Section titled “node.relay – who sees what”The packet filter and its cache. Visibility groups (Player:setGroup, Vehicle:setGroup) are the cheap alternative for room-style rules.
node.relay.filter(fn)
Section titled “node.relay.filter(fn)”Raw: node.raw.on
Installs a relayRequest hook (the same thing as node.on("relayRequest", fn)): fn(fromPid, toPid, category, subtype, globalId) returns false to hide a packet from that recipient. Runs with ids on the hot path; verdicts are cached until invalidate. Prefer visibility groups for room-style rules.
node.relay.unfilter(fn?) -> number
Section titled “node.relay.unfilter(fn?) -> number”Raw: node.raw.off
Removes this resource’s hooks, or only fn.
node.relay.invalidate()
Section titled “node.relay.invalidate()”Raw: node.raw.invalidateRelayCache
Drops every cached verdict so the hooks are consulted again. Call it whenever the data a hook reads has changed.
node.resources and node.config
Section titled “node.resources and node.config”The resource’s own manifest and settings, and reloading resources by name.
node.resources.reload(name) -> boolean
Section titled “node.resources.reload(name) -> boolean”Raw: node.raw.reloadResource
Queues a reload of a resource by name – registrations, timers and coroutines dropped, the folder loaded again. true means accepted. A resource may reload itself.
node.resources.manifest() -> record{name,version,type,server,client,config}?
Section titled “node.resources.manifest() -> record{name,version,type,server,client,config}?”Raw: node.raw.getManifest
This resource’s resource.toml as a table, re-read from disk.
node.chat
Section titled “node.chat”Speak into the chat as the server. Needs the chat resource (it owns the screen side); silent without it.
node.chat.say(text, ...)
Section titled “node.chat.say(text, ...)”Says a system line in everyone’s chat (via the chat resource). Format arguments accepted.
node.chat.tell(target, text, ...)
Section titled “node.chat.tell(target, text, ...)”Says a system line in one player’s chat; target is a Player or an id.
node.commands
Section titled “node.commands”Chat commands: a line starting with / reaches the handler registered for its first word, with the sender as a Player. Needs the chat resource.
node.commands.add(name, fn, opts?)
Section titled “node.commands.add(name, fn, opts?)”Registers a chat command: a line /name ... typed by any player calls fn(player, args, raw) with the sender as a Player, the words after the name as an array and the whole line. opts.role restricts it to players whose role equals it; others are told they may not. Names are case-insensitive. Needs the chat resource, which publishes the lines.
node.commands.remove(name)
Section titled “node.commands.remove(name)”Unregisters a command.
