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
OptionTypeDefaultDescription
debugbooleanfalseExtra console output while you place garages or hunt down a problem. Off in production
ignoreInvalidVehiclesbooleantrueHides 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",
}
OptionWhere it appears
titleLarge heading of the menu — your server or garage name
subtitleSmaller line underneath
hintExplanatory 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,
}
OptionDefaultDescription
price500Flat fee for releasing a vehicle from the impound. Set 0 to make it free
refuel.enabledtrueAllows refuelling from the menu. false removes the option
refuel.tankSize65Tank size in litres used to convert a fuel percentage into litres
refuel.pricePerLiter8Price per litre. A car at 50 % costs 65 × 0.5 × 8 = 260
repairCost.enabledtrueAllows repairing from the menu
repairCost.min / max100 / 1000Price 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, …).

SourceUsed for
cdnThe primary image source
fallbackUsed when the CDN has no picture for that model — the official FiveM vehicle images
nuiLocal 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:

PresetSpriteColourLabel
garage_car5240Garage
garage_boat7800Boot Garage
garage_truck4770LKW Garage
garage_airplane5570Hangar
garage_heli640Helipad
impound_car5241Abschlepphof
impound_boat7801Boot Abschlepphof
impound_truck4771LKW Abschlepphof
impound_airplane5571Flugzeug Abschlepphof
impound_heli641Heli Abschlepphof
FieldDescription
enabledfalse hides every blip that uses this preset — the location itself keeps working
spriteBlip icon id from the FiveM blip list
scaleIcon size, 0.7 is a good default
colorBlip colour id — the garages use 0 (white), the impounds 1 (red)
labelName 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.

FieldTypeDescription
blipstringName of a preset from XGARAGE.blips
coordsvector3Where the player interacts to open the garage — this is also where the blip and the NPC are placed
parkinvector3The spot where a vehicle is stored. Drive up to it to put a car away
typestableWhich vehicle types this location handles — see below
spawnstable of vector4Where vehicles are taken out. The fourth value is the heading
npctableOptional. 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

TypeCovers
carRegular land vehicles
truckTrucks and heavy vehicles
boatBoats
heliHelicopters
planePlanes

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),
}
OptionDefaultDescription
models_m_m_autoshop_02Ped model spawned at every location
taskWORLD_HUMAN_HANG_OUT_STREETScenario the ped plays. Any GTA scenario name works, e.g. WORLD_HUMAN_CLIPBOARD or WORLD_HUMAN_SMOKING
offsetvector3(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
FunctionSidePurpose
notify(kind, message)ClientNotification for the local player. kind is the notification type, message the finished text
notifyPlayer(source, kind, message)ServerThe same, sent to one player by server id
helpKey(state, message)ClientShows or hides the persistent "press E" hint. state = true starts it, false stops it
hideHud(hidden)ClientCalled when the menu opens and closes — hook your HUD resource in here
getFuel(vehicle)ClientMust return the vehicle's fuel level
setFuel(vehicle, level)ClientWrites a fuel level back after refuelling
getTrunkWeight(modelHash)ClientMust return the trunk capacity of a model
openTrunk(vehicle)ClientOpens the trunk in your inventory
refuelPrice(currentFuel)ClientOptional 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.