Theme
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.
| Side | Where it runs | Engine |
|---|---|---|
| Server | inside node server.js | Lua 5.4 (wasmoon), game-server/runtime/ |
| Client | inside the game, plugin wildm_script | Lua 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 onlyjson
{
"name": "example-scripts",
"shared_scripts": ["shared/config.lua"],
"server_scripts": ["server/main.lua"],
"client_scripts": ["client/main.lua"],
"server_files": ["data/**"],
"dependencies": ["wildm-base"]
}| Field | Meaning |
|---|---|
name | Resource name (defaults to the folder name). Letters, digits, _ . -. |
shared_scripts | Loaded on the server and on every client, before the side scripts. |
server_scripts | Server only. Never listed in the download manifest. |
client_scripts | Client 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_host | Optional Node module for server logic that needs Node (database…). See Node host modules. |
dependencies | Started first. A missing dependency means this resource does not start. |
files | Informative: 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
- The launcher downloads the resource files (client and shared scripts included, server scripts excluded).
- The player joins; right after
welcomethe server sends the list of client scripts with their sha256. - The
wildm_scriptplugin checks each file in the cache against that hash and starts the resources in order. - 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)| Function | Side | |
|---|---|---|
AddEventHandler(name, fn) | both | Handle a local event. Returns a handle for RemoveEventHandler. |
RegisterNetEvent(name[, fn]) | both | Allow 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, ...) | both | Local event. source is nil in its handlers. |
TriggerServerEvent(name, ...) | client | Event to the server; there source is the player id. |
TriggerClientEvent(name, target, ...) | server | target = 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:
| Event | Side | Arguments |
|---|---|---|
onResourceStart / onResourceStop | both | resource name |
onClientResourceStart / onClientResourceStop | client | resource name |
playerJoining | server | — (source = new player) |
playerDropped | server | reason (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:
sourceis the player id, or0from the server console (type the command in thenode server.jswindow). A restricted command (trueas 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)runscommandon 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.executeand the rest ofos,require/package,debug,dofile,loadfile,string.dump,collectgarbageother than"count";loadonly 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.