1
0
Fork 0
forked from Simnation/Main
Main/resources/[carscripts]/AdvancedParking/server/server.lua
2025-08-07 14:38:30 +02:00

1179 lines
35 KiB
Lua

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)