From 0f1f6e82ce7892f1527485a4aa16625db9f168bf Mon Sep 17 00:00:00 2001 From: Solareon <769465+solareon@users.noreply.github.com> Date: Sun, 29 Sep 2024 19:42:40 +0200 Subject: [PATCH] refactor(server): split util and finance to separate files --- fxmanifest.lua | 3 +- server/finance.lua | 355 ++++++++++++++++++++++++++++++++ server/main.lua | 501 +++------------------------------------------ server/utils.lua | 113 ++++++++++ 4 files changed, 493 insertions(+), 479 deletions(-) create mode 100644 server/finance.lua create mode 100644 server/utils.lua diff --git a/fxmanifest.lua b/fxmanifest.lua index 05ad0f6..dbeb6b6 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -21,6 +21,8 @@ client_scripts { server_scripts { '@oxmysql/lib/MySQL.lua', 'server/main.lua', + 'server/utils.lua', + 'server/finance.lua' } files { @@ -30,6 +32,5 @@ files { 'locales/*.json' } -provide 'qb-vehicleshop' lua54 'yes' use_experimental_fxv2_oal 'yes' \ No newline at end of file diff --git a/server/finance.lua b/server/finance.lua new file mode 100644 index 0000000..5fb4ae5 --- /dev/null +++ b/server/finance.lua @@ -0,0 +1,355 @@ +local config = require 'config.server' +local sharedConfig = require 'config.shared' + +if not sharedConfig.enableFinance then return end + +local financeStorage = require 'server.storage' +local financeTimer = {} + +function SetHasFinanced(src, bool) + financeTimer[src].hasFinanced = bool +end + +---@param src number +---@param citizenid string +local function addPlayerToFinanceTimer(src, citizenid) + citizenid = citizenid or exports.qbx_core:GetPlayer(src).PlayerData.citizenid + + local hasFinanced = financeStorage.hasFinancedVehicles(citizenid) + if hasFinanced then + exports.qbx_core:Notify(src, locale('general.paymentduein', config.finance.paymentWarning)) + end + + financeTimer[src] = { + citizenid = citizenid, + time = os.time(), + hasFinanced = hasFinanced + } +end + +CreateThread(function() + local players = exports.qbx_core:GetPlayersData() + if players then + for i = 1, #players do + local player = players[i] + addPlayerToFinanceTimer(player.source, player.citizenid) + end + end +end) + +---@param src number +local function updatePlayerFinanceTime(src) + local playerData = financeTimer[src] + if not playerData then return end + + local vehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(playerData.citizenid) + + local playTime = math.floor((os.time() - playerData.time) / 60) + for i = 1, #vehicles do + local v = vehicles[i] + local newTime = lib.math.clamp(v.financetime - playTime, 0, math.maxinteger) + + if v.balance >= 1 then + financeStorage.updateVehicleEntityFinanceTime(newTime, v.vehicleId) + end + end + + financeTimer[src] = nil +end + +---@param src number +local function checkFinancedVehicles(src) + local financeData = financeTimer[src] + if not financeData.hasFinanced then return end + + local citizenid = financeData.citizenid + local time = math.floor((os.time() - financeData.time) / 60) + local vehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(citizenid) + + if not vehicles then + financeTimer[src].hasFinanced = false + return + end + local paymentReminder = false + for i = 1, #vehicles do + local v = vehicles[i] + local timeLeft = v.financetime - time + if timeLeft <= 0 then + if config.deleteUnpaidFinancedVehicle then + exports.qbx_vehicles:DeletePlayerVehicles('vehicleId', v.id) + else + exports.qbx_vehicles:SetPlayerVehicleOwner(v.id, nil) + end + exports.qbx_core:Notify(src, locale('error.repossessed', v.plate), 'error') + elseif timeLeft <= config.finance.paymentWarning then + paymentReminder = true + end + end + + if paymentReminder then + exports.qbx_core:Notify(src, locale('general.paymentduein', config.finance.paymentWarning)) + end +end + +AddEventHandler('playerDropped', function() + local src = source + updatePlayerFinanceTime(src) +end) + +AddEventHandler('QBCore:Server:OnPlayerUnload', function(src) + updatePlayerFinanceTime(src) +end) + +RegisterNetEvent('QBCore:Server:OnPlayerLoaded', function() + local src = source + local playerData = exports.qbx_core:GetPlayer(src).PlayerData + addPlayerToFinanceTimer(src, playerData.citizenid) +end) + +lib.cron.new(config.finance.cronSchedule, function() + for src in pairs(financeTimer) do + checkFinancedVehicles(src) + end +end) + +---@param vehiclePrice number +---@param downPayment number +---@param paymentamount number +---@return integer balance owed on the vehicle +---@return integer numPayments to pay off the balance +local function calculateFinance(vehiclePrice, downPayment, paymentamount) + local balance = vehiclePrice - downPayment + local vehPaymentAmount = balance / paymentamount + + return lib.math.round(balance), lib.math.round(vehPaymentAmount) +end + +---@param paymentAmount number paid +---@param vehData VehicleFinancingEntity +---@return integer newBalance +---@return integer newPayment +---@return integer numPaymentsLeft +local function calculateNewFinance(paymentAmount, vehData) + local newBalance = tonumber(vehData.balance - paymentAmount) --[[@as number]] + local minusPayment = vehData.paymentsleft - 1 + local newPaymentsLeft = newBalance / minusPayment + local newPayment = newBalance / newPaymentsLeft + + return lib.math.round(newBalance), lib.math.round(newPayment), newPaymentsLeft +end + + +---@param paymentAmount number +---@param vehId number +RegisterNetEvent('qbx_vehicleshop:server:financePayment', function(paymentAmount, vehId) + local src = source + local vehData = financeStorage.fetchFinancedVehicleEntityById(vehId) + + paymentAmount = tonumber(paymentAmount) --[[@as number]] + + local minPayment = tonumber(vehData.paymentamount) --[[@as number]] + local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) + local newBalance, newPaymentsLeft, newPayment = calculateNewFinance(paymentAmount, vehData) + + if newBalance <= 0 then + exports.qbx_core:Notify(src, locale('error.overpaid'), 'error') + return + end + + if paymentAmount < minPayment then + exports.qbx_core:Notify(src, locale('error.minimumallowed')..lib.math.groupdigits(minPayment), 'error') + return + end + + if not RemoveMoney(src, paymentAmount, 'vehicle-finance-payment') then return end + + financeStorage.updateVehicleFinance({ + balance = newBalance, + payment = newPayment, + paymentsLeft = newPaymentsLeft, + timer = timer + }, vehId) +end) + + +---@param vehId number +RegisterNetEvent('qbx_vehicleshop:server:financePaymentFull', function(vehId) + local src = source + local vehData = financeStorage.fetchFinancedVehicleEntityById(vehId) + + if not RemoveMoney(src, vehData.balance, 'vehicle-finance-payment-full') then return end + + financeStorage.updateVehicleFinance({ + balance = 0, + payment = 0, + paymentsLeft = 0, + timer = 0, + }, vehId) +end) + +---@param downPayment number +---@param paymentAmount number +---@param vehicle string +---@param playerId string|number +RegisterNetEvent('qbx_vehicleshop:server:sellfinanceVehicle', function(downPayment, paymentAmount, vehicle, playerId) + local src = source + local target = exports.qbx_core:GetPlayer(tonumber(playerId)) + + if not target then + return exports.qbx_core:Notify(src, locale('error.Invalid_ID'), 'error') + end + + if #(GetEntityCoords(GetPlayerPed(src)) - GetEntityCoords(GetPlayerPed(target.PlayerData.source))) >= 3 then + return exports.qbx_core:Notify(src, locale('error.playertoofar'), 'error') + end + + local shopId = GetShopZone(target.PlayerData.source) + local shop = sharedConfig.shops[shopId] + if not shop then return end + + if not CheckVehicleList(vehicle, shopId) then + return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') + end + + downPayment = tonumber(downPayment) --[[@as number]] + paymentAmount = tonumber(paymentAmount) --[[@as number]] + + local vehiclePrice = coreVehicles[vehicle].price + local minDown = tonumber(lib.math.round((sharedConfig.finance.minimumDown / 100) * vehiclePrice)) --[[@as number]] + + if downPayment > vehiclePrice then + return exports.qbx_core:Notify(src, locale('error.notworth'), 'error') + end + + if downPayment < minDown then + return exports.qbx_core:Notify(src, locale('error.downtoosmall'), 'error') + end + + if paymentAmount > sharedConfig.finance.maximumPayments then + return exports.qbx_core:Notify(src, locale('error.exceededmax'), 'error') + end + + local cid = target.PlayerData.citizenid + local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) + local balance, vehPaymentAmount = calculateFinance(vehiclePrice, downPayment, paymentAmount) + + if not SellShowroomVehicleTransact(src, target, vehiclePrice, downPayment) then return end + + local vehicleId = financeStorage.insertVehicleEntityWithFinance({ + insertVehicleEntityRequest = { + citizenId = cid, + model = vehicle, + }, + + vehicleFinance = { + balance = balance, + payment = vehPaymentAmount, + paymentsLeft = paymentAmount, + timer = timer, + } + }) + + SpawnVehicle(src, { + coords = shop.vehicleSpawn, + vehicleId = vehicleId + }) + financeTimer[target.PlayerData.source].hasFinanced = true +end) + +---@param downPayment number +---@param paymentAmount number +---@param vehicle string +RegisterNetEvent('qbx_vehicleshop:server:financeVehicle', function(downPayment, paymentAmount, vehicle) + local src = source + + local shopId = GetShopZone(src) + + local shop = sharedConfig.shops[shopId] + if not shop then return end + + if not CheckVehicleList(vehicle, shopId) then + return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') + end + + local player = exports.qbx_core:GetPlayer(src) + local vehiclePrice = coreVehicles[vehicle].price + local minDown = tonumber(lib.math.round((sharedConfig.finance.minimumDown / 100) * vehiclePrice)) --[[@as number]] + + downPayment = tonumber(downPayment) --[[@as number]] + paymentAmount = tonumber(paymentAmount) --[[@as number]] + + if downPayment > vehiclePrice then + return exports.qbx_core:Notify(src, locale('error.notworth'), 'error') + end + + if downPayment < minDown then + return exports.qbx_core:Notify(src, locale('error.downtoosmall'), 'error') + end + + if paymentAmount > sharedConfig.finance.maximumPayments then + return exports.qbx_core:Notify(src, locale('error.exceededmax'), 'error') + end + + if not RemoveMoney(src, downPayment, 'vehicle-financed-in-showroom') then + return exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') + end + + local balance, vehPaymentAmount = calculateFinance(vehiclePrice, downPayment, paymentAmount) + local cid = player.PlayerData.citizenid + local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) + + local vehicleId = financeStorage.insertVehicleEntityWithFinance({ + insertVehicleEntityRequest = { + citizenId = cid, + model = vehicle, + }, + + vehicleFinance = { + balance = balance, + payment = vehPaymentAmount, + paymentsLeft = paymentAmount, + timer = timer, + } + }) + + exports.qbx_core:Notify(src, locale('success.purchased'), 'success') + + SpawnVehicle(src, { + coords = shop.vehicleSpawn, + vehicleId = vehicleId + }) + + financeTimer[src].hasFinanced = true +end) + +---@param source number +lib.callback.register('qbx_vehicleshop:server:GetFinancedVehicles', function(source) + local src = source + local player = exports.qbx_core:GetPlayer(src) + if not player then return end + + local financeVehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(player.PlayerData.citizenid) + local vehicles = {} + + for i = 1, #financeVehicles do + local v = financeVehicles[i] + local vehicle = exports.qbx_vehicles:GetPlayerVehicle(v.vehicleId) + + if vehicle then + vehicle.balance = v.balance + vehicle.paymentamount = v.paymentamount + vehicle.paymentsleft = v.paymentsleft + vehicle.financetime = v.financetime + end + vehicles[#vehicles+1] = vehicle + end + + return vehicles[1] and vehicles +end) + +---@param vehicleId integer +---@return boolean +local function isFinanced(vehicleId) + return financeStorage.fetchIsFinanced(vehicleId) +end +exports('IsFinanced', isFinanced) \ No newline at end of file diff --git a/server/main.lua b/server/main.lua index 92f327a..bb38d6f 100644 --- a/server/main.lua +++ b/server/main.lua @@ -1,315 +1,20 @@ lib.versionCheck('Qbox-project/qbx_vehicleshop') +assert(lib.checkDependency('qbx_core', '1.17.2'), 'qbx_core v1.17.2 or higher is required') assert(lib.checkDependency('qbx_vehicles', '1.4.1'), 'qbx_vehicles v1.4.1 or higher is required') local config = require 'config.server' local sharedConfig = require 'config.shared' local financeStorage = require 'server.storage' -local allowedVehicles = require 'server.vehicles' -local financeTimer = {} local coreVehicles = exports.qbx_core:GetVehiclesByName() -local shopZones = {} local saleTimeout = {} local testDrives = {} -local qbx_vehicles = exports.qbx_vehicles - ----@param src number ----@param citizenid string -local function addPlayerToFinanceTimer(src, citizenid) - citizenid = citizenid or exports.qbx_core:GetPlayer(src).PlayerData.citizenid - - local hasFinanced = financeStorage.hasFinancedVehicles(citizenid) - if hasFinanced then - exports.qbx_core:Notify(src, locale('general.paymentduein', config.finance.paymentWarning)) - end - - financeTimer[src] = { - citizenid = citizenid, - time = os.time(), - hasFinanced = hasFinanced - } -end - -CreateThread(function() - local players = exports.qbx_core:GetPlayersData() - if players then - for i = 1, #players do - local player = players[i] - addPlayerToFinanceTimer(player.source, player.citizenid) - end - end -end) - ----@param src number -local function updatePlayerFinanceTime(src) - local playerData = financeTimer[src] - if not playerData then return end - - local vehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(playerData.citizenid) - - local playTime = math.floor((os.time() - playerData.time) / 60) - for i = 1, #vehicles do - local v = vehicles[i] - local newTime = lib.math.clamp(v.financetime - playTime, 0, math.maxinteger) - - if v.balance >= 1 then - financeStorage.updateVehicleEntityFinanceTime(newTime, v.vehicleId) - end - end - - financeTimer[src] = nil -end - ----@param src number -local function checkFinancedVehicles(src) - local financeData = financeTimer[src] - if not financeData.hasFinanced then return end - - local citizenid = financeData.citizenid - local time = math.floor((os.time() - financeData.time) / 60) - local vehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(citizenid) - - if not vehicles then - financeTimer[src].hasFinanced = false - return - end - local paymentReminder = false - for i = 1, #vehicles do - local v = vehicles[i] - local timeLeft = v.financetime - time - if timeLeft <= 0 then - if config.deleteUnpaidFinancedVehicle then - qbx_vehicles:DeletePlayerVehicles('vehicleId', v.id) - else - qbx_vehicles:SetPlayerVehicleOwner(v.id, nil) - end - exports.qbx_core:Notify(src, locale('error.repossessed', v.plate), 'error') - elseif timeLeft <= config.finance.paymentWarning then - paymentReminder = true - end - end - - if paymentReminder then - exports.qbx_core:Notify(src, locale('general.paymentduein', config.finance.paymentWarning)) - end -end - -AddEventHandler('playerDropped', function() - local src = source - updatePlayerFinanceTime(src) -end) - -AddEventHandler('QBCore:Server:OnPlayerUnload', function(src) - updatePlayerFinanceTime(src) -end) - -RegisterNetEvent('QBCore:Server:OnPlayerLoaded', function() - local src = source - local playerData = exports.qbx_core:GetPlayer(src).PlayerData - addPlayerToFinanceTimer(src, playerData.citizenid) -end) - -lib.cron.new(config.finance.cronSchedule, function() - for src in pairs(financeTimer) do - checkFinancedVehicles(src) - end -end) - ----@param vehicle string Vehicle model name to check if allowed for purchase/testdrive/etc. ----@param shop string? Shop name to check if vehicle is allowed in that shop ----@return boolean -local function checkVehicleList(vehicle, shop) - for i = 1, allowedVehicles.count do - local allowedVeh = allowedVehicles.vehicles[i] - if allowedVeh.model == vehicle then - if shop and allowedVeh.shopType == shop then - return true - elseif not shop then - return true - end - end - end - return false -end ---@param data {toVehicle: string} RegisterNetEvent('qbx_vehicleshop:server:swapVehicle', function(data) - if not checkVehicleList(data.toVehicle) then return end + if not CheckVehicleList(data.toVehicle) then return end TriggerClientEvent('qbx_vehicleshop:client:swapVehicle', -1, data) end) ----@param source number ----@return string? -local function getShopZone(source) - local coords = GetEntityCoords(GetPlayerPed(source)) - for i = 1, #shopZones do - local zone = shopZones[i] - if zone:contains(coords) then - return zone.name - end - end -end - ----@param vehiclePrice number ----@param downPayment number ----@param paymentamount number ----@return integer balance owed on the vehicle ----@return integer numPayments to pay off the balance -local function calculateFinance(vehiclePrice, downPayment, paymentamount) - local balance = vehiclePrice - downPayment - local vehPaymentAmount = balance / paymentamount - - return lib.math.round(balance), lib.math.round(vehPaymentAmount) -end - ----@param paymentAmount number paid ----@param vehData VehicleFinancingEntity ----@return integer newBalance ----@return integer newPayment ----@return integer numPaymentsLeft -local function calculateNewFinance(paymentAmount, vehData) - local newBalance = tonumber(vehData.balance - paymentAmount) --[[@as number]] - local minusPayment = vehData.paymentsleft - 1 - local newPaymentsLeft = newBalance / minusPayment - local newPayment = newBalance / newPaymentsLeft - - return lib.math.round(newBalance), lib.math.round(newPayment), newPaymentsLeft -end - ----@param source number -lib.callback.register('qbx_vehicleshop:server:GetFinancedVehicles', function(source) - local src = source - local player = exports.qbx_core:GetPlayer(src) - if not player then return end - - local financeVehicles = financeStorage.fetchFinancedVehicleEntitiesByCitizenId(player.PlayerData.citizenid) - local vehicles = {} - - for i = 1, #financeVehicles do - local v = financeVehicles[i] - local vehicle = qbx_vehicles:GetPlayerVehicle(v.vehicleId) - - if vehicle then - vehicle.balance = v.balance - vehicle.paymentamount = v.paymentamount - vehicle.paymentsleft = v.paymentsleft - vehicle.financetime = v.financetime - end - vehicles[#vehicles+1] = vehicle - end - - return vehicles[1] and vehicles -end) - ----@param price number ----@param cash number ----@param bank number ----@return 'cash'|'bank'|nil -local function findChargeableCurrencyType(price, cash, bank) - if cash >= price then - return 'cash' - elseif bank >= price then - return 'bank' - else - return - end -end - ----@param src number ----@param amount number ----@param reason string? ----@return boolean success if money was removed -local function removeMoney(src, amount, reason) - local player = exports.qbx_core:GetPlayer(src) - local cash = player.PlayerData.money.cash - local bank = player.PlayerData.money.bank - local currencyType = findChargeableCurrencyType(amount, cash, bank) - - if not currencyType then - exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') - return false - end - - return config.removePlayerFunds(player, currencyType, amount, reason) -end - ----@param paymentAmount number ----@param vehId number -RegisterNetEvent('qbx_vehicleshop:server:financePayment', function(paymentAmount, vehId) - local src = source - local vehData = financeStorage.fetchFinancedVehicleEntityById(vehId) - - paymentAmount = tonumber(paymentAmount) --[[@as number]] - - local minPayment = tonumber(vehData.paymentamount) --[[@as number]] - local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) - local newBalance, newPaymentsLeft, newPayment = calculateNewFinance(paymentAmount, vehData) - - if newBalance <= 0 then - exports.qbx_core:Notify(src, locale('error.overpaid'), 'error') - return - end - - if paymentAmount < minPayment then - exports.qbx_core:Notify(src, locale('error.minimumallowed')..lib.math.groupdigits(minPayment), 'error') - return - end - - if not removeMoney(src, paymentAmount, 'vehicle-finance-payment') then return end - - financeStorage.updateVehicleFinance({ - balance = newBalance, - payment = newPayment, - paymentsLeft = newPaymentsLeft, - timer = timer - }, vehId) -end) - - ----@param vehId number -RegisterNetEvent('qbx_vehicleshop:server:financePaymentFull', function(vehId) - local src = source - local vehData = financeStorage.fetchFinancedVehicleEntityById(vehId) - - if not removeMoney(src, vehData.balance, 'vehicle-finance-payment-full') then return end - - financeStorage.updateVehicleFinance({ - balance = 0, - payment = 0, - paymentsLeft = 0, - timer = 0, - }, vehId) -end) - ----@param src number ----@param data {coords: vector4, vehicleId?: number, modelName: string, plate?: string, props?: {plate: string}} ----@return number|nil -local function spawnVehicle(src, data) - local coords, vehicleId = data.coords, data.vehicleId - local newVehicle = vehicleId and qbx_vehicles:GetPlayerVehicle(vehicleId) or data - if not newVehicle then return end - - local plate = newVehicle.plate or newVehicle.props.plate - - local netId, vehicle = qbx.spawnVehicle({ - model = newVehicle.modelName, - spawnSource = coords, - warp = GetPlayerPed(src), - props = { - plate = plate - } - }) - - if not netId or netId == 0 then return end - - if not vehicle or vehicle == 0 then return end - - if vehicleId then Entity(vehicle).state:set('vehicleid', vehicleId, false) end - - config.giveKeys(src, plate, vehicle) - - return netId -end - ---@param data {vehicle: string} RegisterNetEvent('qbx_vehicleshop:server:testDrive', function(data) local src = source @@ -318,17 +23,17 @@ RegisterNetEvent('qbx_vehicleshop:server:testDrive', function(data) return exports.qbx_core:Notify(src, locale('error.testdrive_alreadyin'), 'error') end - local shopId = getShopZone(src) + local shopId = GetShopZone(src) if not shopId then return end - if not checkVehicleList(data.vehicle, shopId) then + if not CheckVehicleList(data.vehicle, shopId) then return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') end local testDrive = sharedConfig.shops[shopId].testDrive local plate = 'TEST'..lib.string.random('1111') - local netId = spawnVehicle(src, { + local netId = SpawnVehicle(src, { modelName = data.vehicle, coords = testDrive.spawn, plate = plate @@ -379,23 +84,23 @@ end) RegisterNetEvent('qbx_vehicleshop:server:buyShowroomVehicle', function(vehicleData) local src = source - local shopId = getShopZone(src) + local shopId = GetShopZone(src) local shop = sharedConfig.shops[shopId] if not shop then return end local vehicle = vehicleData.buyVehicle - if not checkVehicleList(vehicle, shopId) then + if not CheckVehicleList(vehicle, shopId) then return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') end local player = exports.qbx_core:GetPlayer(src) local vehiclePrice = coreVehicles[vehicle].price - if not removeMoney(src, vehiclePrice, 'vehicle-bought-in-showroom') then + if not RemoveMoney(src, vehiclePrice, 'vehicle-bought-in-showroom') then return exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') end - local vehicleId = qbx_vehicles:CreatePlayerVehicle({ + local vehicleId = exports.qbx_vehicles:CreatePlayerVehicle({ model = vehicle, citizenid = player.PlayerData.citizenid, }) @@ -403,76 +108,10 @@ RegisterNetEvent('qbx_vehicleshop:server:buyShowroomVehicle', function(vehicleDa exports.qbx_core:Notify(src, locale('success.purchased'), 'success') - spawnVehicle(src, { - coords = shop.vehicleSpawn, - vehicleId = vehicleId - }) -end) - ----@param downPayment number ----@param paymentAmount number ----@param vehicle string -RegisterNetEvent('qbx_vehicleshop:server:financeVehicle', function(downPayment, paymentAmount, vehicle) - local src = source - - local shopId = getShopZone(src) - - local shop = sharedConfig.shops[shopId] - if not shop then return end - - if not checkVehicleList(vehicle, shopId) then - return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') - end - - local player = exports.qbx_core:GetPlayer(src) - local vehiclePrice = coreVehicles[vehicle].price - local minDown = tonumber(lib.math.round((sharedConfig.finance.minimumDown / 100) * vehiclePrice)) --[[@as number]] - - downPayment = tonumber(downPayment) --[[@as number]] - paymentAmount = tonumber(paymentAmount) --[[@as number]] - - if downPayment > vehiclePrice then - return exports.qbx_core:Notify(src, locale('error.notworth'), 'error') - end - - if downPayment < minDown then - return exports.qbx_core:Notify(src, locale('error.downtoosmall'), 'error') - end - - if paymentAmount > sharedConfig.finance.maximumPayments then - return exports.qbx_core:Notify(src, locale('error.exceededmax'), 'error') - end - - if not removeMoney(src, downPayment, 'vehicle-financed-in-showroom') then - return exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') - end - - local balance, vehPaymentAmount = calculateFinance(vehiclePrice, downPayment, paymentAmount) - local cid = player.PlayerData.citizenid - local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) - - local vehicleId = financeStorage.insertVehicleEntityWithFinance({ - insertVehicleEntityRequest = { - citizenId = cid, - model = vehicle, - }, - - vehicleFinance = { - balance = balance, - payment = vehPaymentAmount, - paymentsLeft = paymentAmount, - timer = timer, - } - }) - - exports.qbx_core:Notify(src, locale('success.purchased'), 'success') - - spawnVehicle(src, { + SpawnVehicle(src, { coords = shop.vehicleSpawn, vehicleId = vehicleId }) - - financeTimer[src].hasFinanced = true end) ---@param src number @@ -480,10 +119,10 @@ end) ---@param price number ---@param downPayment number ---@return boolean success -local function sellShowroomVehicleTransact(src, target, price, downPayment) +function SellShowroomVehicleTransact(src, target, price, downPayment) local player = exports.qbx_core:GetPlayer(src) - if not removeMoney(target.PlayerData.source, downPayment, 'vehicle-bought-in-showroom') then + if not RemoveMoney(target.PlayerData.source, downPayment, 'vehicle-bought-in-showroom') then exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') return false end @@ -512,97 +151,28 @@ RegisterNetEvent('qbx_vehicleshop:server:sellShowroomVehicle', function(vehicle, return exports.qbx_core:Notify(src, locale('error.playertoofar'), 'error') end - local shopId = getShopZone(target.PlayerData.source) + local shopId = GetShopZone(target.PlayerData.source) local shop = sharedConfig.shops[shopId] if not shop then return end - if not checkVehicleList(vehicle, shopId) then + if not CheckVehicleList(vehicle, shopId) then return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') end local vehiclePrice = coreVehicles[vehicle].price local cid = target.PlayerData.citizenid - if not sellShowroomVehicleTransact(src, target, vehiclePrice, vehiclePrice) then return end + if not SellShowroomVehicleTransact(src, target, vehiclePrice, vehiclePrice) then return end - local vehicleId = qbx_vehicles:CreatePlayerVehicle({ + local vehicleId = exports.qbx_vehicles:CreatePlayerVehicle({ model = vehicle, citizenid = cid, }) - spawnVehicle(src, { - coords = shop.vehicleSpawn, - vehicleId = vehicleId - }) -end) - ----@param downPayment number ----@param paymentAmount number ----@param vehicle string ----@param playerId string|number -RegisterNetEvent('qbx_vehicleshop:server:sellfinanceVehicle', function(downPayment, paymentAmount, vehicle, playerId) - local src = source - local target = exports.qbx_core:GetPlayer(tonumber(playerId)) - - if not target then - return exports.qbx_core:Notify(src, locale('error.Invalid_ID'), 'error') - end - - if #(GetEntityCoords(GetPlayerPed(src)) - GetEntityCoords(GetPlayerPed(target.PlayerData.source))) >= 3 then - return exports.qbx_core:Notify(src, locale('error.playertoofar'), 'error') - end - - local shopId = getShopZone(target.PlayerData.source) - local shop = sharedConfig.shops[shopId] - if not shop then return end - - if not checkVehicleList(vehicle, shopId) then - return exports.qbx_core:Notify(src, locale('error.notallowed'), 'error') - end - - downPayment = tonumber(downPayment) --[[@as number]] - paymentAmount = tonumber(paymentAmount) --[[@as number]] - - local vehiclePrice = coreVehicles[vehicle].price - local minDown = tonumber(lib.math.round((sharedConfig.finance.minimumDown / 100) * vehiclePrice)) --[[@as number]] - - if downPayment > vehiclePrice then - return exports.qbx_core:Notify(src, locale('error.notworth'), 'error') - end - - if downPayment < minDown then - return exports.qbx_core:Notify(src, locale('error.downtoosmall'), 'error') - end - - if paymentAmount > sharedConfig.finance.maximumPayments then - return exports.qbx_core:Notify(src, locale('error.exceededmax'), 'error') - end - - local cid = target.PlayerData.citizenid - local timer = (config.finance.paymentInterval * 60) + (math.floor((os.time() - financeTimer[src].time) / 60)) - local balance, vehPaymentAmount = calculateFinance(vehiclePrice, downPayment, paymentAmount) - - if not sellShowroomVehicleTransact(src, target, vehiclePrice, downPayment) then return end - - local vehicleId = financeStorage.insertVehicleEntityWithFinance({ - insertVehicleEntityRequest = { - citizenId = cid, - model = vehicle, - }, - - vehicleFinance = { - balance = balance, - payment = vehPaymentAmount, - paymentsLeft = paymentAmount, - timer = timer, - } - }) - - spawnVehicle(src, { + SpawnVehicle(src, { coords = shop.vehicleSpawn, vehicleId = vehicleId }) - financeTimer[target.PlayerData.source].hasFinanced = true end) -- Transfer vehicle to player in passenger seat @@ -646,15 +216,15 @@ lib.addCommand('transfervehicle', { return exports.qbx_core:Notify(source, locale('error.notinveh'), 'error') end - local vehicleId = Entity(vehicle).state.vehicleid or qbx_vehicles:GetVehicleIdByPlate(GetVehicleNumberPlateText(vehicle)) + local vehicleId = Entity(vehicle).state.vehicleid or exports.qbx_vehicles:GetVehicleIdByPlate(GetVehicleNumberPlateText(vehicle)) if not vehicleId then return exports.qbx_core:Notify(source, locale('error.notowned'), 'error') end local player = exports.qbx_core:GetPlayer(source) local target = exports.qbx_core:GetPlayer(buyerId) - local row = qbx_vehicles:GetPlayerVehicle(vehicleId) - local isFinanced = financeStorage.fetchIsFinanced(vehicleId) + local row = exports.qbx_vehicles:GetPlayerVehicle(vehicleId) + local isFinanced = sharedConfig.finance.enable and financeStorage.fetchIsFinanced(vehicleId) if not row then return end @@ -696,7 +266,7 @@ lib.addCommand('transfervehicle', { end if sellAmount > 0 then - local currencyType = findChargeableCurrencyType(sellAmount, target.PlayerData.money.cash, target.PlayerData.money.bank) + local currencyType = FindChargeableCurrencyType(sellAmount, target.PlayerData.money.cash, target.PlayerData.money.bank) if not currencyType then return exports.qbx_core:Notify(source, locale('error.buyertoopoor'), 'error') @@ -706,7 +276,7 @@ lib.addCommand('transfervehicle', { config.removePlayerFunds(target, currencyType, sellAmount, 'vehicle-bought-from-player') end - qbx_vehicles:SetPlayerVehicleOwner(row.id, targetcid) + exports.qbx_vehicles:SetPlayerVehicleOwner(row.id, targetcid) config.giveKeys(buyerId, row.plate, vehicle) local sellerMessage = sellAmount > 0 and locale('success.soldfor') .. lib.math.groupdigits(sellAmount) or locale('success.gifted') @@ -715,32 +285,7 @@ lib.addCommand('transfervehicle', { exports.qbx_core:Notify(source, sellerMessage, 'success') exports.qbx_core:Notify(buyerId, buyerMessage, 'success') if isFinanced then - financeTimer[buyerId].hasFinanced = true + SetHasFinanced(buyerId, true) end end, GetEntityModel(vehicle), sellAmount) end) - ----@param vehicleId integer ----@return boolean -local function isFinanced(vehicleId) - return financeStorage.fetchIsFinanced(vehicleId) -end - -exports('IsFinanced', isFinanced) - ----@param shopShape vector3[] ----@param shopName string -local function createShop(shopShape, shopName) - return lib.zones.poly({ - name = shopName, - points = shopShape, - thickness = 5, - }) -end - --- Create shop zones for point checking -CreateThread(function() - for shopName, shop in pairs(sharedConfig.shops) do - shopZones[#shopZones + 1] = createShop(shop.zone.shape, shopName) - end -end) diff --git a/server/utils.lua b/server/utils.lua new file mode 100644 index 0000000..b7b9d6e --- /dev/null +++ b/server/utils.lua @@ -0,0 +1,113 @@ +local config = require 'config.server' +local allowedVehicles = require 'server.vehicles' + +---@param vehicle string Vehicle model name to check if allowed for purchase/testdrive/etc. +---@param shop string? Shop name to check if vehicle is allowed in that shop +---@return boolean +function CheckVehicleList(vehicle, shop) + for i = 1, allowedVehicles.count do + local allowedVeh = allowedVehicles.vehicles[i] + if allowedVeh.model == vehicle then + if shop and allowedVeh.shopType == shop then + return true + elseif not shop then + return true + end + end + end + return false +end + +local shops = require 'config.shared'.shops +local shopZones = {} + +---@param source number +---@return string? +function GetShopZone(source) + local coords = GetEntityCoords(GetPlayerPed(source)) + for i = 1, #shopZones do + local zone = shopZones[i] + if zone:contains(coords) then + return zone.name + end + end +end + +---@param shopShape vector3[] +---@param shopName string +local function createShop(shopShape, shopName) + return lib.zones.poly({ + name = shopName, + points = shopShape, + thickness = 5, + }) +end + +-- Create shop zones for point checking +CreateThread(function() + for shopName, shop in pairs(shops) do + shopZones[#shopZones + 1] = createShop(shop.zone.shape, shopName) + end +end) + +---@param price number +---@param cash number +---@param bank number +---@return 'cash'|'bank'|nil +function FindChargeableCurrencyType(price, cash, bank) + if cash >= price then + return 'cash' + elseif bank >= price then + return 'bank' + else + return + end +end + +---@param src number +---@param amount number +---@param reason string? +---@return boolean success if money was removed +function RemoveMoney(src, amount, reason) + local player = exports.qbx_core:GetPlayer(src) + local cash = player.PlayerData.money.cash + local bank = player.PlayerData.money.bank + local currencyType = FindChargeableCurrencyType(amount, cash, bank) + + if not currencyType then + exports.qbx_core:Notify(src, locale('error.notenoughmoney'), 'error') + return false + end + + return config.removePlayerFunds(player, currencyType, amount, reason) +end + +---@param src number +---@param data {coords: vector4, vehicleId?: number, modelName: string, plate?: string, props?: {plate: string}} +---@return number|nil +function SpawnVehicle(src, data) + local coords, vehicleId = data.coords, data.vehicleId + local newVehicle = vehicleId and exports.qbx_vehicles:GetPlayerVehicle(vehicleId) or data + if not newVehicle then return end + + local plate = newVehicle.plate or newVehicle.props.plate + + local netId, vehicle = qbx.spawnVehicle({ + model = newVehicle.modelName, + spawnSource = coords, + warp = GetPlayerPed(src), + props = { + plate = plate + } + }) + + if not netId or netId == 0 then return end + + if not vehicle or vehicle == 0 then return end + + if vehicleId then Entity(vehicle).state:set('vehicleid', vehicleId, false) end + + config.giveKeys(src, plate, vehicle) + + return netId +end \ No newline at end of file