Skip to content

Entities, state bags & permissions ​

Everything a server developer needs to build admin menus, jobs, garages or spectate in Lua only, without touching the client. The API follows FiveM (OneSync) names, so existing knowledge and examples carry over. Status: server side done and tested; client side waits for the next client build (see Status).

Network entities ​

A vehicle, ped or object created by a script exists for every player nearby (400 m by default, server.json → entityRange).

  • Owner. One player is authoritative on its position; everyone else interpolates it. A server-created entity is handed to the closest player. Whoever drives a vehicle becomes its owner. If the owner leaves, the closest player takes over (or the server, which freezes it in place).
  • Network id. Stable for the entity's whole life. On the server, the entity handle is the network id. On the client, convert with NetworkGetEntityFromNetworkId / NetworkGetNetworkIdFromEntity.
  • Deletion reaches every player. Entities created by a player's script persist after they leave (like OneSync's default).

Server ​

lua
RegisterCommand('car', function(source, args)
  local ped = GetPlayerPed(source)                 -- network id of the player's ped
  local pos = GetEntityCoords(ped)
  local veh = CreateVehicle(args[1] or 'kamaz', pos.x + 5.0, pos.y, pos.z, 90.0, true, false)
  Entity(veh).state.fuel = 80                      -- replicated with the entity
  TriggerClientEvent('garage:spawned', source, NetworkGetNetworkIdFromEntity(veh))
end, true)                                         -- restricted: needs the ACE "command.car"
FunctionNotes
CreateVehicle(model, x, y, z, heading)model is a GRW vehicle name (GetVehicleModels() on the client lists them). Heading in degrees. Accepts a vector3 for x. Returns the handle (= network id), 0 if refused.
CreatePed(pedType, model, x, y, z, heading) / CreateObject(model, x, y, z)Network peds/objects. Not yet displayed by the client (vehicles first).
DeleteEntity(e), DoesEntityExist(e)
GetEntityCoords(e), SetEntityCoords(e, x, y, z)SetEntityCoords forwards the move to the owner. Not for player peds.
GetEntityHeading(e), SetEntityHeading(e, deg), GetEntityModel(e), GetEntityType(e)Type: 1 ped, 2 vehicle, 3 object.
NetworkGetEntityOwner(e)Player id, 0 = server.
NetworkGetNetworkIdFromEntity(e), NetworkGetEntityFromNetworkId(id)Identity on the server.
GetAllVehicles(), GetAllPeds(), GetAllObjects()Handles. Player peds are included in GetAllPeds.
GetPlayerPed(src), IsPedAPlayer(ped), GetVehiclePedIsIn(ped)Player's network ped; the vehicle they drive (0 if none).

Server events: entityCreated(handle), entityRemoved(handle).

Client ​

FunctionNotes
CreateVehicle(model, x, y, z, heading[, isNetwork = true])Spawns locally, then the server registers it and tells everyone nearby. The local player owns it.
DeleteEntity(ent)A network entity can only be deleted by its owner.
NetworkGetNetworkIdFromEntity(ent), NetworkGetEntityFromNetworkId(id), NetworkDoesNetworkIdExist(id)
NetworkGetEntityOwner(ent), NetworkHasControlOfEntity(ent)
PlayerId(), GetPlayerFromServerId(id), GetPlayerPed(player), GetActivePlayers()On WildM a player index is the server id. GetPlayerPed(-1) is you.

Spectate an other player from a client script: local ped = GetPlayerPed(GetPlayerFromServerId(target)), then GetEntityCoords(ped) each frame for your camera. The server should check the admin's permission first (see below) and only then send the target id.

State bags ​

Key/value data attached to the world, a player or an entity, replicated by the server.

lua
-- server
GlobalState.weather = 'rain'
Player(source).state.job = { name = 'police', grade = 2 }
Player(source).state:set('adminDuty', true, true)       -- 3rd argument: replicated (default true)
Player(source).state:set('lastBank', 1200, false)       -- server only, never sent
Entity(veh).state.locked = true

AddStateBagChangeHandler('job', nil, function(bagName, key, value, _reserved, replicated)
  local src = GetPlayerFromStateBagName(bagName)       -- 0 if it is not a player bag
  print(('player %d is now %s'):format(src, value and value.name or 'unemployed'))
end)
lua
-- client
print(GlobalState.weather, LocalPlayer.state.job)
LocalPlayer.state:set('emote', 'wave', true)            -- allowed: your own bag
AddStateBagChangeHandler('locked', 'entity:', function(bag, key, value)
  local veh = GetEntityFromStateBagName(bag)
end)
  • Bag names: global, player:<id>, entity:<netId>. The bag filter of AddStateBagChangeHandler is an exact name, a prefix ending with : ('entity:'), or nil for all.
  • Players can write only their own player: bag and entities they own, and never keys starting with srv_. server.json → "stateBagClientWrite": false forbids client writes entirely. Never trust a client-written key for anything sensitive: keep money, permissions and job grades in srv_* keys or server-only state.
  • Limits: key ≤ 64 characters, value ≤ 16 KB once encoded as JSON.

Identity ​

lua
for _, id in ipairs(GetPlayerIdentifiers(source)) do print(id) end
-- wfx:alice            WFX.re account, verified by the master
-- license:3f9c…        stable per account, does not reveal the username
-- ip:192.168.1.20
local license = GetPlayerIdentifierByType(source, 'license')   -- nil for players without a linked account
FunctionNotes
GetPlayers(), GetPlayerName(src)The name is display only, never proof of anything.
GetPlayerIdentifiers(src), GetNumPlayerIdentifiers(src), GetPlayerIdentifier(src, index)index starts at 0 (FiveM).
GetPlayerIdentifierByType(src, 'wfx' | 'license' | 'ip')Full identifier (license:…) or nil.
GetPlayerEndpoint(src)IP address.

How it works: the launcher asks the WFX.re master for a ticket valid for your server only and the game server has the master verify it with your svKey. The player's account token never reaches your server. Without a server key, or for players without a linked account, only ip: is available. Set "requireAccount": true in server.json to refuse players without a verified account.

Permissions (ACE) ​

Same model as FiveM. Put the rules in server.json → "permissions" or in game-server/permissions.cfg (one per line):

text
# groups
add_ace group.admin command allow            # every command
add_ace group.mod command.kick allow
add_ace group.mod command.ban deny
add_principal group.admin group.mod          # admins inherit mod rights
# people (use the license or the wfx account, never the name)
add_principal identifier.wfx:alice group.admin
add_principal identifier.license:3f9c0a… group.mod
  • A rule on command covers command.kick, command.car… The most specific object wins; at the same level deny beats allow. No rule means denied.
  • RegisterCommand(name, handler, true) runs only for players with command.<name> (the server console always may).
  • Everything is decided on the server: nothing a client sends can change permissions.
lua
-- admin menu: the client only asks, the server decides
RegisterNetEvent('admin:kick', function(target, reason)
  if not IsPlayerAceAllowed(source, 'command.kick') then return end
  DropPlayer(target, reason or 'Kicked by an admin')
end)
FunctionNotes
IsPlayerAceAllowed(src, object)
IsPrincipalAceAllowed(principal, object)e.g. ('group.mod', 'command.ban').
ExecuteCommand('add_principal identifier.wfx:bob group.mod')Changes rules at runtime (server scripts only). Not saved: write them to permissions.cfg to keep them.

Kicks and bans ​

FunctionNotes
DropPlayer(src, reason)Kick; the reason is shown to the player.
BanPlayer(src, reason, durationSeconds, by)WildM extension. Bans the player's wfx:/license: identifiers (or ip: without account), 0 = permanent. Returns the ban id.
BanIdentifiers({ 'license:…' }, reason, durationSeconds, by)Ban someone offline.
UnbanPlayer(banId), GetBans()

Bans are stored in game-server/data/bans.json (never downloaded by players) and checked before a player enters the game.

Server console commands: add_ace, add_principal, remove_ace, remove_principal, aces, players, kick <id> [reason], ban <id> [minutes] [reason], unban <banId>, bans.

Testing without a second PC ​

bash
node tools/bots.js --count 2 --entities --cmd "car kamaz"

Bots connect like real clients, print the network entities they receive, upload positions of the ones they own, and the first bot can run a command. The automated end-to-end test is node --test game-server/test/onesync.test.js.

Status ​

PartState
Server: entities, owners, state bags, identity, ACE, bansDone, covered by game-server/test/core.test.js and onesync.test.js
Master tickets, launcherDone (master must be redeployed for verified identities)
Client: network vehicles, state bags, GetPlayerPed of remote playersWritten (game-server/core/lua/client.lua), needs the next wildm_script / wildm_sync build
Client display of network peds and objectsNot yet

Protocol details: Game server protocol and docs/CONTRACT.md.

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