EndCore Framework

Content API

Read, follow and save world content that admins place in game, such as traders, quest NPCs and crafting benches, with encore.content.

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_content table 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

SideFunctionReturnsNotes
bothencore.content.isReady()booleanWhether en-core has finished loading content
bothencore.content.getAll(kind)table<string, table>Every entry of a kind, keyed by id. Yields until content has loaded
bothencore.content.onChange(kind, handler)nonehandler(id, data) on save, handler(id, nil) on delete
bothencore.content.watch(kind, onAll, onChange)noneLoads everything once, then follows changes
serverencore.content.save(kind, id?, data, author?)ok, idOrErrorCreates or replaces an entry
serverencore.content.remove(kind, id)booleanDeletes an entry
bothencore.content.vec3(value)vector3?Turns stored coordinates back into a vector
bothencore.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:

  1. In a new thread, it waits for content, then calls onAll(entries) once with everything.
  2. Afterwards it calls onChange(id, data) for every change.
  3. Changes that arrive while the first load is still running are held, then applied in order once onAll returns. Nothing is missed and nothing is applied out of order.

save and remove (server)

encore.content.save(kind, id, data, author):

ArgumentMeaning
kindThe content kind
idThe id to create or replace. Pass nil or '' to create a new entry with a generated id like <kind>_<hex>
dataA table
authorOptional. 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:

MessageCause
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.

FunctionAcceptsReturns
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 valuevector4 (heading defaults to 0.0), or nil

Security

Warning

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.warnOnce so a broken entry doesn't flood the console.

Examples

Spawning placed traders

lua
-- 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

lua
-- 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)