diff --git a/resources/[carscripts]/AdvancedParking/.fxap b/resources/[carscripts]/AdvancedParking/.fxap new file mode 100644 index 000000000..f8ec9a971 Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/.fxap differ diff --git a/resources/[carscripts]/AdvancedParking/LICENSE.md b/resources/[carscripts]/AdvancedParking/LICENSE.md new file mode 100644 index 000000000..336b8b8ab --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/LICENSE.md @@ -0,0 +1,174 @@ + +Custom license + +Copyright (c) since 2024 by Philipp Decker + +Last updated: July 27, 2025 + +Contact for business enquiries: kiminaze@yahoo.de + +"AdvancedParking" (the "software") is a resource for the "FiveM" modification for the game "Grand +Theft Auto V" and is comprised of the files this license is attached to. It is only meant to +function inside of this environment. + +The software is made legally available through the online store located at https://tbx.kiminaze.de +and can be downloaded at https://portal.cfx.re/assets/granted-assets after purchase. + +The information in this license can be changed by any update of the software. + +The "customer" (the person that acquired this software legally) is granted permission to use and +modify all of its parts except the encrypted portions of the software. + +The customer is not allowed to publish and/or distribute the software, copies of the software, +parts of the software and/or modifications of the software. However - for the software to function +properly within the environment - distribution of the necessary client side files via the FiveM +server is permitted, provided these files have been legally acquired. + +By using this software the customer is accepting this license and by updating the software any +potential changes are accepted as well. + + +## Updates + +Updates to the software are provided by the copyright holder given there are new features to be +implemented into the software or problems with the software (errors, bugs, glitches) that need to +be resolved. + +Updates will only be provided as deemed necessary by the copyright holder. + +Updates can be downloaded at https://portal.cfx.re/assets/granted-assets. + +Major updates can potentially contain breaking changes towards backwards compatibility that will be +communicated in the patch notes. It is the customers responsibility to carefully read the patch +notes. + +Patch notes for outdated software can be found in the FiveM server console log when starting the +software. Notifications for new updates can be seen in the official Discord server and as new posts +in the official FiveM forum. + +All patch notes are located at https://github.com/Kiminaze/version_history. + + +## Support + +Support for the software is provided at the [official Discord server](https://discord.kiminaze.de) +in English and German. + +Support is provided to the customer and a second person ("developer") that can be chosen at the +customers discretion. + +Support is provided for, but not limited to: +- Installation of the software +- Configuration of the software +- Problems revolving directly around the software (errors, bugs, glitches) + +Support can be provided in the following forms: +- text chat +- voice call +- voice call with screenshare + +Support for the software will be terminated under the following conditions: +- The online store closes. +- The software is no longer being sold. + +The customer can be excluded from support when infringing upon this license or breaking any rule in +the Discord server or in Discord in general. Not providing necessary information after repeated +requests can also lead to exclusion. Should this happen and a developer had been chosen for +support, they will also be excluded from support. + +If the customer has been excluded from support, support can be reinstated by resolving any +previously arisen issues. + +All liability claims no longer apply if changes have been made to the software by the customer. + + +## Limitation of Liability + +The copyright holder is not liable for any damages arising from modifying the software given its +accessibility of the code. + +If the software has been obtained from a non-legal source, the copyright +holder is no longer liable for any damages from using the software. + + +## Supremacy of Platform License Agreement + +This License Agreement is subordinate to and does not replace or override the original Creator +Platform License Agreement ("PLA") set in place by Cfx (https://fivem.net/terms). In the event of +any conflict between this License Agreement and the PLA, the terms of the PLA shall prevail. All +users must comply with the terms and conditions of the PLA in addition to the terms set forth in +this License Agreement. + + +## Further information + +Further information referring to the software can be found in the +[documentation](https://docs.kiminaze.de). + + +## Privacy Policy + +### When is information collected? + +Data is collected **only** when telemetry is **explicitly enabled** by the customer via the +[provided convar](https://docs.kiminaze.de/scripts/advancedparking/convars#telemetry). Telemetry +can be enabled or disabled at any time at the customer's discretion. Telemetry data is disabled +by default and opt-in. + +Once enabled, telemetry data is sent in the following situations: +- When a defined number of log entries is reached, +- When this resource is stopped, +- When the server is about to be shut down, +- When a script error has occured, +- When a client disconnects from the server. + +### What information is collected? + +- Cfx (FiveM) identifier of the server owner +- Server build version +- Gamebuild version +- Version of this resource +- Timestamps of log entries +- Client logs produced by this resource +- Server logs produced by this resource +- Script errors generated by this resource + +**Note:** No personal data such as names or IP addresses is collected apart from the aforementioned +Cfx identifier. + +### Why is information collected? + +Telemetry data is collected for the following purposes: +- Debugging issues related to this resource +- Providing technical support and troubleshooting +- Improving the stability and reliability of this resource + +### Who has access to the data and how is it protected? + +Data is transmitted via secure HTTPS requests and access to the data is strictly limited to the +copyright owner (Philipp Decker) and authorized support staff. + +### Is data shared with third parties? + +Telemetry data is generally not shared with third parties. In support situations, telemetry +data may be used to visualize problems to the customer (and their developer) and support tickets +are generally visible to everyone who has access to this resource's section in the +[official Discord server](https://discord.kiminaze.de). + +### How long is data stored? + +Collected data is stored for **a maximum of 8 days**, after which it is automatically and +permanently erased. Should the server owner disable the telemetry, collected data will be erased +after **a maximum of 8 days**. + +### How can I get my data deleted? + +To request immediate deletion of telemetry data, please contact support in the +[official Discord server](https://discord.kiminaze.de). Deletion requests are processed as soon as +possible. + +### Who is responsible for GDPR compliance? + +Philipp Decker - Videogame and Software Development (German small business) + +Email: kiminaze@yahoo.de diff --git a/resources/[carscripts]/AdvancedParking/README.md b/resources/[carscripts]/AdvancedParking/README.md new file mode 100644 index 000000000..f3b6554ae --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/README.md @@ -0,0 +1,15 @@ + +## Support + +If you require any form of support after buying this resource, the right place to ask is our +Discord Server: https://discord.kiminaze.de + + +## Documentation + +https://docs.kiminaze.de/scripts/advancedparking + + +## Patchnotes + +https://github.com/Kiminaze/version_history/blob/main/AdvancedParking.json diff --git a/resources/[carscripts]/AdvancedParking/client/cl_integrations.lua b/resources/[carscripts]/AdvancedParking/client/cl_integrations.lua new file mode 100644 index 000000000..9a6fcc5c2 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/client/cl_integrations.lua @@ -0,0 +1,33 @@ + +-- get vehicle fuel level +function GetFuelLevel(vehicle) + if (GetResourceState("LegacyFuel") == "started") then + return exports["LegacyFuel"]:GetFuel(vehicle) + elseif (GetResourceState("ox_fuel") == "started") then + return Entity(vehicle).state.fuel or GetVehicleFuelLevel(vehicle) + elseif (GetResourceState("myFuel") == "started") then + return exports["myFuel"]:GetFuel(vehicle) + else + return GetVehicleFuelLevel(vehicle) + end +end + +-- set vehicle fuel level +function SetFuelLevel(vehicle, fuelLevel) + if (GetResourceState("LegacyFuel") == "started") then + exports["LegacyFuel"]:SetFuel(vehicle, fuelLevel) + elseif (GetResourceState("ox_fuel") == "started") then + Entity(vehicle).state.fuel = fuelLevel + elseif (GetResourceState("myFuel") == "started") then + exports["myFuel"]:SetFuel(vehicle, fuelLevel) + else + SetVehicleFuelLevel(vehicle, fuelLevel) + end +end + +-- notification (only used for the delete timer) +function ShowNotification(text) + SetNotificationTextEntry('STRING') + AddTextComponentSubstringPlayerName(text) + return DrawNotification(false, true) +end diff --git a/resources/[carscripts]/AdvancedParking/client/cl_utils.lua b/resources/[carscripts]/AdvancedParking/client/cl_utils.lua new file mode 100644 index 000000000..ef15c3bf9 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/client/cl_utils.lua @@ -0,0 +1,475 @@ + +local math_min, math_max, math_floor = math.min, math.max, math.floor + +-- returns all tuning of a vehicle +function GetVehicleTuning(vehicle) + -- main colors + local primaryColor, secondaryColor = GetVehicleColours(vehicle) + local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) + + -- custom colors + local customPrimaryColor, customSecondaryColor + if (GetIsVehiclePrimaryColourCustom(vehicle)) then + local r, g, b = GetVehicleCustomPrimaryColour(vehicle) + customPrimaryColor = { r, g, b } + end + if (GetIsVehicleSecondaryColourCustom(vehicle)) then + local r, g, b = GetVehicleCustomSecondaryColour(vehicle) + customSecondaryColor = { r, g, b } + end + + -- tire smoke color + r, g, b = GetVehicleTyreSmokeColor(vehicle) + local tireSmokeColor = { r, g, b } + + -- neon lights color + local r, g, b = GetVehicleNeonLightsColour(vehicle) + local neonLightsColor = { r, g, b } + local enabledNeonLights = { + IsVehicleNeonLightEnabled(vehicle, 0), + IsVehicleNeonLightEnabled(vehicle, 1), + IsVehicleNeonLightEnabled(vehicle, 2), + IsVehicleNeonLightEnabled(vehicle, 3), + } + + return { + -- 1 + GetVehicleNumberPlateText(vehicle), + + -- 2 + GetVehicleMods(vehicle), + + -- 3 + primaryColor, + -- 4 + secondaryColor, + -- 5 + pearlescentColor, + -- 6 + wheelColor, + -- 7 + customPrimaryColor, + -- 8 + customSecondaryColor, + -- 9 + GetVehicleInteriorColor(vehicle), + -- 10 + GetVehicleDashboardColor(vehicle), + -- 11 + tireSmokeColor, + + -- 12 + GetVehicleXenonLightsColour(vehicle), + -- 13 + neonLightsColor, + -- 14 + enabledNeonLights, + + -- 15 + GetVehicleExtras(vehicle), + + -- 16 + GetVehicleWheelType(vehicle), + -- 17 + GetVehicleModVariation(vehicle, 23), + -- 18 + GetVehicleModVariation(vehicle, 24), + -- 19 + not GetVehicleTyresCanBurst(vehicle), + -- 20 + (GetGameBuildNumber() >= 2372) and GetDriftTyresEnabled(vehicle), + + -- 21 + GetVehicleNumberPlateTextIndex(vehicle), + + -- 22 + GetVehicleWindowTint(vehicle), + + -- 23 + GetVehicleLivery(vehicle), + + -- 24 + GetVehicleRoofLivery(vehicle) + } +end + +-- apply all vehicle tuning +function SetVehicleTuning(vehicle, tuning) + SetVehicleModKit(vehicle, 0) + + -- 16 wheelType + SetVehicleWheelType(vehicle, tuning[16]) + + -- 2 mods + SetVehicleMods(vehicle, tuning[2], tuning[17], tuning[18]) + + -- 3-4 primary/secondaryColor + SetVehicleColours(vehicle, tuning[3], tuning[4]) + -- 5-6 pearlescent/wheelColor + SetVehicleExtraColours(vehicle, tuning[5], tuning[6]) + -- 7 customPrimaryColor + if (tuning[7]) then + SetVehicleCustomPrimaryColour(vehicle, tuning[7][1], tuning[7][2], tuning[7][3]) + end + -- 8 customSecondaryColor + if (tuning[8]) then + SetVehicleCustomSecondaryColour(vehicle, tuning[8][1], tuning[8][2], tuning[8][3]) + end + -- 9 interiorColor + SetVehicleInteriorColor(vehicle, tuning[9]) + -- 10 dashboardColor + SetVehicleDashboardColor(vehicle, tuning[10]) + -- 11 tireSmokeColor + SetVehicleTyreSmokeColor(vehicle, tuning[11][1], tuning[11][2], tuning[11][3]) + + -- 12 xenonLightsColor + SetVehicleXenonLightsColour(vehicle, tuning[12]) + -- 13 neonLightsColor + SetVehicleNeonLightsColour(vehicle, tuning[13][1], tuning[13][2], tuning[13][3]) + -- 14 enabledNeonLights + for i = 0, 3, 1 do + SetVehicleNeonLightEnabled(vehicle, i, tuning[14][i + 1]) + end + + -- 15 extras + SetVehicleExtras(vehicle, tuning[15]) + + -- 19 bulletproofTires + SetVehicleTyresCanBurst(vehicle, not tuning[19]) + -- 20 driftTires + if (GetGameBuildNumber() >= 2372) then + SetDriftTyresEnabled(vehicle, tuning[20]) + end + + -- 1 numberPlateText + SetVehicleNumberPlateText(vehicle, tuning[1]) + -- 21 numberPlateTextIndex + SetVehicleNumberPlateTextIndex(vehicle, tuning[21]) + -- 22 windowTint + SetVehicleWindowTint(vehicle, tuning[22]) + -- 23 livery + SetVehicleLivery(vehicle, tuning[23]) + -- 24 roofLivery + SetVehicleRoofLivery(vehicle, tuning[24]) +end + +-- returns the status values of a vehicle +function GetVehicleStatus(vehicle) + return { + -- 1 entity health + math_max(math_min(GetEntityHealth(vehicle), 1000), 0), + -- 2 body health + math_floor(math_max(math_min(GetVehicleBodyHealth(vehicle), 1000.0), 0.0) * 10.0) / 10.0, + -- 3 engine health + math_floor(math_max(math_min(GetVehicleEngineHealth(vehicle), 1000.0), -4000.0) * 10.0) / 10.0, + -- 4 petrol tank health + math_floor(math_max(math_min(GetVehiclePetrolTankHealth(vehicle), 1000.0), -1000.0) * 10.0) / 10.0, + + -- 5 dirt level + math_floor(GetVehicleDirtLevel(vehicle) * 10.0) / 10.0, + -- 6 fuel level + math_floor(GetFuelLevel(vehicle) * 10.0) / 10.0, + + -- 7 lock status + GetVehicleDoorLockStatus(vehicle), + + -- 8 tire states + GetVehicleTireStates(vehicle), + -- 9 door states + GetVehicleDoorStates(vehicle), + -- 10 window states + GetVehicleWindowStates(vehicle), + } +end +-- apply all vehicle status values +function SetVehicleStatus(vehicle, status) + -- 1 entity health + SetEntityHealth(vehicle, math_floor(status[1])) + -- 2 body health + SetVehicleBodyHealth(vehicle, status[2] * 1.0) + -- 3 engine health + SetVehicleEngineHealth(vehicle, status[3] * 1.0) + -- 4 petrol tank health + SetVehiclePetrolTankHealth(vehicle, status[4] * 1.0) + + -- 5 dirt level + SetVehicleDirtLevel(vehicle, status[5] * 1.0) + -- 6 fuel level + SetFuelLevel(vehicle, status[6] * 1.0) + + -- 7 lock status + SetVehicleDoorsLocked(vehicle, status[7]) + + -- 8 tire states + SetVehicleTireStates(vehicle, status[8]) + -- 9 door states + SetVehicleDoorStates(vehicle, status[9]) + -- 10 window states + SetVehicleWindowStates(vehicle, status[10]) +end + +-- get additional values +local function GetConvertibleRoofIsOpen(vehicle) + if (not IsVehicleAConvertible(vehicle)) then return false end + + local state = GetConvertibleRoofState(vehicle) + return state == 1 or state == 2 or state == 6 +end +local function GetLandingGearIsUp(vehicle) + if (not DoesVehicleHaveLandingGear(vehicle)) then return false end + + local state = GetLandingGearState(vehicle) + return state == 1 or state == 4 +end +local function GetVtolMode(vehicle, model) + if (not IsThisModelAPlane(model)) then return false end + + return GetVehicleFlightNozzlePosition(vehicle) +end +local function GetStubWingsAreDeployed(vehicle, model) + if (model ~= `akula` and model ~= `annihilator`) then return false end + + return AreHeliStubWingsDeployed(vehicle) +end +local function GetBoatAnchorState(vehicle, model) + if (not IsThisModelABoat(model)) then return false end + + return IsBoatAnchored(vehicle) +end + +function GetVehicleExtraValues(vehicle) + local model = GetEntityModel(vehicle) + + return { + engineState = GetIsVehicleEngineRunning(vehicle) or nil, + roofState = GetConvertibleRoofIsOpen(vehicle) or nil, + --outriggers = AreOutriggerLegsDeployed(vehicle) or nil, -- no setter counterpart + landingGearUp = GetLandingGearIsUp(vehicle) or nil, + vtolMode = GetVtolMode(vehicle, model) or nil, + stubWings = GetStubWingsAreDeployed(vehicle, model) or nil, + boatAnchor = GetBoatAnchorState(vehicle, model) or nil, + -- hydraulics? + } +end +-- set additional values +function SetVehicleExtraValues(vehicle, values) + if (values.engineState) then + SetVehicleEngineOn(vehicle, true, true, false) + end + + if (values.roofState) then + LowerConvertibleRoof(vehicle, true) + end + + if (values.landingGearUp) then + ControlLandingGear(vehicle, 3) + end + + if (values.vtolMode) then + SetVehicleFlightNozzlePositionImmediate(vehicle, values.vtolMode) + end + + if (values.stubWings) then + SetDeployHeliStubWings(vehicle, true, false) + end + + if (values.boatAnchor) then + SetBoatRemainsAnchoredWhilePlayerIsDriver(vehicle, true) + SetBoatAnchor(vehicle, true) + end + + -- hydraulics? +end + + +-- returns all non-stock vehicle mods +function GetVehicleMods(vehicle) + local mods = {} + + for i = 0, 49, 1 do + -- TODO check for 17, 19, 21 -- toggle or normal mods? -- currently not possible + if (i == 18 or i == 20 or i == 22) then + if (IsToggleModOn(vehicle, i)) then + mods[#mods + 1] = { + -- 1 index + i, + -- 2 isToggledOn + true + } + end + else + local modIndex = GetVehicleMod(vehicle, i) + if (modIndex ~= -1) then + mods[#mods + 1] = { + -- 1 index + i, + -- 2 modIndex + modIndex + } + end + end + end + + return mods +end +-- apply all vehicle mods +function SetVehicleMods(vehicle, mods, customFrontWheels, customRearWheels) + for i = 1, #mods do + local id = mods[i][1] + + -- TODO check for 17, 19, 21 -- toggle or normal mods? -- currently not possible + if (id == 18 or id == 20 or id == 22) then + ToggleVehicleMod(vehicle, id, mods[i][2]) + else + SetVehicleMod(vehicle, id, mods[i][2], (id == 24) and customRearWheels or customFrontWheels) + end + end +end + +-- returns all vehicle extras +function GetVehicleExtras(vehicle) + local extras = {} + + for i = 0, 20 do + if (DoesExtraExist(vehicle, i)) then + if (IsVehicleExtraTurnedOn(vehicle, i)) then + extras[#extras + 1] = { + -- 1 index + i, + -- 2 isToggledOn + 0 + } + else + extras[#extras + 1] = { + -- 1 index + i, + -- 2 isToggledOn + 1 + } + end + end + end + + return extras +end +-- apply all vehicle extras +function SetVehicleExtras(vehicle, extras) + for i = 1, #extras do + SetVehicleExtra(vehicle, extras[i][1], extras[i][2]) + end +end + +-- returns all tire states +function GetVehicleTireStates(vehicle) + local burstTires = {} + + for i = 0, 5 do + if (IsVehicleTyreBurst(vehicle, i, true)) then + burstTires[#burstTires + 1] = { + -- 1 index + i, + -- 2 isBurst + true + } + elseif (IsVehicleTyreBurst(vehicle, i, false)) then + burstTires[#burstTires + 1] = { + -- 1 index + i, + -- 2 isBurst + false + } + end + end + + return burstTires +end +-- apply all tire states +function SetVehicleTireStates(vehicle, tireStates) + for i = 1, #tireStates do + SetVehicleTyreBurst(vehicle, tireStates[i][1], tireStates[i][2], 1000.0) + end +end + +-- returns all door states +function GetVehicleDoorStates(vehicle) + local doorStates = {} + + for i = 0, 7 do + if (GetIsDoorValid(vehicle, i)) then + doorStates[#doorStates + 1] = { + -- 1 index + i, + -- 2 missing + IsVehicleDoorDamaged(vehicle, i), + -- 3 angle (unused, causes problems) + --GetVehicleDoorAngleRatio(vehicle, i) + } + end + end + + return doorStates +end +-- apply all door states +function SetVehicleDoorStates(vehicle, doorStates) + for i = 1, #doorStates do + if (doorStates[i][2]) then + SetVehicleDoorBroken(vehicle, doorStates[i][1], true) + --elseif (doorStates[i][3] > 0.0) then + -- SetVehicleDoorControl(vehicle, doorStates[i][1], 1000, doorStates[i][3]) + end + end +end + +-- returns all window states +function GetVehicleWindowStates(vehicle) + if (AreAllVehicleWindowsIntact(vehicle)) then + return {} + end + + local windowStates = {} + + for i = 0, 13 do + if (not IsVehicleWindowIntact(vehicle, i)) then + windowStates[#windowStates + 1] = i + end + end + + return windowStates +end +-- apply all window states +function SetVehicleWindowStates(vehicle, windowStates) + for i = 1, #windowStates do + SmashVehicleWindow(vehicle, windowStates[i]) + end +end + + + +-- returns true if vehicle is blacklisted and should not be saved +function IsVehicleBlacklisted(vehicle) + -- check class + local class = GetVehicleClass(vehicle) + for i = 1, #classesBlacklist do + if (class == classesBlacklist[i]) then + return true + end + end + + -- check model + local modelHash = GetEntityModel(vehicle) + for i = 1, #vehiclesBlacklist do + if (modelHash == vehiclesBlacklist[i]) then + return true + end + end + + -- check plate + local plate = GetVehicleNumberPlateText(vehicle) + for i = 1, #platesBlacklist do + if (plate:find(platesBlacklist[i]:upper())) then + return true + end + end + + return false +end diff --git a/resources/[carscripts]/AdvancedParking/client/client.lua b/resources/[carscripts]/AdvancedParking/client/client.lua new file mode 100644 index 000000000..fc5df76a6 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/client/client.lua @@ -0,0 +1,193 @@ + +-- script status on startup +local enabled = true + +-- update vehicle on server side +local function UpdateVehicle(vehicle, reason) + local tuning = GetVehicleTuning(vehicle) + LogDebug("Updating vehicle \"%s\" (%s)", tuning[1], reason) + + TriggerServerEvent( + "AP:updateVehicle", + NetworkGetNetworkIdFromEntity(vehicle), + GetEntityModel(vehicle), + tuning, + GetVehicleStatus(vehicle), + GetVehicleExtraValues(vehicle), + reason + ) +end +exports("UpdateVehicle", function(vehicle) + if (not DoesEntityExist(vehicle) or not NetworkGetEntityIsNetworked(vehicle)) then return end + + UpdateVehicle(vehicle, "Resource \"" .. GetInvokingResource() .. "\"") +end) + +-- when a player entered a vehicle +local function EnteredVehicle(vehicle) + if (not DoesEntityExist(vehicle) or not NetworkGetEntityIsNetworked(vehicle) or IsVehicleBlacklisted(vehicle)) then return end + + if (GetVehicleTypeRaw(vehicle) == 2) then + TriggerServerEvent("AP:enteredTrailer", NetworkGetNetworkIdFromEntity(vehicle)) + end + + UpdateVehicle(vehicle, "Player enter") +end + +-- when a player left a vehicle +local function LeftVehicle(vehicle) + if (not DoesEntityExist(vehicle) or not NetworkGetEntityIsNetworked(vehicle) or IsVehicleBlacklisted(vehicle)) then return end + + if (GetVehicleTypeRaw(vehicle) == 2) then + TriggerServerEvent("AP:leftTrailer", NetworkGetNetworkIdFromEntity(vehicle)) + end + + UpdateVehicle(vehicle, "Player exit") +end + +-- localising loop natives +local IsPedInAnyVehicle, GetVehiclePedIsIn, GetVehicleTrailerVehicle = IsPedInAnyVehicle, GetVehiclePedIsIn, GetVehicleTrailerVehicle + +local isInVehicle = false +AddEventHandler("gameEventTriggered", function(eventName, eventData) + if (not enabled or isInVehicle or eventName ~= "CEventNetworkPlayerEnteredVehicle") then return end + + -- entered vehicle + isInVehicle = true + + local playerPed = PlayerPedId() + local vehicle = eventData[2] + local trailer = nil + + EnteredVehicle(vehicle) + + while (IsPedInAnyVehicle(playerPed)) do + -- check for instant vehicle switch + local newVehicle = GetVehiclePedIsIn(playerPed) + if (vehicle ~= newVehicle) then + LeftVehicle(vehicle) + EnteredVehicle(newVehicle) + + vehicle = newVehicle + end + + -- check for trailer + local hasTrailer, newTrailer = GetVehicleTrailerVehicle(vehicle) + if (hasTrailer) then + if (trailer ~= newTrailer) then + LeftVehicle(trailer) + EnteredVehicle(newTrailer) + end + + trailer = newTrailer + elseif (trailer) then + LeftVehicle(trailer) + + trailer = nil + end + + Wait(0) + end + + -- left vehicle + LeftVehicle(vehicle) + if (trailer) then + LeftVehicle(trailer) + end + + isInVehicle = false +end) + +-- setting tuning +AddStateBagChangeHandler("ap_data", nil, function(bagName, key, value, _unused, replicated) + if (bagName:find("entity") == nil or value == nil) then return end + + local networkIdString = bagName:gsub("entity:", "") + local networkId = tonumber(networkIdString) + if (not WaitUntilEntityWithNetworkIdExists(networkId, 5000)) then return end + + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (not WaitUntilPlayerEqualsEntityOwner(vehicle, 5000)) then return end + + if (Cleanup.submergedVehicles and IsEntityInWater(vehicle) and GetEntitySubmergedLevel(vehicle) >= 0.5) then + local vehicleType = GetVehicleTypeRaw(vehicle) + if (vehicleType ~= 5 and vehicleType ~= 15) then + LogDebug("\"%s\" is submerged and will be deleted.", value[1][1]) + + DeleteSubmergedVehicle(vehicle) + + return + end + end + + LogDebug("Setting properties for \"%s\"", value[1][1]) + + SetVehicleTuning(vehicle, value[1]) + SetVehicleStatus(vehicle, value[2]) + SetVehicleExtraValues(vehicle, value[3]) + + SetEntityCoordsNoOffset(vehicle, value[4].x, value[4].y, value[4].z, false, false, true) + SetEntityRotation(vehicle, value[5].x, value[5].y, value[5].z) + + TriggerEvent("AP:vehicleSpawned", vehicle) +end) + + + +-- notification for delete timer +RegisterNetEvent("AP:showNotification", function(text) + ShowNotification(text) +end) + +-- enables/disables the client sending enter/left events +local function Enable(enable) + assert(enable ~= nil and type(enable) == "boolean", "Parameter \"enable\" must be a bool!") + + enabled = enable +end +exports("Enable", Enable) + +-- export for force freezing vehicles +if (forceUnfreezeVehicles) then + local function FreezeVehicle(vehicle, freeze) + TriggerServerEvent("AP:freezeVehicle", NetworkGetNetworkIdFromEntity(vehicle), freeze) + end + exports("FreezeVehicle", FreezeVehicle) +end + + + +local damageUpdates = {} +AddEventHandler("gameEventTriggered", function (name, args) + if (name ~= "CEventNetworkEntityDamage") then return end + + local vehicle = args[1] + if (not DoesEntityExist(vehicle) or not IsEntityAVehicle(vehicle)) then return end + if (not NetworkGetEntityIsNetworked(vehicle)) then return end + + local id = Entity(vehicle).state.ap_id + if (not id) then return end + + local plate = GetVehicleNumberPlateText(vehicle) + + LogDebug("Damage detected on vehicle \"%s\" (%s)", plate, id) + + if (damageUpdates[vehicle]) then + damageUpdates[vehicle] = GetGameTimer() + 5000 + return + end + + damageUpdates[vehicle] = GetGameTimer() + 5000 + + while (damageUpdates[vehicle] > GetGameTimer()) do + Wait(0) + end + + damageUpdates[vehicle] = nil + + if (not DoesEntityExist(vehicle) or NetworkGetEntityOwner(vehicle) ~= PlayerId()) then return end + + LogDebug("Sending damage update to server for vehicle \"%s\" (%s)", plate, id) + + TriggerServerEvent("AP:onVehicleDamage", NetworkGetNetworkIdFromEntity(vehicle), GetVehicleStatus(vehicle)) +end) diff --git a/resources/[carscripts]/AdvancedParking/config.lua b/resources/[carscripts]/AdvancedParking/config.lua new file mode 100644 index 000000000..7379867ca --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/config.lua @@ -0,0 +1,118 @@ + +-- settings for the cleanup process +Cleanup = { + -- all vehicles will be removed that haven't had an update for X hours + -- set to nil to disable + timeThreshold = 24 * 7, + + -- all vehicles with an engine health value equal to or below X will be removed + -- set to nil to disable + -- set to 0.0 for vehicles with a broken engine + -- set to -3999.0 for exploded Vehicles + engineThreshold = nil, + + -- all vehicles further than X meters away from players will be removed + -- set to nil to disable + distanceThreshold = nil, + + -- all submerged vehicles will be removed + submergedVehicles = false, + + -- all vehicles inside these zones will be cleared + zones = { + --{ position = vector3(0, 0, 0), radius = 10.0 }, + }, + + -- all vehicles inside these zones will be ignored and not cleared + ignoredZones = { + --{ position = vector3(0, 0, 0), radius = 10.0 }, + }, + + -- plates listed here will be ignored and not removed (can include partial strings and not case sensitive) + ignoredPlates = { + --"XYZ 404 ", + --"xyz 404", + --"mech", + }, + + -- vehicle models listed here will be ignored and not removed + ignoredModels = { + --`blista`, + }, + + -- if ALL vehicles on the server should be affected, not only saved vehicles + allVehicles = false, + + -- send (owned) vehicles to e.g. garage or impound on cleanup (see sv_integrations.lua for implementation) + storeVehicles = false, + + -- cleanup on script start + onScriptStart = true, + + -- cleanup at set times (uses system time of the server) (day: 0-6 (Sunday-Monday) (can be omitted); hour: 0-23; minute: 0-59) + times = { + --{ hour = 3, minute = 0 }, -- every day 3 am + --{ day = 3, hour = 16, minute = 0 }, -- wednesday 4 pm + }, + + -- when players should be notified before a cleanup (in minutes) + notificationTimes = { 5, 3, 2, 1 }, + + -- notification to show players before removing vehicles (use %s as placeholder for time left in minutes) + -- check cl_integrations.lua for custom notifications + timeLeftNotification = "Vehicles will be deleted in %s minutes.", + + -- notification to show players when removing unused vehicles + -- check cl_integrations.lua for custom notifications + deleteNotification = "Removing vehicles..." +} + +-- This changes the default routing bucket where the script will detect and spawn vehicles. +-- This option becomes obsolete when enabling multiBucketSupport. +-- Do not change unless you know what you are doing! +routingBucket = 0 + +-- Allows detecting and saving vehicles in all routing buckets. +-- Do not change unless you know what you are doing! +multiBucketSupport = false + +-- Enable if you have problems with frozen vehicles. +-- Make sure to add fixFreezeEntity.lua to scripts that actually freeze vehicles. +forceUnfreezeVehicles = false + +-- only save vehicles that are owned (only works with ESX or QB by default) +saveOnlyOwnedVehicles = false + +-- If set to true, it will delete outside vehicles with the same plate on update +-- This is just a compatibility feature. You should still properly edit your scripts to prevent +-- duplicate vehicles in the first place. +preventDuplicateVehicles = false + +-- comma separated list of vehicle classes that you do not want to save +-- ids can be found here: https://docs.fivem.net/natives/?_0x29439776AAA00A62 +classesBlacklist = { + 21 --[[Trains]], +} + +-- other vehicles that you do not want to save can be inserted here (use `MODELNAME` when you put +-- them in there) +vehiclesBlacklist = { + --`blista`, + --`firetruk`, + --`adder`, +} + +-- any plates from vehicles you do not want to save, go here (not case sensitive and can use +-- partial strings) +platesBlacklist = { + --"XYZ 404 ", + --"xyz 404", + --"mech", +} + +-- ignore these state bags from being saved altogether (can include partial strings) +ignoreStateBags = {} + +-- prevent auto updates of these state bags and only save them on full update to database (can +-- include partial strings) +preventStateBagAutoUpdate = {} diff --git a/resources/[carscripts]/AdvancedParking/encrypted/client/client_encrypted.lua b/resources/[carscripts]/AdvancedParking/encrypted/client/client_encrypted.lua new file mode 100644 index 000000000..568a1cf9e Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/encrypted/client/client_encrypted.lua differ diff --git a/resources/[carscripts]/AdvancedParking/encrypted/client/log.lua b/resources/[carscripts]/AdvancedParking/encrypted/client/log.lua new file mode 100644 index 000000000..557c74d0b Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/encrypted/client/log.lua differ diff --git a/resources/[carscripts]/AdvancedParking/encrypted/server/log.lua b/resources/[carscripts]/AdvancedParking/encrypted/server/log.lua new file mode 100644 index 000000000..fd2ccd701 Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/encrypted/server/log.lua differ diff --git a/resources/[carscripts]/AdvancedParking/encrypted/server/server_encrypted.lua b/resources/[carscripts]/AdvancedParking/encrypted/server/server_encrypted.lua new file mode 100644 index 000000000..1e7a9a941 Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/encrypted/server/server_encrypted.lua differ diff --git a/resources/[carscripts]/AdvancedParking/fixDeleteVehicle.lua b/resources/[carscripts]/AdvancedParking/fixDeleteVehicle.lua new file mode 100644 index 000000000..e7340f2fd --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/fixDeleteVehicle.lua @@ -0,0 +1,32 @@ + +local AP_RESOURCE_NAME = "AdvancedParking" + +if (GetCurrentResourceName() == AP_RESOURCE_NAME) then return end + +local AP = exports[AP_RESOURCE_NAME] + +-- replaces DeleteEntity native on client and server side +local DeleteEntityOriginal = DeleteEntity +DeleteEntity = function(entity) + if (not DoesEntityExist(entity)) then return end + + if (GetEntityType(entity) ~= 2 or GetResourceState(AP_RESOURCE_NAME) ~= "started") then + DeleteEntityOriginal(entity) + return + end + + AP:DeleteVehicle(entity) +end + +-- replaces DeleteVehicle native on client side +if (not IsDuplicityVersion()) then + local DeleteVehicleOriginal = DeleteVehicle + DeleteVehicle = function(vehicle) + if (GetResourceState(AP_RESOURCE_NAME) ~= "started") then + DeleteVehicleOriginal(vehicle) + return + end + + AP:DeleteVehicle(vehicle) + end +end diff --git a/resources/[carscripts]/AdvancedParking/fixFreezeEntity.lua b/resources/[carscripts]/AdvancedParking/fixFreezeEntity.lua new file mode 100644 index 000000000..1b102186b --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/fixFreezeEntity.lua @@ -0,0 +1,20 @@ + +local AP_RESOURCE_NAME = "AdvancedParking" + +if (GetCurrentResourceName() == AP_RESOURCE_NAME) then return end + +local IS_CLIENT = not IsDuplicityVersion() +local AP = exports[AP_RESOURCE_NAME] + +-- replaces FreezeEntityPosition native on client and server side +local original_FreezeEntityPosition = FreezeEntityPosition +FreezeEntityPosition = function(entity, freeze) + if (not DoesEntityExist(entity)) then return end + + if (GetEntityType(entity) ~= 2 or (IS_CLIENT and not NetworkGetEntityIsNetworked(entity)) or GetResourceState(AP_RESOURCE_NAME) ~= "started" or not AP.FreezeVehicle) then + original_FreezeEntityPosition(entity, freeze) + return + end + + AP:FreezeVehicle(entity, freeze) +end diff --git a/resources/[carscripts]/AdvancedParking/fxmanifest.lua b/resources/[carscripts]/AdvancedParking/fxmanifest.lua new file mode 100644 index 000000000..30df2119e --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/fxmanifest.lua @@ -0,0 +1,59 @@ + +fx_version "cerulean" +games { "gta5" } + +author "Philipp Decker" +description "Saves and respawns vehicles during sessions and across server restarts." +version "4.11.1" + +lua54 "yes" +use_experimental_fxv2_oal "yes" + +escrow_ignore { + "*.lua", + "client/*.lua", + "server/*.lua", + "server/storage/oxmysql.lua" +} + +dependencies { + "/server:14758", + "/onesync", + "kimi_callbacks" +} + +files { + "fixDeleteVehicle.lua", + "fixFreezeEntity.lua" +} + +server_scripts { + "encrypted/server/log.lua", + + "config.lua", + + "server/storage/storage.lua", + "server/storage/oxmysql.lua", + "server/storage/kimi_sql.lua", + + "encrypted/server/server_encrypted.lua", + + "server/sv_utils.lua", + "server/cleanup.lua", + "server/server.lua", + "server/sv_integrations.lua" +} + +client_scripts { + "encrypted/client/log.lua", + + "config.lua", + + "encrypted/client/client_encrypted.lua", + + "client/cl_utils.lua", + "client/client.lua", + "client/cl_integrations.lua" +} + +dependency '/assetpacks' \ No newline at end of file diff --git a/resources/[carscripts]/AdvancedParking/server/cleanup.lua b/resources/[carscripts]/AdvancedParking/server/cleanup.lua new file mode 100644 index 000000000..410905b92 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/server/cleanup.lua @@ -0,0 +1,279 @@ + +-- localise frequently used Lua globals +local os_time, os_difftime, os_date, math_floor = os.time, os.difftime, os.date, math.floor + +-- localise frequently used natives +local DoesEntityExist, GetPlayerRoutingBucket, GetEntityCoords, DeleteEntity, GetVehicleEngineHealth, GetEntityRoutingBucket = +DoesEntityExist, GetPlayerRoutingBucket, GetEntityCoords, DeleteEntity, GetVehicleEngineHealth, GetEntityRoutingBucket + +-- task system +local tasks = {} + +local function GetTime() + local time = os_time() + + return { + day = tonumber(os_date("%w", time)), + hour = tonumber(os_date("%H", time)), + minute = tonumber(os_date("%M", time)) + } +end + +local taskRunning = false +local function StartTaskThread() + if (taskRunning) then return end + + taskRunning = true + + local lastTime = GetTime() + + while (taskRunning) do + Wait(1000) + + local time = GetTime() + + if (time.minute ~= lastTime.minute or time.hour ~= lastTime.hour or time.day ~= lastTime.day) then + for i = 1, #tasks do + if ((not tasks[i].day or tasks[i].day == time.day) and tasks[i].hour == time.hour and tasks[i].minute == time.minute) then + tasks[i].Run() + end + end + + lastTime = time + end + end +end + +local function AddTask(task, d, h, m) + assert(task and type(task) == "function", "Parameter \"task\" must be a function!") + assert(not d or type(d) == "number", "Parameter \"day\" must be a number!") + assert(h and type(h) == "number", "Parameter \"hour\" must be a number!") + assert(m and type(m) == "number", "Parameter \"minute\" must be a number!") + + tasks[#tasks + 1] = { + day = d and math_floor(d), + hour = math_floor(h), + minute = math_floor(m), + Run = task + } + + if (not taskRunning) then + CreateThread(StartTaskThread) + end +end + + + +-- get all players sorted by bucket +local function GetPositionsOfAllPlayersByBucket() + local playerPositions = {} + + local players = GetPlayers() + for i = 1, #players do + local ped = GetPlayerPed(players[i]) + if (DoesEntityExist(ped)) then + local bucket = GetPlayerRoutingBucket(players[i]) + if (not playerPositions[bucket]) then + playerPositions[bucket] = {} + end + + playerPositions[bucket][#playerPositions[bucket] + 1] = GetEntityCoords(ped) + end + end + + return playerPositions +end + +-- get closest position and distance from list +local function GetClosestDistanceFromList(position, positionList) + local closestDistance = 100000 + + for i = 1, positionList and #positionList or 0 do + local tempDist = #(position - positionList[i]) + if (tempDist < closestDistance) then + closestDistance = tempDist + end + end + + return closestDistance +end + +-- runs the whole cleanup process once +function CleanupProcess() + local timeDiff = 0 + if (Cleanup.timeThreshold) then + local currentTime = os_time() + local threshold = math_floor(3600 * Cleanup.timeThreshold) + timeDiff = os_difftime(currentTime, threshold) + end + + local playerPositions = GetPositionsOfAllPlayersByBucket() + + local toDelete = {} + + for id, vehicleData in pairs(savedVehicles) do + local position = DoesEntityExist(vehicleData.handle) and GetEntityCoords(vehicleData.handle) or vehicleData.position + + for i = 1, #Cleanup.ignoredZones do + if (#(position - Cleanup.ignoredZones[i].position) < Cleanup.ignoredZones[i].radius) then + goto cleanupDone -- continue + end + end + + for i = 1, #Cleanup.ignoredPlates do + if (vehicleData.tuning[1]:find(Cleanup.ignoredPlates[i]:upper())) then + goto cleanupDone -- continue + end + end + + for i = 1, #Cleanup.ignoredModels do + if (vehicleData.model == Cleanup.ignoredModels[i]) then + goto cleanupDone -- continue + end + end + + if (Cleanup.timeThreshold and vehicleData.lastUpdate < timeDiff) then + toDelete[#toDelete + 1] = id + TriggerEvent("AP:cleanup:deletingVehicle", vehicleData.handle, vehicleData.tuning[1], "time") + goto cleanupDone -- continue + end + + if (Cleanup.engineThreshold and vehicleData.status[3] <= Cleanup.engineThreshold) then + toDelete[#toDelete + 1] = id + TriggerEvent("AP:cleanup:deletingVehicle", vehicleData.handle, vehicleData.tuning[1], "engineHealth") + goto cleanupDone -- continue + end + + if (Cleanup.distanceThreshold) then + local distance = GetClosestDistanceFromList(position, playerPositions[vehicleData.bucket]) + if (distance > Cleanup.distanceThreshold) then + toDelete[#toDelete + 1] = id + TriggerEvent("AP:cleanup:deletingVehicle", vehicleData.handle, vehicleData.tuning[1], "distance") + goto cleanupDone -- continue + end + end + + for i = 1, #Cleanup.zones do + if (#(position - Cleanup.zones[i].position) < Cleanup.zones[i].radius) then + toDelete[#toDelete + 1] = id + TriggerEvent("AP:cleanup:deletingVehicle", vehicleData.handle, vehicleData.tuning[1], "zone_" .. i) + goto cleanupDone -- continue + end + end + + ::cleanupDone:: + end + + for i, id in ipairs(toDelete) do + if (savedVehicles[id].handle and DoesEntityExist(savedVehicles[id].handle)) then + DeleteEntity(savedVehicles[id].handle) + + LogDebug("Cleanup removed \"%s\" (\"%s\").", id, savedVehicles[id].tuning[1]) + end + + if (Cleanup.storeVehicles) then + StoreVehicle(savedVehicles[id].tuning[1], savedVehicles[id].handle) + end + + savedVehicles[id] = nil + spawnQueue[id] = nil + end + + DeleteVehiclesFromDB(toDelete) + + local othersCount = 0 + if (Cleanup.allVehicles) then + local vehicles = GetAllVehicles() + for i = 1, #vehicles do + if (not DoesEntityExist(vehicles[i])) then + goto cleanupOthersDone -- continue + end + + local position = GetEntityCoords(vehicles[i]) + + for i = 1, #Cleanup.ignoredZones do + if (#(position - Cleanup.ignoredZones[i].position) < Cleanup.ignoredZones[i].radius) then + goto cleanupOthersDone -- continue + end + end + + if (#Cleanup.ignoredPlates > 0) then + local plate = GetVehicleNumberPlateText(vehicles[i]) + for i = 1, #Cleanup.ignoredPlates do + if (plate:find(Cleanup.ignoredPlates[i]:upper())) then + goto cleanupOthersDone -- continue + end + end + end + + if (#Cleanup.ignoredModels > 0) then + local model = GetEntityModel(vehicles[i]) + for i = 1, #Cleanup.ignoredModels do + if (model == Cleanup.ignoredModels[i]) then + goto cleanupOthersDone -- continue + end + end + end + + if (Cleanup.engineThreshold and GetVehicleEngineHealth(vehicles[i]) <= Cleanup.engineThreshold) then + DeleteEntity(vehicles[i]) + othersCount += 1 + goto cleanupOthersDone -- continue + end + + if (Cleanup.distanceThreshold) then + local distance = GetClosestDistanceFromList(position, playerPositions[GetEntityRoutingBucket(vehicles[i])]) + if (distance > Cleanup.distanceThreshold) then + DeleteEntity(vehicles[i]) + othersCount += 1 + goto cleanupOthersDone -- continue + end + end + + for i = 1, #Cleanup.zones do + if (#(position - Cleanup.zones[i].position) < Cleanup.zones[i].radius) then + DeleteEntity(vehicles[i]) + othersCount += 1 + goto cleanupOthersDone -- continue + end + end + + ::cleanupOthersDone:: + end + end + + TriggerClientEvent("AP:showNotification", -1, Cleanup.deleteNotification) + + Log("Cleanup complete. Removed %s saved vehicles. Removed %s other vehicles.", #toDelete, othersCount) +end + +-- add timed clean up tasks +for i = 1, #Cleanup.times do + local day = Cleanup.times[i].day + local hour = Cleanup.times[i].hour + local minute = Cleanup.times[i].minute + AddTask(CleanupProcess, day, hour, minute) + + for j = 1, #Cleanup.notificationTimes do + local d = day + local h = hour + local m = minute - Cleanup.notificationTimes[j] + if (m < 0) then + m += 60 + h -= 1 + if (h < 0) then + h += 24 + if (d) then + d -= 1 + if (d < 0) then + d += 7 + end + end + end + end + + AddTask(function() + TriggerClientEvent("AP:showNotification", -1, Cleanup.timeLeftNotification:format(Cleanup.notificationTimes[j])) + end, d, h, m) + end +end diff --git a/resources/[carscripts]/AdvancedParking/server/server.lua b/resources/[carscripts]/AdvancedParking/server/server.lua new file mode 100644 index 000000000..ad44b1c7b --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/server/server.lua @@ -0,0 +1,1179 @@ + +local SPAWN_TIMEOUT = 10000 +local SPAWN_DISTANCE = 200 + +-- optimize Lua functions +local math_floor = math.floor +local table_concat = table.concat +local os_time, os_difftime, os_nanotime = os.time, os.difftime, os.nanotime +local json_encode, json_decode = json.encode, json.decode + +-- optimize natives +local GetAllVehicles, GetGameTimer, DoesEntityExist, CreateVehicleServerSetter, DeleteEntity, NetworkGetEntityOwner, NetworkGetNetworkIdFromEntity, NetworkGetEntityFromNetworkId, +GetEntityRoutingBucket, SetEntityRoutingBucket, SetEntityOrphanMode, GetEntityCoords, GetEntityRotation, GetVehicleDoorLockStatus, +GetEntityType, GetVehicleType, GetVehicleNumberPlateText, IsEntityPositionFrozen, GetPlayerIdentifierByType, GetEntityFromStateBagName, GetInvokingResource = +GetAllVehicles, GetGameTimer, DoesEntityExist, CreateVehicleServerSetter, DeleteEntity, NetworkGetEntityOwner, NetworkGetNetworkIdFromEntity, NetworkGetEntityFromNetworkId, +GetEntityRoutingBucket, SetEntityRoutingBucket, SetEntityOrphanMode, GetEntityCoords, GetEntityRotation, GetVehicleDoorLockStatus, +GetEntityType, GetVehicleType, GetVehicleNumberPlateText, IsEntityPositionFrozen, GetPlayerIdentifierByType, GetEntityFromStateBagName, GetInvokingResource + +-- list of all detected state bags on vehicle entities +local stateBagList = {} + +-- list of vehicles that fail to spawn and how often it happened +local failedSpawnList = {} + +local Ox = GetResourceState("ox_core") == "started" and exports["ox_core"] or nil + + + +-- script start up +CreateThread(function() + CreateAndReadFromStorage() + + if (Cleanup.onScriptStart) then + CleanupProcess() + end + + StartMainLoop() +end) + +-- get all vehicles from the database +function CreateAndReadFromStorage() + Storage.Create() + + local rows = Storage.GetAllVehicles() + + Log("Found %s saved vehicles in database.", #rows) + + local loadedVehicles = GetLoadedVehiclesWithId(GetAllVehicles()) + + for i, row in ipairs(rows) do + local tuning = json_decode(row.tuning) + if (not tuning) then + LogDebug("Vehicle \"%s\" has no tuning defined, skipping.", row.id) + goto skipEntry + end + if (not tuning[1] or tuning[1]:len() < 8) then + LogDebug("Vehicle \"%s\" has an invalid plate defined, skipping.", row.id) + goto skipEntry + end + if (not row.model) then + LogDebug("Vehicle \"%s\" (\"%s\") has no model defined, skipping.", row.id, tuning[1]) + goto skipEntry + end + if (not row.type) then + LogDebug("Vehicle \"%s\" (\"%s\") has no type defined, skipping.", row.id, tuning[1]) + goto skipEntry + end + local status = json_decode(row.status) + if (not status) then + LogDebug("Vehicle \"%s\" (\"%s\") has no status defined, skipping.", row.id, tuning[1]) + goto skipEntry + end + local position = vector3(row.posX, row.posY, row.posZ) + if (#(vector3(0,0,0) - position) < 1.0) then + LogDebug("Vehicle \"%s\" (\"%s\") detected at origin, skipping.", row.id, tuning[1]) + goto skipEntry + end + + savedVehicles[row.id] = { + handle = nil, + model = row.model, + type = row.type, + status = status, + tuning = tuning, + extraValues = json_decode(row.extraValues), + stateBags = ChangeTablesToVector(json_decode(row.stateBags)), + bucket = row.bucket, + --attachedTo = json_decode(row.attachedTo), + --attachedVehicles = {}, + position = position, + rotation = vector3(row.rotX, row.rotY, row.rotZ), + lastUpdate = row.lastUpdate, + initialPlayer = row.initialPlayer, + lastPlayer = row.lastPlayer, + spawning = false + } + + if (loadedVehicles[row.id]) then + -- if vehicle exists, add handle + savedVehicles[row.id].handle = loadedVehicles[row.id] + + LogDebug("Found vehicle \"%s\" (\"%s\") at %s", row.id, tuning[1], RoundVector3(GetEntityCoords(savedVehicles[row.id].handle), 2)) + else + -- ... otherwise add to spawn queue + spawnQueue[row.id] = true + end + + -- add state bags to list + for bagName, _ in pairs(savedVehicles[row.id].stateBags) do + if (not stateBagList[bagName]) then + stateBagList[bagName] = true + end + end + + ::skipEntry:: + end + + -- handle attached vehicles + --for id, vehicleData in pairs(savedVehicles) do + -- if (vehicleData.attachedTo[1]) then + -- table_insert(savedVehicles[vehicleData.attachedTo[1]].attachedVehicles, { id, vehicleData.attachedTo[2], vehicleData.attachedTo[3] }) + -- end + --end +end + +-- main loop for spawning and updating vehicles +function StartMainLoop() + CreateThread(function() + while (true) do + Wait(5000) + + local players = GetPlayers() + if (#players > 0) then + local playerPedsWithHandlers = GetAllPlayerPedsWithHandles(players) + local playerPeds = GetAllPlayerPeds() + + SpawnVehicles(players, playerPedsWithHandlers) + UpdateVehicles(players, playerPedsWithHandlers, playerPeds) + end + end + end) +end + +-- try spawning vehicles +function SpawnVehicles(players, playerPedsWithHandlers) + -- check if any vehicle needs respawning + for id, _ in pairs(spawnQueue) do + if (not savedVehicles[id].spawnTime and savedVehicles[id].handle == nil and GetClosestPlayer(savedVehicles[id].position, SPAWN_DISTANCE, players, playerPedsWithHandlers, savedVehicles[id].bucket)) then + -- vehicle not found, spawn it when player is close + CreateThread(function() + SpawnVehicle(id, savedVehicles[id]) + end) + end + end +end + +-- update vehicles +function UpdateVehicles(players, playerPedsWithHandlers, playerPeds) + for id, vehicleData in pairs(savedVehicles) do + local exists = vehicleData.handle and DoesEntityExist(vehicleData.handle) + if (exists and GetClosestPlayer(vehicleData.position, 150.0, players, playerPedsWithHandlers, vehicleData.bucket) and NetworkGetEntityOwner(vehicleData.handle) ~= -1) then + TryUpdateVehicle(id, vehicleData, playerPeds) + elseif (not exists and not spawnQueue[id]) then + spawnQueue[id] = true + end + end +end + + + +-- handle removal of entities if not from AP itself +AddEventHandler("entityRemoved", function(entity) + if (GetEntityType(entity) ~= 2) then return end + + local position = GetEntityCoords(entity) + local rotation = GetEntityRotation(entity) + + local id = Entity(entity)?.state?.ap_id + if (not id or not savedVehicles[id]) then return end + + savedVehicles[id].position = position + savedVehicles[id].rotation = rotation + + savedVehicles[id].handle = nil + savedVehicles[id].spawnTime = nil + + spawnQueue[id] = true + + UpdateVehicleInDB(id, savedVehicles[id], "entityRemoved event") +end) + +local function IsInBounds(value, min, max) + return value >= min and value <= max +end + +-- check if update is necessary and update +function TryUpdateVehicle(id, vehicleData, playerPeds, ignorePlayerInside) + local vehicle = vehicleData.handle + + if ((not ignorePlayerInside and IsAnyPlayerInsideVehicle(vehicle, playerPeds)) or Entity(vehicle)?.state?.ap_trailer) then return end + + if (GetVehicleNumberPlateText(vehicle) ~= vehicleData.tuning[1]) then + LogDebug("Faulty data on vehicle \"%s\" (\"%s\"). Respawning with correct data.", id, vehicleData.tuning[1]) + + DeleteEntity(vehicle) + savedVehicles[id].handle = nil + spawnQueue[id] = true + return + end + + if (forceUnfreezeVehicles and not vehicleData.isFrozen and IsEntityPositionFrozen(vehicle)) then + FreezeEntityPosition(vehicle, false) + LogDebug("Unfreezing vehicle \"%s\" (\"%s\")", id, vehicleData.tuning[1]) + end + + local newPos = RoundVector3(GetEntityCoords(vehicle), 2) + local newRot = RoundVector3(GetEntityRotation(vehicle), 2) + local newLockStatus = GetVehicleDoorLockStatus(vehicle) + --local attachedToEntity = GetEntityAttachedTo(vehicle) + + local status = vehicleData.status + + local posChange = not vehicleData.extraValues.boatAnchor and #(vehicleData.position - newPos) > 1.0 + local rotChange = GetRotationDifference(vehicleData.rotation, newRot) > 15.0 + local lockChange = newLockStatus ~= status[7] and not ((newLockStatus == 0 and status[7] == 1) or (newLockStatus == 1 and status[7] == 0)) + --local attachmentChange = attachedToEntity ~= vehicleData.attachedTo[1] + + --if (attachmentChange and attachedToEntity and DoesEntityExist(attachedToEntity)) then + -- local attachedTo_Id = Entity(attachedToEntity).state.ap_id + -- if (attachedTo_Id and savedVehicles[attachedTo_Id]) then + -- if (not savedVehicles[attachedTo_Id].attachedVehicles) then + -- savedVehicles[attachedTo_Id].attachedVehicles = {} + -- end + -- + -- table_insert(savedVehicles[attachedTo_Id].attachedVehicles, { id, newPos, newRot }) + -- end + --end + + if (posChange or rotChange or lockChange or attachmentChange) then + local reasons = { "Server side:" } + if (posChange) then reasons[#reasons + 1] = (" - Position from %s to %s"):format(vehicleData.position, newPos) end + if (rotChange) then reasons[#reasons + 1] = (" - Rotation from %s to %s"):format(vehicleData.rotation, newRot) end + if (lockChange) then reasons[#reasons + 1] = (" - Door lock state from %s to %s"):format(status[7], newLockStatus) end + --if (attachmentChange) then reasons[#reasons + 1] = ("Attached from: \"%s\" to \"%s\""):format(vehicleData.attachedTo, attachedToEntity) end + + vehicleData.position = newPos + vehicleData.rotation = newRot + vehicleData.status[7] = newLockStatus + --vehicleData.attachedTo = attachedToEntity + + vehicleData.lastUpdate = os_time() + + UpdateVehicleInDB(id, vehicleData, table_concat(reasons, "\n")) + end +end + +local function AddVehicleToFailedSpawns(id) + if (not failedSpawnList[id]) then + failedSpawnList[id] = 1 + else + failedSpawnList[id] += 1 + end + + if (failedSpawnList[id] >= 5) then + failedSpawnList[id] = nil + LogWarning("There seems to be an issue spawning vehicle \"%s\" (\"%s\")", id, savedVehicles[id].tuning[1]) + end +end + +-- spawn a vehicle from its data +function SpawnVehicle(id, vehicleData) + if (vehicleData.model == nil) then return end + + LogDebug("Creating vehicle \"%s\" (\"%s\") at %s", id, vehicleData.tuning[1], vehicleData.position) + + vehicleData.spawnTime = os_nanotime() + + local vehicle = nil + if (Ox and vehicleData.extraValues.oxId) then + vehicle = Ox:SpawnVehicle(vehicleData.extraValues.oxId, vehicleData.position, vehicleData.rotation.z)?.entity + + if (not vehicle) then + LogDebug("OxVehicle %s does not exist anymore. Deleting from data.", vehicleData.extraValues.oxId) + DeleteVehiclesFromDB(id) + savedVehicles[id] = nil + spawnQueue[id] = nil + return + end + else + vehicle = CreateVehicleServerSetter(vehicleData.model, vehicleData.type, vehicleData.position.x, vehicleData.position.y, vehicleData.position.z, vehicleData.rotation.z) + end + + local timer = GetGameTimer() + if (not WaitUntilVehicleExists(vehicle, 5000)) then + LogDebug("Vehicle didn't exist after spawning \"%s\" (\"%s\")", id, vehicleData.tuning[1]) + + vehicleData.spawnTime = nil + + AddVehicleToFailedSpawns(id) + + return + end + if (not WaitUntilVehicleHasPlateData(vehicle, 5000)) then + LogDebug("No plate set while spawning \"%s\" (\"%s\")", id, vehicleData.tuning[1]) + if (DoesEntityExist(vehicle)) then + DeleteEntity(vehicle) + end + + vehicleData.spawnTime = nil + + AddVehicleToFailedSpawns(id) + + return + end + + if (vehicleData.bucket) then + SetEntityRoutingBucket(vehicle, vehicleData.bucket) + elseif (DEFAULT_BUCKET ~= 0) then + SetEntityRoutingBucket(vehicle, DEFAULT_BUCKET) + end + + LogDebug("Setting properties and state bags for vehicle \"%s\" (\"%s\")", id, vehicleData.tuning[1]) + + -- apply state bags + local state = Entity(vehicle).state + + state.ap_id = id + state.ap_data = { vehicleData.tuning, vehicleData.status, vehicleData.extraValues, vehicleData.position, vehicleData.rotation }--, vehicleData.attachedTo } + + state.ap_spawned = false + + for bagName, bagData in pairs(vehicleData.stateBags) do + state:set(bagName, bagData, true) + + if (not stateBagList[bagName]) then + stateBagList[bagName] = true + end + end + + vehicleData.handle = vehicle + + spawnQueue[id] = nil + + local plate = savedVehicles[id].tuning[1] + + local endTime = GetGameTimer() + SPAWN_TIMEOUT + while (GetGameTimer() < endTime) do + Wait(0) + + if (not savedVehicles[id]) then + LogDebug("Data was deleted before vehicle \"%s\" (\"%s\") was fully created!", id, plate) + break + end + + if (not DoesEntityExist(vehicle)) then + LogDebug("Vehicle \"%s\" (\"%s\") was removed during creation process!", id, plate) + break + end + + if (GetVehicleNumberPlateText(vehicle) == plate and savedVehicles[id].spawnTime) then + LogDebug("Vehicle creation was successful for \"%s\" (\"%s\")! Took %.2fms", id, plate, (os_nanotime() - savedVehicles[id].spawnTime) * 0.000001) + + savedVehicles[id].spawnTime = nil + Entity(vehicle).state.ap_data = nil + + TriggerEvent("AP:vehicleSpawned", savedVehicles[id].handle) + + return + end + end + + LogDebug("Failed setting properties for vehicle \"%s\" (\"%s\")", id, plate) + + AddVehicleToFailedSpawns(id) + + if (DoesEntityExist(vehicle)) then + DeleteEntity(vehicle) + end + if (savedVehicles[id]) then + savedVehicles[id].handle = nil + spawnQueue[id] = true + end +end + +-- triggered from client side to either update or insert a vehicle +RegisterNetEvent("AP:updateVehicle", function(networkId, model, tuning, status, extraValues, reason) + local src = source + + if (not networkId or type(networkId) ~= "number") then + LogDebug("Tried to save vehicle with invalid \"networkId\"!") + return + end + if (not model or type(model) ~= "number") then + LogDebug("Tried to save vehicle with invalid \"model\"!") + return + end + if (not tuning or type(tuning) ~= "table") then + LogDebug("Tried to save vehicle with invalid \"tuning\" data!") + return + end + if (not tuning[1] or type(tuning[1]) ~= "string" or tuning[1]:len() ~= 8) then + LogDebug("Tried to save vehicle with invalid \"plate\"!") + return + end + if (not status or type(status) ~= "table") then + LogDebug("Tried to save vehicle with invalid \"status\" data!") + return + end + if (not extraValues or type(extraValues) ~= "table") then + LogDebug("Tried to save vehicle with invalid \"extraValues\" data!") + return + end + + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (not DoesEntityExist(vehicle)) then + LogDebug("Tried to save entity that does not exist on server side (yet)!") + return + end + + if (saveOnlyOwnedVehicles and not reason:find("Resource") and not IsOwnedVehicle(tuning[1], vehicle)) then + LogDebug("Tried to save unowned vehicle!") + return + end + + local bucket = GetEntityRoutingBucket(vehicle) + if (not multiBucketSupport and bucket ~= DEFAULT_BUCKET) then + LogDebug("Tried to save vehicle from non-default routing bucket %s!", bucket) + return + end + + local id = Entity(vehicle).state.ap_id + if (id and savedVehicles[id]) then + local oxId = savedVehicles[id].extraValues["oxId"] + + -- already exists + savedVehicles[id].status = status + savedVehicles[id].tuning = tuning + savedVehicles[id].extraValues = extraValues + savedVehicles[id].stateBags = GetVehicleStateBags(vehicle) + savedVehicles[id].bucket = bucket + savedVehicles[id].position = RoundVector3(GetEntityCoords(vehicle), 2) + savedVehicles[id].rotation = RoundVector3(GetEntityRotation(vehicle), 2) + savedVehicles[id].lastUpdate = os_time() + savedVehicles[id].lastPlayer = GetPlayerIdentifierByType(src, "license") + + if (oxId) then + savedVehicles[id].extraValues["oxId"] = oxId + end + + UpdateVehicleInDB(id, savedVehicles[id], reason) + else + -- does not exist + if (preventDuplicateVehicles) then + local oldId = GetVehicleIdentifierUsingPlate(tuning[1]) + if (oldId) then + DeleteVehicleUsingIdentifier(oldId) + end + end + + -- enable persistence (for client spawned vehicles) + SetEntityOrphanMode(vehicle, 2) + + id = GetNewVehicleIdentifier() + + Entity(vehicle).state.ap_id = id + + local playerIdentifier = GetPlayerIdentifierByType(src, "license") + + if (Ox) then + extraValues["oxId"] = Ox:GetVehicle(vehicle).id + end + + savedVehicles[id] = { + handle = vehicle, + model = model, + type = GetVehicleType(vehicle), + status = status, + tuning = tuning, + extraValues = extraValues, + stateBags = GetVehicleStateBags(vehicle), + bucket = bucket, + position = RoundVector3(GetEntityCoords(vehicle), 2), + rotation = RoundVector3(GetEntityRotation(vehicle), 2), + lastUpdate = os_time(), + initialPlayer = playerIdentifier, + lastPlayer = playerIdentifier, + spawning = false + } + + InsertVehicleInDB(id, savedVehicles[id], reason) + end +end) + +-- insert vehicle into database +function InsertVehicleInDB(id, vehicleData, reason) + assert(id ~= nil and type(id) == "string", "Parameter \"id\" must be a string!") + + LogDebug("Inserting new vehicle \"%s\" (\"%s\") (Reason: %s)", id, vehicleData.tuning[1], reason) + + Storage.InsertVehicle({ + id, + vehicleData.model, + vehicleData.type, + json_encode(vehicleData.status), + json_encode(vehicleData.tuning), + json_encode(vehicleData.extraValues), + json_encode(vehicleData.stateBags), + vehicleData.bucket, + vehicleData.position.x, vehicleData.position.y, vehicleData.position.z, + vehicleData.rotation.x, vehicleData.rotation.y, vehicleData.rotation.z, + vehicleData.lastUpdate, + vehicleData.initialPlayer, vehicleData.lastPlayer + }) +end + +-- update vehicle in database +function UpdateVehicleInDB(id, vehicleData, reason) + assert(id ~= nil and type(id) == "string", "Parameter \"id\" must be a string!") + + LogDebug("Updating vehicle \"%s\" (\"%s\") (Reason: %s)", id, vehicleData.tuning[1], reason) + + Storage.UpdateVehicle({ + json_encode(vehicleData.status), + json_encode(vehicleData.tuning), + json_encode(vehicleData.extraValues), + json_encode(vehicleData.stateBags), + vehicleData.bucket, + --json_encode(vehicleData.attachedTo), + vehicleData.position.x, vehicleData.position.y, vehicleData.position.z, + vehicleData.rotation.x, vehicleData.rotation.y, vehicleData.rotation.z, + vehicleData.lastUpdate, + vehicleData.lastPlayer, + id + }) +end + +-- delete vehicle(s) from database +function DeleteVehiclesFromDB(...) + local idList = {...} + + if (type(idList[1]) == "table") then + idList = idList[1] + end + if (#idList == 0) then + return + end + + local str = json_encode(idList) + str = str:sub(2, str:len() - 1) + + Storage.DeleteByIds(str) +end + +-- delete vehicles that are still being spawned before actually stopping the resource +AddEventHandler("onResourceStop", function(name) + if (name ~= GetCurrentResourceName()) then + return + end + + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.spawnTime and DoesEntityExist(vehicleData.handle)) then + LogDebug("Deleted vehicle \"%s\" because it was still spawning", id) + DeleteEntity(vehicleData.handle) + end + end +end) + +local function DeleteVehicleUsingData(identifier, networkId, plate, keepInWorld, resourceName) + if (identifier == nil and (networkId == nil or networkId == 0) and plate == nil) then + LogWarning("Tried to delete vehicle without \"id\", \"netId\" and \"plate\"! (Resource: \"%s\")", resourceName) + return false + end + + if (identifier and DeleteVehicleUsingIdentifier(identifier, keepInWorld)) then + LogDebug("Deleting vehicle (id \"%s\"; Resource: \"%s\")", identifier, resourceName) + return true + end + if (networkId and DeleteVehicleUsingNetworkId(networkId, keepInWorld)) then + LogDebug("Deleting vehicle (netId \"%s\"; Resource: \"%s\")", networkId, resourceName) + return true + end + if (plate and DeleteVehicleUsingPlate(plate, keepInWorld)) then + LogDebug("Deleting vehicle (plate \"%s\"; Resource: \"%s\")", plate, resourceName) + return true + end + + LogDebug("Deleting vehicle failed (id \"%s\", netId \"%s\", plate \"%s\"; Resource: \"%s\")", identifier, networkId, plate, resourceName) + return false +end +exports("DeleteVehicleUsingData", function(identifier, networkId, plate, keepInWorld) + return DeleteVehicleUsingData(identifier, networkId, plate, keepInWorld, GetInvokingResource()) +end) + +local function DeleteVehicle(vehicle, keepInWorld, resourceName) + if (not DoesEntityExist(vehicle)) then + LogWarning("Tried to delete vehicle that does not exist! (Entity \"%s\"; Resource: \"%s\")", vehicle, resourceName) + return false + end + + return DeleteVehicleUsingData(Entity(vehicle).state.ap_id, NetworkGetNetworkIdFromEntity(vehicle), GetVehicleNumberPlateText(vehicle), keepInWorld, resourceName) +end +exports("DeleteVehicle", function(vehicle, keepInWorld) + return DeleteVehicle(vehicle, keepInWorld, GetInvokingResource()) +end) + +-- delete vehicle from client side using identifier, network id or plate +RegisterNetEvent("AP:deleteVehicle", function(identifier, networkId, plate, keepInWorld, resourceName) + DeleteVehicleUsingData(identifier, networkId, plate, keepInWorld, resourceName) +end) + +-- delete vehicle using identifier +function DeleteVehicleUsingIdentifier(id, keepInWorld) + if (not savedVehicles[id]) then + return false + end + + if (not keepInWorld and savedVehicles[id].handle and DoesEntityExist(savedVehicles[id].handle)) then + DeleteEntity(savedVehicles[id].handle) + end + + local result, error = pcall(DeleteVehiclesFromDB, id) + if (not result) then + LogError("Error occured while calling \"DeleteVehiclesFromDB\" inside \"DeleteVehicleUsingIdentifier\"!") + LogError("Full error: %s", error) + end + + savedVehicles[id] = nil + spawnQueue[id] = nil + + return true +end + +-- delete vehicle using network id +function DeleteVehicleUsingNetworkId(networkId, keepInWorld) + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (not DoesEntityExist(vehicle)) then + return false + end + + local id = Entity(vehicle)?.state?.ap_id + if (id and savedVehicles[id]) then + if (not keepInWorld) then + DeleteEntity(vehicle) + end + + DeleteVehiclesFromDB(id) + + savedVehicles[id] = nil + spawnQueue[id] = nil + + return true + end + + if (not keepInWorld) then + DeleteEntity(vehicle) + end + + return true +end + +-- delete vehicle using plate +function DeleteVehicleUsingPlate(plate, keepInWorld) + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.tuning[1] == plate or Trim(vehicleData.tuning[1]) == plate) then + if (not keepInWorld and vehicleData.handle and DoesEntityExist(vehicleData.handle)) then + DeleteEntity(vehicleData.handle) + end + + DeleteVehiclesFromDB(id) + + savedVehicles[id] = nil + spawnQueue[id] = nil + + return true + end + end + + if (not keepInWorld) then + local vehicle = TryGetLoadedVehicleFromPlate(plate, GetAllVehicles()) + if (vehicle and DoesEntityExist(vehicle)) then + DeleteEntity(vehicle) + + return true + end + end + + return false +end + +-- update a vehicles state bags in database +function UpdateVehicleStateBagsInDB(id, stateBags) + assert(id ~= nil and type(id) == "string", "Parameter \"id\" must be a string!") + + LogDebug("Updating state bags of vehicle \"%s\" (\"%s\") in database", id, savedVehicles[id].tuning[1]) + + Storage.UpdateStateBags({ + json_encode(stateBags), + id + }) +end + +function GetVehicleStateBags(vehicle) + local stateBags = {} + + local vehicleStateBags = Entity(vehicle).state + + for bagName, _ in pairs(stateBagList) do + if (vehicleStateBags[bagName]) then + stateBags[bagName] = vehicleStateBags[bagName] + end + end + + return stateBags +end + +-- state bag change handler for detecting changes on a vehicle +AddStateBagChangeHandler(nil, nil, function(bagName, key, value, _unused, replicated) + if (key:find("ap_")) then return end + + if (ignoreStateBags) then + for i = 1, #ignoreStateBags do + if (key:find(ignoreStateBags[i])) then return end + end + end + + local entity = GetEntityFromStateBagName(bagName) + if (entity == 0 or GetEntityType(entity) ~= 2) then return end + + if (not stateBagList[key]) then + stateBagList[key] = true + end + + local id = Entity(entity).state.ap_id + if (not id or not savedVehicles[id]) then return end + + if (IsAnyPlayerInsideVehicle(entity, GetAllPlayerPeds())) then return end + + if (savedVehicles[id].stateBags[key] == nil or not TableEquals(savedVehicles[id].stateBags[key], value)) then + -- new state bag OR update + savedVehicles[id].stateBags[key] = value + + if (preventStateBagAutoUpdate) then + for i = 1, #preventStateBagAutoUpdate do + if (key:find(preventStateBagAutoUpdate[i])) then return end + end + end + + UpdateVehicleStateBagsInDB(id, savedVehicles[id].stateBags) + + LogDebug(" Reason: Updating entity state bag \"%s\" for \"%s\". Value: %s", key, id, value) + end +end) + +-- forces a vehicle to update to the database +function ForceVehicleUpdateInDB(id) + if (not id or not savedVehicles[id]) then + return + end + + savedVehicles[id].lastUpdate = os_time() + UpdateVehicleInDB(id, savedVehicles[id], "Resource forced: \"" .. GetInvokingResource() .. "\"") +end +exports("ForceVehicleUpdateInDB", ForceVehicleUpdateInDB) + +-- DEPRECATED: ensures a state bag is saved on the vehicle +function EnsureStateBag() + LogWarning("Executing the \"EnsureStateBag\" export is no longer necessary. Remove it from \"" .. GetInvokingResource() .. "\"!") + + return false +end +exports("EnsureStateBag", EnsureStateBag) + +-- returns a vehicle handle from a given state bag value +function GetVehicleFromStateBagValue(key, value) + for id, vehicleData in pairs(savedVehicles) do + if (TableEquals(vehicleData.stateBags[key], value)) then + return vehicleData.handle + end + end + + return nil +end +exports("GetVehicleFromStateBagValue", GetVehicleFromStateBagValue) + +-- returns all saved state bags from a vehicle +function GetStateBagsFromVehicle(vehicle) + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.handle == vehicle) then + return vehicleData.stateBags + end + end + + return nil +end +exports("GetStateBagsFromVehicle", GetStateBagsFromVehicle) + +-- returns all saved state bags from a vehicle with plate X +function GetStateBagsFromPlate(plate) + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.tuning[1] == plate) then + return vehicleData.stateBags + end + end + + return nil +end +exports("GetStateBagsFromPlate", GetStateBagsFromPlate) + +-- returns all of AP's vehicle data of a specific Vehicle +function GetVehicleData(vehicle) + if (not DoesEntityExist(vehicle)) then return nil end + + local id = Entity(vehicle).state.ap_id + if (not id) then return nil end + + return savedVehicles[id] +end +exports("GetVehicleData", GetVehicleData) + +-- returns all of AP's saved vehicle data of a specific Vehicle +function GetVehicleTuningFromData(vehicle) + if (not DoesEntityExist(vehicle)) then return nil end + + local id = Entity(vehicle).state.ap_id + if (not id) then return nil end + + return savedVehicles[id]?.tuning +end +exports("GetVehicleTuningFromData", GetVehicleTuningFromData) + +-- getting a vehicle position using its plate +local function GetVehiclePosition(plate, resourceName) + if (plate == nil or type(plate) ~= "string") then + LogError("Parameter \"plate\" must be a string! (Export: \"GetVehiclePosition\"; Resource: \"%s\")", resourceName) + return + end + + LogDebug("Position request for \"%s\" (Resource: \"%s\")", plate, resourceName) + + plate = plate:upper() + + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.tuning and (plate == vehicleData.tuning[1] or plate == Trim(vehicleData.tuning[1]))) then + return vehicleData.handle and DoesEntityExist(vehicleData.handle) and GetEntityCoords(vehicleData.handle) or vehicleData.position + end + end + + local vehicles = GetAllVehicles() + for i = 1, #vehicles do + if (DoesEntityExist(vehicles[i])) then + local vehPlate = GetVehicleNumberPlateText(vehicles[i]) + if (plate == vehPlate or plate == Trim(vehPlate)) then + return GetEntityCoords(vehicles[i]) + end + end + end + + return nil +end +exports("GetVehiclePosition", function(plate) + return GetVehiclePosition(plate, GetInvokingResource()) +end) + +-- getting vehicle positions using more than one plate +local function GetVehiclePositions(plates, resourceName) + if (plates == nil or type(plates) ~= "table") then + LogError("Parameter \"plates\" must be a table! (Export: \"GetVehiclePositions\"; Resource: \"%s\")", resourceName) + return {} + end + + for i = 1, #plates do + if (plates[i] == nil or type(plates[i]) ~= "string") then + LogError("Parameter \"plate\" (at index %s) must be a string! (Export: \"GetVehiclePositions\"; Resource: \"%s\")", i, resourceName) + return {} + end + + plates[i] = plates[i]:upper() + end + + LogDebug("Position request for \"%s\" (Resource: \"%s\")", table_concat(plates, "\", \""), resourceName) + + local platePositions = {} + + -- check all loaded vehicles first + local vehicles = GetAllVehicles() + for i = 1, #vehicles do + if (DoesEntityExist(vehicles[i])) then + local vehPlate = GetVehicleNumberPlateText(vehicles[i]) + local trimmedVehPlate = Trim(vehPlate) + + for j = 1, #plates do + if (plates[j] == vehPlate or plates[j] == trimmedVehPlate) then + platePositions[ plates[j] ] = GetEntityCoords(vehicles[i]) + + break + end + end + end + end + + -- then search missing vehicles in APs saved vehicles + for i = 1, #plates do + if (platePositions[ plates[i] ] == nil) then + for id, vehicleData in pairs(savedVehicles) do + local trimmedVehPlate = Trim(vehicleData.tuning[1]) + + if (vehicleData.tuning and (plates[i] == vehicleData.tuning[1] or plates[i] == trimmedVehPlate)) then + platePositions[ plates[i] ] = vehicleData.position + + break + end + end + end + end + + return platePositions +end +exports("GetVehiclePositions", function(plates) + return GetVehiclePositions(plates, GetInvokingResource()) +end) + +-- callbacks for client side getting of vehicle position(s) +local CB = exports["kimi_callbacks"] +CB:Register("AP:getVehiclePosition", function(source, plate, resourceName) + return GetVehiclePosition(plate, resourceName) +end) +CB:Register("AP:getVehiclePositions", function(source, plates, resourceName) + return GetVehiclePositions(plates, resourceName) +end) + +-- command to delete ALL vehicles from the database table. Needs to be executed twice for security reason. +local deleteSavedVehicles = false +RegisterCommand("deleteSavedVehicles", function(source, args, raw) + if (deleteSavedVehicles) then + Storage.DeleteAllVehicles() + + savedVehicles = {} + spawnQueue = {} + + Log("Deleted all vehicles from the vehicle_parking table.") + else + Log("Are you sure that you want to delete all vehicles from the parking list?\nIf yes, execute the command a second time!") + end + + deleteSavedVehicles = not deleteSavedVehicles +end, true) + +-- command to delete ALL vehicles from the database table. Needs to be executed twice for security reason. +local deleteAndStore = false +RegisterCommand("deleteandstore", function(source, args, raw) + if (deleteAndStore) then + local ids = {} + local playerPeds = GetAllPlayerPeds() + + for id, vehicleData in pairs(savedVehicles) do + if (vehicleData.handle and DoesEntityExist(vehicleData.handle)) then + if (IsAnyPlayerInsideVehicle(vehicleData.handle, playerPeds)) then + goto skipVeh + end + + DeleteEntity(vehicleData.handle) + end + + StoreVehicle(vehicleData.tuning[1], vehicleData.handle) + + savedVehicles[id] = nil + spawnQueue[id] = nil + + ids[#ids + 1] = id + + ::skipVeh:: + end + + DeleteVehiclesFromDB(ids) + + Log("Deleted all vehicles from the vehicle_parking table.") + else + Log("Are you sure that you want to delete all vehicles from the parking list and store them?\nIf yes, execute the command a second time!") + end + + deleteAndStore = not deleteAndStore +end, true) + +RegisterCommand("apdv", function(src, args, raw) + local id = args[1] + if (not id) then + LogError("First argument needs to be a plate or identifier!") + return + end + + local success = false + + -- interpret first param as id or plate + if (id:len() == 16) then + success = DeleteVehicleUsingData(id, nil, nil, false, "Command \"apdv\"") + else + success = DeleteVehicleUsingData(nil, nil, id, false, "Command \"apdv\"") + end + + if (success) then + Log("Vehicle \"%s\" deleted through command \"apdv\".", id) + else + Log("Vehicle \"%s\" could not be deleted through command \"apdv\".", id) + end +end, true) + +RegisterCommand("apbring", function(playerId, args, raw) + if (playerId == 0) then + LogError("Command \"apbring\" can only be executed from client side!") + return + end + + local id = args[1] + if (not id) then + LogError("Command \"apbring\": First argument needs to be a plate or identifier!") + return + end + + -- check first arg if it is a plate + if (id:len() == 8) then + id = GetVehicleIdentifierUsingPlate(id) + end + + if (not id or not savedVehicles[id]) then + LogError("Command \"apbring\": Vehicle could not be found!") + return + end + + local ped = GetPlayerPed(playerId) + if (not DoesEntityExist(ped)) then + LogError("Command \"apbring\": Could not find ped to spawn vehicle at!") + return + end + + if (savedVehicles[id].handle and DoesEntityExist(savedVehicles[id].handle)) then + DeleteEntity(savedVehicles[id].handle) + Wait(100) + end + + savedVehicles[id].handle = nil + savedVehicles[id].position = GetEntityCoords(ped) + vector3(2.0, 3.0, 0.0) + savedVehicles[id].rotation = vector3(0.0, 0.0, 0.0) + + Log("Vehicle \"%s\" teleported through command \"apbring\".", id) +end, true) + + + +function UpdatePlate(networkId, newPlate, oldPlate) + if (networkId == nil) then + LogError("\"networkId\" was nil while trying to update a plate!") + return + end + if (newPlate == nil or newPlate:len() > 8) then + LogError("\"newPlate\" was nil or too long while trying to update a plate!") + return + end + + -- format plates + newPlate = Trim(newPlate:upper()) + if (oldPlate) then + oldPlate = Trim(oldPlate:upper()) + end + + -- change plate on vehicle + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (DoesEntityExist(vehicle)) then + SetVehicleNumberPlateText(vehicle, newPlate) + + local found = false + while (not found) do + Wait(0) + + found = Trim(GetVehicleNumberPlateText(vehicle)) == newPlate + end + + newPlate = GetVehicleNumberPlateText(vehicle) + end + + -- search for plate + for id, vehicleData in pairs(savedVehicles) do + if (vehicle == vehicleData.handle) then + local old = vehicleData.tuning[1] + vehicleData.tuning[1] = newPlate + + UpdateVehicleInDB(id, vehicleData, "\"UpdatePlate\" export") + + return + end + end + + -- search for plate by using oldPlate + if (oldPlate) then + newPlate = FillPlateWithSpaces(newPlate) + + for id, vehicleData in pairs(vehicles) do + if (Trim(vehicleData.tuning[1]) == oldPlate) then + vehicleData.tuning[1] = newPlate + + UpdateVehicleInDB(id, vehicleData, "\"UpdatePlate\" export") + + return + end + end + end + + LogDebug("No vehicle found to change plate to \"%s\"", newPlate) +end +exports("UpdatePlate", UpdatePlate) + +RegisterNetEvent("AP:updatePlate", function(networkId, newPlate) + UpdatePlate(networkId, newPlate) +end) + + + +RegisterNetEvent("AP:enteredTrailer", function(trailerNetId) + local trailer = NetworkGetEntityFromNetworkId(trailerNetId) + if (not DoesEntityExist(trailer)) then return end + + Entity(trailer).state:set("ap_trailer", true, true) +end) + +RegisterNetEvent("AP:leftTrailer", function(trailerNetId) + local trailer = NetworkGetEntityFromNetworkId(trailerNetId) + if (not DoesEntityExist(trailer)) then return end + + Entity(trailer).state:set("ap_trailer", nil, true) +end) + + + +-- export and event for force freezing vehicles +if (forceUnfreezeVehicles) then + local function FreezeVehicle(vehicle, freeze) + local id = Entity(vehicle)?.state?.ap_id + if (id and savedVehicles[id]) then + savedVehicles[id].isFrozen = freeze + end + + FreezeEntityPosition(vehicle, freeze) + end + exports("FreezeVehicle", FreezeVehicle) + + RegisterNetEvent("AP:freezeVehicle", function(networkId, freeze) + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (not DoesEntityExist(vehicle)) then return end + + FreezeVehicle(vehicle, freeze) + end) +end + +AddEventHandler("onEntityBucketChange", function(entity, newBucket, oldBucket) + if (GetEntityType(entity) ~= 2) then return end + + local id = Entity(entity).state.ap_id + if (not id or not savedVehicles[id]) then return end + if (savedVehicles[id].bucket == newBucket) then return end + + savedVehicles[id].bucket = newBucket + + LogDebug("Updating vehicle \"%s\" (\"%s\") (Reason: %s)", id, savedVehicles[id].tuning[1], ("Routing bucket from %s to %s"):format(oldBucket, newBucket)) + + Storage.UpdateBucket(newBucket, id) +end) + +RegisterNetEvent("AP:onVehicleDamage", function(networkId, status) + local vehicle = NetworkGetEntityFromNetworkId(networkId) + if (not DoesEntityExist(vehicle)) then return end + + local id = Entity(vehicle).state.ap_id + if (not id or not savedVehicles[id]) then return end + + savedVehicles[id].status = status + + LogDebug("Updating status for vehicle \"%s\" (\"%s\") (Reason: %s)", id, savedVehicles[id].tuning[1], "Vehicle damage") + + Storage.UpdateStatus(json_encode(status), id) +end) diff --git a/resources/[carscripts]/AdvancedParking/server/storage/kimi_sql.lua b/resources/[carscripts]/AdvancedParking/server/storage/kimi_sql.lua new file mode 100644 index 000000000..f09db4eab Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/server/storage/kimi_sql.lua differ diff --git a/resources/[carscripts]/AdvancedParking/server/storage/oxmysql.lua b/resources/[carscripts]/AdvancedParking/server/storage/oxmysql.lua new file mode 100644 index 000000000..1e057fc75 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/server/storage/oxmysql.lua @@ -0,0 +1,178 @@ + +if (GetResourceState("oxmysql") == "missing") then return end + +local tableName, colName = nil, nil +local function GetOwnedVehiclesTableName() + if (tableName ~= nil) then + return tableName + end + + tableName = (GetResourceState("es_extended") ~= "missing") and "owned_vehicles" or "player_vehicles" + return tableName +end +local function GetStoredColumnName() + if (colName ~= nil) then + return colName + end + + colName = (GetResourceState("es_extended") ~= "missing") and "stored" or "state" + return colName +end + +local oxmysql = exports["oxmysql"] + +local function DoesColumnExist(colName) + return oxmysql:scalar_async([[ + SELECT COUNT(*) FROM `INFORMATION_SCHEMA`.`COLUMNS` + WHERE `TABLE_SCHEMA` = DATABASE() AND `TABLE_NAME` = ? AND `COLUMN_NAME` = ?; + ]], { "vehicle_parking", colName }) > 0 +end + +Storage.Create = function() + oxmysql:update_async([[ + CREATE TABLE IF NOT EXISTS `vehicle_parking` ( + `id` varchar(16) NOT NULL, + `model` int(11) NOT NULL, + `type` varchar(16) NOT NULL, + `status` text NOT NULL, + `tuning` text NOT NULL, + `extraValues` text NOT NULL DEFAULT '[]', + `stateBags` longtext NOT NULL DEFAULT '[]', + `bucket` int(11) NOT NULL DEFAULT '0', + `posX` float NOT NULL, + `posY` float NOT NULL, + `posZ` float NOT NULL, + `rotX` float NOT NULL, + `rotY` float NOT NULL, + `rotZ` float NOT NULL, + `lastUpdate` int(11) NOT NULL DEFAULT '0', + `initialPlayer` varchar(50), + `lastPlayer` varchar(50), + PRIMARY KEY (`id`) + ); + ]]) + + -- v3 backwards compatibility + if (not DoesColumnExist("bucket")) then + oxmysql:update_async([[ + ALTER TABLE `vehicle_parking` + ADD COLUMN `bucket` INT(11) NOT NULL DEFAULT 0 AFTER `stateBags`; + ]]) + end + if (not DoesColumnExist("initialPlayer")) then + oxmysql:update_async([[ + ALTER TABLE `vehicle_parking` + ADD COLUMN `initialPlayer` varchar(50) AFTER `lastUpdate`; + ]]) + end + if (not DoesColumnExist("lastPlayer")) then + oxmysql:update_async([[ + ALTER TABLE `vehicle_parking` + ADD COLUMN `lastPlayer` varchar(50) AFTER `initialPlayer`; + ]]) + end + if (not DoesColumnExist("extraValues")) then + oxmysql:update_async([[ + ALTER TABLE `vehicle_parking` + ADD COLUMN `extraValues` TEXT NOT NULL DEFAULT '[]' AFTER `tuning`; + ]]) + end + oxmysql:update_async([[ + ALTER TABLE `vehicle_parking` + MODIFY COLUMN `stateBags` longtext NOT NULL DEFAULT '[]'; + ]]) +end + +Storage.GetAllVehicles = function() + return oxmysql:query_async([[ + SELECT `id`, `model`, `type`, `status`, `tuning`, `extraValues`, `stateBags`, `bucket`, `posX`, `posY`, `posZ`, `rotX`, `rotY`, `rotZ`, `lastUpdate`, `initialPlayer`, `lastPlayer` + FROM `vehicle_parking`; + ]]) +end + +Storage.DeleteById = function(id) + oxmysql:update([[ + DELETE FROM `vehicle_parking` + WHERE `id` = ?; + ]], { id }) +end + +Storage.DeleteByIds = function(formattedIds) + oxmysql:update(([[ + DELETE FROM `vehicle_parking` + WHERE `id` IN (%s); + ]]):format(formattedIds)) +end + +Storage.StoreVehicleInGarage = function(params) + oxmysql:update(([[ + UPDATE `%s` SET `%s` = 1 + WHERE `plate` = ? OR `plate` = ?; + ]]):format(GetOwnedVehiclesTableName(), GetStoredColumnName()), params) +end + +Storage.InsertVehicle = function(params) + oxmysql:insert([[ + INSERT INTO `vehicle_parking` (`id`, `model`, `type`, `status`, `tuning`, `extraValues`, `stateBags`, `bucket`, `posX`, `posY`, `posZ`, `rotX`, `rotY`, `rotZ`, `lastUpdate`, `initialPlayer`, `lastPlayer`) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + ]], params) +end + +Storage.UpdateVehicle = function(params) + oxmysql:update([[ + UPDATE `vehicle_parking` + SET `status` = ?, `tuning` = ?, `extraValues` = ?, + `stateBags` = ?, + `bucket` = ?, + `posX` = ?, `posY` = ?, `posZ` = ?, + `rotX` = ?, `rotY` = ?, `rotZ` = ?, + `lastUpdate` = ?, + `lastPlayer` = ? + WHERE `id` = ?; + ]], params) +end + +Storage.UpdateBucket = function(bucket, id) + oxmysql:update([[ + UPDATE `vehicle_parking` + SET `bucket` = ? + WHERE `id` = ?; + ]], { bucket, id }) +end + +Storage.UpdateStatus = function(status, id) + oxmysql:update([[ + UPDATE `vehicle_parking` + SET `status` = ? + WHERE `id` = ?; + ]], { status, id }) +end + +Storage.UpdatePosition = function(position, rotation, id) + oxmysql:update([[ + UPDATE `vehicle_parking` + SET `posX` = ?, `posY` = ?, `posZ` = ?, + `rotX` = ?, `rotY` = ?, `rotZ` = ?, + WHERE `id` = ?; + ]], { position.x, position.y, position.z, rotation.x, rotation.y, rotation.z, id }) +end + +Storage.UpdateStateBags = function(params) + oxmysql:update([[ + UPDATE `vehicle_parking` + SET `stateBags` = ? + WHERE `id` = ?; + ]], params) +end + +Storage.IsVehicleOwned = function(params) + return oxmysql:scalar_async(([[ + SELECT `plate` + FROM `%s` + WHERE `plate` = ? OR `plate` = ?; + ]]):format(GetOwnedVehiclesTableName()), params) +end + +Storage.DeleteAllVehicles = function() + oxmysql:update("DELETE FROM `vehicle_parking`;") +end diff --git a/resources/[carscripts]/AdvancedParking/server/storage/storage.lua b/resources/[carscripts]/AdvancedParking/server/storage/storage.lua new file mode 100644 index 000000000..5e69c848c Binary files /dev/null and b/resources/[carscripts]/AdvancedParking/server/storage/storage.lua differ diff --git a/resources/[carscripts]/AdvancedParking/server/sv_integrations.lua b/resources/[carscripts]/AdvancedParking/server/sv_integrations.lua new file mode 100644 index 000000000..fb8cfb229 --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/server/sv_integrations.lua @@ -0,0 +1,30 @@ + +local Ox = GetResourceState("ox_core") == "started" and exports["ox_core"] or nil + +function IsOwnedVehicle(plate, vehicle) + if (Ox) then + return Ox:GetVehicle(vehicle) + end + + local results = Storage.IsVehicleOwned({ plate, Trim(plate) }) + + if (not results) then + return false + end + + return #results > 0 +end + +function StoreVehicle(plate, vehicle) + if (Ox) then + if (vehicle) then + local oxVeh = Ox:GetVehicle(vehicle) + if (oxVeh) then + oxVeh.setStored() + end + end + return + end + + Storage.StoreVehicleInGarage({ plate, Trim(plate) }) +end diff --git a/resources/[carscripts]/AdvancedParking/server/sv_utils.lua b/resources/[carscripts]/AdvancedParking/server/sv_utils.lua new file mode 100644 index 000000000..fa46dda0a --- /dev/null +++ b/resources/[carscripts]/AdvancedParking/server/sv_utils.lua @@ -0,0 +1,74 @@ + +-- returns if any player is inside a given vehicle +function IsAnyPlayerInsideVehicle(vehicle, playerPeds) + for i = 1, #playerPeds do + local veh = GetVehiclePedIsIn(playerPeds[i], false) + + if (DoesEntityExist(veh) and veh == vehicle) then + return true + end + end + + return false +end + +-- return the id and distance of the closest player +function GetClosestPlayer(position, maxRadius, players, playerPedsWithHandles, bucket) + local closestDistance = maxRadius or 1000.0 + local closestPlayer = nil + + for i = 1, #players do + if (GetPlayerRoutingBucket(players[i]) == bucket) then + local distance = #(position - GetEntityCoords(playerPedsWithHandles[ players[i] ])) + if (distance < closestDistance) then + closestDistance = distance + closestPlayer = players[i] + end + end + end + + return closestPlayer, closestDistance +end + +-- return all player peds associated to their player handles +function GetAllPlayerPedsWithHandles(players) + local peds = {} + + for i = 1, #players do + local ped = GetPlayerPed(players[i]) + peds[players[i]] = DoesEntityExist(ped) and ped or nil + end + + return peds +end + +-- returns all currently loaded player peds +function GetAllPlayerPeds() + local playerPeds = {} + + local players = GetPlayers() + for i = 1, #players do + local ped = GetPlayerPed(players[i]) + if (DoesEntityExist(ped)) then + playerPeds[#playerPeds + 1] = ped + end + end + + return playerPeds +end + +-- returns a list of all vehicles that are loaded and are registered within AP already +function GetLoadedVehiclesWithId(vehicles) + local list = {} + + for i = 1, #vehicles do + if (DoesEntityExist(vehicles[i])) then + local id = Entity(vehicles[i])?.state?.ap_id + if (id) then + list[id] = vehicles[i] + end + end + end + + return list +end diff --git a/resources/[carscripts]/jg-advancedgarages/fxmanifest.lua b/resources/[carscripts]/jg-advancedgarages/fxmanifest.lua index 2b292bdaf..a5b541407 100644 --- a/resources/[carscripts]/jg-advancedgarages/fxmanifest.lua +++ b/resources/[carscripts]/jg-advancedgarages/fxmanifest.lua @@ -53,4 +53,6 @@ escrow_ignore { "server/sv-webhooks.lua" } -dependency '/assetpacks' \ No newline at end of file +dependency '/assetpacks' + +shared_script "@AdvancedParking/fixDeleteVehicle.lua" \ No newline at end of file diff --git a/resources/[carscripts]/nordi_antidespawn/client/main.lua b/resources/[carscripts]/nordi_antidespawn/client/main.lua deleted file mode 100644 index ddcefb89b..000000000 --- a/resources/[carscripts]/nordi_antidespawn/client/main.lua +++ /dev/null @@ -1,1039 +0,0 @@ -local QBCore = exports['qb-core']:GetCoreObject() -local trackedVehicles = {} -local lastKnownCoords = {} -local garagePending = {} -- Fahrzeuge, die gerade in die Garage gestellt werden -local spawnedVehicles = {} -- Track recently spawned vehicles to prevent duplication -local vehicleOwnership = {} -- Cache vehicle ownership status -local vehicleExistsCallbacks = {} -- Callbacks for vehicle existence checks - --- Helper function to count table entries -function tableLength(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end - --- Debug Funktion -local function Debug(msg) - if Config.Debug then - print("[AntiDespawn] " .. msg) - end -end - --- Function to check if a vehicle is a temporary/special vehicle (like police cars, ambulances, etc.) -local function IsTemporaryVehicle(vehicle) - local plate = QBCore.Functions.GetPlate(vehicle) - - -- Check for common temporary vehicle patterns - if not plate or plate == "" then return true end - - -- Some servers use specific patterns for temporary vehicles - if string.match(plate, "^TEMP%d%d%d%d%d$") then return true end - if string.match(plate, "^[A-Z][A-Z][A-Z][A-Z]%d%d%d$") and GetEntityModel(vehicle) == GetHashKey("police") then return true end - - -- Add more patterns as needed for your server - - return false -end - --- Function to check if a vehicle might be a job vehicle -local function IsLikelyJobVehicle(vehicle) - local model = GetEntityModel(vehicle) - - -- List of common job vehicle models - local jobVehicles = { - "police", "police2", "police3", "police4", - "ambulance", "firetruk", - "taxi", - "flatbed", "towtruck", "towtruck2", - -- Add more as needed - } - - for _, jobVehicle in ipairs(jobVehicles) do - if model == GetHashKey(jobVehicle) then - return true - end - end - - return false -end - --- Anti-Duplication: Check if a vehicle with this plate already exists in the world -local function DoesVehicleExistInWorld(plate) - local vehicles = GetGamePool('CVehicle') - local count = 0 - - for _, vehicle in pairs(vehicles) do - local vehPlate = QBCore.Functions.GetPlate(vehicle) - if vehPlate == plate then - count = count + 1 - if count > 1 then - -- More than one vehicle with this plate exists! - Debug("DUPLICATION DETECTED: Multiple vehicles with plate " .. plate .. " exist!") - return true - end - end - end - - return false -end - --- Anti-Duplication: Check if vehicle was recently spawned -local function WasVehicleRecentlySpawned(plate) - if spawnedVehicles[plate] then - local timeSinceSpawn = GetGameTimer() - spawnedVehicles[plate] - if timeSinceSpawn < 60000 then -- 60 seconds cooldown - Debug("Anti-Dupe: Vehicle " .. plate .. " was recently spawned (" .. math.floor(timeSinceSpawn/1000) .. " seconds ago)") - return true - else - -- Reset the timer if it's been more than 60 seconds - spawnedVehicles[plate] = nil - end - end - return false -end - --- Anti-Duplication: Mark vehicle as recently spawned -local function MarkVehicleAsSpawned(plate) - spawnedVehicles[plate] = GetGameTimer() - Debug("Anti-Dupe: Marked vehicle " .. plate .. " as recently spawned") - - -- Clean up old entries every 5 minutes - SetTimeout(300000, function() - for p, time in pairs(spawnedVehicles) do - if GetGameTimer() - time > 300000 then -- 5 minutes - spawnedVehicles[p] = nil - end - end - end) -end - --- Function to check if player owns the vehicle with caching -local function DoesPlayerOwnVehicle(plate) - -- Check cache first - if vehicleOwnership[plate] ~= nil then - -- Cache expires after 5 minutes - local timeSinceCheck = GetGameTimer() - vehicleOwnership[plate].timestamp - if timeSinceCheck < 300000 then -- 5 minutes - return vehicleOwnership[plate].owned - end - end - - local playerData = QBCore.Functions.GetPlayerData() - if not playerData then return false end - - -- Trigger server event to check ownership and wait for response - local isOwned = nil - - -- Request ownership check from server - TriggerServerEvent('antidespawn:server:checkVehicleOwnership', plate) - - -- Register one-time event handler for the response - local eventHandler = AddEventHandler('antidespawn:client:vehicleOwnershipResult', function(result, checkPlate) - if plate == checkPlate then - isOwned = result - -- Cache the result - vehicleOwnership[plate] = { - owned = result, - timestamp = GetGameTimer() - } - end - end) - - -- Wait for response with timeout - local timeout = 0 - while isOwned == nil and timeout < 50 do - Wait(10) - timeout = timeout + 1 - end - - -- Remove event handler - RemoveEventHandler(eventHandler) - - return isOwned == true -end - --- Function to check if a vehicle exists in the database -local function CheckVehicleExists(plate, callback) - vehicleExistsCallbacks[plate] = callback - TriggerServerEvent('antidespawn:server:checkVehicleExists', plate) - - -- Set a timeout to clean up the callback if no response is received - SetTimeout(5000, function() - if vehicleExistsCallbacks[plate] then - vehicleExistsCallbacks[plate](false) -- Assume it doesn't exist if no response - vehicleExistsCallbacks[plate] = nil - end - end) -end - --- Register the event handler for vehicle existence check -RegisterNetEvent('antidespawn:client:vehicleExistsResult', function(exists, plate) - if vehicleExistsCallbacks[plate] then - vehicleExistsCallbacks[plate](exists) - vehicleExistsCallbacks[plate] = nil - end -end) - --- Funktion um zu prüfen ob Fahrzeugklasse erlaubt ist -local function IsVehicleClassAllowed(vehicle) - local vehicleClass = GetVehicleClass(vehicle) - - -- Prüfe Blacklist - for _, blacklistedClass in pairs(Config.BlacklistedVehicleClasses) do - if vehicleClass == blacklistedClass then - return false - end - end - - -- Prüfe Whitelist - for _, allowedClass in pairs(Config.AllowedVehicleClasses) do - if vehicleClass == allowedClass then - return true - end - end - - return false -end - --- Extrem starke Anti-Despawn Funktion -local function PreventDespawn(vehicle) - if not DoesEntityExist(vehicle) then return false end - - -- Grundlegende Persistenz - SetEntityAsMissionEntity(vehicle, true, true) - SetVehicleHasBeenOwnedByPlayer(vehicle, true) - SetVehicleNeedsToBeHotwired(vehicle, false) - - -- Zusätzliche Flags - SetEntityLoadCollisionFlag(vehicle, true) - SetVehicleIsStolen(vehicle, false) - SetVehicleIsWanted(vehicle, false) - - -- Verhindere dass das Fahrzeug als "abandoned" markiert wird - if DecorIsRegisteredAsType("IgnoredByQuickSave", 2) then - DecorSetBool(vehicle, "IgnoredByQuickSave", false) - end - - -- Setze Fahrzeug auf Boden - SetVehicleOnGroundProperly(vehicle) - - -- Verhindere dass das Fahrzeug gelöscht wird - NetworkRegisterEntityAsNetworked(vehicle) - local netID = NetworkGetNetworkIdFromEntity(vehicle) - SetNetworkIdExistsOnAllMachines(netID, true) - SetNetworkIdCanMigrate(netID, true) - - return true -end - --- Funktion um Fahrzeugmods zu erhalten -local function GetVehicleMods(vehicle) - local mods = {} - - -- Basis Mods - for i = 0, 49 do - mods[tostring(i)] = GetVehicleMod(vehicle, i) - end - - -- Extras - mods.extras = {} - for i = 1, 12 do - if DoesExtraExist(vehicle, i) then - mods.extras[tostring(i)] = IsVehicleExtraTurnedOn(vehicle, i) - end - end - - -- Farben - local primaryColor, secondaryColor = GetVehicleColours(vehicle) - local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) - - mods.colors = { - primary = primaryColor, - secondary = secondaryColor, - pearlescent = pearlescentColor, - wheels = wheelColor - } - - -- Custom Farben - local hasCustomPrimaryColor = GetIsVehiclePrimaryColourCustom(vehicle) - if hasCustomPrimaryColor then - local r, g, b = GetVehicleCustomPrimaryColour(vehicle) - mods.customPrimaryColor = {r = r, g = g, b = b} - end - - local hasCustomSecondaryColor = GetIsVehicleSecondaryColourCustom(vehicle) - if hasCustomSecondaryColor then - local r, g, b = GetVehicleCustomSecondaryColour(vehicle) - mods.customSecondaryColor = {r = r, g = g, b = b} - end - - -- Neon - mods.neon = { - left = IsVehicleNeonLightEnabled(vehicle, 0), - right = IsVehicleNeonLightEnabled(vehicle, 1), - front = IsVehicleNeonLightEnabled(vehicle, 2), - back = IsVehicleNeonLightEnabled(vehicle, 3) - } - - local r, g, b = GetVehicleNeonLightsColour(vehicle) - mods.neonColor = {r = r, g = g, b = b} - - -- Xenon - mods.xenonColor = GetVehicleXenonLightsColour(vehicle) - mods.xenonEnabled = IsToggleModOn(vehicle, 22) - - -- Livery - mods.livery = GetVehicleLivery(vehicle) - - -- Fenster Tint - mods.windowTint = GetVehicleWindowTint(vehicle) - - -- Rad Typ - mods.wheelType = GetVehicleWheelType(vehicle) - - -- Rauch Farbe - local r, g, b = GetVehicleTyreSmokeColor(vehicle) - mods.tyreSmokeColor = {r = r, g = g, b = b} - - -- Dashboard & Interior Farbe - mods.dashboardColor = GetVehicleDashboardColour(vehicle) - mods.interiorColor = GetVehicleInteriorColour(vehicle) - - -- Toggles - mods.bulletProofTires = not GetVehicleTyresCanBurst(vehicle) - mods.turbo = IsToggleModOn(vehicle, 18) - mods.xeonHeadlights = IsToggleModOn(vehicle, 22) - - return mods -end - --- Funktion um Fahrzeugmods zu setzen -local function SetVehicleMods(vehicle, mods) - if not mods then return end - - -- Setze Modkit - SetVehicleModKit(vehicle, 0) - - -- Rad Typ zuerst setzen - if mods.wheelType then - SetVehicleWheelType(vehicle, mods.wheelType) - end - - -- Basis Mods - for i = 0, 49 do - if mods[tostring(i)] ~= nil then - SetVehicleMod(vehicle, i, mods[tostring(i)], false) - end - end - - -- Extras - if mods.extras then - for i = 1, 12 do - if mods.extras[tostring(i)] ~= nil then - SetVehicleExtra(vehicle, i, not mods.extras[tostring(i)]) - end - end - end - - -- Farben - if mods.colors then - SetVehicleColours(vehicle, mods.colors.primary or 0, mods.colors.secondary or 0) - SetVehicleExtraColours(vehicle, mods.colors.pearlescent or 0, mods.colors.wheels or 0) - end - - -- Custom Farben - if mods.customPrimaryColor then - SetVehicleCustomPrimaryColour(vehicle, mods.customPrimaryColor.r, mods.customPrimaryColor.g, mods.customPrimaryColor.b) - end - - if mods.customSecondaryColor then - SetVehicleCustomSecondaryColour(vehicle, mods.customSecondaryColor.r, mods.customSecondaryColor.g, mods.customSecondaryColor.b) - end - - -- Neon - if mods.neon then - SetVehicleNeonLightEnabled(vehicle, 0, mods.neon.left or false) - SetVehicleNeonLightEnabled(vehicle, 1, mods.neon.right or false) - SetVehicleNeonLightEnabled(vehicle, 2, mods.neon.front or false) - SetVehicleNeonLightEnabled(vehicle, 3, mods.neon.back or false) - end - - if mods.neonColor then - SetVehicleNeonLightsColour(vehicle, mods.neonColor.r, mods.neonColor.g, mods.neonColor.b) - end - - -- Xenon - if mods.xenonEnabled then - ToggleVehicleMod(vehicle, 22, true) - if mods.xenonColor then - SetVehicleXenonLightsColour(vehicle, mods.xenonColor) - end - end - - -- Livery - if mods.livery then - SetVehicleLivery(vehicle, mods.livery) - end - - -- Fenster Tint - if mods.windowTint then - SetVehicleWindowTint(vehicle, mods.windowTint) - end - - -- Rauch Farbe - if mods.tyreSmokeColor then - ToggleVehicleMod(vehicle, 20, true) -- Aktiviere Rauch - SetVehicleTyreSmokeColor(vehicle, mods.tyreSmokeColor.r, mods.tyreSmokeColor.g, mods.tyreSmokeColor.b) - end - - -- Dashboard & Interior Farbe - if mods.dashboardColor then - SetVehicleDashboardColour(vehicle, mods.dashboardColor) - end - - if mods.interiorColor then - SetVehicleInteriorColour(vehicle, mods.interiorColor) - end - - -- Toggles - if mods.bulletProofTires ~= nil then - SetVehicleTyresCanBurst(vehicle, not mods.bulletProofTires) - end - - if mods.turbo ~= nil then - ToggleVehicleMod(vehicle, 18, mods.turbo) - end - - -- Setze Felgen nochmal explizit - if mods["23"] ~= nil then -- Vorderräder - SetVehicleMod(vehicle, 23, mods["23"], false) - end - - if mods["24"] ~= nil then -- Hinterräder - SetVehicleMod(vehicle, 24, mods["24"], false) - end -end - --- Hilfsfunktion um Fahrzeug anhand Kennzeichen zu finden -function GetVehicleByPlate(plate) - local vehicles = GetGamePool('CVehicle') - for _, vehicle in pairs(vehicles) do - if QBCore.Functions.GetPlate(vehicle) == plate then - return vehicle - end - end - return nil -end - --- Event Handler für Fahrzeug betreten (nur Fahrersitz) -CreateThread(function() - while true do - Wait(1000) - - local playerPed = PlayerPedId() - local currentVehicle = GetVehiclePedIsIn(playerPed, false) - - -- Spieler ist als Fahrer in ein Fahrzeug eingestiegen - if currentVehicle ~= 0 then - -- Prüfe ob Spieler auf Fahrersitz ist - local driver = GetPedInVehicleSeat(currentVehicle, -1) - - -- Nur wenn Spieler der Fahrer ist (Seat -1) - if driver == playerPed and IsVehicleClassAllowed(currentVehicle) then - local plate = QBCore.Functions.GetPlate(currentVehicle) - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping vehicle with invalid plate") - goto continue - end - - -- Skip temporary vehicles - if IsTemporaryVehicle(currentVehicle) then - Debug("Skipping temporary vehicle") - goto continue - end - - -- Skip likely job vehicles - if IsLikelyJobVehicle(currentVehicle) then - Debug("Skipping likely job vehicle") - goto continue - end - - -- Skip recently spawned vehicles - if WasVehicleRecentlySpawned(plate) then - Debug("Skipping recently spawned vehicle: " .. plate) - goto continue - end - - -- Anti-Duplication: Check if this plate already exists multiple times - if DoesVehicleExistInWorld(plate) then - Debug("Anti-Dupe: Detected duplicate vehicle with plate " .. plate .. ", not tracking") - goto continue - end - - -- Check if this vehicle is already being tracked - if not trackedVehicles[plate] and not garagePending[plate] then - -- First verify this vehicle exists in the database - CheckVehicleExists(plate, function(exists) - if not exists then - Debug("Vehicle not in database, not tracking: " .. plate) - return - end - - -- Track all vehicles that exist in the database - trackedVehicles[plate] = currentVehicle - - -- Speichere letzte bekannte Position - lastKnownCoords[plate] = GetEntityCoords(currentVehicle) - - -- Sofort starke Despawn-Verhinderung - PreventDespawn(currentVehicle) - - Debug("Fahrzeug wird nun getrackt: " .. plate) - - -- Hole Fahrzeugmods - local vehicleMods = GetVehicleMods(currentVehicle) - - -- Registriere Fahrzeug beim Server - local vehicleCoords = GetEntityCoords(currentVehicle) - local vehicleHeading = GetEntityHeading(currentVehicle) - local vehicleModel = GetEntityModel(currentVehicle) - - TriggerServerEvent('antidespawn:server:registerVehicle', plate, vehicleModel, vehicleCoords, vehicleHeading, vehicleMods) - end) - end - end - end - ::continue:: - end -end) - --- Kontinuierliche Despawn-Verhinderung für alle getrackten Fahrzeuge -CreateThread(function() - while true do - Wait(5000) -- Alle 5 Sekunden - - local playerPed = PlayerPedId() - local playerPos = GetEntityCoords(playerPed) - - for plate, vehicle in pairs(trackedVehicles) do - -- Anti-Duplication: Check if multiple vehicles with this plate exist - if DoesVehicleExistInWorld(plate) then - Debug("Anti-Dupe: Detected duplicate during tracking for plate " .. plate .. ", removing from tracking") - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - TriggerServerEvent('antidespawn:server:removeVehicle', plate) - goto continue - end - - -- Prüfe ob Fahrzeug gerade in die Garage gestellt wird - if garagePending[plate] then - Debug("Fahrzeug wird gerade in Garage gestellt, entferne aus Tracking: " .. plate) - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - TriggerServerEvent('antidespawn:server:removeVehicle', plate) - elseif DoesEntityExist(vehicle) then - -- Check distance to player - local vehiclePos = GetEntityCoords(vehicle) - local distance = #(playerPos - vehiclePos) - - if distance > 500.0 then -- 500 units = about 500 meters - Debug("Fahrzeug zu weit entfernt, entferne aus Tracking: " .. plate) - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - TriggerServerEvent('antidespawn:server:removeVehicle', plate) - else - PreventDespawn(vehicle) - - -- Aktualisiere letzte bekannte Position - lastKnownCoords[plate] = vehiclePos - - -- Hole Fahrzeugmods - local vehicleMods = GetVehicleMods(vehicle) - - -- Aktualisiere Position - local vehicleHeading = GetEntityHeading(vehicle) - - TriggerServerEvent('antidespawn:server:updateVehicle', plate, vehiclePos, vehicleHeading, vehicleMods) - - Debug("Aktualisiere Fahrzeug: " .. plate) - end - else - Debug("Fahrzeug existiert nicht mehr: " .. plate) - - -- Anti-Duplication: Check if vehicle was recently spawned before respawning - if WasVehicleRecentlySpawned(plate) then - Debug("Anti-Dupe: Not respawning recently spawned vehicle: " .. plate) - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - goto continue - end - - -- Store the plate and coords for use in the callback - local currentPlate = plate - local currentCoords = lastKnownCoords[plate] - - -- Verify this vehicle still exists in the database before respawning - CheckVehicleExists(plate, function(exists) - if not exists then - Debug("Vehicle no longer in database, not respawning: " .. currentPlate) - trackedVehicles[currentPlate] = nil - lastKnownCoords[currentPlate] = nil - return - end - - -- Versuche Fahrzeug wiederherzustellen, aber nur wenn es nicht in die Garage gestellt wird - if currentCoords and not garagePending[currentPlate] then - Debug("Versuche Fahrzeug wiederherzustellen: " .. currentPlate) - TriggerServerEvent('antidespawn:server:respawnVehicle', currentPlate) - - -- Entferne aus lokaler Tracking-Liste, wird nach Respawn wieder hinzugefügt - trackedVehicles[currentPlate] = nil - lastKnownCoords[currentPlate] = nil - else - -- Entferne aus Tracking - trackedVehicles[currentPlate] = nil - lastKnownCoords[currentPlate] = nil - end - end) - - -- Remove from tracking immediately to avoid processing it again while waiting for the callback - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - end - ::continue:: - end - end -end) - - --- Lade Fahrzeuge beim Spawn -RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() - Debug("Spieler geladen, warte vor dem Laden der Fahrzeuge...") - -- Längere Wartezeit, um sicherzustellen, dass alles geladen ist - Wait(15000) - TriggerServerEvent('antidespawn:server:loadVehicles') -end) - --- Automatisches Laden beim Resource Start -CreateThread(function() - -- Längere Wartezeit beim Serverstart - Wait(20000) - - -- Prüfe ob Spieler eingeloggt ist - local playerData = QBCore.Functions.GetPlayerData() - if playerData and playerData.citizenid then - Debug("Resource gestartet, lade Fahrzeuge...") - TriggerServerEvent('antidespawn:server:loadVehicles') - else - -- Warte auf Login, wenn Spieler noch nicht eingeloggt ist - Debug("Warte auf Spieler-Login...") - while true do - Wait(5000) - playerData = QBCore.Functions.GetPlayerData() - if playerData and playerData.citizenid then - Debug("Spieler jetzt eingeloggt, lade Fahrzeuge...") - TriggerServerEvent('antidespawn:server:loadVehicles') - break - end - end - end -end) - --- Spawne ein Fahrzeug -RegisterNetEvent('antidespawn:client:spawnVehicle', function(data) - Debug("Spawne Fahrzeug: " .. data.plate) - - -- Skip vehicles with empty or invalid plates - if not data.plate or data.plate == "" or string.len(data.plate) < 2 then - Debug("Skipping spawn of vehicle with invalid plate") - return - end - - -- Anti-Duplication: Check if vehicle was recently spawned - if WasVehicleRecentlySpawned(data.plate) then - Debug("Anti-Dupe: Blocking respawn of recently spawned vehicle: " .. data.plate) - return - end - - -- Prüfe ob Fahrzeug bereits existiert - local existingVehicle = GetVehicleByPlate(data.plate) - if existingVehicle then - Debug("Fahrzeug existiert bereits: " .. data.plate) - trackedVehicles[data.plate] = existingVehicle - lastKnownCoords[data.plate] = GetEntityCoords(existingVehicle) - PreventDespawn(existingVehicle) - return - end - - -- Prüfe ob Fahrzeug gerade in die Garage gestellt wird - if garagePending[data.plate] then - Debug("Fahrzeug wird gerade in Garage gestellt, nicht spawnen: " .. data.plate) - return - end - - -- Verify this vehicle exists in the database before spawning - CheckVehicleExists(data.plate, function(exists) - if not exists then - Debug("Vehicle not in database, not spawning: " .. data.plate) - return - end - - -- Konvertiere Modell zu Hash wenn nötig - local modelHash = data.model - if type(modelHash) == "string" then - -- Versuche den String als Hash zu interpretieren - if tonumber(modelHash) then - modelHash = tonumber(modelHash) - else - -- Versuche den String als Modellnamen zu interpretieren - modelHash = GetHashKey(modelHash) - end - end - - Debug("Versuche Modell zu laden: " .. tostring(modelHash)) - - -- Prüfe ob Modell existiert - if not IsModelInCdimage(modelHash) then - Debug("Modell existiert nicht in CD Image: " .. tostring(modelHash)) - return - end - - RequestModel(modelHash) - local timeout = 0 - while not HasModelLoaded(modelHash) and timeout < 100 do - Wait(100) - timeout = timeout + 1 - Debug("Warte auf Modell: " .. tostring(timeout) .. "/100") - end - - if HasModelLoaded(modelHash) then - Debug("Modell geladen, erstelle Fahrzeug...") - - -- Anti-Duplication: Final check before spawning - if GetVehicleByPlate(data.plate) then - Debug("Anti-Dupe: Vehicle with plate " .. data.plate .. " appeared during model loading, aborting spawn") - SetModelAsNoLongerNeeded(modelHash) - return - end - - -- Verwende CREATE_AUTOMOBILE für bessere Persistenz - local vehicle - - if Citizen and Citizen.InvokeNative then - -- OneSync Methode - Debug("Verwende OneSync Methode") - vehicle = Citizen.InvokeNative(0xAF35D0D2583051B0, modelHash, data.coords.x, data.coords.y, data.coords.z, data.heading, true, true) - else - -- Fallback - Debug("Verwende Fallback Methode") - vehicle = CreateVehicle(modelHash, data.coords.x, data.coords.y, data.coords.z, data.heading, true, false) - end - - if DoesEntityExist(vehicle) then - -- Anti-Duplication: Mark as recently spawned - MarkVehicleAsSpawned(data.plate) - - -- Warte bis Fahrzeug vollständig geladen ist - Wait(500) - - -- Setze Kennzeichen - SetVehicleNumberPlateText(vehicle, data.plate) - - -- Setze Mods - if data.mods then - Debug("Setze Fahrzeugmods...") - SetVehicleMods(vehicle, data.mods) - end - - -- Setze Fuel - if GetResourceState(Config.FuelSystem) == 'started' then - exports[Config.FuelSystem]:SetFuel(vehicle, data.fuel or 100) - end - - -- Verhindere Despawn - PreventDespawn(vehicle) - - -- Füge zu getrackten Fahrzeugen hinzu - trackedVehicles[data.plate] = vehicle - lastKnownCoords[data.plate] = GetEntityCoords(vehicle) - - -- Registriere beim Server - TriggerServerEvent('antidespawn:server:registerVehicle', data.plate, modelHash, GetEntityCoords(vehicle), GetEntityHeading(vehicle), GetVehicleMods(vehicle)) - - Debug("Fahrzeug erfolgreich gespawnt: " .. data.plate) - else - Debug("Fehler beim Spawnen des Fahrzeugs: " .. data.plate) - end - - SetModelAsNoLongerNeeded(modelHash) - else - Debug("Modell konnte nicht geladen werden: " .. data.plate .. " (Hash: " .. tostring(modelHash) .. ")") - end - end) -end) - - --- Event für Garage Store (wird ausgelöst, wenn der Spieler ein Fahrzeug in die Garage stellen will) -RegisterNetEvent('jg-advancedgarages:client:store-vehicle', function(garageId, garageVehicleType) - local playerPed = PlayerPedId() - local vehicle = GetVehiclePedIsIn(playerPed, false) - - if vehicle ~= 0 then - local plate = QBCore.Functions.GetPlate(vehicle) - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping garage storage for vehicle with invalid plate") - return - end - - -- Markiere Fahrzeug als "wird in Garage gestellt" - garagePending[plate] = true - SetTimeout(10000, function() - if garagePending[plate] then - Debug("Garage storage timeout for vehicle: " .. plate) - garagePending[plate] = nil - end - end) - -- Entferne aus Tracking - if trackedVehicles[plate] then - Debug("Fahrzeug wird in Garage gestellt, entferne aus Tracking: " .. plate) - trackedVehicles[plate] = nil - lastKnownCoords[plate] = nil - TriggerServerEvent('antidespawn:server:removeVehicle', plate) - end - end -end) - --- jg-advanced-garage Events -RegisterNetEvent('jg-advancedgarages:client:vehicle-stored', function(data) - if data and data.plate then - -- Skip vehicles with empty or invalid plates - if not data.plate or data.plate == "" or string.len(data.plate) < 2 then - Debug("Skipping garage storage completion for vehicle with invalid plate") - return - end - - -- Markiere Fahrzeug als "in Garage" - garagePending[data.plate] = nil - - -- Entferne aus Tracking - if trackedVehicles[data.plate] then - trackedVehicles[data.plate] = nil - lastKnownCoords[data.plate] = nil - end - - -- Entferne aus Datenbank - TriggerServerEvent('antidespawn:server:removeVehicle', data.plate) - - Debug("Fahrzeug in Garage gespeichert, aus DB entfernt: " .. data.plate) - end -end) - -RegisterNetEvent('jg-advancedgarages:client:vehicle-spawned', function(data) - if data and data.plate then - -- Skip vehicles with empty or invalid plates - if not data.plate or data.plate == "" or string.len(data.plate) < 2 then - Debug("Skipping garage spawn completion for vehicle with invalid plate") - return - end - - -- Entferne Markierung "in Garage" - garagePending[data.plate] = nil - - Debug("Fahrzeug aus Garage gespawnt: " .. data.plate) - - -- Warte kurz bis das Fahrzeug vollständig gespawnt ist - Wait(1000) - - -- Finde das Fahrzeug - local vehicle = GetVehicleByPlate(data.plate) - if vehicle then - -- Füge zu getrackten Fahrzeugen hinzu - trackedVehicles[data.plate] = vehicle - lastKnownCoords[data.plate] = GetEntityCoords(vehicle) - - -- Verhindere Despawn - PreventDespawn(vehicle) - - Debug("Fahrzeug aus Garage zum Tracking hinzugefügt: " .. data.plate) - end - end -end) - --- Öffnen der Garage -RegisterNetEvent('jg-advancedgarages:client:open-garage', function(garageId, vehicleType, spawnCoords) - Debug("Garage geöffnet: " .. garageId) - - -- No need to mark vehicles as potentially being stored here - -- Let the actual store-vehicle event handle this -end) - --- Manuelle Lade-Funktion -RegisterCommand('loadvehicles', function() - Debug("Manuelles Laden der Fahrzeuge...") - TriggerServerEvent('antidespawn:server:loadVehicles') -end, false) - --- Debug Command -RegisterCommand('fixvehicle', function() - local playerPed = PlayerPedId() - local vehicle = GetVehiclePedIsIn(playerPed, false) - - if vehicle ~= 0 then - local plate = QBCore.Functions.GetPlate(vehicle) - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Cannot fix vehicle with invalid plate") - return - end - - -- Skip temporary vehicles - if IsTemporaryVehicle(vehicle) then - Debug("Cannot fix temporary vehicle") - return - end - - -- Skip likely job vehicles - if IsLikelyJobVehicle(vehicle) then - Debug("Cannot fix likely job vehicle") - return - end - - -- Anti-Duplication: Check if this plate already exists multiple times - if DoesVehicleExistInWorld(plate) then - Debug("Anti-Dupe: Detected duplicate vehicle with plate " .. plate .. ", not fixing") - return - end - - -- Prüfe ob Fahrzeug gerade in die Garage gestellt wird - if not garagePending[plate] then - -- Verify this vehicle exists in the database - CheckVehicleExists(plate, function(exists) - if not exists then - Debug("Vehicle not in database, cannot fix: " .. plate) - return - end - - PreventDespawn(vehicle) - trackedVehicles[plate] = vehicle - lastKnownCoords[plate] = GetEntityCoords(vehicle) - - -- Registriere Fahrzeug beim Server - local vehicleCoords = GetEntityCoords(vehicle) - local vehicleHeading = GetEntityHeading(vehicle) - local vehicleModel = GetEntityModel(vehicle) - local vehicleMods = GetVehicleMods(vehicle) - - TriggerServerEvent('antidespawn:server:registerVehicle', plate, vehicleModel, vehicleCoords, vehicleHeading, vehicleMods) - - Debug("Anti-Despawn für Fahrzeug aktiviert: " .. plate) - end) - else - Debug("Fahrzeug wird gerade in Garage gestellt, kann nicht fixiert werden: " .. plate) - end - else - Debug("Du musst in einem Fahrzeug sitzen!") - end -end, false) - --- Server-side event handler for ownership verification -RegisterNetEvent('antidespawn:client:vehicleOwnershipResult', function(result, plate) - -- This event is handled by the DoesPlayerOwnVehicle function -end) - --- Clean up resources when script stops -AddEventHandler('onResourceStop', function(resourceName) - if resourceName == GetCurrentResourceName() then - Debug("Resource stopping, clearing all data") - trackedVehicles = {} - lastKnownCoords = {} - garagePending = {} - spawnedVehicles = {} - vehicleOwnership = {} - vehicleExistsCallbacks = {} - end -end) - --- Debug command to check vehicle duplication -RegisterCommand('checkdupes', function() - local vehicles = GetGamePool('CVehicle') - local plates = {} - local dupes = {} - - for _, vehicle in pairs(vehicles) do - local plate = QBCore.Functions.GetPlate(vehicle) - if plate and plate ~= "" then - if plates[plate] then - if not dupes[plate] then - dupes[plate] = 2 - else - dupes[plate] = dupes[plate] + 1 - end - else - plates[plate] = true - end - end - end - - local dupeCount = 0 - for plate, count in pairs(dupes) do - Debug("Duplicate found: " .. plate .. " (Count: " .. count .. ")") - dupeCount = dupeCount + 1 - end - - if dupeCount == 0 then - Debug("No duplicate vehicles found") - else - Debug("Found " .. dupeCount .. " duplicate vehicles") - end -end, false) - --- Debug command to clear recently spawned vehicles list -RegisterCommand('clearspawned', function() - spawnedVehicles = {} - Debug("Cleared recently spawned vehicles list") -end, false) - --- Debug command to clear ownership cache -RegisterCommand('clearownership', function() - vehicleOwnership = {} - Debug("Cleared vehicle ownership cache") -end, false) - --- Debug command to list all tracked vehicles -RegisterCommand('listtracked', function() - local count = 0 - Debug("Currently tracked vehicles:") - for plate, _ in pairs(trackedVehicles) do - Debug("- " .. plate) - count = count + 1 - end - Debug("Total tracked vehicles: " .. count) -end, false) - --- Debug command to delete duplicate vehicles -RegisterCommand('deletedupes', function() - local vehicles = GetGamePool('CVehicle') - local plates = {} - local deleted = 0 - - for _, vehicle in pairs(vehicles) do - local plate = QBCore.Functions.GetPlate(vehicle) - if plate and plate ~= "" then - if plates[plate] then - DeleteEntity(vehicle) - deleted = deleted + 1 - Debug("Deleted duplicate vehicle with plate: " .. plate) - else - plates[plate] = true - end - end - end - - Debug("Deleted " .. deleted .. " duplicate vehicles") -end, false) diff --git a/resources/[carscripts]/nordi_antidespawn/config.lua b/resources/[carscripts]/nordi_antidespawn/config.lua deleted file mode 100644 index d6489d808..000000000 --- a/resources/[carscripts]/nordi_antidespawn/config.lua +++ /dev/null @@ -1,42 +0,0 @@ -Config = {} - --- Debug Modus -Config.Debug = true - --- Speicherintervall in Millisekunden (5000 = 5 Sekunden) -Config.SaveInterval = 10000 - --- Fuel System -Config.FuelSystem = "LegacyFuel" -- Anpassen an dein Fuel System - --- Fahrzeugklassen die gespeichert werden sollen -Config.AllowedVehicleClasses = { - 0, -- Compacts - 1, -- Sedans - 2, -- SUVs - 3, -- Coupes - 4, -- Muscle - 5, -- Sports Classics - 6, -- Sports - 7, -- Super - 8, -- Motorcycles - 9, -- Off-road - 10, -- Industrial - 11, -- Utility - 12, -- Vans - 13, -- Cycles - 14, -- Boats - 15, -- Helicopters - 16, -- Planes - 17, -- Service - 18, -- Emergency - 19, -- Military - 20, -- Commercial - 21, -- Trains - 22, -- Open Wheel -} - --- Blacklisted vehicle classes -Config.BlacklistedVehicleClasses = { - -- 21, -- Trains -} diff --git a/resources/[carscripts]/nordi_antidespawn/fxmanifest.lua b/resources/[carscripts]/nordi_antidespawn/fxmanifest.lua deleted file mode 100644 index 805320201..000000000 --- a/resources/[carscripts]/nordi_antidespawn/fxmanifest.lua +++ /dev/null @@ -1,25 +0,0 @@ -fx_version 'cerulean' -game 'gta5' - -author 'YourName' -description 'Vehicle Anti-Despawn System for QB-Core' -version '1.0.0' - -shared_scripts { - 'config.lua' -} - -server_scripts { - '@oxmysql/lib/MySQL.lua', - 'server/main.lua' -} - -client_scripts { - 'config.lua', - 'client/main.lua' -} - -dependencies { - 'qb-core', - 'jg-advancedgarages' -} diff --git a/resources/[carscripts]/nordi_antidespawn/server/main.lua b/resources/[carscripts]/nordi_antidespawn/server/main.lua deleted file mode 100644 index a6116781f..000000000 --- a/resources/[carscripts]/nordi_antidespawn/server/main.lua +++ /dev/null @@ -1,598 +0,0 @@ -local QBCore = exports['qb-core']:GetCoreObject() -local vehicles = {} -local activeSpawns = {} -- Track active spawn requests to prevent duplicates - --- Debug Funktion -local function Debug(msg) - if Config.Debug then - print("[AntiDespawn] " .. msg) - end -end - --- Erstelle Tabelle bei Serverstart -CreateThread(function() - -- Prüfe ob die Tabelle existiert - MySQL.query("SHOW TABLES LIKE 'vehicle_antidespawn'", {}, function(result) - if result and #result > 0 then - -- Tabelle existiert, prüfe ob das mods-Feld existiert - MySQL.query("SHOW COLUMNS FROM vehicle_antidespawn LIKE 'mods'", {}, function(columns) - if columns and #columns == 0 then - -- mods-Feld existiert nicht, füge es hinzu - Debug("Füge mods-Feld zur Tabelle hinzu...") - MySQL.query("ALTER TABLE vehicle_antidespawn ADD COLUMN mods LONGTEXT DEFAULT NULL", {}) - end - end) - - -- Prüfe ob das owner-Feld existiert - MySQL.query("SHOW COLUMNS FROM vehicle_antidespawn LIKE 'owner'", {}, function(columns) - if columns and #columns == 0 then - -- owner-Feld existiert nicht, füge es hinzu - Debug("Füge owner-Feld zur Tabelle hinzu...") - MySQL.query("ALTER TABLE vehicle_antidespawn ADD COLUMN owner VARCHAR(50) DEFAULT NULL", {}) - end - end) - else - -- Tabelle existiert nicht, erstelle sie - Debug("Erstelle Datenbank-Tabelle...") - MySQL.query([[ - CREATE TABLE IF NOT EXISTS `vehicle_antidespawn` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `plate` varchar(50) NOT NULL, - `model` varchar(50) NOT NULL, - `coords` longtext NOT NULL, - `heading` float NOT NULL, - `fuel` int(11) DEFAULT 100, - `mods` longtext DEFAULT NULL, - `owner` varchar(50) DEFAULT NULL, - `last_updated` timestamp DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `plate` (`plate`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; - ]]) - end - end) - - Debug("Datenbank initialisiert") - - -- Warte kurz, bis die Tabelle aktualisiert wurde - Wait(1000) - - -- Lade alle Fahrzeuge aus der Datenbank - MySQL.query("SELECT * FROM vehicle_antidespawn", {}, function(results) - if results and #results > 0 then - Debug("Lade " .. #results .. " Fahrzeuge aus der Datenbank") - - for _, vehicle in pairs(results) do - vehicles[vehicle.plate] = { - model = vehicle.model, - coords = json.decode(vehicle.coords), - heading = vehicle.heading, - fuel = vehicle.fuel, - mods = vehicle.mods and json.decode(vehicle.mods) or nil, - owner = vehicle.owner, - last_updated = vehicle.last_updated - } - end - end - end) -end) - --- Check if a vehicle exists in the database -RegisterNetEvent('antidespawn:server:checkVehicleExists', function(plate) - local src = source - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - TriggerClientEvent('antidespawn:client:vehicleExistsResult', src, false, plate) - return - end - - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - local exists = result and #result > 0 - TriggerClientEvent('antidespawn:client:vehicleExistsResult', src, exists, plate) - end) -end) - --- Check if a player owns a vehicle -RegisterNetEvent('antidespawn:server:checkVehicleOwnership', function(plate) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then - TriggerClientEvent('antidespawn:client:vehicleOwnershipResult', src, false, plate) - return - end - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - TriggerClientEvent('antidespawn:client:vehicleOwnershipResult', src, false, plate) - return - end - - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ? AND citizenid = ?', {plate, Player.PlayerData.citizenid}, function(result) - local isOwned = result and #result > 0 - TriggerClientEvent('antidespawn:client:vehicleOwnershipResult', src, isOwned, plate) - end) -end) - --- Register a vehicle (track all vehicles, regardless of ownership) -RegisterNetEvent('antidespawn:server:registerVehicle', function(plate, model, coords, heading, mods) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then return end - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping vehicle with invalid plate") - return - end - - -- Check if vehicle exists in player_vehicles (any player) - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - if not result or #result == 0 then - Debug("Vehicle not found in database: " .. plate) - return - end - - -- Check if vehicle is in garage - local inGarage = false - local ownerId = nil - - for _, veh in ipairs(result) do - if veh.state == 1 then - inGarage = true - end - ownerId = veh.citizenid -- Store the owner ID - end - - if inGarage then - Debug("Fahrzeug ist in der Garage, nicht registrieren: " .. plate) - - -- Remove from Anti-Despawn database if present - if vehicles[plate] then - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - Debug("Fahrzeug aus Anti-Despawn entfernt: " .. plate) - end - return - end - - -- Continue with registration as before - vehicles[plate] = { - model = model, - coords = coords, - heading = heading, - fuel = 100, - mods = mods, - owner = ownerId, - last_updated = os.time() - } - - MySQL.query("INSERT INTO vehicle_antidespawn (plate, model, coords, heading, fuel, mods, owner) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE coords = VALUES(coords), heading = VALUES(heading), mods = VALUES(mods), owner = VALUES(owner), last_updated = CURRENT_TIMESTAMP", { - plate, - tostring(model), - json.encode(coords), - heading, - 100, - json.encode(mods), - ownerId - }) - - Debug("Fahrzeug registriert: " .. plate .. " (Modell: " .. tostring(model) .. ", Besitzer: " .. tostring(ownerId) .. ")") - end) -end) - --- Update a vehicle -RegisterNetEvent('antidespawn:server:updateVehicle', function(plate, coords, heading, mods) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then return end - if not vehicles[plate] then return end - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping update for vehicle with invalid plate") - return - end - - -- Check if vehicle exists in player_vehicles (any player) - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - if not result or #result == 0 then - Debug("Vehicle not found in database: " .. plate) - -- Remove from tracking as it no longer exists in the database - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - return - end - - -- Check if vehicle is in garage - local inGarage = false - for _, veh in ipairs(result) do - if veh.state == 1 then - inGarage = true - break - end - end - - if inGarage then - Debug("Fahrzeug ist in der Garage, entferne aus Tracking: " .. plate) - vehicles[plate] = nil - - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", { - plate - }) - return - end - - vehicles[plate].coords = coords - vehicles[plate].heading = heading - vehicles[plate].mods = mods - vehicles[plate].last_updated = os.time() - - MySQL.query("UPDATE vehicle_antidespawn SET coords = ?, heading = ?, mods = ?, last_updated = CURRENT_TIMESTAMP WHERE plate = ?", { - json.encode(coords), - heading, - json.encode(mods), - plate - }) - - Debug("Fahrzeug aktualisiert: " .. plate) - end) -end) - --- Remove a vehicle -RegisterNetEvent('antidespawn:server:removeVehicle', function(plate) - local src = source - if not vehicles[plate] then return end - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping removal for vehicle with invalid plate") - return - end - - vehicles[plate] = nil - - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", { - plate - }) - - Debug("Fahrzeug entfernt: " .. plate) -end) - --- Respawn a vehicle (allow respawning any tracked vehicle) -RegisterNetEvent('antidespawn:server:respawnVehicle', function(plate) - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then return end - - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping respawn for vehicle with invalid plate") - return - end - - -- Anti-Duplication: Check if there's already an active spawn request for this plate - if activeSpawns[plate] then - Debug("Anti-Dupe: Already processing spawn request for: " .. plate) - return - end - - -- Mark as active spawn - activeSpawns[plate] = true - - -- Set a timeout to clear the active spawn status - SetTimeout(10000, function() - activeSpawns[plate] = nil - end) - - -- Check if vehicle exists in database (any player) - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - if not result or #result == 0 then - Debug("Vehicle not found in database: " .. plate) - activeSpawns[plate] = nil - return - end - - if not vehicles[plate] then - Debug("Fahrzeug nicht in Datenbank: " .. plate) - activeSpawns[plate] = nil - return - end - - -- Check if vehicle is in garage - local inGarage = false - for _, veh in ipairs(result) do - if veh.state == 1 then - inGarage = true - break - end - end - - if inGarage then - Debug("Fahrzeug ist in der Garage, nicht respawnen: " .. plate) - - -- Remove from Anti-Despawn database - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - activeSpawns[plate] = nil - return - end - - -- Send spawn event back to client - TriggerClientEvent('antidespawn:client:spawnVehicle', src, { - plate = plate, - model = vehicles[plate].model, - coords = vehicles[plate].coords, - heading = vehicles[plate].heading, - fuel = vehicles[plate].fuel, - mods = vehicles[plate].mods - }) - - Debug("Fahrzeug Respawn angefordert: " .. plate .. " (Besitzer: " .. tostring(vehicles[plate].owner) .. ")") - end) -end) - --- Load vehicles for a player (load all vehicles in range, not just owned ones) -RegisterNetEvent('antidespawn:server:loadVehicles', function() - local src = source - local Player = QBCore.Functions.GetPlayer(src) - - if not Player then - Debug("Spieler nicht gefunden") - return - end - - Debug("Lade Fahrzeuge für Spieler: " .. Player.PlayerData.citizenid) - - local playerCoords = GetEntityCoords(GetPlayerPed(src)) - local loadedCount = 0 - local vehiclesToLoad = {} - - -- Load all vehicles in the database, not just owned ones - for plate, vehicle in pairs(vehicles) do - -- Skip vehicles with empty or invalid plates - if not plate or plate == "" or string.len(plate) < 2 then - Debug("Skipping load for vehicle with invalid plate") - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - goto continue - end - - -- Check if vehicle is in garage by querying the database - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - if not result or #result == 0 then - Debug("Fahrzeug existiert nicht in player_vehicles: " .. plate) - -- Entferne aus Anti-Despawn Datenbank - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - return - end - - -- Check if vehicle is in garage - local inGarage = false - for _, veh in ipairs(result) do - if veh.state == 1 then - inGarage = true - break - end - end - - if inGarage then - Debug("Fahrzeug ist in der Garage, nicht laden: " .. plate) - -- Entferne aus Anti-Despawn Datenbank - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - return - end - - -- Lade nur Fahrzeuge in der Nähe des Spielers - local distance = #(playerCoords - vector3(vehicle.coords.x, vehicle.coords.y, vehicle.coords.z)) - - if distance < 100.0 then - -- Stelle sicher, dass das Modell als Zahl gespeichert ist - local model = vehicle.model - if type(model) == "string" then - model = tonumber(model) or model - end - - table.insert(vehiclesToLoad, { - plate = plate, - model = model, - coords = vehicle.coords, - heading = vehicle.heading, - fuel = vehicle.fuel, - mods = vehicle.mods - }) - - loadedCount = loadedCount + 1 - end - end) - ::continue:: - end - - -- Warte kurz und lade dann die Fahrzeuge - SetTimeout(3000, function() - for _, vehicleData in ipairs(vehiclesToLoad) do - -- Anti-Duplication: Check if there's already an active spawn request for this plate - if not activeSpawns[vehicleData.plate] then - activeSpawns[vehicleData.plate] = true - - -- Set a timeout to clear the active spawn status - SetTimeout(10000, function() - activeSpawns[vehicleData.plate] = nil - end) - - TriggerClientEvent('antidespawn:client:spawnVehicle', src, vehicleData) - Debug("Fahrzeug für Spieler geladen: " .. vehicleData.plate) - else - Debug("Anti-Dupe: Already processing spawn request for: " .. vehicleData.plate) - end - end - - Debug("Fahrzeugladung abgeschlossen. " .. loadedCount .. " Fahrzeuge geladen.") - end) -end) - - --- Cleanup alte Einträge (älter als 24 Stunden) -CreateThread(function() - while true do - Wait(3600000) -- 1 Stunde - MySQL.query("DELETE FROM vehicle_antidespawn WHERE last_updated < DATE_SUB(NOW(), INTERVAL 24 HOUR)") - Debug("Alte Fahrzeugeinträge bereinigt") - end -end) - --- Registriere jg-advancedgarages Events -RegisterNetEvent('jg-advancedgarages:server:vehicle-stored', function(data) - if data and data.plate then - Debug("Fahrzeug in Garage gespeichert: " .. data.plate) - - -- Entferne aus Anti-Despawn Datenbank - if vehicles[data.plate] then - vehicles[data.plate] = nil - - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", { - data.plate - }) - - Debug("Fahrzeug aus Anti-Despawn entfernt: " .. data.plate) - end - end -end) - -RegisterNetEvent('jg-advancedgarages:server:vehicle-spawned', function(data) - if data and data.plate then - Debug("Fahrzeug aus Garage gespawnt: " .. data.plate) - - -- Entferne aus Anti-Despawn Datenbank, da es jetzt von der Garage verwaltet wird - if vehicles[data.plate] then - vehicles[data.plate] = nil - - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", { - data.plate - }) - - Debug("Fahrzeug aus Anti-Despawn entfernt: " .. data.plate) - end - end -end) - --- Befehl zum Anzeigen aller gespeicherten Fahrzeuge -RegisterCommand('listvehicles', function(source, args, rawCommand) - if source == 0 then -- Nur über Konsole ausführbar - Debug("Gespeicherte Fahrzeuge:") - local count = 0 - for plate, vehicle in pairs(vehicles) do - Debug(plate .. " - Modell: " .. tostring(vehicle.model) .. " - Position: " .. - tostring(vehicle.coords.x) .. ", " .. tostring(vehicle.coords.y) .. ", " .. tostring(vehicle.coords.z) .. - " - Besitzer: " .. tostring(vehicle.owner)) - count = count + 1 - end - Debug("Insgesamt " .. count .. " Fahrzeuge gespeichert.") - end -end, true) - --- Befehl zum Prüfen des Garage-Status eines Fahrzeugs -RegisterCommand('checkgarage', function(source, args, rawCommand) - if source == 0 and args[1] then -- Nur über Konsole ausführbar - local plate = args[1] - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - if result and #result > 0 then - for _, veh in ipairs(result) do - Debug("Fahrzeug " .. plate .. " - State: " .. veh.state .. " - Owner: " .. veh.citizenid) - end - else - Debug("Fahrzeug " .. plate .. " nicht in player_vehicles gefunden.") - end - end) - end -end, true) - --- Befehl zum manuellen Entfernen eines Fahrzeugs -RegisterCommand('removevehicle', function(source, args, rawCommand) - if source == 0 and args[1] then -- Nur über Konsole ausführbar - local plate = args[1] - if vehicles[plate] then - vehicles[plate] = nil - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - Debug("Fahrzeug " .. plate .. " aus Anti-Despawn entfernt.") - else - Debug("Fahrzeug " .. plate .. " nicht in Anti-Despawn gefunden.") - end - end -end, true) - --- Befehl zum Bereinigen der Datenbank -RegisterCommand('clearvehicles', function(source, args, rawCommand) - if source == 0 then -- Nur über Konsole ausführbar - local count = 0 - - for plate, vehicle in pairs(vehicles) do - local model = vehicle.model - - -- Prüfe ob das Modell gültig ist - if type(model) == "string" and not tonumber(model) then - -- Ungültiges Modell, entferne aus Datenbank - MySQL.query("DELETE FROM vehicle_antidespawn WHERE plate = ?", {plate}) - vehicles[plate] = nil - count = count + 1 - Debug("Ungültiges Modell entfernt: " .. plate .. " (Modell: " .. tostring(model) .. ")") - end - end - - Debug("Bereinigung abgeschlossen. " .. count .. " Fahrzeuge entfernt.") - end -end, true) - --- Befehl zum Leeren der Datenbank -RegisterCommand('clearalldespawn', function(source, args, rawCommand) - if source == 0 then -- Nur über Konsole ausführbar - MySQL.query("DELETE FROM vehicle_antidespawn", {}) - vehicles = {} - Debug("Alle Fahrzeuge aus der Datenbank entfernt.") - end -end, true) - --- Debug command to check active spawns -RegisterCommand('activespawns', function(source, args, rawCommand) - if source == 0 then -- Nur über Konsole ausführbar - local count = 0 - for plate, _ in pairs(activeSpawns) do - Debug("Active spawn: " .. plate) - count = count + 1 - end - Debug("Total active spawns: " .. count) - end -end, true) - --- Check if a vehicle exists in the database -RegisterNetEvent('antidespawn:server:checkVehicleExists', function(plate, callback) - local src = source - - MySQL.query('SELECT * FROM player_vehicles WHERE plate = ?', {plate}, function(result) - local exists = result and #result > 0 - TriggerClientEvent('antidespawn:client:vehicleExistsResult', src, exists, plate) - end) -end) - --- Client callback for vehicle existence check -RegisterNetEvent('antidespawn:client:vehicleExistsResult', function(exists, plate) - -- This event will be handled by the callback system -end) - - - - --- Clean up when resource stops -AddEventHandler('onResourceStop', function(resourceName) - if resourceName == GetCurrentResourceName() then - Debug("Resource stopping, clearing all data") - vehicles = {} - activeSpawns = {} - end -end) diff --git a/resources/[qb]/qb-core/fxmanifest.lua b/resources/[qb]/qb-core/fxmanifest.lua index 924a561f4..ed87548c1 100644 --- a/resources/[qb]/qb-core/fxmanifest.lua +++ b/resources/[qb]/qb-core/fxmanifest.lua @@ -17,6 +17,7 @@ shared_scripts { 'shared/gangs.lua', 'shared/weapons.lua', 'shared/locations.lua' + } client_scripts { @@ -48,3 +49,5 @@ files { } dependency 'oxmysql' + +shared_script "@AdvancedParking/fixDeleteVehicle.lua"