Sal's Kewl Korner Documentation / Sal's Kewl Vending Discord

Sal's Kewl Vending — owner's manual

Every vending prop on the map becomes a machine that holds its own stock, runs out, can be forced open for the cash inside, and needs somebody to drive out and refill it. This manual covers installation, the machine and item configuration, stock, pricing, break-ins, the restock job and troubleshooting for server owners.

documents resource version 2.0.0

Overview

GTA's map is covered in vending machines nobody can use. This resource turns every one of them into a working shop — and then gives that shop a reason to exist in your economy.

A machine holds a finite amount of stock. Players buy from it, it empties, and it stays empty until either the slow automatic trickle refills it or a player driving the restock route loads it by hand. Somebody with a crowbar can force it open for the cash float, which puts a call out to your police.

Nothing about this needs you to place machines. The resource keys off the prop models already in the map, so the soda machine outside the Pillbox ER and the one in the airport terminal are both live the moment you start the resource — and they keep their stock separately.

It runs on qb-core, qbx_core, es_extended or standalone, and every integration is optional and detected at runtime.

Requirements

None. The resource starts and works on a bare server. Everything below is optional; each is detected at runtime and the resource degrades cleanly without it.

OptionalWhat it addsWithout it
qb-core / qbx_core / es_extendedMoney, jobs, inventoryStandalone: machines vend, nothing is charged
ox_inventoryItem handling, item images, weight checksFalls back to your framework's inventory
ox_target or qb-targetThird-eye interactionA drawn marker with an E prompt
ox_libContext menu, quantity dialog, skill check, progress barqb-menu, then a built-in NUI shop
qb-menuMenu when ox_lib is absentThe built-in NUI shop
oxmysqlStock that survives a restartStock works, but resets on restart
qb-management / qb-banking / Renewed-Banking / esx_addonaccountSociety revenueTakings are not routed anywhere
sals_kewldispatch / ps-dispatchPolice alerts on break-insOn-duty officers get a notification and a blip
wasabi_ambulanceAccurate downed-player checksFalls back to framework metadata, then entity health

Do not edit fxmanifest.lua. Version 1.x shipped with instructions to comment out dependency lines depending on what you ran. That step is gone — there is no dependencies block to edit, because nothing is a hard requirement.

Installation

  1. Extract the folder into your server's resources/ directory. The folder must be named sals_kewlvending.
  2. Add the start line to server.cfg:
    ensure sals_kewlvending
  3. Grant configurator access to whoever should have it:
    add_ace group.admin sals_kewlvending.config allow
  4. Open config.lua and set your machines, items and prices.

Database

Nothing to import. With oxmysql running, the sals_kewlvending_stock table is created on first start. Without oxmysql, stock is kept in memory for the uptime of the server and machines refill on restart.

Items your inventory needs

The resource sells whatever item names you put in config.lua. On the shipped defaults that is sprunk, ecola, water, coffee, latte, cigarette, joint, the sweet_* snacks, and vending_crate for the restock job.

Upgrading from 1.x: the cigarette machine now sells cigarette, not cigar. Add the item or change it back in your config.

How machines work

This is the one concept worth reading before you edit anything, because the config reads strangely until you have it.

A machine in config.lua is not one prop in the world — it is a kind of machine. One entry covers every soda machine on the map and lists what that kind sells.

Stock, however, is tracked per physical machine. The one outside the Pillbox ER and the one at the airport are the same kind, sell the same Sprunk, and run out independently.

A physical machine is identified by its kind's Id plus its rounded world position. That is why stock survives a restart without you placing anything: a map prop does not move, so its position is its identity.

Changing a machine's Id after players have been using it resets that machine's stock, because the identity changed. Pick an Id and leave it alone.

Machines & items

Machines live in Config.VendingMachines. A full entry:

{
    Id        = 'soda',                  -- unique, and permanent
    Label     = 'Soda Machine',
    Icon      = 'fa-solid fa-bottle-water',
    Payment   = 'either',                -- 'cash', 'bank' or 'either'
    Society   = 'vendingco',             -- optional; see Pricing & payment
    PropTypes = {
        'prop_vend_soda_01',
        'prop_vend_soda_02',
    },
    Items = {
        { Item = 'sprunk', Label = 'Sprunk', Price = 5 },
        { Item = 'ecola',  Label = 'eCola',  Price = 7 },
    },
},
KeyMeaning
IdUnique, permanent. Half of how a physical machine is identified.
LabelWhat players see in the menu and on the target.
IconFontAwesome class for the target option. Optional.
Paymentcash, bank, or either to let the player choose. Falls back to Config.Purchase.DefaultPayment.
SocietyAccount that receives this machine's takings. Optional.
PropTypesProp model names this kind covers. A prop may belong to only one machine.
ItemsWhat it sells. Price must be a whole number.

Any prop model works, not only vending props — point a machine at a fridge, a crate or a market stall and it behaves the same way.

Stock

Config.Stock = {
    Enabled         = true,
    Persist         = true,   -- needs oxmysql
    Default         = 20,     -- units each slot holds when full
    RestockMinutes  = 45,     -- how often idle machines trickle back up
    RestockAmount   = 5,      -- units added per slot per trickle
    ShowCount       = true,   -- show "12 left" in the menu
}

A machine the server has never seen is full by definition, so there is nothing to seed and no startup cost for a map full of props. Machines enter the database the first time somebody buys from one or robs it.

Set Enabled = false and machines are bottomless, which is how version 1.x behaved.

The passive trickle exists so a server with nobody driving the restock route does not end up with a map of empty boxes. Turn it down or set RestockAmount = 0 if you want the restock job to be the only way machines refill.

Pricing & payment

Location pricing

A machine inside the airport terminal charges what an airport charges. The first zone a machine falls inside wins; machines outside every zone pay the base price.

Config.Pricing = {
    Enabled = true,
    Zones = {
        { Label = 'Los Santos International', Coords = vec3(-1037.0, -2737.0, 20.0), Radius = 400.0, Multiplier = 1.60 },
        { Label = 'Bolingbroke Penitentiary', Coords = vec3(1845.0,  2585.0,  45.0), Radius = 350.0, Multiplier = 1.75 },
        { Label = 'Sandy Shores',             Coords = vec3(1961.0,  3740.0,  32.0), Radius = 500.0, Multiplier = 0.85 },
    },
}

The adjusted price is rounded to a whole dollar and shown in the menu, so a player always sees what they are about to be charged.

Cash or card

Set Payment per machine. With either, the player is asked at checkout (with ox_lib) or charged cash (without it).

Society revenue

Config.Ownership = {
    Enabled = false,
    Share   = 1.0,      -- 0.0-1.0 of each sale that reaches the society
}

Turn this on and give a machine a Society, and that machine's takings land in the business account instead of vanishing. Works with qb-management, qb-banking, Renewed-Banking and esx_addonaccount. With no banking resource installed the sale still stands; the money simply is not routed.

Break-ins

A player carrying a lockpick, screwdriver or crowbar can force a machine open for the cash float and whatever stock is still inside.

Config.Robbery = {
    Enabled       = true,
    RequiredItems = { 'lockpick', 'screwdriver', 'crowbar' },  -- any one
    ConsumeItem   = false,   -- take the tool on a failed attempt
    Duration      = 12000,
    Cooldown      = 60,      -- minutes before the same machine can be hit again
    CashMin       = 40,
    CashMax       = 220,
    ItemChance    = 60,      -- % chance to also spill stock
    MaxItems      = 5,
    PoliceAlert   = true,
    MinimumPolice = 0,       -- officers on duty required; 0 disables the check
    AlertChance   = 85,      -- % chance the alarm reaches dispatch
    PoliceJobs    = { 'police', 'sheriff', 'statepolice', 'bcso' },
    SkillCheck    = { 'easy', 'easy', 'medium' },
}

With ox_lib installed the break-in is a skill check; without it, a timed progress bar. The alarm fires when the machine is forced, not when it is emptied, so police have a chance to arrive before the thief is finished.

AlertChance below 100 means some break-ins go unreported, which keeps players from treating the alarm as a certainty. The cooldown applies to the attempt whether it succeeded or failed, so a failed try cannot be retried instantly.

Break-ins ship enabled. Set Enabled = false if you do not want them on your server.

The restock job

A paid loop for players: collect crates from the depot, drive to machines that are actually low, and refill them.

Config.Restock = {
    Enabled       = true,
    Job           = nil,            -- nil = anyone may do it; or 'trucker'
    Depot         = vec3(-424.0, -2790.0, 6.0),
    CrateItem     = 'vending_crate',
    CrateCapacity = 40,             -- units one crate refills
    CratePrice    = 0,              -- charge per crate, 0 for free
    PayPerUnit    = 6,
    PayAccount    = 'bank',
    MaxCrates     = 3,
    LowStockPct   = 50,             -- machines at or below this show on the route
    RouteBlips    = 6,
}

A driver runs /vendingroute to mark the nearest machines that need something, drives to one, and uses the restock option on it. Pay is calculated from the units actually loaded, after the refill succeeds.

Only machines the server has seen can be low — an untouched machine is full by definition. That falls out of how stock works, and it means the route only ever sends a driver somewhere worth going.

Eating & drinking

Vended items restore hunger and thirst, and coffee takes the edge off stress. Only the items listed in Config.Consumables.Items are registered — leave an item out and whatever food resource you already run keeps ownership of it.

Config.Consumables = {
    Enabled = true,
    Items = {
        ['sprunk'] = { thirst = 24, hunger = 0,  drink = true },
        ['coffee'] = { thirst = 18, hunger = 0,  drink = true, stress = -6 },
        ['sweet_cake_a'] = { thirst = 0, hunger = 26 },
    },
}

Values are percentage points added to the player's status.

With ox_inventory, item use is driven by the item definition, not from here. Point your definitions at this resource in ox_inventory/data/items.lua:

['sprunk'] = {
    label = 'Sprunk',
    weight = 200,
    client = { status = { thirst = 200000 } },
    server = { export = 'sals_kewlvending.useConsumable' },
},

On qb-core and ESX the items are registered as usable automatically.

The configurator

/vendingconfig opens an in-game editor for your machine list — machines, prop models, items and prices — with changes applied live to everyone online.

Access

Access is an ACE permission, checked on the server:

add_ace group.admin sals_kewlvending.config allow

If you would rather name individual people, list their identifiers in Config.Permissions.AllowedIdentifiers. It is an OR with the ACE check and ships empty on purpose.

How changes are stored

The configurator never touches config.lua. It validates what you typed and writes it to a separate config_overrides.json that layers on top at start. Your comments and formatting survive, and deleting that one file — or running /vendingreset — puts you back exactly where you began.

A save is rejected as a whole rather than repaired. Prices must be whole numbers, machine Ids must be unique, and prop and item names are restricted to sane characters. If a save is refused the editor tells you which entry was the problem.

Commands

CommandWhoWhat
/vendingconfigACE sals_kewlvending.configOpen the configurator
/vendingreloadACE / consoleReload configuration from disk
/vendingresetACE / consoleDiscard configurator changes, return to config.lua
/vendingstockACE / consoleList tracked machines and their fill level
/vendingstock resetACE / consoleRefill every machine
/vendingrouteAnyone, or the restock jobMark machines that need restocking

Audit logging

Config.Logging = {
    Enabled   = true,
    Console   = true,
    Webhook   = '',            -- a secret; keep it out of any repository
    Events    = {
        purchase  = false,     -- every sale is noisy; on for economy audits
        robbery   = true,
        restock   = true,
        config    = true,      -- who changed the configurator, and when
        rejected  = true,      -- refused purchases: where cheating shows up
    },
}

Logs go to the server console always, and to a Discord webhook when one is set. A webhook that is down never costs a player their money — logging is fire and forget and cannot block a sale.

The rejected channel is the useful one for spotting trouble: it records purchases the server refused, which is what an automated client trying its luck looks like from the outside.

Upgrading from 1.x

Your 1.x config.lua will not carry over. The machine format changed. Start from the new shipped config and re-enter your prices rather than trying to patch the old one.

1.x2.0.0
propTypes, items, item, label, pricePropTypes, Items, Item, Label, Price — and every machine needs a unique Id
Config.AllowedIDsadd_ace group.admin sals_kewlvending.config allow
Config.Debug = trueConfig.Debug = { Enabled = false, Prefix = '...' } (a plain boolean still works)
/openvendingconfig/vendingconfig
cigarcigarette
Edit fxmanifest.lua to match your frameworkNothing to edit

The sals_kewlvending:purchase event signature changed and no longer accepts a price. Anything you built against it must be updated.

Default snack prices were spread out rather than left flat at $15 — lollipops $12, candy $14, cookies $13, chocolate $16, cake $18. Your own prices are unaffected if you re-enter them.

What the server verifies

Worth stating plainly, because it is the difference between a vending script and a hole in your economy.

Every transaction is decided by the server. The client may say "I am at this kind of machine, at roughly this spot, and I want slot 3, four of them." Everything that matters is answered from your config:

  • the player's real position is within reach of the machine they claim;
  • the item is one that machine actually sells;
  • the price is the one in your config, adjusted by your location zones;
  • the money is really deducted before the item is handed over, and refunded if the inventory refuses it;
  • the stock really depletes;
  • and the whole thing is rate limited per player.

What the server cannot do is confirm that a vending prop exists at a given spot — map props are client-side and a FiveM server has no handle on them. A determined cheater can therefore claim to be standing at a machine while standing in a field. They still cannot change what is sold, what it costs, or get something for nothing.

Version 1.x was not server-authoritative: the purchase event accepted an item name and a price from the client. If you are running 1.x, upgrading is the fix.

Troubleshooting

Nothing happens when I look at a vending machine

Check the console on start for the targeting ready line with debug on. If it says marker, you have no targeting resource and should walk up and press E. If the prop is not one of yours, add its model name to a machine's PropTypes.

Machines are always full

Config.Stock.Enabled is false, or RestockAmount and RestockMinutes are refilling them faster than players empty them.

Stock resets every restart

oxmysql is not running, or Config.Stock.Persist is false. The console says which on start.

Players are charged but get nothing

They should not be — a failed inventory add is refunded, stock included, and logged under rejected. If you see this, check that log channel and send it to support.

The restock route says nothing needs restocking

Only machines the server has seen can be low. Until somebody buys from a machine it is full by definition, so a fresh server has an empty route. This is expected.

The configurator says access denied

The ACE is missing from server.cfg, or you are not in the group it was granted to. Confirm with add_ace group.admin sals_kewlvending.config allow and check the player is actually in group.admin.

Consumables do nothing when used

With ox_inventory, item use comes from the item definition — point it at server = { export = 'sals_kewlvending.useConsumable' }. Without ox_inventory, check the item name in Config.Consumables.Items matches your inventory exactly.

Support

Support, release news and coupon codes are on Discord. Purchases are handled by Tebex, who own the checkout, billing support and refunds.

The resource ships README.md and a full CHANGELOG.md inside the download.