local config = require 'config.server' local sharedConfig = require 'config.shared' local logger = require '@qbx_core.modules.logger' local enteredProperty = {} local insideProperty = {} local citizenid = {} local ring = {} function EnterProperty(playerSource, id, isSpawn) local property = MySQL.single.await('SELECT * FROM properties WHERE id = ?', {id}) if not property then return end -- Lua and its stupid need check nil warnings local propertyCoords = json.decode(property.coords) propertyCoords = vec3(propertyCoords.x, propertyCoords.y, propertyCoords.z) local playerCoords = GetEntityCoords(GetPlayerPed(playerSource)) if not isSpawn and #(playerCoords - propertyCoords) > 8.0 then return end local player = exports.qbx_core:GetPlayer(playerSource) citizenid[playerSource] = player.PlayerData.citizenid local interactions = {} local isInteriorShell = tonumber(property.interior) ~= nil local stashes = json.decode(property.stash_options) for i = 1, #stashes do local stashCoords = isInteriorShell and CalculateOffsetCoords(propertyCoords, stashes[i].coords) or stashes[i].coords interactions[#interactions + 1] = { type = 'stash', coords = vec3(stashCoords.x, stashCoords.y, stashCoords.z) } exports.ox_inventory:RegisterStash(string.format('qbx_properties_%s', property.property_name), string.format('Property: %s', property.property_name), stashes[i].slots, stashes[i].maxWeight, property.owner) end if isInteriorShell then TriggerClientEvent('qbx_properties:client:createInterior', playerSource, tonumber(property.interior), vec3(propertyCoords.x, propertyCoords.y, propertyCoords.z - sharedConfig.shellUndergroundOffset)) end local interactData = json.decode(property.interact_options) for i = 1, #interactData do local coords = isInteriorShell and CalculateOffsetCoords(propertyCoords, interactData[i].coords) or interactData[i].coords interactions[#interactions + 1] = { type = interactData[i].type, coords = vec3(coords.x, coords.y, coords.z) } if interactData[i].type == 'exit' then SetEntityCoords(GetPlayerPed(playerSource), coords.x, coords.y, coords.z, false, false, false, false) SetEntityHeading(GetPlayerPed(playerSource), coords.w) end end enteredProperty[playerSource] = id insideProperty[id] = insideProperty[id] or {} insideProperty[id][#insideProperty[id] + 1] = playerSource lib.triggerClientEvent('qbx_properties:client:concealPlayers', insideProperty[id], insideProperty[id]) player.Functions.SetMetaData('currentPropertyId', id) local decorations = MySQL.query.await('SELECT `id`, `model`, `coords`, `rotation` FROM `properties_decorations` WHERE `property_id` = ?', {id}) for i = 1, #decorations do local temp = json.decode(decorations[i].coords) decorations[i].coords = isInteriorShell and CalculateOffsetCoords(propertyCoords, vec3(temp.x, temp.y, temp.z)) or vec3(temp.x, temp.y, temp.z) temp = json.decode(decorations[i].rotation) decorations[i].rotation = vec3(temp.x, temp.y, temp.z) end TriggerClientEvent('qbx_properties:client:loadDecorations', playerSource, decorations) TriggerClientEvent('qbx_properties:client:updateInteractions', playerSource, interactions, property.interior, type(property.rent_interval) == 'number') logger.log({ source = playerSource, event = 'qbx_properties:server:enterProperty', message = locale('logs.enter_property', player.PlayerData.citizenid, property.property_name), webhook = config.discordWebhook }) end ---@param playerSource integer local function exitProperty(playerSource, isLogout) if not enteredProperty[playerSource] then return end TriggerClientEvent('qbx_properties:client:unloadProperty', playerSource) TriggerClientEvent('qbx_properties:client:revealPlayers', playerSource) if not isLogout then local enterCoords = json.decode(MySQL.single.await('SELECT coords FROM properties WHERE id = ?', {enteredProperty[playerSource]}).coords) SetEntityCoords(GetPlayerPed(playerSource), enterCoords.x, enterCoords.y, enterCoords.z, false, false, false, false) end for i = 1, #insideProperty[enteredProperty[playerSource]] do if insideProperty[enteredProperty[playerSource]][i] == playerSource then table.remove(insideProperty[enteredProperty[playerSource]], i) break end end lib.triggerClientEvent('qbx_properties:client:concealPlayers', insideProperty[enteredProperty[playerSource]], insideProperty[enteredProperty[playerSource]]) local logPropertyId = enteredProperty[playerSource] enteredProperty[playerSource] = nil if isLogout then return end local player = exports.qbx_core:GetPlayer(playerSource) if not player then return end player.Functions.SetMetaData('currentPropertyId', nil) logger.log({ source = playerSource, event = 'qbx_properties:server:exitProperty', message = locale('logs.exit_property', player.PlayerData.citizenid, logPropertyId), webhook = config.discordWebhook }) end RegisterNetEvent('qbx_properties:server:exitProperty', function() exitProperty(source --[[@as number]]) end) AddEventHandler('QBCore:Server:OnPlayerUnload', function(source) exitProperty(source, true) end) lib.callback.register('qbx_properties:callback:loadProperties', function() local result = MySQL.query.await('SELECT coords FROM properties GROUP BY coords') local properties = {} for i = 1, #result do local coords = json.decode(result[i].coords) properties[i] = vec3(coords.x, coords.y, coords.z) end return properties end) lib.callback.register('qbx_properties:callback:requestProperties', function(_, propertyCoords) return MySQL.query.await('SELECT property_name, owner, id, price, rent_interval, keyholders FROM properties WHERE coords = ?', {json.encode(propertyCoords)}) end) local function hasAccess(citizenId, propertyId) local property = MySQL.single.await('SELECT owner, keyholders FROM properties WHERE id = ?', {propertyId}) if citizenId == property.owner then return true end local keyholders = json.decode(property.keyholders) for i = 1, #keyholders do if citizenId == keyholders[i] then return true end end return false end RegisterNetEvent('qbx_properties:server:enterProperty', function(data) local playerSource = source --[[@as number]] local player = exports.qbx_core:GetPlayer(playerSource) local propertyId = data.id if not hasAccess(player.PlayerData.citizenid, propertyId) then return end EnterProperty(playerSource, propertyId, data.isSpawn) end) RegisterNetEvent('qbx_properties:server:ringProperty', function(data) local playerSource = source --[[@as number]] local propertyId = data.id local property = MySQL.single.await('SELECT owner FROM properties WHERE id = ?', {propertyId}) local owner = exports.qbx_core:GetPlayerByCitizenId(property.owner) ring[propertyId] = ring[propertyId] or {} if not lib.table.contains(ring[propertyId], playerSource) then ring[propertyId][#ring[propertyId] + 1] = playerSource SetTimeout(300000, function() for i = 1, #ring[propertyId] do if ring[propertyId][i] == playerSource then table.remove(ring[propertyId], i) break end end end) end if owner and enteredProperty[owner.PlayerData.source] == propertyId then exports.qbx_core:Notify(owner.PlayerData.source, locale('notify.someone_at_door')) end end) lib.callback.register('qbx_properties:callback:requestKeyHolders', function(source) local propertyId = enteredProperty[source] local result = MySQL.single.await('SELECT owner, keyholders FROM properties WHERE id = ?', {propertyId}) local player = exports.qbx_core:GetPlayer(source) if player.PlayerData.citizenid ~= result.owner then return end local keyholders = json.decode(result.keyholders) local currentholders = {} for i = 1, #keyholders do local offlinePlayer = exports.qbx_core:GetOfflinePlayer(keyholders[i]) if offlinePlayer then currentholders[#currentholders + 1] = { citizenid = offlinePlayer.PlayerData.citizenid, name = offlinePlayer.PlayerData.charinfo.firstname .. ' ' .. offlinePlayer.PlayerData.charinfo.lastname } end end return currentholders end) lib.callback.register('qbx_properties:callback:requestPotentialKeyholders', function(source) local propertyId = enteredProperty[source] local result = MySQL.single.await('SELECT owner FROM properties WHERE id = ?', {propertyId}) local owner = exports.qbx_core:GetPlayer(source) if owner.PlayerData.citizenid ~= result.owner then return end local players = insideProperty[propertyId] local insidePlayers = {} for i = 1, #players do local player = exports.qbx_core:GetPlayer(players[i]) if player and not hasAccess(player.PlayerData.citizenid, propertyId) then insidePlayers[#insidePlayers + 1] = { citizenid = player.PlayerData.citizenid, name = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname } end end return insidePlayers end) lib.callback.register('qbx_properties:callback:requestRingers', function(source) local propertyId = enteredProperty[source] local players = ring[propertyId] or {} local ringers = {} for i = 1, #players do local player = exports.qbx_core:GetPlayer(players[i]) if player then ringers[#ringers + 1] = { citizenid = player.PlayerData.citizenid, name = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname } end end return ringers end) lib.callback.register('qbx_properties:callback:checkAccess', function(source) local propertyId = enteredProperty[source] local result = MySQL.single.await('SELECT owner FROM properties WHERE id = ?', {propertyId}) return result.owner == exports.qbx_core:GetPlayer(source).PlayerData.citizenid end) RegisterNetEvent('qbx_properties:server:letRingerIn', function(visitorCid) local playerSource = source --[[@as number]] local player = exports.qbx_core:GetPlayer(playerSource) local propertyId = enteredProperty[playerSource] local result = MySQL.single.await('SELECT owner, interior FROM properties WHERE id = ?', {propertyId}) if player.PlayerData.citizenid ~= result.owner then return end local visitor = exports.qbx_core:GetPlayerByCitizenId(visitorCid) if not visitor then return end EnterProperty(visitor.PlayerData.source, propertyId) for i = 1, #ring[propertyId] do if ring[propertyId][i] == visitor.PlayerData.source then table.remove(ring[propertyId], i) break end end end) RegisterNetEvent('qbx_properties:server:addKeyholder', function(keyholderCid) local playerSource = source --[[@as number]] local owner = exports.qbx_core:GetPlayer(playerSource) local propertyId = enteredProperty[playerSource] local result = MySQL.single.await('SELECT owner, keyholders FROM properties WHERE id = ?', {propertyId}) if owner.PlayerData.citizenid ~= result.owner then return end local keyholders = json.decode(result.keyholders) if lib.table.contains(keyholders, keyholderCid) then return end keyholders[#keyholders + 1] = keyholderCid MySQL.Sync.execute('UPDATE properties SET keyholders = ? WHERE id = ?', {json.encode(keyholders), propertyId}) local keyholder = exports.qbx_core:GetPlayerByCitizenId(keyholderCid) exports.qbx_core:Notify(playerSource, keyholder.PlayerData.charinfo.firstname.. locale('notify.keyholder')) exports.qbx_core:Notify(keyholder.PlayerData.source, locale('notify.added_as_keyholder')) logger.log({ source = playerSource, event = 'qbx_properties:server:addKeyholder', message = locale('logs.added_keyholder', keyholderCid, propertyId), webhook = config.discordWebhook }) end) RegisterNetEvent('qbx_properties:server:removeKeyholder', function(keyholderCid) local playerSource = source --[[@as number]] local owner = exports.qbx_core:GetPlayer(playerSource) local propertyId = enteredProperty[playerSource] local result = MySQL.single.await('SELECT owner, keyholders FROM properties WHERE id = ?', {propertyId}) if owner.PlayerData.citizenid ~= result.owner then return end local keyholders = json.decode(result.keyholders) if not lib.table.contains(keyholders, keyholderCid) then return end for i = 1, #keyholders do if keyholders[i] == keyholderCid then table.remove(keyholders, i) break end end MySQL.Sync.execute('UPDATE properties SET keyholders = ? WHERE id = ?', {json.encode(keyholders), propertyId}) local keyholder = exports.qbx_core:GetOfflinePlayer(keyholderCid) exports.qbx_core:Notify(playerSource, keyholder.PlayerData.charinfo.firstname.. locale('notify.removed_as_keyholder')) logger.log({ source = playerSource, event = 'qbx_properties:server:removeKeyholder', message = locale('logs.removed_keyholder', keyholderCid, propertyId), webhook = config.discordWebhook }) end) RegisterNetEvent('qbx_properties:server:logoutProperty', function() local playerSource = source --[[@as number]] local propertyId = enteredProperty[playerSource] if not propertyId then return end local result = MySQL.single.await('SELECT owner, coords FROM properties WHERE id = ?', {propertyId}) local player = exports.qbx_core:GetPlayer(playerSource) if player.PlayerData.citizenid ~= result.owner then return end TriggerClientEvent('qbx_properties:client:unloadProperty', playerSource) TriggerClientEvent('qbx_properties:client:revealPlayers', playerSource) for i = 1, #insideProperty[enteredProperty[playerSource]] do if insideProperty[enteredProperty[playerSource]][i] == playerSource then table.remove(insideProperty[enteredProperty[playerSource]], i) break end end lib.triggerClientEvent('qbx_properties:client:concealPlayers', insideProperty[enteredProperty[playerSource]], insideProperty[enteredProperty[playerSource]]) enteredProperty[playerSource] = nil exports.qbx_core:Logout(playerSource) Wait(50) local coords = json.decode(result.coords) MySQL.update('UPDATE players SET position = ? WHERE citizenid = ?', { json.encode(vec4(coords.x, coords.y, coords.z, 0.0)), player.PlayerData.citizenid }) end) RegisterNetEvent('qbx_properties:server:openStash', function() local playerSource = source --[[@as number]] local propertyId = enteredProperty[playerSource] local player = exports.qbx_core:GetPlayer(playerSource) if not hasAccess(player.PlayerData.citizenid, propertyId) then return end local property = MySQL.single.await('SELECT property_name FROM properties WHERE id = ?', {propertyId}) exports.ox_inventory:forceOpenInventory(playerSource, 'stash', { id = string.format('qbx_properties_%s', property.property_name) }) end) AddEventHandler('playerDropped', function () local playerSource = source --[[@as number]] if not enteredProperty[playerSource] then return end for i = 1, #insideProperty[enteredProperty[playerSource]] do if insideProperty[enteredProperty[playerSource]][i] == playerSource then table.remove(insideProperty[enteredProperty[playerSource]], i) break end end Wait(50) local coords = json.decode(MySQL.single.await('SELECT coords FROM properties WHERE id = ?', {enteredProperty[playerSource]}).coords) 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 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 if player.Offline then player.PlayerData.money.bank = player.PlayerData.money.bank - property.price if player.PlayerData.money.bank < 0 then break end exports.qbx_core:SaveOffline(player.PlayerData) else if not player.Functions.RemoveMoney('bank', property.price, string.format('Rent for %s', property.property_name)) then exports.qbx_core:Notify(player.PlayerData.source, string.format('Not enough money to pay rent for %s', property.property_name), 'error') break end end Wait(property.rent_interval * 3600000) end MySQL.update('UPDATE properties SET owner = ? WHERE id = ?', {nil, propertyId}) end) end 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, 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 if not property.rent_interval then return end if player.PlayerData.money.bank < property.price then exports.qbx_core:Notify(playerSource, 'Not enough money to rent property.', 'error') 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() logger.log({ source = playerSource, event = 'qbx_properties:server:rentProperty', message = locale('logs.rent_property', player.PlayerData.citizenid, propertyId), webhook = config.discordWebhook }) end) 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, 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 if not player.Functions.RemoveMoney('cash', property.price, string.format('Purchased %s', property.property_name)) and not player.Functions.RemoveMoney('bank', property.price, string.format('Purchased %s', property.property_name)) then exports.qbx_core:Notify(playerSource, 'Not enough money to purchase property.', 'error') 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)) logger.log({ source = playerSource, event = 'qbx_properties:server:buyProperty', message = locale('logs.buy_property', player.PlayerData.citizenid, propertyId), webhook = config.discordWebhook }) 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() local player = exports.qbx_core:GetPlayer(source) local propertyId = enteredProperty[source] local property = MySQL.single.await('SELECT owner, property_name FROM properties WHERE id = ?', {propertyId}) if player.PlayerData.citizenid ~= property.owner then return end exports.qbx_core:Notify(player.PlayerData.source, string.format('You stopped your rental contract for %s', property.property_name), 'success') MySQL.update.await('UPDATE properties SET owner = ?, keyholders = JSON_OBJECT() WHERE id = ?', {nil, propertyId}) for _ = 1, #insideProperty[propertyId] do exitProperty(insideProperty[propertyId][1]) end logger.log({ source = player.PlayerData.source, event = 'qbx_properties:server:stopRenting', message = locale('logs.stop_renting', player.PlayerData.citizenid, propertyId), webhook = config.discordWebhook }) end) RegisterNetEvent('qbx_properties:server:addDecoration', function(hash, coords, rotation, objectId) local player = exports.qbx_core:GetPlayer(source) local propertyId = enteredProperty[source] local property = MySQL.single.await('SELECT owner, property_name FROM properties WHERE id = ?', {propertyId}) if player.PlayerData.citizenid ~= property.owner then return end if objectId then lib.triggerClientEvent('qbx_properties:client:removeDecoration', insideProperty[propertyId], objectId) MySQL.update.await('UPDATE properties_decorations SET coords = ?, rotation = ? WHERE id = ?', { json.encode(coords), json.encode(rotation), objectId }) lib.triggerClientEvent('qbx_properties:client:addDecoration', insideProperty[propertyId], objectId, hash, coords, rotation) else local id = MySQL.insert.await('INSERT INTO `properties_decorations` (property_id, model, coords, rotation) VALUES (?, ?, ?, ?)', {propertyId, hash, json.encode(coords), json.encode(rotation)}) lib.triggerClientEvent('qbx_properties:client:addDecoration', insideProperty[propertyId], id, hash, coords, rotation) end logger.log({ source = player.PlayerData.source, event = 'qbx_properties:server:addDecoration', message = locale('logs.add_decoration', player.PlayerData.citizenid, hash, propertyId), webhook = config.discordWebhook }) end) RegisterNetEvent('qbx_properties:server:removeDecoration', function(objectId) local player = exports.qbx_core:GetPlayer(source) local propertyId = enteredProperty[source] local property = MySQL.single.await('SELECT owner FROM properties WHERE id = ?', {propertyId}) if player.PlayerData.citizenid ~= property.owner then return end MySQL.query.await('DELETE FROM properties_decorations WHERE id = ?', {objectId}) lib.triggerClientEvent('qbx_properties:client:removeDecoration', insideProperty[propertyId], objectId) logger.log({ source = player.PlayerData.source, event = 'qbx_properties:server:removeDecoration', message = locale('logs.remove_decoration', player.PlayerData.citizenid, objectId, propertyId), webhook = config.discordWebhook }) end)