qb-vineyard
qb-vineyard is a three-stage production chain for the vineyard job: pick grapes, press them into
grape juice, ferment juice into wine, then sell either product to an NPC at a fixed price.
Of the job resources audited on this site, it has the tightest server-side validation. It is a useful reference implementation when you write your own.
The manifest at the commit linked above declares version 1.5.0.
Dependencies and start order
The README names qb-core and PolyZone. The manifest loads @qb-core/shared/locale.lua, locales,
and config.lua as shared scripts, then PolyZone client.lua and BoxZone.lua plus client.lua,
and server.lua on the server.
The implementation also uses qb-inventory for AddItem,
RemoveItem, and the item box.
Start order: qb-core, qb-inventory, PolyZone, then qb-vineyard.
Required shared items: grape, grapejuice, wine.
Configuration
| Key | Default | Meaning |
|---|---|---|
Debug | false | Draws zone outlines |
PickAmount | { min = 8, max = 12 } | Client-side pick animation count |
GrapeAmount | { min = 8, max = 12 } | Grapes granted per pick |
GrapeJuiceAmount | { min = 6, max = 10 } | Juice granted per press |
WineAmount | { min = 6, max = 10 } | Wine granted per fermentation |
wineTimer | 180 | Fermentation duration |
Sell.enabled | true | Enable the NPC buyer |
Sell.prices | { wine = 15, grapejuice = 10 } | Cash paid per unit |
Sell.coords, Sell.zones, Sell.minZ, Sell.maxZ | Sell zone geometry | |
Vineyard.start, Vineyard.wine, Vineyard.grapejuice | Stage zone geometry |
The crafting input quantities are not in the config. They are hardcoded in server.lua: 16 grape
produce one batch of grape juice, and 23 grapejuice produce one batch of wine.
The 20 grape pickup points are also hardcoded in server.lua as a local grapeLocations table, not
in config.lua. Moving the vineyard means editing both files.
Server surface
| Handler | Kind | Arguments |
|---|---|---|
qb-vineyard:server:getGrapes | net event | none |
qb-vineyard:server:loadIngredients | callback | none, returns a boolean |
qb-vineyard:server:grapeJuice | callback | none, returns a boolean |
qb-vineyard:server:receiveWine | net event | none |
qb-vineyard:server:receiveGrapeJuice | net event | none |
qb-vineyard:server:sellItems | net event | none |
Why this one is worth copying
Every reward path takes zero arguments. There is nothing for a client to forge. The server decides everything:
- Job check.
playerHasVineyardJobrequiresPlayerData.job.name == 'vineyard'. - Server-read position.
playerIsNearGrapeLocationreads the ped withGetPlayerPed(src)and compares against the server’s own coordinate list, within 5.0 units. - Rate limit. Grape pickups are throttled to one per 3000 ms per source using
GetGameTimer. - One-shot reward tokens. The crafting callbacks consume the input items first, then set
wineRewardState[src]orgrapeJuiceRewardState[src]. The matching reward event refuses unless that token is set, and clears it immediately. - Cleanup. A
playerDroppedhandler clears all three per-source tables.
The token pattern in step 4 is the piece most often missing elsewhere. It makes the reward event safe to expose, because firing it without having paid the input does nothing.
-- the shape, condensed from server.lua
RegisterNetEvent('qb-vineyard:server:receiveWine', function()
local src = source
local Player = exports['qb-core']:GetPlayer(src)
if not playerHasVineyardJob(Player) then return end
if not wineRewardState[src] then return end
wineRewardState[src] = nil
-- grant the reward
end)Selling
sellItems iterates Config.Sell.prices, removes every unit of each matching item the player
holds, sums amount * price, and adds the total to cash. The price comes from the config, never
from the client.
It does not check the player’s distance to the sell zone, and it does not check the job. The client
zone is the only gate, so a client that fires the event from anywhere sells its whole stock. If that
matters, add a distance check against Config.Sell.coords, mirroring
playerIsNearGrapeLocation.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Nothing happens when picking | The job is not exactly vineyard, the player is more than 5 units from a hardcoded grape point, or the 3 second cooldown is active |
| Crafting consumes items but grants nothing | The reward event never fired. The token is set and cleared only by the matching pair |
| Crafting refuses with enough items | The thresholds are 16 grape and 23 grapejuice, hardcoded in server.lua |
| Moving the vineyard breaks picking | grapeLocations lives in server.lua, not in config.lua |
| Selling pays nothing | The player holds none of the items listed in Config.Sell.prices |
Proposed manual smoke test
Not yet executed. On staging:
- Confirm
grape,grapejuice, andwineexist in your shared items. - Pick grapes twice within three seconds and confirm the second is ignored.
- Run the full chain and confirm the hardcoded input thresholds.
- Fire a reward event without crafting first and confirm nothing is granted.
- Sell both products and confirm cash matches
Config.Sell.prices.