Merge remote-tracking branch 'Stane/esx_lib'

This commit is contained in:
_Not_
2026-07-15 23:39:44 -05:00
23 changed files with 1369 additions and 49 deletions
+8
View File
@@ -0,0 +1,8 @@
--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g:
ESX.Streaming.RequestModel = xLib.streaming.requestModel
ESX.Streaming.RequestStreamedTextureDict = xLib.streaming.requestStreamedTextureDict
ESX.Streaming.RequestNamedPtfxAsset = xLib.streaming.requestNamedPtfxAsset
ESX.Streaming.RequestAnimSet = xLib.streaming.requestAnimSet
ESX.Streaming.RequestAnimDict = xLib.streaming.requestAnimDict
ESX.Streaming.RequestWeaponAsset = xLib.streaming.requestWeaponAsset
+5
View File
@@ -15,6 +15,7 @@ shared_scripts {
'shared/main.lua',
'shared/functions.lua',
'shared/modules/*.lua',
'shared/compat.lua'
}
server_scripts {
@@ -39,6 +40,8 @@ server_scripts {
'server/modules/createJob.lua',
'server/migration/**/main.lua',
'server/migration/main.lua',
'server/compat.lua'
}
client_scripts {
@@ -57,6 +60,8 @@ client_scripts {
'client/modules/interactions.lua',
'client/modules/scaleform.lua',
'client/modules/streaming.lua',
'shared/compat.lua'
}
ui_page {
+2
View File
@@ -0,0 +1,2 @@
--[[ All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g:
--]]
+5
View File
@@ -0,0 +1,5 @@
--All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g:
ESX.SetTimeout = xLib.timeout.setTimeout
ESX.ClearTimeout = xLib.timeout.clearTimeout
ESX.Await = xLib.waitFor
-39
View File
@@ -178,45 +178,6 @@ function ESX.IsFunctionReference(val)
return typeVal == "function" or (typeVal == "table" and type(getmetatable(val)?.__call) == "function")
end
---@param conditionFunc function A function that is repeatedly called until it returns a truthy value or the timeout is exceeded.
---@param errorMessage? string Optional. If set, an error will be thrown with this message if the condition is not met within the timeout. If not set, no error will be thrown.
---@param timeoutMs? number Optional. The maximum time to wait (in milliseconds) for the condition to be met. Defaults to 1000ms.
---@return boolean, any: Returns success status and the returned value of the condition function.
function ESX.Await(conditionFunc, errorMessage, timeoutMs)
timeoutMs = timeoutMs or 1000
if timeoutMs < 0 then
error("Timeout should be a positive number.")
end
if not ESX.IsFunctionReference(conditionFunc) then
error("Condition Function should be a function reference.")
end
-- since errorMessage is optional, we only validate it if the user provided it.
if errorMessage then
ESX.AssertType(errorMessage, "string", "errorMessage should be a string.")
end
local invokingResource = GetInvokingResource()
local startTimeMs = GetGameTimer()
while GetGameTimer() - startTimeMs < timeoutMs do
local result = conditionFunc()
if result then
return true, result
end
Wait(0)
end
if errorMessage then
error(("[%s] -> %s"):format(invokingResource, errorMessage))
end
return false
end
---@param str string
---@param allowDigits boolean? Allow numbers if necessary
---@return boolean
+31
View File
@@ -0,0 +1,31 @@
fx_version 'cerulean'
game 'gta5'
use_experimental_fxv2_oal 'yes'
lua54 'yes'
author 'ESX Team'
version '0.01'
description 'Official ESX library'
files {
'imports.lua',
'imports/**/client.lua',
'imports/**/shared.lua',
}
shared_scripts {
'resource/init.lua',
'resource/**/shared.lua',
}
client_scripts {
'resource/**/client.lua',
}
server_scripts {
'resource/**/server.lua',
}
+118
View File
@@ -0,0 +1,118 @@
--! 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>
]]
-- Const Variables
local CONTEXT<const> = IsDuplicityVersion() and 'server' or 'client'
local LIB_NAME <const> = 'esx_lib'
local IS_DEBUG <const> = GetConvar('xLib:debug', 'false') == 'true'
---@alias Module table | function
---@class xLib
---@field name string
---@field side 'server' | 'client'
-------------------------------------------------
--- Core functions for modules
-------------------------------------------------
--- Function that returns nothing
--- Used as a "shortcut"
function Noop() end
---Loads a module into memory
---@param self xLib
---@param name string -- module name
---@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))
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
end
if not chunk then
return
end
-- Second argument is a chunk name
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
print(('[%s] Found an error while importing - %s! Try updating %s to the latest version!'):format(LIB_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'))
end
if not fn or err then
return error(('Completly failed importing module - %s; error - %s'):format(name, err))
end
end
local result = fn()
self[name] = result or Noop
return self[name]
end
---Function that is responsible for lazy loading
---@param self xLib
---@param index string -- name of the function/key that is called
---@param ... unknown -- function params
---@return Module
local function call(self, index, ...)
local module = rawget(self, index) -- uses rawget not to trigger any metamethods
-- Module Loading
if not module then
self[index] = Noop -- to prevent module from loading again if doesn't exists.
module = loadModule(self, index)
if not module then
local function method(...)
return exports[LIB_NAME][index](nil, ...)
end
if not ... then
self[index] = method
end
return method
end
end
return module
end
-------------------------------------------------
--- Environment Setup
-------------------------------------------------
-- Creation of xLib object
---@diagnostic disable-next-line: lowercase-global
local xLib = setmetatable({
name = LIB_NAME,
side = CONTEXT,
debug = IS_DEBUG
}, {
-- Lazy Loading - loads module only when accessed
__index = call,
__call = call,
})
-- Allows to xLib to be accessible in resource that imports a lib
_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
@@ -1,9 +1,9 @@
ESX.Streaming = {}
xLib.streaming = {}
---@param modelHash number | string
---@param cb? function
---@return number | nil
function ESX.Streaming.RequestModel(modelHash, cb)
xLib.streaming.requestModel = function(modelHash, cb)
modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash)
if not IsModelInCdimage(modelHash) then return end
@@ -17,7 +17,7 @@ end
---@param textureDict string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestStreamedTextureDict(textureDict, cb)
xLib.streaming.requestStreamedTextureDict = function(textureDict, cb)
RequestStreamedTextureDict(textureDict, false)
while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end
@@ -28,7 +28,7 @@ end
---@param assetName string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestNamedPtfxAsset(assetName, cb)
xLib.streaming.requestNamedPtfxAsset = function(assetName, cb)
RequestNamedPtfxAsset(assetName)
while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end
@@ -39,7 +39,7 @@ end
---@param animSet string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestAnimSet(animSet, cb)
xLib.streaming.requestAnimSet = function(animSet, cb)
RequestAnimSet(animSet)
while not HasAnimSetLoaded(animSet) do Wait(500) end
@@ -50,7 +50,7 @@ end
---@param animDict string
---@param cb? function
---@return string | nil
function ESX.Streaming.RequestAnimDict(animDict, cb)
xLib.streaming.requestAnimDict = function(animDict, cb)
RequestAnimDict(animDict)
while not HasAnimDictLoaded(animDict) do Wait(500) end
@@ -61,10 +61,24 @@ end
---@param weaponHash number | string
---@param cb? function
---@return string | number | nil
function ESX.Streaming.RequestWeaponAsset(weaponHash, cb)
xLib.streaming.requestWeaponAsset = function(weaponHash, cb)
RequestWeaponAsset(weaponHash, 31, 0)
while not HasWeaponAssetLoaded(weaponHash) do Wait(500) end
return cb and cb(weaponHash) or weaponHash
end
---@param bankName string
---@param cb? function
---@return string | nil
xLib.streaming.requestAudioBank = function(bankName, cb)
RequestAudioBank(bankName, false)
while not RequestScriptAudioBank(bankName, false) do Wait(500) end
return cb and cb(bankName) or bankName
end
return xLib.streaming
+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
@@ -1,10 +1,14 @@
xLib.timeout = {}
local TimeoutCount = 0
local CancelledTimeouts = {}
---@param msec number
---@param cb function
---@return number
ESX.SetTimeout = function(msec, cb)
xLib.timeout.setTimeout = function(msec, cb)
xLib.verify(cb, "function", true)
local id <const> = TimeoutCount + 1
SetTimeout(msec, function()
@@ -12,7 +16,6 @@ ESX.SetTimeout = function(msec, cb)
CancelledTimeouts[id] = nil
return
end
cb()
end)
@@ -23,6 +26,8 @@ end
---@param id number
---@return nil
ESX.ClearTimeout = function(id)
xLib.timeout.clearTimeout = function(id)
CancelledTimeouts[id] = true
end
return xLib.timeout
+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
+39
View File
@@ -0,0 +1,39 @@
---@param conditionFunc function A function that is repeatedly called until it returns a truthy value or the timeout is exceeded.
---@param errorMessage? string Optional. If set, an error will be thrown with this message if the condition is not met within the timeout. If not set, no error will be thrown.
---@param timeoutMs? number Optional. The maximum time to wait (in milliseconds) for the condition to be met. Defaults to 1000ms.
---@return boolean, any: Returns success status and the returned value of the condition function.
xLib.waitFor = function(conditionFunc, errorMessage, timeoutMs)
timeoutMs = timeoutMs or 1000
if timeoutMs < 0 then
error("Timeout should be a positive number.")
end
xLib.verify(conditionFunc, "function", true)
-- since errorMessage is optional, we only validate it if the user provided it.
if errorMessage then
xLib.verify(errorMessage, "string", true)
end
local invokingResource = GetInvokingResource()
local startTimeMs = GetGameTimer()
while GetGameTimer() - startTimeMs < timeoutMs do
local result = conditionFunc()
if result then
return true, result
end
Wait(0)
end
if errorMessage then
error(("[%s] -> %s"):format(invokingResource, errorMessage))
end
return false
end
return xLib.waitFor
@@ -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