An NPC gas station attendant who walks over, talks, fuels your car and takes payment. Install, configure, and hook it into your server.
RD-Fuel replaces the usual "hold a key at the pump" refuel with a gas station attendant NPC. The player parks next to a station, presses E, and the attendant walks to the driver window, greets them, asks them to turn the engine off, lets them pick how much fuel, takes cash or card, fills the car with a real nozzle and rope while chatting, and offers a tip at the end.
It works with ESX, QBCore, QBox and standalone servers, and with native fuel, ox_fuel, LegacyFuel, cdn-fuel or your own system.
Free version vs open source version. In the free (escrowed) version, config.lua and custom_functions.lua are open and everything in this guide that points at those two files applies. Anything that mentions client.lua, server.lua or the UI (dialogue lines, animations, UI styling) is only editable in the open source version.
ox_fuel / LegacyFuel / cdn-fuelresources directory and keep the folder name rd-fuel.html/sounds/ and that visits.json exists in the resource folder (it can contain just {}). The visit history is saved there.server.cfg:ensure es_extended # or qb-core / qbx_core
ensure ox_fuel # or LegacyFuel / cdn-fuel (optional)
ensure rd-fuel
config.lua and set Config.Framework and Config.FuelSystem (see below).Config.Debug = false before going live.Your vehicle fuel system should be the one actually reading the fuel level. Set Config.FuelSystem to match it, otherwise the attendant fills a value your HUD never reads.
Everything lives in config.lua. Defaults below are the values shipped in the file.
| Key | Default | What it does |
|---|---|---|
Framework | "standalone" | "auto", "standalone", "esx", "qbcore", "qbox" or "hook". See Framework & payment. |
FuelSystem | "native" | "native", "ox_fuel", "LegacyFuel", "cdn-fuel" or "hook". |
Debug | true | Enables the debug commands. Set to false in production. The Test* keys under it are only used by the debug test spawn and can be ignored. |
ResetVisitsOnStop | true | Wipes the saved visit history when the resource stops/restarts. Good for testing, set false on a real server so attendants remember players. |
| Key | Default | What it does |
|---|---|---|
AttendantModel | s_m_y_xmech_02 | Ped model of the attendant. |
NozzleProp | prop_cs_fuel_nozle | Nozzle prop he carries and attaches to the car. |
EnableRope | true | Draws the rope between the station and the nozzle. |
PedHeightOffset | 1.0 | Lowers the ped so he sits on the ground properly while idling. |
InteractControl | 38 | Key that calls the attendant (38 = E / X on controller). |
InteractPromptText | "Call the attendant over" | Text of the on-screen prompt. |
InteractRange | 3.0 | How close the parked vehicle must be to the station's stopCoord to show the prompt. |
DriverWindowIndex | 0 | Which window the attendant talks through (0 = driver side). |
RequireWindowDown | true | He asks you to roll the window down and does it when you accept. |
AutoRollDownWindow | false | Rolls the window down silently instead of asking. Ignored if RequireWindowDown is true. |
RegularVisitThreshold | 5 | Visits to one station before he treats you as a regular. |
Each entry needs two vector4 values: where the attendant stands (pedCoord) and where the customer's vehicle should park (stopCoord). The server spawns one attendant per entry.
Config.Stations = {
{
pedCoord = vector4(-78.3760, -1761.2500, 29.6482, 251.1720),
stopCoord = vector4(-75.8011, -1762.2400, 29.5319, 339.1300),
},
}
Config.FillOptions = {
{ id = "fill_quarter", label = "Quarter tank", durationMs = 22000, price = 15, fuelAmount = 25 },
{ id = "fill_half", label = "Half tank", durationMs = 40000, price = 28, fuelAmount = 50 },
{ id = "fill_full", label = "Fill it up", durationMs = 70000, price = 45, fuelAmount = 100 },
}
Keep the list sorted from small to big. Prices and fill times are scaled automatically to the room left in the tank, and an option that would overfill becomes "Top it off". You can freely change label, durationMs, price and fuelAmount.
Keep the three id values (fill_quarter, fill_half, fill_full). The attendant's spoken acknowledgement for each option is keyed by that id, so adding or renaming options needs matching dialogue lines (open source version).
| Key | Default | What it does |
|---|---|---|
FullTankThreshold | 90.0 | At or above this %, the attendant refuses to fill and doesn't charge. |
LowFuelThreshold | 15.0 | Below this %, he greets you with the "running on fumes" line. |
MinFillDurationMs | 10000 | A small top-off never takes less than this. |
Config.Payment = {
Enabled = true,
Currency = "$",
AllowCash = true,
AllowCard = true,
CardDeclineChance = 0.0, -- 0.0 - 1.0
CashAccount = "cash",
CardAccount = "bank",
}
| Key | What it does |
|---|---|
Enabled | false = fuel is free on every framework. |
AllowCash / AllowCard | Which payment buttons are offered. |
CardDeclineChance | Chance a card payment fails (checked server-side). Handy for testing. |
CashAccount / CardAccount | Account each method draws from. ESX: "cash" uses the cash balance, anything else is a named account. QBCore/QBox: passed as the money type. |
Config.Tip = {
Enabled = true,
Options = { 2, 5, 10 }, -- amounts shown as buttons
}
Tips are paid with the same method used for the fuel. Where the tip money goes is decided in Hooks.OnTip (see Hooks). With no code there, the tip is simply removed from the player. Tips are off whenever payment is inactive.
Config.RefundOnDrop = {
Enabled = true,
Prorated = true, -- true = refund only the unfinished part, false = full price
}
| Key | Default | What it does |
|---|---|---|
StationClaimTimeoutMs | 420000 | A claim older than this can be taken over by another player (crash safety). |
VoiceRelayRadius | 20.0 | Players this close to the attendant hear his voice lines while he serves someone else. |
| Key | Default | What it does |
|---|---|---|
UseVoiceAudio | true | false = captions only, no mp3 playback. |
AttendantVoiceVolume | 0.85 | Master volume of his voice lines. |
PhoneCallChance | 0.15 | Chance per idle roll that he picks up a phone call. |
PhoneCallCooldownMs | {30000, 90000} | Min/max time between rolls. |
PhoneCallDurationMs | {8000, 20000} | How long a call runs if nobody interrupts it. |
PhoneTalkDelayMs | 3000 | Pause after he answers before the first voice line. |
PhoneRingCutMs | {4000, 5000} | When during the ring clip he "answers" and cuts it. |
The remaining phone keys (PhoneProp, PhoneBoneId, PhoneAnim, PhoneAnimEnterMs, PhoneAnimExitMs, PhoneRingFile) and FacialAnims only need changing if you swap the prop, animations or ring sound.
Config.TestMoney = {
Enabled = true,
Cash = 100,
Bank = 500,
}
Only used when the framework resolves to standalone. See Framework & payment.
Config.Framework | Behaviour |
|---|---|
"auto" | Detects qbx_core, then qb-core, then es_extended, otherwise standalone. |
"esx" / "qbcore" / "qbox" | Real payment through that framework, using CashAccount / CardAccount. |
"hook" | Payment goes through custom_functions.lua (GetBalance, RemoveMoney, AddMoney). |
"standalone" | No economy: fuel is free. With Config.TestMoney.Enabled = true you get a fake in-memory wallet so you can test the full cash/card/tip flow. |
Prices are always calculated on the server. The client only sends the option id and the fraction of the option that is needed, so a modified client can't pay less.
With Config.Framework = "standalone" and Config.TestMoney.Enabled = true, each player gets a fake wallet (cash and bank) that lives in server memory and resets when the resource restarts. Use /fuelmoney to see your balance and /fuelmoney reset to restore the starting amounts. To test failures, lower Cash for "insufficient funds" or set CardDeclineChance = 1.0 for declined cards.
Config.FuelSystem | How fuel is read / written |
|---|---|
"native" | GetVehicleFuelLevel / SetVehicleFuelLevel |
"ox_fuel" | The vehicle's fuel state bag plus the native level |
"LegacyFuel" | exports['LegacyFuel']:GetFuel / SetFuel |
"cdn-fuel" | exports['cdn-fuel']:GetFuel / SetFuel |
"hook" | Hooks.GetFuel and Hooks.SetFuel in custom_functions.lua |
Fuel levels are 0-100. While the attendant fills, the level ramps up smoothly over the fill duration and is locked to the exact final value when it finishes.
custom_functions.lua is shared between client and server. The top half runs on the server, the bottom half on the client. GetBalance, RemoveMoney and AddMoney are only used when Config.Framework = "hook"; GetFuel and SetFuel only when Config.FuelSystem = "hook".
| Hook | Return | When it runs |
|---|---|---|
GetIdentifier(source) | string or nil | Stable player id used for visit history. nil falls back to the license. |
GetBalance(source, account) | number | Balance check. account is "cash" or "bank" (from CashAccount / CardAccount). |
RemoveMoney(source, account, amount, reason) | true if removed | Charges the player for fuel and tips. |
AddMoney(source, account, amount, reason) | true on success | Used for refunds. |
OnPayment(source, optionId, price, method) | - | After a successful fuel payment (logging, society money...). |
OnTip(source, amount, method) | - | After a successful tip. The money is already removed; send it to a business, society, the attendant's pay, etc. |
Hooks.RemoveMoney = function(source, account, amount, reason)
return exports['my_bank']:Remove(source, account, amount, reason)
end
Hooks.OnTip = function(source, amount, method)
exports['qb-management']:AddMoney('mechanic', amount)
end
| Hook | Description |
|---|---|
GetFuel(vehicle) | Return the fuel level (0-100). Used when FuelSystem = "hook". |
SetFuel(vehicle, level) | Apply a fuel level (0-100). Used when FuelSystem = "hook". |
OnFillStart(vehicle, fill) | Always fires when fuel starts flowing, whatever the fuel system is. |
OnFillComplete(vehicle, fill, finalLevel) | Always fires when the fill finishes. |
The fill table contains id, label, fuelAmount, fraction, price, durationMs and (after payment) paidMethod.
Other resources can listen to these without touching RD-Fuel.
| Event | Side | Arguments |
|---|---|---|
rdfuel:fillStarted | client | vehicle, fill |
rdfuel:fillCompleted | client | vehicle, fill |
rdfuel:tipPaid | server | source, amount, method |
rdfuel:refundPaid | server | source, amount, method |
AddEventHandler('rdfuel:tipPaid', function(source, amount, method)
print(('%s tipped $%d (%s)'):format(GetPlayerName(source), amount, method))
end)
InteractRange of a station's stopCoord (nearly stopped) and presses E.FullTankThreshold? He says so and walks back. No charge.RefundOnDrop) is credited the next time the player talks to an attendant, and the attendant mentions it.One player is served per attendant at a time. Nearby players see the nozzle and rope and hear his voice within VoiceRelayRadius. If a client crashes mid-service, the claim expires after StationClaimTimeoutMs.
Every voice line is an .mp3 in html/sounds/, played with distance-based volume and left/right panning from the attendant's position. To replace a line, drop a new file in with the same file name. Set Config.UseVoiceAudio = false to run with on-screen captions only.
| Group | Files |
|---|---|
| Greetings | attendant_greeting, _rain, _morning, _evening, _night, _lowfuel, _returning, _returning_regular, _dropped |
| Greeting replies | attendant_resp_good, _tired, _skip, _lowfuel_close, _lowfuel_fill, _return, _return_skip, _regular_yes, _regular_change, _dropped_sorry, _dropped_skip |
| Window & engine | attendant_window_request/nag/thanks, attendant_engine_request/nag/thanks/giveup |
| Fuel & payment | attendant_fill_prompt, attendant_ack_quarter/half/full/topoff, attendant_tank_full, attendant_payment_prompt/cash_ok/card_ok/declined/insufficient |
| Tips & goodbye | attendant_tip_prompt/thanks/none/failed, attendant_farewell |
| Small talk | attendant_smalltalk_01 to 05, each with _resp_a and _resp_b |
| Phone | phone_ring, attendant_phone_chatter_01 to 12, attendant_phone_bye_01/02/03, attendant_phone_interrupt, attendant_phone_wrapup |
| Drive-off | attendant_driveoff |
| Command | Description |
|---|---|
/toggleengine | Starts/stops the engine of the vehicle you drive. Bound to J by default (rebindable in FiveM keybinds). While the attendant is fueling, the engine can't be started. |
/fuelmoney [reset] | Standalone test wallet only: shows your fake cash and bank balance, or resets it. |
/testphone [station] | Only when Config.Debug = true. Forces the attendant at the given (or closest) station to get a phone call right now. |
visits.json (resource folder): visit count per player and per station, keyed by framework identifier or license. Cleared on resource stop when ResetVisitsOnStop = true.You must be in the driver seat, nearly stopped, and within InteractRange of the station's stopCoord. Also check the attendant is loaded: he's spawned by the server, so OneSync must be enabled.
Check the server console for errors and confirm OneSync is on. The server respawns a missing attendant automatically within about 10 seconds.
Config.FuelSystem doesn't match the resource your HUD reads. Set it to "ox_fuel", "LegacyFuel", "cdn-fuel", or use "hook".
On "standalone" payment is skipped unless Config.TestMoney.Enabled = true. On other frameworks check Config.Payment.Enabled.
Check Config.UseVoiceAudio and that the mp3 files are in html/sounds/ with the exact names listed above. A missing file just skips that line.
Config.ResetVisitsOnStop is true, or visits.json is missing or not writable.
That's expected until you add code to Hooks.OnTip in custom_functions.lua to send the money somewhere.
Still stuck? Contact us or ask in the release thread on the Cfx.re forum.