Config (config/config.lua)
x-garage is configured from a single file: config/config.lua.
shared_script "config/config.lua"
dependencies { "es_extended", "oxmysql" }
The shipped file has 55 garages and 16 impounds already placed across the map and contains no comments at all — this page is the explanation that goes with it.
Restart the resource after every change. Coordinates, blips and NPCs are read once on start.
General
XGARAGE.debug = false
XGARAGE.ignoreInvalidVehicles = true
| Option | Type | Default | Description |
|---|---|---|---|
debug | boolean | false | Extra console output while you place garages or hunt down a problem. Off in production |
ignoreInvalidVehicles | boolean | true | Hides vehicles whose model does not exist on the server (removed addon cars, typos in the database) instead of listing entries the player can never spawn |
Interface text
XGARAGE.ui = {
title = "X-GARAGE",
subtitle = "Fahrzeug Garage",
hint = "Hier siehst du alle deine Fahrzeuge auf einen Blick",
}
| Option | Where it appears |
|---|---|
title | Large heading of the menu — your server or garage name |
subtitle | Smaller line underneath |
hint | Explanatory line in the header |
The defaults are German. An English set:
XGARAGE.ui = {
title = "X-GARAGE",
subtitle = "Vehicle Garage",
hint = "All of your vehicles at a glance",
}
Prices
Three separate costs, each with its own table:
XGARAGE.price = 500
XGARAGE.refuel = {
enabled = true,
tankSize = 65,
pricePerLiter = 8,
}
XGARAGE.repairCost = {
enabled = true,
min = 100,
max = 1000,
}
| Option | Default | Description |
|---|---|---|
price | 500 | Flat fee for releasing a vehicle from the impound. Set 0 to make it free |
refuel.enabled | true | Allows refuelling from the menu. false removes the option |
refuel.tankSize | 65 | Tank size in litres used to convert a fuel percentage into litres |
refuel.pricePerLiter | 8 | Price per litre. A car at 50 % costs 65 × 0.5 × 8 = 260 |
repairCost.enabled | true | Allows repairing from the menu |
repairCost.min / max | 100 / 1000 | Price range for a repair — a lightly scratched car sits near min, a wreck near max |
If you price a refuel differently — a discount for a job, a fuel price that follows your economy — override bridge.refuelPrice instead of changing these numbers.
Vehicle images
XGARAGE.vehicleImages = {
cdn = "https://strada.x-studios.dev/coinshop/%s.png",
fallback = "https://docs.fivem.net/vehicles/%s.webp",
nui = "./images/vehicles/%s.webp",
}
Each entry is a URL template; the %s is replaced with the vehicle's model name (adder, sultan2, …).
| Source | Used for |
|---|---|
cdn | The primary image source |
fallback | Used when the CDN has no picture for that model — the official FiveM vehicle images |
nui | Local files inside the resource, relative to frontend/, i.e. frontend/images/vehicles/<model>.webp |
For addon vehicles the local path is the reliable one: drop mycar.webp into frontend/images/vehicles/ and it is picked up under that model name. The files { "frontend/**/*" } rule in fxmanifest.lua ships anything you add there.
Vehicle classes
XGARAGE.classes = {
[0] = "Compacts",
[1] = "Sedans",
[2] = "SUVs",
…
[21] = "Trains"
}
The labels for GTA's 22 vehicle classes, used as category names in the menu. The index is the class id the game reports, so keep the numbers as they are and only translate the text.
Blips
XGARAGE.blips = {
garage_car = {
enabled = true,
sprite = 524,
scale = 0.7,
color = 0,
label = "Garage"
},
…
}
Blips are defined once as named presets and every garage then points at one of them by name. Ten presets ship with the resource:
| Preset | Sprite | Colour | Label |
|---|---|---|---|
garage_car | 524 | 0 | Garage |
garage_boat | 780 | 0 | Boot Garage |
garage_truck | 477 | 0 | LKW Garage |
garage_airplane | 557 | 0 | Hangar |
garage_heli | 64 | 0 | Helipad |
impound_car | 524 | 1 | Abschlepphof |
impound_boat | 780 | 1 | Boot Abschlepphof |
impound_truck | 477 | 1 | LKW Abschlepphof |
impound_airplane | 557 | 1 | Flugzeug Abschlepphof |
impound_heli | 64 | 1 | Heli Abschlepphof |
| Field | Description |
|---|---|
enabled | false hides every blip that uses this preset — the location itself keeps working |
sprite | Blip icon id from the FiveM blip list |
scale | Icon size, 0.7 is a good default |
color | Blip colour id — the garages use 0 (white), the impounds 1 (red) |
label | Name shown on the map and in the legend |
The labels are German out of the box. Because the presets are shared, translating garage_car once renames all 47 car garages at the same time.
You can add your own preset — garage_vip = { … } — and reference it from a garage by that name.
Garages
XGARAGE.garages = {
{
blip = "garage_car",
coords = vector3(213.894, -808.807, 31.014),
parkin = vector3(212.716, -797.658, 30.868),
types = { "car", "truck" },
spawns = {
vector4(231.271, -796.613, 30.573, 160.246)
}
},
…
}
Every entry in the list is one garage.
| Field | Type | Description |
|---|---|---|
blip | string | Name of a preset from XGARAGE.blips |
coords | vector3 | Where the player interacts to open the garage — this is also where the blip and the NPC are placed |
parkin | vector3 | The spot where a vehicle is stored. Drive up to it to put a car away |
types | table | Which vehicle types this location handles — see below |
spawns | table of vector4 | Where vehicles are taken out. The fourth value is the heading |
npc | table | Optional. Per-garage override for the NPC, e.g. npc = { heading = 330.0 } |
spawns is a list on purpose: add more than one vector4 and the garage can hand out vehicles even while the first spot is blocked. All 55 shipped garages come with exactly one spawn point.
Vehicle types
| Type | Covers |
|---|---|
car | Regular land vehicles |
truck | Trucks and heavy vehicles |
boat | Boats |
heli | Helicopters |
plane | Planes |
A location only lists vehicles matching its own types, which is what keeps a boat out of a multi-storey car park. The shipped distribution: 47 × {"car", "truck"}, 8 × {"truck"}, 6 × {"heli"}, 5 × {"plane"}, 5 × {"boat"}.
Combine them freely — types = { "car", "truck", "boat" } makes one location accept all three.
Impounds
XGARAGE.impounds = {
{
blip = "impound_car",
coords = vector3(-1725.3767, -911.1889, 7.6764),
types = { "car", "truck" },
spawns = {
vector4(-1719.8505, -907.4643, 7.2627, 300.4797)
}
},
…
}
Same structure as a garage with one difference: impounds have no parkin. You only take vehicles out of them, never put them in, so the field does not exist on any of the 16 shipped entries.
Releasing a vehicle costs XGARAGE.price.
Garage NPC
XGARAGE.garageNPC = {
model = "s_m_m_autoshop_02",
task = "WORLD_HUMAN_HANG_OUT_STREET",
offset = vector3(0.0, 0.0, 0.0),
}
| Option | Default | Description |
|---|---|---|
model | s_m_m_autoshop_02 | Ped model spawned at every location |
task | WORLD_HUMAN_HANG_OUT_STREET | Scenario the ped plays. Any GTA scenario name works, e.g. WORLD_HUMAN_CLIPBOARD or WORLD_HUMAN_SMOKING |
offset | vector3(0.0, 0.0, 0.0) | Shift relative to the garage coords. Useful when the ped stands inside a wall — a z of -1.0 also helps when a ped floats |
Individual locations can override this with an npc table on the garage itself; the shipped config uses that once to turn a ped towards the street:
npc = { heading = 330.0007 },
Access check
XGARAGE.checkAccess = function()
return nil
end
The hook the resource calls to decide whether the player may use the garage. Shipped as a stub that returns nil, which means no restriction — every player can use every location.
This is where a job check, a whitelist or a state bag lookup belongs, for example:
XGARAGE.checkAccess = function()
return LocalPlayer.state.hasGarageAccess
end
Since the evaluation happens inside the protected client code, verify the behaviour of your return value in game before you rely on it.
The bridge
XGARAGE.bridge is the adapter layer between the resource and the rest of your server. Everything framework-specific lives here, so nothing outside this block has to be touched when you swap a fuel or inventory script.
The ESX object is already cached at the top of the block:
local _esx
local function getESX()
if not _esx then
_esx = exports["es_extended"]:getSharedObject()
end
return _esx
end
| Function | Side | Purpose |
|---|---|---|
notify(kind, message) | Client | Notification for the local player. kind is the notification type, message the finished text |
notifyPlayer(source, kind, message) | Server | The same, sent to one player by server id |
helpKey(state, message) | Client | Shows or hides the persistent "press E" hint. state = true starts it, false stops it |
hideHud(hidden) | Client | Called when the menu opens and closes — hook your HUD resource in here |
getFuel(vehicle) | Client | Must return the vehicle's fuel level |
setFuel(vehicle, level) | Client | Writes a fuel level back after refuelling |
getTrunkWeight(modelHash) | Client | Must return the trunk capacity of a model |
openTrunk(vehicle) | Client | Opens the trunk in your inventory |
refuelPrice(currentFuel) | Client | Optional price override for a refuel |
Notifications
XGARAGE.bridge.notify = function(kind, message)
getESX().ShowNotification(message, kind)
end
XGARAGE.bridge.notifyPlayer = function(source, kind, message)
TriggerClientEvent("esx:showNotification", source, message, kind)
end
For other systems:
-- ox_lib
XGARAGE.bridge.notify = function(kind, message)
exports.ox_lib:notify({ type = kind, description = message })
end
-- QBCore
XGARAGE.bridge.notify = function(kind, message)
TriggerEvent("QBCore:Notify", message, kind)
end
Help text
XGARAGE.bridge.helpKey = function(state, message)
-- default: loops ESX.ShowHelpNotification every frame while state is true
end
The default implementation starts a thread that redraws the help notification each frame, because GTA's help text disappears on its own after a moment. If you replace it, make sure state = false really stops your display — otherwise the hint sticks on screen after the player walks away.
Fuel
XGARAGE.bridge.getFuel = function(vehicle)
return 100
end
XGARAGE.bridge.setFuel = function(vehicle, level)
end
Both are stubs: without a fuel integration every vehicle is treated as full, and refuelling changes nothing. Wire them to your fuel script:
-- LegacyFuel
XGARAGE.bridge.getFuel = function(vehicle)
return exports["LegacyFuel"]:GetFuel(vehicle)
end
XGARAGE.bridge.setFuel = function(vehicle, level)
exports["LegacyFuel"]:SetFuel(vehicle, level)
end
-- ox_fuel (state bag)
XGARAGE.bridge.getFuel = function(vehicle)
return Entity(vehicle).state.fuel or 100
end
XGARAGE.bridge.setFuel = function(vehicle, level)
Entity(vehicle).state:set("fuel", level, true)
end
Refuel price
XGARAGE.bridge.refuelPrice = function(currentFuel)
return nil
end
Return nil and the price is calculated from XGARAGE.refuel — the missing litres multiplied by pricePerLiter. Return a number and that number is used instead, which is the place for discounts or a fuel price that follows your economy:
XGARAGE.bridge.refuelPrice = function(currentFuel)
local missing = (100 - currentFuel) / 100 * XGARAGE.refuel.tankSize
return math.ceil(missing * XGARAGE.refuel.pricePerLiter * 0.5) -- 50 % off
end
Trunk and inventory
XGARAGE.bridge.getTrunkWeight = function(modelHash)
return 0
end
XGARAGE.bridge.openTrunk = function(vehicle)
end
getTrunkWeight returns the capacity shown for a vehicle, openTrunk opens the actual storage. Both are empty stubs, so the trunk stays at 0 until you connect your inventory:
-- ox_inventory
XGARAGE.bridge.openTrunk = function(vehicle)
local plate = GetVehicleNumberPlateText(vehicle)
exports.ox_inventory:openInventory("trunk", { id = "trunk" .. plate, class = GetVehicleClass(vehicle), model = GetEntityModel(vehicle) })
end
HUD
XGARAGE.bridge.hideHud = function(hidden)
end
Called with true when the menu opens and false when it closes. Hook your HUD resource in here so speedometer and status bars do not sit on top of the garage UI.
Theming
The look of the menu is not part of config.lua — it lives in frontend/config.css, a short file of CSS variables:
:root {
--main-color: rgb(255, 255, 255);
--background-base: rgb(0, 0, 0);
--background-glow: rgba(255, 255, 255, 0.15);
--favorite-color: rgb(255, 198, 86);
…
}
Change the values, keep the variable names. --main-color, --background-base and --background-glow carry most of the look; --background itself is a stack of radial gradients built from --background-glow, so recolouring the glow is usually enough to match the menu to your server's colour.