├── .vscode └── settings.json ├── README.md ├── fxmanifest.lua ├── server ├── open.lua └── server.lua ├── shared └── config.lua └── client ├── open.lua └── client.lua /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Lua.diagnostics.disable": [ 3 | "lowercase-global", 4 | "undefined-field" 5 | ] 6 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Storage Units 3 | 4 | A storage unit system for fivem servers. Buy and store you legal/illegal items. 5 | 6 | ## Dependencies 7 | - Ox inventory 8 | - Ox lib 9 | 10 | ## Features 11 | 12 | - Unit Password 13 | - Cutting Power (robable units) 14 | - Resetting Password (owner only) 15 | - Units Limit for players 16 | 17 | 18 | ## Screenshots 19 | 20 | ![Unit](https://i.imgur.com/Hbc5EJl.png) 21 | 22 | -------------------------------------------------------------------------------- /fxmanifest.lua: -------------------------------------------------------------------------------- 1 | 2 | fx_version 'cerulean' 3 | use_experimental_fxv2_oal 'yes' 4 | author 'KevinGirardx' 5 | lua54 'yes' 6 | game 'gta5' 7 | 8 | files { 9 | 'shared/*.lua', 10 | } 11 | 12 | shared_scripts { 13 | '@ox_lib/init.lua', 14 | } 15 | 16 | client_scripts { 17 | '@qbx_core/modules/playerdata.lua', 18 | 'client/*.lua', 19 | } 20 | 21 | server_scripts { 22 | '@oxmysql/lib/MySQL.lua', 23 | 'server/*.lua', 24 | } 25 | 26 | ox_libs { 27 | 'math' 28 | } 29 | 30 | escrow_ignore { 31 | 'configs/*.lua', 32 | 'locales/*.json', 33 | } -------------------------------------------------------------------------------- /server/open.lua: -------------------------------------------------------------------------------- 1 | Framework = nil 2 | 3 | if GetResourceState('qbx_core') == 'started' then 4 | Framework = exports.qbx_core 5 | elseif GetResourceState('qb-core') == 'started' then 6 | Framework = exports['qb-core']:GetCoreObject() 7 | end 8 | 9 | function getPlayerCitizenId(source) 10 | if GetResourceState('qbx_core') == 'started' then 11 | return Framework:GetPlayer(source).PlayerData.citizenid 12 | elseif GetResourceState('qb-core') == 'started' then 13 | return Framework.Functions.GetPlayer(source).PlayerData.citizenid 14 | end 15 | end 16 | 17 | function getPlayerCash(source) 18 | if GetResourceState('qbx_core') == 'started' then 19 | return Framework:GetPlayer(source).PlayerData.money.cash 20 | elseif GetResourceState('qb-core') == 'started' then 21 | return Framework.Functions.GetPlayer(source).PlayerData.money.cash 22 | end 23 | end -------------------------------------------------------------------------------- /shared/config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | textUi = 'jg', -- jg, qb, ox 3 | notify = 'ox', -- ox, qb 4 | progressBar = 'ox_circle', -- ox_circle, ox_bar 5 | minigame = 'ox', -- ox 6 | target = { 7 | resource = 'ox', -- ox, qb, interact ( if using interact remove all the coords below the top section in the crates table) 8 | distance = 3.0, 9 | }, 10 | 11 | unitsAllowed = 2, -- how many units can a player own 12 | storageUnits = { 13 | [1] = { 14 | cost = 5000, 15 | inventory = { 16 | slots = 10, 17 | maxWeight = 150000, 18 | label = 'Storage Unit 1', 19 | }, 20 | coords = vector4(-44.00, -1232.85, 29.60, 89.99), 21 | }, 22 | [2] = { 23 | cost = 5000, 24 | inventory = { 25 | slots = 10, 26 | maxWeight = 150000, 27 | label = 'Storage Unit 2', 28 | }, 29 | coords = vector4(-44.0, -1239.23, 29.72, 89.99), 30 | }, 31 | [3] = { 32 | cost = 5000, 33 | inventory = { 34 | slots = 10, 35 | maxWeight = 150000, 36 | label = 'Storage Unit 3', 37 | }, 38 | coords = vector4(-64.9, -1224.47, 29.13, 51.0), 39 | }, 40 | [4] = { 41 | cost = 5000, 42 | inventory = { 43 | slots = 10, 44 | maxWeight = 150000, 45 | label = 'Storage Unit 4', 46 | }, 47 | coords = vector4(-70.56, -1231.53, 29.28, 51.0), 48 | }, 49 | [5] = { 50 | cost = 5000, 51 | inventory = { 52 | slots = 10, 53 | maxWeight = 150000, 54 | label = 'Storage Unit 5', 55 | }, 56 | coords = vector4(-70.84, -1243.02, 29.45, 180.0), 57 | }, 58 | [6] = { 59 | cost = 5000, 60 | inventory = { 61 | slots = 10, 62 | maxWeight = 150000, 63 | label = 'Storage Unit 6', 64 | }, 65 | coords = vector4(-54.25, -1212.38, 29.05, -43.0), 66 | }, 67 | [7] = { 68 | cost = 5000, 69 | inventory = { 70 | slots = 10, 71 | maxWeight = 150000, 72 | label = 'Storage Unit 7', 73 | }, 74 | coords = vector4(-59.57, -1207.06, 28.79, -43.0), 75 | }, 76 | } 77 | } -------------------------------------------------------------------------------- /client/open.lua: -------------------------------------------------------------------------------- 1 | -- This file is used to handle the different UI systems for kevin Scripts 2 | local config = require 'shared.config' 3 | 4 | function getPlayerCitizenId() 5 | if GetResourceState('qbx_core') == 'started' then 6 | return exports.qbx_core:GetPlayerData().citizenid 7 | elseif GetResourceState('qb-core') == 'started' then 8 | return exports['qb-core']:GetCoreObject().Functions.GetPlayerData().citizenid 9 | end 10 | 11 | return nil 12 | end 13 | 14 | function showTextUi(text) -- feel free to implement your own text ui system here 15 | if config.textUi == 'ox' then 16 | lib.showTextUI(text, { position = 'left-center' }) 17 | elseif config.textUi == 'jg' then 18 | exports['jg-textui']:DrawText(text) 19 | elseif config.textUi == 'qb' then 20 | exports['qb-core']:DrawText(text) 21 | end 22 | end 23 | 24 | function hideTextUi() 25 | if config.textUi == 'ox' then 26 | lib.hideTextUI() 27 | elseif config.textUi == 'jg' then 28 | exports['jg-textui']:HideText() 29 | elseif config.textUi == 'qb' then 30 | exports['qb-core']:HideText() 31 | end 32 | end 33 | 34 | function showNotify(text, type) -- feel free to implement your own notify system here 35 | if config.notify == 'ox' then 36 | lib.notify({ description = text, type = type }) 37 | elseif config.notify == 'qb' then 38 | exports['qb-core']:GetCoreObject().Functions.Notify(text, type) 39 | end 40 | end 41 | 42 | local function addTargetOptions(options) 43 | local targetOptions = {} 44 | if config.target.resource == 'ox' then 45 | for i = 1, #options do 46 | targetOptions[i] = { 47 | label = options[i].label, 48 | onSelect = function (data) 49 | options[i].onSelect(data) 50 | end, 51 | canInteract = options[i].canInteract, 52 | distance = config.target.distance, 53 | } 54 | end 55 | elseif config.target.resource == 'qb' then 56 | for i = 1, #options do 57 | targetOptions[i] = { 58 | icon = options[i].icon, 59 | label = options[i].label, 60 | action = function(entity) 61 | options[i].onSelect(entity) 62 | end, 63 | canInteract = options[i].canInteract, 64 | } 65 | end 66 | elseif config.target.resource == 'interact' then 67 | for i = 1, #options do 68 | targetOptions[i] = { 69 | label = options[i].label, 70 | action = function(entity, coords, args) 71 | options[i].onSelect() 72 | end, 73 | canInteract = options[i].canInteract, 74 | } 75 | end 76 | end 77 | return targetOptions 78 | end 79 | 80 | function addTargetToEntity(options) -- feel free to implement your own target system here 81 | -- options = options.entity, options.label, optins.distance, options.icon, options.onSelect, options.canInteract (if you want to add your own target/interaction) 82 | if config.target.resource == 'ox' then 83 | exports.ox_target:addLocalEntity(options.entity, addTargetOptions(options.options)) 84 | elseif config.target.resource == 'qb' then 85 | exports['qb-target']:AddTargetEntity(options.entity, { 86 | options = addTargetOptions(options.options), 87 | distance = config.target.distance, 88 | }) 89 | elseif config.target.resource == 'interact' then 90 | exports.interact:AddLocalEntityInteraction({ 91 | entity = options.entity, 92 | interactDst = config.target.distance, 93 | offset = vec3(0.0, 0.0, 1.0), 94 | ignoreLos = true, -- optional ignores line of sight 95 | options = addTargetOptions(options.options), 96 | }) 97 | end 98 | end 99 | 100 | function progressBar(data) -- feel free to implement your own progress bar system here 101 | if config.progressBar == 'ox_circle' then 102 | if lib.progressCircle({ 103 | label = data.label, duration = data.duration, position = data.position, useWhileDead = false, 104 | canCancel = true, 105 | disable = { move = true,combat = true,sprint = true,car = true,}, 106 | anim = { dict = data.anim.dict, clip = data.anim.clip, data.anim.scenario, data.flag}, 107 | }) then 108 | data.onSuccess() 109 | else 110 | data.onCancel() 111 | end 112 | elseif config.progressBar == 'ox_bar' then 113 | if lib.progressBar({ 114 | label = data.label, duration = data.duration, position = data.position, useWhileDead = false, 115 | canCancel = true, 116 | disable = { move = true,combat = true,sprint = true,car = true,}, 117 | anim = { dict = data.anim.dict, clip = data.anim.clip, data.anim.scenario, data.flag}, 118 | }) then 119 | data.onSuccess() 120 | else 121 | data.onCancel() 122 | end 123 | end 124 | end 125 | 126 | function showMinigame(data) -- feel free to implement your own minigame system here 127 | if config.minigame == 'ox' then 128 | local success = lib.skillCheck({'easy', 'easy', {areaSize = 60, speedMultiplier = 1}, 'hard'}, {'1', '2', '3', '4'}) 129 | if success then 130 | data.onSuccess() 131 | else 132 | data.onFail() 133 | end 134 | end 135 | end -------------------------------------------------------------------------------- /server/server.lua: -------------------------------------------------------------------------------- 1 | local config = require 'shared.config' 2 | local serverUnits = {} 3 | 4 | local function checkSqlSet() 5 | local success = pcall(MySQL.scalar.await, 'SELECT 1 FROM storage_units') 6 | 7 | if not success then 8 | MySQL.query([[CREATE TABLE `storage_units` ( 9 | `id` int(11) NOT NULL AUTO_INCREMENT, 10 | `owner` VARCHAR(255) NOT NULL, 11 | `owned` TINYINT(1) NOT NULL DEFAULT 0, 12 | `password` VARCHAR(255) DEFAULT NULL, 13 | PRIMARY KEY (`id`) 14 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;]]) 15 | end 16 | 17 | return true 18 | end 19 | 20 | local function setUpStorageUnits() 21 | local storageUnits = MySQL.query.await('SELECT * FROM storage_units') 22 | for id, unit in pairs(config.storageUnits) do 23 | serverUnits[id] = { 24 | id = id, 25 | owner = nil, 26 | owned = false, 27 | password = nil, 28 | inventory = unit.inventory, 29 | cost = unit.cost, 30 | coords = unit.coords, 31 | } 32 | 33 | for _, storageUnit in pairs(storageUnits) do 34 | if storageUnit.id == id then 35 | serverUnits[id].owner = storageUnit.owner 36 | serverUnits[id].owned = storageUnit.owned 37 | serverUnits[id].password = storageUnit.password 38 | break 39 | end 40 | end 41 | 42 | exports.ox_inventory:RegisterStash('storageUnit:'..id, unit.inventory.label, unit.inventory.slots, unit.inventory.maxWeight, nil, nil, vector3(unit.coords)) 43 | end 44 | end 45 | 46 | local function isPlayerOwnedUnitsMet(source) 47 | local citizenId = getPlayerCitizenId(source) 48 | local ownedUnits = 0 49 | for unitId, unit in pairs(serverUnits) do 50 | if unit.owner == citizenId then 51 | ownedUnits = ownedUnits + 1 52 | end 53 | end 54 | 55 | if ownedUnits == config.unitsAllowed then 56 | return true 57 | end 58 | 59 | return false 60 | end 61 | 62 | local function validatePurchaseRequest(source, unitId) 63 | if not config.storageUnits[unitId] then 64 | return false, 'Invalid Storage Unit', 'error' 65 | end 66 | 67 | if isPlayerOwnedUnitsMet(source) then 68 | return false, 'Max Units Owned', 'error' 69 | end 70 | 71 | local playerCash = getPlayerCash(source) 72 | if playerCash < config.storageUnits[unitId].cost then 73 | return false, 'Insufficient Funds', 'error' 74 | end 75 | 76 | return true 77 | end 78 | 79 | local function requestUnitPurchase(source, unitId, password) 80 | local unit = serverUnits[unitId] 81 | local citizenId = getPlayerCitizenId(source) 82 | if not unit then 83 | return 'Invalid Storage Unit', 'error' 84 | end 85 | 86 | if unit.owned then 87 | return 'Storage Unit Already Owned', 'error' 88 | end 89 | 90 | local success, response = exports.ox_inventory:RemoveItem(source, 'money', config.storageUnits[unitId].cost) 91 | if not success then 92 | return 'Insufficient Funds', 'error' 93 | end 94 | 95 | MySQL.query('INSERT INTO storage_units (id, owner, owned, password) VALUES (@id, @owner, @owned, @password)', { 96 | ['@id'] = unitId, 97 | ['@owner'] = citizenId, 98 | ['@owned'] = true, 99 | ['@password'] = password, 100 | }) 101 | serverUnits[unitId].owned = true 102 | serverUnits[unitId].owner = citizenId 103 | serverUnits[unitId].password = password 104 | TriggerClientEvent('kevin-storageunits:client:updateStorageUnit', -1, serverUnits[unitId]) 105 | return 'Storage Unit Purchased', 'success' 106 | end 107 | 108 | local function validataUnitPassword(source, unitId, password) 109 | local unit = serverUnits[unitId] 110 | if not unit then return end 111 | 112 | if unit.password == password then 113 | return true 114 | end 115 | 116 | return false 117 | end 118 | 119 | lib.callback.register('kevin-storageunits:server:getStorageUnits', function (source) 120 | return serverUnits 121 | end) 122 | 123 | lib.callback.register('kevin-storageunits:server:validatePurchaseRequest', function (source, unitId) 124 | return validatePurchaseRequest(source, unitId) 125 | end) 126 | 127 | lib.callback.register('kevin-storageunits:server:purchaseStorageUnit', function (source, unitId, password) 128 | return requestUnitPurchase(source, unitId, password) 129 | end) 130 | 131 | lib.callback.register('kevin-storageunits:server:validateStorageUnitPassword', function (source, unitId, password) 132 | return validataUnitPassword(source, unitId, password) 133 | end) 134 | 135 | RegisterNetEvent('kevin-storageunits:server:updateStorageUnit', function (data) 136 | if not serverUnits[data.id] then return end 137 | local citizenId = getPlayerCitizenId(source) 138 | if serverUnits[data.id].owner ~= citizenId then 139 | return 140 | end 141 | 142 | serverUnits[data.id] = data.unit 143 | 144 | serverUnits[data.id].password = data.password 145 | MySQL.query('UPDATE storage_units SET password = @password WHERE id = @id', { 146 | ['@id'] = data.id, 147 | ['@password'] = data.password, 148 | }) 149 | TriggerClientEvent('kevin-storageunits:client:updateStorageUnit', -1, serverUnits[data.id]) 150 | end) 151 | 152 | CreateThread(function () 153 | local sqlValid = checkSqlSet() 154 | if sqlValid then 155 | setUpStorageUnits() 156 | end 157 | end) -------------------------------------------------------------------------------- /client/client.lua: -------------------------------------------------------------------------------- 1 | local zones = {} 2 | local clientUnits = {} 3 | 4 | 5 | local function requestUnitPurchase(unit) 6 | local validated, response, type = lib.callback.await('kevin-storageunits:server:validatePurchaseRequest', false, unit.id) 7 | if not validated then 8 | showNotify(response, type) 9 | return 10 | end 11 | 12 | local input = lib.inputDialog('Storage Unit: '..unit.id, { 13 | {type = 'input', label = 'Password', description = 'Storage Unit Password', password = true, required = true, min = 4, max = 16}, 14 | }) 15 | if not input then return end 16 | 17 | local password = input[1] 18 | 19 | response, type = lib.callback.await('kevin-storageunits:server:purchaseStorageUnit',false, unit.id, password) 20 | showNotify(response, type) 21 | end 22 | 23 | local function openUnit(unit) 24 | if unit.password == nil then 25 | exports.ox_inventory:openInventory('stash', 'storageUnit:'..unit.id) 26 | return 27 | end 28 | 29 | local input = lib.inputDialog('Storage Unit: '..unit.id, { 30 | {type = 'input', label = 'Password', description = 'Storage Unit Password', password = true, required = true, min = 4, max = 16}, 31 | }) 32 | if not input then return end 33 | 34 | local password = input[1] 35 | 36 | local success = lib.callback.await('kevin-storageunits:server:validateStorageUnitPassword', false, unit.id, password) 37 | if not success then 38 | showNotify('Invalid Password', 'error') 39 | return 40 | end 41 | 42 | exports.ox_inventory:openInventory('stash', 'storageUnit:'..unit.id) 43 | end 44 | 45 | local function cutUnitKeyPadPower(unit) 46 | if not unit.owned then 47 | showNotify('Storage Unit Not Owned', 'error') 48 | return 49 | end 50 | 51 | showMinigame({ 52 | onSuccess = function () 53 | showNotify('Storage Unit Unlocked', 'success') 54 | unit.password = nil 55 | 56 | local data = { id = unit.id, unit = unit, password = nil,} 57 | TriggerServerEvent('kevin-storageunits:server:updateStorageUnit', data) 58 | Wait(1000) 59 | exports.ox_inventory:openInventory('stash', 'storageUnit:'..unit.id) 60 | end, 61 | onFail = function () 62 | showNotify('Failed To Unlock Storage Unit', 'error') 63 | end 64 | }) 65 | end 66 | 67 | local function resetUnitPassword(unit) 68 | local input = lib.inputDialog('Storage Unit: '..unit.id, { 69 | {type = 'input', label = 'Password', description = 'Storage Unit Password', password = true, required = true, min = 4, max = 16}, 70 | }) 71 | 72 | if not input then return end 73 | 74 | local password = input[1] 75 | local data = { id = unit.id, unit = unit, password = password,} 76 | TriggerServerEvent('kevin-storageunits:server:updateStorageUnit', data) 77 | end 78 | 79 | local function createUnitKeyPad(unit) 80 | local model = `h4_prop_h4_ld_keypad_01b` 81 | lib.requestModel(model) 82 | local keypad = CreateObject(model, clientUnits[unit.id].coords.x, clientUnits[unit.id].coords.y, clientUnits[unit.id].coords.z, false, false, false) 83 | SetEntityHeading(keypad, clientUnits[unit.id].coords.w) 84 | SetEntityAsMissionEntity(keypad, true, true) 85 | FreezeEntityPosition(keypad, true) 86 | SetModelAsNoLongerNeeded(model) 87 | clientUnits[unit.id].keypad = keypad 88 | addTargetToEntity({ 89 | entity = clientUnits[unit.id].keypad, 90 | options = { 91 | { 92 | label = 'Open Storage Unit', 93 | onSelect = function () 94 | openUnit(unit) 95 | end, 96 | canInteract = function () 97 | return clientUnits[unit.id].owned 98 | end 99 | }, 100 | { 101 | label = 'Buy Storage Unit $'..clientUnits[unit.id].cost, 102 | onSelect = function () 103 | requestUnitPurchase(unit) 104 | end, 105 | canInteract = function () 106 | return not clientUnits[unit.id].owned 107 | end 108 | }, 109 | { 110 | label = 'Cut Lock', 111 | onSelect = function () 112 | cutUnitKeyPadPower(unit) 113 | end, 114 | canInteract = function () 115 | return clientUnits[unit.id].owned and clientUnits[unit.id].password and clientUnits[unit.id].owner ~= getPlayerCitizenId() 116 | end 117 | }, 118 | { 119 | label = 'Reset Lock', 120 | onSelect = function () 121 | resetUnitPassword(unit) 122 | end, 123 | canInteract = function () 124 | return clientUnits[unit.id].password == nil and clientUnits[unit.id].owned and clientUnits[unit.id].owner == getPlayerCitizenId() 125 | end 126 | } 127 | } 128 | }) 129 | end 130 | 131 | local function setupStorageUnit(id, unit) 132 | zones[id]= lib.zones.sphere({ 133 | coords = unit.coords, 134 | radius = 25.0, 135 | debug = false, 136 | onEnter = function () 137 | createUnitKeyPad(unit) 138 | end, 139 | onExit = function () 140 | if clientUnits[id].keypad then 141 | DeleteEntity(clientUnits[id].keypad) 142 | end 143 | end, 144 | }) 145 | end 146 | 147 | local function getServerStorageUnits() 148 | local units = lib.callback.await('kevin-storageunits:server:getStorageUnits', false) 149 | clientUnits = units 150 | 151 | for id, unit in pairs(clientUnits) do 152 | setupStorageUnit(id, unit) 153 | end 154 | end 155 | 156 | RegisterNetEvent('kevin-storageunits:client:updateStorageUnit', function (unit) 157 | clientUnits[unit.id] = unit 158 | setupStorageUnit(unit.id, unit) 159 | end) 160 | 161 | RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() 162 | Wait(500) 163 | getServerStorageUnits() 164 | end) 165 | 166 | AddEventHandler('onResourceStart', function(resource) 167 | if resource == GetCurrentResourceName() then 168 | Wait(500) 169 | getServerStorageUnits() 170 | end 171 | end) 172 | 173 | AddEventHandler('onResourceStop', function (resource) 174 | if resource == GetCurrentResourceName() then 175 | for _, unit in pairs(clientUnits) do 176 | if unit.keypad then 177 | DeleteEntity(unit.keypad) 178 | end 179 | end 180 | end 181 | end) --------------------------------------------------------------------------------