EndCore Framework

en-shops

NPC traders who sell for cash or barter, buy scavenged goods, and set prices from limited, restocking stock.

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 onoxmysql, en-core, en-ui, en-inventory
Start afterAll of the above
Database tablesencore_shop_stock
Config filesconfig/shared.lua
Key bindsen_shops_trade (E), only active when en-target isn't running
In-game builderContent 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:

lua
price = math.max(1, round(base * (1 + (1 - stock / max) * Config.pricing.scarcity)))
-- base = offer.price or Config.values[item] or 1

Sell price, per unit:

lua
price = math.floor(value * rate * (1 - math.min(maxDrop, holdings / saturation * maxDrop)))
-- rate = trader.buys.rate or Config.pricing.sellRate

The 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

KeyDefaultWhat it does
Config.valuese.g. water = 20, scrapmetal = 8, WEAPON_PISTOL = 900, ammo_rifle = 8Base cash value per item, and the default buy price. Items with no value can't be sold to anyone
Config.pricing.scarcity0.5An empty shelf charges up to 50% more
Config.pricing.sellRate0.45Fraction of the value a trader pays, before saturation
Config.pricing.saturation25Holdings at which the sell price drop reaches maxDrop
Config.pricing.maxDrop0.6Biggest reduction of the sell price
Config.pricing.maxBatch50Most units in one buy or sell
Config.restock.minutes30Minutes between restocks
Config.restock.refill0.25Fraction of each shelf's max refilled per restock (at least 1)
Config.restock.clearance0.3Fraction of holdings cleared per restock
Config.distance3.0Trade reach in metres
Config.tradersburton_quartermaster, sandy_scrapper, paleto_medicTrader definitions

Trader fields

FieldWhat it does
idUnique id
labelStall name
nameTrader's name
greetingA string, or a list of lines picked at random
ped{ model, coords = vec4, scenario }
blip{ sprite, colour }, or false for no blip
sellsList 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:

FieldWhat it does
itemItem name
maxShelf capacity. Stock starts full
priceOptional fixed base price; defaults to Config.values[item]
barter{ { item, count }, ... }, paid per unit
barterOnlytrue to disable cash (needs barter)
requires{ level = n } for character level, or { skill = 'name', level = n }

Adding a trader

lua
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 },
}
Tip

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.

ExportArgumentsReturns
GetBuyPricetraderId, itemNameCurrent cash price, or nil if the item isn't sold or is barter-only
GetSellPricetraderId, itemNamePrice per unit the trader pays (0 if not accepted)
GetStocktraderId, itemNameCurrent stock (0 for an unknown trader)
RestocktraderIdRuns one restock tick for that trader
BuilderListkindFor 'trader': { id, label, subtitle, coords, placed, inConfig }[]
BuilderGetkind, idEditable trader table
BuilderValidatekind, id, dataok, err
BuilderSchemakinden-admin form fields
BuilderTemplatekindDefaults for a new trader

Commands

CommandRestrictedWhat it does
/restockshopsgroup.adminFills every shelf to max and clears all holdings

Events

Server-local events:

EventPayload
en-shops:server:boughtsource, traderId, item, amount, method ('cash' or 'barter')
en-shops:server:soldsource, 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:

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

Restock a trader when a supply convoy arrives:

lua
AddEventHandler('myconvoy:server:arrived', function(traderId)
    exports['en-shops']:Restock(traderId)
end)

Track what players sell:

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