EndCore Framework

Writing a resource

Build your own EndCore resource on the library, with a manifest, require, callbacks, notifications, a target option and the inventory interface.

This guide builds a complete, small resource from scratch: searchable dumpsters. Players look at a dumpster, search it with a progress bar, and the server rolls loot into their inventory. Along the way it uses the main parts of the EndCore library: the manifest import, require, server callbacks, notifications, progress bars, a target option, the inventory interface, skills and an admin command.

The rules of an EndCore resource

  • Import the library with @en-core/lib/init.lua. It gives you the global encore.
  • Call interfaces, not resources. Use encore.inventory, encore.target, encore.skills and friends rather than calling en-inventory or en-target directly. The library checks the resource is running and falls back safely when it isn't.
  • The server decides. Clients ask; the server validates and acts. Only source is trusted.
  • Prefix every name (callbacks, target options, keybinds, events) with your resource name.
  • No ox dependencies. Everything you need is in the library.

1. Folder layout

text
resources/
  [local]/
    my-scavenge/
      fxmanifest.lua
      config/
        shared.lua
        server.lua
      client/
        main.lua
      server/
        main.lua

2. The manifest

lua
-- fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
lua54 'yes'

name 'my-scavenge'
description 'Searchable dumpsters'

shared_scripts {
    '@en-core/lib/init.lua',   -- must come before anything that uses `encore`
    'config/shared.lua',
}

client_scripts { 'client/main.lua' }
server_scripts { 'server/main.lua' }

dependencies {
    'en-core',
    'en-ui',    -- needed for encore.notify, encore.progress and dialogs
}

config/server.lua is not listed. It is loaded with require on the server, which reads straight from disk, so it needs no manifest entry and is never sent to players.

Add the resource to server.cfg after EndCore:

cfg
ensure [encore]
ensure my-scavenge

3. Config

Settings players' clients need go in a shared script, as a global:

lua
-- config/shared.lua
Config = {
    models = { 'prop_dumpster_01a', 'prop_dumpster_02a' },
    distance = 2.0,
    searchTime = 5000,
}

Settings only the server should see return a table and are loaded with require:

lua
-- config/server.lua
return {
    cooldown = 15 * 60,  -- seconds before the same dumpster can be searched again
    xp = 3,              -- scavenging skill XP per search
    loot = {
        { item = 'scrapmetal', min = 1, max = 3, chance = 0.6 },
        { item = 'garbage',    min = 1, max = 2, chance = 0.5 },
        { item = 'bandage',    min = 1, max = 1, chance = 0.1 },
    },
}

How require works

The library replaces Lua's require with a resource-aware one:

CallLoads
require 'config.server'config/server.lua in this resource
require 'modules.loot'modules/loot.lua, or modules/loot/init.lua
require '@en-core.shared.jobs'shared/jobs.lua from the en-core resource

Results are cached per resource. On the client, a required file must be listed in your manifest's files {} so the client has downloaded it. Don't also list required files as scripts, or they run twice. See Library overview.

4. The server

The server registers a callback that the client calls after the search finishes. It checks everything the client sent before giving anything out.

lua
-- server/main.lua
local ServerConfig = require 'config.server'

local searched = {} -- [dumpster key] = os.time() of the last search

local function keyFor(coords)
    return ('%d:%d:%d'):format(math.floor(coords.x), math.floor(coords.y), math.floor(coords.z))
end

encore.callback.register('my-scavenge:search', function(source, coords)
    if not exports['en-core']:IsPlayerLoaded(source) then return false end

    -- Never trust the client: check the type, then check the player is really there.
    if type(coords) ~= 'vector3' then return false, 'Nothing to search.' end
    if #(GetEntityCoords(GetPlayerPed(source)) - coords) > Config.distance + 1.5 then
        return false, 'You are too far away.'
    end

    local key = keyFor(coords)
    if searched[key] and os.time() - searched[key] < ServerConfig.cooldown then
        return false, 'Someone already picked this clean.'
    end
    searched[key] = os.time()

    local found = {}
    for _, entry in ipairs(ServerConfig.loot) do
        if math.random() < entry.chance then
            local count = math.random(entry.min, entry.max)
            if encore.inventory.canCarry(source, entry.item, count)
                and encore.inventory.addItem(source, entry.item, count) then
                local def = encore.inventory.getItemDefinition(entry.item)
                found[#found + 1] = ('%dx %s'):format(count, def and def.label or entry.item)
            end
        end
    end

    encore.skills.addXP(source, 'scavenging', ServerConfig.xp)

    return true, #found > 0 and table.concat(found, ', ') or nil
end)

encore.addCommand('resetdumpsters', {
    help = 'Make every dumpster searchable again',
    restricted = 'group.admin',
}, function(source)
    searched = {}
    encore.notify(source, { description = 'Dumpsters reset.', type = 'success' })
end)

What the library is doing here:

  • encore.callback.register answers calls from any client. Handler errors are caught and logged, and multiple return values reach the client intact.
  • encore.inventory.canCarry and addItem go to en-inventory. If en-inventory isn't running they return false, so nothing breaks.
  • encore.skills.addXP does nothing if en-skills isn't running.
  • encore.addCommand with restricted = 'group.admin' grants the ACE to that principal and only shows the chat suggestion to players who can use it.
Tip

This example trusts the client's progress bar for timing. For valuable loot, do what the shipped resources do: a start callback that records the time on the server, and a finish callback that refuses answers that come back too early.

5. The client

The client adds a look-at option to the dumpster models. When picked, it runs a progress bar, asks the server, and shows the result.

lua
-- client/main.lua
encore.target.addModel(Config.models, {
    name = 'my-scavenge:search',
    label = 'Search dumpster',
    icon = 'search',
    distance = Config.distance,
    onSelect = function(data)
        local done = encore.progress({
            label = 'Searching',
            duration = Config.searchTime,
            disable = { move = true, combat = true },
            anim = { scenario = 'PROP_HUMAN_BUM_BIN' },
            maxDistance = 2.0,
        })
        if not done then return end

        local ok, result = encore.callback.await('my-scavenge:search', GetEntityCoords(data.entity))
        if not ok then
            return encore.notify({ description = result or 'Search failed.', type = 'error' })
        end

        encore.notify({
            title = 'Dumpster',
            description = result and ('Found ' .. result) or 'Nothing useful.',
            type = result and 'success' or 'inform',
        })
    end,
})

What the library is doing here:

  • encore.target.addModel registers the option with en-target. It is remembered and re-applied if en-target restarts, and removed when your resource stops.
  • onSelect runs in its own thread in your resource, so it can wait on a progress bar and a callback. canInteract, if you add one, runs on every look check and must not wait.
  • encore.progress blocks until the bar finishes or is cancelled (the player presses X, moves too far, or dies), and returns true only on completion.
  • encore.callback.await returns nothing if the server doesn't answer within 15 seconds, which the if not ok branch handles.

Start the server, walk up to a dumpster, hold Left Alt, and pick Search dumpster.

6. Notifications

encore.notify works on both sides:

lua
-- client
encore.notify({ title = 'Radio', description = 'Static.', type = 'warning', duration = 4000 })

-- server: send to one player
encore.notify(source, { description = 'Your base needs upkeep.', type = 'warning' })
FieldDefaultNotes
titlenoneOptional heading
descriptionnoneBody text. A plain string works too.
type'inform'inform, success, warning or error
duration5000Milliseconds, clamped 1500 to 15000
iconby typeAny en-ui icon name

Text is rendered safely, so player-written strings can't inject markup. See UI services.

7. The inventory interface

These calls work with en-inventory, or with any replacement that implements the same exports.

SideCallReturns
serverencore.inventory.addItem(source, item, count, metadata?)boolean
serverencore.inventory.removeItem(source, item, count, metadata?)boolean
serverencore.inventory.canCarry(source, item, count, metadata?)boolean
serverencore.inventory.getItemCount(source, item, metadata?)number
serverencore.inventory.registerUsable(item, handler)none
clientencore.inventory.getItemCount(item, metadata?)number
bothencore.inventory.getItemDefinition(item)table or nil
bothencore.inventory.isAvailable()boolean

To make an item do something when used, register a handler and return true to consume one:

lua
-- server
encore.inventory.registerUsable('scrap_bundle', function(source, item, slot)
    if not encore.inventory.canCarry(source, 'scrapmetal', 5) then return false end
    encore.inventory.addItem(source, 'scrapmetal', 5)
    return true
end)

See Add an item and Inventory interface.

8. Going further

The same patterns extend to the rest of the library:

You want toUseDocs
Gate an option by job, group or itemgroups and items on a target optionTarget interface
Ask the player for inputencore.inputDialogUI services
Show a key promptencore.showPrompt / encore.hidePromptUI services
Add a rebindable keyencore.addKeybindKeybinds
Add a radial menu entryencore.radial.addItemMinigames and radial
Add a lockpick or skill checkencore.minigame.lockpick, skillCheckMinigames and radial
Share XP with a partyencore.party.shareXPSkills and party
Load models and animationsencore.requestModel, encore.requestAnimDictStreaming and world
Let admins place your contentencore.content.watch and builder exportsContent API
Build a NUI pageencore.css and encore.js from en-uiDesign system
React to players loading, money or survivalencore:server:* eventsEvents

If you'd rather have a resource built for you, Fxora takes custom EndCore development, and Upgrade Scripts has ready-made addons.