From 130ad251fca894b056558c3b931ead13c5dee3ec Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:35:33 +0200 Subject: [PATCH 01/15] feat(sql): add VIN column and index to owned_vehicles table --- [SQL]/legacy.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/[SQL]/legacy.sql b/[SQL]/legacy.sql index 655e5d7eb..552c01eef 100644 --- a/[SQL]/legacy.sql +++ b/[SQL]/legacy.sql @@ -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, @@ -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`); -- -- From 965d74e667353b88c5c4f1a349cff35072af9874 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:36:30 +0200 Subject: [PATCH 02/15] feat(vehicle): add VIN field and getter method to CVehicleData class --- [core]/es_extended/server/classes/vehicle.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index b85c8f655..f0ff152ef 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -1,5 +1,6 @@ ---@class CVehicleData ---@field plate string +---@field vin string ---@field netId number ---@field entity number ---@field modelHash number @@ -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? From 295c2a128b7f5ace8e144fa6e388d7126fe5c51b Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:42:46 +0200 Subject: [PATCH 03/15] feat(es_extended/server/classes/vehicle): retrieve VIN along with vehicle properties from owned_vehicles table --- [core]/es_extended/server/classes/vehicle.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index f0ff152ef..cba4cfbcb 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -33,11 +33,11 @@ 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) if type(vehicleProps.model) ~= "number" then vehicleProps.model = joaat(vehicleProps.model) From 03396a9439c47733a5620aee4c4904090aa8a5b2 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:45:02 +0200 Subject: [PATCH 04/15] feat(es_extended/server/classes/vehicle): add VIN to vehicle entity state and data object --- [core]/es_extended/server/classes/vehicle.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index cba4cfbcb..efdd336a5 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -54,16 +54,18 @@ Core.vehicleClass = { end Entity(entity).state:set("owner", owner, false) Entity(entity).state:set("plate", plate, false) + Entity(entity).state:set("vin", vehicleData.vin, false) ---@type CVehicleData - local vehicleData = { + local vehicleDataObj = { plate = plate, + vin = vehicleData.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 }) From 9da6e9d402e79c486d6d350e2dd82b64c0751b09 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:46:07 +0200 Subject: [PATCH 05/15] feat(es_extended/server/classes/vehicle): add getVin method to retrieve vehicle VIN --- [core]/es_extended/server/classes/vehicle.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index efdd336a5..b3cde4bae 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -124,6 +124,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 From b2d76e15367e7a9c781bbb74a9170f7bc4177e6b Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 03:54:36 +0200 Subject: [PATCH 06/15] feat(es_extended/server/functions): add ESX.GenerateVIN function for vehicle identification --- [core]/es_extended/server/functions.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index dc4a8fe63..584bf8713 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -862,6 +862,19 @@ function Core.generateSSN() end end +---@return string +function ESX.GenerateVIN() + local charset = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" + local vin = "" + + for i = 1, 17 do + local rand = math.random(1, #charset) + vin = vin .. charset:sub(rand, rand) + end + + return vin +end + ---@param owner string ---@param plate string ---@param coords vector4 From 31bc9fb791960cd07b5c89d7d486778ad77e288b Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 04:11:29 +0200 Subject: [PATCH 07/15] feat(es_extended/server/classes/vehicle): generate and persist VIN for vehicles without one --- [core]/es_extended/server/classes/vehicle.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index b3cde4bae..95a346780 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -38,11 +38,17 @@ Core.vehicleClass = { return end local vehicleProps = json.decode(vehicleData.vehicle) + local vin = vehicleData.vin if type(vehicleProps.model) ~= "number" then vehicleProps.model = joaat(vehicleProps.model) end + if not vin then + vin = ESX.GenerateVIN() + 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 @@ -54,12 +60,12 @@ Core.vehicleClass = { end Entity(entity).state:set("owner", owner, false) Entity(entity).state:set("plate", plate, false) - Entity(entity).state:set("vin", vehicleData.vin, false) + Entity(entity).state:set("vin", vin, false) ---@type CVehicleData local vehicleDataObj = { plate = plate, - vin = vehicleData.vin, + vin = vin, entity = entity, netId = netId, modelHash = vehicleProps.model, From 0e35777508cbadf04215afaf106155ea9286202c Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 04:12:47 +0200 Subject: [PATCH 08/15] feat(es_extended/server/classes/vehicle): ensure VIN uniqueness with database check --- [core]/es_extended/server/classes/vehicle.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index 95a346780..fd8c38943 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -45,7 +45,11 @@ Core.vehicleClass = { end if not vin then - vin = ESX.GenerateVIN() + repeat + vin = ESX.GenerateVIN() + local existingVin = MySQL.scalar.await("SELECT 1 FROM `owned_vehicles` WHERE `vin` = ? LIMIT 1", { vin }) + until not existingVin + MySQL.update.await("UPDATE `owned_vehicles` SET `vin` = ? WHERE `owner` = ? AND `plate` = ?", { vin, owner, plate }) end From 19739c628576763b5d37c0be428a416e02ee0045 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 04:16:48 +0200 Subject: [PATCH 09/15] feat(es_extended/server/modules/commands): add admin command to get vehicle VIN by plate --- .../es_extended/server/modules/commands.lua | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/[core]/es_extended/server/modules/commands.lua b/[core]/es_extended/server/modules/commands.lua index a8c7d6a67..8f369fddb 100644 --- a/[core]/es_extended/server/modules/commands.lua +++ b/[core]/es_extended/server/modules/commands.lua @@ -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" }, + }, + } +) From e4f5bec59efff4def83c917483c7c8759058ada4 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 04:17:35 +0200 Subject: [PATCH 10/15] feat(es_extended/server/functions): add ESX.GetExtendedVehicleFromVIN to find vehicles by VIN --- [core]/es_extended/server/functions.lua | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 584bf8713..f072111b8 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -888,3 +888,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 From 02f1902f31c48c65d7c712b499d00dfa5af429b5 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Thu, 21 Aug 2025 04:26:00 +0200 Subject: [PATCH 11/15] feat(es_extended/server/modules/commands): add admin command to get vehicle VIN by plate --- [core]/es_extended/server/classes/vehicle.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index fd8c38943..38992bfcf 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -38,6 +38,7 @@ Core.vehicleClass = { return end local vehicleProps = json.decode(vehicleData.vehicle) + ---@type string? local vin = vehicleData.vin if type(vehicleProps.model) ~= "number" then @@ -111,6 +112,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) From e5351b0b6a5ab270476b146524006d885739b483 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Tue, 26 Aug 2025 03:48:20 +0200 Subject: [PATCH 12/15] feat(es_extended/server/function): enhance VIN system with meaningful format and uniqueness check --- [core]/es_extended/server/functions.lua | 70 ++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index f072111b8..232463da8 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -862,16 +862,72 @@ function Core.generateSSN() end end +---@param vehicleData? table ---@return string -function ESX.GenerateVIN() - local charset = "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" - local vin = "" - - for i = 1, 17 do - local rand = math.random(1, #charset) - vin = vin .. charset:sub(rand, rand) +function ESX.GenerateVIN(vehicleData) + local function generateRandomString(length, useNumbers) + local charset = useNumbers and "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" 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 + repeat + -- VIN Format: MMMM-TTTTT-XXXXXXXX + -- M = Model (4 chars) + -- T = Type + timestamp (5 chars) + -- X = Random (8 chars) + local modelPart = "" + local typePart = "" + + if vehicleData and vehicleData.model then + local modelName = type(vehicleData.model) == "string" and vehicleData.model:upper() or "UNKN" + modelPart = modelName:sub(1, 4) + if #modelPart < 4 then + modelPart = modelPart .. generateRandomString(4 - #modelPart, false) + end + else + modelPart = generateRandomString(4, false) + end + + if vehicleData and vehicleData.vehicleType then + -- Vehicle type mapping: + -- B=bike + -- C=car + -- T=truck + -- P=plane + -- H=heli + -- B=boat (S for sea) + -- U=unknown + local typeMap = { + ["bike"] = "B", + ["automobile"] = "C", + ["car"] = "C", + ["truck"] = "T", + ["plane"] = "P", + ["helicopter"] = "H", + ["heli"] = "H", + ["boat"] = "S", + } + typePart = typeMap[vehicleData.vehicleType:lower()] or "U" + else + typePart = "U" + end + + local timestamp = tostring(os.time()):sub(-4) + typePart = typePart .. timestamp + + local randomPart = generateRandomString(8, true) + + vin = modelPart .. typePart .. randomPart + + local existingVin = MySQL.scalar.await("SELECT 1 FROM `owned_vehicles` WHERE `vin` = ? LIMIT 1", { vin }) + until not existingVin + return vin end From 3b529cd8767f0cd3595f2313d1141f56adcaff07 Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Tue, 26 Aug 2025 03:55:47 +0200 Subject: [PATCH 13/15] feat(es_extended): add Config.EnableVehicleVIN (shared/config) & preserve model names in VIN generation (server/classes/vehicle) --- [core]/es_extended/server/classes/vehicle.lua | 14 ++++++++------ [core]/es_extended/shared/config/main.lua | 2 ++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index 38992bfcf..628920203 100644 --- a/[core]/es_extended/server/classes/vehicle.lua +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -40,17 +40,19 @@ Core.vehicleClass = { 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 then - repeat - vin = ESX.GenerateVIN() - local existingVin = MySQL.scalar.await("SELECT 1 FROM `owned_vehicles` WHERE `vin` = ? LIMIT 1", { vin }) - until not existingVin - + 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 diff --git a/[core]/es_extended/shared/config/main.lua b/[core]/es_extended/shared/config/main.lua index 0bb551171..0d6f7b139 100644 --- a/[core]/es_extended/shared/config/main.lua +++ b/[core]/es_extended/shared/config/main.lua @@ -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 !!! ------------------------------------- From 8e8391cd2f6cc014ffe228cf6a4ae566bf2e192e Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Tue, 26 Aug 2025 04:07:45 +0200 Subject: [PATCH 14/15] fix(es_extended/server/function): adjust VIN format to fit database VARCHAR(17) constraint --- [core]/es_extended/server/functions.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 232463da8..1abfd4c16 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -921,9 +921,9 @@ function ESX.GenerateVIN(vehicleData) local timestamp = tostring(os.time()):sub(-4) typePart = typePart .. timestamp - local randomPart = generateRandomString(8, true) + local randomPart = generateRandomString(6, true) - vin = modelPart .. typePart .. randomPart + vin = modelPart .. "-" .. typePart .. "-" .. randomPart local existingVin = MySQL.scalar.await("SELECT 1 FROM `owned_vehicles` WHERE `vin` = ? LIMIT 1", { vin }) until not existingVin From 5ec72fef07336decf812636aca4fea32d97a6b3e Mon Sep 17 00:00:00 2001 From: Cruck Store Date: Tue, 26 Aug 2025 04:34:35 +0200 Subject: [PATCH 15/15] fix(es_extended/server): update VIN vehicle type mappings to match ESX types (remove car/truck/helicopter, add quadbike/amphibious/trailer) --- [core]/es_extended/server/functions.lua | 110 ++++++++++++++++-------- 1 file changed, 75 insertions(+), 35 deletions(-) diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 1abfd4c16..1958fb879 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -865,8 +865,8 @@ end ---@param vehicleData? table ---@return string function ESX.GenerateVIN(vehicleData) - local function generateRandomString(length, useNumbers) - local charset = useNumbers and "ABCDEFGHJKLMNPRSTUVWXYZ0123456789" or "ABCDEFGHJKLMNPRSTUVWXYZ" + 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) @@ -876,54 +876,64 @@ function ESX.GenerateVIN(vehicleData) end local vin + local attempts = 0 + local maxAttempts = 10 + repeat - -- VIN Format: MMMM-TTTTT-XXXXXXXX - -- M = Model (4 chars) - -- T = Type + timestamp (5 chars) - -- X = Random (8 chars) - local modelPart = "" - local typePart = "" + attempts = attempts + 1 + if attempts > maxAttempts then + print("^1[ESX] Failed to generate unique VIN after " .. maxAttempts .. " attempts^7") + return "XXXXXXXXXXXXXXXXX" + end - if vehicleData and vehicleData.model then - local modelName = type(vehicleData.model) == "string" and vehicleData.model:upper() or "UNKN" + -- 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) - if #modelPart < 4 then - modelPart = modelPart .. generateRandomString(4 - #modelPart, false) - end - else - modelPart = generateRandomString(4, false) + end + if #modelPart < 4 then + modelPart = modelPart .. generateRandomString(4 - #modelPart, false) end + local typePart = "U" if vehicleData and vehicleData.vehicleType then - -- Vehicle type mapping: - -- B=bike - -- C=car - -- T=truck - -- P=plane - -- H=heli - -- B=boat (S for sea) - -- U=unknown local typeMap = { ["bike"] = "B", ["automobile"] = "C", - ["car"] = "C", - ["truck"] = "T", - ["plane"] = "P", - ["helicopter"] = "H", - ["heli"] = "H", + ["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 "U" - else - typePart = "U" + typePart = typeMap[vehicleData.vehicleType:lower()] or "X" end - local timestamp = tostring(os.time()):sub(-4) - typePart = typePart .. timestamp + local timestamp = string.format("%04d", os.time() % 10000) + + local serial = generateRandomString(7, true) - local randomPart = generateRandomString(6, true) + vin = wmi .. modelPart .. typePart .. timestamp .. serial - vin = modelPart .. "-" .. typePart .. "-" .. randomPart + 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 @@ -931,6 +941,36 @@ function ESX.GenerateVIN(vehicleData) 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