Skip to content

Scripting (Lua) ​

A WildM resource can carry scripts, the same way FiveM resources do: server code, client code, and events between the two. Scripts are written in Lua 5.4 on both sides, with an API close to FiveM's (RegisterNetEvent, TriggerServerEvent, Citizen.CreateThread…), so FiveM developers will feel at home.

SideWhere it runsEngine
Serverinside node server.jsLua 5.4 (wasmoon), game-server/runtime/
Clientinside the game, plugin wildm_scriptLua 5.4, sandboxed

Both sides share the same core (wildm_core.lua): events, threads, commands, exports and JSON behave identically.

A resource with scripts ​

text
game-server/resources/
└── example-scripts/
    ├── resource.json
    ├── shared/config.lua      ← runs on both sides, first
    ├── server/main.lua        ← server only, never sent to players
    └── client/main.lua        ← client only
json
{
  "name": "example-scripts",
  "shared_scripts": ["shared/config.lua"],
  "server_scripts": ["server/main.lua"],
  "client_scripts": ["client/main.lua"],
  "server_files": ["data/**"],
  "dependencies": ["wildm-base"]
}
FieldMeaning
nameResource name (defaults to the folder name). Letters, digits, _ . -.
shared_scriptsLoaded on the server and on every client, before the side scripts.
server_scriptsServer only. Never listed in the download manifest.
client_scriptsClient only. Downloaded by the launcher, verified (sha256) by the client before running.
server_files (alias server_only)Other server-side files (data, config): never downloaded by players.
server_hostOptional Node module for server logic that needs Node (database…). See Node host modules.
dependenciesStarted first. A missing dependency means this resource does not start.
filesInformative: every other file of the resource is downloaded anyway.

Patterns: * matches one file name, ** any sub-path (data/**). Scripts must be .lua. A pattern can include a file of another resource with @resource/path (e.g. "@wf_core/client/ui.lua").

A resource is any folder of resources/ that contains a resource.json, at any depth (resources/[framework]/wf_core/ works). Which resources start is set by server.json → "resources": ["wildm-base", "example-scripts"] (all of them when the key is absent), in that order, dependencies first.

Restart after changing scripts

Like the manifest, scripts are loaded once, at start-up. There is no hot reload: restart the server, and players reconnect.

How it flows ​

  1. The launcher downloads the resource files (client and shared scripts included, server scripts excluded).
  2. The player joins; right after welcome the server sends the list of client scripts with their sha256.
  3. The wildm_script plugin checks each file in the cache against that hash and starts the resources in order.
  4. Events travel as JSON lines on the game connection, in both directions.

When the player disconnects, every client script stops; they start again on the next join.

Events ​

lua
-- server/main.lua
RegisterNetEvent("example:ping", function(clientTime)
  local src = source                       -- copy it now: `source` changes with every event
  TriggerClientEvent("example:pong", src, clientTime, GetGameTimer())
end)

-- client/main.lua
RegisterCommand("ping", function()
  TriggerServerEvent("example:ping", GetGameTimer())
end)
RegisterNetEvent("example:pong", function(sentAt)
  ShowNotification(("pong: %d ms"):format(GetGameTimer() - sentAt))
end)
FunctionSide
AddEventHandler(name, fn)bothHandle a local event. Returns a handle for RemoveEventHandler.
RegisterNetEvent(name[, fn])bothAllow the other side to trigger name (and optionally add a handler). An event that was not registered is ignored when it comes from the network.
TriggerEvent(name, ...)bothLocal event. source is nil in its handlers.
TriggerServerEvent(name, ...)clientEvent to the server; there source is the player id.
TriggerClientEvent(name, target, ...)servertarget = player id, or -1 for everyone.
RemoveEventHandler(handle)both

Arguments are serialised as JSON: numbers (64-bit integers kept exact), strings, booleans, nil and tables. Functions and cyclic tables cannot be sent. An empty table arrives as {}; table keys that look like integers ("3") come back as integers. One client event is limited to about 15 KB, a server event to 32 KB; the server also drops a player's events beyond 50 per second (burst of 100).

Built-in events:

EventSideArguments
onResourceStart / onResourceStopbothresource name
onClientResourceStart / onClientResourceStopclientresource name
playerJoiningserver— (source = new player)
playerDroppedserverreason (source = leaving player)

Commands and keys ​

lua
RegisterCommand("car", function(source, args, raw)   -- /car suv
  ...
end)
RegisterCommand("players", function(source) ... end, true)   -- restricted
RegisterKeyMapping("ping", "Measure latency", "keyboard", "F7")   -- client
  • Client: a command typed by the player runs the client command of that name if there is one; otherwise it is sent to the server, which runs its own command with source = the player (or answers Unknown command).
  • Server: source is the player id, or 0 from the server console (type the command in the node server.js window). A restricted command (true as third argument) only runs from the server console.
  • Until there is a chat box or an F8 console, players type commands in %USERPROFILE%\WildM\client\wildm_cmd.txt, one per line, starting with / (for example /car suv). The file is read and deleted every 250 ms.
  • RegisterKeyMapping(command, description, "keyboard", key) runs command on a key press (only while the game window has focus). Keys: A–Z, 0–9, F1–F12, SPACE, RETURN, TAB, arrows, INSERT, DELETE, HOME, END, PAGEUP, PAGEDOWN, NUMPAD0–NUMPAD9, LSHIFT, LCONTROL, LMENU… ExecuteCommand(raw) runs a command from a client script.

Threads and timers ​

lua
Citizen.CreateThread(function()
  while true do
    Wait(1000)
    print("one more second")
  end
end)
SetTimeout(5000, function() print("5 s later") end)

Wait(ms) only works inside a thread, an event handler or a command (they all run in their own thread), never inside your own coroutine. A script that runs for more than 2 seconds without calling Wait is interrupted with an error.

Exports ​

lua
-- in resource "banking"
exports("getBalance", function(id) return 100 end)
-- anywhere else, on the same side
local balance = exports.banking:getBalance(3)       -- or exports["banking"].getBalance(3)

Client natives ​

Positions use vector3 (.x .y .z, + - *, #v = length, tostring). Heading is in radians, 0 = north (+Y), growing towards east (+X).

Native
IsInGame()true once the world is loaded.
GetPlayerCoords()vector3 of the local player, or nil.
GetPlayerHeading()Heading in radians, or nil.
GetPlayerForwardVector()Unit vector3 the player faces.
SetPlayerCoords(x, y, z) / SetPlayerCoords(vec)Teleport the player.
IsInVehicle()
GetPlayerServerId()Player id given by the server.
SpawnVehicle(model[, pos[, heading]])Returns entity, name or nil, reason. model = exact name, part of a name (case-insensitive) or id "0x408A2E76". Default position: 6 m in front of the player.
GetVehicleModels([filter])List of { id = "0x…", name = "…" }.
SpawnNpcs({ group, archetype, count, distance, formation })Spawns NPCs in front of the player and waits (non-blocking) until they exist; returns the list of entities. group: 0 Santa Blanca, 1 Unidad, 2 Rebels, 3 Civilians, 4 Special (random archetype of the group); count 1–50; formation 0 line … 4 random. Call it from a thread, handler or command.
GetEntityCoords(entity)vector3 or nil.

Servers with "world": { "npc": 0 }

While the server forces "no NPC", the engine's global spawning state is held off: a vehicle from SpawnVehicle appears but cannot be entered yet, and NPCs from SpawnNpcs are not shown. A fix (filtering only humans) is in progress in wildm_net.

| DeleteEntity(entity) | Not effective on the current game build (returns false). | | GetGroundZ(x, y) | Ground height, or nil. | | ShowNotification(text[, ms[, rgb]]) | Short message at the top of the screen (default 4 s, white). | | SetHudText(id, text[, corner]) | Persistent HUD line per id (16 max); empty text hides it. Corner: 0 top-left, 1 top-right, 2 bottom-left, 3 bottom-right, 4 top-centre. | | IsKeyDown(key) | Physical key state, only while the game window has focus. | | LoadResourceFile(resource, path) | Read a downloaded file of a resource (text), or nil. |

Server natives ​

Native
GetPlayers()List of player ids.
GetNumPlayerIndices()Number of players.
GetPlayerName(id) / DoesPlayerExist(id)
GetPlayerCoords(id) / GetPlayerHeading(id)Last position sent by the player (nil before the first one).
DropPlayer(id, reason)Disconnects the player with that message.
LoadResourceFile(resource, path)Read a file of a resource (10 MB max).
SaveResourceFile(resource, path, data)Write a file inside a resource folder (10 MB max). Declare it in server_files so players never download it.

Common to both sides: print, json.encode / json.decode, vector3, GetGameTimer() (ms), GetCurrentResourceName(), IsDuplicityVersion() (true on the server), Citizen.Trace.

Sandbox ​

Client scripts come from the server you join, so the client treats them as untrusted code:

  • available: the Lua base functions, string, table, math, utf8, coroutine, os.time / os.clock / os.date / os.difftime;
  • not available: io, os.execute and the rest of os, require / package, debug, dofile, loadfile, string.dump, collectgarbage other than "count"; load only accepts text (never bytecode) and runs in the resource's own environment;
  • each resource has its own globals; resources talk through events and exports;
  • memory is capped at 256 MB for all scripts, and a script that keeps the CPU for 2 s is interrupted;
  • scripts are only read from the server's cache folder, by relative path, and must match the server's sha256;
  • natives only reach the game through the ScriptHook API (no raw memory or engine calls).

The server applies the same rules (same core). Server scripts are your own code; they can still loop forever only for 2 s before being interrupted.

The client logs everything in %USERPROFILE%\WildM\client\logs\wildm_script.log (script errors with resource, file and line; print output).

Node host modules ​

For server logic that needs Node (a database with node:sqlite, HTTP calls…), a resource can declare "server_host": "server/index.js". After its Lua scripts, the runtime calls require(module).start(ctx) (and stop() on shutdown):

ctx.
resourceName, resourceDir, log(level, msg)
onNet(name, fn(source, ...args))RegisterNetEvent + handler.
on(name, fn)AddEventHandler; fn(source, ...args) when the event has a source (playerJoining, playerDropped), otherwise fn(...args).
emit(name, ...args) / emitClient(name, target, ...args)TriggerEvent / TriggerClientEvent.
exports(name, fn)exports.<resource>:<name>(...) for Lua; a returned array gives several Lua values.
registerCommand(name, fn(source, args, raw))
getPlayers(), getIdentity(id), getCoords(id), dropPlayer(id, reason)getIdentity → { name, identifiers } (no verified identifier yet).

Values cross the boundary as JSON; a Lua function passed to JS becomes a JS function returning an array of the Lua results. The module file and its server_files are never downloaded by players.

Example resource ​

game-server/resources/example-scripts/ shows the three patterns:

  • /car [model] — client command: spawns a vehicle 6 m ahead, then tells the server, which announces it to everyone (-1).
  • /npc [count] — server command: the client does not know it, so it is sent to the server, which checks the count and asks that client to spawn NPCs.
  • /ping (or F7) — client → server → client round trip, showing the latency.

Try it: start the server, join with the launcher, then write /car, /npc 2 or /ping in %USERPROFILE%\WildM\client\wildm_cmd.txt.

WildM is an independent community project. It is not affiliated with or endorsed by Ubisoft. Ghost Recon Wildlands is a trademark of Ubisoft Entertainment.