From 82e7fc6981bc4e485086e58c00c9855ecd4932f9 Mon Sep 17 00:00:00 2001 From: bitpredator <67551273+bitpredator@users.noreply.github.com> Date: Sun, 31 Mar 2024 19:27:07 +0200 Subject: [PATCH] Add files via upload --- .../bpt_doorlock/server/convert.lua | 130 +++++++ .../server/framework/es_extended.lua | 67 ++++ .../bpt_doorlock/server/framework/qb-core.lua | 99 +++++ .../[bpt_addons]/bpt_doorlock/server/main.lua | 350 ++++++++++++++++++ .../bpt_doorlock/server/utils.lua | 27 ++ 5 files changed, 673 insertions(+) create mode 100644 server-data/resources/[bpt_addons]/bpt_doorlock/server/convert.lua create mode 100644 server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/es_extended.lua create mode 100644 server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/qb-core.lua create mode 100644 server-data/resources/[bpt_addons]/bpt_doorlock/server/main.lua create mode 100644 server-data/resources/[bpt_addons]/bpt_doorlock/server/utils.lua diff --git a/server-data/resources/[bpt_addons]/bpt_doorlock/server/convert.lua b/server-data/resources/[bpt_addons]/bpt_doorlock/server/convert.lua new file mode 100644 index 000000000..9314e0fdb --- /dev/null +++ b/server-data/resources/[bpt_addons]/bpt_doorlock/server/convert.lua @@ -0,0 +1,130 @@ +---@type table? +Config.DoorList = {} + +local function flattenTableToArray(tbl) + if type(tbl) == 'table' then + if table.type(tbl) == 'array' then return tbl end + + local array = {} + + for k in pairs(tbl) do + array[#array + 1] = k + end + + return array + end +end + +MySQL.ready(function() + local files, fileCount = require 'server.utils'.getFilesInDirectory('convert', '%.lua') + + if fileCount > 0 then + print(('^3Found %d nui_doorlock config files.^0'):format(fileCount)) + end + + local query = 'INSERT INTO `bpt_doorlock` (`name`, `data`) SELECT ?, ? WHERE NOT EXISTS (SELECT 1 FROM `bpt_doorlock` WHERE `name` = ?)' + local queries = {} + + for i = 1, fileCount do + local fileName = files[i] + local file = LoadResourceFile('bpt_doorlock', ('convert/%s.lua'):format(fileName)) + + if file then + load(file)() + + if next(Config.DoorList) then + local size = 0 + + for k, door in pairs(Config.DoorList) do + size += 1 + local double = door.doors + local qb = door.objName or (double and double[1].objName) + + if qb then + if double then + for j = 1, 2 do + double[j].objHash = double[j].objName + double[j].objHeading = double[j].objYaw or 0 + end + else + door.objHash = door.objName + door.objHeading = door.objYaw or 0 + end + + local groups = door.authorizedJobs or {} + + if door.authorizedGangs then + for gang, grade in pairs(door.authorizedGangs) do + groups[gang] = grade + end + end + + door.authorizedJobs = next(groups) and groups + door.lockpick = door.pickable + door.showNUI = not door.hideLabel + door.characters = flattenTableToArray(door.authorizedCitizenIDs) + end + + local data = { + auto = door.slides or door.garage or door.sliding or door.doublesliding, + autolock = (door.autolock and door.autolock / 1000) or (door.autoLock and door.autoLock / 1000), + coords = door.objCoords, + heading = door.objHeading and math.floor(door.objHeading + 0.5), + model = door.objHash, + characters = door.characters, + groups = door.authorizedJobs, + items = door.items, + lockpick = door.lockpick, + hideUi = door.showNUI ~= nil and not door.showNUI or false, + lockSound = door.audioLock?.file and door.audioLock.file:gsub('%.ogg', ''), + unlockSound = door.audioUnlock?.file and door.audioUnlock.file:gsub('%.ogg', ''), + maxDistance = door.maxDistance or door.distance, + doorRate = door.doorRate and door.doorRate + 0.0 or nil, + state = door.locked and 1 or 0, + passcode = door.passcode, + doors = double and { + { + coords = double[1].objCoords, + heading = math.floor(double[1].objHeading + 0.5), + model = double[1].objHash, + }, + { + coords = double[2].objCoords, + heading = math.floor(double[2].objHeading + 0.5), + model = double[2].objHash, + }, + }, + } + + if data.auto and not data.lockSound then + if door.audioRemote then + data.lockSound = 'button-remote' + end + end + + if double and not data.coords then + double = data.doors + data.coords = double[1].coords - ((double[1].coords - double[2].coords) / 2) + end + + local name = ('%s %s'):format(fileName, k) + + queries[size] = { + query = query, values = { name, json.encode(data), name } + } + end + + print(('^3Loaded %d doors from convert/%s.lua.^0'):format(size, fileName)) + + if MySQL.transaction.await(queries) then + SaveResourceFile('bpt_doorlock', ('convert/%s.lua'):format(fileName), '-- This file has already been converted for bpt_doorlock and should be removed.\r\ndo return end\r\n\r\n' .. file, -1) + end + + table.wipe(Config.DoorList) + table.wipe(queries) + end + end + end + + Config.DoorList = nil +end) \ No newline at end of file diff --git a/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/es_extended.lua b/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/es_extended.lua new file mode 100644 index 000000000..a5107a51d --- /dev/null +++ b/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/es_extended.lua @@ -0,0 +1,67 @@ +local resourceName = 'es_extended' + +if not GetResourceState(resourceName):find('start') then return end + +SetTimeout(0, function() + local ESX = exports[resourceName]:getSharedObject() + + GetPlayer = ESX.GetPlayerFromId + + if not ESX.GetConfig().OxInventory then + function RemoveItem(playerId, item) + local player = GetPlayer(playerId) + + if player then player.removeInventoryItem(item, 1) end + end + + ---@param player table + ---@param items string[] | { name: string, remove?: boolean, metadata?: string }[] + ---@param removeItem? boolean + ---@return string? + function DoesPlayerHaveItem(player, items, removeItem) + for i = 1, #items do + local item = items[i] + local itemName = item.name or item + local data = player.getInventoryItem(itemName) + + if data?.count > 0 then + if removeItem or item.remove then + player.removeInventoryItem(itemName, 1) + end + + return itemName + end + end + end + end +end) + +function GetCharacterId(player) + return player.identifier +end + +function IsPlayerInGroup(player, filter) + local type = type(filter) + + if type == 'string' then + if player.job.name == filter then + return player.job.name, player.job.grade + end + else + local tabletype = table.type(filter) + + if tabletype == 'hash' then + local grade = filter[player.job.name] + + if grade and grade <= player.job.grade then + return player.job.name, player.job.grade + end + elseif tabletype == 'array' then + for i = 1, #filter do + if player.job.name == filter[i] then + return player.job.name, player.job.grade + end + end + end + end +end \ No newline at end of file diff --git a/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/qb-core.lua b/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/qb-core.lua new file mode 100644 index 000000000..dbe1fd9cf --- /dev/null +++ b/server-data/resources/[bpt_addons]/bpt_doorlock/server/framework/qb-core.lua @@ -0,0 +1,99 @@ +local resourceName = 'qb-core' + +if not GetResourceState(resourceName):find('start') then return end + +SetTimeout(0, function() + local QB = exports[resourceName]:GetCoreObject() + + GetPlayer = QB.Functions.GetPlayer + + if GetResourceState('ox_inventory') == 'missing' then + function RemoveItem(playerId, item, slot) + local player = GetPlayer(playerId) + + if player then player.Functions.RemoveItem(item, 1, slot) end + end + + ---@param player table + ---@param items string[] | { name: string, remove?: boolean, metadata?: string }[] + ---@param removeItem? boolean + ---@return string? + function DoesPlayerHaveItem(player, items, removeItem) + for i = 1, #items do + local item = items[i] + local itemName = item.name or item + + if item.metadata then + local playerItems = player.Functions.GetItemsByName(itemName) + + for j = 1, #playerItems do + local data = playerItems[j] + + if data.info.type == item.metadata then + if removeItem or item.remove then + player.Functions.RemoveItem(itemName, 1, data.slot) + end + + return itemName + end + end + else + local data = player.Functions.GetItemByName(itemName) + + if data then + if item.remove then + player.Functions.RemoveItem(itemName, 1, data.slot) + end + + return itemName + end + end + end + end + end +end) + +function GetCharacterId(player) + return player.PlayerData.citizenid +end + +local groups = { 'job', 'gang' } + +function IsPlayerInGroup(player, filter) + local type = type(filter) + + if type == 'string' then + for i = 1, #groups do + local data = player.PlayerData[groups[i]] + + if data.name == filter then + return data.name, data.grade.level + end + end + else + local tabletype = table.type(filter) + + if tabletype == 'hash' then + for i = 1, #groups do + local data = player.PlayerData[groups[i]] + local grade = filter[data.name] + + if grade and grade <= data.grade.level then + return data.name, data.grade.level + end + end + elseif tabletype == 'array' then + for i = 1, #filter do + local group = filter[i] + + for j = 1, #groups do + local data = player.PlayerData[groups[j]] + + if data.name == group then + return data.name, data.grade.level + end + end + end + end + end +end \ No newline at end of file diff --git a/server-data/resources/[bpt_addons]/bpt_doorlock/server/main.lua b/server-data/resources/[bpt_addons]/bpt_doorlock/server/main.lua new file mode 100644 index 000000000..a1608999e --- /dev/null +++ b/server-data/resources/[bpt_addons]/bpt_doorlock/server/main.lua @@ -0,0 +1,350 @@ +if not LoadResourceFile(lib.name, 'web/build/index.html') then + error('Unable to load UI. Build bpt_doorlock or download the latest release.\n ^3https://github.com/bitpredator/bpt_doorlock/releases/latest/download/bpt_doorlock.zip^0') +end + +do + local success, msg = lib.checkDependency('oxmysql', '2.4.0') + if not success then error(msg) end + + success, msg = lib.checkDependency('ox_lib', '3.0.0') + if not success then error(msg) end +end + +lib.versionCheck('bitpredator/bpt_doorlock') +lib.locale() + +local doors = {} + +local function encodeData(door) + local double = door.doors + + return json.encode({ + auto = door.auto, + autolock = door.autolock, + coords = door.coords, + doors = double and { + { + coords = double[1].coords, + heading = double[1].heading, + model = double[1].model, + }, + { + coords = double[2].coords, + heading = double[2].heading, + model = double[2].model, + }, + }, + characters = door.characters, + groups = door.groups, + heading = door.heading, + items = door.items, + lockpick = door.lockpick, + hideUi = door.hideUi, + holdOpen = door.holdOpen, + lockSound = door.lockSound, + maxDistance = door.maxDistance, + doorRate = door.doorRate, + model = door.model, + state = door.state, + unlockSound = door.unlockSound, + passcode = door.passcode, + lockpickDifficulty = door.lockpickDifficulty + }) +end + +local function getDoor(door) + door = type(door) == 'table' and door or doors[door] + + return { + id = door.id, + name = door.name, + state = door.state, + coords = door.coords, + characters = door.characters, + groups = door.groups, + items = door.items, + maxDistance = door.maxDistance, + } +end + +exports('getDoor', getDoor) + +exports('getDoorFromName', function(name) + for _, door in pairs(doors) do + if door.name == name then + return getDoor(door) + end + end +end) + +exports('editDoor', function(id, data) + local door = doors[id] + + if door then + for k, v in pairs(data) do + if k ~= 'id' then + local current = door[k] + local t1 = type(current) + local t2 = type(v) + + if t1 ~= 'nil' and v ~= '' and t1 ~= t2 then + error(("Expected '%s' for door.%s, received %s (%s)"):format(t1, k, t2, v)) + end + + door[k] = v ~= '' and v or nil + end + end + + MySQL.update('UPDATE bpt_doorlock SET name = ?, data = ? WHERE id = ?', { door.name, encodeData(door), id }) + TriggerClientEvent('bpt_doorlock:editDoorlock', -1, id, door) + end +end) + +local soundDirectory = Config.NativeAudio and 'audio/dlc_bptdoorlock/bptdoorlock' or 'web/build/sounds' +local fileFormat = Config.NativeAudio and '%.wav' or '%.ogg' +local sounds = require 'server.utils'.getFilesInDirectory(soundDirectory, fileFormat) + +lib.callback.register('bpt_doorlock:getSounds', function() + return sounds +end) + +local function createDoor(id, door, name) + local double = door.doors + door.id = id + door.name = name + + if double then + for i = 1, 2 do + double[i].hash = joaat(('bpt_door_%s_%s'):format(id, i)) + + local coords = double[i].coords + double[i].coords = vector3(coords.x, coords.y, coords.z) + end + + if not door.coords then + door.coords = double[1].coords - ((double[1].coords - double[2].coords) / 2) + end + else + door.hash = joaat(('bpt_door_%s'):format(id)) + end + + door.coords = vector3(door.coords.x, door.coords.y, door.coords.z) + + if not door.state then + door.state = 1 + end + + if type(door.items?[1]) == 'string' then + local items = {} + + for i = 1, #door.items do + items[i] = { + name = door.items[i], + remove = false, + } + end + + door.items = items + MySQL.update('UPDATE bpt_doorlock SET data = ? WHERE id = ?', { encodeData(door), id }) + end + + doors[id] = door + return door +end + +local isLoaded = false +local table = lib.table +local ox_inventory = exports.ox_inventory + +SetTimeout(1000, function() + if not GetPlayer then + -- because some people want to use this on their vmenu servers or some shit lmao + -- only supports passcodes + warn('no compatible framework was loaded, most features will not work') + function GetPlayer(_) end + end +end) + +function RemoveItem(playerId, item, slot) + local player = GetPlayer(playerId) + + if player then ox_inventory:RemoveItem(playerId, item, 1, nil, slot) end +end + +---@param player table +---@param items string[] | { name: string, remove?: boolean, metadata?: string }[] +---@param removeItem? boolean +---@return string? +function DoesPlayerHaveItem(player, items, removeItem) + local playerId = player.source or player.PlayerData.source + + for i = 1, #items do + local item = items[i] + local itemName = item.name or item + local data = ox_inventory:Search(playerId, 'slots', itemName, item.metadata)[1] + + if data and data.count > 0 then + if removeItem or item.remove then + ox_inventory:RemoveItem(playerId, itemName, 1, nil, data.slot) + end + + return itemName + end + end +end + +local function isAuthorised(playerId, door, lockpick) + if Config.PlayerAceAuthorised and IsPlayerAceAllowed(playerId, 'command.doorlock') then + return true + end + + -- e.g. add_ace group.police "doorlock.mrpd locker rooms" allow + -- add_principal fivem:123456 group.police + -- or add_ace identifier.fivem:123456 "doorlock.mrpd locker rooms" allow + if IsPlayerAceAllowed(playerId, ('doorlock.%s'):format(door.name)) then + return true + end + + local player = GetPlayer(playerId) + local authorised = door.passcode or false --[[@as boolean | string | nil]] + + if player then + if lockpick then + return DoesPlayerHaveItem(player, Config.LockpickItems) + end + + if door.characters and table.contains(door.characters, GetCharacterId(player)) then + return true + end + + if door.groups then + authorised = IsPlayerInGroup(player, door.groups) or nil + end + + if not authorised and door.items then + authorised = DoesPlayerHaveItem(player, door.items) or nil + end + + if authorised ~= nil and door.passcode and not door.characters and not door.groups then + authorised = door.passcode == lib.callback.await('bpt_doorlock:inputPassCode', playerId) + end + end + + return authorised +end + +local sql = LoadResourceFile(cache.resource, 'sql/bpt_doorlock.sql') + +if sql then MySQL.query(sql) end + +MySQL.ready(function() + while Config.DoorList do Wait(100) end + + local response = MySQL.query.await('SELECT `id`, `name`, `data` FROM `bpt_doorlock`') + + for i = 1, #response do + local door = response[i] + createDoor(door.id, json.decode(door.data), door.name) + end + + isLoaded = true +end) + +---@param id number +---@param state 0 | 1 | boolean +---@param lockpick? boolean +---@return boolean +local function setDoorState(id, state, lockpick) + local door = doors[id] + + state = (state == 1 or state == 0) and state or (state and 1 or 0) + + if door then + local authorised = not source or source == '' or isAuthorised(source, door, lockpick) + + if authorised then + door.state = state + TriggerClientEvent('bpt_doorlock:setState', -1, id, state, source) + + if door.autolock and state == 0 then + SetTimeout(door.autolock * 1000, function() + if door.state ~= 1 then + door.state = 1 + + TriggerClientEvent('bpt_doorlock:setState', -1, id, door.state) + TriggerEvent('bpt_doorlock:stateChanged', nil, door.id, door.state == 1) + end + end) + end + + TriggerEvent('bpt_doorlock:stateChanged', source, door.id, state == 1, type(authorised) == 'string' and authorised) + + return true + end + + if source then + lib.notify(source, { type = 'error', icon = 'lock', description = state == 0 and 'cannot_unlock' or 'cannot_lock' }) + end + end + + return false +end + +RegisterNetEvent('bpt_doorlock:setState', setDoorState) +exports('setDoorState', setDoorState) + +lib.callback.register('bpt_doorlock:getDoors', function() + while not isLoaded do Wait(100) end + + return doors, sounds +end) + +RegisterNetEvent('bpt_doorlock:editDoorlock', function(id, data) + if IsPlayerAceAllowed(source, 'command.doorlock') then + if data then + if not data.coords then + local double = data.doors + data.coords = double[1].coords - ((double[1].coords - double[2].coords) / 2) + end + + if not data.name then + data.name = tostring(data.coords) + end + end + + if id then + if data then + MySQL.update('UPDATE bpt_doorlock SET name = ?, data = ? WHERE id = ?', { data.name, encodeData(data), id }) + else + MySQL.update('DELETE FROM bpt_doorlock WHERE id = ?', { id }) + end + + doors[id] = data + TriggerClientEvent('bpt_doorlock:editDoorlock', -1, id, data) + else + local insertId = MySQL.insert.await('INSERT INTO bpt_doorlock (name, data) VALUES (?, ?)', { data.name, encodeData(data) }) + local door = createDoor(insertId, data, data.name) + + TriggerClientEvent('bpt_doorlock:setState', -1, door.id, door.state, false, door) + end + end +end) + +RegisterNetEvent('bpt_doorlock:breakLockpick', function() + local player = GetPlayer(source) + return player and DoesPlayerHaveItem(player, Config.LockpickItems, true) +end) + +lib.addCommand('doorlock', { + help = locale('create_modify_lock'), + params = { + { + name = 'closest', + help = locale('command_closest'), + optional = true, + }, + }, + restricted = Config.CommandPrincipal +}, function(source, args) + TriggerClientEvent('bpt_doorlock:triggeredCommand', source, args.closest) +end) \ No newline at end of file diff --git a/server-data/resources/[bpt_addons]/bpt_doorlock/server/utils.lua b/server-data/resources/[bpt_addons]/bpt_doorlock/server/utils.lua new file mode 100644 index 000000000..32fd26fa7 --- /dev/null +++ b/server-data/resources/[bpt_addons]/bpt_doorlock/server/utils.lua @@ -0,0 +1,27 @@ +local resourcePath = GetResourcePath(cache.resource):gsub('//', '/') .. '/' + +local utils = {} + +function utils.getFilesInDirectory(path, pattern) + local files = {} + local fileCount = 0 + local system = os.getenv('OS') + local command = system and system:match('Windows') and 'dir "' or 'ls "' + local suffix = command == 'dir "' and '/" /b' or '/"' + local dir = io.popen(command .. resourcePath .. path .. suffix) + + if dir then + for line in dir:lines() do + if line:match(pattern) then + fileCount += 1 + files[fileCount] = line:gsub(pattern, '') + end + end + + dir:close() + end + + return files, fileCount +end + +return utils \ No newline at end of file