From fe82f8c6c4f60d9bc0052dcc3e68bc7f18c1b98d Mon Sep 17 00:00:00 2001 From: Kr3mu Date: Thu, 28 Aug 2025 23:00:06 +0200 Subject: [PATCH 01/42] 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/42] =?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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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() From ae19c7cd253aadaec7eab0977d67e3f07c19bded Mon Sep 17 00:00:00 2001 From: SNAILX Date: Tue, 28 Oct 2025 18:29:36 +0100 Subject: [PATCH 17/42] Refactor keybind registration to use xLib.addKeybind --- [core]/es_extended/client/compat.lua | 4 +- [core]/es_extended/client/functions.lua | 16 --- [core]/es_extended/fxmanifest.lua | 4 +- [core]/esx_lib/imports/addKeybind/client.lua | 107 +++++++++++++++++++ 4 files changed, 112 insertions(+), 19 deletions(-) create mode 100644 [core]/esx_lib/imports/addKeybind/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 254148ac..6b1babe8 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1 +1,3 @@ ---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 +--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: + +ESX.RegisterInput = xLib.addKeybind \ No newline at end of file diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index 953296d2..2884bc93 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -245,22 +245,6 @@ function ESX.RefreshContext(...) return IsResourceFound('esx_context') and exports['esx_context']:Refresh(...) end ----@param command_name string The command name ----@param label string The label to show ----@param input_group string The input group ----@param key string The key to bind ----@param on_press function The function to call on press ----@param on_release? function The function to call on release -function ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release) - local command = on_release and '+' .. command_name or command_name - RegisterCommand(command, on_press, false) - Core.Input[command_name] = ESX.HashString(command) - if on_release then - RegisterCommand('-' .. command_name, on_release, false) - end - RegisterKeyMapping(command, label or '', input_group or 'keyboard', key or '') -end - ---@param menuType string ---@param open function The function to call on open ---@param close function The function to call on close diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 88f1b760..f4005de1 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -6,6 +6,7 @@ lua54 'yes' version '1.13.3' shared_scripts { + '@esx_lib/imports.lua', 'locale.lua', 'shared/config/main.lua', @@ -44,6 +45,7 @@ server_scripts { client_scripts { 'client/main.lua', 'client/functions.lua', + 'client/compat.lua', 'client/modules/wrapper.lua', 'client/modules/callback.lua', 'client/modules/adjustments.lua', @@ -57,8 +59,6 @@ client_scripts { 'client/modules/interactions.lua', 'client/modules/scaleform.lua', 'client/modules/streaming.lua', - - 'shared/compat.lua' } ui_page { diff --git a/[core]/esx_lib/imports/addKeybind/client.lua b/[core]/esx_lib/imports/addKeybind/client.lua new file mode 100644 index 00000000..8533c8f7 --- /dev/null +++ b/[core]/esx_lib/imports/addKeybind/client.lua @@ -0,0 +1,107 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@class KeybindProps +---@field name string +---@field description string +---@field defaultMapper? string (see: https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/) +---@field defaultKey? string +---@field disabled? boolean +---@field disable? fun(self: CKeybind, toggle: boolean) +---@field onPressed? fun(self: CKeybind) +---@field onReleased? fun(self: CKeybind) +---@field remove fun(self: CKeybind) +---@field [string] any + +---@class CKeybind : KeybindProps +---@field currentKey string +---@field disabled boolean +---@field isPressed boolean +---@field hash number +---@field getCurrentKey fun(): string +---@field isControlPressed fun(): boolean + +local keybinds = {} + +local IsPauseMenuActive = IsPauseMenuActive +local GetControlInstructionalButton = GetControlInstructionalButton + +local keybind_mt = { + disabled = false, + isPressed = false, + defaultKey = '', + defaultMapper = 'keyboard', +} + +function keybind_mt:__index(index) + return index == 'currentKey' and self:getCurrentKey() or keybind_mt[index] +end + +function keybind_mt:getCurrentKey() + return GetControlInstructionalButton(0, self.hash, true):sub(3) +end + +function keybind_mt:isControlPressed() + return self.isPressed +end + +function keybind_mt:disable(toggle) + self.disabled = toggle +end + +function keybind_mt:remove() + if not keybinds[self.name] then return end + keybinds[self.name] = nil +end + +---@param command_name string The keybind name +---@param label string The description to show +---@param input_group? string The input group (default: 'keyboard') +---@param key? string The default key +---@param on_press? function The function to call on press +---@param on_release? function The function to call on release +---@return CKeybind +function xLib.addKeybind(command_name, label, input_group, key, on_press, on_release) + + local data = { + name = command_name, + description = label, + defaultMapper = input_group or 'keyboard', + defaultKey = key or '', + onPressed = on_press, + onReleased = on_release, + hash = joaat('+' .. command_name) | 0x80000000 + } + + keybinds[data.name] = setmetatable(data, keybind_mt) + + RegisterCommand('+' .. data.name, function() + if data.disabled or IsPauseMenuActive() then return end + data.isPressed = true + if not keybinds[data.name] then return end + if data.onPressed then data:onPressed() end + end) + + RegisterCommand('-' .. data.name, function() + if data.disabled or IsPauseMenuActive() then return end + data.isPressed = false + if not keybinds[data.name] then return end + if data.onReleased then data:onReleased() end + end) + + RegisterKeyMapping('+' .. data.name, data.description, data.defaultMapper, data.defaultKey) + + SetTimeout(500, function() + TriggerEvent('chat:removeSuggestion', ('/+%s'):format(data.name)) + TriggerEvent('chat:removeSuggestion', ('/-%s'):format(data.name)) + end) + + return data +end + +return xLib.addKeybind \ No newline at end of file From fa52482720d756166cb86fef9ca89de170429977 Mon Sep 17 00:00:00 2001 From: SNAILX Date: Wed, 29 Oct 2025 01:54:53 +0100 Subject: [PATCH 18/42] Fix keybind command order for proper validation --- [core]/esx_lib/imports/addKeybind/client.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/[core]/esx_lib/imports/addKeybind/client.lua b/[core]/esx_lib/imports/addKeybind/client.lua index 8533c8f7..01233d11 100644 --- a/[core]/esx_lib/imports/addKeybind/client.lua +++ b/[core]/esx_lib/imports/addKeybind/client.lua @@ -81,16 +81,16 @@ function xLib.addKeybind(command_name, label, input_group, key, on_press, on_rel keybinds[data.name] = setmetatable(data, keybind_mt) RegisterCommand('+' .. data.name, function() + if not keybinds[data.name] then return end if data.disabled or IsPauseMenuActive() then return end data.isPressed = true - if not keybinds[data.name] then return end if data.onPressed then data:onPressed() end end) RegisterCommand('-' .. data.name, function() + if not keybinds[data.name] then return end if data.disabled or IsPauseMenuActive() then return end data.isPressed = false - if not keybinds[data.name] then return end if data.onReleased then data:onReleased() end end) From 7ebc92ec0fdfcff34e345c514568c783d71a9d6f Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:14:08 +0100 Subject: [PATCH 19/42] refactor(lib/keybind) Change args structure --- [core]/esx_lib/imports/addKeybind/client.lua | 23 ++++---------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/[core]/esx_lib/imports/addKeybind/client.lua b/[core]/esx_lib/imports/addKeybind/client.lua index 01233d11..65c4467c 100644 --- a/[core]/esx_lib/imports/addKeybind/client.lua +++ b/[core]/esx_lib/imports/addKeybind/client.lua @@ -15,7 +15,7 @@ ---@field disable? fun(self: CKeybind, toggle: boolean) ---@field onPressed? fun(self: CKeybind) ---@field onReleased? fun(self: CKeybind) ----@field remove fun(self: CKeybind) +---@field remove? fun(self: CKeybind) ---@field [string] any ---@class CKeybind : KeybindProps @@ -59,25 +59,10 @@ function keybind_mt:remove() keybinds[self.name] = nil end ----@param command_name string The keybind name ----@param label string The description to show ----@param input_group? string The input group (default: 'keyboard') ----@param key? string The default key ----@param on_press? function The function to call on press ----@param on_release? function The function to call on release +---@param data KeybindProps ---@return CKeybind -function xLib.addKeybind(command_name, label, input_group, key, on_press, on_release) - - local data = { - name = command_name, - description = label, - defaultMapper = input_group or 'keyboard', - defaultKey = key or '', - onPressed = on_press, - onReleased = on_release, - hash = joaat('+' .. command_name) | 0x80000000 - } - +function xLib.addKeybind(data) + data.hash = joaat("+" .. data.name) | 0x80000000 keybinds[data.name] = setmetatable(data, keybind_mt) RegisterCommand('+' .. data.name, function() From cb41916ad1a1018818df0fde95fbbdd13a25e139 Mon Sep 17 00:00:00 2001 From: zykem#0643 <86602828+Zykem@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:14:24 +0100 Subject: [PATCH 20/42] refactor(lib/addKeybind) Compat for old core structure --- [core]/es_extended/client/compat.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 6b1babe8..a173f77b 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1,3 +1,12 @@ --All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: -ESX.RegisterInput = xLib.addKeybind \ No newline at end of file +function ESX.RegisterInput(command_name, label, input_group, key, on_press, on_release) + return xLib.addKeybind({ + name = command_name, + description = label, + defaultMapper = input_group, + defaultKey = key, + onPressed = on_press, + onReleased = on_release + }) +end \ No newline at end of file From 3c20728860b5a9eefb549724695caccc704a5f20 Mon Sep 17 00:00:00 2001 From: Zuntie Date: Sun, 9 Nov 2025 02:44:10 +0100 Subject: [PATCH 21/42] feat(esx_lib): initial DUI addition --- [core]/esx_lib/imports/dui/client.lua | 118 ++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 [core]/esx_lib/imports/dui/client.lua diff --git a/[core]/esx_lib/imports/dui/client.lua b/[core]/esx_lib/imports/dui/client.lua new file mode 100644 index 00000000..613a55d3 --- /dev/null +++ b/[core]/esx_lib/imports/dui/client.lua @@ -0,0 +1,118 @@ +--[[ + https://github.com/overextended/ox_lib + + This file is licensed under LGPL-3.0 or higher + + Copyright © 2025 Linden +]] + +---@class DuiProperties +---@field url string +---@field width number +---@field height number +---@field debug? boolean + +---@class Dui +---@field private private { id: string, debug: boolean } +---@field url string +---@field duiObject number +---@field duiHandle string +---@field runtimeTxd number +---@field txdObject number +---@field dictName string +---@field txtName string +xLib.dui = xLib.class() + +---@type table +local duis = {} + +local currentId = 0 + +---@param data DuiProperties +function xLib.dui:constructor(data) + local time = GetGameTimer() + local id = ("%s_%s_%s"):format(cache.resource, time, currentId) + currentId = currentId + 1 + local dictName = ('ox_lib_dui_dict_%s'):format(id) + local txtName = ('ox_lib_dui_txt_%s'):format(id) + local duiObject = CreateDui(data.url, data.width, data.height) + local duiHandle = GetDuiHandle(duiObject) + local runtimeTxd = CreateRuntimeTxd(dictName) + local txdObject = CreateRuntimeTextureFromDuiHandle(runtimeTxd, txtName, duiHandle) + self.private.id = id + self.private.debug = data.debug or false + self.url = data.url + self.duiObject = duiObject + self.duiHandle = duiHandle + self.runtimeTxd = runtimeTxd + self.txdObject = txdObject + self.dictName = dictName + self.txtName = txtName + duis[id] = self + + if self.private.debug then + print(('Dui %s created'):format(id)) + end +end + +function xLib.dui:remove() + SetDuiUrl(self.duiObject, 'about:blank') + DestroyDui(self.duiObject) + duis[self.private.id] = nil + + if self.private.debug then + print(('Dui %s removed'):format(self.private.id)) + end +end + +---@param url string +function xLib.dui:setUrl(url) + self.url = url + SetDuiUrl(self.duiObject, url) + + if self.private.debug then + print(('Dui %s url set to %s'):format(self.private.id, url)) + end +end + +---@param message table +function xLib.dui:sendMessage(message) + SendDuiMessage(self.duiObject, json.encode(message)) + + if self.private.debug then + print(('Dui %s message sent with data :'):format(self.private.id), json.encode(message, { indent = true })) + end +end + +---@param x number +---@param y number +function xLib.dui:sendMouseMove(x, y) + SendDuiMouseMove(self.duiObject, x, y) +end + +---@param button 'left' | 'middle' | 'right' +function xLib.dui:sendMouseDown(button) + SendDuiMouseDown(self.duiObject, button) +end + +---@param button 'left' | 'middle' | 'right' +function xLib.dui:sendMouseUp(button) + SendDuiMouseUp(self.duiObject, button) +end + +---@param deltaX number +---@param deltaY number +function xLib.dui:sendMouseWheel(deltaX, deltaY) + SendDuiMouseWheel(self.duiObject, deltaY, deltaX) +end + + +AddEventHandler('onResourceStop', function(resourceName) + if cache.resource ~= resourceName then return end + + for _, dui in pairs(duis) do + dui:remove() + end +end) + +return xLib.dui From edcfa009fb11266fbd9b1128323ed538b1826725 Mon Sep 17 00:00:00 2001 From: Zuntie Date: Sun, 9 Nov 2025 02:49:24 +0100 Subject: [PATCH 22/42] feat(esx_lib): DUI development guidelines compliant --- [core]/esx_lib/imports/dui/client.lua | 44 +++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/[core]/esx_lib/imports/dui/client.lua b/[core]/esx_lib/imports/dui/client.lua index 613a55d3..4b8ffae1 100644 --- a/[core]/esx_lib/imports/dui/client.lua +++ b/[core]/esx_lib/imports/dui/client.lua @@ -13,7 +13,8 @@ ---@field debug? boolean ---@class Dui ----@field private private { id: string, debug: boolean } +---@field private_id string +---@field private_debug boolean ---@field url string ---@field duiObject number ---@field duiHandle string @@ -24,7 +25,7 @@ xLib.dui = xLib.class() ---@type table -local duis = {} +local Duis = {} local currentId = 0 @@ -33,14 +34,14 @@ function xLib.dui:constructor(data) local time = GetGameTimer() local id = ("%s_%s_%s"):format(cache.resource, time, currentId) currentId = currentId + 1 - local dictName = ('ox_lib_dui_dict_%s'):format(id) - local txtName = ('ox_lib_dui_txt_%s'):format(id) + local dictName = ("ox_lib_dui_dict_%s"):format(id) + local txtName = ("ox_lib_dui_txt_%s"):format(id) local duiObject = CreateDui(data.url, data.width, data.height) local duiHandle = GetDuiHandle(duiObject) local runtimeTxd = CreateRuntimeTxd(dictName) local txdObject = CreateRuntimeTextureFromDuiHandle(runtimeTxd, txtName, duiHandle) - self.private.id = id - self.private.debug = data.debug or false + self.private_id = id + self.private_debug = data.debug or false self.url = data.url self.duiObject = duiObject self.duiHandle = duiHandle @@ -48,20 +49,20 @@ function xLib.dui:constructor(data) self.txdObject = txdObject self.dictName = dictName self.txtName = txtName - duis[id] = self + Duis[id] = self - if self.private.debug then - print(('Dui %s created'):format(id)) + if self.private_debug then + print(("Dui %s created"):format(id)) end end function xLib.dui:remove() - SetDuiUrl(self.duiObject, 'about:blank') + SetDuiUrl(self.duiObject, "about:blank") DestroyDui(self.duiObject) - duis[self.private.id] = nil + Duis[self.private_id] = nil - if self.private.debug then - print(('Dui %s removed'):format(self.private.id)) + if self.private_debug then + print(("Dui %s removed"):format(self.private_id)) end end @@ -70,8 +71,8 @@ function xLib.dui:setUrl(url) self.url = url SetDuiUrl(self.duiObject, url) - if self.private.debug then - print(('Dui %s url set to %s'):format(self.private.id, url)) + if self.private_debug then + print(("Dui %s url set to %s"):format(self.private_id, url)) end end @@ -79,8 +80,8 @@ end function xLib.dui:sendMessage(message) SendDuiMessage(self.duiObject, json.encode(message)) - if self.private.debug then - print(('Dui %s message sent with data :'):format(self.private.id), json.encode(message, { indent = true })) + if self.private_debug then + print(("Dui %s message sent with data :"):format(self.private_id), json.encode(message, { indent = true })) end end @@ -90,12 +91,12 @@ function xLib.dui:sendMouseMove(x, y) SendDuiMouseMove(self.duiObject, x, y) end ----@param button 'left' | 'middle' | 'right' +---@param button "left" | "middle" | "right" function xLib.dui:sendMouseDown(button) SendDuiMouseDown(self.duiObject, button) end ----@param button 'left' | 'middle' | 'right' +---@param button "left" | "middle" | "right" function xLib.dui:sendMouseUp(button) SendDuiMouseUp(self.duiObject, button) end @@ -106,11 +107,10 @@ function xLib.dui:sendMouseWheel(deltaX, deltaY) SendDuiMouseWheel(self.duiObject, deltaY, deltaX) end - -AddEventHandler('onResourceStop', function(resourceName) +AddEventHandler("onResourceStop", function(resourceName) if cache.resource ~= resourceName then return end - for _, dui in pairs(duis) do + for _, dui in pairs(Duis) do dui:remove() end end) From 1a27c365b677ea1d19ced371505d240e37bd0143 Mon Sep 17 00:00:00 2001 From: Zuntie Date: Sun, 9 Nov 2025 17:44:52 +0100 Subject: [PATCH 23/42] fix(esx_lib): changed runtime texture names; tiny performance optimizaion. Working, Finished. --- [core]/esx_lib/imports/dui/client.lua | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/[core]/esx_lib/imports/dui/client.lua b/[core]/esx_lib/imports/dui/client.lua index 4b8ffae1..3fd9eb61 100644 --- a/[core]/esx_lib/imports/dui/client.lua +++ b/[core]/esx_lib/imports/dui/client.lua @@ -24,6 +24,8 @@ ---@field txtName string xLib.dui = xLib.class() +local resource = GetCurrentResourceName() + ---@type table local Duis = {} @@ -32,10 +34,10 @@ local currentId = 0 ---@param data DuiProperties function xLib.dui:constructor(data) local time = GetGameTimer() - local id = ("%s_%s_%s"):format(cache.resource, time, currentId) + local id = ("%s_%s_%s"):format(resource, time, currentId) currentId = currentId + 1 - local dictName = ("ox_lib_dui_dict_%s"):format(id) - local txtName = ("ox_lib_dui_txt_%s"):format(id) + local dictName = ("%s_dui_dict_%s"):format(resource, id) + local txtName = ("%s_lib_dui_txt_%s"):format(resource, id) local duiObject = CreateDui(data.url, data.width, data.height) local duiHandle = GetDuiHandle(duiObject) local runtimeTxd = CreateRuntimeTxd(dictName) @@ -108,11 +110,11 @@ function xLib.dui:sendMouseWheel(deltaX, deltaY) end AddEventHandler("onResourceStop", function(resourceName) - if cache.resource ~= resourceName then return end + if resource ~= resourceName then return end - for _, dui in pairs(Duis) do - dui:remove() - end + for id in next, Duis do + Duis[id]:remove() + end end) -return xLib.dui +return xLib.dui \ No newline at end of file From d9084186376045759dcd5ef4780c5c98cafeb625 Mon Sep 17 00:00:00 2001 From: SNAILX <180862994+SNAILX808@users.noreply.github.com> Date: Tue, 11 Nov 2025 00:57:39 +0100 Subject: [PATCH 24/42] Move-refactor-raycast-functions-esx_lib --- [core]/es_extended/client/compat.lua | 23 ++++++- [core]/es_extended/client/functions.lua | 21 ------- [core]/esx_lib/imports/raycast/client.lua | 77 +++++++++++++++++++++++ 3 files changed, 99 insertions(+), 22 deletions(-) create mode 100644 [core]/esx_lib/imports/raycast/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 254148ac..feef08bf 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1 +1,22 @@ ---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 +--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: + +ESX.Game.GetShapeTestResultSync = xLib.Raycast.GetShapeTestResult +ESX.Game.RaycastScreen = xLib.Raycast.FromScreen +ESX.Game.StartRaycasting = xLib.Raycast.Start +ESX.Game.StopRaycasting = function(raycast) + if raycast and raycast.active then + raycast:Stop() + end +end +ESX.Game.IsRaycastActive = function(raycast) + if raycast and raycast.active then + return raycast:IsActive() + end + return false +end +ESX.Game.GetRaycastResult = function(raycast) + if raycast and raycast.active then + return raycast.result + end + return nil +end \ No newline at end of file diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index 953296d2..5e9dbd3b 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -736,27 +736,6 @@ function ESX.Game.IsSpawnPointClear(coords, maxDistance) return #ESX.Game.GetVehiclesInArea(coords, maxDistance) == 0 end ----@param shape integer The shape to get the test result from ----@return boolean, table, table, integer, integer -function ESX.Game.GetShapeTestResultSync(shape) - local handle, hit, coords, normal, material, entity - repeat - handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape) - Wait(0) - until handle ~= 1 - return hit, coords, normal, material, entity -end - ----@param depth number The depth to raycast ----@vararg any The arguments to pass to the shape test ----@return table, boolean, table, table, integer, integer -function ESX.Game.RaycastScreen(depth, ...) - local world, normal = GetWorldCoordFromScreenCoord(.5, .5) - local origin = world + normal - local target = world + normal * depth - return target, ESX.Game.GetShapeTestResultSync(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...)) -end - ---@param entities table The entities to search through ---@param isPlayerEntities boolean Whether the entities are players ---@param coords? table | vector3 The coords to search from diff --git a/[core]/esx_lib/imports/raycast/client.lua b/[core]/esx_lib/imports/raycast/client.lua new file mode 100644 index 00000000..f0464be5 --- /dev/null +++ b/[core]/esx_lib/imports/raycast/client.lua @@ -0,0 +1,77 @@ +xLib.Raycast = {} +xLib.Raycast.__index = xLib.Raycast + +local unpack = table.unpack +local GetShapeTestResultIncludingMaterial = GetShapeTestResultIncludingMaterial +local StartShapeTestLosProbe = StartShapeTestLosProbe +local GetWorldCoordFromScreenCoord = GetWorldCoordFromScreenCoord + +---@param shape integer The shape test handle to wait for +---@return boolean, table, table, integer, integer +function xLib.Raycast.GetShapeTestResult(shape) + local handle, hit, coords, normal, material, entity + + repeat + handle, hit, coords, normal, material, entity = GetShapeTestResultIncludingMaterial(shape) + Wait(0) + until handle ~= 1 + + return hit, coords, normal, material, entity +end + +---@param depth number The raycast distance +---@vararg any Additional arguments to pass to shape test +---@return table, boolean, table, table, integer, integer +function xLib.Raycast.FromScreen(depth, ...) + local worldCoords, normalVector = GetWorldCoordFromScreenCoord(0.5, 0.5) + local origin = worldCoords + normalVector + local target = worldCoords + normalVector * depth + return target, xLib.Raycast.GetShapeTestResult(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...)) +end + +-- Start continuous raycasting +---@param depth number The raycast distance +---@vararg any Additional arguments to pass to shape test +function xLib.Raycast.Start(depth, ...) + local self = setmetatable({}, xLib.Raycast) + + self.refreshRate = refreshRate + self.depth = depth + self.args = {...} + + self.active = true + + self._thread = CreateThread(function() + while self.active do + local target, hit, coords, normal, material, entity = xLib.Raycast.FromScreen(self.depth, unpack(self.args)) + self.result = { + hit = hit, + coords = coords, + normal = normal, + material = material, + entity = entity, + } + Wait(1) + end + end) + + return self +end + +-- Stop raycasting +function xLib.Raycast:Stop() + if not self and not self.active then print('already stopped') return end + self.active = false + if self._thread then + self._thread = nil + end +end + +-- Check if the raycating is active +---@return boolean +function xLib.Raycast:IsActive() + if not self and not self.active then return end + return self.active +end + +return xLib.Raycast From 109a5c6038094048248bc2f708cbcd26f29cc22a Mon Sep 17 00:00:00 2001 From: SNAILX <180862994+SNAILX808@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:31:19 +0100 Subject: [PATCH 25/42] small fixes --- [core]/es_extended/client/compat.lua | 3 +++ [core]/esx_lib/imports/raycast/client.lua | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index feef08bf..a4b3cfce 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -3,17 +3,20 @@ ESX.Game.GetShapeTestResultSync = xLib.Raycast.GetShapeTestResult ESX.Game.RaycastScreen = xLib.Raycast.FromScreen ESX.Game.StartRaycasting = xLib.Raycast.Start +---@param raycast table The raycast object returned from ESX.Game.StartRaycasting ESX.Game.StopRaycasting = function(raycast) if raycast and raycast.active then raycast:Stop() end end +---@param raycast table The raycast object returned from ESX.Game.StartRaycasting ESX.Game.IsRaycastActive = function(raycast) if raycast and raycast.active then return raycast:IsActive() end return false end +---@param raycast table The raycast object returned from ESX.Game.StartRaycasting ESX.Game.GetRaycastResult = function(raycast) if raycast and raycast.active then return raycast.result diff --git a/[core]/esx_lib/imports/raycast/client.lua b/[core]/esx_lib/imports/raycast/client.lua index f0464be5..131fc79b 100644 --- a/[core]/esx_lib/imports/raycast/client.lua +++ b/[core]/esx_lib/imports/raycast/client.lua @@ -60,7 +60,7 @@ end -- Stop raycasting function xLib.Raycast:Stop() - if not self and not self.active then print('already stopped') return end + if not self and not self.active then return end self.active = false if self._thread then self._thread = nil From 9b9b4b3fa4ea73ed2c60849e101415db3665bfe3 Mon Sep 17 00:00:00 2001 From: SNAILX <180862994+SNAILX808@users.noreply.github.com> Date: Tue, 11 Nov 2025 05:52:38 +0100 Subject: [PATCH 26/42] Move-refactor-entity-functions-esx_lib --- [core]/es_extended/client/compat.lua | 6 +- [core]/es_extended/client/functions.lua | 82 ----------------------- [core]/esx_lib/imports/entity/client.lua | 85 ++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 83 deletions(-) create mode 100644 [core]/esx_lib/imports/entity/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 254148ac..96a57a55 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -1 +1,5 @@ ---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 +--All client-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: + +ESX.Game.GetClosestEntity = xLib.entity.closest +EnumerateEntitiesWithinDistance = xLib.entity.EnumerateWithinDistance +ESX.Game.Teleport = xLib.entity.Teleport \ No newline at end of file diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index 953296d2..419adc4e 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -473,26 +473,6 @@ function ESX.Game.GetPedMugshot(ped, transparent) return mugshot, GetPedheadshotTxdString(mugshot) end ----@param entity integer The entity to get the coords of ----@param coords table | vector3 | vector4 The coords to teleport the entity to ----@param cb? function The callback function -function ESX.Game.Teleport(entity, coords, cb) - - if DoesEntityExist(entity) then - RequestCollisionAtCoord(coords.x, coords.y, coords.z) - while not HasCollisionLoadedAroundEntity(entity) do - Wait(0) - end - - SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false) - SetEntityHeading(entity, coords.w or coords.heading or 0.0) - end - - if cb then - cb() - end -end - ---@param object integer | string The object to spawn ---@param coords table | vector3 The coords to spawn the object at ---@param cb? function The callback function @@ -689,32 +669,6 @@ function ESX.Game.GetClosestVehicle(coords, modelFilter) return ESX.Game.GetClosestEntity(ESX.Game.GetVehicles(), false, coords, modelFilter) end ----@param entities table The entities to search through ----@param isPlayerEntities boolean Whether the entities are players ----@param coords table | vector3 The coords to search from ----@param maxDistance number The max distance to search within ----@return table -local function EnumerateEntitiesWithinDistance(entities, isPlayerEntities, coords, maxDistance) - local nearbyEntities = {} - - if coords then - coords = vector3(coords.x, coords.y, coords.z) - else - local playerPed = ESX.PlayerData.ped - coords = GetEntityCoords(playerPed) - end - - for k, entity in pairs(entities) do - local distance = #(coords - GetEntityCoords(entity)) - - if distance <= maxDistance then - nearbyEntities[#nearbyEntities + 1] = isPlayerEntities and k or entity - end - end - - return nearbyEntities -end - ---@param coords table | vector3 The coords to search from ---@param maxDistance number The max distance to search within ---@return table @@ -757,42 +711,6 @@ function ESX.Game.RaycastScreen(depth, ...) return target, ESX.Game.GetShapeTestResultSync(StartShapeTestLosProbe(origin.x, origin.y, origin.z, target.x, target.y, target.z, ...)) end ----@param entities table The entities to search through ----@param isPlayerEntities boolean Whether the entities are players ----@param coords? table | vector3 The coords to search from ----@param modelFilter? table The model filter ----@return integer, integer -function ESX.Game.GetClosestEntity(entities, isPlayerEntities, coords, modelFilter) - local closestEntity, closestEntityDistance, filteredEntities = -1, -1, nil - - if coords then - coords = vector3(coords.x, coords.y, coords.z) - else - local playerPed = ESX.PlayerData.ped - coords = GetEntityCoords(playerPed) - end - - if modelFilter then - filteredEntities = {} - - for currentEntityIndex = 1, #entities do - if modelFilter[GetEntityModel(entities[currentEntityIndex])] then - filteredEntities[#filteredEntities + 1] = entities[currentEntityIndex] - end - end - end - - for k, entity in pairs(filteredEntities or entities) do - local distance = #(coords - GetEntityCoords(entity)) - - if closestEntityDistance == -1 or distance < closestEntityDistance then - closestEntity, closestEntityDistance = isPlayerEntities and k or entity, distance - end - end - - return closestEntity, closestEntityDistance -end - ---@return integer | nil, vector3 | nil function ESX.Game.GetVehicleInDirection() local _, hit, coords, _, _, entity = ESX.Game.RaycastScreen(5, 10, ESX.PlayerData.ped) diff --git a/[core]/esx_lib/imports/entity/client.lua b/[core]/esx_lib/imports/entity/client.lua new file mode 100644 index 00000000..6b861e3b --- /dev/null +++ b/[core]/esx_lib/imports/entity/client.lua @@ -0,0 +1,85 @@ +xLib.entity = {} + +---@param entities table The entities to search through +---@param isPlayerEntities boolean Whether the entities are players +---@param coords? table | vector3 The coords to search from +---@param modelFilter? table The model filter +---@return integer, integer +function xLib.entity.closest(entities, isPlayerEntities, coords, modelFilter) + local closestEntity, closestEntityDistance, filteredEntities = -1, -1, nil + + if coords then + coords = vector3(coords.x, coords.y, coords.z) + else + local playerPed = PlayerPedId() + coords = GetEntityCoords(playerPed) + end + + if modelFilter then + filteredEntities = {} + + for currentEntityIndex = 1, #entities do + if modelFilter[GetEntityModel(entities[currentEntityIndex])] then + filteredEntities[#filteredEntities + 1] = entities[currentEntityIndex] + end + end + end + + for k, entity in pairs(filteredEntities or entities) do + local distance = #(coords - GetEntityCoords(entity)) + + if closestEntityDistance == -1 or distance < closestEntityDistance then + closestEntity, closestEntityDistance = isPlayerEntities and k or entity, distance + end + end + + return closestEntity, closestEntityDistance +end + +---@param entities table The entities to search through +---@param isPlayerEntities boolean Whether the entities are players +---@param coords? table | vector3 The coords to search from +---@param maxDistance number The maximum distance +---@return table +function xLib.entity.EnumerateWithinDistance(entities, isPlayerEntities, coords, maxDistance) + local nearbyEntities = {} + + if coords then + coords = vector3(coords.x, coords.y, coords.z) + else + local playerPed = PlayerPedId() + coords = GetEntityCoords(playerPed) + end + + for k, entity in pairs(entities) do + local distance = #(coords - GetEntityCoords(entity)) + + if distance <= maxDistance then + nearbyEntities[#nearbyEntities + 1] = isPlayerEntities and k or entity + end + end + + return nearbyEntities +end + +---@param entity integer The entity to get the coords of +---@param coords table | vector3 | vector4 The coords to teleport the entity to +---@param cb? function The callback function +function xLib.entity.Teleport(entity, coords, cb) + + if DoesEntityExist(entity) then + RequestCollisionAtCoord(coords.x, coords.y, coords.z) + while not HasCollisionLoadedAroundEntity(entity) do + Wait(0) + end + + SetEntityCoords(entity, coords.x, coords.y, coords.z, false, false, false, false) + SetEntityHeading(entity, coords.w or coords.heading or 0.0) + end + + if cb then + cb() + end +end + +return xLib.entity \ No newline at end of file From d3868429ed02461809702712a648616fc2882852 Mon Sep 17 00:00:00 2001 From: SNAILX Date: Mon, 15 Jun 2026 02:08:30 +0100 Subject: [PATCH 27/42] Moved esx.table from the core to esx_lib --- [core]/es_extended/shared/compat.lua | 18 +- [core]/es_extended/shared/modules/table.lua | 241 -------------------- [core]/esx_lib/imports/table/shared.lua | 226 ++++++++++++++++++ 3 files changed, 243 insertions(+), 242 deletions(-) delete mode 100644 [core]/es_extended/shared/modules/table.lua diff --git a/[core]/es_extended/shared/compat.lua b/[core]/es_extended/shared/compat.lua index 68492380..182657d8 100644 --- a/[core]/es_extended/shared/compat.lua +++ b/[core]/es_extended/shared/compat.lua @@ -1 +1,17 @@ ---All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g: \ No newline at end of file +--All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g: +ESX.Table.SizeOf = xLib.table.size +ESX.Table.Set = xLib.table.set +ESX.Table.IndexOf = xLib.table.indexOf +ESX.Table.LastIndexOf = xLib.table.lastIndexOf +ESX.Table.Find = xLib.table.find +ESX.Table.FindIndex = xLib.table.findIndex +ESX.Table.Filter = xLib.table.filter +ESX.Table.Map = xLib.table.map +ESX.Table.Reverse = xLib.table.reverse +ESX.Table.Clone = xLib.table.clone +ESX.Table.Concat = xLib.table.concat +ESX.Table.Join = xLib.table.join +ESX.Table.TableContains = xLib.table.contains +ESX.Table.Sort = xLib.table.sort +ESX.Table.ToArray = xLib.table.toArray +ESX.Table.Wipe = xLib.table.wipe diff --git a/[core]/es_extended/shared/modules/table.lua b/[core]/es_extended/shared/modules/table.lua deleted file mode 100644 index ab4b147d..00000000 --- a/[core]/es_extended/shared/modules/table.lua +++ /dev/null @@ -1,241 +0,0 @@ -ESX.Table = {} - --- nil proof alternative to #table ----@param t table ----@return number -function ESX.Table.SizeOf(t) - local count = 0 - - for _, _ in pairs(t) do - count = count + 1 - end - - return count -end - ----@param t table ----@return table -function ESX.Table.Set(t) - local set = {} - for _, v in ipairs(t) do - set[v] = true - end - return set -end - ----@param t table ----@param value any ----@return number -function ESX.Table.IndexOf(t, value) - for i = 1, #t, 1 do - if t[i] == value then - return i - end - end - - return -1 -end - ----@param t table ----@param value any ----@return number -function ESX.Table.LastIndexOf(t, value) - for i = #t, 1, -1 do - if t[i] == value then - return i - end - end - - return -1 -end - ----@param t table ----@param cb function ----@return any -function ESX.Table.Find(t, cb) - for i = 1, #t, 1 do - if cb(t[i]) then - return t[i] - end - end - - return nil -end - ----@param t table ----@param cb function ----@return number -function ESX.Table.FindIndex(t, cb) - for i = 1, #t, 1 do - if cb(t[i]) then - return i - end - end - - return -1 -end - ----@param t table ----@param cb function ----@return table -function ESX.Table.Filter(t, cb) - local newTable = {} - - for i = 1, #t, 1 do - if cb(t[i]) then - newTable[#newTable + 1] = t[i] - end - end - - return newTable -end - ----@param t table ----@param cb function ----@return table -function ESX.Table.Map(t, cb) - local newTable = {} - - for i = 1, #t, 1 do - newTable[i] = cb(t[i], i) - end - - return newTable -end - ----@param t table ----@return table -function ESX.Table.Reverse(t) - local newTable = {} - - for i = #t, 1, -1 do - table.insert(newTable, t[i]) - end - - return newTable -end - ----@param t table ----@return table -function ESX.Table.Clone(t) - if type(t) ~= "table" then - return t - end - - local meta = getmetatable(t) - local target = {} - - for k, v in pairs(t) do - if type(v) == "table" then - target[k] = ESX.Table.Clone(v) - else - target[k] = v - end - end - - setmetatable(target, meta) - - return target -end - ----@param t1 table ----@param t2 table ----@return table -function ESX.Table.Concat(t1, t2) - local t3 = ESX.Table.Clone(t1) - - for i = 1, #t2, 1 do - table.insert(t3, t2[i]) - end - - return t3 -end - ----@param t table ----@param sep string ----@return string -function ESX.Table.Join(t, sep) - local str = "" - - for i = 1, #t, 1 do - if i > 1 then - str = str .. (sep or ",") - end - - str = str .. t[i] - end - - return str -end - --- Credits: https://github.com/JonasDev99/qb-garages/blob/b0335d67cb72a6b9ac60f62a87fb3946f5c2f33d/server/main.lua#L5 ----@param tab table ----@param val any ----@return boolean -function ESX.Table.TableContains(tab, val) - if type(val) == "table" then - for _, value in pairs(tab) do - if ESX.Table.TableContains(val, value) then - return true - end - end - return false - else - for _, value in pairs(tab) do - if value == val then - return true - end - end - end - return false -end - --- Credit: https://stackoverflow.com/a/15706820 --- Description: sort function for pairs ----@param t table ----@param order function ----@return function -function ESX.Table.Sort(t, order) - -- collect the keys - local keys = {} - - for k, _ in pairs(t) do - keys[#keys + 1] = k - end - - -- if order function given, sort by it by passing the table and keys a, b, - -- otherwise just sort the keys - if order then - table.sort(keys, function(a, b) - return order(t, a, b) - end) - else - table.sort(keys) - end - - -- return the iterator function - local i = 0 - - return function() - i = i + 1 - if keys[i] then - return keys[i], t[keys[i]] - end - end -end - ----@param t table ----@return Array -function ESX.Table.ToArray(t) - local array = {} - for _, v in pairs(t) do - array[#array + 1] = v - end - return array -end - ----@param t table ----@return table -function ESX.Table.Wipe(t) - return table.wipe(t) -end \ No newline at end of file diff --git a/[core]/esx_lib/imports/table/shared.lua b/[core]/esx_lib/imports/table/shared.lua index c3550717..ed49db65 100644 --- a/[core]/esx_lib/imports/table/shared.lua +++ b/[core]/esx_lib/imports/table/shared.lua @@ -130,4 +130,230 @@ function xLib.table.dump(tbl) end end +-- nil proof alternative to #table +---@param t table +---@return number +function xLib.table.sizeOf(t) + local count = 0 + + for _, _ in pairs(t) do + count = count + 1 + end + + return count +end + +---@param t table +---@return table +function xLib.table.set(t) + local set = {} + for _, v in ipairs(t) do + set[v] = true + end + return set +end + +---@param t table +---@param value any +---@return number +function xLib.table.indexOf(t, value) + for i = 1, #t, 1 do + if t[i] == value then + return i + end + end + + return -1 +end + +---@param t table +---@param value any +---@return number +function xLib.table.lastIndexOf(t, value) + for i = #t, 1, -1 do + if t[i] == value then + return i + end + end + + return -1 +end + +---@param t table +---@param cb function +---@return any +function xLib.table.find(t, cb) + for i = 1, #t, 1 do + if cb(t[i]) then + return t[i] + end + end + + return nil +end + +---@param t table +---@param cb function +---@return number +function xLib.table.findIndex(t, cb) + for i = 1, #t, 1 do + if cb(t[i]) then + return i + end + end + + return -1 +end + +---@param t table +---@param cb function +---@return table +function xLib.table.map(t, cb) + local newTable = {} + + for i = 1, #t, 1 do + newTable[i] = cb(t[i], i) + end + + return newTable +end + +---@param t table +---@return table +function xLib.table.reverse(t) + local newTable = {} + + for i = #t, 1, -1 do + table.insert(newTable, t[i]) + end + + return newTable +end + +---@param t table +---@return table +function xLib.table.clone(t) + if type(t) ~= "table" then + return t + end + + local meta = getmetatable(t) + local target = {} + + for k, v in pairs(t) do + if type(v) == "table" then + target[k] = xLib.table.clone(v) + else + target[k] = v + end + end + + setmetatable(target, meta) + + return target +end + +---@param t1 table +---@param t2 table +---@return table +function xLib.table.concat(t1, t2) + local t3 = xLib.table.clone(t1) + + for i = 1, #t2, 1 do + table.insert(t3, t2[i]) + end + + return t3 +end + +---@param t table +---@param sep string +---@return string +function xLib.table.join(t, sep) + local str = "" + + for i = 1, #t, 1 do + if i > 1 then + str = str .. (sep or ",") + end + + str = str .. t[i] + end + + return str +end + +-- Credits: https://github.com/JonasDev99/qb-garages/blob/b0335d67cb72a6b9ac60f62a87fb3946f5c2f33d/server/main.lua#L5 +---@param tab table +---@param val any +---@return boolean +function xLib.table.contains(tab, val) + if type(val) == "table" then + for _, value in pairs(tab) do + if xLib.table.contains(val, value) then + return true + end + end + return false + else + for _, value in pairs(tab) do + if value == val then + return true + end + end + end + return false +end + +-- Credit: https://stackoverflow.com/a/15706820 +-- Description: sort function for pairs +---@param t table +---@param order function +---@return function +function xLib.table.sort(t, order) + -- collect the keys + local keys = {} + + for k, _ in pairs(t) do + keys[#keys + 1] = k + end + + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + if order then + table.sort(keys, function(a, b) + return order(t, a, b) + end) + else + table.sort(keys) + end + + -- return the iterator function + local i = 0 + + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +---@param t table +---@return Array +function xLib.table.toArray(t) + local array = {} + for _, v in pairs(t) do + array[#array + 1] = v + end + return array +end + +---@param t table +---@return table +function xLib.table.wipe(t) + return table.wipe(t) +end + + return xLib.table From f4af0d90bd42b7c7ba3ef14a80cce6fc7d13a189 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 28/42] feat(esx_lib/streaming): move streaming request helpers to xLib --- [core]/es_extended/client/compat.lua | 9 ++ .../es_extended/client/modules/streaming.lua | 70 --------------- [core]/es_extended/fxmanifest.lua | 1 - [core]/esx_lib/imports/streaming/client.lua | 86 +++++++++++++++++++ 4 files changed, 95 insertions(+), 71 deletions(-) delete mode 100644 [core]/es_extended/client/modules/streaming.lua create mode 100644 [core]/esx_lib/imports/streaming/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index ea60dc70..c92dd193 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -37,3 +37,12 @@ end ESX.Game.GetClosestEntity = xLib.entity.closest EnumerateEntitiesWithinDistance = xLib.entity.EnumerateWithinDistance ESX.Game.Teleport = xLib.entity.Teleport + +ESX.Streaming = { + RequestModel = xLib.streaming.requestModel, + RequestStreamedTextureDict = xLib.streaming.requestStreamedTextureDict, + RequestNamedPtfxAsset = xLib.streaming.requestNamedPtfxAsset, + RequestAnimSet = xLib.streaming.requestAnimSet, + RequestAnimDict = xLib.streaming.requestAnimDict, + RequestWeaponAsset = xLib.streaming.requestWeaponAsset, +} diff --git a/[core]/es_extended/client/modules/streaming.lua b/[core]/es_extended/client/modules/streaming.lua deleted file mode 100644 index 8614c4fb..00000000 --- a/[core]/es_extended/client/modules/streaming.lua +++ /dev/null @@ -1,70 +0,0 @@ -ESX.Streaming = {} - ----@param modelHash number | string ----@param cb? function ----@return number | nil -function ESX.Streaming.RequestModel(modelHash, cb) - modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash) - - if not IsModelInCdimage(modelHash) then return end - - RequestModel(modelHash) - while not HasModelLoaded(modelHash) do Wait(500) end - - return cb and cb(modelHash) or modelHash -end - ----@param textureDict string ----@param cb? function ----@return string | nil -function ESX.Streaming.RequestStreamedTextureDict(textureDict, cb) - RequestStreamedTextureDict(textureDict, false) - - while not HasStreamedTextureDictLoaded(textureDict) do Wait(500) end - - return cb and cb(textureDict) or textureDict -end - ----@param assetName string ----@param cb? function ----@return string | nil -function ESX.Streaming.RequestNamedPtfxAsset(assetName, cb) - RequestNamedPtfxAsset(assetName) - - while not HasNamedPtfxAssetLoaded(assetName) do Wait(500) end - - return cb and cb(assetName) or assetName -end - ----@param animSet string ----@param cb? function ----@return string | nil -function ESX.Streaming.RequestAnimSet(animSet, cb) - RequestAnimSet(animSet) - - while not HasAnimSetLoaded(animSet) do Wait(500) end - - return cb and cb(animSet) or animSet -end - ----@param animDict string ----@param cb? function ----@return string | nil -function ESX.Streaming.RequestAnimDict(animDict, cb) - RequestAnimDict(animDict) - - while not HasAnimDictLoaded(animDict) do Wait(500) end - - return cb and cb(animDict) or animDict -end - ----@param weaponHash number | string ----@param cb? function ----@return string | number | nil -function ESX.Streaming.RequestWeaponAsset(weaponHash, cb) - RequestWeaponAsset(weaponHash, 31, 0) - - while not HasWeaponAssetLoaded(weaponHash) do Wait(500) end - - return cb and cb(weaponHash) or weaponHash -end diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index f4005de1..d8c2112b 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -58,7 +58,6 @@ client_scripts { 'client/modules/npwd.lua', 'client/modules/interactions.lua', 'client/modules/scaleform.lua', - 'client/modules/streaming.lua', } ui_page { diff --git a/[core]/esx_lib/imports/streaming/client.lua b/[core]/esx_lib/imports/streaming/client.lua new file mode 100644 index 00000000..db392e17 --- /dev/null +++ b/[core]/esx_lib/imports/streaming/client.lua @@ -0,0 +1,86 @@ +---@class streaminglib +xLib.streaming = {} + +---@param modelHash number | string +---@param cb? function +---@return number | nil +function xLib.streaming.requestModel(modelHash, cb) + modelHash = type(modelHash) == "number" and modelHash or joaat(modelHash) + + if not IsModelInCdimage(modelHash) then return end + + RequestModel(modelHash) + + while not HasModelLoaded(modelHash) do + Wait(500) + end + + return cb and cb(modelHash) or modelHash +end + +---@param textureDict string +---@param cb? function +---@return string | nil +function xLib.streaming.requestStreamedTextureDict(textureDict, cb) + RequestStreamedTextureDict(textureDict, false) + + while not HasStreamedTextureDictLoaded(textureDict) do + Wait(500) + end + + return cb and cb(textureDict) or textureDict +end + +---@param assetName string +---@param cb? function +---@return string | nil +function xLib.streaming.requestNamedPtfxAsset(assetName, cb) + RequestNamedPtfxAsset(assetName) + + while not HasNamedPtfxAssetLoaded(assetName) do + Wait(500) + end + + return cb and cb(assetName) or assetName +end + +---@param animSet string +---@param cb? function +---@return string | nil +function xLib.streaming.requestAnimSet(animSet, cb) + RequestAnimSet(animSet) + + while not HasAnimSetLoaded(animSet) do + Wait(500) + end + + return cb and cb(animSet) or animSet +end + +---@param animDict string +---@param cb? function +---@return string | nil +function xLib.streaming.requestAnimDict(animDict, cb) + RequestAnimDict(animDict) + + while not HasAnimDictLoaded(animDict) do + Wait(500) + end + + return cb and cb(animDict) or animDict +end + +---@param weaponHash number | string +---@param cb? function +---@return string | number | nil +function xLib.streaming.requestWeaponAsset(weaponHash, cb) + RequestWeaponAsset(weaponHash, 31, 0) + + while not HasWeaponAssetLoaded(weaponHash) do + Wait(500) + end + + return cb and cb(weaponHash) or weaponHash +end + +return xLib.streaming From 6a721819925f99a5f76e179686b62645684dd9a1 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 29/42] feat(esx_lib/scaleform): move scaleform helpers to xLib --- [core]/es_extended/client/compat.lua | 11 +++ [core]/es_extended/fxmanifest.lua | 1 - .../imports/scaleform/client.lua} | 70 ++++++++++++------- 3 files changed, 56 insertions(+), 26 deletions(-) rename [core]/{es_extended/client/modules/scaleform.lua => esx_lib/imports/scaleform/client.lua} (61%) diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index c92dd193..51f8b986 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -46,3 +46,14 @@ ESX.Streaming = { RequestAnimDict = xLib.streaming.requestAnimDict, RequestWeaponAsset = xLib.streaming.requestWeaponAsset, } + +ESX.Scaleform = { + ShowFreemodeMessage = xLib.scaleform.showFreemodeMessage, + ShowBreakingNews = xLib.scaleform.showBreakingNews, + ShowPopupWarning = xLib.scaleform.showPopupWarning, + ShowTrafficMovie = xLib.scaleform.showTrafficMovie, + Utils = { + RequestScaleformMovie = xLib.scaleform.utils.requestScaleformMovie, + RunMethod = xLib.scaleform.utils.runMethod, + }, +} diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index d8c2112b..3d2e9e2c 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -57,7 +57,6 @@ client_scripts { 'client/modules/death.lua', 'client/modules/npwd.lua', 'client/modules/interactions.lua', - 'client/modules/scaleform.lua', } ui_page { diff --git a/[core]/es_extended/client/modules/scaleform.lua b/[core]/esx_lib/imports/scaleform/client.lua similarity index 61% rename from [core]/es_extended/client/modules/scaleform.lua rename to [core]/esx_lib/imports/scaleform/client.lua index 0662f9c3..e5729ced 100644 --- a/[core]/es_extended/client/modules/scaleform.lua +++ b/[core]/esx_lib/imports/scaleform/client.lua @@ -1,22 +1,12 @@ -ESX.Scaleform = {} -ESX.Scaleform.Utils = {} +---@class scaleformlib +xLib.scaleform = {} +xLib.scaleform.utils = {} -function ESX.Scaleform.ShowFreemodeMessage(title, msg, sec) - local scaleform = ESX.Scaleform.Utils.RunMethod("MP_BIG_MESSAGE_FREEMODE", "SHOW_SHARD_WASTED_MP_MESSAGE", false, title, msg) - - local endTime = GetGameTimer() + (sec * 1000) - while GetGameTimer() < endTime do - Wait(0) - DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) - end - - SetScaleformMovieAsNoLongerNeeded(scaleform) -end - -function ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec) - local scaleform = ESX.Scaleform.Utils.RunMethod("BREAKING_NEWS", "SET_TEXT", false, msg, bottom) - ESX.Scaleform.Utils.RunMethod(scaleform, "SET_SCROLL_TEXT", false, 0, 0, title) - ESX.Scaleform.Utils.RunMethod(scaleform, "DISPLAY_SCROLL_TEXT", false, 0, 0) +---@param title string +---@param msg string +---@param sec number +function xLib.scaleform.showFreemodeMessage(title, msg, sec) + local scaleform = xLib.scaleform.utils.runMethod("MP_BIG_MESSAGE_FREEMODE", "SHOW_SHARD_WASTED_MP_MESSAGE", false, title, msg) local endTime = GetGameTimer() + (sec * 1000) while GetGameTimer() < endTime do @@ -27,8 +17,14 @@ function ESX.Scaleform.ShowBreakingNews(title, msg, bottom, sec) SetScaleformMovieAsNoLongerNeeded(scaleform) end -function ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec) - local scaleform = ESX.Scaleform.Utils.RunMethod("POPUP_WARNING", "SHOW_POPUP_WARNING", false, 500.0, title, msg, bottom, true) +---@param title string +---@param msg string +---@param bottom string +---@param sec number +function xLib.scaleform.showBreakingNews(title, msg, bottom, sec) + local scaleform = xLib.scaleform.utils.runMethod("BREAKING_NEWS", "SET_TEXT", false, msg, bottom) + xLib.scaleform.utils.runMethod(scaleform, "SET_SCROLL_TEXT", false, 0, 0, title) + xLib.scaleform.utils.runMethod(scaleform, "DISPLAY_SCROLL_TEXT", false, 0, 0) local endTime = GetGameTimer() + (sec * 1000) while GetGameTimer() < endTime do @@ -39,8 +35,12 @@ function ESX.Scaleform.ShowPopupWarning(title, msg, bottom, sec) SetScaleformMovieAsNoLongerNeeded(scaleform) end -function ESX.Scaleform.ShowTrafficMovie(sec) - local scaleform = ESX.Scaleform.Utils.RunMethod("TRAFFIC_CAM", "PLAY_CAM_MOVIE", false) +---@param title string +---@param msg string +---@param bottom string +---@param sec number +function xLib.scaleform.showPopupWarning(title, msg, bottom, sec) + local scaleform = xLib.scaleform.utils.runMethod("POPUP_WARNING", "SHOW_POPUP_WARNING", false, 500.0, title, msg, bottom, true) local endTime = GetGameTimer() + (sec * 1000) while GetGameTimer() < endTime do @@ -51,7 +51,22 @@ function ESX.Scaleform.ShowTrafficMovie(sec) SetScaleformMovieAsNoLongerNeeded(scaleform) end -function ESX.Scaleform.Utils.RequestScaleformMovie(movie) +---@param sec number +function xLib.scaleform.showTrafficMovie(sec) + local scaleform = xLib.scaleform.utils.runMethod("TRAFFIC_CAM", "PLAY_CAM_MOVIE", false) + + local endTime = GetGameTimer() + (sec * 1000) + while GetGameTimer() < endTime do + Wait(0) + DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0) + end + + SetScaleformMovieAsNoLongerNeeded(scaleform) +end + +---@param movie string +---@return number +function xLib.scaleform.utils.requestScaleformMovie(movie) local scaleform = RequestScaleformMovie(movie) while not HasScaleformMovieLoaded(scaleform) do @@ -68,8 +83,11 @@ end ---@param returnValue? boolean # Whether to return the value from the method ---@param ... number|string|boolean # Arguments to pass to the method ---@return number, number? # The scaleform handle, and the return value if `returnValue` is true -function ESX.Scaleform.Utils.RunMethod(scaleform, methodName, returnValue, ...) - scaleform = type(scaleform) == "number" and scaleform or ESX.Scaleform.Utils.RequestScaleformMovie(scaleform) +function xLib.scaleform.utils.runMethod(scaleform, methodName, returnValue, ...) + if type(scaleform) ~= "number" then + scaleform = xLib.scaleform.utils.requestScaleformMovie(scaleform) + end + BeginScaleformMovieMethod(scaleform, methodName) local args = { ... } @@ -97,3 +115,5 @@ function ESX.Scaleform.Utils.RunMethod(scaleform, methodName, returnValue, ...) return scaleform end + +return xLib.scaleform From ca278be1daa6436e4d1af90d2c53c809cfc98784 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 30/42] feat(esx_lib/points): move points and Point class to xLib Integrated fixes during the move: - monotonic handle counter (handleCount) instead of count+1; no handle collision after a point is removed - unified single proximity loop: per-frame inside() plus a 500ms enter/leave scan, adaptive Wait(next(insidePoints) and 0 or 500), replacing the previous two separate loops - emptiness detected via next() instead of # on a sparse-keyed table Non-portable substitutions documented: - ESX.PlayerData.ped -> PlayerPedId() (ESX.PlayerData is not available in the lib VM) - GetGameTimer() used to gate the 500ms scan within the single loop --- [core]/es_extended/client/compat.lua | 6 ++ [core]/es_extended/client/imports/point.lua | 53 ---------- [core]/es_extended/client/modules/points.lua | 54 ---------- [core]/es_extended/fxmanifest.lua | 1 - [core]/es_extended/imports.lua | 2 +- [core]/esx_lib/imports/point/client.lua | 52 ++++++++++ [core]/esx_lib/imports/points/client.lua | 100 +++++++++++++++++++ 7 files changed, 159 insertions(+), 109 deletions(-) delete mode 100644 [core]/es_extended/client/imports/point.lua delete mode 100644 [core]/es_extended/client/modules/points.lua create mode 100644 [core]/esx_lib/imports/point/client.lua create mode 100644 [core]/esx_lib/imports/points/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 51f8b986..02948a23 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -57,3 +57,9 @@ ESX.Scaleform = { RunMethod = xLib.scaleform.utils.runMethod, }, } + +ESX.CreatePointInternal = xLib.points.create +ESX.RemovePointInternal = xLib.points.remove +ESX.HidePointInternal = xLib.points.hide +StartPointsLoop = xLib.points.startLoop +ESX.Point = xLib.point diff --git a/[core]/es_extended/client/imports/point.lua b/[core]/es_extended/client/imports/point.lua deleted file mode 100644 index afe42955..00000000 --- a/[core]/es_extended/client/imports/point.lua +++ /dev/null @@ -1,53 +0,0 @@ -local Point = ESX.Class() - -local nearby, loop = {}, nil - -function Point:constructor(properties) - self.coords = properties.coords - self.hidden = properties.hidden - self.enter = properties.enter - self.leave = properties.leave - self.inside = properties.inside - self.handle = ESX.CreatePointInternal(properties.coords, properties.distance, properties.hidden, function() - nearby[self.handle] = self - if self.enter then - self:enter() - end - if not loop then - loop = true - CreateThread(function() - while loop do - local coords = GetEntityCoords(ESX.PlayerData.ped) - for _, point in pairs(nearby) do - if point.inside then - point:inside(#(coords - point.coords)) - end - end - Wait(0) - end - end) - end - end, function() - nearby[self.handle] = nil - if self.leave then - self:leave() - end - if #nearby == 0 then - loop = false - end - end) -end - -function Point:delete() - ESX.RemovePointInternal(self.handle) -end - -function Point:toggle(hidden) - if hidden == nil then - hidden = not self.hidden - end - self.hidden = hidden - ESX.HidePointInternal(self.handle, hidden) -end - -return Point \ No newline at end of file diff --git a/[core]/es_extended/client/modules/points.lua b/[core]/es_extended/client/modules/points.lua deleted file mode 100644 index bbcaf8a8..00000000 --- a/[core]/es_extended/client/modules/points.lua +++ /dev/null @@ -1,54 +0,0 @@ -local points = {} - -function ESX.CreatePointInternal(coords, distance, hidden, enter, leave) - local point = { - coords = coords, - distance = distance, - hidden = hidden, - enter = enter, - leave = leave, - resource = GetInvokingResource() - } - local handle = ESX.Table.SizeOf(points) + 1 - points[handle] = point - return handle -end - -function ESX.RemovePointInternal(handle) - points[handle] = nil -end - -function ESX.HidePointInternal(handle, hidden) - if points[handle] then - points[handle].hidden = hidden - end -end - -function StartPointsLoop() - CreateThread(function() - while true do - local coords = GetEntityCoords(ESX.PlayerData.ped) - for handle, point in pairs(points) do - if not point.hidden and #(coords - point.coords) <= point.distance then - if not point.nearby then - points[handle].nearby = true - points[handle].enter() - end - elseif point.nearby then - points[handle].nearby = false - points[handle].leave() - end - end - Wait(500) - end - end) -end - - -AddEventHandler('onResourceStop', function(resource) - for handle, point in pairs(points) do - if point.resource == resource then - points[handle] = nil - end - end -end) \ No newline at end of file diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index 3d2e9e2c..bf9cea0b 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -49,7 +49,6 @@ client_scripts { 'client/modules/wrapper.lua', 'client/modules/callback.lua', 'client/modules/adjustments.lua', - 'client/modules/points.lua', 'client/modules/events.lua', diff --git a/[core]/es_extended/imports.lua b/[core]/es_extended/imports.lua index 95797ba6..6ca908ee 100644 --- a/[core]/es_extended/imports.lua +++ b/[core]/es_extended/imports.lua @@ -87,7 +87,7 @@ if not IsDuplicityVersion() then -- Only register this event for the client end) end - local external = { { "Class", "class.lua" }, { "Point", "point.lua" } } + local external = { { "Class", "class.lua" } } for i = 1, #external do local module = external[i] local path = string.format("client/imports/%s", module[2]) diff --git a/[core]/esx_lib/imports/point/client.lua b/[core]/esx_lib/imports/point/client.lua new file mode 100644 index 00000000..3ffb0063 --- /dev/null +++ b/[core]/esx_lib/imports/point/client.lua @@ -0,0 +1,52 @@ +---@class xPoint +---@field coords vector3 +---@field hidden? boolean +---@field enter? function +---@field leave? function +---@field inside? function +---@field handle integer +xLib.point = xLib.class() + +---@param properties table coords, distance, hidden, enter, leave, inside +function xLib.point:constructor(properties) + self.coords = properties.coords + self.hidden = properties.hidden + self.enter = properties.enter + self.leave = properties.leave + self.inside = properties.inside + + self.handle = xLib.points.create( + properties.coords, + properties.distance, + properties.hidden, + function() + if self.enter then + self:enter() + end + end, + function() + if self.leave then + self:leave() + end + end, + properties.inside and function(dist) + self:inside(dist) + end or nil + ) +end + +function xLib.point:delete() + xLib.points.remove(self.handle) +end + +---@param hidden? boolean +function xLib.point:toggle(hidden) + if hidden == nil then + hidden = not self.hidden + end + + self.hidden = hidden + xLib.points.hide(self.handle, hidden) +end + +return xLib.point diff --git a/[core]/esx_lib/imports/points/client.lua b/[core]/esx_lib/imports/points/client.lua new file mode 100644 index 00000000..7fd04eb1 --- /dev/null +++ b/[core]/esx_lib/imports/points/client.lua @@ -0,0 +1,100 @@ +---@class pointslib +xLib.points = {} + +local points = {} +local insidePoints = {} +local handleCount = 0 + +---@param coords vector3 +---@param distance number +---@param hidden? boolean +---@param enter function +---@param leave function +---@param inside? function +---@return integer handle +function xLib.points.create(coords, distance, hidden, enter, leave, inside) + handleCount = handleCount + 1 + local handle = handleCount + + points[handle] = { + coords = coords, + distance = distance, + hidden = hidden, + enter = enter, + leave = leave, + inside = inside, + resource = GetInvokingResource() + } + + return handle +end + +---@param handle integer +function xLib.points.remove(handle) + points[handle] = nil + insidePoints[handle] = nil +end + +---@param handle integer +---@param hidden boolean +function xLib.points.hide(handle, hidden) + if points[handle] then + points[handle].hidden = hidden + end +end + +function xLib.points.startLoop() + CreateThread(function() + local lastScan = 0 + + while true do + local coords = GetEntityCoords(PlayerPedId()) + + for _, point in pairs(insidePoints) do + point.inside(#(coords - point.coords)) + end + + local now = GetGameTimer() + if now - lastScan >= 500 then + lastScan = now + + for handle, point in pairs(points) do + if not point.hidden and #(coords - point.coords) <= point.distance then + if not point.nearby then + point.nearby = true + + if point.enter then + point.enter() + end + + if point.inside then + insidePoints[handle] = point + end + end + elseif point.nearby then + point.nearby = false + + if point.leave then + point.leave() + end + + insidePoints[handle] = nil + end + end + end + + Wait(next(insidePoints) and 0 or 500) + end + end) +end + +AddEventHandler("onResourceStop", function(resource) + for handle, point in pairs(points) do + if point.resource == resource then + points[handle] = nil + insidePoints[handle] = nil + end + end +end) + +return xLib.points From 3fd3bf7877f6b04cec2206dd066f3531fc699d58 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 31/42] feat(esx_lib/interactions): move interactions to xLib --- [core]/es_extended/client/compat.lua | 4 ++ .../client/modules/interactions.lua | 38 ------------- [core]/es_extended/fxmanifest.lua | 1 - .../esx_lib/imports/interactions/client.lua | 54 +++++++++++++++++++ 4 files changed, 58 insertions(+), 39 deletions(-) delete mode 100644 [core]/es_extended/client/modules/interactions.lua create mode 100644 [core]/esx_lib/imports/interactions/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index 02948a23..cca17842 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -63,3 +63,7 @@ ESX.RemovePointInternal = xLib.points.remove ESX.HidePointInternal = xLib.points.hide StartPointsLoop = xLib.points.startLoop ESX.Point = xLib.point + +ESX.RegisterInteraction = xLib.interactions.register +ESX.RemoveInteraction = xLib.interactions.remove +ESX.GetInteractKey = xLib.interactions.getInteractKey diff --git a/[core]/es_extended/client/modules/interactions.lua b/[core]/es_extended/client/modules/interactions.lua deleted file mode 100644 index 0a1a31fe..00000000 --- a/[core]/es_extended/client/modules/interactions.lua +++ /dev/null @@ -1,38 +0,0 @@ -local interactions = {} -local pressedInteractions = {} - -function ESX.RemoveInteraction(name) - if not interactions[name] then return end - interactions[name] = nil -end - -ESX.RegisterInteraction = function(name, onPress, condition) - interactions[name] = { - condition = condition or function() return true end, - onPress = onPress, - creator = GetInvokingResource() or "es_extended" - } -end - -function ESX.GetInteractKey() - local hash = joaat('esx_interact') | 0x80000000 - return GetControlInstructionalButton(0, hash, true):sub(3) -end - -ESX.RegisterInput("esx_interact", "Interact", "keyboard", "e", function() - for _, interaction in pairs(interactions) do - local success, result = pcall(interaction.condition) - if success and result then - pressedInteractions[#pressedInteractions+1] = interaction - interaction.onPress() - end - end -end) - -AddEventHandler("onResourceStop", function(resource) - for name, interaction in pairs(interactions) do - if interaction.creator == resource then - interactions[name] = nil - end - end -end) \ No newline at end of file diff --git a/[core]/es_extended/fxmanifest.lua b/[core]/es_extended/fxmanifest.lua index bf9cea0b..72c552eb 100644 --- a/[core]/es_extended/fxmanifest.lua +++ b/[core]/es_extended/fxmanifest.lua @@ -55,7 +55,6 @@ client_scripts { 'client/modules/actions.lua', 'client/modules/death.lua', 'client/modules/npwd.lua', - 'client/modules/interactions.lua', } ui_page { diff --git a/[core]/esx_lib/imports/interactions/client.lua b/[core]/esx_lib/imports/interactions/client.lua new file mode 100644 index 00000000..01b88c09 --- /dev/null +++ b/[core]/esx_lib/imports/interactions/client.lua @@ -0,0 +1,54 @@ +---@class interactionslib +xLib.interactions = {} + +local interactions = {} +local pressedInteractions = {} + +---@param name string +function xLib.interactions.remove(name) + if not interactions[name] then return end + interactions[name] = nil +end + +---@param name string +---@param onPress function +---@param condition? function +function xLib.interactions.register(name, onPress, condition) + interactions[name] = { + condition = condition or function() return true end, + onPress = onPress, + creator = GetInvokingResource() or "es_extended" + } +end + +---@return string +function xLib.interactions.getInteractKey() + local hash = joaat("esx_interact") | 0x80000000 + return GetControlInstructionalButton(0, hash, true):sub(3) +end + +xLib.addKeybind({ + name = "esx_interact", + description = "Interact", + defaultMapper = "keyboard", + defaultKey = "e", + onPressed = function() + for _, interaction in pairs(interactions) do + local success, result = pcall(interaction.condition) + if success and result then + pressedInteractions[#pressedInteractions + 1] = interaction + interaction.onPress() + end + end + end +}) + +AddEventHandler("onResourceStop", function(resource) + for name, interaction in pairs(interactions) do + if interaction.creator == resource then + interactions[name] = nil + end + end +end) + +return xLib.interactions From 700b7a646bc5b8376bd2d24f81661882b361d2cd Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 32/42] feat(esx_lib/game): move game spawn/pool/closest/area and vehicle properties to xLib Integrated fix during the move: - spawnVehicle uses the computed isNetworked for the network-id/migrate guard, fixing the original that branched on the raw networked argument modSmokeEnabled preserved as-is in get/setVehicleProperties (flagged: the set path toggles mod 20 without applying a smoke color, matching legacy behavior). Non-portable substitution documented: - ESX.PlayerData.ped -> PlayerPedId() (ESX.PlayerData is not available in the lib VM) --- [core]/es_extended/client/compat.lua | 23 + [core]/es_extended/client/functions.lua | 670 ----------------------- [core]/esx_lib/imports/game/client.lua | 695 ++++++++++++++++++++++++ 3 files changed, 718 insertions(+), 670 deletions(-) create mode 100644 [core]/esx_lib/imports/game/client.lua diff --git a/[core]/es_extended/client/compat.lua b/[core]/es_extended/client/compat.lua index cca17842..13c90448 100644 --- a/[core]/es_extended/client/compat.lua +++ b/[core]/es_extended/client/compat.lua @@ -67,3 +67,26 @@ ESX.Point = xLib.point ESX.RegisterInteraction = xLib.interactions.register ESX.RemoveInteraction = xLib.interactions.remove ESX.GetInteractKey = xLib.interactions.getInteractKey + +ESX.Game.GetPedMugshot = xLib.game.getPedMugshot +ESX.Game.SpawnObject = xLib.game.spawnObject +ESX.Game.SpawnLocalObject = xLib.game.spawnLocalObject +ESX.Game.DeleteVehicle = xLib.game.deleteVehicle +ESX.Game.DeleteObject = xLib.game.deleteObject +ESX.Game.SpawnVehicle = xLib.game.spawnVehicle +ESX.Game.SpawnLocalVehicle = xLib.game.spawnLocalVehicle +ESX.Game.IsVehicleEmpty = xLib.game.isVehicleEmpty +ESX.Game.GetObjects = xLib.game.getObjects +ESX.Game.GetPeds = xLib.game.getPeds +ESX.Game.GetVehicles = xLib.game.getVehicles +ESX.Game.GetPlayers = xLib.game.getPlayers +ESX.Game.GetClosestObject = xLib.game.getClosestObject +ESX.Game.GetClosestPed = xLib.game.getClosestPed +ESX.Game.GetClosestPlayer = xLib.game.getClosestPlayer +ESX.Game.GetClosestVehicle = xLib.game.getClosestVehicle +ESX.Game.GetPlayersInArea = xLib.game.getPlayersInArea +ESX.Game.GetVehiclesInArea = xLib.game.getVehiclesInArea +ESX.Game.IsSpawnPointClear = xLib.game.isSpawnPointClear +ESX.Game.GetVehicleInDirection = xLib.game.getVehicleInDirection +ESX.Game.GetVehicleProperties = xLib.game.getVehicleProperties +ESX.Game.SetVehicleProperties = xLib.game.setVehicleProperties diff --git a/[core]/es_extended/client/functions.lua b/[core]/es_extended/client/functions.lua index 2f459d9e..92c2ebb3 100644 --- a/[core]/es_extended/client/functions.lua +++ b/[core]/es_extended/client/functions.lua @@ -442,676 +442,6 @@ function ESX.UI.ShowInventoryItemNotification(add, item, count) }) end ----@param ped integer The ped to get the mugshot of ----@param transparent? boolean Whether the mugshot should be transparent -function ESX.Game.GetPedMugshot(ped, transparent) - if not DoesEntityExist(ped) then - return - end - local mugshot = transparent and RegisterPedheadshotTransparent(ped) or RegisterPedheadshot(ped) - - while not IsPedheadshotReady(mugshot) do - Wait(0) - end - - return mugshot, GetPedheadshotTxdString(mugshot) -end - ----@param object integer | string The object to spawn ----@param coords table | vector3 The coords to spawn the object at ----@param cb? function The callback function ----@param networked? boolean Whether the object should be networked ----@return integer | nil -function ESX.Game.SpawnObject(object, coords, cb, networked) - local model = type(object) == "number" and object or joaat(object) - - ESX.Streaming.RequestModel(model) - - local obj = CreateObject(model, coords.x, coords.y, coords.z, networked == nil or networked, false, true) - return cb and cb(obj) or obj -end - ----@param object integer | string The object to spawn ----@param coords table | vector3 The coords to spawn the object at ----@param cb? function The callback function ----@return nil -function ESX.Game.SpawnLocalObject(object, coords, cb) - ESX.Game.SpawnObject(object, coords, cb, false) -end - ----@param vehicle integer The vehicle to delete ----@return nil -function ESX.Game.DeleteVehicle(vehicle) - SetEntityAsMissionEntity(vehicle, true, true) - DeleteVehicle(vehicle) -end - ----@param object integer The object to delete ----@return nil -function ESX.Game.DeleteObject(object) - SetEntityAsMissionEntity(object, false, true) - DeleteObject(object) -end - ----@param vehicleModel integer | string The vehicle to spawn ----@param coords table | vector3 The coords to spawn the vehicle at ----@param heading number The heading of the vehicle ----@param cb? fun(vehicle: number) The callback function ----@param networked? boolean Whether the vehicle should be networked ----@return number? vehicle -function ESX.Game.SpawnVehicle(vehicleModel, coords, heading, cb, networked) - if cb and not ESX.IsFunctionReference(cb) then - error("Invalid callback function") - end - - local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel) - local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z) - local isNetworked = networked == nil or networked - - local playerCoords = GetEntityCoords(ESX.PlayerData.ped) - if not vector or not playerCoords then - return - end - - local dist = #(playerCoords - vector) - if dist > 424 then -- Onesync infinity Range (https://docs.fivem.net/docs/scripting-reference/onesync/) - local executingResource = GetInvokingResource() or "Unknown" - return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource)) - end - - local promise = not cb and promise.new() - CreateThread(function() - local modelHash = ESX.Streaming.RequestModel(model) - if not modelHash then - if promise then - promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) - return - end - error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) - end - - local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true) - - if networked then - local id = NetworkGetNetworkIdFromEntity(vehicle) - SetNetworkIdCanMigrate(id, true) - SetEntityAsMissionEntity(vehicle, true, true) - end - SetVehicleHasBeenOwnedByPlayer(vehicle, true) - SetVehicleNeedsToBeHotwired(vehicle, false) - SetModelAsNoLongerNeeded(model) - SetVehRadioStation(vehicle, "OFF") - - RequestCollisionAtCoord(vector.x, vector.y, vector.z) - while not HasCollisionLoadedAroundEntity(vehicle) do - Wait(0) - end - - if promise then - promise:resolve(vehicle) - elseif cb then - cb(vehicle) - end - end) - - if promise then - return Citizen.Await(promise) - end -end - ----@param vehicle integer The vehicle to spawn ----@param coords table | vector3 The coords to spawn the vehicle at ----@param heading number The heading of the vehicle ----@param cb? function The callback function ----@return nil -function ESX.Game.SpawnLocalVehicle(vehicle, coords, heading, cb) - ESX.Game.SpawnVehicle(vehicle, coords, heading, cb, false) -end - ----@param vehicle integer The vehicle to check ----@return boolean -function ESX.Game.IsVehicleEmpty(vehicle) - return GetVehicleNumberOfPassengers(vehicle) == 0 and IsVehicleSeatFree(vehicle, -1) -end - ----@return table -function ESX.Game.GetObjects() -- Leave the function for compatibility - return GetGamePool("CObject") -end - ----@param onlyOtherPeds? boolean Whether to exlude the player ped ----@return table -function ESX.Game.GetPeds(onlyOtherPeds) - local pool = GetGamePool("CPed") - - if onlyOtherPeds then - local myPed = ESX.PlayerData.ped - for i = 1, #pool do - if pool[i] == myPed then - table.remove(pool, i) - break - end - end - end - - return pool -end - ----@return table -function ESX.Game.GetVehicles() -- Leave the function for compatibility - return GetGamePool("CVehicle") -end - ----@param onlyOtherPlayers? boolean Whether to exclude the player ----@param returnKeyValue? boolean Whether to return the key value pair ----@param returnPeds? boolean Whether to return the peds ----@return table -function ESX.Game.GetPlayers(onlyOtherPlayers, returnKeyValue, returnPeds) - local players = {} - local active = GetActivePlayers() - - for i = 1, #active do - local currentPlayer = active[i] - local ped = GetPlayerPed(currentPlayer) - - if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= ESX.playerId) or not onlyOtherPlayers) then - if returnKeyValue then - players[currentPlayer] = ped - else - players[#players + 1] = returnPeds and ped or currentPlayer - end - end - end - - return players -end - ----@param coords? table | vector3 The coords to get the closest object to ----@param modelFilter? table The model filter ----@return integer, integer -function ESX.Game.GetClosestObject(coords, modelFilter) - return ESX.Game.GetClosestEntity(ESX.Game.GetObjects(), false, coords, modelFilter) -end - ----@param coords? table | vector3 The coords to get the closest ped to ----@param modelFilter? table The model filter ----@return integer, integer -function ESX.Game.GetClosestPed(coords, modelFilter) - return ESX.Game.GetClosestEntity(ESX.Game.GetPeds(true), false, coords, modelFilter) -end - ----@param coords? table | vector3 The coords to get the closest player to ----@return integer, integer -function ESX.Game.GetClosestPlayer(coords) - return ESX.Game.GetClosestEntity(ESX.Game.GetPlayers(true, true), true, coords, nil) -end - ----@param coords? table | vector3 The coords to get the closest vehicle to ----@param modelFilter? table The model filter ----@return integer, integer -function ESX.Game.GetClosestVehicle(coords, modelFilter) - return ESX.Game.GetClosestEntity(ESX.Game.GetVehicles(), false, coords, modelFilter) -end - ----@param coords table | vector3 The coords to search from ----@param maxDistance number The max distance to search within ----@return table -function ESX.Game.GetPlayersInArea(coords, maxDistance) - return EnumerateEntitiesWithinDistance(ESX.Game.GetPlayers(true, true), true, coords, maxDistance) -end - ----@param coords table | vector3 The coords to search from ----@param maxDistance number The max distance to search within ----@return table -function ESX.Game.GetVehiclesInArea(coords, maxDistance) - return EnumerateEntitiesWithinDistance(ESX.Game.GetVehicles(), false, coords, maxDistance) -end - ----@param coords table | vector3 The coords to search from ----@param maxDistance number The max distance to search within ----@return boolean -function ESX.Game.IsSpawnPointClear(coords, maxDistance) - return #ESX.Game.GetVehiclesInArea(coords, maxDistance) == 0 -end - ----@return integer | nil, vector3 | nil -function ESX.Game.GetVehicleInDirection() - local _, hit, coords, _, _, entity = ESX.Game.RaycastScreen(5, 10, ESX.PlayerData.ped) - if hit and IsEntityAVehicle(entity) then - return entity, coords - end -end - ----@param vehicle integer The vehicle to get the properties of ----@return table | nil -function ESX.Game.GetVehicleProperties(vehicle) - if not DoesEntityExist(vehicle) then - return - end - - ---@type number | number[], number | number[] - local colorPrimary, colorSecondary = GetVehicleColours(vehicle) - local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) - local dashboardColor = GetVehicleDashboardColor(vehicle) - local interiorColor = GetVehicleInteriorColour(vehicle) - - if GetIsVehiclePrimaryColourCustom(vehicle) then - colorPrimary = { GetVehicleCustomPrimaryColour(vehicle) } - end - - if GetIsVehicleSecondaryColourCustom(vehicle) then - colorSecondary = { GetVehicleCustomSecondaryColour(vehicle) } - end - - local hasCustomXenonColor, customXenonColorR, customXenonColorG, customXenonColorB = GetVehicleXenonLightsCustomColor(vehicle) - local customXenonColor = nil - if hasCustomXenonColor then - customXenonColor = { customXenonColorR, customXenonColorG, customXenonColorB } - end - - local extras = {} - for extraId = 0, 20 do - if DoesExtraExist(vehicle, extraId) then - extras[tostring(extraId)] = IsVehicleExtraTurnedOn(vehicle, extraId) - end - end - - local doorsBroken, windowsBroken, tyreBurst = {}, {}, {} - local numWheels = tostring(GetVehicleNumberOfWheels(vehicle)) - - local TyresIndex = { -- Wheel index list according to the number of vehicle wheels. - ["2"] = { 0, 4 }, -- Bike and cycle. - ["3"] = { 0, 1, 4, 5 }, -- Vehicle with 3 wheels (get for wheels because some 3 wheels vehicles have 2 wheels on front and one rear or the reverse). - ["4"] = { 0, 1, 4, 5 }, -- Vehicle with 4 wheels. - ["6"] = { 0, 1, 2, 3, 4, 5 }, -- Vehicle with 6 wheels. - } - - if TyresIndex[numWheels] then - for _, idx in pairs(TyresIndex[numWheels]) do - tyreBurst[tostring(idx)] = IsVehicleTyreBurst(vehicle, idx, false) - end - end - - for windowId = 0, 7 do -- 13 - RollUpWindow(vehicle, windowId) --fix when you put the car away with the window down - windowsBroken[tostring(windowId)] = not IsVehicleWindowIntact(vehicle, windowId) - end - - local numDoors = GetNumberOfVehicleDoors(vehicle) - if numDoors and numDoors > 0 then - for doorsId = 0, numDoors do - doorsBroken[tostring(doorsId)] = IsVehicleDoorDamaged(vehicle, doorsId) - end - end - - return { - model = GetEntityModel(vehicle), - doorsBroken = doorsBroken, - windowsBroken = windowsBroken, - tyreBurst = tyreBurst, - tyresCanBurst = GetVehicleTyresCanBurst(vehicle), - plate = ESX.Math.Trim(GetVehicleNumberPlateText(vehicle)), - plateIndex = GetVehicleNumberPlateTextIndex(vehicle), - - bodyHealth = ESX.Math.Round(GetVehicleBodyHealth(vehicle), 1), - engineHealth = ESX.Math.Round(GetVehicleEngineHealth(vehicle), 1), - tankHealth = ESX.Math.Round(GetVehiclePetrolTankHealth(vehicle), 1), - - fuelLevel = ESX.Math.Round(GetVehicleFuelLevel(vehicle), 1), - dirtLevel = ESX.Math.Round(GetVehicleDirtLevel(vehicle), 1), - color1 = colorPrimary, - color2 = colorSecondary, - - pearlescentColor = pearlescentColor, - wheelColor = wheelColor, - - dashboardColor = dashboardColor, - interiorColor = interiorColor, - - wheels = GetVehicleWheelType(vehicle), - windowTint = GetVehicleWindowTint(vehicle), - xenonColor = GetVehicleXenonLightsColor(vehicle), - customXenonColor = customXenonColor, - - neonEnabled = { IsVehicleNeonLightEnabled(vehicle, 0), IsVehicleNeonLightEnabled(vehicle, 1), IsVehicleNeonLightEnabled(vehicle, 2), IsVehicleNeonLightEnabled(vehicle, 3) }, - - neonColor = table.pack(GetVehicleNeonLightsColour(vehicle)), - extras = extras, - tyreSmokeColor = table.pack(GetVehicleTyreSmokeColor(vehicle)), - - modSpoilers = GetVehicleMod(vehicle, 0), - modFrontBumper = GetVehicleMod(vehicle, 1), - modRearBumper = GetVehicleMod(vehicle, 2), - modSideSkirt = GetVehicleMod(vehicle, 3), - modExhaust = GetVehicleMod(vehicle, 4), - modFrame = GetVehicleMod(vehicle, 5), - modGrille = GetVehicleMod(vehicle, 6), - modHood = GetVehicleMod(vehicle, 7), - modFender = GetVehicleMod(vehicle, 8), - modRightFender = GetVehicleMod(vehicle, 9), - modRoof = GetVehicleMod(vehicle, 10), - modRoofLivery = GetVehicleRoofLivery(vehicle), - - modEngine = GetVehicleMod(vehicle, 11), - modBrakes = GetVehicleMod(vehicle, 12), - modTransmission = GetVehicleMod(vehicle, 13), - modHorns = GetVehicleMod(vehicle, 14), - modSuspension = GetVehicleMod(vehicle, 15), - modArmor = GetVehicleMod(vehicle, 16), - - modTurbo = IsToggleModOn(vehicle, 18), - modSmokeEnabled = IsToggleModOn(vehicle, 20), - modXenon = IsToggleModOn(vehicle, 22), - - modFrontWheels = GetVehicleMod(vehicle, 23), - modCustomFrontWheels = GetVehicleModVariation(vehicle, 23), - modBackWheels = GetVehicleMod(vehicle, 24), - modCustomBackWheels = GetVehicleModVariation(vehicle, 24), - - modPlateHolder = GetVehicleMod(vehicle, 25), - modVanityPlate = GetVehicleMod(vehicle, 26), - modTrimA = GetVehicleMod(vehicle, 27), - modOrnaments = GetVehicleMod(vehicle, 28), - modDashboard = GetVehicleMod(vehicle, 29), - modDial = GetVehicleMod(vehicle, 30), - modDoorSpeaker = GetVehicleMod(vehicle, 31), - modSeats = GetVehicleMod(vehicle, 32), - modSteeringWheel = GetVehicleMod(vehicle, 33), - modShifterLeavers = GetVehicleMod(vehicle, 34), - modAPlate = GetVehicleMod(vehicle, 35), - modSpeakers = GetVehicleMod(vehicle, 36), - modTrunk = GetVehicleMod(vehicle, 37), - modHydrolic = GetVehicleMod(vehicle, 38), - modEngineBlock = GetVehicleMod(vehicle, 39), - modAirFilter = GetVehicleMod(vehicle, 40), - modStruts = GetVehicleMod(vehicle, 41), - modArchCover = GetVehicleMod(vehicle, 42), - modAerials = GetVehicleMod(vehicle, 43), - modTrimB = GetVehicleMod(vehicle, 44), - modTank = GetVehicleMod(vehicle, 45), - modWindows = GetVehicleMod(vehicle, 46), - modLivery = GetVehicleMod(vehicle, 48) == -1 and GetVehicleLivery(vehicle) or GetVehicleMod(vehicle, 48), - modLightbar = GetVehicleMod(vehicle, 49), - } -end - ----@param vehicle integer The vehicle to set the properties of ----@param props table The properties to set ----@return nil -function ESX.Game.SetVehicleProperties(vehicle, props) - if not DoesEntityExist(vehicle) then - return - end - local colorPrimary, colorSecondary = GetVehicleColours(vehicle) - local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) - SetVehicleModKit(vehicle, 0) - - if props.tyresCanBurst ~= nil then - SetVehicleTyresCanBurst(vehicle, props.tyresCanBurst) - end - - if props.plate ~= nil then - SetVehicleNumberPlateText(vehicle, props.plate) - end - if props.plateIndex ~= nil then - SetVehicleNumberPlateTextIndex(vehicle, props.plateIndex) - end - if props.bodyHealth ~= nil then - SetVehicleBodyHealth(vehicle, props.bodyHealth + 0.0) - end - if props.engineHealth ~= nil then - SetVehicleEngineHealth(vehicle, props.engineHealth + 0.0) - end - if props.tankHealth ~= nil then - SetVehiclePetrolTankHealth(vehicle, props.tankHealth + 0.0) - end - if props.fuelLevel ~= nil then - SetVehicleFuelLevel(vehicle, props.fuelLevel + 0.0) - end - if props.dirtLevel ~= nil then - SetVehicleDirtLevel(vehicle, props.dirtLevel + 0.0) - end - if props.color1 ~= nil then - if type(props.color1) == "table" then - SetVehicleCustomPrimaryColour(vehicle, props.color1[1], props.color1[2], props.color1[3]) - else - SetVehicleColours(vehicle, props.color1, colorSecondary) - end - end - if props.color2 ~= nil then - if type(props.color2) == "table" then - SetVehicleCustomSecondaryColour(vehicle, props.color2[1], props.color2[2], props.color2[3]) - else - SetVehicleColours(vehicle, props.color1 or colorPrimary, props.color2) - end - end - if props.pearlescentColor ~= nil then - SetVehicleExtraColours(vehicle, props.pearlescentColor, wheelColor) - end - - if props.interiorColor ~= nil then - SetVehicleInteriorColor(vehicle, props.interiorColor) - end - - if props.dashboardColor ~= nil then - SetVehicleDashboardColor(vehicle, props.dashboardColor) - end - - if props.wheelColor ~= nil then - SetVehicleExtraColours(vehicle, props.pearlescentColor or pearlescentColor, props.wheelColor) - end - if props.wheels ~= nil then - SetVehicleWheelType(vehicle, props.wheels) - end - if props.windowTint ~= nil then - SetVehicleWindowTint(vehicle, props.windowTint) - end - - if props.neonEnabled ~= nil then - SetVehicleNeonLightEnabled(vehicle, 0, props.neonEnabled[1]) - SetVehicleNeonLightEnabled(vehicle, 1, props.neonEnabled[2]) - SetVehicleNeonLightEnabled(vehicle, 2, props.neonEnabled[3]) - SetVehicleNeonLightEnabled(vehicle, 3, props.neonEnabled[4]) - end - - if props.extras ~= nil then - for extraId, enabled in pairs(props.extras) do - extraId = tonumber(extraId) - if extraId then - SetVehicleExtra(vehicle, extraId, not enabled) - end - end - end - - if props.neonColor ~= nil then - SetVehicleNeonLightsColour(vehicle, props.neonColor[1], props.neonColor[2], props.neonColor[3]) - end - if props.xenonColor ~= nil then - SetVehicleXenonLightsColor(vehicle, props.xenonColor) - end - if props.customXenonColor ~= nil then - SetVehicleXenonLightsCustomColor(vehicle, props.customXenonColor[1], props.customXenonColor[2], props.customXenonColor[3]) - end - if props.modSmokeEnabled ~= nil then - ToggleVehicleMod(vehicle, 20, true) - end - if props.tyreSmokeColor ~= nil then - SetVehicleTyreSmokeColor(vehicle, props.tyreSmokeColor[1], props.tyreSmokeColor[2], props.tyreSmokeColor[3]) - end - if props.modSpoilers ~= nil then - SetVehicleMod(vehicle, 0, props.modSpoilers, false) - end - if props.modFrontBumper ~= nil then - SetVehicleMod(vehicle, 1, props.modFrontBumper, false) - end - if props.modRearBumper ~= nil then - SetVehicleMod(vehicle, 2, props.modRearBumper, false) - end - if props.modSideSkirt ~= nil then - SetVehicleMod(vehicle, 3, props.modSideSkirt, false) - end - if props.modExhaust ~= nil then - SetVehicleMod(vehicle, 4, props.modExhaust, false) - end - if props.modFrame ~= nil then - SetVehicleMod(vehicle, 5, props.modFrame, false) - end - if props.modGrille ~= nil then - SetVehicleMod(vehicle, 6, props.modGrille, false) - end - if props.modHood ~= nil then - SetVehicleMod(vehicle, 7, props.modHood, false) - end - if props.modFender ~= nil then - SetVehicleMod(vehicle, 8, props.modFender, false) - end - if props.modRightFender ~= nil then - SetVehicleMod(vehicle, 9, props.modRightFender, false) - end - if props.modRoof ~= nil then - SetVehicleMod(vehicle, 10, props.modRoof, false) - end - - if props.modRoofLivery ~= nil then - SetVehicleRoofLivery(vehicle, props.modRoofLivery) - end - - if props.modEngine ~= nil then - SetVehicleMod(vehicle, 11, props.modEngine, false) - end - if props.modBrakes ~= nil then - SetVehicleMod(vehicle, 12, props.modBrakes, false) - end - if props.modTransmission ~= nil then - SetVehicleMod(vehicle, 13, props.modTransmission, false) - end - if props.modHorns ~= nil then - SetVehicleMod(vehicle, 14, props.modHorns, false) - end - if props.modSuspension ~= nil then - SetVehicleMod(vehicle, 15, props.modSuspension, false) - end - if props.modArmor ~= nil then - SetVehicleMod(vehicle, 16, props.modArmor, false) - end - if props.modTurbo ~= nil then - ToggleVehicleMod(vehicle, 18, props.modTurbo) - end - if props.modXenon ~= nil then - ToggleVehicleMod(vehicle, 22, props.modXenon) - end - if props.modFrontWheels ~= nil then - SetVehicleMod(vehicle, 23, props.modFrontWheels, props.modCustomFrontWheels) - end - if props.modBackWheels ~= nil then - SetVehicleMod(vehicle, 24, props.modBackWheels, props.modCustomBackWheels) - end - if props.modPlateHolder ~= nil then - SetVehicleMod(vehicle, 25, props.modPlateHolder, false) - end - if props.modVanityPlate ~= nil then - SetVehicleMod(vehicle, 26, props.modVanityPlate, false) - end - if props.modTrimA ~= nil then - SetVehicleMod(vehicle, 27, props.modTrimA, false) - end - if props.modOrnaments ~= nil then - SetVehicleMod(vehicle, 28, props.modOrnaments, false) - end - if props.modDashboard ~= nil then - SetVehicleMod(vehicle, 29, props.modDashboard, false) - end - if props.modDial ~= nil then - SetVehicleMod(vehicle, 30, props.modDial, false) - end - if props.modDoorSpeaker ~= nil then - SetVehicleMod(vehicle, 31, props.modDoorSpeaker, false) - end - if props.modSeats ~= nil then - SetVehicleMod(vehicle, 32, props.modSeats, false) - end - if props.modSteeringWheel ~= nil then - SetVehicleMod(vehicle, 33, props.modSteeringWheel, false) - end - if props.modShifterLeavers ~= nil then - SetVehicleMod(vehicle, 34, props.modShifterLeavers, false) - end - if props.modAPlate ~= nil then - SetVehicleMod(vehicle, 35, props.modAPlate, false) - end - if props.modSpeakers ~= nil then - SetVehicleMod(vehicle, 36, props.modSpeakers, false) - end - if props.modTrunk ~= nil then - SetVehicleMod(vehicle, 37, props.modTrunk, false) - end - if props.modHydrolic ~= nil then - SetVehicleMod(vehicle, 38, props.modHydrolic, false) - end - if props.modEngineBlock ~= nil then - SetVehicleMod(vehicle, 39, props.modEngineBlock, false) - end - if props.modAirFilter ~= nil then - SetVehicleMod(vehicle, 40, props.modAirFilter, false) - end - if props.modStruts ~= nil then - SetVehicleMod(vehicle, 41, props.modStruts, false) - end - if props.modArchCover ~= nil then - SetVehicleMod(vehicle, 42, props.modArchCover, false) - end - if props.modAerials ~= nil then - SetVehicleMod(vehicle, 43, props.modAerials, false) - end - if props.modTrimB ~= nil then - SetVehicleMod(vehicle, 44, props.modTrimB, false) - end - if props.modTank ~= nil then - SetVehicleMod(vehicle, 45, props.modTank, false) - end - if props.modWindows ~= nil then - SetVehicleMod(vehicle, 46, props.modWindows, false) - end - - if props.modLivery ~= nil then - SetVehicleMod(vehicle, 48, props.modLivery, false) - SetVehicleLivery(vehicle, props.modLivery) - end - - if props.windowsBroken ~= nil then - for k, v in pairs(props.windowsBroken) do - if v then - k = tonumber(k) - if k then - RemoveVehicleWindow(vehicle, k) - end - end - end - end - - if props.doorsBroken ~= nil then - for k, v in pairs(props.doorsBroken) do - if v then - k = tonumber(k) - if k then - SetVehicleDoorBroken(vehicle, k, true) - end - end - end - end - - if props.tyreBurst ~= nil then - for k, v in pairs(props.tyreBurst) do - if v then - k = tonumber(k) - if k then - SetVehicleTyreBurst(vehicle, k, true, 1000.0) - end - end - end - end -end - ---@param coords vector3 | table coords to get the closest pickup to ---@param text string The text to display ---@param size? number The size of the text diff --git a/[core]/esx_lib/imports/game/client.lua b/[core]/esx_lib/imports/game/client.lua new file mode 100644 index 00000000..ab285815 --- /dev/null +++ b/[core]/esx_lib/imports/game/client.lua @@ -0,0 +1,695 @@ +---@class gamelib +xLib.game = {} + +local function isCallable(val) + local valType = type(val) + local mt = getmetatable(val) + return valType == "function" or (valType == "table" and mt ~= nil and type(mt.__call) == "function") +end + +---@param ped integer The ped to get the mugshot of +---@param transparent? boolean Whether the mugshot should be transparent +function xLib.game.getPedMugshot(ped, transparent) + if not DoesEntityExist(ped) then + return + end + + local mugshot = transparent and RegisterPedheadshotTransparent(ped) or RegisterPedheadshot(ped) + + while not IsPedheadshotReady(mugshot) do + Wait(0) + end + + return mugshot, GetPedheadshotTxdString(mugshot) +end + +---@param object integer | string The object to spawn +---@param coords table | vector3 The coords to spawn the object at +---@param cb? function The callback function +---@param networked? boolean Whether the object should be networked +---@return integer | nil +function xLib.game.spawnObject(object, coords, cb, networked) + local model = type(object) == "number" and object or joaat(object) + + xLib.streaming.requestModel(model) + + local obj = CreateObject(model, coords.x, coords.y, coords.z, networked == nil or networked, false, true) + return cb and cb(obj) or obj +end + +---@param object integer | string The object to spawn +---@param coords table | vector3 The coords to spawn the object at +---@param cb? function The callback function +---@return nil +function xLib.game.spawnLocalObject(object, coords, cb) + xLib.game.spawnObject(object, coords, cb, false) +end + +---@param vehicle integer The vehicle to delete +---@return nil +function xLib.game.deleteVehicle(vehicle) + SetEntityAsMissionEntity(vehicle, true, true) + DeleteVehicle(vehicle) +end + +---@param object integer The object to delete +---@return nil +function xLib.game.deleteObject(object) + SetEntityAsMissionEntity(object, false, true) + DeleteObject(object) +end + +---@param vehicleModel integer | string The vehicle to spawn +---@param coords table | vector3 The coords to spawn the vehicle at +---@param heading number The heading of the vehicle +---@param cb? fun(vehicle: number) The callback function +---@param networked? boolean Whether the vehicle should be networked +---@return number? vehicle +function xLib.game.spawnVehicle(vehicleModel, coords, heading, cb, networked) + if cb and not isCallable(cb) then + error("Invalid callback function") + end + + local model = type(vehicleModel) == "number" and vehicleModel or joaat(vehicleModel) + local vector = type(coords) == "vector3" and coords or vec(coords.x, coords.y, coords.z) + local isNetworked = networked == nil or networked + + local playerCoords = GetEntityCoords(PlayerPedId()) + if not vector or not playerCoords then + return + end + + local dist = #(playerCoords - vector) + if dist > 424 then -- Onesync infinity Range (https://docs.fivem.net/docs/scripting-reference/onesync/) + local executingResource = GetInvokingResource() or "Unknown" + return error(("Resource ^5%s^1 Tried to spawn vehicle on the client but the position is too far away (Out of onesync range)."):format(executingResource)) + end + + local promise = not cb and promise.new() + CreateThread(function() + local modelHash = xLib.streaming.requestModel(model) + if not modelHash then + if promise then + promise:reject(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + return + end + error(("Tried to spawn invalid vehicle - ^5%s^7!"):format(model)) + end + + local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true) + + if isNetworked then + local id = NetworkGetNetworkIdFromEntity(vehicle) + SetNetworkIdCanMigrate(id, true) + SetEntityAsMissionEntity(vehicle, true, true) + end + SetVehicleHasBeenOwnedByPlayer(vehicle, true) + SetVehicleNeedsToBeHotwired(vehicle, false) + SetModelAsNoLongerNeeded(model) + SetVehRadioStation(vehicle, "OFF") + + RequestCollisionAtCoord(vector.x, vector.y, vector.z) + while not HasCollisionLoadedAroundEntity(vehicle) do + Wait(0) + end + + if promise then + promise:resolve(vehicle) + elseif cb then + cb(vehicle) + end + end) + + if promise then + return Citizen.Await(promise) + end +end + +---@param vehicle integer The vehicle to spawn +---@param coords table | vector3 The coords to spawn the vehicle at +---@param heading number The heading of the vehicle +---@param cb? function The callback function +---@return nil +function xLib.game.spawnLocalVehicle(vehicle, coords, heading, cb) + xLib.game.spawnVehicle(vehicle, coords, heading, cb, false) +end + +---@param vehicle integer The vehicle to check +---@return boolean +function xLib.game.isVehicleEmpty(vehicle) + return GetVehicleNumberOfPassengers(vehicle) == 0 and IsVehicleSeatFree(vehicle, -1) +end + +---@return table +function xLib.game.getObjects() -- Leave the function for compatibility + return GetGamePool("CObject") +end + +---@param onlyOtherPeds? boolean Whether to exlude the player ped +---@return table +function xLib.game.getPeds(onlyOtherPeds) + local pool = GetGamePool("CPed") + + if onlyOtherPeds then + local myPed = PlayerPedId() + for i = 1, #pool do + if pool[i] == myPed then + table.remove(pool, i) + break + end + end + end + + return pool +end + +---@return table +function xLib.game.getVehicles() -- Leave the function for compatibility + return GetGamePool("CVehicle") +end + +---@param onlyOtherPlayers? boolean Whether to exclude the player +---@param returnKeyValue? boolean Whether to return the key value pair +---@param returnPeds? boolean Whether to return the peds +---@return table +function xLib.game.getPlayers(onlyOtherPlayers, returnKeyValue, returnPeds) + local players = {} + local active = GetActivePlayers() + + for i = 1, #active do + local currentPlayer = active[i] + local ped = GetPlayerPed(currentPlayer) + + if DoesEntityExist(ped) and ((onlyOtherPlayers and currentPlayer ~= PlayerId()) or not onlyOtherPlayers) then + if returnKeyValue then + players[currentPlayer] = ped + else + players[#players + 1] = returnPeds and ped or currentPlayer + end + end + end + + return players +end + +---@param coords? table | vector3 The coords to get the closest object to +---@param modelFilter? table The model filter +---@return integer, integer +function xLib.game.getClosestObject(coords, modelFilter) + return xLib.entity.closest(xLib.game.getObjects(), false, coords, modelFilter) +end + +---@param coords? table | vector3 The coords to get the closest ped to +---@param modelFilter? table The model filter +---@return integer, integer +function xLib.game.getClosestPed(coords, modelFilter) + return xLib.entity.closest(xLib.game.getPeds(true), false, coords, modelFilter) +end + +---@param coords? table | vector3 The coords to get the closest player to +---@return integer, integer +function xLib.game.getClosestPlayer(coords) + return xLib.entity.closest(xLib.game.getPlayers(true, true), true, coords, nil) +end + +---@param coords? table | vector3 The coords to get the closest vehicle to +---@param modelFilter? table The model filter +---@return integer, integer +function xLib.game.getClosestVehicle(coords, modelFilter) + return xLib.entity.closest(xLib.game.getVehicles(), false, coords, modelFilter) +end + +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return table +function xLib.game.getPlayersInArea(coords, maxDistance) + return xLib.entity.EnumerateWithinDistance(xLib.game.getPlayers(true, true), true, coords, maxDistance) +end + +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return table +function xLib.game.getVehiclesInArea(coords, maxDistance) + return xLib.entity.EnumerateWithinDistance(xLib.game.getVehicles(), false, coords, maxDistance) +end + +---@param coords table | vector3 The coords to search from +---@param maxDistance number The max distance to search within +---@return boolean +function xLib.game.isSpawnPointClear(coords, maxDistance) + return #xLib.game.getVehiclesInArea(coords, maxDistance) == 0 +end + +---@return integer | nil, vector3 | nil +function xLib.game.getVehicleInDirection() + local _, hit, coords, _, _, entity = xLib.Raycast.FromScreen(5, 10, PlayerPedId()) + if hit and IsEntityAVehicle(entity) then + return entity, coords + end +end + +local function round(value, numDecimalPlaces) + if numDecimalPlaces then + local power = 10 ^ numDecimalPlaces + return math.floor((value * power) + 0.5) / power + else + return math.floor(value + 0.5) + end +end + +local function trim(value) + value = tostring(value) + return (string.gsub(value, "^%s*(.-)%s*$", "%1")) +end + +---@param vehicle integer The vehicle to get the properties of +---@return table | nil +function xLib.game.getVehicleProperties(vehicle) + if not DoesEntityExist(vehicle) then + return + end + + ---@type number | number[], number | number[] + local colorPrimary, colorSecondary = GetVehicleColours(vehicle) + local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) + local dashboardColor = GetVehicleDashboardColor(vehicle) + local interiorColor = GetVehicleInteriorColour(vehicle) + + if GetIsVehiclePrimaryColourCustom(vehicle) then + colorPrimary = { GetVehicleCustomPrimaryColour(vehicle) } + end + + if GetIsVehicleSecondaryColourCustom(vehicle) then + colorSecondary = { GetVehicleCustomSecondaryColour(vehicle) } + end + + local hasCustomXenonColor, customXenonColorR, customXenonColorG, customXenonColorB = GetVehicleXenonLightsCustomColor(vehicle) + local customXenonColor = nil + if hasCustomXenonColor then + customXenonColor = { customXenonColorR, customXenonColorG, customXenonColorB } + end + + local extras = {} + for extraId = 0, 20 do + if DoesExtraExist(vehicle, extraId) then + extras[tostring(extraId)] = IsVehicleExtraTurnedOn(vehicle, extraId) + end + end + + local doorsBroken, windowsBroken, tyreBurst = {}, {}, {} + local numWheels = tostring(GetVehicleNumberOfWheels(vehicle)) + + local TyresIndex = { -- Wheel index list according to the number of vehicle wheels. + ["2"] = { 0, 4 }, -- Bike and cycle. + ["3"] = { 0, 1, 4, 5 }, -- Vehicle with 3 wheels (get for wheels because some 3 wheels vehicles have 2 wheels on front and one rear or the reverse). + ["4"] = { 0, 1, 4, 5 }, -- Vehicle with 4 wheels. + ["6"] = { 0, 1, 2, 3, 4, 5 }, -- Vehicle with 6 wheels. + } + + if TyresIndex[numWheels] then + for _, idx in pairs(TyresIndex[numWheels]) do + tyreBurst[tostring(idx)] = IsVehicleTyreBurst(vehicle, idx, false) + end + end + + for windowId = 0, 7 do -- 13 + RollUpWindow(vehicle, windowId) --fix when you put the car away with the window down + windowsBroken[tostring(windowId)] = not IsVehicleWindowIntact(vehicle, windowId) + end + + local numDoors = GetNumberOfVehicleDoors(vehicle) + if numDoors and numDoors > 0 then + for doorsId = 0, numDoors do + doorsBroken[tostring(doorsId)] = IsVehicleDoorDamaged(vehicle, doorsId) + end + end + + return { + model = GetEntityModel(vehicle), + doorsBroken = doorsBroken, + windowsBroken = windowsBroken, + tyreBurst = tyreBurst, + tyresCanBurst = GetVehicleTyresCanBurst(vehicle), + plate = trim(GetVehicleNumberPlateText(vehicle)), + plateIndex = GetVehicleNumberPlateTextIndex(vehicle), + + bodyHealth = round(GetVehicleBodyHealth(vehicle), 1), + engineHealth = round(GetVehicleEngineHealth(vehicle), 1), + tankHealth = round(GetVehiclePetrolTankHealth(vehicle), 1), + + fuelLevel = round(GetVehicleFuelLevel(vehicle), 1), + dirtLevel = round(GetVehicleDirtLevel(vehicle), 1), + color1 = colorPrimary, + color2 = colorSecondary, + + pearlescentColor = pearlescentColor, + wheelColor = wheelColor, + + dashboardColor = dashboardColor, + interiorColor = interiorColor, + + wheels = GetVehicleWheelType(vehicle), + windowTint = GetVehicleWindowTint(vehicle), + xenonColor = GetVehicleXenonLightsColor(vehicle), + customXenonColor = customXenonColor, + + neonEnabled = { IsVehicleNeonLightEnabled(vehicle, 0), IsVehicleNeonLightEnabled(vehicle, 1), IsVehicleNeonLightEnabled(vehicle, 2), IsVehicleNeonLightEnabled(vehicle, 3) }, + + neonColor = table.pack(GetVehicleNeonLightsColour(vehicle)), + extras = extras, + tyreSmokeColor = table.pack(GetVehicleTyreSmokeColor(vehicle)), + + modSpoilers = GetVehicleMod(vehicle, 0), + modFrontBumper = GetVehicleMod(vehicle, 1), + modRearBumper = GetVehicleMod(vehicle, 2), + modSideSkirt = GetVehicleMod(vehicle, 3), + modExhaust = GetVehicleMod(vehicle, 4), + modFrame = GetVehicleMod(vehicle, 5), + modGrille = GetVehicleMod(vehicle, 6), + modHood = GetVehicleMod(vehicle, 7), + modFender = GetVehicleMod(vehicle, 8), + modRightFender = GetVehicleMod(vehicle, 9), + modRoof = GetVehicleMod(vehicle, 10), + modRoofLivery = GetVehicleRoofLivery(vehicle), + + modEngine = GetVehicleMod(vehicle, 11), + modBrakes = GetVehicleMod(vehicle, 12), + modTransmission = GetVehicleMod(vehicle, 13), + modHorns = GetVehicleMod(vehicle, 14), + modSuspension = GetVehicleMod(vehicle, 15), + modArmor = GetVehicleMod(vehicle, 16), + + modTurbo = IsToggleModOn(vehicle, 18), + modSmokeEnabled = IsToggleModOn(vehicle, 20), + modXenon = IsToggleModOn(vehicle, 22), + + modFrontWheels = GetVehicleMod(vehicle, 23), + modCustomFrontWheels = GetVehicleModVariation(vehicle, 23), + modBackWheels = GetVehicleMod(vehicle, 24), + modCustomBackWheels = GetVehicleModVariation(vehicle, 24), + + modPlateHolder = GetVehicleMod(vehicle, 25), + modVanityPlate = GetVehicleMod(vehicle, 26), + modTrimA = GetVehicleMod(vehicle, 27), + modOrnaments = GetVehicleMod(vehicle, 28), + modDashboard = GetVehicleMod(vehicle, 29), + modDial = GetVehicleMod(vehicle, 30), + modDoorSpeaker = GetVehicleMod(vehicle, 31), + modSeats = GetVehicleMod(vehicle, 32), + modSteeringWheel = GetVehicleMod(vehicle, 33), + modShifterLeavers = GetVehicleMod(vehicle, 34), + modAPlate = GetVehicleMod(vehicle, 35), + modSpeakers = GetVehicleMod(vehicle, 36), + modTrunk = GetVehicleMod(vehicle, 37), + modHydrolic = GetVehicleMod(vehicle, 38), + modEngineBlock = GetVehicleMod(vehicle, 39), + modAirFilter = GetVehicleMod(vehicle, 40), + modStruts = GetVehicleMod(vehicle, 41), + modArchCover = GetVehicleMod(vehicle, 42), + modAerials = GetVehicleMod(vehicle, 43), + modTrimB = GetVehicleMod(vehicle, 44), + modTank = GetVehicleMod(vehicle, 45), + modWindows = GetVehicleMod(vehicle, 46), + modLivery = GetVehicleMod(vehicle, 48) == -1 and GetVehicleLivery(vehicle) or GetVehicleMod(vehicle, 48), + modLightbar = GetVehicleMod(vehicle, 49), + } +end + +---@param vehicle integer The vehicle to set the properties of +---@param props table The properties to set +---@return nil +function xLib.game.setVehicleProperties(vehicle, props) + if not DoesEntityExist(vehicle) then + return + end + local colorPrimary, colorSecondary = GetVehicleColours(vehicle) + local pearlescentColor, wheelColor = GetVehicleExtraColours(vehicle) + SetVehicleModKit(vehicle, 0) + + if props.tyresCanBurst ~= nil then + SetVehicleTyresCanBurst(vehicle, props.tyresCanBurst) + end + + if props.plate ~= nil then + SetVehicleNumberPlateText(vehicle, props.plate) + end + if props.plateIndex ~= nil then + SetVehicleNumberPlateTextIndex(vehicle, props.plateIndex) + end + if props.bodyHealth ~= nil then + SetVehicleBodyHealth(vehicle, props.bodyHealth + 0.0) + end + if props.engineHealth ~= nil then + SetVehicleEngineHealth(vehicle, props.engineHealth + 0.0) + end + if props.tankHealth ~= nil then + SetVehiclePetrolTankHealth(vehicle, props.tankHealth + 0.0) + end + if props.fuelLevel ~= nil then + SetVehicleFuelLevel(vehicle, props.fuelLevel + 0.0) + end + if props.dirtLevel ~= nil then + SetVehicleDirtLevel(vehicle, props.dirtLevel + 0.0) + end + if props.color1 ~= nil then + if type(props.color1) == "table" then + SetVehicleCustomPrimaryColour(vehicle, props.color1[1], props.color1[2], props.color1[3]) + else + SetVehicleColours(vehicle, props.color1, colorSecondary) + end + end + if props.color2 ~= nil then + if type(props.color2) == "table" then + SetVehicleCustomSecondaryColour(vehicle, props.color2[1], props.color2[2], props.color2[3]) + else + SetVehicleColours(vehicle, props.color1 or colorPrimary, props.color2) + end + end + if props.pearlescentColor ~= nil then + SetVehicleExtraColours(vehicle, props.pearlescentColor, wheelColor) + end + + if props.interiorColor ~= nil then + SetVehicleInteriorColor(vehicle, props.interiorColor) + end + + if props.dashboardColor ~= nil then + SetVehicleDashboardColor(vehicle, props.dashboardColor) + end + + if props.wheelColor ~= nil then + SetVehicleExtraColours(vehicle, props.pearlescentColor or pearlescentColor, props.wheelColor) + end + if props.wheels ~= nil then + SetVehicleWheelType(vehicle, props.wheels) + end + if props.windowTint ~= nil then + SetVehicleWindowTint(vehicle, props.windowTint) + end + + if props.neonEnabled ~= nil then + SetVehicleNeonLightEnabled(vehicle, 0, props.neonEnabled[1]) + SetVehicleNeonLightEnabled(vehicle, 1, props.neonEnabled[2]) + SetVehicleNeonLightEnabled(vehicle, 2, props.neonEnabled[3]) + SetVehicleNeonLightEnabled(vehicle, 3, props.neonEnabled[4]) + end + + if props.extras ~= nil then + for extraId, enabled in pairs(props.extras) do + extraId = tonumber(extraId) + if extraId then + SetVehicleExtra(vehicle, extraId, not enabled) + end + end + end + + if props.neonColor ~= nil then + SetVehicleNeonLightsColour(vehicle, props.neonColor[1], props.neonColor[2], props.neonColor[3]) + end + if props.xenonColor ~= nil then + SetVehicleXenonLightsColor(vehicle, props.xenonColor) + end + if props.customXenonColor ~= nil then + SetVehicleXenonLightsCustomColor(vehicle, props.customXenonColor[1], props.customXenonColor[2], props.customXenonColor[3]) + end + if props.modSmokeEnabled ~= nil then + ToggleVehicleMod(vehicle, 20, true) + end + if props.tyreSmokeColor ~= nil then + SetVehicleTyreSmokeColor(vehicle, props.tyreSmokeColor[1], props.tyreSmokeColor[2], props.tyreSmokeColor[3]) + end + if props.modSpoilers ~= nil then + SetVehicleMod(vehicle, 0, props.modSpoilers, false) + end + if props.modFrontBumper ~= nil then + SetVehicleMod(vehicle, 1, props.modFrontBumper, false) + end + if props.modRearBumper ~= nil then + SetVehicleMod(vehicle, 2, props.modRearBumper, false) + end + if props.modSideSkirt ~= nil then + SetVehicleMod(vehicle, 3, props.modSideSkirt, false) + end + if props.modExhaust ~= nil then + SetVehicleMod(vehicle, 4, props.modExhaust, false) + end + if props.modFrame ~= nil then + SetVehicleMod(vehicle, 5, props.modFrame, false) + end + if props.modGrille ~= nil then + SetVehicleMod(vehicle, 6, props.modGrille, false) + end + if props.modHood ~= nil then + SetVehicleMod(vehicle, 7, props.modHood, false) + end + if props.modFender ~= nil then + SetVehicleMod(vehicle, 8, props.modFender, false) + end + if props.modRightFender ~= nil then + SetVehicleMod(vehicle, 9, props.modRightFender, false) + end + if props.modRoof ~= nil then + SetVehicleMod(vehicle, 10, props.modRoof, false) + end + + if props.modRoofLivery ~= nil then + SetVehicleRoofLivery(vehicle, props.modRoofLivery) + end + + if props.modEngine ~= nil then + SetVehicleMod(vehicle, 11, props.modEngine, false) + end + if props.modBrakes ~= nil then + SetVehicleMod(vehicle, 12, props.modBrakes, false) + end + if props.modTransmission ~= nil then + SetVehicleMod(vehicle, 13, props.modTransmission, false) + end + if props.modHorns ~= nil then + SetVehicleMod(vehicle, 14, props.modHorns, false) + end + if props.modSuspension ~= nil then + SetVehicleMod(vehicle, 15, props.modSuspension, false) + end + if props.modArmor ~= nil then + SetVehicleMod(vehicle, 16, props.modArmor, false) + end + if props.modTurbo ~= nil then + ToggleVehicleMod(vehicle, 18, props.modTurbo) + end + if props.modXenon ~= nil then + ToggleVehicleMod(vehicle, 22, props.modXenon) + end + if props.modFrontWheels ~= nil then + SetVehicleMod(vehicle, 23, props.modFrontWheels, props.modCustomFrontWheels) + end + if props.modBackWheels ~= nil then + SetVehicleMod(vehicle, 24, props.modBackWheels, props.modCustomBackWheels) + end + if props.modPlateHolder ~= nil then + SetVehicleMod(vehicle, 25, props.modPlateHolder, false) + end + if props.modVanityPlate ~= nil then + SetVehicleMod(vehicle, 26, props.modVanityPlate, false) + end + if props.modTrimA ~= nil then + SetVehicleMod(vehicle, 27, props.modTrimA, false) + end + if props.modOrnaments ~= nil then + SetVehicleMod(vehicle, 28, props.modOrnaments, false) + end + if props.modDashboard ~= nil then + SetVehicleMod(vehicle, 29, props.modDashboard, false) + end + if props.modDial ~= nil then + SetVehicleMod(vehicle, 30, props.modDial, false) + end + if props.modDoorSpeaker ~= nil then + SetVehicleMod(vehicle, 31, props.modDoorSpeaker, false) + end + if props.modSeats ~= nil then + SetVehicleMod(vehicle, 32, props.modSeats, false) + end + if props.modSteeringWheel ~= nil then + SetVehicleMod(vehicle, 33, props.modSteeringWheel, false) + end + if props.modShifterLeavers ~= nil then + SetVehicleMod(vehicle, 34, props.modShifterLeavers, false) + end + if props.modAPlate ~= nil then + SetVehicleMod(vehicle, 35, props.modAPlate, false) + end + if props.modSpeakers ~= nil then + SetVehicleMod(vehicle, 36, props.modSpeakers, false) + end + if props.modTrunk ~= nil then + SetVehicleMod(vehicle, 37, props.modTrunk, false) + end + if props.modHydrolic ~= nil then + SetVehicleMod(vehicle, 38, props.modHydrolic, false) + end + if props.modEngineBlock ~= nil then + SetVehicleMod(vehicle, 39, props.modEngineBlock, false) + end + if props.modAirFilter ~= nil then + SetVehicleMod(vehicle, 40, props.modAirFilter, false) + end + if props.modStruts ~= nil then + SetVehicleMod(vehicle, 41, props.modStruts, false) + end + if props.modArchCover ~= nil then + SetVehicleMod(vehicle, 42, props.modArchCover, false) + end + if props.modAerials ~= nil then + SetVehicleMod(vehicle, 43, props.modAerials, false) + end + if props.modTrimB ~= nil then + SetVehicleMod(vehicle, 44, props.modTrimB, false) + end + if props.modTank ~= nil then + SetVehicleMod(vehicle, 45, props.modTank, false) + end + if props.modWindows ~= nil then + SetVehicleMod(vehicle, 46, props.modWindows, false) + end + + if props.modLivery ~= nil then + SetVehicleMod(vehicle, 48, props.modLivery, false) + SetVehicleLivery(vehicle, props.modLivery) + end + + if props.windowsBroken ~= nil then + for k, v in pairs(props.windowsBroken) do + if v then + k = tonumber(k) + if k then + RemoveVehicleWindow(vehicle, k) + end + end + end + end + + if props.doorsBroken ~= nil then + for k, v in pairs(props.doorsBroken) do + if v then + k = tonumber(k) + if k then + SetVehicleDoorBroken(vehicle, k, true) + end + end + end + end + + if props.tyreBurst ~= nil then + for k, v in pairs(props.tyreBurst) do + if v then + k = tonumber(k) + if k then + SetVehicleTyreBurst(vehicle, k, true, 1000.0) + end + end + end + end +end + +return xLib.game From c32138eedd754903e9e89fcc6ddf5a426fd90f5c Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 33/42] feat(esx_lib/onesync): move onesync query helpers to xLib Moved getPlayersInArea / getClosestPlayer / getPedsInArea / getObjectsInArea / getVehiclesInArea / getClosestPed / getClosestObject / getClosestVehicle to xLib; es_extended re-exposes them through server compat shims. Flagged: getNearbyPlayers iterates ESX.Players (es_extended server state), which is not portable across the lib boundary - the lib relies on that global being present at call time. --- [core]/es_extended/server/compat.lua | 11 +- [core]/es_extended/server/modules/onesync.lua | 160 ----------------- [core]/esx_lib/imports/onesync/server.lua | 164 ++++++++++++++++++ 3 files changed, 174 insertions(+), 161 deletions(-) create mode 100644 [core]/esx_lib/imports/onesync/server.lua diff --git a/[core]/es_extended/server/compat.lua b/[core]/es_extended/server/compat.lua index cc4e3271..9f36b39c 100644 --- a/[core]/es_extended/server/compat.lua +++ b/[core]/es_extended/server/compat.lua @@ -1 +1,10 @@ ---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 +--All server-side functions outsourced from the Core to the lib will be stored here for compatability, e.g: + +ESX.OneSync.GetPlayersInArea = xLib.onesync.getPlayersInArea +ESX.OneSync.GetClosestPlayer = xLib.onesync.getClosestPlayer +ESX.OneSync.GetPedsInArea = xLib.onesync.getPedsInArea +ESX.OneSync.GetObjectsInArea = xLib.onesync.getObjectsInArea +ESX.OneSync.GetVehiclesInArea = xLib.onesync.getVehiclesInArea +ESX.OneSync.GetClosestPed = xLib.onesync.getClosestPed +ESX.OneSync.GetClosestObject = xLib.onesync.getClosestObject +ESX.OneSync.GetClosestVehicle = xLib.onesync.getClosestVehicle \ No newline at end of file diff --git a/[core]/es_extended/server/modules/onesync.lua b/[core]/es_extended/server/modules/onesync.lua index 573dc888..18e179e5 100644 --- a/[core]/es_extended/server/modules/onesync.lua +++ b/[core]/es_extended/server/modules/onesync.lua @@ -1,84 +1,5 @@ ESX.OneSync = {} ----@param source number|vector3 ----@param closest boolean ----@param distance? number ----@param ignore? table ----@param routingBucket? number -local function getNearbyPlayers(source, closest, distance, ignore, routingBucket) - local result = {} - local count = 0 - local playerPed - local playerCoords - ignore = ignore or {} - - if not distance then - distance = 100 - end - - if type(source) == "number" then - playerPed = GetPlayerPed(source) - - if not source then - error("Received invalid first argument (source); should be playerId") - end - - playerCoords = GetEntityCoords(playerPed) - - if not playerCoords then - error("Received nil value (playerCoords); perhaps source is nil at first place?") - end - end - - if type(source) == "vector3" then - playerCoords = source - - if not playerCoords then - error("Received nil value (playerCoords); perhaps source is nil at first place?") - end - end - - for _, xPlayer in pairs(ESX.Players) do - if not ignore[xPlayer.source] and (not routingBucket or GetPlayerRoutingBucket(xPlayer.source) == routingBucket) then - local entity = GetPlayerPed(xPlayer.source) - local coords = GetEntityCoords(entity) - - if not closest then - local dist = #(playerCoords - coords) - if dist <= distance then - count = count + 1 - result[count] = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist } - end - else - if xPlayer.source ~= source then - local dist = #(playerCoords - coords) - if dist <= (result.dist or distance) then - result = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist } - end - end - end - end - end - - return result -end - ----@param source vector3|number playerId or vector3 coordinates ----@param maxDistance number ----@param ignore? table playerIds to ignore, where the key is playerId and value is true ----@param routingBucket? number -function ESX.OneSync.GetPlayersInArea(source, maxDistance, ignore, routingBucket) - return getNearbyPlayers(source, false, maxDistance, ignore, routingBucket) -end - ----@param source vector3|number playerId or vector3 coordinates ----@param maxDistance number ----@param ignore? table playerIds to ignore, where the key is playerId and value is true ----@param routingBucket? number -function ESX.OneSync.GetClosestPlayer(source, maxDistance, ignore, routingBucket) - return getNearbyPlayers(source, true, maxDistance, ignore, routingBucket) -end - ---@param vehicleModel number|string ---@param coords vector3|table ---@param heading number @@ -304,84 +225,3 @@ function ESX.OneSync.SpawnPedInVehicle(model, vehicle, seat, cb) return Citizen.Await(promise) end end - -local function getNearbyEntities(entities, coords, modelFilter, maxDistance, isPed) - local nearbyEntities = {} - coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z) - for _, entity in pairs(entities) do - if not isPed or (isPed and not IsPedAPlayer(entity)) then - if not modelFilter or modelFilter[GetEntityModel(entity)] then - local entityCoords = GetEntityCoords(entity) - if not maxDistance or #(coords - entityCoords) <= maxDistance then - nearbyEntities[#nearbyEntities + 1] = NetworkGetNetworkIdFromEntity(entity) - end - end - end - end - - return nearbyEntities -end - ----@param coords vector3 ----@param maxDistance number ----@param modelFilter table models to ignore, where the key is the model hash and the value is true ----@return table -function ESX.OneSync.GetPedsInArea(coords, maxDistance, modelFilter) - return getNearbyEntities(GetAllPeds(), coords, modelFilter, maxDistance, true) -end - ----@param coords vector3 ----@param maxDistance number ----@param modelFilter table models to ignore, where the key is the model hash and the value is true ----@return table -function ESX.OneSync.GetObjectsInArea(coords, maxDistance, modelFilter) - return getNearbyEntities(GetAllObjects(), coords, modelFilter, maxDistance) -end - ----@param coords vector3 ----@param maxDistance number ----@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true ----@return table -function ESX.OneSync.GetVehiclesInArea(coords, maxDistance, modelFilter) - return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance) -end - -local function getClosestEntity(entities, coords, modelFilter, isPed) - local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0) - coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z) - - for _, entity in pairs(entities) do - if not isPed or (isPed and not IsPedAPlayer(entity)) then - if not modelFilter or modelFilter[GetEntityModel(entity)] then - local entityCoords = GetEntityCoords(entity) - local dist = #(coords - entityCoords) - if dist < distance then - closestEntity, distance, closestCoords = entity, dist, entityCoords - end - end - end - end - - return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords -end - ----@param coords vector3 ----@param modelFilter table models to ignore, where the key is the model hash and the value is true ----@return number entityId, number distance, vector3 coords -function ESX.OneSync.GetClosestPed(coords, modelFilter) - return getClosestEntity(GetAllPeds(), coords, modelFilter, true) -end - ----@param coords vector3 ----@param modelFilter table models to ignore, where the key is the model hash and the value is true ----@return number entityId, number distance, vector3 coords -function ESX.OneSync.GetClosestObject(coords, modelFilter) - return getClosestEntity(GetAllObjects(), coords, modelFilter) -end - ----@param coords vector3 ----@param modelFilter table models to ignore, where the key is the model hash and the value is true ----@return number entityId, number distance, vector3 coords -function ESX.OneSync.GetClosestVehicle(coords, modelFilter) - return getClosestEntity(GetAllVehicles(), coords, modelFilter) -end diff --git a/[core]/esx_lib/imports/onesync/server.lua b/[core]/esx_lib/imports/onesync/server.lua new file mode 100644 index 00000000..c07be074 --- /dev/null +++ b/[core]/esx_lib/imports/onesync/server.lua @@ -0,0 +1,164 @@ +---@class onesynclib +xLib.onesync = {} + +---@param source number|vector3 +---@param closest boolean +---@param distance? number +---@param ignore? table +---@param routingBucket? number +local function getNearbyPlayers(source, closest, distance, ignore, routingBucket) + local result = {} + local count = 0 + local playerPed + local playerCoords + ignore = ignore or {} + + if not distance then + distance = 100 + end + + if type(source) == "number" then + playerPed = GetPlayerPed(source) + + if not source then + error("Received invalid first argument (source); should be playerId") + end + + playerCoords = GetEntityCoords(playerPed) + + if not playerCoords then + error("Received nil value (playerCoords); perhaps source is nil at first place?") + end + end + + if type(source) == "vector3" then + playerCoords = source + + if not playerCoords then + error("Received nil value (playerCoords); perhaps source is nil at first place?") + end + end + + for _, xPlayer in pairs(ESX.Players) do + if not ignore[xPlayer.source] and (not routingBucket or GetPlayerRoutingBucket(xPlayer.source) == routingBucket) then + local entity = GetPlayerPed(xPlayer.source) + local coords = GetEntityCoords(entity) + + if not closest then + local dist = #(playerCoords - coords) + if dist <= distance then + count = count + 1 + result[count] = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist } + end + else + if xPlayer.source ~= source then + local dist = #(playerCoords - coords) + if dist <= (result.dist or distance) then + result = { id = xPlayer.source, ped = NetworkGetNetworkIdFromEntity(entity), coords = coords, dist = dist } + end + end + end + end + end + + return result +end + +---@param source vector3|number playerId or vector3 coordinates +---@param maxDistance number +---@param ignore? table playerIds to ignore, where the key is playerId and value is true +---@param routingBucket? number +function xLib.onesync.getPlayersInArea(source, maxDistance, ignore, routingBucket) + return getNearbyPlayers(source, false, maxDistance, ignore, routingBucket) +end + +---@param source vector3|number playerId or vector3 coordinates +---@param maxDistance number +---@param ignore? table playerIds to ignore, where the key is playerId and value is true +---@param routingBucket? number +function xLib.onesync.getClosestPlayer(source, maxDistance, ignore, routingBucket) + return getNearbyPlayers(source, true, maxDistance, ignore, routingBucket) +end + +local function getNearbyEntities(entities, coords, modelFilter, maxDistance, isPed) + local nearbyEntities = {} + coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z) + for _, entity in pairs(entities) do + if not isPed or (isPed and not IsPedAPlayer(entity)) then + if not modelFilter or modelFilter[GetEntityModel(entity)] then + local entityCoords = GetEntityCoords(entity) + if not maxDistance or #(coords - entityCoords) <= maxDistance then + nearbyEntities[#nearbyEntities + 1] = NetworkGetNetworkIdFromEntity(entity) + end + end + end + end + + return nearbyEntities +end + +---@param coords vector3 +---@param maxDistance number +---@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@return table +function xLib.onesync.getPedsInArea(coords, maxDistance, modelFilter) + return getNearbyEntities(GetAllPeds(), coords, modelFilter, maxDistance, true) +end + +---@param coords vector3 +---@param maxDistance number +---@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@return table +function xLib.onesync.getObjectsInArea(coords, maxDistance, modelFilter) + return getNearbyEntities(GetAllObjects(), coords, modelFilter, maxDistance) +end + +---@param coords vector3 +---@param maxDistance number +---@param modelFilter table | nil models to ignore, where the key is the model hash and the value is true +---@return table +function xLib.onesync.getVehiclesInArea(coords, maxDistance, modelFilter) + return getNearbyEntities(GetAllVehicles(), coords, modelFilter, maxDistance) +end + +local function getClosestEntity(entities, coords, modelFilter, isPed) + local distance, closestEntity, closestCoords = 100, 0, vector3(0, 0, 0) + coords = type(coords) == "number" and GetEntityCoords(GetPlayerPed(coords)) or vector3(coords.x, coords.y, coords.z) + + for _, entity in pairs(entities) do + if not isPed or (isPed and not IsPedAPlayer(entity)) then + if not modelFilter or modelFilter[GetEntityModel(entity)] then + local entityCoords = GetEntityCoords(entity) + local dist = #(coords - entityCoords) + if dist < distance then + closestEntity, distance, closestCoords = entity, dist, entityCoords + end + end + end + end + + return NetworkGetNetworkIdFromEntity(closestEntity), distance, closestCoords +end + +---@param coords vector3 +---@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@return number entityId, number distance, vector3 coords +function xLib.onesync.getClosestPed(coords, modelFilter) + return getClosestEntity(GetAllPeds(), coords, modelFilter, true) +end + +---@param coords vector3 +---@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@return number entityId, number distance, vector3 coords +function xLib.onesync.getClosestObject(coords, modelFilter) + return getClosestEntity(GetAllObjects(), coords, modelFilter) +end + +---@param coords vector3 +---@param modelFilter table models to ignore, where the key is the model hash and the value is true +---@return number entityId, number distance, vector3 coords +function xLib.onesync.getClosestVehicle(coords, modelFilter) + return getClosestEntity(GetAllVehicles(), coords, modelFilter) +end + +return xLib.onesync From 88a883973a3cc5fcf65a7568b96e1e1f0725f3b9 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Tue, 16 Jun 2026 21:03:41 +0200 Subject: [PATCH 34/42] chore(esx_lib/callback): gate callback ownership logs behind xLib:debug --- [core]/esx_lib/resource/callback/shared.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/[core]/esx_lib/resource/callback/shared.lua b/[core]/esx_lib/resource/callback/shared.lua index c8e0c7a2..aae0c0ca 100644 --- a/[core]/esx_lib/resource/callback/shared.lua +++ b/[core]/esx_lib/resource/callback/shared.lua @@ -8,6 +8,7 @@ local registeredCallbacks = {} local resource_name = GetCurrentResourceName() --TODO: Add cache +local IS_DEBUG = GetConvar("xLib:debug", "false") == "true" AddEventHandler('onResourceStop', function(resourceName) @@ -37,13 +38,19 @@ function xLib.setValidCallback(callbackName, isValid) if callbackResource == resourceName then return end - local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource) + if IS_DEBUG then + 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 '')) + print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage, + Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or '')) + end + + return end - print(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName)) + if IS_DEBUG then + print(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName)) + end registeredCallbacks[callbackName] = resourceName end From 6acaedde1faf6dddaf44b4f4636236e14efbf89f Mon Sep 17 00:00:00 2001 From: Astrxw Date: Wed, 17 Jun 2026 15:45:53 +0200 Subject: [PATCH 35/42] refactor(esx_lib/game): preserve original networked guard in spawnVehicle move --- [core]/esx_lib/imports/game/client.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/[core]/esx_lib/imports/game/client.lua b/[core]/esx_lib/imports/game/client.lua index ab285815..c7bdd1e8 100644 --- a/[core]/esx_lib/imports/game/client.lua +++ b/[core]/esx_lib/imports/game/client.lua @@ -98,7 +98,7 @@ function xLib.game.spawnVehicle(vehicleModel, coords, heading, cb, networked) local vehicle = CreateVehicle(model, vector.x, vector.y, vector.z, heading, isNetworked, true) - if isNetworked then + if networked then local id = NetworkGetNetworkIdFromEntity(vehicle) SetNetworkIdCanMigrate(id, true) SetEntityAsMissionEntity(vehicle, true, true) From 409c2ccc63108a1bd906b5e48962692c34dc776f Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Wed, 24 Jun 2026 18:39:07 +0200 Subject: [PATCH 36/42] refactor(esx_lib): move the player-state cache into the lib Adds a generic xLib.cache (ped, vehicle, seat, weapon, coords) modelled on ox_lib cache and framework agnostic, and rewires es_extended to source ped and weapon from it which removes the per-frame ped poll. The vehicle enter/exit state machine and every esx: event are kept unchanged through a thin glue. --- [core]/es_extended/client/modules/actions.lua | 72 ++++++--------- [core]/esx_lib/imports/cache/client.lua | 89 +++++++++++++++++++ 2 files changed, 118 insertions(+), 43 deletions(-) create mode 100644 [core]/esx_lib/imports/cache/client.lua diff --git a/[core]/es_extended/client/modules/actions.lua b/[core]/es_extended/client/modules/actions.lua index de174910..846e8307 100644 --- a/[core]/es_extended/client/modules/actions.lua +++ b/[core]/es_extended/client/modules/actions.lua @@ -1,10 +1,14 @@ +-- ESX glue over the generic xLib.cache. +-- ped and weapon are tracked by the lib cache; this module mirrors them into +-- ESX.PlayerData, re-emits the legacy esx: events resources depend on, and keeps +-- the vehicle enter/exit state machine that produces the richer vehicle events. + Actions = {} Actions._index = Actions Actions.inVehicle = false Actions.enteringVehicle = false Actions.inPauseMenu = false -Actions.currentWeapon = false function Actions:GetSeatPedIsIn() for i = -1, 16 do @@ -55,21 +59,6 @@ function Actions:TrackPedCoordsOnce() end) end -function Actions:TrackPed() - local playerPed = ESX.PlayerData.ped - local newPed = PlayerPedId() - - if playerPed ~= newPed then - ESX.SetPlayerData("ped", newPed) - - TriggerEvent("esx:playerPedChanged", newPed) - - if Config.EnableDebug then - print("[DEBUG] Player ped changed:", newPed) - end - end -end - function Actions:TrackPauseMenu() local isActive = IsPauseMenuActive() @@ -189,46 +178,43 @@ function Actions:TrackSeat() end end -function Actions:TrackWeapon() - ---@type number|false - local newWeapon = GetSelectedPedWeapon(ESX.PlayerData.ped) - newWeapon = newWeapon ~= `WEAPON_UNARMED` and newWeapon or false - - if newWeapon ~= self.currentWeapon then - self.currentWeapon = newWeapon - ESX.SetPlayerData("weapon", self.currentWeapon) - TriggerEvent("esx:weaponChanged", self.currentWeapon) - - if Config.EnableDebug then - print("[DEBUG] Weapon changed:", self.currentWeapon) - end - end -end - function Actions:SlowLoop() CreateThread(function() while ESX.PlayerLoaded do self:TrackPauseMenu() self:TrackVehicle() - self:TrackWeapon() Wait(500) end end) end -function Actions:PedLoop() - CreateThread(function() - while ESX.PlayerLoaded do - self:TrackPed() - Wait(0) - end - end) -end - function Actions:Init() + -- Re-seed the cached values on every (re)login. The change handlers below are + -- registered once at file load so a relogin never stacks duplicate handlers. + ESX.SetPlayerData("ped", xLib.cache.ped) + ESX.SetPlayerData("weapon", xLib.cache.weapon) + self:SlowLoop() - self:PedLoop() self:TrackPedCoordsOnce() end +-- Mirror the lib cache into ESX.PlayerData and re-emit the legacy esx: events. +AddEventHandler("xLib:cache:ped", function(ped) + ESX.SetPlayerData("ped", ped) + TriggerEvent("esx:playerPedChanged", ped) + + if Config.EnableDebug then + print("[DEBUG] Player ped changed:", ped) + end +end) + +AddEventHandler("xLib:cache:weapon", function(weapon) + ESX.SetPlayerData("weapon", weapon) + TriggerEvent("esx:weaponChanged", weapon) + + if Config.EnableDebug then + print("[DEBUG] Weapon changed:", weapon) + end +end) + Actions:Init() diff --git a/[core]/esx_lib/imports/cache/client.lua b/[core]/esx_lib/imports/cache/client.lua new file mode 100644 index 00000000..5ab1c3d2 --- /dev/null +++ b/[core]/esx_lib/imports/cache/client.lua @@ -0,0 +1,89 @@ +--[[ + Generic client-side player state cache, modelled on ox_lib cache. + Framework agnostic: natives only, no ESX coupling. + + Loaded per-resource VM through the lazy loader. PlayerPedId() and friends + return the same value in every client VM, so the cached values are correct + in each resource that requires it. + + Exposes the live values as xLib.cache and emits `xLib:cache:` (value, previous) + whenever a tracked value changes. `coords` is read on demand from the live ped + and never stored. +]] + +local playerId = PlayerId() + +local cache = setmetatable({ + playerId = playerId, + ped = PlayerPedId(), + vehicle = false, + seat = false, + weapon = false, +}, { + __index = function(self, key) + if key == "coords" then + return GetEntityCoords(self.ped) + elseif key == "serverId" then + -- resolved lazily: GetPlayerServerId can return -1 before the network + -- session is ready, so only cache it once it is valid. + local id = GetPlayerServerId(playerId) + if id and id ~= -1 then + rawset(self, "serverId", id) + end + return id + end + end, +}) + +local function set(key, value) + if cache[key] == value then + return + end + + local previous = cache[key] + rawset(cache, key, value) + TriggerEvent(("xLib:cache:%s"):format(key), value, previous) +end + +local function getSeat(ped, vehicle) + for seat = -1, 16 do + if GetPedInVehicleSeat(vehicle, seat) == ped then + return seat + end + end + return false +end + +CreateThread(function() + while true do + local ped = PlayerPedId() + if ped ~= cache.ped then + set("ped", ped) + end + + ---@type integer|false + local vehicle = GetVehiclePedIsIn(ped, false) + vehicle = vehicle ~= 0 and vehicle or false + + if vehicle ~= cache.vehicle then + set("vehicle", vehicle) + set("seat", vehicle and getSeat(ped, vehicle) or false) + elseif vehicle then + local seat = getSeat(ped, vehicle) + if seat ~= cache.seat then + set("seat", seat) + end + end + + ---@type integer|false + local weapon = GetSelectedPedWeapon(ped) + weapon = weapon ~= `WEAPON_UNARMED` and weapon or false + if weapon ~= cache.weapon then + set("weapon", weapon) + end + + Wait(100) + end +end) + +return cache From 2d1facf59dd0cdca074fa637518cbe3472b53736 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Wed, 24 Jun 2026 21:38:16 +0200 Subject: [PATCH 37/42] feat(esx_lib): add a pub/sub multicast module Adds xLib.pubsub for server-driven topic multicast: a producer resource subscribes players to a topic server-side and publishes data that is pushed only to the subscribed players. Clients listen with xLib.pubsub.on and never subscribe or publish themselves. The subscription registry is a single shared instance in the esx_lib server VM, reached cross-resource through exports; the client side is a per-VM listener over one net event. Subscriptions are cleaned up on playerDropped, so a reused serverId never inherits them, and on the owning resource stopping. Also adds xLib.triggerClientEvent, which packs the payload once when sending to many players; publish uses it so a broadcast to N subscribers serialises once. --- [core]/esx_lib/imports/pubsub/client.lua | 79 +++++++ [core]/esx_lib/imports/pubsub/server.lua | 47 ++++ [core]/esx_lib/resource/pubsub/server.lua | 207 ++++++++++++++++++ .../resource/triggerClientEvent/server.lua | 21 ++ 4 files changed, 354 insertions(+) create mode 100644 [core]/esx_lib/imports/pubsub/client.lua create mode 100644 [core]/esx_lib/imports/pubsub/server.lua create mode 100644 [core]/esx_lib/resource/pubsub/server.lua create mode 100644 [core]/esx_lib/resource/triggerClientEvent/server.lua diff --git a/[core]/esx_lib/imports/pubsub/client.lua b/[core]/esx_lib/imports/pubsub/client.lua new file mode 100644 index 00000000..7fd26b65 --- /dev/null +++ b/[core]/esx_lib/imports/pubsub/client.lua @@ -0,0 +1,79 @@ +-- Per-VM client listener for xLib pub/sub. Routes incoming topic data to the +-- handlers registered with xLib.pubsub.on. on() only listens; the player must be +-- subscribed server-side to actually receive anything. + +local PUBSUB_EVENT = '__xLib_pubsub' -- must match resource/pubsub/server.lua + +-- topic -> array of handlers +local handlers = {} + +local pubsub = {} + +---Register a handler for a topic. Multiple handlers per topic are allowed. +---@param topic string +---@param handler fun(data: any, topic: string) +function pubsub.on(topic, handler) + if type(topic) ~= 'string' or type(handler) ~= 'function' then + return + end + + local list = handlers[topic] + if not list then + list = {} + handlers[topic] = list + end + list[#list + 1] = handler +end + +---Remove handlers for a topic. Without a handler argument, removes all of them. +---@param topic string +---@param handler? function +function pubsub.off(topic, handler) + if not handler then + handlers[topic] = nil + return + end + + local list = handlers[topic] + if not list then + return + end + + for i = #list, 1, -1 do + if list[i] == handler then + table.remove(list, i) + end + end + + if list[1] == nil then + handlers[topic] = nil + end +end + +RegisterNetEvent(PUBSUB_EVENT, function(topic, data) + local list = handlers[topic] + if not list then + return + end + + -- fast path: a topic almost always has a single handler, so skip the copy + local n = #list + if n == 1 then + list[1](data, topic) + return + end + + -- snapshot so a handler that calls off() mid-dispatch cannot shift the list + -- and skip a sibling that has not run yet + local snapshot = {} + for i = 1, n do + snapshot[i] = list[i] + end + + for i = 1, n do + snapshot[i](data, topic) + end +end) + +xLib.pubsub = pubsub +return pubsub diff --git a/[core]/esx_lib/imports/pubsub/server.lua b/[core]/esx_lib/imports/pubsub/server.lua new file mode 100644 index 00000000..585c21dd --- /dev/null +++ b/[core]/esx_lib/imports/pubsub/server.lua @@ -0,0 +1,47 @@ +-- Per-VM wrapper exposing xLib.pubsub.* on the server; forwards to the shared +-- registry (resource/pubsub/server.lua) through its exports. + +local pubsub = {} + +---@param src number serverId of the player to add +---@param topic string +---@return boolean added +function pubsub.subscribe(src, topic) + return xLib.pubsub_subscribe(src, topic) +end + +---@param src number +---@param topic string +---@return boolean removed +function pubsub.unsubscribe(src, topic) + return xLib.pubsub_unsubscribe(src, topic) +end + +---@param topic string +---@param data any server-trusted data only +---@return integer count number of players notified +function pubsub.publish(topic, data) + return xLib.pubsub_publish(topic, data) +end + +---@param topic string +---@return number[] serverIds +function pubsub.subscribers(topic) + return xLib.pubsub_subscribers(topic) +end + +---@param src number +---@param topic string +---@return boolean +function pubsub.isSubscribed(src, topic) + return xLib.pubsub_isSubscribed(src, topic) +end + +---@param topic string +---@return integer count +function pubsub.clear(topic) + return xLib.pubsub_clear(topic) +end + +xLib.pubsub = pubsub +return pubsub diff --git a/[core]/esx_lib/resource/pubsub/server.lua b/[core]/esx_lib/resource/pubsub/server.lua new file mode 100644 index 00000000..65c725cd --- /dev/null +++ b/[core]/esx_lib/resource/pubsub/server.lua @@ -0,0 +1,207 @@ +-- xLib pub/sub: a producer subscribes players to a topic server-side and +-- publishes data pushed only to them. Clients only listen, never subscribe. +-- The registry is shared here (esx_lib server VM). Namespace topics by resource +-- (e.g. "esx_shops:247:stock") so two resources cannot clash on a bare name. + +---@diagnostic disable: duplicate-set-field + +local PUBSUB_EVENT = '__xLib_pubsub' -- must match imports/pubsub/client.lua +local TOPIC_PATTERN = '^[%w_:%.%-]+$' +local MAX_TOPIC_LEN = 200 +local resourceName = GetCurrentResourceName() + +-- topic -> set of subscribed serverIds: subs[topic][src] = true +local subs = {} +-- serverId -> set of topics: memberOf[src][topic] = true (O(degree) cleanup on drop) +local memberOf = {} +-- topic -> resource that created it, purged on that resource's onResourceStop +local ownerOf = {} + +local function isValidTopic(topic) + return type(topic) == 'string' + and #topic > 0 + and #topic <= MAX_TOPIC_LEN + and topic:match(TOPIC_PATTERN) ~= nil +end + +local function isLivePlayer(src) + -- a connected player has a name; rejects 0, stale or recycled serverIds + return type(src) == 'number' and src > 0 and GetPlayerName(src) ~= nil +end + +local function detach(src, topic) + local set = subs[topic] + if set then + set[src] = nil + if next(set) == nil then + subs[topic] = nil + ownerOf[topic] = nil + end + end + + local topics = memberOf[src] + if topics then + topics[topic] = nil + if next(topics) == nil then + memberOf[src] = nil + end + end +end + +---Subscribe a player to a topic. Called server-side by the producer resource. +---@param src number serverId of the player to add +---@param topic string +---@return boolean added false if already subscribed or input is invalid +function xLib.pubsub_subscribe(src, topic) + if not isLivePlayer(src) or not isValidTopic(topic) then + return false + end + + local set = subs[topic] + if set and set[src] then + return false -- idempotent: already subscribed + end + + if not set then + set = {} + subs[topic] = set + ownerOf[topic] = GetInvokingResource() or resourceName + end + set[src] = true + + local topics = memberOf[src] + if not topics then + topics = {} + memberOf[src] = topics + end + topics[topic] = true + + return true +end + +---Unsubscribe a player from a topic. +---@param src number +---@param topic string +---@return boolean removed false if it was not subscribed +function xLib.pubsub_unsubscribe(src, topic) + local set = subs[topic] + if not set or not set[src] then + return false -- idempotent + end + detach(src, topic) + return true +end + +---Push data to every player subscribed to a topic. Server-trusted data only. +---@param topic string +---@param data any +---@return integer count number of players notified +function xLib.pubsub_publish(topic, data) + -- no isValidTopic() here on purpose: an invalid topic never created a set, + -- so the lookup below already returns 0 for it without a regex on the hot path + local set = subs[topic] + if not set then + return 0 + end + + local targets, count = {}, 0 + for src in pairs(set) do + count = count + 1 + targets[count] = src + end + + -- packs the payload once for the whole set instead of once per client + xLib.triggerClientEvent(PUBSUB_EVENT, targets, topic, data) + return count +end + +---@param topic string +---@return number[] serverIds a fresh copy, empty if the topic has no subscribers +function xLib.pubsub_subscribers(topic) + local set = subs[topic] + if not set then + return {} + end + + local list, i = {}, 0 + for src in pairs(set) do + i = i + 1 + list[i] = src + end + return list +end + +---@param src number +---@param topic string +---@return boolean +function xLib.pubsub_isSubscribed(src, topic) + local set = subs[topic] + return set ~= nil and set[src] == true +end + +---Drop a whole topic and all of its subscribers. +---@param topic string +---@return integer count number of subscribers that were removed +function xLib.pubsub_clear(topic) + local set = subs[topic] + if not set then + return 0 + end + + local count = 0 + for src in pairs(set) do + local topics = memberOf[src] + if topics then + topics[topic] = nil + if next(topics) == nil then + memberOf[src] = nil + end + end + count = count + 1 + end + + subs[topic] = nil + ownerOf[topic] = nil + return count +end + +-- Remove a leaving player from every topic. serverIds are reused by the server, +-- so skipping this would leak a topic's data to whoever inherits the slot. +AddEventHandler('playerDropped', function() + local src = source + local topics = memberOf[src] + if not topics then + return + end + + for topic in pairs(topics) do + local set = subs[topic] + if set then + set[src] = nil + if next(set) == nil then + subs[topic] = nil + ownerOf[topic] = nil + end + end + end + memberOf[src] = nil +end) + +-- Purge the topics a stopped producer owned, so a producer restart never leaves +-- orphan subscriptions that keep receiving pushes. +AddEventHandler('onResourceStop', function(stopped) + if stopped == resourceName then + return + end + + local orphaned = {} + for topic, owner in pairs(ownerOf) do + if owner == stopped then + orphaned[#orphaned + 1] = topic + end + end + + for i = 1, #orphaned do + xLib.pubsub_clear(orphaned[i]) + end +end) diff --git a/[core]/esx_lib/resource/triggerClientEvent/server.lua b/[core]/esx_lib/resource/triggerClientEvent/server.lua new file mode 100644 index 00000000..8f5d0412 --- /dev/null +++ b/[core]/esx_lib/resource/triggerClientEvent/server.lua @@ -0,0 +1,21 @@ +-- Triggers an event for one or more clients. For an array of players the payload +-- is packed once instead of being re-serialised per client. + +---@diagnostic disable: duplicate-set-field + +local pack = msgpack.pack_args + +---@param eventName string +---@param targets number | number[] a single serverId, or an array of them +---@param ... any +function xLib.triggerClientEvent(eventName, targets, ...) + if type(targets) == 'number' then + return TriggerClientEvent(eventName, targets, ...) + end + + local payload = pack(...) + local length = #payload + for i = 1, #targets do + TriggerClientEventInternal(eventName, targets[i], payload, length) + end +end From cf61bbd0c394815358d3993da6a9e2240322c93c Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Wed, 15 Jul 2026 16:23:01 +0200 Subject: [PATCH 38/42] feat(esx_lib): add xLib.waitFor --- [core]/esx_lib/imports/waitFor/shared.lua | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 [core]/esx_lib/imports/waitFor/shared.lua diff --git a/[core]/esx_lib/imports/waitFor/shared.lua b/[core]/esx_lib/imports/waitFor/shared.lua new file mode 100644 index 00000000..d680dc93 --- /dev/null +++ b/[core]/esx_lib/imports/waitFor/shared.lua @@ -0,0 +1,45 @@ +---Yields the current thread until the callback returns a non-nil value. +---@generic T +---@param cb fun(): T? +---@param errMessage? string +---@param timeout? number | false Error out after `~x` ms if the callback hasn't resolved. Defaults to 1000, unless set to `false`. +---@param interval? integer Polling interval in ms. Defaults to 0. A value above `timeout` overshoots it by one cycle. +---@return T +---@async +function xLib.waitFor(cb, errMessage, timeout, interval) + xLib.verify(cb, 'function', true) + + if interval then + xLib.verify(interval, 'int', true) + end + + local value = cb() + + if value ~= nil then + return value + end + + if timeout ~= false and type(timeout) ~= 'number' then + timeout = 1000 + end + + local start = timeout and GetGameTimer() + + while value == nil do + Wait(interval or 0) + + value = cb() + + if value == nil and timeout then + local elapsed = GetGameTimer() - start + + if elapsed > timeout then + return error(('%s (waited %.1fms)'):format(errMessage or 'failed to resolve callback', elapsed), 2) + end + end + end + + return value +end + +return xLib.waitFor From 40139a1a4a22b8ae0f7a6cd5077c032f4f639fb2 Mon Sep 17 00:00:00 2001 From: Astrxw <116487456+ASTROWwwW@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:20:18 +0200 Subject: [PATCH 39/42] Update shared.lua --- [core]/esx_lib/imports/waitFor/shared.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/[core]/esx_lib/imports/waitFor/shared.lua b/[core]/esx_lib/imports/waitFor/shared.lua index d680dc93..c86ccb78 100644 --- a/[core]/esx_lib/imports/waitFor/shared.lua +++ b/[core]/esx_lib/imports/waitFor/shared.lua @@ -20,7 +20,7 @@ function xLib.waitFor(cb, errMessage, timeout, interval) end if timeout ~= false and type(timeout) ~= 'number' then - timeout = 1000 + timeout = 5000 end local start = timeout and GetGameTimer() From 9a4c492b51e09511d3cd1a18cc8132921ce86dcf Mon Sep 17 00:00:00 2001 From: _Not_ Date: Thu, 16 Jul 2026 00:55:38 -0500 Subject: [PATCH 40/42] Add initialization for ESX.Table in compat.lua --- [core]/es_extended/shared/compat.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/[core]/es_extended/shared/compat.lua b/[core]/es_extended/shared/compat.lua index 69f99c05..d0d0d9f0 100644 --- a/[core]/es_extended/shared/compat.lua +++ b/[core]/es_extended/shared/compat.lua @@ -1,5 +1,7 @@ --All shared functions outsourced from the Core to the lib will be stored here for compatability, e.g: +ESX.Table = {} + ESX.SetTimeout = xLib.timeout.setTimeout ESX.ClearTimeout = xLib.timeout.clearTimeout ESX.Await = xLib.waitFor From 915332b092d154b65b32f61ad5f6b983b6b2c051 Mon Sep 17 00:00:00 2001 From: _Not_ Date: Thu, 16 Jul 2026 00:58:00 -0500 Subject: [PATCH 41/42] Fix typo in ESX.Table.SizeOf assignment in compat.lua --- [core]/es_extended/shared/compat.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/[core]/es_extended/shared/compat.lua b/[core]/es_extended/shared/compat.lua index d0d0d9f0..9894d149 100644 --- a/[core]/es_extended/shared/compat.lua +++ b/[core]/es_extended/shared/compat.lua @@ -5,7 +5,7 @@ ESX.Table = {} ESX.SetTimeout = xLib.timeout.setTimeout ESX.ClearTimeout = xLib.timeout.clearTimeout ESX.Await = xLib.waitFor -ESX.Table.SizeOf = xLib.table.size +ESX.Table.SizeOf = xLib.table.sizeOf ESX.Table.Set = xLib.table.set ESX.Table.IndexOf = xLib.table.indexOf ESX.Table.LastIndexOf = xLib.table.lastIndexOf From a33913aa4110be52211185152d796126d37fecc5 Mon Sep 17 00:00:00 2001 From: ASTROWwwW Date: Thu, 16 Jul 2026 14:51:53 +0200 Subject: [PATCH 42/42] fix(esx_lib): stop the table module from overwriting the stdlib --- [core]/esx_lib/imports/table/shared.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/[core]/esx_lib/imports/table/shared.lua b/[core]/esx_lib/imports/table/shared.lua index ed49db65..1a590f34 100644 --- a/[core]/esx_lib/imports/table/shared.lua +++ b/[core]/esx_lib/imports/table/shared.lua @@ -1,5 +1,5 @@ ----@class tablelib -xLib.table = table +---@class xtable : tablelib +xLib.table = setmetatable({}, { __index = table }) ---@param tbl table ---@return boolean