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 globalencore. - Call interfaces, not resources. Use
encore.inventory,encore.target,encore.skillsand 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
sourceis 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
resources/
[local]/
my-scavenge/
fxmanifest.lua
config/
shared.lua
server.lua
client/
main.lua
server/
main.lua2. The manifest
-- 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:
ensure [encore]
ensure my-scavenge3. Config
Settings players' clients need go in a shared script, as a global:
-- 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:
-- 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:
| Call | Loads |
|---|---|
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.
-- 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.registeranswers calls from any client. Handler errors are caught and logged, and multiple return values reach the client intact.encore.inventory.canCarryandaddItemgo to en-inventory. If en-inventory isn't running they returnfalse, so nothing breaks.encore.skills.addXPdoes nothing if en-skills isn't running.encore.addCommandwithrestricted = 'group.admin'grants the ACE to that principal and only shows the chat suggestion to players who can use it.
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.
-- 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.addModelregisters the option with en-target. It is remembered and re-applied if en-target restarts, and removed when your resource stops.onSelectruns 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.progressblocks until the bar finishes or is cancelled (the player presses X, moves too far, or dies), and returnstrueonly on completion.encore.callback.awaitreturns nothing if the server doesn't answer within 15 seconds, which theif not okbranch handles.
Start the server, walk up to a dumpster, hold Left Alt, and pick Search dumpster.
6. Notifications
encore.notify works on both sides:
-- 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' })| Field | Default | Notes |
|---|---|---|
title | none | Optional heading |
description | none | Body text. A plain string works too. |
type | 'inform' | inform, success, warning or error |
duration | 5000 | Milliseconds, clamped 1500 to 15000 |
icon | by type | Any 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.
| Side | Call | Returns |
|---|---|---|
| server | encore.inventory.addItem(source, item, count, metadata?) | boolean |
| server | encore.inventory.removeItem(source, item, count, metadata?) | boolean |
| server | encore.inventory.canCarry(source, item, count, metadata?) | boolean |
| server | encore.inventory.getItemCount(source, item, metadata?) | number |
| server | encore.inventory.registerUsable(item, handler) | none |
| client | encore.inventory.getItemCount(item, metadata?) | number |
| both | encore.inventory.getItemDefinition(item) | table or nil |
| both | encore.inventory.isAvailable() | boolean |
To make an item do something when used, register a handler and return true to consume one:
-- 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 to | Use | Docs |
|---|---|---|
| Gate an option by job, group or item | groups and items on a target option | Target interface |
| Ask the player for input | encore.inputDialog | UI services |
| Show a key prompt | encore.showPrompt / encore.hidePrompt | UI services |
| Add a rebindable key | encore.addKeybind | Keybinds |
| Add a radial menu entry | encore.radial.addItem | Minigames and radial |
| Add a lockpick or skill check | encore.minigame.lockpick, skillCheck | Minigames and radial |
| Share XP with a party | encore.party.shareXP | Skills and party |
| Load models and animations | encore.requestModel, encore.requestAnimDict | Streaming and world |
| Let admins place your content | encore.content.watch and builder exports | Content API |
| Build a NUI page | encore.css and encore.js from en-ui | Design system |
| React to players loading, money or survival | encore:server:* events | Events |
If you'd rather have a resource built for you, Fxora takes custom EndCore development, and Upgrade Scripts has ready-made addons.