Dealerships
Configurables
All config & open sourced files included within this script.
Config Files
--[[
BY RX Scripts © rxscripts.xyz
--]]
Config = {}
Config.Locale = 'en'
Config.SaveInterval = 10 -- Minutes (Set to 'false' to disable saving on interval, only on server shutdown)
Config.ShowVehicleImages = true -- Show vehicle images in the dealerships menus
Config.VehicleImage = {
host = '', -- Custom image host URL (e.g., 'https://my-cdn.com/vehicles'). Leave empty to skip. Fetched server-side for security.
extension = 'png', -- File extension for custom host images (png, webp, jpg, etc.)
}
Config.MoneyTypes = { -- Account names from your framework
money = 'money',
bank = 'bank',
}
Config.TestDrive = {
duration = 120, -- Test drive duration in seconds (2 minutes)
}
Config.Plate = {
--[[
Plate Template Configuration
The plate will be generated using the template below.
Available placeholders:
- {LETTERS:n} = n random uppercase letters (e.g., {LETTERS:3} = "ABC")
- {NUMBERS:n} = n random digits (e.g., {NUMBERS:3} = "123")
- {CHECK} = 2-digit check number (random 00-99)
- {RANDOM:n} = n random alphanumeric characters
Example templates:
- "{LETTERS:3} {NUMBERS:3}" = "ABC 123" (default)
- "{LETTERS:2}{NUMBERS:4}" = "AB1234"
- "{NUMBERS:2}-{LETTERS:3}-{NUMBERS:2}" = "12-ABC-34"
- "{RANDOM:8}" = "A1B2C3D4"
- "RX-{NUMBERS:4}" = "RX-1234"
--]]
template = "{LETTERS:3} {NUMBERS:3}",
maxRetries = 100, -- Maximum attempts to generate a unique plate
}
Config.DisplayVehicleSpawnDistance = 200.0 -- Distance in meters to spawn/despawn display vehicles
Config.Finance = {
intervalMinutes = 60, -- Real minutes between payments (default 1 hour)
maxMissedPayments = 3, -- Missed payments before vehicle is defaulted/repossessed
repossessOnDefault = true, -- Delete vehicle from player's garage on default
}
Config.ShowRoomColors = {
-- Example format: { label = 'ColorName', displayRGB = {r, g, b}, primaryIndex = X, secondaryIndex = Y }
-- Color indexes reference: https://pastebin.com/pwHci0xK
{ label = 'Red', displayRGB = {200, 25, 25}, primaryIndex = 27, secondaryIndex = 27 },
{ label = 'Pink', displayRGB = {230, 80, 150}, primaryIndex = 137, secondaryIndex = 137 },
{ label = 'Orange', displayRGB = {240, 130, 20}, primaryIndex = 38, secondaryIndex = 38 },
{ label = 'Yellow', displayRGB = {240, 200, 30}, primaryIndex = 88, secondaryIndex = 88 },
{ label = 'Green', displayRGB = {30, 150, 50}, primaryIndex = 92, secondaryIndex = 92 },
{ label = 'Teal', displayRGB = {0, 180, 180}, primaryIndex = 70, secondaryIndex = 70 },
{ label = 'Blue', displayRGB = {30, 80, 200}, primaryIndex = 64, secondaryIndex = 64 },
{ label = 'Purple', displayRGB = {100, 20, 150}, primaryIndex = 145, secondaryIndex = 145 },
{ label = 'White', displayRGB = {255, 255, 255}, primaryIndex = 111, secondaryIndex = 111 },
{ label = 'Silver', displayRGB = {192, 192, 192}, primaryIndex = 4, secondaryIndex = 4 },
{ label = 'Grey', displayRGB = {100, 100, 100}, primaryIndex = 6, secondaryIndex = 6 },
{ label = 'Black', displayRGB = {10, 10, 10}, primaryIndex = 0, secondaryIndex = 0 },
}
--[[
YOU CAN USE ACE PERMISSIONS TO ALLOW CERTAIN PLAYERS/GROUPS TO ACCESS THE ADMIN DEALERSHIPS PANEL
EXAMPLE:
add_ace group.admin dealerships allow
add_ace identifier.fivem:1432744 dealerships allow #Rejox
OR YOU CAN USE THE STAFF GROUPS/JOBS BELOW
--]]
Config.AllowedToCreate = {
jobs = {
-- 'real_estate_agent',
},
groups = {
'admin',
'superadmin',
}
}
Config.Commands = {
admin = 'dealerships:admin', -- Opens the admin panel
}
Config.UI = {
color = {
primary = { -- Different shades of primary color
[50] = "#FEDDE9",
[100] = "#FCBAD3",
[200] = "#FA76A6",
[300] = "#F7317A",
[400] = "#D80955",
[500] = "#95063B",
[600] = "#76052E",
[700] = "#580423",
[800] = "#3B0217",
[900] = "#1D010C",
[950] = "#0F0106"
},
}
}
--[[
ONLY CHANGE THIS PART IF YOU HAVE RENAMED SCRIPTS SUCH AS FRAMEWORK, TARGET, INVENTORY ETC
RENAME THE SCRIPT NAME TO THE NEW NAME
--]]
---@type table Only change these if you have changed the name of a resource
Resources = {
FM = { name = 'fmLib', export = 'new' },
OXTarget = { name = 'ox_target', export = 'all' },
QBTarget = { name = 'qb-target', export = 'all' },
}
IgnoreScriptFoundLogs = false
ShowDebugPrints = true
Opensource Files
All script-related open source code is contained within these files. Third-party components, including frameworks, inventory systems, and other external code, are separately maintained & open sourced in our fmLib repository.
--[[
BY RX Scripts © rxscripts.xyz
--]]
function GetVehicleCategoryFromClass(classId)
local classMap = {
[0] = "compacts",
[1] = "sedans",
[2] = "suvs",
[3] = "coupes",
[4] = "muscle",
[5] = "sports classics",
[6] = "sports",
[7] = "super",
[8] = "motorcycles",
[9] = "off-road",
[10] = "industrial",
[11] = "utility",
[12] = "vans",
[13] = "cycles",
[14] = "boats",
[15] = "helicopters",
[16] = "planes",
[17] = "service",
[18] = "emergency",
[19] = "military",
[20] = "commercial",
[21] = "trains",
[22] = "open wheel"
}
return classMap[classId] or nil
end
function GetVehicleCategoryFromModel(modelName)
if not modelName or modelName == "" then
return nil
end
local modelHash = joaat(modelName)
if not IsModelInCdimage(modelHash) or not IsModelAVehicle(modelHash) then
return nil
end
RequestModel(modelHash)
local timeout = 0
while not HasModelLoaded(modelHash) and timeout < 500 do
Wait(10)
timeout = timeout + 10
end
if not HasModelLoaded(modelHash) then
return nil
end
local vehicleClass = GetVehicleClassFromName(modelHash)
SetModelAsNoLongerNeeded(modelHash)
return GetVehicleCategoryFromClass(vehicleClass)
end
function ShowMarker(type, coords)
if type == 'managementMenu' then
DrawMarker(2, coords, 0, 0, 0, 0, 180.0, 0, 0.3, 0.3, 0.3, 204, 0, 102, 100, false, false, 2, true, false, false, false)
elseif type == 'serverDealership' then
DrawMarker(2, coords, 0, 0, 0, 0, 180.0, 0, 0.3, 0.3, 0.3, 52, 152, 219, 100, false, false, 2, true, false, false, false)
elseif type == 'playerDealership' then
DrawMarker(2, coords, 0, 0, 0, 0, 180.0, 0, 0.3, 0.3, 0.3, 46, 204, 113, 100, false, false, 2, true, false, false, false)
elseif type == 'tebexDealership' then
DrawMarker(2, coords, 0, 0, 0, 0, 180.0, 0, 0.3, 0.3, 0.3, 155, 89, 182, 100, false, false, 2, true, false, false, false)
end
end
function AddDisplayVehicleTarget(vehicle, vehicleData, dealership)
if not vehicle or not DoesEntityExist(vehicle) then return end
local displayId = vehicleData.id
local label, canInteract = GetDisplayVehicleLabel(vehicleData)
local icon = canInteract and 'fas fa-shopping-cart' or 'fas fa-ban'
if OXTarget then
OXTarget:addLocalEntity(vehicle, {
{
label = label,
name = 'display_vehicle_' .. displayId,
icon = icon,
distance = 2.5,
onSelect = function(data)
if canInteract then
OnDisplayVehicleInteract(vehicleData, dealership)
else
FM.utils.notify(label, 'error')
end
end
}
})
DisplayVehicleTargets[displayId] = true
elseif QBTarget then
QBTarget:AddTargetEntity(vehicle, {
options = {
{
label = label,
icon = icon,
action = function(entity)
if canInteract then
OnDisplayVehicleInteract(vehicleData, dealership)
else
FM.utils.notify(label, 'error')
end
end
}
},
distance = 2.5
})
DisplayVehicleTargets[displayId] = true
end
end
function RemoveDisplayVehicleTarget(displayId)
local vehicle = SpawnedDisplayVehicles[displayId]
if not vehicle or not DoesEntityExist(vehicle) then
DisplayVehicleTargets[displayId] = nil
return
end
if OXTarget then
OXTarget:removeLocalEntity(vehicle, { 'display_vehicle_' .. displayId })
elseif QBTarget then
QBTarget:RemoveTargetEntity(vehicle)
end
DisplayVehicleTargets[displayId] = nil
end
function GetShowroomVehicleSpecs(vehicle)
local model = GetEntityModel(vehicle)
local class = GetVehicleClassFromName(model)
local speedMult = 3.6
local isBikeOrCycle = (class == 8 or class == 13 or class == 14 or class == 15 or class == 16)
local topSpeed
if isBikeOrCycle then
topSpeed = math.ceil(GetVehicleModelEstimatedMaxSpeed(model) * speedMult * 0.85)
else
topSpeed = math.ceil(GetVehicleModelEstimatedMaxSpeed(model) * speedMult)
end
local baseAccel = GetVehicleModelAcceleration(model)
local acceleration = baseAccel * 0.95
local baseBraking = GetVehicleModelMaxBraking(model)
local braking
if isBikeOrCycle then
braking = baseBraking * 0.12
else
braking = baseBraking
end
local traction = GetVehicleModelMaxTraction(model)
return {
topSpeed = topSpeed,
acceleration = acceleration,
braking = braking,
traction = traction,
seats = GetVehicleMaxNumberOfPassengers(vehicle) + 1,
}
end
--[[
EVENTS
--]]
RegisterNetEvent('rxdealerships:onTestDriveStarted', function(dealershipId, vehicleModel)
-- Called when a player starts a test drive. Use for custom logic.
end)
RegisterNetEvent('rxdealerships:onTestDriveEnded', function(dealershipId)
-- Called when a player ends a test drive. Use for custom logic.
end)
--[[
BY RX Scripts © rxscripts.xyz
--]]
Config.DiscordWebhook = ''
---@param title string
---@param fields {name: string, value: string, inline: boolean}[]
---@param color number
function Log(title, fields, color)
-- You can modify this function to send logs to your preferred service
-- Default implementation uses Discord webhooks
ToDiscord(title, fields, color)
end
-- Get vehicles from the active framework
function GetVehiclesFromFramework()
local vehicles = {}
if GetResourceState('es_extended') == 'started' then
local result = MySQL.query.await("SELECT model, name, category, price FROM vehicles")
if result then
for _, v in ipairs(result) do
vehicles[v.model] = {
model = v.model,
name = v.name,
category = v.category or 'compacts',
default_price = v.price or 0,
default_finance_price = math.floor((v.price or 0) * 1.15),
default_order_price = math.floor((v.price or 0) * 0.7),
default_delivery_time = 0,
default_stock = 10,
default_stock_reset_interval_hours = 24,
global_stock = 30,
global_stock_reset = 24,
}
end
end
elseif GetResourceState('qb-core') == 'started' then
local QBCore = exports['qb-core']:GetCoreObject()
if QBCore and QBCore.Shared and QBCore.Shared.Vehicles then
for model, data in pairs(QBCore.Shared.Vehicles) do
vehicles[model] = {
model = model,
name = data.name or data.label or model,
category = data.category or 'compacts',
default_price = data.price or 0,
default_finance_price = math.floor((data.price or 0) * 1.15),
default_order_price = math.floor((data.price or 0) * 0.7),
default_delivery_time = 0,
default_stock = 10,
default_stock_reset_interval_hours = 24,
global_stock = 30,
global_stock_reset = 24,
}
end
end
elseif GetResourceState('qbx_core') == 'started' then
local success, vehicles_data = pcall(function()
return exports.qbx_core:GetVehicles()
end)
if success and vehicles_data then
for model, data in pairs(vehicles_data) do
vehicles[model] = {
model = model,
name = data.name or data.label or model,
category = data.category or 'compacts',
default_price = data.price or 0,
default_finance_price = math.floor((data.price or 0) * 1.15),
default_order_price = math.floor((data.price or 0) * 0.7),
default_delivery_time = 0,
default_stock = 10,
default_stock_reset_interval_hours = 24,
global_stock = 30,
global_stock_reset = 24,
}
end
end
end
return vehicles
end
--[[
PLATE GENERATION SYSTEM
]]
--- Generate a license plate from template
---@param template? string The plate template (defaults to Config.Plate.template)
---@return string Generated plate
function GeneratePlate(template)
template = template or Config.Plate.template
local plate = template
plate = plate:gsub("{LETTERS:(%d+)}", function(length)
local len = tonumber(length)
local letters = ""
for i = 1, len do
letters = letters .. string.char(math.random(65, 90))
end
return letters
end)
plate = plate:gsub("{NUMBERS:(%d+)}", function(length)
local len = tonumber(length)
local maxNum = tonumber(string.rep("9", len))
return string.format("%0" .. len .. "d", math.random(0, maxNum))
end)
plate = plate:gsub("{CHECK}", function()
return string.format("%02d", math.random(0, 99))
end)
plate = plate:gsub("{RANDOM:(%d+)}", function(length)
local len = tonumber(length)
local chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
local result = ""
for i = 1, len do
local idx = math.random(1, #chars)
result = result .. chars:sub(idx, idx)
end
return result
end)
return plate
end
--- Check if a plate already exists in the database
---@param plate string The plate to check
---@return boolean True if plate exists
function CheckPlateExists(plate)
local result
if GetResourceState('es_extended') == 'started' then
result = MySQL.scalar.await("SELECT 1 FROM owned_vehicles WHERE plate = ?", { plate })
elseif GetResourceState('qb-core') == 'started' or GetResourceState('qbx_core') == 'started' then
result = MySQL.scalar.await("SELECT 1 FROM player_vehicles WHERE plate = ?", { plate })
end
return result ~= nil
end
--- Generate a unique plate with retry logic
---@return string|nil Generated unique plate or nil if max retries exceeded
function GenerateUniquePlate()
local maxRetries = Config.Plate.maxRetries or 100
for i = 1, maxRetries do
local plate = GeneratePlate()
if not CheckPlateExists(plate) then
return plate
end
end
return nil
end
--[[
VEHICLE GARAGE SYSTEM
]]
--- Save vehicle to player's garage (framework-specific)
---@param src number Player source
---@param model string Vehicle model name
---@param plate string Vehicle plate
---@param mods? table Vehicle modifications (primaryColor, secondaryColor)
---@return boolean Success
function SaveVehicleToGarage(src, model, plate, mods)
local p = FM.player.get(src)
if not p then return false end
local identifier = p.getIdentifier()
local modelHash = type(model) == 'string' and GetHashKey(model) or model
local vehicleProps = {
model = modelHash,
plate = plate,
}
if mods then
if mods.primaryColor then
vehicleProps.color1 = mods.primaryColor
end
if mods.secondaryColor then
vehicleProps.color2 = mods.secondaryColor
end
end
local propsJson = json.encode(vehicleProps)
if GetResourceState('es_extended') == 'started' then
local success = MySQL.insert.await([[
INSERT INTO owned_vehicles (owner, plate, vehicle)
VALUES (?, ?, ?)
]], { identifier, plate, propsJson })
if success then return true end
elseif GetResourceState('qb-core') == 'started' then
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return false end
local citizenid = Player.PlayerData.citizenid
local license = Player.PlayerData.license
local success = MySQL.insert.await([[
INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, garage, state)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
]], { license, citizenid, model, modelHash, propsJson, plate, 'pillboxgarage', 0 })
if success then return true end
elseif GetResourceState('qbx_core') == 'started' then
local Player = exports.qbx_core:GetPlayer(src)
if not Player then return false end
local citizenid = Player.PlayerData.citizenid
local success = MySQL.insert.await([[
INSERT INTO player_vehicles (citizenid, vehicle, hash, mods, plate, garage, state)
VALUES (?, ?, ?, ?, ?, ?, ?)
]], { citizenid, model, modelHash, propsJson, plate, 'pillboxgarage', 0 })
if success then return true end
end
return false
end
function RemoveMoneyFromBankSQL(identifier, amount)
local table = GetResourceState('es_extended') == 'started' and 'users' or 'players'
local accountsColumn = GetResourceState('es_extended') == 'started' and 'accounts' or 'money'
local identifierColumn = GetResourceState('es_extended') == 'started' and 'identifier' or 'citizenid'
return MySQL.update.await(
string.format('UPDATE %s SET %s = JSON_SET(%s, "$.bank", JSON_EXTRACT(%s, "$.bank") - @amount) WHERE %s = @identifier',
table, accountsColumn, accountsColumn, accountsColumn, identifierColumn),
{
['@amount'] = amount,
['@identifier'] = identifier,
}
)
end
function GetMoneyFromBankSQL(identifier)
local table = GetResourceState('es_extended') == 'started' and 'users' or 'players'
local accountsColumn = GetResourceState('es_extended') == 'started' and 'accounts' or 'money'
local identifierColumn = GetResourceState('es_extended') == 'started' and 'identifier' or 'citizenid'
local result = MySQL.query.await(
string.format('SELECT JSON_UNQUOTE(JSON_EXTRACT(%s, "$.bank")) AS bank FROM %s WHERE %s = @identifier',
accountsColumn, table, identifierColumn),
{
['@identifier'] = identifier,
}
)
if not result or not result[1] then return 0 end
return tonumber(result[1].bank) or 0
end
--[[
EVENTS
--]]
RegisterNetEvent('rxdealerships:onVehiclePurchased', function(src, dealershipId, vehicleModel, plate, price, paymentType)
-- Called when a player purchases a vehicle (cash/bank). Use for custom logic.
end)
RegisterNetEvent('rxdealerships:onVehicleFinanced', function(src, dealershipId, vehicleModel, plate, financePrice, installment, totalPayments)
-- Called when a player finances a vehicle. Use for custom logic.
end)
RegisterNetEvent('rxdealerships:onFinancePaymentProcessed', function(buyer, financeId, paymentAmount, paymentsMade, totalPayments, status)
-- Called when an automatic finance payment is processed.
end)
RegisterNetEvent('rxdealerships:onTebexVehiclePickedUp', function(src, dealershipId, vehicleModel, plate)
-- Called when a player picks up a Tebex vehicle.
end)
RegisterNetEvent('rxdealerships:onDealershipCreated', function(dealership)
-- Called when a new dealership is created.
end)
RegisterNetEvent('rxdealerships:onDealershipDeleted', function(dealershipId, dealershipName, dealershipType)
-- Called when a dealership is deleted.
end)
RegisterNetEvent('rxdealerships:onBalanceChanged', function(src, dealershipId, action, amount, newBalance)
-- Called when a dealership balance changes (deposit/withdraw).
end)
RegisterNetEvent('rxdealerships:onOrderPlaced', function(src, dealershipId, orders, totalCost)
-- Called when vehicle orders are placed by a dealership owner.
end)
RegisterNetEvent('rxdealerships:onOrderDelivered', function(dealershipId, orderId, vehicleModel, quantity)
-- Called when an order is delivered to a player-owned dealership.
end)
RegisterNetEvent('rxdealerships:onDealershipStatusChanged', function(src, dealershipId, isOpen)
-- Called when a dealership is opened or closed.
end)
