mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 03:01:42 +00:00
ADD - implement valet delivery client
This commit is contained in:
@@ -0,0 +1,520 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user