diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index 8c35eae6a..8a577e7e1 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -530,10 +530,14 @@ end ---@param vehicleModel integer | string The vehicle to spawn ---@param coords table | vector3 The coords to spawn the vehicle at ---@param heading number The heading of the vehicle ----@param cb? function The callback function +---@param cb? fun(vehicle: number) The callback function ---@param networked? boolean Whether the vehicle should be networked ----@return nil +---@return number? vehicle function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) + if cb and not ESX.IsFunctionReference(cb) then + error("Invalid callback function") + end + local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel) local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z) local isNetworked = networked == nil or networked @@ -549,8 +553,15 @@ function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource)) end + local promise = not cb and promise.new() CreateThread(function() - ESX.Streaming.RequestModel(model) + local modelHash = ESX.Streaming.RequestModel(model) + if not modelHash then + if promise then + return promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + end + error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + end local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true) @@ -569,10 +580,16 @@ function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) Wait(0) end - if cb then + if promise then + promise:resolve(vehicle) + elseif cb then cb(vehicle) end end) + + if promise then + return Citizen.Await(promise) + end end ---@param vehicle integer The vehicle to spawn diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 848289bf8..ffc8aa8c5 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -25,6 +25,7 @@ server_scripts { 'server/common.lua', 'server/modules/callback.lua', 'server/classes/player.lua', + 'server/classes/vehicle.lua', 'server/classes/overrides/*.lua', 'server/functions.lua', 'server/modules/onesync.lua', diff --git a/[core]/es_extended/server/classes/vehicle.lua b/[core]/es_extended/server/classes/vehicle.lua new file mode 100644 index 000000000..b85c8f655 --- /dev/null +++ b/[core]/es_extended/server/classes/vehicle.lua @@ -0,0 +1,237 @@ +---@class CVehicleData +---@field plate string +---@field netId number +---@field entity number +---@field modelHash number +---@field owner string + +---@class CExtendedVehicle +---@field plate string +---@field isValid fun(self:CExtendedVehicle):boolean +---@field new fun(owner:string, plate:string, coords:vector4): CExtendedVehicle? +---@field getFromPlate fun(plate:string):CExtendedVehicle? +---@field getPlate fun(self:CExtendedVehicle):string? +---@field getNetId fun(self:CExtendedVehicle):number? +---@field getEntity fun(self:CExtendedVehicle):number? +---@field getModelHash fun(self:CExtendedVehicle):number? +---@field getOwner fun(self:CExtendedVehicle):string? +---@field setPlate fun(self:CExtendedVehicle, newPlate:string):boolean +---@field setProps fun(self:CExtendedVehicle, newProps:table):boolean +---@field setOwner fun(self:CExtendedVehicle, newOwner:string):boolean +---@field delete fun(self:CExtendedVehicle, garageName:string?, isImpound:boolean?):nil +Core.vehicleClass = { + plate = "", + new = function(owner, plate, coords) + assert(type(owner) == "string", "Expected 'owner' to be a string") + assert(type(plate) == "string", "Expected 'plate' to be a string") + assert(type(coords) == "vector4", "Expected 'coords' to be a vector4") + + local xVehicle = Core.vehicleClass.getFromPlate(plate) + if xVehicle then + 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 + return + end + vehicleProps = json.decode(vehicleProps) + + if type(vehicleProps.model) ~= "number" then + vehicleProps.model = joaat(vehicleProps.model) + end + + local netId = ESX.OneSync.SpawnVehicle(vehicleProps.model, coords.xyz, coords.w, vehicleProps) + if not netId then + return + end + + local entity = NetworkGetEntityFromNetworkId(netId) + if entity <= 0 then + return + end + Entity(entity).state:set("owner", owner, false) + Entity(entity).state:set("plate", plate, false) + + ---@type CVehicleData + local vehicleData = { + plate = plate, + entity = entity, + netId = netId, + modelHash = vehicleProps.model, + owner = owner, + } + Core.vehicles[plate] = vehicleData + + MySQL.update.await("UPDATE `owned_vehicles` SET `stored` = false WHERE `owner` = ? AND `plate` = ?", { owner, plate }) + + local obj = table.clone(Core.vehicleClass) + obj.plate = plate + TriggerEvent("esx:createdExtendedVehicle", obj) + + return obj + end, + getFromPlate = function(plate) + assert(type(plate) == "string", "Expected 'plate' to be a string") + + if Core.vehicles[plate] then + local obj = table.clone(Core.vehicleClass) + obj.plate = plate + + if obj:isValid() then + return obj + end + end + end, + isValid = function(self) + local vehicleData = Core.vehicles[self.plate] + if not vehicleData then + return false + end + + local entity = NetworkGetEntityFromNetworkId(vehicleData.netId) + if entity <= 0 or Entity(entity).state.owner ~= vehicleData.owner or Entity(entity).state.plate ~= vehicleData.plate then + self:delete() + return false + end + + vehicleData.entity = entity + + return true + end, + getNetId = function(self) + if not self:isValid() then + return + end + + return Core.vehicles[self.plate].netId + end, + getEntity = function(self) + if not self:isValid() then + return + end + + return Core.vehicles[self.plate].entity + end, + getPlate = function(self) + if not self:isValid() then + return + end + + return Core.vehicles[self.plate].plate + end, + getModelHash = function(self) + if not self:isValid() then + return + end + + return Core.vehicles[self.plate].modelHash + end, + getOwner = function(self) + if not self:isValid() then + return + end + + return Core.vehicles[self.plate].owner + end, + setPlate = function(self, newPlate) + if not self:isValid() then + return false + end + assert(type(newPlate) == "string", "Expected 'plate' to be a string") + + local vehicleData = Core.vehicles[self.plate] + local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `plate` = ? WHERE `plate` = ? AND `owner` = ?", { newPlate, vehicleData.plate, vehicleData.owner }) + if affectedRows <= 0 then + self:delete() + return false + end + + Entity(vehicleData.entity).state:set("plate", newPlate, false) + SetVehicleNumberPlateText(vehicleData.entity, newPlate) + + local oldPlate = vehicleData.plate + vehicleData.plate = newPlate + Core.vehicles[newPlate] = table.clone(vehicleData) + Core.vehicles[oldPlate] = nil + + TriggerEvent("esx:changedExtendedVehiclePlate", vehicleData.plate, oldPlate) + Wait(0) + + return true + end, + setProps = function(self, newProps) + if not self:isValid() then + return false + end + assert(type(newProps) == "table", "Expected 'props' to be a table") + + local vehicleData = Core.vehicles[self.plate] + local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `vehicle` = ? WHERE `plate` = ? AND `owner` = ?", json.encode(newProps), vehicleData.plate, vehicleData.owner) + if affectedRows <= 0 then + self:delete() + return false + end + + Entity(vehicleData.entity).state:set("VehicleProperties", newProps, true) + + return true + end, + setOwner = function(self, newOwner) + if not self:isValid() then + return false + end + assert(type(newOwner) == "string", "Expected 'owner' to be a string") + + local vehicleData = Core.vehicles[self.plate] + if vehicleData.owner == newOwner then + return true + end + + local affectedRows = MySQL.update.await("UPDATE `owned_vehicles` SET `owner` = ? WHERE owner = ? AND `plate` = ?", { newOwner, vehicleData.owner, vehicleData.plate }) + if affectedRows <= 0 then + self:delete() + return false + end + + Entity(vehicleData.entity).state:set("owner", newOwner, false) + vehicleData.owner = newOwner + + return true + end, + delete = function(self, garageName, isImpound) + if type(garageName) ~= "string" then + garageName = nil + end + if type(isImpound) ~= "boolean" then + isImpound = false + end + + local vehicleData = Core.vehicles[self.plate] + if not vehicleData then + return + end + + local entity = NetworkGetEntityFromNetworkId(vehicleData.netId) + if entity >= 0 and Entity(entity).state.owner == vehicleData.owner then + DeleteEntity(vehicleData.entity) + end + + local query = "UPDATE `owned_vehicles` SET `stored` = true WHERE `plate` = ? AND `owner` = ?" + local queryParams = { vehicleData.plate, vehicleData.owner } + if garageName then + if isImpound then + query = "UPDATE `owned_vehicles` SET `stored` = true, `parking` = NULL, `pound` = ? WHERE `plate` = ? AND `owner` = ?" + else + query = "UPDATE `owned_vehicles` SET `stored` = true, `pound` = NULL, `parking` = ? WHERE `plate` = ? AND `owner` = ?" + end + + queryParams = { garageName, vehicleData.plate, vehicleData.owner } + end + + MySQL.update.await(query, queryParams) + TriggerEvent("esx:deletedExtendedVehicle", self) + + Core.vehicles[self.plate] = nil + end, +} diff --git a/[core]/es_extended/server/common.lua b/[core]/es_extended/server/common.lua index edbc39529..9c95c5713 100644 --- a/[core]/es_extended/server/common.lua +++ b/[core]/es_extended/server/common.lua @@ -11,6 +11,8 @@ Core.PlayerFunctionOverrides = {} Core.DatabaseConnected = false Core.playersByIdentifier = {} +---@type table +Core.vehicles = {} Core.vehicleTypesByModel = {} RegisterNetEvent("esx:onPlayerSpawn", function() diff --git a/[core]/es_extended/server/functions.lua b/[core]/es_extended/server/functions.lua index 884ef4a57..ae052f728 100644 --- a/[core]/es_extended/server/functions.lua +++ b/[core]/es_extended/server/functions.lua @@ -615,3 +615,17 @@ function Core.IsPlayerAdmin(playerId) local xPlayer = ESX.Players[playerId] return (xPlayer and Config.AdminGroups[xPlayer.group] and true) or false end + +---@param owner string +---@param plate string +---@param coords vector4 +---@return CExtendedVehicle? +function ESX.CreateExtendedVehicle(owner, plate, coords) + return Core.vehicleClass.new(owner, plate, coords) +end + +---@param plate string +---@return CExtendedVehicle? +function ESX.GetExtendedVehicleFromPlate(plate) + return Core.vehicleClass.getFromPlate(plate) +end \ No newline at end of file diff --git a/[core]/es_extended/server/modules/onesync.lua b/[core]/es_extended/server/modules/onesync.lua index 87c5f7f0d..de4f4eb8d 100644 --- a/[core]/es_extended/server/modules/onesync.lua +++ b/[core]/es_extended/server/modules/onesync.lua @@ -82,35 +82,58 @@ end ---@param coords vector3|table ---@param heading number ---@param properties table ----@param cb function +---@param cb? fun(netId: number) +---@return number? netId function ESX.OneSync.SpawnVehicle(model, coords, heading, properties, cb) + if cb and not ESX.IsFunctionReference(cb) then + error("Invalid callback function") + end + + local vehicleModel = joaat(model) local vehicleProperties = properties + local promise = not cb and promise.new() CreateThread(function() local xPlayer = ESX.OneSync.GetClosestPlayer(coords, 300) ESX.GetVehicleType(vehicleModel, xPlayer.id, function(vehicleType) - if vehicleType then - local createdVehicle = CreateVehicleServerSetter(vehicleModel, vehicleType, coords.x, coords.y, coords.z, heading) - local tries = 0 - - while not createdVehicle or createdVehicle == 0 or not GetEntityCoords(createdVehicle) do - Wait(200) - tries = tries + 1 - if tries > 20 then - return error(("Could not spawn vehicle - ^5%s^7!"):format(model)) + if not vehicleType then + if (promise) then + return promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + end + error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + end + + local createdVehicle = CreateVehicleServerSetter(vehicleModel, vehicleType, coords.x, coords.y, coords.z, heading) + local tries = 0 + + while not createdVehicle or createdVehicle == 0 or NetworkGetEntityOwner(createdVehicle) == -1 do + Wait(200) + tries = tries + 1 + if tries > 40 then + if promise then + return promise:reject(("Could not spawn vehicle - ^5%s^7!"):format(model)) end + error(("Could not spawn vehicle - ^5%s^7!"):format(model)) end - -- luacheck: ignore - SetEntityOrphanMode(createdVehicle, 2) - local networkId = NetworkGetNetworkIdFromEntity(createdVehicle) - Entity(createdVehicle).state:set("VehicleProperties", vehicleProperties, true) + end + + -- luacheck: ignore + SetEntityOrphanMode(createdVehicle, 2) + local networkId = NetworkGetNetworkIdFromEntity(createdVehicle) + Entity(createdVehicle).state:set("VehicleProperties", vehicleProperties, true) + + if promise then + promise:resolve(networkId) + elseif cb then cb(networkId) - else - error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) end end) end) + + if promise then + return Citizen.Await(promise) + end end ---@param model number|string