Documentation

RD-Fuel v1.

An NPC gas station attendant who walks over, talks, fuels your car and takes payment. Install, configure, and hook it into your server.

Overview

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.

Installation

Requirements

  • A recent FiveM server artifact
  • OneSync (the attendants are spawned by the server and shared between players)
  • Optional: ESX, QBCore or QBox, and ox_fuel / LegacyFuel / cdn-fuel

Steps

  1. Drop the resource folder into your resources directory and keep the folder name rd-fuel.
  2. Make sure the voice files are in html/sounds/ and that visits.json exists in the resource folder (it can contain just {}). The visit history is saved there.
  3. Start it after your framework and fuel resource in server.cfg:
ensure es_extended   # or qb-core / qbx_core
ensure ox_fuel       # or LegacyFuel / cdn-fuel (optional)
ensure rd-fuel
  1. Open config.lua and set Config.Framework and Config.FuelSystem (see below).
  2. Set 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.

Configuration reference

Everything lives in config.lua. Defaults below are the values shipped in the file.

Core

KeyDefaultWhat it does
Framework"standalone""auto", "standalone", "esx", "qbcore", "qbox" or "hook". See Framework & payment.
FuelSystem"native""native", "ox_fuel", "LegacyFuel", "cdn-fuel" or "hook".
DebugtrueEnables 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.
ResetVisitsOnStoptrueWipes the saved visit history when the resource stops/restarts. Good for testing, set false on a real server so attendants remember players.

Attendant & station

KeyDefaultWhat it does
AttendantModels_m_y_xmech_02Ped model of the attendant.
NozzlePropprop_cs_fuel_nozleNozzle prop he carries and attaches to the car.
EnableRopetrueDraws the rope between the station and the nozzle.
PedHeightOffset1.0Lowers the ped so he sits on the ground properly while idling.
InteractControl38Key that calls the attendant (38 = E / X on controller).
InteractPromptText"Call the attendant over"Text of the on-screen prompt.
InteractRange3.0How close the parked vehicle must be to the station's stopCoord to show the prompt.
DriverWindowIndex0Which window the attendant talks through (0 = driver side).
RequireWindowDowntrueHe asks you to roll the window down and does it when you accept.
AutoRollDownWindowfalseRolls the window down silently instead of asking. Ignored if RequireWindowDown is true.
RegularVisitThreshold5Visits to one station before he treats you as a regular.

Stations

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),
    },
}

Fuel amounts

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

Fuel behaviour

KeyDefaultWhat it does
FullTankThreshold90.0At or above this %, the attendant refuses to fill and doesn't charge.
LowFuelThreshold15.0Below this %, he greets you with the "running on fumes" line.
MinFillDurationMs10000A small top-off never takes less than this.

Payment

Config.Payment = {
    Enabled           = true,
    Currency          = "$",
    AllowCash         = true,
    AllowCard         = true,
    CardDeclineChance = 0.0,    -- 0.0 - 1.0
    CashAccount       = "cash",
    CardAccount       = "bank",
}
KeyWhat it does
Enabledfalse = fuel is free on every framework.
AllowCash / AllowCardWhich payment buttons are offered.
CardDeclineChanceChance a card payment fails (checked server-side). Handy for testing.
CashAccount / CardAccountAccount each method draws from. ESX: "cash" uses the cash balance, anything else is a named account. QBCore/QBox: passed as the money type.

Tips

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.

Refund when a player drops mid-fill

Config.RefundOnDrop = {
    Enabled  = true,
    Prorated = true,   -- true = refund only the unfinished part, false = full price
}

Shared attendants

KeyDefaultWhat it does
StationClaimTimeoutMs420000A claim older than this can be taken over by another player (crash safety).
VoiceRelayRadius20.0Players this close to the attendant hear his voice lines while he serves someone else.

Voice & phone

KeyDefaultWhat it does
UseVoiceAudiotruefalse = captions only, no mp3 playback.
AttendantVoiceVolume0.85Master volume of his voice lines.
PhoneCallChance0.15Chance 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.
PhoneTalkDelayMs3000Pause 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.

Standalone test wallet

Config.TestMoney = {
    Enabled = true,
    Cash = 100,
    Bank = 500,
}

Only used when the framework resolves to standalone. See Framework & payment.

Framework & payment

Config.FrameworkBehaviour
"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.

Standalone test wallet

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.

Fuel systems

Config.FuelSystemHow 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.

Hooks (custom_functions.lua)

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".

Server hooks

HookReturnWhen it runs
GetIdentifier(source)string or nilStable player id used for visit history. nil falls back to the license.
GetBalance(source, account)numberBalance check. account is "cash" or "bank" (from CashAccount / CardAccount).
RemoveMoney(source, account, amount, reason)true if removedCharges the player for fuel and tips.
AddMoney(source, account, amount, reason)true on successUsed 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

Client hooks

HookDescription
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.

Events

Other resources can listen to these without touching RD-Fuel.

EventSideArguments
rdfuel:fillStartedclientvehicle, fill
rdfuel:fillCompletedclientvehicle, fill
rdfuel:tipPaidserversource, amount, method
rdfuel:refundPaidserversource, amount, method
AddEventHandler('rdfuel:tipPaid', function(source, amount, method)
    print(('%s tipped $%d (%s)'):format(GetPlayerName(source), amount, method))
end)

How a service works

  1. The player parks in the driver's seat within InteractRange of a station's stopCoord (nearly stopped) and presses E.
  2. The server gives that attendant to the player. Other players see "The attendant is busy" until he's free.
  3. If he's on the phone he finishes the call first ("Hold on a sec..."), then walks to the driver window.
  4. Tank already at or above FullTankThreshold? He says so and walks back. No charge.
  5. Window request, greeting (rain, time of day, low fuel, returning, regular or "you vanished"), then the engine-off request.
  6. The player picks the amount, then pays cash or card. If payment fails, no fuel is added.
  7. He hooks the nozzle to the fuel cap, walks back to his spot and chats while the fuel ramps up.
  8. He returns, removes the nozzle, offers the tip, says goodbye and goes back to idle. The engine unlocks once the nozzle is off.

Leaving mid-service

  • Before the fill is halfway done: moving the car away or leaving aborts the service. The attendant reacts, cleans up, and the unfinished part is refunded.
  • After the halfway point: the fill always completes, and driving off while he walks back does not trigger anything.
  • Disconnect or crash mid-fill: the refund (prorated or full, see RefundOnDrop) is credited the next time the player talks to an attendant, and the attendant mentions it.

Shared attendants

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.

Audio

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.

GroupFiles
Greetingsattendant_greeting, _rain, _morning, _evening, _night, _lowfuel, _returning, _returning_regular, _dropped
Greeting repliesattendant_resp_good, _tired, _skip, _lowfuel_close, _lowfuel_fill, _return, _return_skip, _regular_yes, _regular_change, _dropped_sorry, _dropped_skip
Window & engineattendant_window_request/nag/thanks, attendant_engine_request/nag/thanks/giveup
Fuel & paymentattendant_fill_prompt, attendant_ack_quarter/half/full/topoff, attendant_tank_full, attendant_payment_prompt/cash_ok/card_ok/declined/insufficient
Tips & goodbyeattendant_tip_prompt/thanks/none/failed, attendant_farewell
Small talkattendant_smalltalk_01 to 05, each with _resp_a and _resp_b
Phonephone_ring, attendant_phone_chatter_01 to 12, attendant_phone_bye_01/02/03, attendant_phone_interrupt, attendant_phone_wrapup
Drive-offattendant_driveoff

Commands

CommandDescription
/toggleengineStarts/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.

Saved data

  • visits.json (resource folder): visit count per player and per station, keyed by framework identifier or license. Cleared on resource stop when ResetVisitsOnStop = true.
  • Server KVP: pending refunds and the "player vanished mid-fill" flag, per license.
  • Client KVP: the small-talk history, so he doesn't repeat the same topic order after a restart.

Troubleshooting

The "Call the attendant over" prompt doesn't show

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.

No attendant in the world

Check the server console for errors and confirm OneSync is on. The server respawns a missing attendant automatically within about 10 seconds.

Fuel is added but my HUD doesn't change

Config.FuelSystem doesn't match the resource your HUD reads. Set it to "ox_fuel", "LegacyFuel", "cdn-fuel", or use "hook".

Fuel is free / no payment prompt

On "standalone" payment is skipped unless Config.TestMoney.Enabled = true. On other frameworks check Config.Payment.Enabled.

No voice

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.

He always greets me as a first-time visitor

Config.ResetVisitsOnStop is true, or visits.json is missing or not writable.

Tips disappear

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.