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 phone_locale = SkyPhoneLocales.Resolve(Config.Bridge.Locale) local garage_locale = phone_locale.Nui.Apps.garage 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 type(data) ~= "table" then cb({ success = false, error = "invalid_request" }) return end 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)