diff --git a/[core]/esx_lib/fxmanifest.lua b/[core]/esx_lib/fxmanifest.lua index 91010f8b..d0a98222 100644 --- a/[core]/esx_lib/fxmanifest.lua +++ b/[core]/esx_lib/fxmanifest.lua @@ -15,6 +15,7 @@ files { } shared_scripts { + 'resource/init.lua', 'resource/**/shared.lua', } diff --git a/[core]/esx_lib/imports.lua b/[core]/esx_lib/imports.lua index 4e5851aa..9c0ba362 100644 --- a/[core]/esx_lib/imports.lua +++ b/[core]/esx_lib/imports.lua @@ -9,21 +9,22 @@ -- Const Variables local CONTEXT = IsDuplicityVersion() and 'server' or 'client' -local LIB_NAME = 'esx_lib' +local LIB_NAME = 'esx_lib' +local IS_DEBUG = GetConvar('xLib:debug', 'false') == 'true' --- xLib class for Language Server + +---@alias Module table | function ---@class xLib ---@field name string ---@field side 'server' | 'client' ----@alias Module table | function - ------------------------------------------------- --- Core functions for modules ------------------------------------------------- --- Function that returns nothing -local function noop() end +--- Used as a "shortcut" +function Noop() end ---Loads a module into memory ---@param self xLib @@ -31,20 +32,19 @@ local function noop() end ---@return Module | nil local function loadModule(self, name) local directory = ('imports/%s'):format(name) - local chunk = LoadResourceFile(LIB_NAME, ('%s/%s.lua'):format(directory, CONTEXT)) -- returns a raw lua code as a string (SERVER/CLIENT side) - local shared_chunk = LoadResourceFile(LIB_NAME, ('%s/shared.lua'):format(directory)) -- returns a raw lua code as a string (SHARED) + local chunk = LoadResourceFile(LIB_NAME, ('%s/%s.lua'):format(directory, CONTEXT)) + local shared_chunk = LoadResourceFile(LIB_NAME, ('%s/shared.lua'):format(directory)) if shared_chunk then - chunk = chunk and ('%s\n%s'):format(shared_chunk, chunk) or shared_chunk -- creates a string with shared and client/server code combined + chunk = chunk and ('%s\n%s'):format(shared_chunk, chunk) or shared_chunk end if not chunk then - -- Early exit if the code doesn't exist - return + return end -- Second argument is a chunk name - local fn, err = load(chunk, ('@%s/imports/%s/%s.lua'):format(LIB_NAME, name, CONTEXT)) + local fn, err = load(chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, CONTEXT)) if not fn or err then if shared_chunk then @@ -52,16 +52,16 @@ local function loadModule(self, name) name, LIB_NAME)) -- Tries to load only shared.lua - fn, err = load(shared_chunk, ('@%s/imports/%s/%s.lua'):format(LIB_NAME, name, 'shared')) + fn, err = load(shared_chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, 'shared')) end if not fn or err then - return error(('Completly field importing module - %s; error - %s'):format(name, err)) + return error(('Completly failed importing module - %s; error - %s'):format(name, err)) end end local result = fn() - self[name] = result or noop + self[name] = result or Noop return self[name] end @@ -76,12 +76,20 @@ local function call(self, index, ...) -- Module Loading if not module then - self[index] = noop -- to prevent module from loading again if doesn't exists. + self[index] = Noop -- to prevent module from loading again if doesn't exists. module = loadModule(self, index) if not module then - return error(('[%s] Tried to access an invalid module - %s!'):format(LIB_NAME, index)) + local function method(...) + return exports[index](nil, ...) + end + + if not ... then + self[index] = method + end + + return method end end @@ -93,12 +101,12 @@ end --- Environment Setup ------------------------------------------------- - -- Creation of xLib object ----@class xLib +---@diagnostic disable-next-line: lowercase-global local xLib = setmetatable({ name = LIB_NAME, - side = CONTEXT + side = CONTEXT, + debug = IS_DEBUG }, { -- Lazy Loading - loads module only when accessed __index = call, @@ -106,4 +114,5 @@ local xLib = setmetatable({ }) -- Allows to xLib to be accessible in resource that imports a lib -_ENV.xLib = xLib \ No newline at end of file +_ENV.xLib = xLib +_ENV.require = xLib.require \ No newline at end of file diff --git a/[core]/esx_lib/imports/callback/client.lua b/[core]/esx_lib/imports/callback/client.lua new file mode 100644 index 00000000..9422c296 --- /dev/null +++ b/[core]/esx_lib/imports/callback/client.lua @@ -0,0 +1,148 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +local pendingCallbacks = {} +local timers = {} +local cbEvent = '__xLib_cb_%s' +local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000) +local resource_name = GetCurrentResourceName() --TODO: Add cache + +RegisterNetEvent(cbEvent:format(resource_name), function(key, ...) + if source == '' then return end + + local cb = pendingCallbacks[key] + + if not cb then return end + + pendingCallbacks[key] = nil + + cb(...) +end) + +---@param event string +---@param delay? number | false prevent the event from being called for the given time +local function eventTimer(event, delay) + if xLib.verify(delay, 'number') then + if delay > 0 then + local time = GetGameTimer() + + if (timers[event] or 0) > time then + return false + end + + timers[event] = time + delay + end + end + + return true +end + +---@param _ any +---@param event string +---@param delay number | false | nil +---@param cb function | false +---@param ... any +---@return ... +local function triggerServerCallback(_, event, delay, cb, ...) + if not eventTimer(event, delay) then return end + + local key + + repeat + key = ('%s:%s'):format(event, math.random(0, 100000)) + until not pendingCallbacks[key] + + TriggerServerEvent('xLib:validateCallback', event, resource_name, key) + TriggerServerEvent(cbEvent:format(event), resource_name, key, ...) + + ---@type promise | false + local promise = not cb and promise.new() + + pendingCallbacks[key] = function(response, ...) + if response == 'cb_invalid' then + response = ("callback '%s' does not exist"):format(event) + + return promise and promise:reject(response) or error(response) + end + + response = { response, ... } + + if promise then + return promise:resolve(response) + end + + if cb then + cb(table.unpack(response)) + end + end + + if promise then + SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end) + + return table.unpack(Citizen.Await(promise)) + end +end + +---@overload fun(event: string, delay: number | false, cb: function, ...) +xLib.callback = setmetatable({}, { + __call = function(_, event, delay, cb, ...) + if not cb then + warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning") + :format(event)) + else + local cbType = type(cb) + + if cbType == 'table' and getmetatable(cb)?.__call then + cbType = 'function' + end + + xLib.verify(cbType, 'function', true) + end + + return triggerServerCallback(_, event, delay, cb, ...) + end +}) + +---@param event string +---@param delay? number | false prevent the event from being called for the given time. +---Sends an event to the server and halts the current thread until a response is returned. +---@diagnostic disable-next-line: duplicate-set-field +function xLib.callback.await(event, delay, ...) + return triggerServerCallback(nil, event, delay, false, ...) +end + +local function callbackResponse(success, result, ...) + if not success then + if result then + return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result, + Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or '')) + end + + return false + end + + return result, ... +end + +local pcall = pcall + +---@param name string +---@param cb function +---Registers an event handler and callback function to respond to server requests. +---@diagnostic disable-next-line: duplicate-set-field +function xLib.callback.register(name, cb) + local event = cbEvent:format(name) + + xLib.setValidCallback(name, true) + + RegisterNetEvent(event, function(resource, key, ...) + TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...))) + end) +end + +return xLib.callback \ No newline at end of file diff --git a/[core]/esx_lib/imports/callback/server.lua b/[core]/esx_lib/imports/callback/server.lua new file mode 100644 index 00000000..f1d1080d --- /dev/null +++ b/[core]/esx_lib/imports/callback/server.lua @@ -0,0 +1,127 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +local pendingCallbacks = {} +local cbEvent = '__xLib_cb_%s' +local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000) +local resource_name = GetCurrentResourceName() --TODO: Add cache + +RegisterNetEvent(cbEvent:format(resource_name), function(key, ...) + local cb = pendingCallbacks[key] + + if not cb then return end + + pendingCallbacks[key] = nil + + cb(...) +end) + +---@param _ any +---@param event string +---@param playerId number +---@param cb function|false +---@param ... any +---@return ... +local function triggerClientCallback(_, event, playerId, cb, ...) + xLib.verify(playerId, 'playerId', true) + + local key + + repeat + key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId) + until not pendingCallbacks[key] + + TriggerClientEvent('xLib:validateCallback', playerId, event, resource_name, key) + TriggerClientEvent(cbEvent:format(event), playerId, resource_name, key, ...) + + ---@type promise | false + local promise = not cb and promise.new() + + pendingCallbacks[key] = function(response, ...) + if response == 'cb_invalid' then + response = ("callback '%s' does not exist"):format(event) + + return promise and promise:reject(response) or error(response) + end + + response = { response, ... } + + if promise then + return promise:resolve(response) + end + + if cb then + cb(table.unpack(response)) + end + end + + if promise then + SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end) + + return table.unpack(Citizen.Await(promise)) + end +end + +---@overload fun(event: string, playerId: number, cb: function, ...) +xLib.callback = setmetatable({}, { + __call = function(_, event, playerId, cb, ...) + if not cb then + warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning") + :format(event)) + else + local cbType = type(cb) + + if cbType == 'table' and getmetatable(cb)?.__call then + cbType = 'function' + end + + xLib.verify(cbType, 'function', true) + end + + return triggerClientCallback(_, event, playerId, cb, ...) + end +}) + +---@param event string +---@param playerId number +--- Sends an event to a client and halts the current thread until a response is returned. +---@diagnostic disable-next-line: duplicate-set-field +function xLib.callback.await(event, playerId, ...) + return triggerClientCallback(nil, event, playerId, false, ...) +end + +local function callbackResponse(success, result, ...) + if not success then + if result then + return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result, + Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or '')) + end + + return false + end + + return result, ... +end + +local pcall = pcall + +---@param name string +---@param cb function +---Registers an event handler and callback function to respond to client requests. +---@diagnostic disable-next-line: duplicate-set-field +function xLib.callback.register(name, cb) + event = cbEvent:format(name) + + xLib.setValidCallback(name, true) + + RegisterNetEvent(event, function(resource, key, ...) + TriggerClientEvent(cbEvent:format(resource), source, key, callbackResponse(pcall(cb, source, ...))) + end) +end + +return xLib.callback \ No newline at end of file diff --git a/[core]/esx_lib/imports/class/shared.lua b/[core]/esx_lib/imports/class/shared.lua new file mode 100644 index 00000000..93653446 --- /dev/null +++ b/[core]/esx_lib/imports/class/shared.lua @@ -0,0 +1,35 @@ +---@class xClass +---@field new function +---@field constructor function? + +---Create or register a class +---@param copy? table +---@return table +function xLib.class(copy) + xLib.verify(copy, { 'table', 'nil' }, true) + + local class = copy and xLib.table.deepcopy(copy) or {} + + class.__index = class + class._isClass = true + + setmetatable(class, { + __newindex = function (_, k,v) + rawset(class, k, v) + end + }) + + function class:new(...) + local obj = setmetatable({}, class) + + if xLib.verify(obj.constructor, 'function') then + obj:constructor(...) + end + + return obj + end + + return class +end + +return xLib.class \ No newline at end of file diff --git a/[core]/esx_lib/imports/colors/client.lua b/[core]/esx_lib/imports/colors/client.lua new file mode 100644 index 00000000..4196f99a --- /dev/null +++ b/[core]/esx_lib/imports/colors/client.lua @@ -0,0 +1,10 @@ +xLib.colors = {} + +xLib.colors.brand = GetConvar("esx:brand-color", "#FB9B04") +xLib.colors.darkest = GetConvar("esx:darkest-color", "#161616") +xLib.colors.dark = GetConvar("esx:dark-color", "#252525") +xLib.colors.mid = GetConvar("esx:mid-color", "#383838") +xLib.colors.light = GetConvar("esx:light-color", "#969696") +xLib.colors.lightest = GetConvar("esx:lightest-color", "#F2F2F2") + +return xLib.colors diff --git a/[core]/esx_lib/imports/overload/shared.lua b/[core]/esx_lib/imports/overload/shared.lua new file mode 100644 index 00000000..9f2e7915 --- /dev/null +++ b/[core]/esx_lib/imports/overload/shared.lua @@ -0,0 +1,66 @@ +local overloads = {} + +---@param name string +---@param ... unknown +local function getOverloadFunction(name, ...) + local params = {...} + local params_count = #params + local is_valid, overload, correct_type + + + for i=1, #overloads[name] do + overload = overloads[name][i] + + if not overload then + goto continue + end + + is_valid = #overload.valid_types == params_count + + if is_valid then + for j = 1, #overload.valid_types do + correct_type = overload.valid_types[j] + if not xLib.verify(params[j], correct_type) then + is_valid = false + break + end + end + + if is_valid then + return overload.cb(...) + end + end + + ::continue:: + end + + error('[xLib] Couldn\'t find a correct method to overload') +end + +---Overloads a function +---@param name string +---@param valid_types CustomType | CustomType[] +---@param cb function +---@param obj? table +function xLib.overload(name, valid_types, cb, obj) + xLib.verify(name, {'string'}, true) + + if not overloads[name] then + overloads[name] = {} + end + + overloads[name][#overloads[name] + 1] = { + valid_types = valid_types, + cb = cb + } + + local env = obj and obj or _ENV + + if not env[name] then + env[name] = function(...) + return getOverloadFunction(name, ...) + end + end +end + +return xLib.overload \ No newline at end of file diff --git a/[core]/esx_lib/imports/require/shared.lua b/[core]/esx_lib/imports/require/shared.lua new file mode 100644 index 00000000..b3b514a0 --- /dev/null +++ b/[core]/esx_lib/imports/require/shared.lua @@ -0,0 +1,200 @@ +--!DISCLAIMER +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +local LOADED = {} +local TEMP_DATA = {} + +local _require = require --original lua function + +package = { + path = './?.lua;./?/init.lua', + preload = {}, + loaded = setmetatable({}, { + __index = LOADED, + __newindex = Noop, + __metatable = false, + }) +} + +---Gets the resource name and the module's relative path +---@param name string -- module name +---@return string, string +local function getModuleInfo(name) + local resource = name:match('^@(.-)/.+') -- if name is for example @esx_lib/imports/require/initl.lua it will capture ESX_LIB + + if resource then + return resource, name:sub(#resource + 3) -- returns the path without script name + end + + local idx = 4-- call stack depth (kept slightly lower than expected depth "just in case") + --- When indexing source of 4 it returns @@esx_lib/imports/require + --- When indexing source of 5 it returns for example @@esx_test/client.lua + --- Used for identifing resource name. + + while true do + local src = debug.getinfo(idx, 'S')?.source + + if not src then + error(('Couldn\'t find a source for "%s"'):format(name)) + end + + resource = src:match('^@@([^/]+)/.+') -- returns resource name + + if resource and not src:find('^@@esx_lib/imports/require') then + return resource, name + end + + idx += 1 + end +end + +---Searcher for a accessable path +---@param name string +---@param path string +---@return string?, string? -- filename, error message +---@diagnostic disable-next-line: duplicate-set-field +function package.searchpath(name, path) + local resource, module_name = getModuleInfo(name:gsub('%.', '/')) + + local tried = {} + + for template in path:gmatch('[^;]+') do + local file_name = template:gsub('^%./', ''):gsub('?', module_name:gsub('%.', '/') or module_name) + + local file = LoadResourceFile(resource, file_name) + + if file then + TEMP_DATA[1] = file + TEMP_DATA[2] = resource + + return file_name + end + + tried[#tried+1] = ('no file "@%s/%s"'):format(resource, file_name) + end + + return nil, table.concat(tried, '\n\t') +end + +---Loads module +---@param name string +---@param env? unknown +---@return function?, string? +local function loadModule(name, env) + local file_name, err = package.searchpath(name, package.path) + + if file_name then + local file = TEMP_DATA[1] + local resource = TEMP_DATA[2] + + table.wipe(TEMP_DATA) + + return assert(load(file, ('@@%s/%s'):format(resource, file_name), 't', env or _ENV)) + end + + return nil, err or 'unknown error' +end + +---@diagnostic disable-next-line: duplicate-doc-alias +---@alias PackageSearcher +---| fun(name: string): function loader +---| fun(name: string): nil|false , string error message +---| fun(name: string): function?, string? + +---@type PackageSearcher[] +package.searchers = { + function (name) + local ok, result = pcall(_require, name) + + if ok then + return result + end + + return ok, result + end, + function (name) + if package.preload[name] ~= nil then + return package.preload[name] + end + + return nil, ('no field package.preload["%s"]'):format(name) + end, + function(name) + return loadModule(name) + end, +} + +---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use. +---@param file_path string +---@param env? unknown +function xLib.load(file_path, env) + xLib.verify(file_path, 'string', true) + + local result, err = loadModule(file_path, env) + + if result then + return result() + end + + error(('file "%s" not found\n\t%s'):format(file_path, err)) +end + +---Loads and decodes a json file at the given path. +---@param file_path string +function xLib.loadJson(file_path) + xLib.verify(file_path, 'string', true) + + local res_source, mod_path = getModuleInfo(file_path:gsub('%.', '/')) + + local res_file = LoadResourceFile(res_source, ('%s.json'):format(mod_path)) + + if res_file then + return json.decode(res_file) + end + + error(('json file "%s" not found\n\tno file "%s/%s.json"'):format(file_path, res_source, mod_path)) +end + +---Loads the given module, returns any value returned by the seacher (`true` when `nil`).\ +---Passing `@resourceName.modName` loads a module from a remote resource. +---@param name string +---@return unknown +function xLib.require(name) + xLib.verify(name, 'string', true) + + local module = LOADED[name] + + if module == 'loading' then + error(('^1circular-dependency occurred when loading module "%s"^0'):format(name)) + end + + if module ~= nil then return module end + + LOADED[name] = 'loading' + + local err = {} + + for i = 1, #package.searchers do + local result, err_msg = package.searchers[i](name) + + if result then + if type(result) == 'function' then result = result() end + LOADED[name] = result or result == nil + + return LOADED[name] + end + + + err[#err + 1] = err_msg + end + + error(('%s'):format(table.concat(err, '\n\t'))) +end + +return xLib.require diff --git a/[core]/esx_lib/imports/string/shared.lua b/[core]/esx_lib/imports/string/shared.lua new file mode 100644 index 00000000..b6c00867 --- /dev/null +++ b/[core]/esx_lib/imports/string/shared.lua @@ -0,0 +1,147 @@ +---@class stringlib +xLib.string = string + +--- Normalize (trim + lowercase) +---@param s string +---@return string +function xLib.string.normalize(s) + xLib.verify(s, 'string', true) + + local res = s:match("^%s*(.-)%s*$"):lower() + + return res +end + +---@param s string +---@return string +function xLib.string.capitalize(s) + xLib.verify(s, 'string', true) + + local res = s:gsub("^%l", string.upper) + + return res +end + +---@param s string +---@return string +function xLib.string.toSnake(s) + xLib.verify(s, 'string', true) + + local res = s:gsub("%s+", "_"):gsub("([a-z%d])([A-Z])", "%1_%2"):lower() + + return res +end + +---@param s string +---@return string +function xLib.string.toCamel(s) + xLib.verify(s, 'string', true) + + local res = s:lower():gsub("_%a", function(w) return w:sub(2):upper() end) + + return res +end + +---@param s string +---@return string +function xLib.string.toPascal(s) + xLib.verify(s, 'string', true) + + local res = s:gsub("(%a)([%w_]*)", function(first, rest) + return first:upper() .. rest:lower() + end):gsub("_", "") + + return res +end + +---@param s string +---@return string +function xLib.string.escapePattern(s) + xLib.verify(s, 'string', true) + + local res = s:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1") + + return res +end + +---@param s string +---@param pattern string +---@return boolean +function xLib.string.matchSafe(s, pattern) + local ok, result = pcall(string.match, s, pattern) + return ok and result ~= nil +end + +---@param s string +---@param pattern string +---@return string +function xLib.string.before(s, pattern) + xLib.verify(s, 'string', true) + local start = s:find(pattern, 1, true) + + if not start then return s end + + return s:sub(1, start - 1) +end + +---@param s string +---@param pattern string +---@return string +function xLib.string.after(s, pattern) + xLib.verify(s, 'string', true) + + local _, finish = s:find(pattern, 1, true) + + if not finish then return s end + + return s:sub(finish + 1) +end + + +---@param s string +---@param substr string +---@return boolean +function xLib.string.contains(s, substr) + xLib.verify(s, 'string', true) + + return s:find(substr, 1, true) ~= nil +end + +---@param s string +---@param old string +---@param new string +---@return string +function xLib.string.replace(s, old, new) + xLib.verify(s, 'string', true) + + local result = s:gsub(xLib.string.escapePattern(old), new) + + return result +end + + +---@param length number +---@return string +function xLib.string.randomHex(length) + local t = {} + + for _ = 1, length do + t[#t + 1] = string.format("%x", math.random(0, 15)) + end + + return table.concat(t) +end + +---@return string +function xLib.string.uuid() + local template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx" + + local uuid = template:gsub("[xy]", function(c) + local v = (c == "x") and math.random(0, 15) or math.random(8, 11) + return string.format("%x", v) + end) + + return uuid +end + +return xLib.string diff --git a/[core]/esx_lib/imports/table/shared.lua b/[core]/esx_lib/imports/table/shared.lua new file mode 100644 index 00000000..c3550717 --- /dev/null +++ b/[core]/esx_lib/imports/table/shared.lua @@ -0,0 +1,133 @@ +---@class tablelib +xLib.table = table + +---@param tbl table +---@return boolean +function xLib.table.isArray(tbl) + xLib.verify(tbl, "table", true) + + local count, maxIndex = 0, 0 + + for k, _ in pairs(tbl) do + if type(k) ~= "number" or k < 1 or k % 1 ~= 0 then + return false + end + + count, maxIndex = count + 1, math.max(maxIndex, k) + + if count > maxIndex then + return false + end + end + + return true +end + +---@param tbl table +---@param item any +---@return any +function xLib.table.searchForKey(tbl, item) + xLib.verify(tbl, "table", true) + + for k, v in pairs(tbl) do + if v == item then + return k + end + end + + return nil +end + +---@param tbl table +---@param item any +---@return boolean +function xLib.table.contains(tbl, item) + return xLib.table.searchForKey(tbl, item) ~= nil +end + +---@param tbl table +---@param filter fun(value:any, key:any):boolean +---@return table +function xLib.table.filter(tbl, filter) + xLib.verify(tbl, "table", true) + xLib.verify(filter, "function", true) + + local result = {} + + for k, v in pairs(tbl) do + if filter(v, k) then + result[k] = v + end + end + + return result +end + +---@param tbl table +---@param copies? table +---@return table +function xLib.table.deepcopy(tbl, copies) + xLib.verify(tbl, "table", true) + + copies = copies or {} + + if copies[tbl] then + return copies[tbl] + end + + local copy = {} + copies[tbl] = copy + + for k, v in pairs(tbl) do + copy[k] = type(v) == "table" and xLib.table.deepcopy(v, copies) or v + + if type(v) == "table" and getmetatable(v) then + setmetatable(copy[k], getmetatable(v)) + end + end + + return copy +end + +--- https://github.com/overextended/ox_lib/blob/master/imports/table/shared.lua +---@param t1 table +---@param t2 table +---@param addDuplicateNumbers boolean +---@return table +function xLib.table.merge(t1, t2, addDuplicateNumbers) + xLib.verify(t1, "table", true) + xLib.verify(t2, "table", true) + xLib.verify(addDuplicateNumbers, "boolean", true) + + addDuplicateNumbers = addDuplicateNumbers == nil or addDuplicateNumbers + for k, v2 in pairs(t2) do + local v1 = t1[k] + local type1 = type(v1) + local type2 = type(v2) + + if type1 == 'table' and type2 == 'table' then + xLib.table.merge(v1, v2, addDuplicateNumbers) + elseif addDuplicateNumbers and (type1 == 'number' and type2 == 'number') then + t1[k] = v1 + v2 + else + t1[k] = v2 + end + end + + return t1 +end + +function xLib.table.dump(tbl) + if xLib.verify(tbl, 'table') then + local s = '{ ' + for k,v in pairs(tbl) do + if type(k) ~= 'number' then k = '"'..k..'"' end + s = s .. '['..k..'] = ' .. tbl(v) .. ',' + end + return s .. '} ' + else + return tostring(tbl) + end +end + +return xLib.table diff --git a/[core]/esx_lib/imports/verify/shared.lua b/[core]/esx_lib/imports/verify/shared.lua new file mode 100644 index 00000000..8de48d22 --- /dev/null +++ b/[core]/esx_lib/imports/verify/shared.lua @@ -0,0 +1,161 @@ +---@alias CustomType 'number' | 'boolean' | 'function' | 'table' | 'string' | 'nil' | 'array' | 'int' | 'uint' | 'float' | 'char' | 'vector3' | 'vector4' | 'ped' | 'playerId' | 'vehicle' | 'prop' | 'class' | 'model' + +---Checks if value is an array +---@param value any +---@return boolean +local function isArray(value) + if type(value) ~= 'table' then + return false + end + + local i = 0 + + for _ in pairs(value) do + i = i + 1 + if value[i] == nil then + return false + end + end + + return true +end + +---Checks if player id is correct. +---@param value any +---@return boolean +local function isPlayerId(value) + if type(value) ~= 'number' then + return false + end + + if xLib.side == 'server' then + return GetPlayerName(value) ~= nil + else + return NetworkIsPlayerActive(value) + end +end + +---Checks if function is callable. +---@param value any +---@return boolean +local function isCallable(value) + local value_type = type(value) + + if value_type == 'function' then + return true + end + + if value_type == 'table' then + local mt = getmetatable(value) + + if mt and mt.__call then + return true + end + end + + return false +end + +xLib.callback.register('xLib:validateModel', function(model) + return IsModelValid(model) +end) + +local function validateModel(model) + local players = GetPlayers() + local source = tonumber(players[math.random(1,#players)]) + + return xLib.callback.await('xLib:validateModel', source, false, model) +end + +---Make sure value is a valid type +---@param value any +---@param valid_type CustomType +---@return boolean +local function verifyType(value, valid_type) + if valid_type == 'function' then + return isCallable(value) + elseif valid_type == 'array' then + return isArray(value) + elseif valid_type == 'int' then + return math.type(value) == 'integer' + elseif valid_type == 'float' then + return math.type(value) == 'float' + elseif valid_type == 'uint' then + return math.type(value) == 'int' and value >= 0 + elseif valid_type == 'char' then + return type(value) == 'string' and #value == 1 + elseif valid_type == 'ped' then + return GetEntityType(value) == 1 + elseif valid_type == 'vehicle' then + return GetEntityType(value) == 2 + elseif valid_type == 'prop' then + return GetEntityType(value) == 3 + elseif valid_type == 'playerId' then + return isPlayerId(value) + elseif valid_type == 'class' then + if type(value) ~= 'table' then + return false + end + + return getmetatable(value)?._isClass == true + elseif valid_type == 'model' then + if xLib.side == "server" then + return validateModel(value) + end + + return IsModelValid(value) + end + + return type(value) == valid_type +end + +---@class VerifyTable +---@field debug VerifyFunction + +---@alias VerifyFunction fun(value: any, valid_types: CustomType[] | CustomType, throw_error?: boolean): boolean + +---@type VerifyFunction | VerifyTable +xLib.verify = setmetatable({ + fn = function(value, valid_types, throw_error) + local match = false + + if type(valid_types) == 'table' then + for i = 1, #valid_types do + if xLib.verify(value, valid_types[i]) then + match = true + break + end + end + + if throw_error and not match then + error(('[xLib] Couldn\'t match %s to types %s'):format(value, json.encode(valid_types))) + else + return match + end + end + + match = verifyType(value, valid_types) + + if throw_error and not match then + error(('[xLib] Couldn\'t match %s to type %s'):format(value, valid_types)) + else + return match + end + end +}, { + __index = function(self, k) + if k == 'debug' and xLib.debug then + return rawget(self, 'fn' + ) + elseif k ~= 'debug' then + return rawget(self, k) + else + return Noop + end + end, + __call = function(self, ...) + return rawget(self, 'fn')(...) + end +}) + +return xLib.verify diff --git a/[core]/esx_lib/resource/callback/shared.lua b/[core]/esx_lib/resource/callback/shared.lua new file mode 100644 index 00000000..c8e0c7a2 --- /dev/null +++ b/[core]/esx_lib/resource/callback/shared.lua @@ -0,0 +1,67 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +local registeredCallbacks = {} +local resource_name = GetCurrentResourceName() --TODO: Add cache + + +AddEventHandler('onResourceStop', function(resourceName) + if resource_name == resourceName then return end + + for callbackName, resource in pairs(registeredCallbacks) do + if resource == resourceName then + registeredCallbacks[callbackName] = nil + end + end +end) + +---For internal use only. +---Sets a callback event as registered to a specific resource, preventing it from +---being overwritten. Any unknown callbacks will return an error to the caller. +---@param callbackName string +---@param isValid boolean +function xLib.setValidCallback(callbackName, isValid) + local resourceName = GetInvokingResource() or resource_name + local callbackResource = registeredCallbacks[callbackName] + + if callbackResource then + if not isValid then + callbackResource[callbackName] = nil + return + end + + if callbackResource == resourceName then return end + + local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource) + + return print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage, + Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or '')) + end + + print(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName)) + + registeredCallbacks[callbackName] = resourceName +end + +function xLib.isCallbackValid(callbackName) + return registeredCallbacks[callbackName] == GetInvokingResource() or resource_name +end + +local cbEvent = '__xLib_cb_%s' + +RegisterNetEvent('xLib:validateCallback', function(callbackName, invokingResource, key) + if registeredCallbacks[callbackName] then return end + + local event = cbEvent:format(invokingResource) + + if GetGameName() == 'fxserver' then + return TriggerClientEvent(event, source, key, 'cb_invalid') + end + + TriggerServerEvent(event, key, 'cb_invalid') +end) \ No newline at end of file diff --git a/[core]/esx_lib/resource/init.lua b/[core]/esx_lib/resource/init.lua new file mode 100644 index 00000000..b53f05d2 --- /dev/null +++ b/[core]/esx_lib/resource/init.lua @@ -0,0 +1,38 @@ +---@diagnostic disable: lowercase-global +xLib = setmetatable({ + name = 'xLib', + side = IsDuplicityVersion() and 'server' or 'client' +}, { + __newindex = function(self, key, fn) + rawset(self, key, fn) + + if debug.getinfo(2, 'S').short_src:find('@esx_lib/resource') then + exports(key, fn) + end + end, + + __index = function(self, key) + local dir = ('imports/%s'):format(key) + local chunk = LoadResourceFile(self.name, ('%s/%s.lua'):format(dir, self.side)) + + local shared = LoadResourceFile(self.name, ('%s/shared.lua'):format(dir)) + + if shared then + chunk = (chunk and ('%s\n%s'):format(shared, chunk)) or shared + end + + if chunk then + local fn, err = load(chunk, ('@@esx_lib/%s/%s.lua'):format(key, self.side)) + + if not fn or err then + return error(('\n^1Error importing module (%s): %s^0'):format(dir, err), 3) + end + + rawset(self, key, fn() or Noop) + + return self[key] + end + end +}) + +require = xLib.require \ No newline at end of file diff --git a/[core]/esx_lib/resource/test/client.lua b/[core]/esx_lib/resource/test/client.lua new file mode 100644 index 00000000..e69de29b diff --git a/[core]/esx_lib/resource/test/server.lua b/[core]/esx_lib/resource/test/server.lua new file mode 100644 index 00000000..e69de29b