EndCore Framework

Money and cash

How EndCore stores cash and bank money, how cash works as a physical inventory item, and how balances stay in sync.

EndCore characters have two kinds of money: cash they carry and bank money that stays safe. With en-inventory running, cash is a real item. It takes up space, can be looted from a death bag and can be dropped. Bank money is always a number in the database.

Money types

The keys of defaultMoney in config/shared.lua define which money types exist and what new characters start with.

Key in config/shared.luaDefaultWhat it does
defaultMoney{ cash = 500, bank = 1000 }Starting balances. These keys are the valid money types.
moneyTypes{ cash = 'Cash', bank = 'Bank' }Display labels

Money rules in config/server.lua:

KeyDefaultWhat it does
money.dontAllowMinus{ 'cash', 'bank' }RemoveMoney refuses to take these below 0, and SetMoney clamps negatives to 0
money.roundingMode'floor''floor' or 'round'. Money is always stored in whole units.
money.cashItem'cash'The item that cash becomes while en-inventory runs. nil or false keeps cash as a database balance.
lua
-- config/server.lua
money = {
    dontAllowMinus = { 'cash', 'bank' },
    roundingMode = 'floor',
    cashItem = 'cash',
},

Changing money

Every change goes through the same rules:

  • Amounts are rounded to whole numbers using roundingMode. A value that is not a number becomes 0.
  • AddMoney and RemoveMoney need an amount greater than 0 after rounding, and a valid money type.
  • Every change is printed to the console with [MONEY] and posted to the playermoney webhook.
  • Every change fires encore:server:onMoneyChange and encore:client:onMoneyChange, which carry the new balance.
Player functionExport by sourceReturns
Functions.AddMoney(moneyType, amount, reason?)AddMoney(source, moneyType, amount, reason?)boolean
Functions.RemoveMoney(moneyType, amount, reason?)RemoveMoney(source, moneyType, amount, reason?)boolean
Functions.SetMoney(moneyType, amount, reason?)noneboolean
Functions.GetMoney(moneyType)GetMoney(source, moneyType)number

Admins can use /money add|remove|set <player> <type> <amount>. The reason is recorded as "Admin command by <name>". See Commands.

Cash as an item

Cash becomes an item when all three are true:

  • money.cashItem is a non-empty string.
  • en-inventory is running.
  • The item (by default cash) is defined in en-inventory.

If the item is missing, the console warns once: Item "cash" is not defined in en-inventory; cash stays a database balance.

While cash is an item:

  • Reading. GetMoney('cash') counts the item in the player's inventory and worn backpack.
  • Adding. AddMoney('cash', ...) gives the item. If it does not fit, en-core calls exports['en-inventory']:AddItemOrDrop, so the cash lands in a bag at the player's feet and they are told. It returns false only if even that fails.
  • Removing. RemoveMoney('cash', ...) returns false if the player is not carrying enough. Items can never go negative.
  • Setting. SetMoney('cash', ...) adds or removes the difference.
  • Bank is unaffected and always a database balance.

PlayerData.money.cash is still kept up to date as a mirror of the item count. That way the compatibility bridges, /charinfo and ESX account reads show a sensible number.

Note

A player who drops or trades cash items changes their cash balance, because the item is the cash. Scripts should always read cash with GetMoney, never by caching PlayerData.money.cash themselves.

Keeping cash in sync

en-core reconciles the item count with the stored balance, using two metadata keys: cashSynced and cashPending.

  • Migration. When a character from before cash was an item logs in, or cash changed while en-inventory was stopped or while the character was offline, the difference is handed out as items once, on login. Starting cash is included.
  • Cash that does not fit. Anything that does not fit is stored in cashPending and added when there is room. The player sees "No room for $X of your cash. Make space and it will be added."
  • Inventory changes. When en-inventory reports that a player's inventory changed, en-core refreshes that player's cash after a 250 ms debounce.
  • Inventory restarts. When en-inventory starts or restarts, every online player is refreshed.

Mirror-only updates are sent as encore:client:onMoneySync(moneyType, balance) and encore:server:onMoneySync(source, moneyType, balance). They do not fire onMoneyChange, so no "money received" toast appears when a player simply picks up or drops cash.

Client side

When encore:client:onMoneyChange arrives, the client stores the balance it carries (it does not re-apply the amount), then fires the local event encore:client:moneyChanged(moneyType, balance, action, reason). Sync updates fire the same local event with the action 'sync'. A HUD only needs to listen to that one event.

Events

SideEventPayload
Serverencore:server:onMoneyChangesource, moneyType, amount, action, reason, balance
Serverencore:server:onMoneySyncsource, moneyType, balance
Client (net)encore:client:onMoneyChangemoneyType, amount, action, reason, balance
Client (net)encore:client:onMoneySyncmoneyType, balance
Client (local)encore:client:moneyChangedmoneyType, balance, action, reason

action is 'add', 'remove' or 'set', or 'sync' on the local client event.

Examples

Sell an item for cash:

lua
local player = exports['en-core']:GetPlayer(source)
if not player then return end

if player.Functions.RemoveItem('scrap_metal', 5) then
    player.Functions.AddMoney('cash', 120, 'Sold 5 scrap metal')
end

Charge for a service, preferring cash then bank:

lua
local price = 300
if exports['en-core']:GetMoney(source, 'cash') >= price then
    exports['en-core']:RemoveMoney(source, 'cash', price, 'Vehicle repair')
elseif not exports['en-core']:RemoveMoney(source, 'bank', price, 'Vehicle repair') then
    encore.notify(source, { description = 'You cannot afford this.', type = 'error' })
end

Log large transfers:

lua
AddEventHandler('encore:server:onMoneyChange', function(source, moneyType, amount, action, reason, balance)
    if amount >= 10000 then
        exports['en-core']:Log('playermoney', ('%s %s %d %s (%s), now %d'):format(
            GetPlayerName(source), action, amount, moneyType, reason or 'no reason', balance))
    end
end)

Update a HUD on the client:

lua
AddEventHandler('encore:client:moneyChanged', function(moneyType, balance, action, reason)
    if moneyType == 'cash' then
        -- update your cash display with balance
    end
end)