mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 00:01:29 +00:00
Merge branch 'dev' into feature/funk
This commit is contained in:
@@ -140,6 +140,37 @@ Config.Banking = {
|
||||
HistoryLimit = 50,
|
||||
}
|
||||
|
||||
Config.Garage = {
|
||||
System = "auto", -- auto, custom, esx, qb, qbox, ak47, bp, cd, codem, ds-servercreator, hex, jg, my, okok, op, quasar, rx, vms, ws, zyke_garages
|
||||
MaximumVehicles = 250,
|
||||
RequestsPerMinute = 30,
|
||||
Custom = {
|
||||
Table = "",
|
||||
OwnerColumn = "",
|
||||
},
|
||||
Valet = {
|
||||
Enabled = true,
|
||||
Price = 750,
|
||||
Account = "bank", -- bank or cash
|
||||
RequestsPerMinute = 3,
|
||||
CooldownSeconds = 60,
|
||||
TimeoutSeconds = 180,
|
||||
SpawnDistance = 110.0,
|
||||
ArrivalDistance = 14.0,
|
||||
DriveSpeed = 20.0,
|
||||
DrivingStyle = 786603,
|
||||
DriverModel = "s_m_m_autoshop_01",
|
||||
-- The current valet driver route supports road vehicles. Keep non-road types disabled.
|
||||
VehicleTypes = {
|
||||
car = true,
|
||||
bike = true,
|
||||
boat = false,
|
||||
plane = false,
|
||||
helicopter = false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Config.Marketplace = {
|
||||
PageSize = 20,
|
||||
MessagePageSize = 50,
|
||||
|
||||
@@ -160,7 +160,8 @@ Locales["en"] = {
|
||||
chartDaySummary = "{day}: {incoming} incoming, {outgoing} outgoing",
|
||||
navigation = "Banking navigation", incoming = "Incoming", outgoing = "Outgoing",
|
||||
refresh = "Refresh banking data", unavailable = "Banking unavailable", tryAgain = "Try Again",
|
||||
playerId = "Player ID", playerIdPlaceholder = "Enter the recipient ID",
|
||||
recipientPhone = "Phone number", recipientPhonePlaceholder = "Enter the recipient's number",
|
||||
chooseContact = "Choose from contacts", noContacts = "No contacts saved yet.",
|
||||
amount = "Amount", amountPlaceholder = "Enter an amount",
|
||||
transactions = {
|
||||
deposit = "Cash deposit", withdrawal = "Cash withdrawal",
|
||||
@@ -170,13 +171,66 @@ Locales["en"] = {
|
||||
transfer = { title = "Send money", body = "Transfer money from your bank account to an online player.", submit = "Send transfer" },
|
||||
},
|
||||
errors = {
|
||||
invalid_request = "Enter a valid whole amount and player ID.", insufficient_funds = "There is not enough money in this account.",
|
||||
invalid_request = "Enter a valid phone number and whole amount.", insufficient_funds = "There is not enough money in this account.",
|
||||
target_not_found = "The recipient is not online.", self_transfer = "You cannot send money to yourself.",
|
||||
rate_limited = "Please wait before making another transaction.", transfer_failed = "The transfer could not be completed.",
|
||||
banking_unavailable = "Banking is currently unavailable.", request_failed = "The banking request failed.",
|
||||
default = "The banking request failed.",
|
||||
},
|
||||
},
|
||||
garage = {
|
||||
name = "Garage", subtitle = "Your vehicle collection", myVehicles = "My Vehicles",
|
||||
searchPlaceholder = "Search name, plate or garage",
|
||||
tryAgain = "Try Again", unavailable = "Garage unavailable",
|
||||
noVehicles = "No Vehicles", noVehiclesBody = "Vehicles owned by this character will appear here.",
|
||||
noResults = "No Results", noResultsBody = "Try another vehicle, plate or location.",
|
||||
unknownVehicle = "Unknown Vehicle", unknownLocation = "Location unavailable",
|
||||
location = "Current location", fuel = "Fuel", engine = "Engine", body = "Body",
|
||||
vin = "Vehicle Identification Number", notAvailable = "—",
|
||||
provider = "Vehicle data · {system}",
|
||||
valetDelivered = "Your vehicle has arrived.",
|
||||
valet = {
|
||||
title = "Sky Valet", eyebrow = "Premium delivery",
|
||||
body = "A professional driver brings this vehicle directly to your current location.",
|
||||
deliver = "Deliver Vehicle", liveEyebrow = "Live delivery", tracked = "Live tracked",
|
||||
onDemand = "On demand", cancel = "Cancel", confirmTitle = "Request Valet?",
|
||||
confirmBody = "{vehicle} will be delivered to you for {price}.", confirm = "Request Valet",
|
||||
ordering = "Requesting…", account = "The fee is charged to your {account} account.",
|
||||
connecting = "Locating driver", arriving = "Arriving now", eta = "About {seconds}s away",
|
||||
distance = "{distance} m away", unavailableStatus = "Only parked vehicles can be delivered.",
|
||||
unsupported = "Valet delivery is disabled for this vehicle type.",
|
||||
activeOrder = "A valet driver is already delivering another vehicle.",
|
||||
status = { ordering = "Confirming order", preparing = "Preparing vehicle", en_route = "Driver en route",
|
||||
arriving = "Arriving now", delivered = "Vehicle delivered", cancelled = "Delivery cancelled", failed = "Delivery failed" },
|
||||
},
|
||||
filters = { all = "All", garaged = "Parked", out = "Outside", impounded = "Impound" },
|
||||
status = { garaged = "Parked", out = "Outside", impounded = "Impounded" },
|
||||
kinds = { car = "Vehicle", bike = "Motorcycle", boat = "Boat", plane = "Aircraft", helicopter = "Helicopter" },
|
||||
errors = {
|
||||
garage_unavailable = "Vehicle data is unavailable for this character.",
|
||||
valet_disabled = "Valet service is currently unavailable.",
|
||||
valet_active = "A valet delivery is already active.",
|
||||
valet_cooldown = "Please wait before requesting another vehicle.",
|
||||
invalid_vehicle = "Select a valid vehicle.", vehicle_not_owned = "This vehicle does not belong to this character.",
|
||||
vehicle_not_garaged = "This vehicle is not parked in a garage.",
|
||||
valet_vehicle_type = "Valet supports cars and motorcycles only.",
|
||||
insufficient_funds = "You do not have enough money for this delivery.",
|
||||
valet_status_unsupported = "This garage system cannot release the vehicle.",
|
||||
valet_not_found = "The valet order is no longer active.",
|
||||
valet_not_cancellable = "The delivery can no longer be cancelled.",
|
||||
invalid_vehicle_model = "The vehicle model could not be loaded.",
|
||||
vehicle_already_out = "This vehicle is already nearby.",
|
||||
valet_spawn_failed = "The driver could not prepare your vehicle.",
|
||||
valet_no_road = "No suitable road was found near you.",
|
||||
valet_interrupted = "The valet delivery was interrupted.",
|
||||
valet_timeout = "The driver could not reach you in time. Your fee was refunded.",
|
||||
valet_completion_failed = "The handover could not be confirmed.",
|
||||
valet_vehicle_unverified = "The delivered vehicle could not be verified.",
|
||||
rate_limited = "Please wait before refreshing again.",
|
||||
device_not_open = "Open the phone again to view your vehicles.",
|
||||
request_failed = "The garage request failed.", default = "The garage request failed.",
|
||||
},
|
||||
},
|
||||
calculator = { name = "Calculator" },
|
||||
snake = {
|
||||
name = "Snake", backToMenu = "Back to game menu", board = "Snake game board",
|
||||
@@ -449,7 +503,7 @@ Locales["en"] = {
|
||||
weather = {
|
||||
name = "Weather", now = "Now", today = "Today", refresh = "Refresh weather", details = "Weather details",
|
||||
feelsLike = "Feels Like", wind = "Wind", humidity = "Humidity", rain = "Precipitation",
|
||||
hourly = "24-Hour Forecast", daily = "7-Day Forecast", unavailable = "Weather is unavailable",
|
||||
hourly = "Next Hours", unavailable = "Weather is unavailable",
|
||||
stale = "Showing the last available weather update.", tryAgain = "Try Again",
|
||||
regions = { los_santos = "Los Santos", blaine_county = "Blaine County", cayo_perico = "Cayo Perico" },
|
||||
conditions = {
|
||||
|
||||
@@ -25,6 +25,7 @@ client_scripts {
|
||||
'source/bridge/client/framework.lua',
|
||||
'source/bridge/client/callbacks.lua',
|
||||
'source/client/camera.lua',
|
||||
'source/client/garage.lua',
|
||||
'source/bridge/client/radio.lua',
|
||||
'source/client/main.lua',
|
||||
'source/client/radio.lua',
|
||||
@@ -51,6 +52,7 @@ server_scripts {
|
||||
'source/server/notes.lua',
|
||||
'source/server/mail.lua',
|
||||
'source/server/banking.lua',
|
||||
'source/server/garage.lua',
|
||||
'source/server/marketplace.lua',
|
||||
'source/server/pages.lua',
|
||||
'source/server/calendar.lua',
|
||||
|
||||
@@ -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)
|
||||
@@ -258,10 +258,12 @@ end
|
||||
RegisterNUICallback("weather:get", function(_, cb)
|
||||
local coords = GetEntityCoords(PlayerPedId())
|
||||
local weather_hash = GetPrevWeatherTypeHashName()
|
||||
local next_weather_hash = GetNextWeatherTypeHashName()
|
||||
cb({
|
||||
success = true,
|
||||
data = {
|
||||
condition = weather_types[weather_hash] or "clear",
|
||||
nextCondition = weather_types[next_weather_hash] or weather_types[weather_hash] or "clear",
|
||||
region = weather_region(coords),
|
||||
clock = {
|
||||
year = GetClockYear(),
|
||||
@@ -276,6 +278,47 @@ RegisterNUICallback("weather:get", function(_, cb)
|
||||
})
|
||||
end)
|
||||
|
||||
local function garage_vehicle_kind(model_hash, fallback)
|
||||
if IsThisModelABoat(model_hash) then
|
||||
return "boat"
|
||||
end
|
||||
if IsThisModelAPlane(model_hash) then
|
||||
return "plane"
|
||||
end
|
||||
if IsThisModelAHeli(model_hash) then
|
||||
return "helicopter"
|
||||
end
|
||||
if IsThisModelABike(model_hash) or IsThisModelABicycle(model_hash) then
|
||||
return "bike"
|
||||
end
|
||||
return fallback
|
||||
end
|
||||
|
||||
RegisterNUICallback("garage:vehicles", function(data, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:garage:vehicles", data)
|
||||
if not result or not result.success or type(result.data) ~= "table" then
|
||||
cb(result or { success = false, error = "request_failed" })
|
||||
return
|
||||
end
|
||||
for _, vehicle in ipairs(result.data.vehicles or {}) do
|
||||
local model_hash = tonumber(vehicle.model)
|
||||
if not model_hash and type(vehicle.model) == "string" and vehicle.model ~= "" then
|
||||
model_hash = joaat(vehicle.model)
|
||||
end
|
||||
if model_hash then
|
||||
local display_name = GetDisplayNameFromVehicleModel(model_hash)
|
||||
local label = display_name and GetLabelText(display_name) or nil
|
||||
if label and label ~= "NULL" and label ~= "CARNOTFOUND" then
|
||||
vehicle.name = label
|
||||
elseif type(vehicle.model) == "string" and vehicle.model ~= "" then
|
||||
vehicle.name = vehicle.model
|
||||
end
|
||||
vehicle.kind = garage_vehicle_kind(model_hash, vehicle.kind)
|
||||
end
|
||||
end
|
||||
cb(result)
|
||||
end)
|
||||
|
||||
for _, callback_name in ipairs(server_callbacks) do
|
||||
RegisterNUICallback(callback_name, function(data, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:" .. callback_name, data)
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.0 KiB |
@@ -1,14 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sky Phone</title>
|
||||
<script type="module" crossorigin src="./assets/sky-index-qSN2Wmy5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/sky-index-DnOTtSzB.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -83,6 +83,27 @@ local function notify_changed(source)
|
||||
TriggerClientEvent("sky_phone:banking:changed", source)
|
||||
end
|
||||
|
||||
local function online_source_for_phone(number)
|
||||
local rows = Bridge.Database.Query([[
|
||||
SELECT d.`imei`
|
||||
FROM `sky_phone_sims` s
|
||||
INNER JOIN `sky_phone_devices` d ON d.`sim_id` = s.`id`
|
||||
WHERE s.`phone_number` = ?
|
||||
LIMIT 1
|
||||
]], { number })
|
||||
local device = rows[1]
|
||||
if not device or type(device.imei) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
|
||||
local target = tonumber(player_source) or player_source
|
||||
if SkyPhone.FindDeviceSlots(target, device.imei)[1] then
|
||||
return target
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:banking:overview", function(source)
|
||||
local identifier, error_response = require_banking_session(source)
|
||||
if not identifier then
|
||||
@@ -100,16 +121,18 @@ Bridge.Callbacks.Register("sky_phone:banking:transfer", function(source, data)
|
||||
return error_response
|
||||
end
|
||||
local amount = valid_amount(data and data.amount)
|
||||
local target_value = tonumber(data and data.target)
|
||||
if not amount
|
||||
or not target_value
|
||||
or target_value ~= math.floor(target_value)
|
||||
or target_value <= 0
|
||||
or target_value > 65535
|
||||
then
|
||||
local number = SkyPhoneSimNumber.Normalize(
|
||||
data and data.phoneNumber,
|
||||
Config.Sim.NumberLength,
|
||||
Config.Sim.NumberPrefix
|
||||
)
|
||||
if not amount or not number then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
local target = math.floor(target_value)
|
||||
local target = online_source_for_phone(number)
|
||||
if not target then
|
||||
return { success = false, error = "target_not_found" }
|
||||
end
|
||||
if target == source then
|
||||
return { success = false, error = "self_transfer" }
|
||||
end
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
Bridge.Database.AfterMigration("sky_phone", function()
|
||||
|
||||
local supported_systems = {
|
||||
auto = true,
|
||||
custom = true,
|
||||
esx = true,
|
||||
qb = true,
|
||||
qbox = true,
|
||||
ak47 = true,
|
||||
bp = true,
|
||||
cd = true,
|
||||
codem = true,
|
||||
["ds-servercreator"] = true,
|
||||
hex = true,
|
||||
jg = true,
|
||||
my = true,
|
||||
okok = true,
|
||||
op = true,
|
||||
quasar = true,
|
||||
rx = true,
|
||||
vms = true,
|
||||
ws = true,
|
||||
zyke_garages = true,
|
||||
}
|
||||
|
||||
local function decode_object(value)
|
||||
if type(value) == "table" then
|
||||
return value
|
||||
end
|
||||
if type(value) ~= "string" or value == "" then
|
||||
return {}
|
||||
end
|
||||
local success, decoded = pcall(json.decode, value)
|
||||
if not success or type(decoded) ~= "table" then
|
||||
return {}
|
||||
end
|
||||
return decoded
|
||||
end
|
||||
|
||||
local function first_value(...)
|
||||
local values = { ... }
|
||||
for index = 1, #values do
|
||||
local value = values[index]
|
||||
if value ~= nil and value ~= "" then
|
||||
return value
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function truthy_database_value(value)
|
||||
return value == true or value == 1 or value == "1" or value == "true"
|
||||
end
|
||||
|
||||
local function normalized_health(value, maximum)
|
||||
local number = tonumber(value)
|
||||
if not number then
|
||||
return nil
|
||||
end
|
||||
if maximum == 1000 then
|
||||
number = number / 10
|
||||
end
|
||||
return math.max(0, math.min(100, math.floor(number + 0.5)))
|
||||
end
|
||||
|
||||
local function vehicle_status(row, location, garage_system)
|
||||
local state = tonumber(row.state)
|
||||
local location_key = string.lower(tostring(location or ""))
|
||||
if truthy_database_value(row.impound)
|
||||
or truthy_database_value(row.impounded)
|
||||
or truthy_database_value(row.pound)
|
||||
or location_key:find("impound", 1, true)
|
||||
or location_key:find("pound", 1, true)
|
||||
then
|
||||
return "impounded"
|
||||
end
|
||||
if garage_system == "jg" then
|
||||
return truthy_database_value(row.in_garage) and "garaged" or "out"
|
||||
end
|
||||
if state == 2 then
|
||||
return "impounded"
|
||||
end
|
||||
local stored = first_value(row.stored, row.in_garage, row.parked)
|
||||
if truthy_database_value(stored) or state == 1 then
|
||||
return "garaged"
|
||||
end
|
||||
return "out"
|
||||
end
|
||||
|
||||
local function vehicle_kind(value)
|
||||
local kind = string.lower(tostring(value or ""))
|
||||
if kind == "boat" then
|
||||
return "boat"
|
||||
end
|
||||
if kind == "plane" or kind == "air" or kind == "airplane" then
|
||||
return "plane"
|
||||
end
|
||||
if kind == "heli" or kind == "helicopter" then
|
||||
return "helicopter"
|
||||
end
|
||||
if kind == "bike" or kind == "bicycle" or kind == "motorcycle" then
|
||||
return "bike"
|
||||
end
|
||||
return "car"
|
||||
end
|
||||
|
||||
local function vehicle_properties(row)
|
||||
local properties = decode_object(first_value(row.mods, row.vehicle_data, row.properties))
|
||||
local vehicle_object = decode_object(row.vehicle)
|
||||
if next(vehicle_object) ~= nil then
|
||||
for key, value in pairs(vehicle_object) do
|
||||
if properties[key] == nil then
|
||||
properties[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
return properties
|
||||
end
|
||||
|
||||
local function vehicle_dto(row, garage_system)
|
||||
local mods = vehicle_properties(row)
|
||||
local vehicle_value = row.vehicle
|
||||
|
||||
local model = first_value(row.model, row.hash, mods.model, mods.hash)
|
||||
if type(vehicle_value) == "string" and vehicle_value:sub(1, 1) ~= "{" then
|
||||
model = first_value(model, vehicle_value)
|
||||
elseif type(vehicle_value) == "number" then
|
||||
model = first_value(model, vehicle_value)
|
||||
end
|
||||
local numeric_model = tonumber(model)
|
||||
if numeric_model then
|
||||
model = numeric_model
|
||||
end
|
||||
|
||||
local location = first_value(row.garage_id, row.parking, row.garage, row.parked_at)
|
||||
local plate = tostring(first_value(row.plate, mods.plate) or ""):match("^%s*(.-)%s*$")
|
||||
return {
|
||||
id = tostring(first_value(row.id, row.vin, plate)),
|
||||
plate = plate,
|
||||
vin = tostring(row.vin or ""),
|
||||
nickname = tostring(row.nickname or ""),
|
||||
model = model,
|
||||
kind = vehicle_kind(first_value(row.garage_type, row.type, mods.type)),
|
||||
status = vehicle_status(row, location, garage_system),
|
||||
location = tostring(location or ""),
|
||||
fuel = normalized_health(first_value(row.fuel, mods.fuelLevel, mods.fuel), 100),
|
||||
engine = normalized_health(first_value(row.engine, mods.engineHealth, mods.engine), 1000),
|
||||
body = normalized_health(first_value(row.body, mods.bodyHealth, mods.body), 1000),
|
||||
}
|
||||
end
|
||||
|
||||
local function storage_config()
|
||||
local system = tostring(Config.Garage.System or "auto")
|
||||
if not supported_systems[system] then
|
||||
error(("[sky_phone] Unsupported garage system '%s'."):format(system))
|
||||
end
|
||||
if system == "custom" then
|
||||
local table_name = tostring(Config.Garage.Custom.Table or "")
|
||||
local owner_column = tostring(Config.Garage.Custom.OwnerColumn or "")
|
||||
if not table_name:match("^[%w_]+$") or not owner_column:match("^[%w_]+$") then
|
||||
error("[sky_phone] Custom garage table and owner column must be configured with SQL identifiers.")
|
||||
end
|
||||
return table_name, owner_column, system
|
||||
end
|
||||
if system == "esx" then
|
||||
return "owned_vehicles", "owner", system
|
||||
end
|
||||
if system == "qb" or system == "qbox" then
|
||||
return "player_vehicles", "citizenid", system
|
||||
end
|
||||
if system == "auto" and GetResourceState("jg-advancedgarages") == "started" then
|
||||
system = "jg"
|
||||
end
|
||||
if Bridge.Framework.GetName() == "esx" then
|
||||
return "owned_vehicles", "owner", system == "auto" and "esx" or system
|
||||
end
|
||||
return "player_vehicles", "citizenid", system == "auto" and Bridge.Framework.GetName() or system
|
||||
end
|
||||
|
||||
local active_valets = {}
|
||||
local valet_cooldowns = {}
|
||||
|
||||
local function normalized_plate(value)
|
||||
if type(value) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
local plate = value:match("^%s*(.-)%s*$")
|
||||
if plate == "" or #plate > 16 or plate:find("[%c]") then
|
||||
return nil
|
||||
end
|
||||
return plate
|
||||
end
|
||||
|
||||
local function owned_vehicle_row(identifier, plate)
|
||||
local table_name, owner_column, garage_system = storage_config()
|
||||
local rows = Bridge.Database.Query(
|
||||
("SELECT * FROM %s WHERE %s = ? AND TRIM(plate) = ? LIMIT 1"):format(table_name, owner_column),
|
||||
{ identifier, plate }
|
||||
)
|
||||
return rows[1], table_name, owner_column, garage_system
|
||||
end
|
||||
|
||||
local function status_snapshot(row)
|
||||
local snapshot = {}
|
||||
for _, column in ipairs({ "stored", "state", "in_garage", "parked" }) do
|
||||
if row[column] ~= nil then
|
||||
snapshot[column] = row[column]
|
||||
end
|
||||
end
|
||||
return snapshot
|
||||
end
|
||||
|
||||
local function write_vehicle_status(order, restore)
|
||||
local assignments = {}
|
||||
local parameters = {}
|
||||
for column, original_value in pairs(order.status) do
|
||||
assignments[#assignments + 1] = ("%s = ?"):format(column)
|
||||
parameters[#parameters + 1] = restore and original_value or 0
|
||||
end
|
||||
if #assignments == 0 then
|
||||
return false
|
||||
end
|
||||
parameters[#parameters + 1] = order.identifier
|
||||
parameters[#parameters + 1] = order.plate
|
||||
Bridge.Database.Query(
|
||||
("UPDATE %s SET %s WHERE %s = ? AND TRIM(plate) = ?")
|
||||
:format(order.table_name, table.concat(assignments, ", "), order.owner_column),
|
||||
parameters
|
||||
)
|
||||
return true
|
||||
end
|
||||
|
||||
local function refund_valet(source, order)
|
||||
if order.refunded then
|
||||
return
|
||||
end
|
||||
order.refunded = true
|
||||
if not Bridge.Framework.AddMoney(source, Config.Garage.Valet.Account, order.cost) then
|
||||
Bridge.Debug("error", "[sky_phone] Failed to refund valet order '%s' for player %s.", tostring(order.id), tostring(source))
|
||||
return
|
||||
end
|
||||
TriggerClientEvent("sky_phone:banking:changed", source)
|
||||
end
|
||||
|
||||
local function cancel_valet(source, order)
|
||||
write_vehicle_status(order, true)
|
||||
refund_valet(source, order)
|
||||
active_valets[source] = nil
|
||||
end
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:garage:valet-request", function(source, data)
|
||||
local valet = Config.Garage.Valet
|
||||
if not valet.Enabled then
|
||||
return { success = false, error = "valet_disabled" }
|
||||
end
|
||||
if not SkyPhone.AllowOperation(source, "garage_valet", valet.RequestsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
if active_valets[source] then
|
||||
return { success = false, error = "valet_active" }
|
||||
end
|
||||
local now = os.time()
|
||||
if (valet_cooldowns[source] or 0) > now then
|
||||
return {
|
||||
success = false,
|
||||
error = "valet_cooldown",
|
||||
data = { retryAfter = valet_cooldowns[source] - now },
|
||||
}
|
||||
end
|
||||
local plate = normalized_plate(data and data.plate)
|
||||
if not plate then
|
||||
return { success = false, error = "invalid_vehicle" }
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if type(identifier) ~= "string" or identifier == "" then
|
||||
return { success = false, error = "garage_unavailable" }
|
||||
end
|
||||
local row, table_name, owner_column, garage_system = owned_vehicle_row(identifier, plate)
|
||||
if not row then
|
||||
return { success = false, error = "vehicle_not_owned" }
|
||||
end
|
||||
local vehicle = vehicle_dto(row, garage_system)
|
||||
if vehicle.status ~= "garaged" then
|
||||
return { success = false, error = "vehicle_not_garaged" }
|
||||
end
|
||||
if not valet.VehicleTypes[vehicle.kind] then
|
||||
return { success = false, error = "valet_vehicle_type" }
|
||||
end
|
||||
local price = math.max(0, math.floor(tonumber(valet.Price) or 0))
|
||||
local balance = tonumber(Bridge.Framework.GetMoney(source, valet.Account)) or 0
|
||||
if balance < price then
|
||||
return { success = false, error = "insufficient_funds" }
|
||||
end
|
||||
local order = {
|
||||
cost = price,
|
||||
expires_at = now + valet.TimeoutSeconds,
|
||||
identifier = identifier,
|
||||
id = ("%s:%s:%s"):format(source, now, math.random(100000, 999999)),
|
||||
owner_column = owner_column,
|
||||
plate = plate,
|
||||
status = status_snapshot(row),
|
||||
table_name = table_name,
|
||||
}
|
||||
if not write_vehicle_status(order, false) then
|
||||
return { success = false, error = "valet_status_unsupported" }
|
||||
end
|
||||
if price > 0 and not Bridge.Framework.RemoveMoney(source, valet.Account, price) then
|
||||
write_vehicle_status(order, true)
|
||||
return { success = false, error = "insufficient_funds" }
|
||||
end
|
||||
active_valets[source] = order
|
||||
TriggerClientEvent("sky_phone:banking:changed", source)
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
cost = price,
|
||||
driverModel = valet.DriverModel,
|
||||
orderId = order.id,
|
||||
vehicle = {
|
||||
body = vehicle.body,
|
||||
engine = vehicle.engine,
|
||||
fuel = vehicle.fuel,
|
||||
kind = vehicle.kind,
|
||||
model = vehicle.model,
|
||||
plate = vehicle.plate,
|
||||
properties = vehicle_properties(row),
|
||||
},
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:garage:valet-cancel", function(source, data)
|
||||
local order = active_valets[source]
|
||||
if not order or type(data) ~= "table" or data.orderId ~= order.id then
|
||||
return { success = false, error = "valet_not_found" }
|
||||
end
|
||||
cancel_valet(source, order)
|
||||
return { success = true, data = { refunded = order.cost } }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:garage:valet-complete", function(source, data)
|
||||
local order = active_valets[source]
|
||||
if not order or type(data) ~= "table" or data.orderId ~= order.id then
|
||||
return { success = false, error = "valet_not_found" }
|
||||
end
|
||||
|
||||
local network_id = tonumber(data.networkId)
|
||||
if not network_id or network_id <= 0 or network_id ~= math.floor(network_id) then
|
||||
return { success = false, error = "valet_vehicle_unverified" }
|
||||
end
|
||||
local entity = NetworkGetEntityFromNetworkId(network_id)
|
||||
if entity == 0 or not DoesEntityExist(entity) or tonumber(NetworkGetEntityOwner(entity)) ~= source then
|
||||
return { success = false, error = "valet_vehicle_unverified" }
|
||||
end
|
||||
|
||||
active_valets[source] = nil
|
||||
valet_cooldowns[source] = os.time() + Config.Garage.Valet.CooldownSeconds
|
||||
return { success = true }
|
||||
end)
|
||||
|
||||
AddEventHandler("playerDropped", function()
|
||||
local source = source
|
||||
local order = active_valets[source]
|
||||
if order then
|
||||
cancel_valet(source, order)
|
||||
end
|
||||
valet_cooldowns[source] = nil
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
if resource_name ~= GetCurrentResourceName() then
|
||||
return
|
||||
end
|
||||
for source, order in pairs(active_valets) do
|
||||
cancel_valet(source, order)
|
||||
end
|
||||
end)
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
Wait(5000)
|
||||
local now = os.time()
|
||||
for source, order in pairs(active_valets) do
|
||||
if order.expires_at <= now then
|
||||
cancel_valet(source, order)
|
||||
TriggerClientEvent("sky_phone:garage:valet-aborted", source, "valet_timeout")
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:garage:vehicles", function(source)
|
||||
if not SkyPhone.AllowOperation(source, "garage_vehicles", Config.Garage.RequestsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if type(identifier) ~= "string" or identifier == "" then
|
||||
return { success = false, error = "garage_unavailable" }
|
||||
end
|
||||
local table_name, owner_column, garage_system = storage_config()
|
||||
local rows = Bridge.Database.Query(
|
||||
("SELECT * FROM `%s` WHERE `%s` = ? LIMIT ?"):format(table_name, owner_column),
|
||||
{ identifier, Config.Garage.MaximumVehicles }
|
||||
)
|
||||
local vehicles = {}
|
||||
for _, row in ipairs(rows) do
|
||||
local vehicle = vehicle_dto(row, garage_system)
|
||||
if vehicle.plate ~= "" then
|
||||
vehicles[#vehicles + 1] = vehicle
|
||||
end
|
||||
end
|
||||
table.sort(vehicles, function(left, right)
|
||||
return left.plate < right.plate
|
||||
end)
|
||||
return {
|
||||
success = true,
|
||||
data = {
|
||||
system = garage_system,
|
||||
valet = {
|
||||
account = Config.Garage.Valet.Account,
|
||||
enabled = Config.Garage.Valet.Enabled,
|
||||
price = math.max(0, math.floor(tonumber(Config.Garage.Valet.Price) or 0)),
|
||||
vehicleTypes = Config.Garage.Valet.VehicleTypes,
|
||||
},
|
||||
vehicles = vehicles,
|
||||
},
|
||||
}
|
||||
end)
|
||||
|
||||
end)
|
||||
Reference in New Issue
Block a user