This commit is contained in:
Kr3mu
2025-10-23 16:55:17 +02:00
parent 04318a4d77
commit 23956e4a7f
15 changed files with 1162 additions and 20 deletions
+1
View File
@@ -15,6 +15,7 @@ files {
}
shared_scripts {
'resource/init.lua',
'resource/**/shared.lua',
}
+29 -20
View File
@@ -9,21 +9,22 @@
-- Const Variables
local CONTEXT<const> = IsDuplicityVersion() and 'server' or 'client'
local LIB_NAME<const> = 'esx_lib'
local LIB_NAME <const> = 'esx_lib'
local IS_DEBUG <const> = 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
_ENV.xLib = xLib
_ENV.require = xLib.require
+148
View File
@@ -0,0 +1,148 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
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
+127
View File
@@ -0,0 +1,127 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
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
+35
View File
@@ -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
+10
View File
@@ -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
@@ -0,0 +1,66 @@
local overloads <const> = {}
---@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
+200
View File
@@ -0,0 +1,200 @@
--!DISCLAIMER
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local LOADED <const> = {}
local TEMP_DATA <const> = {}
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 <const> = {}
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
+147
View File
@@ -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
+133
View File
@@ -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
+161
View File
@@ -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
@@ -0,0 +1,67 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
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)
+38
View File
@@ -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