Theme
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"| Function | Notes |
|---|---|
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
| Function | Notes |
|---|---|
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 ofAddStateBagChangeHandleris an exact name, a prefix ending with:('entity:'), ornilfor all. - Players can write only their own
player:bag and entities they own, and never keys starting withsrv_.server.json→"stateBagClientWrite": falseforbids client writes entirely. Never trust a client-written key for anything sensitive: keep money, permissions and job grades insrv_*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| Function | Notes |
|---|---|
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
commandcoverscommand.kick,command.car… The most specific object wins; at the same leveldenybeatsallow. No rule means denied. RegisterCommand(name, handler, true)runs only for players withcommand.<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)| Function | Notes |
|---|---|
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
| Function | Notes |
|---|---|
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
| Part | State |
|---|---|
| Server: entities, owners, state bags, identity, ACE, bans | Done, covered by game-server/test/core.test.js and onesync.test.js |
| Master tickets, launcher | Done (master must be redeployed for verified identities) |
Client: network vehicles, state bags, GetPlayerPed of remote players | Written (game-server/core/lua/client.lua), needs the next wildm_script / wildm_sync build |
| Client display of network peds and objects | Not yet |
Protocol details: Game server protocol and docs/CONTRACT.md.