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`); -- -- diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua index b85c8f655..628920203 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? @@ -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 @@ -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 }) @@ -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) @@ -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 diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index dc4a8fe63..1958fb879 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -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 @@ -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 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" }, + }, + } +) 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 !!! -------------------------------------