en-shops puts NPC traders at settlements and checkpoints. Walk up to a trader and talk to them, either with the look-target or with the E prompt when targeting is off. The camera focuses on them, they greet you, and a trade page opens.
Traders sell goods for cash or for barter (other items, per unit). Some offers are barter-only, and offers can be locked behind a character level or a skill level. Traders also buy scavenged goods, either by item category or by item name.
Prices follow stock. Each trader holds limited stock of each item, which restocks over time. They charge more as a shelf empties and pay less for things they've already bought a lot of. The server makes every decision.
At a glance
| Depends on | oxmysql, en-core, en-ui, en-inventory |
| Start after | All of the above |
| Database tables | encore_shop_stock |
| Config files | config/shared.lua |
| Key binds | en_shops_trade (E), only active when en-target isn't running |
| In-game builder | Content kind trader in en-admin |
How it works
Trader peds
Trader peds are spawned client-side within 80 m. They are invincible and frozen, and can play a scenario. You must be within Config.distance to trade.
Buying
A buy is paid in cash or by barter. Cash buys are refunded if the item doesn't fit in your inventory. A barter buy removes each cost item (count * amount), and everything is rolled back if a step fails. An offer's requires checks your character level (see XP and levels) or a skill level from en-skills.
Selling
The sell tab lists everything you carry that this trader will buy. If you sell an item the trader also stocks, it goes back on their shelf, up to max.
Pricing
Buy price:
price = math.max(1, round(base * (1 + (1 - stock / max) * Config.pricing.scarcity)))
-- base = offer.price or Config.values[item] or 1Sell price, per unit:
price = math.floor(value * rate * (1 - math.min(maxDrop, holdings / saturation * maxDrop)))
-- rate = trader.buys.rate or Config.pricing.sellRateThe sell price is 0 if the trader doesn't accept the item or it has no value.
Restocking
Every Config.restock.minutes, each shelf gains ceil(max * refill) stock and each trader's holdings are multiplied by 1 - clearance. Stock and holdings are saved to the database every 60 s and when the resource stops.
Configuration
| Key | Default | What it does |
|---|---|---|
Config.values | e.g. water = 20, scrapmetal = 8, WEAPON_PISTOL = 900, ammo_rifle = 8 | Base cash value per item, and the default buy price. Items with no value can't be sold to anyone |
Config.pricing.scarcity | 0.5 | An empty shelf charges up to 50% more |
Config.pricing.sellRate | 0.45 | Fraction of the value a trader pays, before saturation |
Config.pricing.saturation | 25 | Holdings at which the sell price drop reaches maxDrop |
Config.pricing.maxDrop | 0.6 | Biggest reduction of the sell price |
Config.pricing.maxBatch | 50 | Most units in one buy or sell |
Config.restock.minutes | 30 | Minutes between restocks |
Config.restock.refill | 0.25 | Fraction of each shelf's max refilled per restock (at least 1) |
Config.restock.clearance | 0.3 | Fraction of holdings cleared per restock |
Config.distance | 3.0 | Trade reach in metres |
Config.traders | burton_quartermaster, sandy_scrapper, paleto_medic | Trader definitions |
Trader fields
| Field | What it does |
|---|---|
id | Unique id |
label | Stall name |
name | Trader's name |
greeting | A string, or a list of lines picked at random |
ped | { model, coords = vec4, scenario } |
blip | { sprite, colour }, or false for no blip |
sells | List of offers (below) |
buys | { categories, items, rate }: en-inventory item categories and specific item names this trader buys; rate overrides sellRate (clamped 0–2) |
Each entry in sells:
| Field | What it does |
|---|---|
item | Item name |
max | Shelf capacity. Stock starts full |
price | Optional fixed base price; defaults to Config.values[item] |
barter | { { item, count }, ... }, paid per unit |
barterOnly | true to disable cash (needs barter) |
requires | { level = n } for character level, or { skill = 'name', level = n } |
Adding a trader
Config.traders[#Config.traders + 1] = {
id = 'vinewood_fence',
label = 'Vinewood Fence',
name = 'Marta',
greeting = { 'Quick. What have you got?', 'No questions, no refunds.' },
ped = { model = 'a_f_y_hipster_02', coords = vec4(300.2, 180.4, 104.1, 70.0), scenario = 'WORLD_HUMAN_SMOKING' },
blip = { sprite = 52, colour = 47 },
sells = {
{ item = 'lockpick', max = 10 },
{ item = 'ammo_rifle', max = 120, barter = { { item = 'scrapmetal', count = 2 } }, requires = { level = 6 } },
{ item = 'gps', max = 2, barterOnly = true, barter = { { item = 'radio', count = 1 }, { item = 'duct_tape', count = 2 } } },
},
buys = { categories = { 'weapon', 'tool' }, items = { 'radio' }, rate = 0.5 },
}Traders can also be placed and edited in game with en-admin. A placed trader with a config id overrides that config trader, and deleting the placement brings the config version back. For a walkthrough, see Add a trader.
Exports
All exports are server-side. There are no client exports.
| Export | Arguments | Returns |
|---|---|---|
GetBuyPrice | traderId, itemName | Current cash price, or nil if the item isn't sold or is barter-only |
GetSellPrice | traderId, itemName | Price per unit the trader pays (0 if not accepted) |
GetStock | traderId, itemName | Current stock (0 for an unknown trader) |
Restock | traderId | Runs one restock tick for that trader |
BuilderList | kind | For 'trader': { id, label, subtitle, coords, placed, inConfig }[] |
BuilderGet | kind, id | Editable trader table |
BuilderValidate | kind, id, data | ok, err |
BuilderSchema | kind | en-admin form fields |
BuilderTemplate | kind | Defaults for a new trader |
Commands
| Command | Restricted | What it does |
|---|---|---|
/restockshops | group.admin | Fills every shelf to max and clears all holdings |
Events
Server-local events:
| Event | Payload |
|---|---|
en-shops:server:bought | source, traderId, item, amount, method ('cash' or 'barter') |
en-shops:server:sold | source, traderId, item, amount, totalCash |
The trade page closes on encore:client:playerDied and encore:client:playerUnloaded.
Database
encore_shop_stock: trader VARCHAR(60), item VARCHAR(60), stock INT, holdings INT. The primary key is (trader, item).
Examples
Show a trader's live price in your own UI:
-- server
local price = exports['en-shops']:GetBuyPrice('sandy_scrapper', 'scrapmetal')
local stock = exports['en-shops']:GetStock('sandy_scrapper', 'scrapmetal')
if price then
print(('Scrap metal: $%d (%d in stock)'):format(price, stock))
endRestock a trader when a supply convoy arrives:
AddEventHandler('myconvoy:server:arrived', function(traderId)
exports['en-shops']:Restock(traderId)
end)Track what players sell:
AddEventHandler('en-shops:server:sold', function(source, traderId, item, amount, totalCash)
print(('%s sold %dx %s to %s for $%d'):format(GetPlayerName(source), amount, item, traderId, totalCash))
end)