Download from KEYMASTER and Unzip the forge-starter.pack.zip and place this folder in your server's resource folder.
Add the resource to your server start config: ensure forge-starter,the name of the folder must not be changed or the resource will not function correctly.
Install the SQL that comes with the script.
Clear the cache of your server and also of your own FiveM.
Reboot the entire server with the forge script well ensured in your server.cfg.
Do not rename this script, this may cause it to fail when opening the interface.
CONFIG
The following will explain all the settings, one of the most important things that I recommend you spend a few minutes to understand in order to offer your users the best possible experience.
Fill all the CONFIG very carefully.
If you want to edit the aesthetics or design. You have the HTML open so you can modify the style and everything as you want.
The script is RESPONSIVE for all resolutions as well.
Config = { }
-- _____ _____ _ _ ______ _____ _____ _ _______ ___ _____ _____ _____ _ _
-- / __ \ _ | \ | || ___|_ _| __ \ | | | ___ \/ _ \_ _|_ _| _ | \ | |
-- | / \/ | | | \| || |_ | | | | \/ | | | |_/ / /_\ \| | | | | | | | \| |
-- | | | | | | . ` || _| | | | | __| | | | /| _ || | | | | | | . ` |
-- | \__/\ \_/ / |\ || | _| |_| |_\ \ |_| | |\ \| | | || | _| |_\ \_/ / |\ |
-- \____/\___/\_| \_/\_| \___/ \____/\___/\_| \_\_| |_/\_/ \___/ \___/\_| \_/
Config.Framework = 'esx' -- Choose between 'esx' or 'qbcore'
Config.SQLWrapper = 'oxmysql' -- Options: 'ghmattimysql', 'mysql-async', 'oxmysql'
Config.UseTarget = false -- If you set it to false, floating text will be displayed
Config.TargetSystem = 'qb-target' -- Choose between 'qb-target' or 'ox_target'
Config.UseStarterPack = true -- Set true to make the button appear in the UI and be usable
Config.UseRoleBasedStarter = false -- Set true to show role selection (CIVILIAN/CRIMINAL) instead of the default single starter pack
Config.UseVehicleStarter = true -- Set true to make the button appear in the UI and be usable
Config.UseGuideBook = true -- Set true to make the button appear in the UI and be usable
Config.UseGuidebookItem = false -- Set to true to enable guidebook as usable item
Config.GuidebookItem = "guidebook" -- Name of the item to use as guidebook
-- Starter Pack Options
Config.StarterPack = {
Male = {
{ Name = 'bread', Quantity = 4 },
{ Name = 'phone', Quantity = 1 }
},
Female = {
{ Name = 'bread', Quantity = 4 },
{ Name = 'phone', Quantity = 1 }
},
Money = {
cash = 1000,
bank = 1000
}
}
-- Role-Based Starter Pack Options (used when Config.UseRoleBasedStarter = true)
-- Each role has its own items, money, and gender-specific configuration
-- Only ONE role can be claimed per character
Config.StarterPackRoles = {
Civilian = {
Male = {
{ Name = 'bread', Quantity = 5 },
{ Name = 'water', Quantity = 5 },
{ Name = 'phone', Quantity = 1 }
},
Female = {
{ Name = 'bread', Quantity = 5 },
{ Name = 'water', Quantity = 5 },
{ Name = 'phone', Quantity = 1 }
},
Money = {
cash = 1500,
bank = 2000
}
},
Criminal = {
Male = {
{ Name = 'lockpick', Quantity = 2 },
{ Name = 'bread', Quantity = 3 },
{ Name = 'phone', Quantity = 1 }
},
Female = {
{ Name = 'lockpick', Quantity = 2 },
{ Name = 'bread', Quantity = 3 },
{ Name = 'phone', Quantity = 1 }
},
Money = {
cash = 2500,
bank = 500
}
}
}
-- Vehicle Starter Options
Config.VehicleStarter = {
VehicleInGarage = false, -- If false, it will be a street vehicle with keys. If true, the player owns the vehicle and it can be stored in the garage
VehicleModels = {
{ Model = "blista", Probability = 0.5 }, -- 50% chance (100% is 1.0)
{ Model = "club", Probability = 0.4 }, -- 40% chance (100% is 1.0)
{ Model = "zentorno", Probability = 0.1 } -- 10% chance (100% is 1.0)
}
}
--Add configurable garage provider and defaults
Config.Garage = {
-- Provider options:
-- 'esx_default' β ESX owned_vehicles
-- 'qbcore_default' β QBCore player_vehicles
-- 'jg_advanced_esx' β ESX + JG Advanced Garages (owned_vehicles with in_garage/garage_id)
-- 'jg_advanced_qb' β QBCore + JG Advanced Garages (player_vehicles with in_garage/garage_id)
-- 'okok_esx' β ESX + okokGarage (owned_vehicles with okok columns)
-- 'okok_qb' β QBCore + okokGarage (player_vehicles with okok columns)
-- 'cd_esx' β ESX + CD Garage (owned_vehicles with cd columns)
-- 'cd_qb' β QBCore + CD Garage (player_vehicles with cd columns)
-- 'codem_esx' β ESX + CodeM Garage (owned_vehicles with stored/parking/favorite)
-- 'codem_qb' β QBCore + CodeM Garage (player_vehicles with stored/parking/favorite)
-- 'vms_garagesv2_esx' β ESX + VMS Garages V2 (uses vms_garagesv2 exports)
-- 'vms_garagesv2_qb' β QBCore + VMS Garages V2 (uses vms_garagesv2 exports)
-- 'op_garages' β OP Garages (vehicle added as owned, player can use any garage)
Provider = 'esx_default',
Defaults = {
esx_default = {
},
qbcore_default = {
garage = 'pillboxgarage',
state = 0
},
jg_advanced_esx = {
in_garage = 1,
garage_id = 'Legion Square'
},
jg_advanced_qb = {
in_garage = 1,
garage_id = 'Legion Square'
},
okok_esx = {
parking = nil,
doorcondition = nil,
windowcondition = nil,
tyrecondition = nil,
favourite = 0,
impoundTime = nil,
location = nil,
reason = nil,
sharedwith = '[]',
vehiclename = nil
},
okok_qb = {
parking = nil,
doorcondition = nil,
windowcondition = nil,
tyrecondition = nil,
favourite = 0,
impoundTime = nil,
location = nil,
reason = nil,
sharedwith = '[]',
vehiclename = nil
},
cd_esx = {
in_garage = 1,
garage_id = 'A',
garage_type = 'car',
job_personalowned = '',
property = 0,
impound = 0,
impound_data = '',
name = nil
},
cd_qb = {
in_garage = 1,
garage_id = 'A',
garage_type = 'car',
job_personalowned = '',
property = 0,
impound = 0,
impound_data = '',
name = nil
},
codem_esx = {
stored = 1,
parking = 'Garage A',
favorite = '0'
},
codem_qb = {
stored = 1,
parking = 'Garage A',
favorite = '0'
},
vms_garagesv2_esx = {
impoundId = 'Impound1' -- Default impound ID for VMS Garages V2
},
vms_garagesv2_qb = {
impoundId = 'Impound1' -- Default impound ID for VMS Garages V2
},
op_garages = {
-- OP Garages - Vehicle is added as owned in player_vehicles/owned_vehicles
-- Player can save/retrieve from any OP Garages zone on the map
garage = 'pillboxgarage' -- Default garage name for QBCore
}
}
}
-- Vehicle Spawn Locations (supports multiple spawn points to prevent collisions)
-- If multiple players claim vehicles simultaneously, they will spawn at different locations
-- The system uses round-robin rotation to distribute vehicles across available spawn points
Config.VehicleSpawn = {
-- Multiple locations format (recommended for servers with concurrent claims):
{
position = vector3(-1040.4397, -2727.0789, 19.8712),
heading = 241.3119
},
{
position = vector3(-1046.2606, -2723.9783, 20.0318),
heading = 238.9994
},
-- Add more spawn locations below to prevent vehicle collisions:
-- {
-- position = vector3(x, y, z),
-- heading = heading_value
-- },
-- Example with multiple locations:
-- {
-- position = vector3(-1045.0, -2730.0, 19.8712),
-- heading = 241.3119
-- },
-- {
-- position = vector3(-1035.0, -2724.0, 19.8712),
-- heading = 241.3119
-- },
}
-- Backward compatibility: If you want to use the old single location format, uncomment below:
-- Config.VehicleSpawn = vector3(-1040.4397, -2727.0789, 19.8712)
-- Config.VehicleSpawnHeading = 241.3119
-- Guide Book Settings
Config.GuideBook = {
AdminPermissions = { 'god', 'admin' }, -- Users who can access admin mode and add posts in-game
Categories = { -- The categories that will appear in the GuideBook and that admins will be able to select to create posts.
{ name = 'WELCOME' },
{ name = 'TUTORIAL' },
{ name = 'JOBS' }
}
}
Config.GuidebookAnimation = {
enabled = true, -- Use animation when reading the guidebook
dict = "missheistdockssetup1clipboard@base", -- which animation dictionary to use
anim = "base", -- animation name
prop = {
enabled = true, -- Use a prop when reading the guidebook
model = "prop_novel_01", -- prop model
bone = 18905, -- bone id
position = vector3(0.12, 0.008, 0.03), -- prop position
rotation = vector3(0.0, 0.0, 0.0) -- prop rotation
}
}
-- Location Settings
Config.Location = {
position = vector3(-1039.4341, -2731.4712, 20.1695), -- Coordinate to open the UI
interactionDistance = 2.0, -- Distance to interact with the NPC
markerSettings = {
enabled = true, -- Display a marker
type = 21, -- Marker type
colorR = 255, -- Red component of marker color
colorG = 255, -- Green component of marker color
colorB = 255, -- Blue component of marker color
alpha = 200, -- Marker transparency
useCustomHeight = true, -- Activate custom height
heightOffset = 1.0 -- Units to raise the marker
},
blipSettings = {
enabled = true, -- Display a blip on the map
id = 835, -- Blip ID
color = 5, -- Blip color
scale = 0.7, -- Blip scale
display = 2, -- Blip display type
name = 'Starter Help' -- Blip name
},
npcSettings = {
enabled = true, -- Enable or disable NPC presence
heading = 209.1426, -- NPC heading
model = 'a_m_y_business_03', -- NPC model
playAnimation = true, -- Enable or disable NPC animation
animationScenario = 'WORLD_HUMAN_CLIPBOARD' -- Scenario to play
}
}
-- UI Settings
Config.UI = {
colors = { -- Change the colors of the UI
color1 = '#ff0451', -- Main color (hover buttons, borders, accents)
color2 = '#7f0027', -- Secondary color (hover text)
color3 = '#ff0451', -- Button background color
textColor = '#ffffff', -- Main text color
guidebookBackground = '#000000', -- Guidebook content area background
guidebookTextColor = '#FFFFFF' -- Text color inside the guidebook
},
translations = { -- Translates UI texts
starterp = 'STARTER PACK',
vehiclep = 'VEHICLE',
guidebookp = 'GUIDEBOOK',
starterp2 = 'STARTER PACK',
starterdesc = 'The <span>City Council</span> offers you, as an incentive to live in the city, a package of essentials to get you started in the city.',
claimitem = 'Claim',
starterveh = 'STARTER VEHICLE',
startervehdesc = 'The <span>Transport Consortium</span> will temporarily lend you a State vehicle to start moving around the city. Remember this vehicle is given to you by the state. Remembered in the voting!',
claimvehicle = 'Claim',
adminbtn = 'Admin Mode',
exitadmin = 'Exit Admin Mode',
addbtn = 'Add a new one',
guidebookt = 'GUIDEBOOK',
gpsmark = 'Gps Mark',
delete = 'Delete',
sure1 = 'Sure?',
yes1 = 'Yes',
no1 = 'No',
save1 = 'Save',
sure2 = 'Sure?',
saveconfirm = 'Yes',
no2 = 'No',
category1 = 'CATEGORY',
gpsmark2 = 'Gps Mark',
yes3 = 'Yes',
no3 = 'No',
content1 = 'Content',
alreadyClaimed = 'Already Claimed',
roleSelectTitle = 'CHOOSE YOUR PATH',
roleSelectDesc = 'The <span>City Council</span> allows you to choose your starting path. Select wisely, this choice defines your initial resources.',
civilianBtn = 'CIVILIAN',
criminalBtn = 'CRIMINAL',
civilianDesc = 'Start as a law-abiding citizen with essential supplies to build an honest life in the city.',
criminalDesc = 'Start on the other side of the law with tools of the trade and street cash.',
alreadyClaimedRole = 'You have already chosen a path.'
}
}
-- Translations for Various Messages
Config.Translations = {
openMenuText = 'Press ~INPUT_CONTEXT~ to open Starter menu',
openMenuTarget = 'Open the Starter menu',
itemMissing = 'You don\'t have the needed items',
alreadyClaimedItem = "You have already claimed the starter pack items.",
alreadyClaimedVehicle = "You have already claimed the starter vehicle.",
receivedStarterPack = 'You have received your starter pack. Check your inventory!',
receivedStarterVehicle = 'You have received a car from the State.',
gpsMark = 'A route has been marked on your GPS',
alreadyClaimed = 'You have already claimed this reward.',
alreadyClaimedRole = 'You have already chosen your starting path.',
invalidCoordinates = 'Invalid coordinates',
postCreated = 'Post created successfully.',
postUpdated = 'Post updated successfully.',
postDeleted = 'Post deleted successfully.',
guidebookOpened = 'Guidebook opened.',
noPermission = "You don't have permission to perform this action.",
adminModeEntered = 'You have entered Admin Mode. You can now add new posts or edit/delete existing ones.',
allSpawnLocationsOccupied = 'All vehicle spawn locations are currently occupied. Please try again later.',
invalidSpawnPosition = 'Error: Invalid spawn position. Please try again.'
}
-- Functions
Config.Functions = {
DrawText = function(text) -- Customize floating text UI
AddTextEntry('ALERT_MESSAGE', text)
BeginTextCommandDisplayHelp('ALERT_MESSAGE')
EndTextCommandDisplayHelp(0, false, false, -1)
end,
Notify = function(text, notifyType, length) -- Notification system (framework or custom)
-- Normalize message to string to avoid showing references like 'table: 0x..' or unexpected values
local message
if type(text) == 'string' then
message = text
elseif type(text) == 'number' then
message = tostring(text)
elseif type(text) == 'table' then
-- Try to serialize table; fallback to tostring
local ok, encoded = pcall(function() return json and json.encode(text) or nil end)
message = (ok and encoded) or tostring(text)
else
message = tostring(text or 'Notification')
end
-- Prefer framework-native notifies if available
if Config.Framework == 'esx' and ESX and ESX.ShowNotification then
ESX.ShowNotification(message)
return
end
if Config.Framework == 'qbcore' and QBCore and QBCore.Functions and QBCore.Functions.Notify then
-- QBCore: message[, type[, length]]
if notifyType ~= nil or length ~= nil then
QBCore.Functions.Notify(message, notifyType, length)
else
QBCore.Functions.Notify(message)
end
return
end
-- Fallback native notification (works on both frameworks)
BeginTextCommandThefeedPost('STRING')
AddTextComponentSubstringPlayerName(message)
EndTextCommandThefeedPostTicker(false, false)
end,
-- Add function for vehicle keys for esx and qb
-- All export calls are fully wrapped in pcall to prevent "No such export" errors
VehicleKeys = function(vehicle)
local plate = GetVehicleNumberPlateText(vehicle)
local model = GetEntityModel(vehicle)
-- Helper: safely attempt an export call, returns true if successful
local function tryExport(fn)
local ok, err = pcall(fn)
return ok
end
-- VMS Garages V2 - Uses wasabi_carlock or other key systems
if Config.Garage and (Config.Garage.Provider == 'vms_garagesv2_esx' or Config.Garage.Provider == 'vms_garagesv2_qb') then
if tryExport(function() exports.wasabi_carlock:GiveKey(plate) end) then return end
if Config.Framework == 'qbcore' then
TriggerEvent('vehiclekeys:client:SetOwner', plate)
return
end
return
end
if Config.Garage and (Config.Garage.Provider == 'okok_esx' or Config.Garage.Provider == 'okok_qb') then
TriggerServerEvent('okokGarage:GiveKeys', plate)
return
elseif Config.Garage and (Config.Garage.Provider == 'cd_esx' or Config.Garage.Provider == 'cd_qb') then
TriggerServerEvent('cd_garage:AddKeysOwnedVehicle', plate, model)
return
elseif Config.Garage and (Config.Garage.Provider == 'codem_esx' or Config.Garage.Provider == 'codem_qb') then
-- Try common key systems used with CodeM Garage
-- qb-vehiclekeys (QBCore native) - try first if using QBCore
if Config.Framework == 'qbcore' then
TriggerEvent('vehiclekeys:client:SetOwner', plate)
return
end
-- MrNewbVehicleKeys
if tryExport(function() exports.MrNewbVehicleKeys:GiveKeysByPlate(plate) end) then return end
-- qs-vehiclekeys
local vehicleName = GetDisplayNameFromVehicleModel(model)
if tryExport(function() exports['qs-vehiclekeys']:GiveKeys(plate, vehicleName) end) then return end
-- wasabi_carlock
if tryExport(function() exports.wasabi_carlock:GiveKey(plate) end) then return end
-- cd_garage as a fallback if present
TriggerServerEvent('cd_garage:AddKeysOwnedVehicle', plate, model)
return
elseif Config.Garage and Config.Garage.Provider == 'op_garages' then
-- OP Garages - uses common key systems
-- QBCore native vehicle keys
if Config.Framework == 'qbcore' then
TriggerEvent('vehiclekeys:client:SetOwner', plate)
return
end
-- ESX key systems
-- MrNewbVehicleKeys
if tryExport(function() exports.MrNewbVehicleKeys:GiveKeysByPlate(plate) end) then return end
-- qs-vehiclekeys
local vehicleName = GetDisplayNameFromVehicleModel(model)
if tryExport(function() exports['qs-vehiclekeys']:GiveKeys(plate, vehicleName) end) then return end
-- wasabi_carlock
if tryExport(function() exports.wasabi_carlock:GiveKey(plate) end) then return end
return
end
if Config.Framework == 'esx' then
-- ESX generic fallbacks for popular key systems
-- MrNewbVehicleKeys
if tryExport(function() exports.MrNewbVehicleKeys:GiveKeysByPlate(plate) end) then return end
-- qs-vehiclekeys
local vehicleName = GetDisplayNameFromVehicleModel(model)
if tryExport(function() exports['qs-vehiclekeys']:GiveKeys(plate, vehicleName) end) then return end
-- wasabi_carlock
if tryExport(function() exports.wasabi_carlock:GiveKey(plate) end) then return end
-- cd_garage fallback
TriggerServerEvent('cd_garage:AddKeysOwnedVehicle', plate, model)
elseif Config.Framework == 'qbcore' then
-- QBCore default vehicle keys (most common, try first)
TriggerEvent('vehiclekeys:client:SetOwner', plate)
end
end
}
-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-- β DISCORD LOGS CONFIGURATION β
-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
DiscordLogs = {
Enabled = false, -- Set to true to enable Discord logging
WebhookURL = '', -- Your Discord webhook URL
BotName = 'Forge Starter Logs', -- Bot display name in Discord
BotAvatar = '', -- Bot avatar URL (leave empty for default)
Color = 16711731, -- Embed color in decimal (default: pink/red #FF0033)
ServerName = 'My Server', -- Your server name (shown in embed footer)
}
-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-- β SQL AUTO-MIGRATION β
-- ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-- Automatically detects missing columns and adds them on resource start
-- so you never need to manually delete/recreate tables when the schema changes.
CreateThread(function()
-- Wait for SQL wrapper to be ready
Wait(2000)
-- Helper: run ALTER TABLE silently (IF NOT EXISTS handles duplicates natively)
local function safeAddColumn(table_, name, definition)
local ok, err = pcall(function()
SQL.Execute(
('ALTER TABLE `%s` ADD COLUMN IF NOT EXISTS `%s` %s'):format(table_, name, definition),
{}
)
end)
if ok then
print(('[forge-starter] Auto-migration: Ensured column "%s" in %s.'):format(name, table_))
else
-- Only warn on actual unexpected errors (not duplicate column)
if err and not tostring(err):find('Duplicate column') then
print(('[forge-starter] Auto-migration WARNING on column "%s": %s'):format(name, tostring(err)))
end
end
end
if Config.Framework == 'esx' then
-- ESX: ensure all required columns exist in 'users' table
safeAddColumn('users', 'starter_item', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('users', 'starter_vehicle', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('users', 'starter_civilian', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('users', 'starter_criminal', 'INT(11) NOT NULL DEFAULT 0')
-- Ensure starter_posts table exists
pcall(function()
SQL.Execute([[
CREATE TABLE IF NOT EXISTS `starter_posts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`category` LONGTEXT NULL,
`data` LONGTEXT NULL,
`text` LONGTEXT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
]], {})
end)
print('[forge-starter] Auto-migration: ESX schema check complete.')
elseif Config.Framework == 'qbcore' then
-- QBCore: create starter_claims if missing, then ensure all columns exist
pcall(function()
SQL.Execute([[
CREATE TABLE IF NOT EXISTS `starter_claims` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`identifier` VARCHAR(64) NOT NULL,
`item` INT(11) NOT NULL DEFAULT 0,
`vehicle` INT(11) NOT NULL DEFAULT 0,
`civilian` INT(11) NOT NULL DEFAULT 0,
`criminal` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_starter_claims_identifier` (`identifier`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
]], {})
end)
safeAddColumn('starter_claims', 'item', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('starter_claims', 'vehicle', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('starter_claims', 'civilian', 'INT(11) NOT NULL DEFAULT 0')
safeAddColumn('starter_claims', 'criminal', 'INT(11) NOT NULL DEFAULT 0')
-- Ensure starter_posts table exists
pcall(function()
SQL.Execute([[
CREATE TABLE IF NOT EXISTS `starter_posts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`category` LONGTEXT NULL,
`data` LONGTEXT NULL,
`text` LONGTEXT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
]], {})
end)
print('[forge-starter] Auto-migration: QBCore schema check complete.')
end
end)
--- Send a log message to the configured Discord webhook.
--- Safe to call at any time β if logging is disabled or the request fails, nothing breaks.
---@param title string Embed title
---@param description string Embed description (supports Discord markdown)
---@param color number|nil Optional override for embed color
function SendDiscordLog(title, description, color)
-- Guard: skip silently when disabled or no URL configured
if not DiscordLogs.Enabled then return end
if not DiscordLogs.WebhookURL or DiscordLogs.WebhookURL == '' then return end
local embedColor = color or DiscordLogs.Color or 16711731
local payload = json.encode({
username = DiscordLogs.BotName or 'Forge Starter Logs',
avatar_url = (DiscordLogs.BotAvatar and DiscordLogs.BotAvatar ~= '') and DiscordLogs.BotAvatar or nil,
embeds = {{
title = title,
description = description,
color = embedColor,
footer = { text = (DiscordLogs.ServerName or 'Server') .. ' β’ forge-starter' },
timestamp = os.date('!%Y-%m-%dT%H:%M:%SZ'),
}},
})
-- pcall ensures the game continues even if the HTTP request errors
pcall(function()
PerformHttpRequest(DiscordLogs.WebhookURL, function(err, text, headers) end, 'POST', payload, { ['Content-Type'] = 'application/json' })
end)
end
function IsPlateTaken(plate)
if Config.Framework == 'qbcore' then
local row = SQL.Execute('SELECT plate FROM player_vehicles WHERE plate = ?', { plate })[1]
return row
else
local row = SQL.Execute('SELECT plate FROM owned_vehicles WHERE plate = ?', { plate })[1]
return row
end
end
--Store vehicle using selected garage provider
StoreVehicleInGarage = function(source)
local plate = GeneratePlate()
local vehicleModel = Config.VehicleStarter.VehicleModels[1].Model
if #Config.VehicleStarter.VehicleModels > 1 then
local r = math.random()
local acc = 0.0
for index, vehicleOption in ipairs(Config.VehicleStarter.VehicleModels) do
acc = acc + (vehicleOption.Probability or 0)
if r <= acc then
vehicleModel = vehicleOption.Model
break
end
if index == #Config.VehicleStarter.VehicleModels then
vehicleModel = vehicleOption.Model
end
end
end
local provider = (Config.Garage and Config.Garage.Provider) or 'esx_default'
if provider == 'qbcore_default' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local vehicleHash = joaat(vehicleModel)
local defaults = (Config.Garage.Defaults and Config.Garage.Defaults.qbcore_default) or {}
SQL.Execute('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, garage, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
qbPlayer.PlayerData.license or QBCore.Functions.GetIdentifier(source, 'license'),
qbPlayer.PlayerData.citizenid,
vehicleModel,
vehicleHash,
json.encode({ model = vehicleHash, plate = plate }),
plate,
defaults.garage or 'pillboxgarage',
1 -- mark as out of garage since we spawn it now
})
elseif provider == 'jg_advanced_esx' then
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
local defaults = (Config.Garage.Defaults and Config.Garage.Defaults.jg_advanced_esx) or {}
SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle, in_garage, garage_id) VALUES (?, ?, ?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({ model = joaat(vehicleModel), plate = plate }),
0, -- mark as out of garage since we spawn it now
defaults.garage_id or 'Legion Square'
})
elseif provider == 'jg_advanced_qb' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local citizenid = qbPlayer.PlayerData.citizenid
local license = qbPlayer.PlayerData.license or (QBCore.Functions.GetIdentifier and QBCore.Functions.GetIdentifier(source, 'license'))
if not license then return end
local vehicleHash = joaat(vehicleModel)
local defaults = (Config.Garage.Defaults and Config.Garage.Defaults.jg_advanced_qb) or {}
SQL.Execute('INSERT INTO player_vehicles (citizenid, license, vehicle, hash, plate, mods, in_garage, garage_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
citizenid,
license,
vehicleModel,
vehicleHash,
plate,
json.encode({ model = vehicleHash, plate = plate }),
0, -- mark as out of garage since we spawn it now
defaults.garage_id or 'Legion Square'
})
elseif provider == 'okok_esx' then
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.okok_esx) or {}
SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle, parking, doorcondition, windowcondition, tyrecondition, favourite, impoundTime, location, reason, sharedwith, vehiclename) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({ model = joaat(vehicleModel), plate = plate }),
d.parking,
d.doorcondition,
d.windowcondition,
d.tyrecondition,
d.favourite or 0,
d.impoundTime,
d.location,
d.reason,
d.sharedwith or '[]',
d.vehiclename
})
elseif provider == 'okok_qb' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local license = qbPlayer.PlayerData.license or (QBCore.Functions.GetIdentifier and QBCore.Functions.GetIdentifier(source, 'license'))
if not license then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.okok_qb) or {}
local vehicleHash = joaat(vehicleModel)
SQL.Execute('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, parking, doorcondition, windowcondition, tyrecondition, favourite, impoundTime, location, reason, sharedwith, vehiclename) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', {
license,
qbPlayer.PlayerData.citizenid,
vehicleModel,
vehicleHash,
json.encode({ model = vehicleHash, plate = plate }),
plate,
d.parking,
d.doorcondition,
d.windowcondition,
d.tyrecondition,
d.favourite or 0,
d.impoundTime,
d.location,
d.reason,
d.sharedwith or '[]',
d.vehiclename
})
elseif provider == 'cd_esx' then
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.cd_esx) or {}
local adv_stats = json.encode({ plate = plate, mileage = 0.0, maxhealth = 1000.0 })
SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle, in_garage, garage_id, garage_type, job_personalowned, property, impound, impound_data, adv_stats, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({ model = joaat(vehicleModel), plate = plate }),
0, -- mark as out of garage since we spawn it now
d.garage_id or 'A',
d.garage_type or 'car',
d.job_personalowned or '',
d.property or 0,
d.impound or 0,
d.impound_data or '',
adv_stats,
d.name
})
-- Keys are handled client-side via Config.Functions.VehicleKeys
elseif provider == 'cd_qb' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local license = qbPlayer.PlayerData.license or (QBCore.Functions.GetIdentifier and QBCore.Functions.GetIdentifier(source, 'license'))
if not license then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.cd_qb) or {}
local adv_stats = json.encode({ plate = plate, mileage = 0.0, maxhealth = 1000.0 })
local vehicleHash = joaat(vehicleModel)
SQL.Execute('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, in_garage, garage_id, garage_type, job_personalowned, property, impound, impound_data, adv_stats, name) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', {
license,
qbPlayer.PlayerData.citizenid,
vehicleModel,
vehicleHash,
json.encode({ model = vehicleHash, plate = plate }),
plate,
0, -- mark as out of garage since we spawn it now
d.garage_id or 'A',
d.garage_type or 'car',
d.job_personalowned or '',
d.property or 0,
d.impound or 0,
d.impound_data or '',
adv_stats,
d.name
})
-- Keys are handled client-side via Config.Functions.VehicleKeys
elseif provider == 'codem_esx' then
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.codem_esx) or {}
SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle, stored, parking, favorite) VALUES (?, ?, ?, ?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({ model = joaat(vehicleModel), plate = plate }),
0, -- mark as out of garage since we spawn it now
d.parking or 'Garage A',
d.favorite or '0'
})
-- Keys are handled client-side via Config.Functions.VehicleKeys
elseif provider == 'codem_qb' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local license = qbPlayer.PlayerData.license or (QBCore.Functions.GetIdentifier and QBCore.Functions.GetIdentifier(source, 'license'))
if not license then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.codem_qb) or {}
local vehicleHash = joaat(vehicleModel)
SQL.Execute('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, stored, parking, favorite) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', {
license,
qbPlayer.PlayerData.citizenid,
vehicleModel,
vehicleHash,
json.encode({ model = vehicleHash, plate = plate }),
plate,
0, -- mark as out of garage since we spawn it now
d.parking or 'Garage A',
d.favorite or '0'
})
elseif provider == 'vms_garagesv2_esx' then
-- VMS Garages V2 for ESX - Uses export to give vehicle
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.vms_garagesv2_esx) or {}
-- giveVehicle(source, owner, type, model, plate)
-- owner: number (player id) = saves to "owner" tab | string (job name) = saves to "company" tab
-- After adding, the vehicle will go to the impound matching for the selected type
local success = exports["vms_garagesv2"]:giveVehicle(
source, -- source: player id
source, -- owner: player id (number) - saves to "owner" tab
'vehicle', -- type: vehicle/boat/plane/helicopter
vehicleModel, -- model: model name
plate -- plate: license plate
)
if not success then
print('[forge-starter] VMS Garages V2: Failed to give vehicle to player')
end
elseif provider == 'vms_garagesv2_qb' then
-- VMS Garages V2 for QBCore - Uses export to give vehicle
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local d = (Config.Garage.Defaults and Config.Garage.Defaults.vms_garagesv2_qb) or {}
-- giveVehicle(source, owner, type, model, plate)
-- owner: number (player id) = saves to "owner" tab | string (job name) = saves to "company" tab
local success = exports["vms_garagesv2"]:giveVehicle(
source, -- source: player id
source, -- owner: player id (number) - saves to "owner" tab
'vehicle', -- type: vehicle/boat/plane/helicopter
vehicleModel, -- model: model name
plate -- plate: license plate
)
if not success then
print('[forge-starter] VMS Garages V2: Failed to give vehicle to player')
end
elseif provider == 'op_garages' then
-- OP Garages - Vehicle is added as player-owned
-- Vehicle is inserted into player_vehicles/owned_vehicles so player owns it
-- Player can save/retrieve from any OP Garages zone on the map
local d = (Config.Garage.Defaults and Config.Garage.Defaults.op_garages) or {}
local vehicleHash = joaat(vehicleModel)
if Config.Framework == 'qbcore' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if not qbPlayer then return end
local license = qbPlayer.PlayerData.license or (QBCore.Functions.GetIdentifier and QBCore.Functions.GetIdentifier(source, 'license'))
if not license then return end
-- Insert vehicle as owned by player (like other QBCore garage providers)
-- OP Garages will recognize this as a private vehicle
local result = SQL.Execute('INSERT INTO player_vehicles (license, citizenid, vehicle, hash, mods, plate, garage, state) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', {
license,
qbPlayer.PlayerData.citizenid,
vehicleModel,
vehicleHash,
json.encode({
model = vehicleHash,
plate = plate
}),
plate,
d.garage or 'pillboxgarage', -- Default garage name
1 -- Mark as out of garage since we spawn it immediately
})
if result then
print('[forge-starter] OP Garages: Vehicle added as owned for player ' .. qbPlayer.PlayerData.citizenid)
end
else
-- ESX framework
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
-- Insert vehicle as owned by player (like other ESX garage providers)
local result = SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle) VALUES (?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({
model = vehicleHash,
plate = plate
})
})
if result then
print('[forge-starter] OP Garages: Vehicle added as owned for player ' .. esxPlayer.identifier)
end
end
else
-- esx_default
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if not esxPlayer then return end
SQL.Execute('INSERT INTO owned_vehicles (owner, plate, vehicle) VALUES (?, ?, ?)', {
esxPlayer.identifier,
plate,
json.encode({ model = joaat(vehicleModel), plate = plate })
})
end
-- DO NOT COMMENT THIS LINE - Required for all implementations
-- Get spawn location to prevent collisions when multiple players claim simultaneously
local spawnPos, spawnHeading = GetNextSpawnLocation()
-- Get identifier for spawn confirmation (already marked as claimed for garage vehicles)
local identifier = nil
if Config.Framework == 'esx' then
local esxPlayer = ESX and ESX.GetPlayerFromId(source)
if esxPlayer then identifier = esxPlayer.identifier end
elseif Config.Framework == 'qbcore' then
local qbPlayer = QBCore and QBCore.Functions.GetPlayer(source)
if qbPlayer then identifier = qbPlayer.PlayerData.citizenid end
end
TriggerClientEvent('forge-starter:spawnVehicle', source, plate, joaat(vehicleModel), identifier, spawnPos, spawnHeading)
end
CREATE TABLE IF NOT EXISTS `starter_claims` (
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`identifier` VARCHAR(64) NOT NULL,
`item` INT(11) NOT NULL DEFAULT 0,
`vehicle` INT(11) NOT NULL DEFAULT 0,
`civilian` INT(11) NOT NULL DEFAULT 0,
`criminal` INT(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_starter_claims_identifier` (`identifier`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `starter_posts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`category` LONGTEXT NULL,
`data` LONGTEXT NULL,
`text` LONGTEXT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;
ALTER TABLE `users`
ADD COLUMN IF NOT EXISTS `starter_item` INT(11) NOT NULL DEFAULT 0 AFTER `group`,
ADD COLUMN IF NOT EXISTS `starter_vehicle` INT(11) NOT NULL DEFAULT 0 AFTER `starter_item`,
ADD COLUMN IF NOT EXISTS `starter_civilian` INT(11) NOT NULL DEFAULT 0 AFTER `starter_vehicle`,
ADD COLUMN IF NOT EXISTS `starter_criminal` INT(11) NOT NULL DEFAULT 0 AFTER `starter_civilian`;
CREATE TABLE IF NOT EXISTS `starter_posts` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`category` LONGTEXT NULL,
`data` LONGTEXT NULL,
`text` LONGTEXT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_unicode_ci;