Last active
December 12, 2024 13:40
-
-
Save 0xDispatch/aec845db904efa7bcc2a795f3c978b92 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--[[ | |
Note that this is just a concept, with large data or vehicles this approach may not be the best. | |
CREATE TABLE `player_vehicles` ( | |
-- Unique identifier for each vehicle entry. | |
`id` int(11) NOT NULL, | |
-- Player's FiveM license (used for ownership verification) | |
`license` varchar(50) DEFAULT NULL, | |
-- Player's unique QB-Core identifier (used for ownership and queries) | |
`citizenid` varchar(11) DEFAULT NULL, | |
-- Vehicle model name (e.g., 'schafter2') | |
`vehicle` varchar(50) DEFAULT NULL, | |
-- Vehicle hash (numeric model hash stored as string, used for spawning) | |
`hash` varchar(50) DEFAULT NULL, | |
-- JSON string containing all vehicle modifications and properties | |
-- Used by QBCore.Functions.SetVehicleProperties to apply all vehicle customizations | |
`mods` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, | |
-- Vehicle's license plate (it can be a unique identifier for the vehicle instance) | |
`plate` varchar(8) NOT NULL, | |
-- Alternate plate for police RP features (not used in our current code) | |
`fakeplate` varchar(8) DEFAULT NULL, | |
-- Current garage identifier where vehicle is stored (e.g., 'pillboxgarage') | |
`garage` varchar(50) DEFAULT NULL, | |
-- Current fuel level (0-100) | |
-- Used by Entity(veh).state.fuel in our code | |
`fuel` int(11) DEFAULT 100, | |
-- Engine health (0-1000) | |
-- Used by Entity(veh).state.engineHealth in our code | |
`engine` float DEFAULT 1000, | |
-- Body health (0-1000) | |
-- Used by Entity(veh).state.bodyHealth in our code | |
`body` float DEFAULT 1000, | |
-- Vehicle state: | |
-- 0 = out/in world | |
-- 1 = in garage | |
-- 2 = in impound | |
`state` int(11) DEFAULT 1, | |
-- Cost to retrieve vehicle from depot/impound | |
-- Used in PayDepotPrice event | |
`depotprice` int(11) NOT NULL DEFAULT 0, | |
-- Total distance driven | |
`drivingdistance` int(50) DEFAULT NULL, | |
-- JSON string for additional vehicle status info not quite sure about the usage | |
-- | |
`status` text DEFAULT NULL, | |
-- Remaining balance on financed vehicle (for dealership scripts) | |
`balance` int(11) NOT NULL DEFAULT 0, | |
-- Payment amount per cycle for financed vehicles | |
`paymentamount` int(11) NOT NULL DEFAULT 0, | |
-- Number of payments remaining on finance | |
`paymentsleft` int(11) NOT NULL DEFAULT 0, | |
-- Unix timestamp for next finance payment | |
`financetime` int(11) NOT NULL DEFAULT 0, | |
-- JSON string storing last known coordinates | |
-- Format: {"x":221.8153839111328,"rx":-0.75409168004989,"z":30.021240234375,"y":-804.1450805664063,"rz":-114.45570373535156,"ry":-0.22070288658142} | |
-- Used by LoadSavedVehicles() to load vehicles where left | |
`position` text NOT NULL | |
-- JSON string storing last known coordinates | |
-- Format: {"x":221.8153839111328,"rx":-0.75409168004989,"z":30.021240234375,"y":-804.1450805664063,"rz":-114.45570373535156,"ry":-0.22070288658142} | |
-- This is same as position but its more like OwlV approach, it will be only set by /park | |
`spawn_position` text NOT NULL | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; | |
-- | |
-- Dumping data for table `player_vehicles` | |
-- | |
INSERT INTO `player_vehicles` (`id`, `license`, `citizenid`, `vehicle`, `hash`, `mods`, `plate`, `fakeplate`, `garage`, `fuel`, `engine`, `body`, `state`, `depotprice`, `drivingdistance`, `status`, `balance`, `paymentamount`, `paymentsleft`, `financetime`, `position`, `spawn_position`) VALUES | |
(3, 'license:5368f1863d6d8f07e676da40d86a9ee571a15945', 'DPW41984', 'ardent', '159274291', '{\"modExhaust\":0,\"modEngineBlock\":-1,\"modCustomTiresF\":false,\"modVanityPlate\":-1,\"modTransmission\":3,\"modSuspension\":2,\"modKit49\":-1,\"modTrunk\":-1,\"plate\":\"6ID293RO\",\"modSmokeEnabled\":false,\"modArmor\":-1,\"liveryRoof\":-1,\"modAerials\":-1,\"modAPlate\":-1,\"dashboardColor\":93,\"modSideSkirt\":-1,\"modTank\":-1,\"wheelWidth\":0.51967996358871,\"modRoof\":-1,\"modFrontWheels\":201,\"fuelLevel\":80.22715170715243,\"wheels\":11,\"modSeats\":-1,\"extras\":[],\"modDashboard\":-1,\"interiorColor\":93,\"wheelColor\":11,\"modSteeringWheel\":-1,\"engineHealth\":1000.0592475178704,\"doorStatus\":{\"1\":false,\"2\":false,\"3\":false,\"4\":false,\"5\":false,\"0\":false},\"modDoorSpeaker\":-1,\"modGrille\":-1,\"modFender\":1,\"dirtLevel\":0.0,\"color2\":0,\"modPlateHolder\":-1,\"modSpeakers\":-1,\"modHydrolic\":-1,\"model\":159274291,\"modAirFilter\":-1,\"tankHealth\":4000.2369900714818,\"neonColor\":[255,0,255],\"modKit19\":-1,\"modTrimA\":-1,\"modFrame\":-1,\"modKit17\":-1,\"modCustomTiresR\":false,\"bodyHealth\":1000.0592475178704,\"modSpoilers\":2,\"modTurbo\":1,\"neonEnabled\":[false,false,false,false],\"modEngine\":3,\"pearlescentColor\":87,\"modXenon\":false,\"plateIndex\":5,\"modHood\":5,\"color1\":88,\"oilLevel\":6.35462587779425,\"tireBurstState\":{\"1\":false,\"2\":false,\"3\":false,\"4\":false,\"5\":false,\"0\":false},\"windowStatus\":{\"1\":true,\"2\":true,\"3\":true,\"4\":false,\"5\":false,\"6\":true,\"7\":true,\"0\":true},\"modDial\":-1,\"modHorns\":55,\"modLivery\":0,\"tireHealth\":{\"1\":1000.0,\"2\":1000.0,\"3\":1000.0,\"0\":1000.0},\"modStruts\":-1,\"wheelSize\":0.63397371768951,\"modRearBumper\":0,\"modKit21\":-1,\"modWindows\":-1,\"modOrnaments\":-1,\"modArchCover\":-1,\"windowTint\":1,\"tireBurstCompletely\":{\"1\":false,\"2\":false,\"3\":false,\"4\":false,\"5\":false,\"0\":false},\"modTrimB\":-1,\"modShifterLeavers\":-1,\"modBrakes\":2,\"modKit47\":-1,\"modBackWheels\":-1,\"modRightFender\":-1,\"tyreSmokeColor\":[255,255,255],\"modFrontBumper\":3,\"xenonColor\":255}', '6ID293RO', NULL, NULL, 100, 100, 100, 0, 0, NULL, '{\"clutch\":100,\"brakes\":100,\"axle\":100,\"fuel\":100,\"radiator\":100}', 0, 0, 0, 0, '', '{\"z\":30.021240234375,\"y\":-804.1714477539063,\"x\":221.8153839111328,\"rz\":-112.84789276123047,\"ry\":-0.27606293559074,\"rx\":-0.80089968442916}'), | |
--]] | |
--pd-vehicle_load server main.lua | |
local QBCore = exports['qb-core']:GetCoreObject() | |
local spawnedCount = 0 | |
-- Function to delete all vehicles when resource restarts. | |
local function DeleteAllVehicles() | |
local vehicles = GetAllVehicles() | |
for _, vehicle in pairs(vehicles) do | |
DeleteEntity(vehicle) | |
end | |
end | |
-- Function to spawn saved vehicles at their last positions | |
local function LoadSavedVehicles() | |
local startTime = os.clock() | |
print('^3[Vehicle Load] Loading vehicles from last positions^7') | |
-- First delete any existing vehicles to prevent duplicates | |
DeleteAllVehicles() | |
local result = MySQL.query.await('SELECT * FROM player_vehicles WHERE (position IS NOT NULL AND position != "") OR (spawn_position IS NOT NULL AND spawn_position != "")') | |
if not result then return end | |
for _, vehicle in pairs(result) do | |
local positionData = nil | |
local success = false | |
-- Try to get position from last position first | |
if vehicle.position and vehicle.position ~= "" then | |
success, positionData = pcall(json.decode, vehicle.position) | |
end | |
-- If no last position, try spawn_position | |
if not success and vehicle.spawn_position and vehicle.spawn_position ~= "" then | |
success, positionData = pcall(json.decode, vehicle.spawn_position) | |
end | |
if success and positionData then | |
local vehType = QBCore.Shared.Vehicles[vehicle.vehicle] and QBCore.Shared.Vehicles[vehicle.vehicle].type or 'automobile' | |
local x, y, z = tonumber(positionData.x), tonumber(positionData.y), tonumber(positionData.z) | |
if x and y and z then | |
local veh = CreateVehicleServerSetter(joaat(vehicle.vehicle), vehType, x, y, z, 0.0) | |
-- Wait for vehicle creation | |
local timeout = 0 | |
while not DoesEntityExist(veh) and timeout < 50 do | |
Wait(100) | |
timeout = timeout + 1 | |
end | |
if DoesEntityExist(veh) then | |
-- Set rotation if available | |
if positionData.rx and positionData.ry and positionData.rz then | |
SetEntityRotation(veh, positionData.rx, positionData.ry, positionData.rz, 2, true) | |
end | |
SetVehicleNumberPlateText(veh, vehicle.plate) | |
-- Set vehicle properties | |
local success2, mods = pcall(json.decode, vehicle.mods) | |
if success2 and mods then | |
Entity(veh).state.vehicleProperties = mods | |
end | |
-- Set all vehicle stats at once | |
local engineHealth = tonumber(vehicle.engine) | |
local bodyHealth = tonumber(vehicle.body) | |
local fuelLevel = tonumber(vehicle.fuel) | |
Entity(veh).state.carStats = { | |
fuel = fuelLevel ~= nil and fuelLevel or 100, | |
engineHealth = engineHealth ~= nil and engineHealth or 1000, | |
bodyHealth = bodyHealth ~= nil and bodyHealth or 1000 | |
} | |
-- Update database and tracking | |
MySQL.update('UPDATE player_vehicles SET state = 0, garage = NULL WHERE plate = ?', { vehicle.plate }) | |
Entity(veh).state:set('isSpawned', true, true) | |
spawnedCount = spawnedCount + 1 | |
else | |
print('^1[Vehicle Load] Failed to create vehicle: '..vehicle.plate..'^7') | |
end | |
end | |
end | |
end | |
local endTime = os.clock() | |
local timeTaken = (endTime - startTime) * 1000 -- Convert to milliseconds | |
print(string.format('^2[Vehicle Load] Spawned %d vehicles in %.2fms^7', spawnedCount, timeTaken)) | |
end | |
-- Handler | |
AddEventHandler('onResourceStart', function(resource) | |
if resource == GetCurrentResourceName() then | |
Wait(100) | |
print('^3[Vehicle Load] Resource starting...^7') | |
if Config.AutoRespawn then | |
local startTime = os.clock() | |
-- First update vehicles without position data | |
MySQL.update('UPDATE player_vehicles SET state = 1 WHERE state = 0 AND (position IS NULL OR position = "")', {}) | |
-- Then load vehicles with position data | |
LoadSavedVehicles() | |
local endTime = os.clock() | |
local totalTime = (endTime - startTime) * 1000 | |
print(string.format('^2[Vehicle Load] Resource started in %.2fms^7', totalTime)) | |
else | |
print('^3[Vehicle Load] AutoRespawn is disabled, updating depot prices...^7') | |
MySQL.update('UPDATE player_vehicles SET depotprice = 500 WHERE state = 0', {}) | |
end | |
end | |
end) | |
-- Add the park command, anyone can use it on any car but it's just concept for now. | |
QBCore.Commands.Add('park', 'Save vehicle parking spot', {}, false, function(source) | |
local src = source | |
local Player = QBCore.Functions.GetPlayer(src) | |
if not Player then return end | |
local vehicle = GetVehiclePedIsIn(GetPlayerPed(src), true) | |
if vehicle == 0 then | |
TriggerClientEvent('QBCore:Notify', src, 'You are not in a vehicle.', 'error') | |
return | |
end | |
local plate = GetVehicleNumberPlateText(vehicle) | |
local coords = GetEntityCoords(vehicle) | |
local rotation = GetEntityRotation(vehicle) | |
local positionData = { | |
x = coords.x, | |
y = coords.y, | |
z = coords.z, | |
rx = rotation.x, | |
ry = rotation.y, | |
rz = rotation.z | |
} | |
MySQL.update('UPDATE player_vehicles SET spawn_position = ? WHERE plate = ? AND citizenid = ?', | |
{ json.encode(positionData), plate, Player.PlayerData.citizenid }, | |
function(affectedRows) | |
if affectedRows > 0 then | |
TriggerClientEvent('QBCore:Notify', src, 'Parking spot saved!', 'success') | |
else | |
TriggerClientEvent('QBCore:Notify', src, 'Could not save parking spot', 'error') | |
end | |
end | |
) | |
end) | |
--[[ | |
---------------------------------------- | |
pd-vehicle_load client main.lua | | |
---------------------------------------- | |
--]] | |
local QBCore = exports['qb-core']:GetCoreObject() | |
-- Handle vehicle properties | |
AddStateBagChangeHandler('vehicleProperties', nil, function(bagName, _, value) | |
local entity = GetEntityFromStateBagName(bagName) | |
if entity == 0 or not DoesEntityExist(entity) then return end | |
if value then | |
QBCore.Functions.SetVehicleProperties(entity, value) | |
end | |
end) | |
-- Handle all car stats (fuel, engine, body) | |
AddStateBagChangeHandler('carStats', nil, function(bagName, _, value) | |
local entity = GetEntityFromStateBagName(bagName) | |
if entity == 0 or not DoesEntityExist(entity) then return end | |
if value then | |
-- Set fuel | |
if value.fuel then | |
exports[Config.FuelResource]:SetFuel(entity, value.fuel) | |
end | |
-- Set engine health | |
if value.engineHealth then | |
SetVehicleEngineHealth(entity, value.engineHealth) | |
end | |
-- Set body health | |
if value.bodyHealth then | |
SetVehicleBodyHealth(entity, value.bodyHealth) | |
end | |
end | |
end) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment