Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
130ad25
feat(sql): add VIN column and index to owned_vehicles table
gtolontop Aug 21, 2025
965d74e
feat(vehicle): add VIN field and getter method to CVehicleData class
gtolontop Aug 21, 2025
295c2a1
feat(es_extended/server/classes/vehicle): retrieve VIN along with veh…
gtolontop Aug 21, 2025
03396a9
feat(es_extended/server/classes/vehicle): add VIN to vehicle entity s…
gtolontop Aug 21, 2025
9da6e9d
feat(es_extended/server/classes/vehicle): add getVin method to retrie…
gtolontop Aug 21, 2025
b2d76e1
feat(es_extended/server/functions): add ESX.GenerateVIN function for …
gtolontop Aug 21, 2025
31bc9fb
feat(es_extended/server/classes/vehicle): generate and persist VIN fo…
gtolontop Aug 21, 2025
0e35777
feat(es_extended/server/classes/vehicle): ensure VIN uniqueness with …
gtolontop Aug 21, 2025
19739c6
feat(es_extended/server/modules/commands): add admin command to get v…
gtolontop Aug 21, 2025
e4f5bec
feat(es_extended/server/functions): add ESX.GetExtendedVehicleFromVIN…
gtolontop Aug 21, 2025
02f1902
feat(es_extended/server/modules/commands): add admin command to get v…
gtolontop Aug 21, 2025
e5351b0
feat(es_extended/server/function): enhance VIN system with meaningful…
gtolontop Aug 26, 2025
3b529cd
feat(es_extended): add Config.EnableVehicleVIN (shared/config) & pres…
gtolontop Aug 26, 2025
8e8391c
fix(es_extended/server/function): adjust VIN format to fit database V…
gtolontop Aug 26, 2025
5ec72fe
fix(es_extended/server): update VIN vehicle type mappings to match ES…
gtolontop Aug 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion [SQL]/legacy.sql
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ INSERT INTO `licenses` (`type`, `label`) VALUES
CREATE TABLE `owned_vehicles` (
`owner` varchar(60) DEFAULT NULL,
`plate` varchar(12) NOT NULL,
`vin` varchar(17) UNIQUE DEFAULT NULL,
`vehicle` longtext DEFAULT NULL,
`type` varchar(20) NOT NULL DEFAULT 'car',
`job` varchar(20) DEFAULT NULL,
Expand Down Expand Up @@ -820,7 +821,8 @@ ALTER TABLE `licenses`
-- Indexes for table `owned_vehicles`
--
ALTER TABLE `owned_vehicles`
ADD PRIMARY KEY (`plate`);
ADD PRIMARY KEY (`plate`),
ADD INDEX `idx_vin` (`vin`);

--
--
Expand Down
38 changes: 33 additions & 5 deletions [core]/es_extended/server/classes/vehicle.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---@class CVehicleData
---@field plate string
---@field vin string
---@field netId number
---@field entity number
---@field modelHash number
Expand All @@ -11,6 +12,7 @@
---@field new fun(owner:string, plate:string, coords:vector4): CExtendedVehicle?
---@field getFromPlate fun(plate:string):CExtendedVehicle?
---@field getPlate fun(self:CExtendedVehicle):string?
---@field getVin fun(self:CExtendedVehicle):string?
---@field getNetId fun(self:CExtendedVehicle):number?
---@field getEntity fun(self:CExtendedVehicle):number?
---@field getModelHash fun(self:CExtendedVehicle):number?
Expand All @@ -31,16 +33,29 @@ Core.vehicleClass = {
return xVehicle
end

local vehicleProps = MySQL.scalar.await("SELECT `vehicle` FROM `owned_vehicles` WHERE `stored` = true AND `owner` = ? AND `plate` = ? LIMIT 1", { owner, plate })
if not vehicleProps then
local vehicleData = MySQL.single.await("SELECT `vehicle`, `vin` FROM `owned_vehicles` WHERE `stored` = true AND `owner` = ? AND `plate` = ? LIMIT 1", { owner, plate })
if not vehicleData then
return
end
vehicleProps = json.decode(vehicleProps)
local vehicleProps = json.decode(vehicleData.vehicle)
---@type string?
local vin = vehicleData.vin
local modelName = nil

if type(vehicleProps.model) ~= "number" then
modelName = vehicleProps.model
vehicleProps.model = joaat(vehicleProps.model)
end

if not vin and Config.EnableVehicleVIN then
local vehicleType = ESX.GetVehicleType(vehicleProps.model, owner)
vin = ESX.GenerateVIN({
model = modelName,
vehicleType = vehicleType
})
MySQL.update.await("UPDATE `owned_vehicles` SET `vin` = ? WHERE `owner` = ? AND `plate` = ?", { vin, owner, plate })
end

local netId = ESX.OneSync.SpawnVehicle(vehicleProps.model, coords.xyz, coords.w, vehicleProps)
if not netId then
return
Expand All @@ -52,16 +67,18 @@ Core.vehicleClass = {
end
Entity(entity).state:set("owner", owner, false)
Entity(entity).state:set("plate", plate, false)
Entity(entity).state:set("vin", vin, false)

---@type CVehicleData
local vehicleData = {
local vehicleDataObj = {
plate = plate,
vin = vin,
entity = entity,
netId = netId,
modelHash = vehicleProps.model,
owner = owner,
}
Core.vehicles[plate] = vehicleData
Core.vehicles[plate] = vehicleDataObj

MySQL.update.await("UPDATE `owned_vehicles` SET `stored` = false WHERE `owner` = ? AND `plate` = ?", { owner, plate })

Expand Down Expand Up @@ -97,6 +114,10 @@ Core.vehicleClass = {

vehicleData.entity = entity

if not vehicleData.vin and Entity(entity).state.vin then
vehicleData.vin = Entity(entity).state.vin
end

return true
end,
getNetId = function(self)
Expand All @@ -120,6 +141,13 @@ Core.vehicleClass = {

return Core.vehicles[self.plate].plate
end,
getVin = function(self)
if not self:isValid() then
return
end

return Core.vehicles[self.plate].vin
end,
getModelHash = function(self)
if not self:isValid() then
return
Expand Down
119 changes: 119 additions & 0 deletions [core]/es_extended/server/functions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,115 @@ function Core.generateSSN()
end
end

---@param vehicleData? table
---@return string
function ESX.GenerateVIN(vehicleData)
local function generateRandomString(length, excludeConfusing)
local charset = excludeConfusing and "ABCDEFGHJKLMNPRSTUVWXYZ123456789" or "ABCDEFGHJKLMNPRSTUVWXYZ"
local str = ""
for i = 1, length do
local rand = math.random(1, #charset)
str = str .. charset:sub(rand, rand)
end
return str
end

local vin
local attempts = 0
local maxAttempts = 10

repeat
attempts = attempts + 1
if attempts > maxAttempts then
print("^1[ESX] Failed to generate unique VIN after " .. maxAttempts .. " attempts^7")
return "XXXXXXXXXXXXXXXXX"
end

-- VIN Format (17 chars total):
-- Position 1: World Manufacturer Identifier (W for custom)
-- Position 2-5: Model identifier (4 chars)
-- Position 6: Vehicle type
-- Position 7-10: Timestamp last 4 digits
-- Position 11-17: Random serial number (7 chars)

local wmi = "W"

local modelPart = ""
if vehicleData and vehicleData.model and type(vehicleData.model) == "string" then
local modelName = vehicleData.model:upper():gsub("[^A-Z0-9]", "")
modelPart = modelName:sub(1, 4)
end
if #modelPart < 4 then
modelPart = modelPart .. generateRandomString(4 - #modelPart, false)
end

local typePart = "U"
if vehicleData and vehicleData.vehicleType then
local typeMap = {
["bike"] = "B",
["automobile"] = "C",
["trailer"] = "T",
["boat"] = "S",
["heli"] = "H",
["plane"] = "P",
["submarine"] = "U",
["train"] = "R",
["quadbike"] = "Q",
["amphibious_automobile"] = "A",
["amphibious_quadbike"] = "A",
["submersible"] = "U",
["submarinecar"] = "U"
}
typePart = typeMap[vehicleData.vehicleType:lower()] or "X"
end

local timestamp = string.format("%04d", os.time() % 10000)

local serial = generateRandomString(7, true)

vin = wmi .. modelPart .. typePart .. timestamp .. serial

if #vin ~= 17 then
print("^1[ESX] VIN generation error: incorrect length " .. #vin .. "^7")
vin = "XXXXXXXXXXXXXXXXX"
end

local existingVin = MySQL.scalar.await("SELECT 1 FROM `owned_vehicles` WHERE `vin` = ? LIMIT 1", { vin })
until not existingVin

return vin
end

---@param vin string
---@return table?
function ESX.ParseVIN(vin)
if type(vin) ~= "string" or #vin ~= 17 then
return nil
end

local typeMap = {
["B"] = "bike",
["C"] = "automobile",
["T"] = "trailer",
["S"] = "boat",
["H"] = "heli",
["P"] = "plane",
["R"] = "train",
["U"] = "submarine",
["Q"] = "quadbike",
["A"] = "amphibious",
["X"] = "unknown"
}

return {
manufacturer = vin:sub(1, 1),
model = vin:sub(2, 5),
vehicleType = typeMap[vin:sub(6, 6)] or "unknown",
timestamp = vin:sub(7, 10),
serial = vin:sub(11, 17)
}
end

---@param owner string
---@param plate string
---@param coords vector4
Expand All @@ -875,3 +984,13 @@ end
function ESX.GetExtendedVehicleFromPlate(plate)
return Core.vehicleClass.getFromPlate(plate)
end

---@param vin string
---@return CExtendedVehicle?
function ESX.GetExtendedVehicleFromVIN(vin)
for plate, vehicleData in pairs(Core.vehicles) do
if vehicleData.vin == vin then
return Core.vehicleClass.getFromPlate(plate)
end
end
end
30 changes: 30 additions & 0 deletions [core]/es_extended/server/modules/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -769,3 +769,33 @@ ESX.RegisterCommand(
},
}
)

ESX.RegisterCommand(
"getvehiclevin",
"admin",
function(xPlayer, args)
local xVehicle = ESX.GetExtendedVehicleFromPlate(args.plate)
if xVehicle then
local vin = xVehicle:getVin()
xPlayer.showNotification(("Vehicle VIN: ~g~%s~s~"):format(vin or "No VIN"))
if Config.AdminLogging then
ESX.DiscordLogFields("UserActions", "Get Vehicle VIN /getvehiclevin Triggered!", "pink", {
{ name = "Player", value = xPlayer and xPlayer.name or "Server Console", inline = true },
{ name = "ID", value = xPlayer and xPlayer.source or "Unknown ID", inline = true },
{ name = "Plate", value = args.plate, inline = true },
{ name = "VIN", value = vin or "No VIN", inline = true },
})
end
else
xPlayer.showNotification("~r~Vehicle not found")
end
end,
false,
{
help = "Get vehicle VIN by plate",
validate = true,
arguments = {
{ name = "plate", help = "Vehicle plate", type = "string" },
},
}
)
2 changes: 2 additions & 0 deletions [core]/es_extended/shared/config/main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ Config.DistanceGive = 4.0 -- Max distance when giving items, weapons etc.

Config.AdminLogging = false -- Logs the usage of certain commands by those with group.admin ace permissions (default is false)

Config.EnableVehicleVIN = true -- Enable auto-generation of Vehicle Identification Numbers (VIN) for spawned vehicles

-------------------------------------
-- DO NOT CHANGE BELOW THIS LINE !!!
-------------------------------------
Expand Down
Loading