Admins place world content in game with en-admin's Build tab: traders, vehicle dealers, garages, quest NPCs, crafting benches, spawn points, zombie zones and more. en-core stores each entry and tells every server script and client when one changes. encore.content is how your resource reads those entries and keeps up with changes, without a restart.
A resource that owns a kind of content typically keeps its config entries as defaults and layers placed entries on top.
How content is stored
- Entries are grouped by kind (for example
'trader'), then keyed by id. - Each entry's data is a plain table. en-core strips it to JSON-safe values, so vectors arrive as
{ x, y, z }or{ x, y, z, w }. - A save is live immediately and written to the
encore_contenttable in the background. - en-core only stores and broadcasts. Your resource validates what it reads.
For the underlying exports and database table, see Content registry.
API
| Side | Function | Returns | Notes |
|---|---|---|---|
| both | encore.content.isReady() | boolean | Whether en-core has finished loading content |
| both | encore.content.getAll(kind) | table<string, table> | Every entry of a kind, keyed by id. Yields until content has loaded |
| both | encore.content.onChange(kind, handler) | none | handler(id, data) on save, handler(id, nil) on delete |
| both | encore.content.watch(kind, onAll, onChange) | none | Loads everything once, then follows changes |
| server | encore.content.save(kind, id?, data, author?) | ok, idOrError | Creates or replaces an entry |
| server | encore.content.remove(kind, id) | boolean | Deletes an entry |
| both | encore.content.vec3(value) | vector3? | Turns stored coordinates back into a vector |
| both | encore.content.vec4(value) | vector4? | Same, with a heading |
isReady and getAll
isReady() reads GlobalState.encoreContentReady.
getAll(kind) waits until content has loaded, then returns every entry of kind, or an empty table if there are none.
- On the server it reads
exports['en-core']:GetContent(kind). - On the client it asks the server through the callback
encore:content:get. If that request times out, you get an empty table. - It yields, so call it from a thread. It keeps waiting for as long as content isn't loaded.
onChange
onChange(kind, handler) calls handler(id, data) whenever an entry of kind is saved, and handler(id, nil) when one is deleted. It listens on the server event encore:content:changed or the client event encore:client:contentChanged, and filters by kind for you.
watch
watch(kind, onAll, onChange) is the call most resources want:
- In a new thread, it waits for content, then calls
onAll(entries)once with everything. - Afterwards it calls
onChange(id, data)for every change. - Changes that arrive while the first load is still running are held, then applied in order once
onAllreturns. Nothing is missed and nothing is applied out of order.
save and remove (server)
encore.content.save(kind, id, data, author):
| Argument | Meaning |
|---|---|
kind | The content kind |
id | The id to create or replace. Pass nil or '' to create a new entry with a generated id like <kind>_<hex> |
data | A table |
author | Optional. Recorded as who last changed the entry, for example the admin's name |
It returns true, id on success, or false, message on failure. Possible messages:
| Message | Cause |
|---|---|
Invalid content kind. | The kind isn't a valid key |
Content hasn't finished loading. | Called before content loaded |
Content must be a table. | data isn't a table |
Ids may only use letters, numbers, _ - . and : | The id contains other characters |
Keys may only use letters, numbers, _, -, . and :, up to 80 characters.
encore.content.remove(kind, id) returns true if the entry was deleted.
vec3 and vec4
Stored coordinates are tables, so convert them before use.
| Function | Accepts | Returns |
|---|---|---|
vec3(value) | a vector3 or vector4, { x, y, z }, or { 1, 2, 3 } | vector3, or nil if any part isn't a number |
vec4(value) | the same, with the heading read from w, h, heading or the fourth array value | vector4 (heading defaults to 0.0), or nil |
Security
Content is typed by a person in an editor. Treat every field as untrusted: check types, clamp numbers, look up item and model names against your config, and skip entries that don't make sense instead of erroring.
- Only save content from the server, and only after checking the caller's permission.
- Show labels and names in NUI as text, never as HTML.
- Log a bad entry once with
encore.warnOnceso a broken entry doesn't flood the console.
Examples
Spawning placed traders
-- client/traders.lua
local traders = {}
local function removeTrader(id)
local ped = traders[id]
if ped and DoesEntityExist(ped) then DeletePed(ped) end
traders[id] = nil
end
local function addTrader(id, data)
removeTrader(id)
local pos = encore.content.vec4(data.coords)
if not pos then
return encore.warnOnce('bad-trader:' .. id, 'trader %s has no coords', id)
end
local model = type(data.model) == 'string' and data.model or 's_m_y_ammucity_01'
local hash = encore.requestModel(model)
if not hash then return end
local ped = CreatePed(4, hash, pos.x, pos.y, pos.z - 1.0, pos.w, false, true)
SetModelAsNoLongerNeeded(hash)
FreezeEntityPosition(ped, true)
SetEntityInvincible(ped, true)
traders[id] = ped
end
encore.content.watch('trader', function(entries)
for id, data in pairs(entries) do addTrader(id, data) end
end, function(id, data)
if data then addTrader(id, data) else removeTrader(id) end
end)Saving from an admin command
-- server/commands.lua
encore.addCommand('placebench', {
help = 'Place a crafting bench where you stand',
restricted = 'group.admin',
params = { { name = 'label', type = 'longString' } },
}, function(source, args)
local ped = GetPlayerPed(source)
local coords = GetEntityCoords(ped)
local ok, result = encore.content.save('my_bench', nil, {
label = args.label:sub(1, 40),
coords = vec4(coords.x, coords.y, coords.z, GetEntityHeading(ped)),
}, GetPlayerName(source))
if ok then
encore.notify(source, { description = 'Bench placed: ' .. result, type = 'success' })
else
encore.notify(source, { description = result, type = 'error' })
end
end)