From fe82f8c6c4f60d9bc0052dcc3e68bc7f18c1b98d Mon Sep 17 00:00:00 2001 From: Kr3mu Date: Thu, 28 Aug 2025 23:00:06 +0200 Subject: [PATCH 01/16] Creates fxmanifest for esx_lib and imports with commented code. --- [core]/esx_lib/fxmanifest.lua | 30 ++++++++++ [core]/esx_lib/imports.lua | 103 ++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 [core]/esx_lib/fxmanifest.lua create mode 100644 [core]/esx_lib/imports.lua diff --git a/[core]/esx_lib/fxmanifest.lua b/[core]/esx_lib/fxmanifest.lua new file mode 100644 index 00000000..91010f8b --- /dev/null +++ b/[core]/esx_lib/fxmanifest.lua @@ -0,0 +1,30 @@ +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/**/shared.lua', +} + +client_scripts { + 'resource/**/client.lua', +} + +server_scripts { + 'resource/**/server.lua', +} + + + diff --git a/[core]/esx_lib/imports.lua b/[core]/esx_lib/imports.lua new file mode 100644 index 00000000..3457ebf1 --- /dev/null +++ b/[core]/esx_lib/imports.lua @@ -0,0 +1,103 @@ +--! DISCLAIMER +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +-- Const Variables +local CONTEXT = IsDuplicityVersion() and 'server' or 'client' +local LIB_NAME = 'esx_lib' + +-- xLib class for Language Server +---@class xLib +---@field name string +---@field side 'server' | 'client' + + +------------------------------------------------- +--- Core functions for modules +------------------------------------------------- + + +local function noop() end + +---Loads a module into memory +---@param self xLib +---@param name string -- module name +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) + + 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 + end + + if not chunk then + -- Early exit if the code doesn't exist + return false + end + + -- Uses @@ to specify that it comes from fivem resource instead of "normal lua script" + -- 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 field importing module - %s; error - %s'):format(name, err)) + end + end + +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 +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 + error(('[%s] Tried to access an invalid module - %s!'):format(LIB_NAME, index)) + end + end + + return module +end + + +------------------------------------------------- +--- Environment Setup +------------------------------------------------- + + +-- Creation of xLib object +---@class xLib +local xLib = setmetatable({ + name = LIB_NAME, + side = CONTEXT +}, { + -- 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 \ No newline at end of file From 6b4d7c1956082a841c1cfcd6a04099af7b6956c1 Mon Sep 17 00:00:00 2001 From: Kr3mu Date: Thu, 28 Aug 2025 23:11:50 +0200 Subject: [PATCH 02/16] =?UTF-8?q?Added=20noop=20comment,=20deleted=20bs=20?= =?UTF-8?q?about=20@@=20=F0=9F=98=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- [core]/esx_lib/imports.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/[core]/esx_lib/imports.lua b/[core]/esx_lib/imports.lua index 3457ebf1..db1f633f 100644 --- a/[core]/esx_lib/imports.lua +++ b/[core]/esx_lib/imports.lua @@ -21,7 +21,7 @@ local LIB_NAME = 'esx_lib' --- Core functions for modules ------------------------------------------------- - +--- Function that returns nothing local function noop() end ---Loads a module into memory @@ -41,9 +41,8 @@ local function loadModule(self, name) return false end - -- Uses @@ to specify that it comes from fivem resource instead of "normal lua script" -- 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 @@ -51,7 +50,7 @@ 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 From 04318a4d774c06c8c5adc2324358251082941dd9 Mon Sep 17 00:00:00 2001 From: Kr3mu Date: Thu, 28 Aug 2025 23:21:35 +0200 Subject: [PATCH 03/16] Adds forgotten return in loadModule, loadModule returns nil instead of false, added return type in ldoc --- [core]/esx_lib/imports.lua | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/[core]/esx_lib/imports.lua b/[core]/esx_lib/imports.lua index db1f633f..4e5851aa 100644 --- a/[core]/esx_lib/imports.lua +++ b/[core]/esx_lib/imports.lua @@ -16,6 +16,7 @@ local LIB_NAME = 'esx_lib' ---@field name string ---@field side 'server' | 'client' +---@alias Module table | function ------------------------------------------------- --- Core functions for modules @@ -27,6 +28,7 @@ local 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)) -- returns a raw lua code as a string (SERVER/CLIENT side) @@ -38,7 +40,7 @@ local function loadModule(self, name) if not chunk then -- Early exit if the code doesn't exist - return false + return end -- Second argument is a chunk name @@ -58,12 +60,17 @@ local function loadModule(self, name) 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 @@ -74,7 +81,7 @@ local function call(self, index, ...) module = loadModule(self, index) if not module then - error(('[%s] Tried to access an invalid module - %s!'):format(LIB_NAME, index)) + return error(('[%s] Tried to access an invalid module - %s!'):format(LIB_NAME, index)) end end From 23956e4a7fe526c3602927518af1017b8141a6be Mon Sep 17 00:00:00 2001 From: Kr3mu Date: Thu, 23 Oct 2025 16:55:17 +0200 Subject: [PATCH 04/16] init --- [core]/esx_lib/fxmanifest.lua | 1 + [core]/esx_lib/imports.lua | 49 +++-- [core]/esx_lib/imports/callback/client.lua | 148 +++++++++++++++ [core]/esx_lib/imports/callback/server.lua | 127 +++++++++++++ [core]/esx_lib/imports/class/shared.lua | 35 ++++ [core]/esx_lib/imports/colors/client.lua | 10 + [core]/esx_lib/imports/overload/shared.lua | 66 +++++++ [core]/esx_lib/imports/require/shared.lua | 200 ++++++++++++++++++++ [core]/esx_lib/imports/string/shared.lua | 147 ++++++++++++++ [core]/esx_lib/imports/table/shared.lua | 133 +++++++++++++ [core]/esx_lib/imports/verify/shared.lua | 161 ++++++++++++++++ [core]/esx_lib/resource/callback/shared.lua | 67 +++++++ [core]/esx_lib/resource/init.lua | 38 ++++ [core]/esx_lib/resource/test/client.lua | 0 [core]/esx_lib/resource/test/server.lua | 0 15 files changed, 1162 insertions(+), 20 deletions(-) create mode 100644 [core]/esx_lib/imports/callback/client.lua create mode 100644 [core]/esx_lib/imports/callback/server.lua create mode 100644 [core]/esx_lib/imports/class/shared.lua create mode 100644 [core]/esx_lib/imports/colors/client.lua create mode 100644 [core]/esx_lib/imports/overload/shared.lua create mode 100644 [core]/esx_lib/imports/require/shared.lua create mode 100644 [core]/esx_lib/imports/string/shared.lua create mode 100644 [core]/esx_lib/imports/table/shared.lua create mode 100644 [core]/esx_lib/imports/verify/shared.lua create mode 100644 [core]/esx_lib/resource/callback/shared.lua create mode 100644 [core]/esx_lib/resource/init.lua create mode 100644 [core]/esx_lib/resource/test/client.lua create mode 100644 [core]/esx_lib/resource/test/server.lua 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 From 921fd13debee52ae035da5c1de2ebdcdb93a7122 Mon Sep 17 00:00:00 2001 From: zykem <86602828+Zykem@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:26:24 +0200 Subject: [PATCH 05/16] Create compat.lua --- [core]/es_extended/client/compat.lua | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 [core]/es_extended/client/compat.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua new file mode 100644 index 00000000..7fee5d96 --- /dev/null +++ b/[core]/es_extended/client/compat.lua @@ -0,0 +1,2 @@ +--[[ All functions outsourced from the Core to the lib will be stored here, e.g: +--]] From eee2b643e400a2690e83cb9d18adbb8b72d4d787 Mon Sep 17 00:00:00 2001 From: zykem <86602828+Zykem@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:26:58 +0200 Subject: [PATCH 06/16] Create compat.lua --- [core]/es_extended/server/compat.lua | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 [core]/es_extended/server/compat.lua diff --git a/[core]/es_extended/server/compat.lua b/[core]/es_extended/server/compat.lua new file mode 100644 index 00000000..dc345fcb --- /dev/null +++ b/[core]/es_extended/server/compat.lua @@ -0,0 +1,2 @@ +--[[ All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: +--]] From bd30d9f744c4b32f7260517625aa03991627175a Mon Sep 17 00:00:00 2001 From: zykem <86602828+Zykem@users.noreply.github.com> Date: Fri, 24 Oct 2025 22:27:15 +0200 Subject: [PATCH 07/16] Update compat.lua --- [core]/es_extended/client/compat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 7fee5d96..a5c9b030 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1,2 +1,2 @@ ---[[ All functions outsourced from the Core to the lib will be stored here, e.g: +--[[ All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: --]] From b7153d4ffa8d5efc20031c1bcb2b624f5902911a Mon Sep 17 00:00:00 2001 From: Stane034 Date: Sat, 25 Oct 2025 09:44:06 +0200 Subject: [PATCH 08/16] moved ESX.Streaming to esx_lib --- [core]/es_extended/client/compat.lua | 10 ++++++-- .../resource/streaming/client.lua} | 25 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) rename [core]/{es_extended/client/modules/streaming.lua => esx_lib/resource/streaming/client.lua} (71%) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index a5c9b030..8ca70408 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1,2 +1,8 @@ ---[[ All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: ---]] +--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: + +ESX.Streaming.RequestModel = xLib.requestModel +ESX.Streaming.RequestStreamedTextureDict = xLib.requestStreamedTextureDict +ESX.Streaming.RequestNamedPtfxAsset = xLib.requestNamedPtfxAsset +ESX.Streaming.RequestAnimSet = xLib.requestAnimSet +ESX.Streaming.RequestAnimDict = xLib.requestAnimDict +ESX.Streaming.RequestWeaponAsset = xLib.requestWeaponAsset diff --git a/[core]/es_extended/client/modules/streaming.lua b/[core]/esx_lib/resource/streaming/client.lua similarity index 71% rename from [core]/es_extended/client/modules/streaming.lua rename to [core]/esx_lib/resource/streaming/client.lua index 8614c4fb..168d5fe6 100644 --- a/[core]/es_extended/client/modules/streaming.lua +++ b/[core]/esx_lib/resource/streaming/client.lua @@ -1,9 +1,7 @@ -ESX.Streaming = {} - ---@param modelHash number | string ---@param cb? function ---@return number | nil -function ESX.Streaming.RequestModel(modelHash, cb) +xLib.requestModel = function(modelHash, cb) modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash) if not IsModelInCdimage(modelHash) then return end @@ -17,7 +15,7 @@ end ---@param textureDict string ---@param cb? function ---@return string | nil -function ESX.Streaming.RequestStreamedTextureDict(textureDict, cb) +xLib.requestStreamedTextureDict = function(textureDict, cb) RequestStreamedTextureDict(textureDict, false) while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end @@ -28,7 +26,7 @@ end ---@param assetName string ---@param cb? function ---@return string | nil -function ESX.Streaming.RequestNamedPtfxAsset(assetName, cb) +xLib.requestNamedPtfxAsset = function(assetName, cb) RequestNamedPtfxAsset(assetName) while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end @@ -39,7 +37,7 @@ end ---@param animSet string ---@param cb? function ---@return string | nil -function ESX.Streaming.RequestAnimSet(animSet, cb) +xLib.requestAnimSet = function(animSet, cb) RequestAnimSet(animSet) while not HasAnimSetLoaded(animSet) do Wait(500) end @@ -50,7 +48,7 @@ end ---@param animDict string ---@param cb? function ---@return string | nil -function ESX.Streaming.RequestAnimDict(animDict, cb) +xLib.requestAnimDict = function(animDict, cb) RequestAnimDict(animDict) while not HasAnimDictLoaded(animDict) do Wait(500) end @@ -61,10 +59,21 @@ end ---@param weaponHash number | string ---@param cb? function ---@return string | number | nil -function ESX.Streaming.RequestWeaponAsset(weaponHash, cb) +xLib.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.requestAudioBank = function(bankName, cb) + RequestAudioBank(bankName, false) + + while not RequestScriptAudioBank(bankName, false) do Wait(500) end + + return cb and cb(bankName) or bankName +end From 54a532baaf2ae53b13c2e188245195cbd314d1a6 Mon Sep 17 00:00:00 2001 From: Kremu <90266455+Kr3mu@users.noreply.github.com> Date: Sat, 25 Oct 2025 12:27:47 +0200 Subject: [PATCH 09/16] Quick fix. --- [core]/esx_lib/imports.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/[core]/esx_lib/imports.lua b/[core]/esx_lib/imports.lua index 9c0ba362..08207307 100644 --- a/[core]/esx_lib/imports.lua +++ b/[core]/esx_lib/imports.lua @@ -82,7 +82,7 @@ local function call(self, index, ...) if not module then local function method(...) - return exports[index](nil, ...) + return exports[LIB_NAME][index](nil, ...) end if not ... then @@ -115,4 +115,4 @@ local xLib = setmetatable({ -- Allows to xLib to be accessible in resource that imports a lib _ENV.xLib = xLib -_ENV.require = xLib.require \ No newline at end of file +_ENV.require = xLib.require From 27962a888fd24c7d81116344b257c07c82a5b3b8 Mon Sep 17 00:00:00 2001 From: Stane034 Date: Sat, 25 Oct 2025 18:53:09 +0200 Subject: [PATCH 10/16] made streaming table in xLib --- [core]/es_extended/client/compat.lua | 12 ++++++------ .../streaming/client.lua | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 13 deletions(-) rename [core]/esx_lib/{resource => imports}/streaming/client.lua (78%) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 8ca70408..604f63ea 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1,8 +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.requestModel -ESX.Streaming.RequestStreamedTextureDict = xLib.requestStreamedTextureDict -ESX.Streaming.RequestNamedPtfxAsset = xLib.requestNamedPtfxAsset -ESX.Streaming.RequestAnimSet = xLib.requestAnimSet -ESX.Streaming.RequestAnimDict = xLib.requestAnimDict -ESX.Streaming.RequestWeaponAsset = xLib.requestWeaponAsset +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 diff --git a/[core]/esx_lib/resource/streaming/client.lua b/[core]/esx_lib/imports/streaming/client.lua similarity index 78% rename from [core]/esx_lib/resource/streaming/client.lua rename to [core]/esx_lib/imports/streaming/client.lua index 168d5fe6..38f6479e 100644 --- a/[core]/esx_lib/resource/streaming/client.lua +++ b/[core]/esx_lib/imports/streaming/client.lua @@ -1,7 +1,9 @@ +xLib.streaming = {} + ---@param modelHash number | string ---@param cb? function ---@return number | nil -xLib.requestModel = function(modelHash, cb) +xLib.streaming.requestModel = function(modelHash, cb) modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash) if not IsModelInCdimage(modelHash) then return end @@ -15,7 +17,7 @@ end ---@param textureDict string ---@param cb? function ---@return string | nil -xLib.requestStreamedTextureDict = function(textureDict, cb) +xLib.streaming.requestStreamedTextureDict = function(textureDict, cb) RequestStreamedTextureDict(textureDict, false) while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end @@ -26,7 +28,7 @@ end ---@param assetName string ---@param cb? function ---@return string | nil -xLib.requestNamedPtfxAsset = function(assetName, cb) +xLib.streaming.requestNamedPtfxAsset = function(assetName, cb) RequestNamedPtfxAsset(assetName) while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end @@ -37,7 +39,7 @@ end ---@param animSet string ---@param cb? function ---@return string | nil -xLib.requestAnimSet = function(animSet, cb) +xLib.streaming.requestAnimSet = function(animSet, cb) RequestAnimSet(animSet) while not HasAnimSetLoaded(animSet) do Wait(500) end @@ -48,7 +50,7 @@ end ---@param animDict string ---@param cb? function ---@return string | nil -xLib.requestAnimDict = function(animDict, cb) +xLib.streaming.requestAnimDict = function(animDict, cb) RequestAnimDict(animDict) while not HasAnimDictLoaded(animDict) do Wait(500) end @@ -59,7 +61,7 @@ end ---@param weaponHash number | string ---@param cb? function ---@return string | number | nil -xLib.requestWeaponAsset = function(weaponHash, cb) +xLib.streaming.requestWeaponAsset = function(weaponHash, cb) RequestWeaponAsset(weaponHash, 31, 0) while not HasWeaponAssetLoaded(weaponHash) do Wait(500) end @@ -70,10 +72,13 @@ end ---@param bankName string ---@param cb? function ---@return string | nil -xLib.requestAudioBank = function(bankName, cb) +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 \ No newline at end of file From ad7375991d1f8df5051dd9b42b08eaab8c37eec7 Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Sat, 25 Oct 2025 19:17:56 +0200 Subject: [PATCH 11/16] refactor: Change comment in compat files --- [core]/es_extended/client/compat.lua | 3 +-- [core]/es_extended/server/compat.lua | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index a5c9b030..254148ac 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1,2 +1 @@ ---[[ All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: ---]] +--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: \ No newline at end of file diff --git a/[core]/es_extended/server/compat.lua b/[core]/es_extended/server/compat.lua index dc345fcb..cc4e3271 100644 --- a/[core]/es_extended/server/compat.lua +++ b/[core]/es_extended/server/compat.lua @@ -1,2 +1 @@ ---[[ All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: ---]] +--All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: \ No newline at end of file From 165c8530c3deaccfc93b932db3174e8dff8ecbdf Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Sat, 25 Oct 2025 19:18:03 +0200 Subject: [PATCH 12/16] feat: Add shared compat file --- [core]/es_extended/shared/compat.lua | 1 + 1 file changed, 1 insertion(+) create mode 100644 [core]/es_extended/shared/compat.lua diff --git a/[core]/es_extended/shared/compat.lua b/[core]/es_extended/shared/compat.lua new file mode 100644 index 00000000..68492380 --- /dev/null +++ b/[core]/es_extended/shared/compat.lua @@ -0,0 +1 @@ +--All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g: \ No newline at end of file From 0f4b8088a997f99f301c33f5386f5c4fb8a92032 Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Sat, 25 Oct 2025 19:18:54 +0200 Subject: [PATCH 13/16] refactor(es_extended/fxmanifest) Add compat files to manifest --- [core]/es_extended/fxmanifest.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 4171d44a..88f1b760 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -15,6 +15,7 @@ shared_scripts { 'shared/main.lua', 'shared/functions.lua', 'shared/modules/*.lua', + 'shared/compat.lua' } server_scripts { @@ -35,7 +36,9 @@ server_scripts { 'server/bridge/**/*.lua', 'server/modules/npwd.lua', - 'server/modules/createJob.lua' + 'server/modules/createJob.lua', + + 'server/compat.lua' } client_scripts { @@ -54,6 +57,8 @@ client_scripts { 'client/modules/interactions.lua', 'client/modules/scaleform.lua', 'client/modules/streaming.lua', + + 'shared/compat.lua' } ui_page { From d5f8feafae091d5c69c996100b845645b0dcd351 Mon Sep 17 00:00:00 2001 From: Stane034 Date: Sun, 26 Oct 2025 00:14:41 +0200 Subject: [PATCH 14/16] Moved Timeouts and Await to esx_lib --- [core]/es_extended/client/compat.lua | 3 ++ [core]/es_extended/server/compat.lua | 4 ++ [core]/es_extended/shared/functions.lua | 39 ------------------ .../imports/timeout/shared.lua} | 9 ++-- [core]/esx_lib/imports/waitFor/shared.lua | 41 +++++++++++++++++++ 5 files changed, 54 insertions(+), 42 deletions(-) rename [core]/{es_extended/shared/modules/timeout.lua => esx_lib/imports/timeout/shared.lua} (77%) create mode 100644 [core]/esx_lib/imports/waitFor/shared.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 604f63ea..f270d129 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -6,3 +6,6 @@ ESX.Streaming.RequestNamedPtfxAsset = xLib.streaming.requestNamedPtfxAsset ESX.Streaming.RequestAnimSet = xLib.streaming.requestAnimSet ESX.Streaming.RequestAnimDict = xLib.streaming.requestAnimDict ESX.Streaming.RequestWeaponAsset = xLib.streaming.requestWeaponAsset +ESX.SetTimeout = xLib.timeout.setTimeout +ESX.ClearTimeout = xLib.timeout.clearTimeout +ESX.Await = xLib.waitFor \ No newline at end of file diff --git a/[core]/es_extended/server/compat.lua b/[core]/es_extended/server/compat.lua index dc345fcb..6ad884de 100644 --- a/[core]/es_extended/server/compat.lua +++ b/[core]/es_extended/server/compat.lua @@ -1,2 +1,6 @@ --[[ All server-side 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 \ No newline at end of file diff --git a/[core]/es_extended/shared/functions.lua b/[core]/es_extended/shared/functions.lua index e8fe8a76..71e1390e 100644 --- a/[core]/es_extended/shared/functions.lua +++ b/[core]/es_extended/shared/functions.lua @@ -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 diff --git a/[core]/es_extended/shared/modules/timeout.lua b/[core]/esx_lib/imports/timeout/shared.lua similarity index 77% rename from [core]/es_extended/shared/modules/timeout.lua rename to [core]/esx_lib/imports/timeout/shared.lua index b0590d5a..c68eec04 100644 --- a/[core]/es_extended/shared/modules/timeout.lua +++ b/[core]/esx_lib/imports/timeout/shared.lua @@ -1,10 +1,12 @@ +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) local id = TimeoutCount + 1 SetTimeout(msec, function() @@ -12,7 +14,6 @@ ESX.SetTimeout = function(msec, cb) CancelledTimeouts[id] = nil return end - cb() end) @@ -23,6 +24,8 @@ end ---@param id number ---@return nil -ESX.ClearTimeout = function(id) +xLib.timeout.clearTimeout = function(id) CancelledTimeouts[id] = true end + +return xLib.timeout diff --git a/[core]/esx_lib/imports/waitFor/shared.lua b/[core]/esx_lib/imports/waitFor/shared.lua new file mode 100644 index 00000000..31da0bfd --- /dev/null +++ b/[core]/esx_lib/imports/waitFor/shared.lua @@ -0,0 +1,41 @@ + +---@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 + + 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 + +return xLib.waitFor \ No newline at end of file From fbd11c52fa0cb017084fc22139c9c4581f89f3a5 Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:34:48 +0100 Subject: [PATCH 15/16] refactor(lib/waitFor) Use verify instead of AssertType --- [core]/esx_lib/imports/waitFor/shared.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/[core]/esx_lib/imports/waitFor/shared.lua b/[core]/esx_lib/imports/waitFor/shared.lua index 31da0bfd..8202a1c6 100644 --- a/[core]/esx_lib/imports/waitFor/shared.lua +++ b/[core]/esx_lib/imports/waitFor/shared.lua @@ -10,13 +10,11 @@ xLib.waitFor = function(conditionFunc, errorMessage, timeoutMs) error("Timeout should be a positive number.") end - if not ESX.IsFunctionReference(conditionFunc) then - error("Condition Function should be a function reference.") - end + xLib.verify(conditionFunc, "function", true) -- 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.") + xLib.verify(errorMessage, "string", true) end local invokingResource = GetInvokingResource() From f4b951a173c5abde3f1cdacc9ff6e478ca443118 Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Sun, 26 Oct 2025 15:37:11 +0100 Subject: [PATCH 16/16] feat(lib/timeout) Type validation for cb function --- [core]/esx_lib/imports/timeout/shared.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/[core]/esx_lib/imports/timeout/shared.lua b/[core]/esx_lib/imports/timeout/shared.lua index c68eec04..e684f3a4 100644 --- a/[core]/esx_lib/imports/timeout/shared.lua +++ b/[core]/esx_lib/imports/timeout/shared.lua @@ -7,6 +7,8 @@ local CancelledTimeouts = {} ---@param cb function ---@return number xLib.timeout.setTimeout = function(msec, cb) + xLib.verify(cb, "function", true) + local id = TimeoutCount + 1 SetTimeout(msec, function()