From a71ebdc7908565f496e5a18c14e0bcfe60caf3ff Mon Sep 17 00:00:00 2001 From: Codexis <85877781+CodexisPhantom@users.noreply.github.com> Date: Sun, 9 Feb 2025 23:20:32 +0100 Subject: [PATCH] feat: add qbx_garages support (#43) * feat: added qbx_garages support * refactor: removed duplicate code * refactor: addGaragePoint checks * refactor: cleaned code * refactor: cleaned code * Apply suggestions from code review --------- Co-authored-by: Frowmza <66181451+Frowmza@users.noreply.github.com> --- client/realtor.lua | 81 ++++++++++++++++++++++++++++++++++++++++++-- locales/en.json | 3 +- property_garages.sql | 1 + server/property.lua | 53 +++++++++++++++++++++++++++-- server/realtor.lua | 18 ++++++---- 5 files changed, 143 insertions(+), 13 deletions(-) create mode 100644 property_garages.sql diff --git a/client/realtor.lua b/client/realtor.lua index 7670acf..ea38717 100644 --- a/client/realtor.lua +++ b/client/realtor.lua @@ -4,10 +4,24 @@ for k in pairs(sharedConfig.interiors) do values[#values + 1] = k end +local car = 0 local shell = 0 local playerCoords = vec3(0, 0, 0) +local garageCoords = nil +local garageHeading = 0.0 local isPreviewing = false +local function showText() + lib.showTextUI('BACKSPACE - Exit \n ARROW LEFT - Turn left \n ARROW RIGHT - Turn right \n ENTER - Confirm Placement') +end + +local function spawnCar() + local model = lib.requestModel('sultanrs', 2500) + car = CreateVehicle(model, 0.0, 0.0, 0.0, 0.0, false, false) + SetEntityCompletelyDisableCollision(car, false, false) + SetModelAsNoLongerNeeded(model) +end + local function previewProperty(propertyIndex) if DoesEntityExist(shell) then DeleteEntity(shell) end if type(propertyIndex) == 'number' then @@ -35,6 +49,63 @@ local function stopPreview() end end +local function addGaragePoint() + local isAddingGarage = true + garageCoords = nil + + showText() + spawnCar() + + while isAddingGarage do + Wait(0) + + local hit, _, endCoords = lib.raycast.fromCamera(511, 4, 25.0) + + if not hit then + SetEntityCoords(car, 0.0, 0.0, 0.0, false, false, false, false) + else + SetEntityCoords(car, endCoords.x, endCoords.y, endCoords.z + 1.0, false, false, false, false) + SetVehicleOnGroundProperly(car) + end + + if IsControlJustPressed(0, 202) then + if DoesEntityExist(car) then + DeleteEntity(car) + end + isAddingGarage = false + end + + if IsControlPressed(0, 190) then + garageHeading = (garageHeading + 2.5) % 360.0 + SetEntityHeading(car, garageHeading) + Wait(100) + end + + if IsControlPressed(0, 189) then + garageHeading = (garageHeading - 2.5) % 360.0 + SetEntityHeading(car, garageHeading) + Wait(100) + end + + if IsControlJustPressed(0, 18) then + if hit then + garageCoords = endCoords + garageHeading = GetEntityHeading(car) + if DoesEntityExist(car) then + DeleteEntity(car) + end + isAddingGarage = false + else + lib.notify({type = 'error', title = 'Error', description = 'Invalid garage location.'}) + end + end + + SetEntityHeading(car, garageHeading) -- Prevent the car from rotating + end + + lib.hideTextUI() +end + lib.registerMenu({ id = 'qbx_properties_realtor_menu', title = locale('menu.interior_preview'), @@ -54,10 +125,14 @@ lib.registerMenu({ {type = 'input', label = locale('alert.property_name'), description = locale('alert.property_name_description'), required = true, min = 4, max = 32, icon = 'home'}, {type = 'number', label = locale('alert.price'), description = locale('alert.price_description'), icon = 'dollar-sign', required = true, min = 1}, {type = 'number', label = locale('alert.rent_interval'), description = locale('alert.rent_interval_description'), icon = 'clock', min = 1, max = 24, step = 1}, + {type = 'checkbox', label = locale('alert.add_garage')}, }) - if input then - TriggerServerEvent('qbx_properties:server:createProperty', args[scrollIndex], input, playerCoords) + if not input then return end + + if input[4] then + addGaragePoint() end + TriggerServerEvent('qbx_properties:server:createProperty', args[scrollIndex], input, playerCoords, vec4(garageCoords.x, garageCoords.y, garageCoords.z + 1.0, garageHeading)) end) RegisterNetEvent('qbx_properties:client:createProperty', function() @@ -74,4 +149,4 @@ RegisterNetEvent('qbx_properties:client:createProperty', function() end Wait(3000) end -end) \ No newline at end of file +end) diff --git a/locales/en.json b/locales/en.json index c6acb49..eba00a4 100644 --- a/locales/en.json +++ b/locales/en.json @@ -17,7 +17,8 @@ "price": "Price", "price_description": "The purchasing price or the price deducted for rent", "rent_interval": "Rent interval", - "rent_interval_description": "The interval in which the rent will be deducted in hours (makes the property rentable instead of purchasable)" + "rent_interval_description": "The interval in which the rent will be deducted in hours (makes the property rentable instead of purchasable)", + "add_garage": "Add Garage" }, "menu": { "add_keyholder": "Add Keyholder", diff --git a/property_garages.sql b/property_garages.sql new file mode 100644 index 0000000..67ff2bc --- /dev/null +++ b/property_garages.sql @@ -0,0 +1 @@ +ALTER TABLE `properties` ADD COLUMN IF NOT EXISTS `garage` JSON DEFAULT NULL AFTER `stash_options`; \ No newline at end of file diff --git a/server/property.lua b/server/property.lua index 6e9f6b6..e7fcc53 100644 --- a/server/property.lua +++ b/server/property.lua @@ -338,11 +338,45 @@ AddEventHandler('playerDropped', function () MySQL.update('UPDATE players SET position = ? WHERE citizenid = ?', { json.encode(vec4(coords.x, coords.y, coords.z, 0.0)), citizenid[playerSource] }) end) +local function canAccess(source, owner, keyholders) + local player = exports.qbx_core:GetPlayer(source) + if player.PlayerData.citizenid == owner then return true end + for i = 1, #keyholders do + if player.PlayerData.citizenid == keyholders[i] then return true end + end + return false +end + +local function registerGarage(name, owner, keyholders, garage) + local garageName = 'property_' .. string.gsub(string.lower(name), ' ', '_') + exports.qbx_garages:RegisterGarage(garageName, { + label = name, + vehicleType = 'car', + accessPoints = { + { + coords = vec4(garage.x, garage.y, garage.z, garage.w), + } + }, + canAccess = function(source) + return canAccess(source, owner, keyholders) + end + }) +end + +local function registerGarages() + local properties = MySQL.query.await('SELECT property_name, owner, keyholders, garage FROM properties WHERE owner IS NOT NULL AND garage IS NOT NULL') + if not properties then return end + for i = 1, #properties do + local property = properties[i] + registerGarage(property.property_name, property.owner, json.decode(property.keyholders), json.decode(property.garage)) + end +end + local function startRentThread(propertyId) CreateThread(function() while true do local property = MySQL.single.await('SELECT owner, price, rent_interval, property_name FROM properties WHERE id = ?', {propertyId}) - if not property or not property.owner then break end + if not property or not property.owner then break end local player = exports.qbx_core:GetPlayerByCitizenId(property.owner) or exports.qbx_core:GetOfflinePlayer(property.owner) if not player then print(string.format('%s does not exist anymore, consider checking property id %s', property.owner, propertyId)) break end @@ -369,7 +403,7 @@ RegisterNetEvent('qbx_properties:server:rentProperty', function(propertyId) local playerSource = source --[[@as number]] local player = exports.qbx_core:GetPlayer(playerSource) local playerCoords = GetEntityCoords(GetPlayerPed(playerSource)) - local property = MySQL.single.await('SELECT owner, price, property_name, coords, rent_interval FROM properties WHERE id = ?', {propertyId}) + local property = MySQL.single.await('SELECT owner, price, property_name, coords, rent_interval, keyholders, garage FROM properties WHERE id = ?', {propertyId}) local propertyCoords = json.decode(property.coords) if #(playerCoords - vec3(propertyCoords.x, propertyCoords.y, propertyCoords.z)) > 8.0 then return end if property.owner then return end @@ -380,6 +414,10 @@ RegisterNetEvent('qbx_properties:server:rentProperty', function(propertyId) return end + if property.garage then + registerGarage(property.property_name, player.PlayerData.citizenid, json.decode(property.keyholders), json.decode(property.garage)) + end + exports.qbx_core:Notify(playerSource, string.format('Successfully started renting %s', property.property_name), 'success') MySQL.update('UPDATE properties SET owner = ? WHERE id = ?', {player.PlayerData.citizenid, propertyId}) startRentThread() @@ -389,7 +427,7 @@ RegisterNetEvent('qbx_properties:server:buyProperty', function(propertyId) local playerSource = source --[[@as number]] local player = exports.qbx_core:GetPlayer(playerSource) local playerCoords = GetEntityCoords(GetPlayerPed(playerSource)) - local property = MySQL.single.await('SELECT owner, price, property_name, coords FROM properties WHERE id = ?', {propertyId}) + local property = MySQL.single.await('SELECT owner, price, property_name, coords, keyholders, garage FROM properties WHERE id = ?', {propertyId}) local propertyCoords = json.decode(property.coords) if #(playerCoords - vec3(propertyCoords.x, propertyCoords.y, propertyCoords.z)) > 8.0 or property.owner then return end @@ -399,6 +437,10 @@ RegisterNetEvent('qbx_properties:server:buyProperty', function(propertyId) return end + if property.garage then + registerGarage(property.property_name, player.PlayerData.citizenid, json.decode(property.keyholders), json.decode(property.garage)) + end + MySQL.update('UPDATE properties SET owner = ? WHERE id = ?', {player.PlayerData.citizenid, propertyId}) exports.qbx_core:Notify(playerSource, string.format('Successfully purchased %s for $%s', property.property_name, property.price)) end) @@ -406,13 +448,18 @@ end) Citizen.CreateThreadNow(function() local sql1 = LoadResourceFile(cache.resource, 'property.sql') local sql2 = LoadResourceFile(cache.resource, 'decorations.sql') + local sql3 = LoadResourceFile(cache.resource, 'property_garages.sql') + MySQL.query.await(sql1) MySQL.query.await(sql2) + MySQL.query.await(sql3) local properties = MySQL.query.await('SELECT id FROM properties WHERE owner IS NOT NULL AND rent_interval IS NOT NULL') for i = 1, #properties do startRentThread(properties[i].id) end + + registerGarages() end) RegisterNetEvent('qbx_properties:server:stopRenting', function() diff --git a/server/realtor.lua b/server/realtor.lua index e7438e6..62b8ceb 100644 --- a/server/realtor.lua +++ b/server/realtor.lua @@ -11,13 +11,14 @@ lib.addCommand('createproperty', { TriggerClientEvent('qbx_properties:client:createProperty', source) end) -RegisterNetEvent('qbx_properties:server:createProperty', function(interiorIndex, data, propertyCoords) +RegisterNetEvent('qbx_properties:server:createProperty', function(interiorIndex, data, propertyCoords, garageCoords) local playerSource = source --[[@as number]] local player = exports.qbx_core:GetPlayer(playerSource) local playerCoords = GetEntityCoords(GetPlayerPed(playerSource)) if player.PlayerData.job.name ~= 'realestate' then return end - if #(playerCoords - propertyCoords) > 5.0 then return end + if not garageCoords and #(playerCoords - propertyCoords) > 5.0 then return end + if garageCoords and #(playerCoords - garageCoords.xyz) > 5.0 then return end local interactData = { { @@ -33,6 +34,7 @@ RegisterNetEvent('qbx_properties:server:createProperty', function(interiorIndex, coords = sharedConfig.interiors[interiorIndex].exit } } + local stashData = { { coords = sharedConfig.interiors[interiorIndex].stash, @@ -40,16 +42,20 @@ RegisterNetEvent('qbx_properties:server:createProperty', function(interiorIndex, maxWeight = config.apartmentStash.maxWeight, } } + local result = MySQL.single.await('SELECT id FROM properties ORDER BY id DESC', {}) - local propertNumber = result?.id or 0 - MySQL.insert('INSERT INTO `properties` (`coords`, `property_name`, `price`, `interior`, `interact_options`, `stash_options`, `rent_interval`) VALUES (?, ?, ?, ?, ?, ?, ?)', { + local propertyNumber = result?.id or 0 + local propertyName = string.format('%s %s', data[1], propertyNumber) + + MySQL.insert('INSERT INTO `properties` (`coords`, `property_name`, `price`, `interior`, `interact_options`, `stash_options`, `rent_interval`, `garage`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', { json.encode(propertyCoords), - string.format('%s %s', data[1], propertNumber), + propertyName, data[2], interiorIndex, json.encode(interactData), json.encode(stashData), - data[3] + data[3], + garageCoords and json.encode(garageCoords) or nil, }) TriggerClientEvent('qbx_properties:client:addProperty', -1, propertyCoords) end) \ No newline at end of file