Files
sky_phone-sky-systems/sky_phone/source/client/garage.lua
T
2026-08-08 18:53:38 +02:00

521 lines
17 KiB
Lua

local current_valet = nil
local vehicle_mods = {
modSpoilers = 0,
modFrontBumper = 1,
modRearBumper = 2,
modSideSkirt = 3,
modExhaust = 4,
modFrame = 5,
modGrille = 6,
modHood = 7,
modFender = 8,
modRightFender = 9,
modRoof = 10,
modEngine = 11,
modBrakes = 12,
modTransmission = 13,
modHorns = 14,
modSuspension = 15,
modArmor = 16,
modFrontWheels = 23,
modBackWheels = 24,
modPlateHolder = 25,
modVanityPlate = 26,
modTrimA = 27,
modOrnaments = 28,
modDashboard = 29,
modDial = 30,
modDoorSpeaker = 31,
modSeats = 32,
modSteeringWheel = 33,
modShifterLeavers = 34,
modAPlate = 35,
modSpeakers = 36,
modTrunk = 37,
modHydrolic = 38,
modEngineBlock = 39,
modAirFilter = 40,
modStruts = 41,
modArchCover = 42,
modAerials = 43,
modTrimB = 44,
modTank = 45,
modWindows = 46,
modLivery = 48,
}
local function garage_locale()
local locale = Locales[Config.Bridge.Locale] or Locales["en"]
return locale.Nui.Apps.garage
end
local function normalized_plate(value)
return tostring(value or ""):match("^%s*(.-)%s*$")
end
local function request_model(model)
RequestModel(model)
local timeout = GetGameTimer() + 10000
while not HasModelLoaded(model) and GetGameTimer() < timeout do
Wait(50)
end
return HasModelLoaded(model)
end
local function set_vehicle_color(vehicle, primary, secondary)
if type(primary) == "table" then
SetVehicleCustomPrimaryColour(
vehicle,
tonumber(primary[1]) or 0,
tonumber(primary[2]) or 0,
tonumber(primary[3]) or 0
)
end
if type(secondary) == "table" then
SetVehicleCustomSecondaryColour(
vehicle,
tonumber(secondary[1]) or 0,
tonumber(secondary[2]) or 0,
tonumber(secondary[3]) or 0
)
end
if type(primary) == "number" or type(secondary) == "number" then
local current_primary, current_secondary = GetVehicleColours(vehicle)
SetVehicleColours(
vehicle,
tonumber(primary) or current_primary,
tonumber(secondary) or current_secondary
)
end
end
local function apply_vehicle_properties(vehicle, properties, fallback)
properties = type(properties) == "table" and properties or {}
SetVehicleModKit(vehicle, 0)
set_vehicle_color(vehicle, properties.color1, properties.color2)
if properties.pearlescentColor or properties.wheelColor then
SetVehicleExtraColours(
vehicle,
tonumber(properties.pearlescentColor) or 0,
tonumber(properties.wheelColor) or 0
)
end
if properties.wheels then
SetVehicleWheelType(vehicle, tonumber(properties.wheels) or 0)
end
if properties.windowTint then
SetVehicleWindowTint(vehicle, tonumber(properties.windowTint) or 0)
end
if properties.xenonColor then
SetVehicleXenonLightsColor(vehicle, tonumber(properties.xenonColor) or -1)
end
for property, mod_type in pairs(vehicle_mods) do
local value = tonumber(properties[property])
if value then
SetVehicleMod(vehicle, mod_type, value, false)
end
end
ToggleVehicleMod(vehicle, 18, properties.modTurbo == true)
ToggleVehicleMod(vehicle, 20, properties.modSmokeEnabled == true)
ToggleVehicleMod(vehicle, 22, properties.modXenon == true)
if type(properties.neonEnabled) == "table" then
for index = 0, 3 do
SetVehicleNeonLightEnabled(vehicle, index, properties.neonEnabled[index + 1] == true)
end
end
if type(properties.neonColor) == "table" then
SetVehicleNeonLightsColour(
vehicle,
tonumber(properties.neonColor[1]) or 0,
tonumber(properties.neonColor[2]) or 0,
tonumber(properties.neonColor[3]) or 0
)
end
if type(properties.tyreSmokeColor) == "table" then
SetVehicleTyreSmokeColor(
vehicle,
tonumber(properties.tyreSmokeColor[1]) or 0,
tonumber(properties.tyreSmokeColor[2]) or 0,
tonumber(properties.tyreSmokeColor[3]) or 0
)
end
if type(properties.extras) == "table" then
for extra_id, enabled in pairs(properties.extras) do
local numeric_id = tonumber(extra_id)
if numeric_id and DoesExtraExist(vehicle, numeric_id) then
SetVehicleExtra(vehicle, numeric_id, not enabled)
end
end
end
local fuel = tonumber(properties.fuelLevel or properties.fuel or fallback.fuel)
local engine = tonumber(properties.engineHealth or properties.engine or fallback.engine and fallback.engine * 10)
local body = tonumber(properties.bodyHealth or properties.body or fallback.body and fallback.body * 10)
if fuel then
SetVehicleFuelLevel(vehicle, math.max(0.0, math.min(100.0, fuel)))
end
if engine then
SetVehicleEngineHealth(vehicle, math.max(-4000.0, math.min(1000.0, engine)))
end
if body then
SetVehicleBodyHealth(vehicle, math.max(0.0, math.min(1000.0, body)))
end
if properties.dirtLevel then
SetVehicleDirtLevel(vehicle, math.max(0.0, math.min(15.0, tonumber(properties.dirtLevel) or 0.0)))
end
SetVehicleNumberPlateText(vehicle, fallback.plate)
end
local function existing_vehicle(plate)
local normalized = string.upper(normalized_plate(plate))
for _, vehicle in ipairs(GetGamePool("CVehicle")) do
if string.upper(normalized_plate(GetVehicleNumberPlateText(vehicle))) == normalized then
return vehicle
end
end
return nil
end
local function public_valet_state()
if not current_valet then
return nil
end
return {
canCancel = current_valet.can_cancel,
cost = current_valet.cost,
distance = current_valet.distance,
etaSeconds = current_valet.eta_seconds,
orderId = current_valet.order_id,
plate = current_valet.plate,
status = current_valet.status,
vehicleName = current_valet.vehicle_name,
}
end
local function send_valet_state()
SendNUIMessage({
type = "garage:valet-status",
data = public_valet_state(),
})
end
local function remove_valet_blip()
if current_valet and current_valet.blip and DoesBlipExist(current_valet.blip) then
RemoveBlip(current_valet.blip)
current_valet.blip = nil
end
end
local function delete_valet_entities()
if not current_valet then
return
end
remove_valet_blip()
if current_valet.driver and DoesEntityExist(current_valet.driver) then
DeleteEntity(current_valet.driver)
end
if current_valet.vehicle and DoesEntityExist(current_valet.vehicle) then
DeleteEntity(current_valet.vehicle)
end
end
local function fail_valet(error_code, server_cancel)
if not current_valet then
return
end
local order_id = current_valet.order_id
current_valet.cancelled = true
delete_valet_entities()
if server_cancel then
Bridge.Callbacks.Trigger("sky_phone:garage:valet-cancel", { orderId = order_id })
end
current_valet.status = "failed"
current_valet.can_cancel = false
current_valet.error = error_code
send_valet_state()
local locale = garage_locale()
Bridge.Framework.Notify(locale.name, locale.errors[error_code] or locale.errors.default, "error", 5000)
SetTimeout(8000, function()
if current_valet and current_valet.order_id == order_id then
current_valet = nil
send_valet_state()
end
end)
end
local function update_drive_target(driver, vehicle)
local coords = GetEntityCoords(PlayerPedId())
TaskVehicleDriveToCoordLongrange(
driver,
vehicle,
coords.x,
coords.y,
coords.z,
Config.Garage.Valet.DriveSpeed,
Config.Garage.Valet.DrivingStyle,
Config.Garage.Valet.ArrivalDistance
)
end
local function run_valet_delivery(order)
local model = tonumber(order.vehicle.model)
if not model and type(order.vehicle.model) == "string" then
model = joaat(order.vehicle.model)
end
local driver_model = joaat(order.driverModel)
if not model or not IsModelInCdimage(model) or not IsModelAVehicle(model) then
fail_valet("invalid_vehicle_model", true)
return
end
if existing_vehicle(order.vehicle.plate) then
fail_valet("vehicle_already_out", true)
return
end
current_valet.status = "preparing"
send_valet_state()
if not request_model(model) or not request_model(driver_model) then
SetModelAsNoLongerNeeded(model)
SetModelAsNoLongerNeeded(driver_model)
fail_valet("valet_spawn_failed", true)
return
end
local player_ped = PlayerPedId()
local player_coords = GetEntityCoords(player_ped)
local target = GetOffsetFromEntityInWorldCoords(player_ped, 0.0, -Config.Garage.Valet.SpawnDistance, 0.0)
local found, spawn_coords, spawn_heading = GetClosestVehicleNodeWithHeading(
target.x,
target.y,
target.z,
1,
3.0,
0
)
if not found then
SetModelAsNoLongerNeeded(model)
SetModelAsNoLongerNeeded(driver_model)
fail_valet("valet_no_road", true)
return
end
local vehicle = CreateVehicle(
model,
spawn_coords.x,
spawn_coords.y,
spawn_coords.z,
spawn_heading,
true,
true
)
if vehicle == 0 or not DoesEntityExist(vehicle) then
SetModelAsNoLongerNeeded(model)
SetModelAsNoLongerNeeded(driver_model)
fail_valet("valet_spawn_failed", true)
return
end
current_valet.vehicle = vehicle
SetEntityAsMissionEntity(vehicle, true, true)
SetVehicleOnGroundProperly(vehicle)
apply_vehicle_properties(vehicle, order.vehicle.properties, order.vehicle)
SetVehicleHasBeenOwnedByPlayer(vehicle, true)
SetVehicleDoorsLocked(vehicle, 2)
SetVehicleEngineOn(vehicle, true, true, false)
local network_id = NetworkGetNetworkIdFromEntity(vehicle)
SetNetworkIdCanMigrate(network_id, true)
local driver = CreatePedInsideVehicle(vehicle, 26, driver_model, -1, true, true)
if driver == 0 or not DoesEntityExist(driver) then
SetModelAsNoLongerNeeded(model)
SetModelAsNoLongerNeeded(driver_model)
fail_valet("valet_spawn_failed", true)
return
end
current_valet.driver = driver
SetEntityAsMissionEntity(driver, true, true)
SetBlockingOfNonTemporaryEvents(driver, true)
SetPedKeepTask(driver, true)
SetDriverAbility(driver, 1.0)
SetDriverAggressiveness(driver, 0.0)
SetModelAsNoLongerNeeded(model)
SetModelAsNoLongerNeeded(driver_model)
current_valet.blip = AddBlipForEntity(vehicle)
SetBlipSprite(current_valet.blip, 225)
SetBlipColour(current_valet.blip, 3)
SetBlipRoute(current_valet.blip, true)
current_valet.status = "en_route"
current_valet.can_cancel = true
update_drive_target(driver, vehicle)
send_valet_state()
local timeout = GetGameTimer() + (Config.Garage.Valet.TimeoutSeconds * 1000)
local next_retask = GetGameTimer() + 8000
while current_valet and not current_valet.cancelled and GetGameTimer() < timeout do
if not DoesEntityExist(vehicle)
or not DoesEntityExist(driver)
or IsEntityDead(driver)
or not IsVehicleDriveable(vehicle, false)
then
fail_valet("valet_interrupted", true)
return
end
player_coords = GetEntityCoords(PlayerPedId())
local vehicle_coords = GetEntityCoords(vehicle)
local distance = #(vehicle_coords - player_coords)
current_valet.distance = math.floor(distance + 0.5)
current_valet.eta_seconds = math.max(1, math.ceil(distance / math.max(1.0, Config.Garage.Valet.DriveSpeed)))
if distance <= Config.Garage.Valet.ArrivalDistance then
break
end
if GetGameTimer() >= next_retask then
update_drive_target(driver, vehicle)
next_retask = GetGameTimer() + 8000
end
send_valet_state()
Wait(750)
end
if not current_valet or current_valet.cancelled then
return
end
if GetGameTimer() >= timeout then
fail_valet("valet_timeout", true)
return
end
current_valet.status = "arriving"
current_valet.can_cancel = false
current_valet.distance = 0
current_valet.eta_seconds = 0
send_valet_state()
TaskVehicleTempAction(driver, vehicle, 27, 1800)
Wait(1800)
SetVehicleEngineOn(vehicle, false, true, true)
SetVehicleDoorsLocked(vehicle, 1)
TaskLeaveVehicle(driver, vehicle, 0)
local leave_timeout = GetGameTimer() + 5000
while IsPedInVehicle(driver, vehicle, false) and GetGameTimer() < leave_timeout do
Wait(100)
end
local driver_target = GetOffsetFromEntityInWorldCoords(vehicle, 4.0, 8.0, 0.0)
TaskGoStraightToCoord(
driver,
driver_target.x,
driver_target.y,
driver_target.z,
1.0,
10000,
GetEntityHeading(driver),
0.5
)
SetPedKeepTask(driver, true)
SetEntityAsNoLongerNeeded(driver)
current_valet.driver = nil
local completion = Bridge.Callbacks.Trigger(
"sky_phone:garage:valet-complete",
{
orderId = current_valet.order_id,
networkId = NetworkGetNetworkIdFromEntity(vehicle),
}
)
if not completion or not completion.success then
fail_valet("valet_completion_failed", false)
return
end
remove_valet_blip()
SetEntityAsNoLongerNeeded(vehicle)
current_valet.vehicle = nil
current_valet.status = "delivered"
current_valet.can_cancel = false
send_valet_state()
local locale = garage_locale()
Bridge.Framework.Notify(locale.name, locale.valetDelivered, "success", 5000)
local completed_order_id = current_valet.order_id
SetTimeout(12000, function()
if current_valet and current_valet.order_id == completed_order_id then
current_valet = nil
send_valet_state()
end
end)
end
RegisterNUICallback("garage:valet-request", function(data, cb)
if current_valet then
cb({ success = false, error = "valet_active" })
return
end
local result = Bridge.Callbacks.Trigger("sky_phone:garage:valet-request", data)
if not result or not result.success or type(result.data) ~= "table" then
cb(result or { success = false, error = "request_failed" })
return
end
local vehicle = result.data.vehicle
local model_hash = tonumber(vehicle.model)
if not model_hash and type(vehicle.model) == "string" then
model_hash = joaat(vehicle.model)
end
local display_name = model_hash and GetDisplayNameFromVehicleModel(model_hash) or nil
local label = display_name and GetLabelText(display_name) or nil
current_valet = {
can_cancel = false,
cancelled = false,
cost = result.data.cost,
distance = nil,
eta_seconds = nil,
order_id = result.data.orderId,
plate = vehicle.plate,
status = "ordering",
vehicle_name = label and label ~= "NULL" and label ~= "CARNOTFOUND"
and label
or tostring(vehicle.model),
}
cb({ success = true, data = public_valet_state() })
send_valet_state()
CreateThread(function()
run_valet_delivery(result.data)
end)
end)
RegisterNUICallback("garage:valet-cancel", function(_, cb)
if not current_valet or not current_valet.can_cancel then
cb({ success = false, error = "valet_not_cancellable" })
return
end
local order_id = current_valet.order_id
local result = Bridge.Callbacks.Trigger("sky_phone:garage:valet-cancel", { orderId = order_id })
if not result or not result.success then
cb(result or { success = false, error = "request_failed" })
return
end
current_valet.cancelled = true
delete_valet_entities()
current_valet.status = "cancelled"
current_valet.can_cancel = false
send_valet_state()
cb({ success = true, data = public_valet_state() })
SetTimeout(5000, function()
if current_valet and current_valet.order_id == order_id then
current_valet = nil
send_valet_state()
end
end)
end)
RegisterNUICallback("garage:valet-state", function(_, cb)
cb({ success = true, data = public_valet_state() })
end)
RegisterNetEvent("sky_phone:garage:valet-aborted", function(error_code)
if current_valet then
fail_valet(error_code or "valet_timeout", false)
end
end)
AddEventHandler("onResourceStop", function(resource_name)
if resource_name ~= GetCurrentResourceName() or not current_valet then
return
end
delete_valet_entities()
end)