mirror of
https://github.com/esx-framework/esx_core.git
synced 2026-08-29 02:01:31 +00:00
init
This commit is contained in:
@@ -15,6 +15,7 @@ files {
|
||||
}
|
||||
|
||||
shared_scripts {
|
||||
'resource/init.lua',
|
||||
'resource/**/shared.lua',
|
||||
}
|
||||
|
||||
|
||||
+29
-20
@@ -9,21 +9,22 @@
|
||||
|
||||
-- Const Variables
|
||||
local CONTEXT<const> = IsDuplicityVersion() and 'server' or 'client'
|
||||
local LIB_NAME<const> = 'esx_lib'
|
||||
local LIB_NAME <const> = 'esx_lib'
|
||||
local IS_DEBUG <const> = GetConvar('xLib:debug', 'false') == 'true'
|
||||
|
||||
-- xLib class for Language Server
|
||||
|
||||
---@alias Module table | function
|
||||
---@class xLib
|
||||
---@field name string
|
||||
---@field side 'server' | 'client'
|
||||
|
||||
---@alias Module table | function
|
||||
|
||||
-------------------------------------------------
|
||||
--- Core functions for modules
|
||||
-------------------------------------------------
|
||||
|
||||
--- Function that returns nothing
|
||||
local function noop() end
|
||||
--- Used as a "shortcut"
|
||||
function Noop() end
|
||||
|
||||
---Loads a module into memory
|
||||
---@param self xLib
|
||||
@@ -31,20 +32,19 @@ local function noop() end
|
||||
---@return Module | nil
|
||||
local function loadModule(self, name)
|
||||
local directory = ('imports/%s'):format(name)
|
||||
local chunk = LoadResourceFile(LIB_NAME, ('%s/%s.lua'):format(directory, CONTEXT)) -- returns a raw lua code as a string (SERVER/CLIENT side)
|
||||
local shared_chunk = LoadResourceFile(LIB_NAME, ('%s/shared.lua'):format(directory)) -- returns a raw lua code as a string (SHARED)
|
||||
local chunk = LoadResourceFile(LIB_NAME, ('%s/%s.lua'):format(directory, CONTEXT))
|
||||
local shared_chunk = LoadResourceFile(LIB_NAME, ('%s/shared.lua'):format(directory))
|
||||
|
||||
if shared_chunk then
|
||||
chunk = chunk and ('%s\n%s'):format(shared_chunk, chunk) or shared_chunk -- creates a string with shared and client/server code combined
|
||||
chunk = chunk and ('%s\n%s'):format(shared_chunk, chunk) or shared_chunk
|
||||
end
|
||||
|
||||
if not chunk then
|
||||
-- Early exit if the code doesn't exist
|
||||
return
|
||||
return
|
||||
end
|
||||
|
||||
-- Second argument is a chunk name
|
||||
local fn, err = load(chunk, ('@%s/imports/%s/%s.lua'):format(LIB_NAME, name, CONTEXT))
|
||||
local fn, err = load(chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, CONTEXT))
|
||||
|
||||
if not fn or err then
|
||||
if shared_chunk then
|
||||
@@ -52,16 +52,16 @@ local function loadModule(self, name)
|
||||
name, LIB_NAME))
|
||||
|
||||
-- Tries to load only shared.lua
|
||||
fn, err = load(shared_chunk, ('@%s/imports/%s/%s.lua'):format(LIB_NAME, name, 'shared'))
|
||||
fn, err = load(shared_chunk, ('@@%s/imports/%s/%s.lua'):format(LIB_NAME, name, 'shared'))
|
||||
end
|
||||
|
||||
if not fn or err then
|
||||
return error(('Completly field importing module - %s; error - %s'):format(name, err))
|
||||
return error(('Completly failed importing module - %s; error - %s'):format(name, err))
|
||||
end
|
||||
end
|
||||
|
||||
local result = fn()
|
||||
self[name] = result or noop
|
||||
self[name] = result or Noop
|
||||
|
||||
return self[name]
|
||||
end
|
||||
@@ -76,12 +76,20 @@ local function call(self, index, ...)
|
||||
|
||||
-- Module Loading
|
||||
if not module then
|
||||
self[index] = noop -- to prevent module from loading again if doesn't exists.
|
||||
self[index] = Noop -- to prevent module from loading again if doesn't exists.
|
||||
|
||||
module = loadModule(self, index)
|
||||
|
||||
if not module then
|
||||
return error(('[%s] Tried to access an invalid module - %s!'):format(LIB_NAME, index))
|
||||
local function method(...)
|
||||
return exports[index](nil, ...)
|
||||
end
|
||||
|
||||
if not ... then
|
||||
self[index] = method
|
||||
end
|
||||
|
||||
return method
|
||||
end
|
||||
end
|
||||
|
||||
@@ -93,12 +101,12 @@ end
|
||||
--- Environment Setup
|
||||
-------------------------------------------------
|
||||
|
||||
|
||||
-- Creation of xLib object
|
||||
---@class xLib
|
||||
---@diagnostic disable-next-line: lowercase-global
|
||||
local xLib = setmetatable({
|
||||
name = LIB_NAME,
|
||||
side = CONTEXT
|
||||
side = CONTEXT,
|
||||
debug = IS_DEBUG
|
||||
}, {
|
||||
-- Lazy Loading - loads module only when accessed
|
||||
__index = call,
|
||||
@@ -106,4 +114,5 @@ local xLib = setmetatable({
|
||||
})
|
||||
|
||||
-- Allows to xLib to be accessible in resource that imports a lib
|
||||
_ENV.xLib = xLib
|
||||
_ENV.xLib = xLib
|
||||
_ENV.require = xLib.require
|
||||
@@ -0,0 +1,148 @@
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local pendingCallbacks = {}
|
||||
local timers = {}
|
||||
local cbEvent = '__xLib_cb_%s'
|
||||
local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000)
|
||||
local resource_name = GetCurrentResourceName() --TODO: Add cache
|
||||
|
||||
RegisterNetEvent(cbEvent:format(resource_name), function(key, ...)
|
||||
if source == '' then return end
|
||||
|
||||
local cb = pendingCallbacks[key]
|
||||
|
||||
if not cb then return end
|
||||
|
||||
pendingCallbacks[key] = nil
|
||||
|
||||
cb(...)
|
||||
end)
|
||||
|
||||
---@param event string
|
||||
---@param delay? number | false prevent the event from being called for the given time
|
||||
local function eventTimer(event, delay)
|
||||
if xLib.verify(delay, 'number') then
|
||||
if delay > 0 then
|
||||
local time = GetGameTimer()
|
||||
|
||||
if (timers[event] or 0) > time then
|
||||
return false
|
||||
end
|
||||
|
||||
timers[event] = time + delay
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@param _ any
|
||||
---@param event string
|
||||
---@param delay number | false | nil
|
||||
---@param cb function | false
|
||||
---@param ... any
|
||||
---@return ...
|
||||
local function triggerServerCallback(_, event, delay, cb, ...)
|
||||
if not eventTimer(event, delay) then return end
|
||||
|
||||
local key
|
||||
|
||||
repeat
|
||||
key = ('%s:%s'):format(event, math.random(0, 100000))
|
||||
until not pendingCallbacks[key]
|
||||
|
||||
TriggerServerEvent('xLib:validateCallback', event, resource_name, key)
|
||||
TriggerServerEvent(cbEvent:format(event), resource_name, key, ...)
|
||||
|
||||
---@type promise | false
|
||||
local promise = not cb and promise.new()
|
||||
|
||||
pendingCallbacks[key] = function(response, ...)
|
||||
if response == 'cb_invalid' then
|
||||
response = ("callback '%s' does not exist"):format(event)
|
||||
|
||||
return promise and promise:reject(response) or error(response)
|
||||
end
|
||||
|
||||
response = { response, ... }
|
||||
|
||||
if promise then
|
||||
return promise:resolve(response)
|
||||
end
|
||||
|
||||
if cb then
|
||||
cb(table.unpack(response))
|
||||
end
|
||||
end
|
||||
|
||||
if promise then
|
||||
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
|
||||
|
||||
return table.unpack(Citizen.Await(promise))
|
||||
end
|
||||
end
|
||||
|
||||
---@overload fun(event: string, delay: number | false, cb: function, ...)
|
||||
xLib.callback = setmetatable({}, {
|
||||
__call = function(_, event, delay, cb, ...)
|
||||
if not cb then
|
||||
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning")
|
||||
:format(event))
|
||||
else
|
||||
local cbType = type(cb)
|
||||
|
||||
if cbType == 'table' and getmetatable(cb)?.__call then
|
||||
cbType = 'function'
|
||||
end
|
||||
|
||||
xLib.verify(cbType, 'function', true)
|
||||
end
|
||||
|
||||
return triggerServerCallback(_, event, delay, cb, ...)
|
||||
end
|
||||
})
|
||||
|
||||
---@param event string
|
||||
---@param delay? number | false prevent the event from being called for the given time.
|
||||
---Sends an event to the server and halts the current thread until a response is returned.
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function xLib.callback.await(event, delay, ...)
|
||||
return triggerServerCallback(nil, event, delay, false, ...)
|
||||
end
|
||||
|
||||
local function callbackResponse(success, result, ...)
|
||||
if not success then
|
||||
if result then
|
||||
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
|
||||
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return result, ...
|
||||
end
|
||||
|
||||
local pcall = pcall
|
||||
|
||||
---@param name string
|
||||
---@param cb function
|
||||
---Registers an event handler and callback function to respond to server requests.
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function xLib.callback.register(name, cb)
|
||||
local event = cbEvent:format(name)
|
||||
|
||||
xLib.setValidCallback(name, true)
|
||||
|
||||
RegisterNetEvent(event, function(resource, key, ...)
|
||||
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
|
||||
end)
|
||||
end
|
||||
|
||||
return xLib.callback
|
||||
@@ -0,0 +1,127 @@
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local pendingCallbacks = {}
|
||||
local cbEvent = '__xLib_cb_%s'
|
||||
local callbackTimeout = GetConvarInt('xLib:callbackTimeout', 300000)
|
||||
local resource_name = GetCurrentResourceName() --TODO: Add cache
|
||||
|
||||
RegisterNetEvent(cbEvent:format(resource_name), function(key, ...)
|
||||
local cb = pendingCallbacks[key]
|
||||
|
||||
if not cb then return end
|
||||
|
||||
pendingCallbacks[key] = nil
|
||||
|
||||
cb(...)
|
||||
end)
|
||||
|
||||
---@param _ any
|
||||
---@param event string
|
||||
---@param playerId number
|
||||
---@param cb function|false
|
||||
---@param ... any
|
||||
---@return ...
|
||||
local function triggerClientCallback(_, event, playerId, cb, ...)
|
||||
xLib.verify(playerId, 'playerId', true)
|
||||
|
||||
local key
|
||||
|
||||
repeat
|
||||
key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId)
|
||||
until not pendingCallbacks[key]
|
||||
|
||||
TriggerClientEvent('xLib:validateCallback', playerId, event, resource_name, key)
|
||||
TriggerClientEvent(cbEvent:format(event), playerId, resource_name, key, ...)
|
||||
|
||||
---@type promise | false
|
||||
local promise = not cb and promise.new()
|
||||
|
||||
pendingCallbacks[key] = function(response, ...)
|
||||
if response == 'cb_invalid' then
|
||||
response = ("callback '%s' does not exist"):format(event)
|
||||
|
||||
return promise and promise:reject(response) or error(response)
|
||||
end
|
||||
|
||||
response = { response, ... }
|
||||
|
||||
if promise then
|
||||
return promise:resolve(response)
|
||||
end
|
||||
|
||||
if cb then
|
||||
cb(table.unpack(response))
|
||||
end
|
||||
end
|
||||
|
||||
if promise then
|
||||
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
|
||||
|
||||
return table.unpack(Citizen.Await(promise))
|
||||
end
|
||||
end
|
||||
|
||||
---@overload fun(event: string, playerId: number, cb: function, ...)
|
||||
xLib.callback = setmetatable({}, {
|
||||
__call = function(_, event, playerId, cb, ...)
|
||||
if not cb then
|
||||
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse xLib.callback.await or a regular event to remove this warning")
|
||||
:format(event))
|
||||
else
|
||||
local cbType = type(cb)
|
||||
|
||||
if cbType == 'table' and getmetatable(cb)?.__call then
|
||||
cbType = 'function'
|
||||
end
|
||||
|
||||
xLib.verify(cbType, 'function', true)
|
||||
end
|
||||
|
||||
return triggerClientCallback(_, event, playerId, cb, ...)
|
||||
end
|
||||
})
|
||||
|
||||
---@param event string
|
||||
---@param playerId number
|
||||
--- Sends an event to a client and halts the current thread until a response is returned.
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function xLib.callback.await(event, playerId, ...)
|
||||
return triggerClientCallback(nil, event, playerId, false, ...)
|
||||
end
|
||||
|
||||
local function callbackResponse(success, result, ...)
|
||||
if not success then
|
||||
if result then
|
||||
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
|
||||
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return result, ...
|
||||
end
|
||||
|
||||
local pcall = pcall
|
||||
|
||||
---@param name string
|
||||
---@param cb function
|
||||
---Registers an event handler and callback function to respond to client requests.
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function xLib.callback.register(name, cb)
|
||||
event = cbEvent:format(name)
|
||||
|
||||
xLib.setValidCallback(name, true)
|
||||
|
||||
RegisterNetEvent(event, function(resource, key, ...)
|
||||
TriggerClientEvent(cbEvent:format(resource), source, key, callbackResponse(pcall(cb, source, ...)))
|
||||
end)
|
||||
end
|
||||
|
||||
return xLib.callback
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
xLib.colors = {}
|
||||
|
||||
xLib.colors.brand = GetConvar("esx:brand-color", "#FB9B04")
|
||||
xLib.colors.darkest = GetConvar("esx:darkest-color", "#161616")
|
||||
xLib.colors.dark = GetConvar("esx:dark-color", "#252525")
|
||||
xLib.colors.mid = GetConvar("esx:mid-color", "#383838")
|
||||
xLib.colors.light = GetConvar("esx:light-color", "#969696")
|
||||
xLib.colors.lightest = GetConvar("esx:lightest-color", "#F2F2F2")
|
||||
|
||||
return xLib.colors
|
||||
@@ -0,0 +1,66 @@
|
||||
local overloads <const> = {}
|
||||
|
||||
---@param name string
|
||||
---@param ... unknown
|
||||
local function getOverloadFunction(name, ...)
|
||||
local params = {...}
|
||||
local params_count = #params
|
||||
local is_valid, overload, correct_type
|
||||
|
||||
|
||||
for i=1, #overloads[name] do
|
||||
overload = overloads[name][i]
|
||||
|
||||
if not overload then
|
||||
goto continue
|
||||
end
|
||||
|
||||
is_valid = #overload.valid_types == params_count
|
||||
|
||||
if is_valid then
|
||||
for j = 1, #overload.valid_types do
|
||||
correct_type = overload.valid_types[j]
|
||||
if not xLib.verify(params[j], correct_type) then
|
||||
is_valid = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if is_valid then
|
||||
return overload.cb(...)
|
||||
end
|
||||
end
|
||||
|
||||
::continue::
|
||||
end
|
||||
|
||||
error('[xLib] Couldn\'t find a correct method to overload')
|
||||
end
|
||||
|
||||
---Overloads a function
|
||||
---@param name string
|
||||
---@param valid_types CustomType | CustomType[]
|
||||
---@param cb function
|
||||
---@param obj? table
|
||||
function xLib.overload(name, valid_types, cb, obj)
|
||||
xLib.verify(name, {'string'}, true)
|
||||
|
||||
if not overloads[name] then
|
||||
overloads[name] = {}
|
||||
end
|
||||
|
||||
overloads[name][#overloads[name] + 1] = {
|
||||
valid_types = valid_types,
|
||||
cb = cb
|
||||
}
|
||||
|
||||
local env = obj and obj or _ENV
|
||||
|
||||
if not env[name] then
|
||||
env[name] = function(...)
|
||||
return getOverloadFunction(name, ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return xLib.overload
|
||||
@@ -0,0 +1,200 @@
|
||||
--!DISCLAIMER
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local LOADED <const> = {}
|
||||
local TEMP_DATA <const> = {}
|
||||
|
||||
local _require = require --original lua function
|
||||
|
||||
package = {
|
||||
path = './?.lua;./?/init.lua',
|
||||
preload = {},
|
||||
loaded = setmetatable({}, {
|
||||
__index = LOADED,
|
||||
__newindex = Noop,
|
||||
__metatable = false,
|
||||
})
|
||||
}
|
||||
|
||||
---Gets the resource name and the module's relative path
|
||||
---@param name string -- module name
|
||||
---@return string, string
|
||||
local function getModuleInfo(name)
|
||||
local resource = name:match('^@(.-)/.+') -- if name is for example @esx_lib/imports/require/initl.lua it will capture ESX_LIB
|
||||
|
||||
if resource then
|
||||
return resource, name:sub(#resource + 3) -- returns the path without script name
|
||||
end
|
||||
|
||||
local idx = 4-- call stack depth (kept slightly lower than expected depth "just in case")
|
||||
--- When indexing source of 4 it returns @@esx_lib/imports/require
|
||||
--- When indexing source of 5 it returns for example @@esx_test/client.lua
|
||||
--- Used for identifing resource name.
|
||||
|
||||
while true do
|
||||
local src = debug.getinfo(idx, 'S')?.source
|
||||
|
||||
if not src then
|
||||
error(('Couldn\'t find a source for "%s"'):format(name))
|
||||
end
|
||||
|
||||
resource = src:match('^@@([^/]+)/.+') -- returns resource name
|
||||
|
||||
if resource and not src:find('^@@esx_lib/imports/require') then
|
||||
return resource, name
|
||||
end
|
||||
|
||||
idx += 1
|
||||
end
|
||||
end
|
||||
|
||||
---Searcher for a accessable path
|
||||
---@param name string
|
||||
---@param path string
|
||||
---@return string?, string? -- filename, error message
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function package.searchpath(name, path)
|
||||
local resource, module_name = getModuleInfo(name:gsub('%.', '/'))
|
||||
|
||||
local tried <const> = {}
|
||||
|
||||
for template in path:gmatch('[^;]+') do
|
||||
local file_name = template:gsub('^%./', ''):gsub('?', module_name:gsub('%.', '/') or module_name)
|
||||
|
||||
local file = LoadResourceFile(resource, file_name)
|
||||
|
||||
if file then
|
||||
TEMP_DATA[1] = file
|
||||
TEMP_DATA[2] = resource
|
||||
|
||||
return file_name
|
||||
end
|
||||
|
||||
tried[#tried+1] = ('no file "@%s/%s"'):format(resource, file_name)
|
||||
end
|
||||
|
||||
return nil, table.concat(tried, '\n\t')
|
||||
end
|
||||
|
||||
---Loads module
|
||||
---@param name string
|
||||
---@param env? unknown
|
||||
---@return function?, string?
|
||||
local function loadModule(name, env)
|
||||
local file_name, err = package.searchpath(name, package.path)
|
||||
|
||||
if file_name then
|
||||
local file = TEMP_DATA[1]
|
||||
local resource = TEMP_DATA[2]
|
||||
|
||||
table.wipe(TEMP_DATA)
|
||||
|
||||
return assert(load(file, ('@@%s/%s'):format(resource, file_name), 't', env or _ENV))
|
||||
end
|
||||
|
||||
return nil, err or 'unknown error'
|
||||
end
|
||||
|
||||
---@diagnostic disable-next-line: duplicate-doc-alias
|
||||
---@alias PackageSearcher
|
||||
---| fun(name: string): function loader
|
||||
---| fun(name: string): nil|false , string error message
|
||||
---| fun(name: string): function?, string?
|
||||
|
||||
---@type PackageSearcher[]
|
||||
package.searchers = {
|
||||
function (name)
|
||||
local ok, result = pcall(_require, name)
|
||||
|
||||
if ok then
|
||||
return result
|
||||
end
|
||||
|
||||
return ok, result
|
||||
end,
|
||||
function (name)
|
||||
if package.preload[name] ~= nil then
|
||||
return package.preload[name]
|
||||
end
|
||||
|
||||
return nil, ('no field package.preload["%s"]'):format(name)
|
||||
end,
|
||||
function(name)
|
||||
return loadModule(name)
|
||||
end,
|
||||
}
|
||||
|
||||
---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use.
|
||||
---@param file_path string
|
||||
---@param env? unknown
|
||||
function xLib.load(file_path, env)
|
||||
xLib.verify(file_path, 'string', true)
|
||||
|
||||
local result, err = loadModule(file_path, env)
|
||||
|
||||
if result then
|
||||
return result()
|
||||
end
|
||||
|
||||
error(('file "%s" not found\n\t%s'):format(file_path, err))
|
||||
end
|
||||
|
||||
---Loads and decodes a json file at the given path.
|
||||
---@param file_path string
|
||||
function xLib.loadJson(file_path)
|
||||
xLib.verify(file_path, 'string', true)
|
||||
|
||||
local res_source, mod_path = getModuleInfo(file_path:gsub('%.', '/'))
|
||||
|
||||
local res_file = LoadResourceFile(res_source, ('%s.json'):format(mod_path))
|
||||
|
||||
if res_file then
|
||||
return json.decode(res_file)
|
||||
end
|
||||
|
||||
error(('json file "%s" not found\n\tno file "%s/%s.json"'):format(file_path, res_source, mod_path))
|
||||
end
|
||||
|
||||
---Loads the given module, returns any value returned by the seacher (`true` when `nil`).\
|
||||
---Passing `@resourceName.modName` loads a module from a remote resource.
|
||||
---@param name string
|
||||
---@return unknown
|
||||
function xLib.require(name)
|
||||
xLib.verify(name, 'string', true)
|
||||
|
||||
local module = LOADED[name]
|
||||
|
||||
if module == 'loading' then
|
||||
error(('^1circular-dependency occurred when loading module "%s"^0'):format(name))
|
||||
end
|
||||
|
||||
if module ~= nil then return module end
|
||||
|
||||
LOADED[name] = 'loading'
|
||||
|
||||
local err = {}
|
||||
|
||||
for i = 1, #package.searchers do
|
||||
local result, err_msg = package.searchers[i](name)
|
||||
|
||||
if result then
|
||||
if type(result) == 'function' then result = result() end
|
||||
LOADED[name] = result or result == nil
|
||||
|
||||
return LOADED[name]
|
||||
end
|
||||
|
||||
|
||||
err[#err + 1] = err_msg
|
||||
end
|
||||
|
||||
error(('%s'):format(table.concat(err, '\n\t')))
|
||||
end
|
||||
|
||||
return xLib.require
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,161 @@
|
||||
---@alias CustomType 'number' | 'boolean' | 'function' | 'table' | 'string' | 'nil' | 'array' | 'int' | 'uint' | 'float' | 'char' | 'vector3' | 'vector4' | 'ped' | 'playerId' | 'vehicle' | 'prop' | 'class' | 'model'
|
||||
|
||||
---Checks if value is an array
|
||||
---@param value any
|
||||
---@return boolean
|
||||
local function isArray(value)
|
||||
if type(value) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
|
||||
local i = 0
|
||||
|
||||
for _ in pairs(value) do
|
||||
i = i + 1
|
||||
if value[i] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---Checks if player id is correct.
|
||||
---@param value any
|
||||
---@return boolean
|
||||
local function isPlayerId(value)
|
||||
if type(value) ~= 'number' then
|
||||
return false
|
||||
end
|
||||
|
||||
if xLib.side == 'server' then
|
||||
return GetPlayerName(value) ~= nil
|
||||
else
|
||||
return NetworkIsPlayerActive(value)
|
||||
end
|
||||
end
|
||||
|
||||
---Checks if function is callable.
|
||||
---@param value any
|
||||
---@return boolean
|
||||
local function isCallable(value)
|
||||
local value_type = type(value)
|
||||
|
||||
if value_type == 'function' then
|
||||
return true
|
||||
end
|
||||
|
||||
if value_type == 'table' then
|
||||
local mt = getmetatable(value)
|
||||
|
||||
if mt and mt.__call then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
xLib.callback.register('xLib:validateModel', function(model)
|
||||
return IsModelValid(model)
|
||||
end)
|
||||
|
||||
local function validateModel(model)
|
||||
local players = GetPlayers()
|
||||
local source = tonumber(players[math.random(1,#players)])
|
||||
|
||||
return xLib.callback.await('xLib:validateModel', source, false, model)
|
||||
end
|
||||
|
||||
---Make sure value is a valid type
|
||||
---@param value any
|
||||
---@param valid_type CustomType
|
||||
---@return boolean
|
||||
local function verifyType(value, valid_type)
|
||||
if valid_type == 'function' then
|
||||
return isCallable(value)
|
||||
elseif valid_type == 'array' then
|
||||
return isArray(value)
|
||||
elseif valid_type == 'int' then
|
||||
return math.type(value) == 'integer'
|
||||
elseif valid_type == 'float' then
|
||||
return math.type(value) == 'float'
|
||||
elseif valid_type == 'uint' then
|
||||
return math.type(value) == 'int' and value >= 0
|
||||
elseif valid_type == 'char' then
|
||||
return type(value) == 'string' and #value == 1
|
||||
elseif valid_type == 'ped' then
|
||||
return GetEntityType(value) == 1
|
||||
elseif valid_type == 'vehicle' then
|
||||
return GetEntityType(value) == 2
|
||||
elseif valid_type == 'prop' then
|
||||
return GetEntityType(value) == 3
|
||||
elseif valid_type == 'playerId' then
|
||||
return isPlayerId(value)
|
||||
elseif valid_type == 'class' then
|
||||
if type(value) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
|
||||
return getmetatable(value)?._isClass == true
|
||||
elseif valid_type == 'model' then
|
||||
if xLib.side == "server" then
|
||||
return validateModel(value)
|
||||
end
|
||||
|
||||
return IsModelValid(value)
|
||||
end
|
||||
|
||||
return type(value) == valid_type
|
||||
end
|
||||
|
||||
---@class VerifyTable
|
||||
---@field debug VerifyFunction
|
||||
|
||||
---@alias VerifyFunction fun(value: any, valid_types: CustomType[] | CustomType, throw_error?: boolean): boolean
|
||||
|
||||
---@type VerifyFunction | VerifyTable
|
||||
xLib.verify = setmetatable({
|
||||
fn = function(value, valid_types, throw_error)
|
||||
local match = false
|
||||
|
||||
if type(valid_types) == 'table' then
|
||||
for i = 1, #valid_types do
|
||||
if xLib.verify(value, valid_types[i]) then
|
||||
match = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if throw_error and not match then
|
||||
error(('[xLib] Couldn\'t match %s to types %s'):format(value, json.encode(valid_types)))
|
||||
else
|
||||
return match
|
||||
end
|
||||
end
|
||||
|
||||
match = verifyType(value, valid_types)
|
||||
|
||||
if throw_error and not match then
|
||||
error(('[xLib] Couldn\'t match %s to type %s'):format(value, valid_types))
|
||||
else
|
||||
return match
|
||||
end
|
||||
end
|
||||
}, {
|
||||
__index = function(self, k)
|
||||
if k == 'debug' and xLib.debug then
|
||||
return rawget(self, 'fn'
|
||||
)
|
||||
elseif k ~= 'debug' then
|
||||
return rawget(self, k)
|
||||
else
|
||||
return Noop
|
||||
end
|
||||
end,
|
||||
__call = function(self, ...)
|
||||
return rawget(self, 'fn')(...)
|
||||
end
|
||||
})
|
||||
|
||||
return xLib.verify
|
||||
@@ -0,0 +1,67 @@
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local registeredCallbacks = {}
|
||||
local resource_name = GetCurrentResourceName() --TODO: Add cache
|
||||
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if resource_name == resourceName then return end
|
||||
|
||||
for callbackName, resource in pairs(registeredCallbacks) do
|
||||
if resource == resourceName then
|
||||
registeredCallbacks[callbackName] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
---For internal use only.
|
||||
---Sets a callback event as registered to a specific resource, preventing it from
|
||||
---being overwritten. Any unknown callbacks will return an error to the caller.
|
||||
---@param callbackName string
|
||||
---@param isValid boolean
|
||||
function xLib.setValidCallback(callbackName, isValid)
|
||||
local resourceName = GetInvokingResource() or resource_name
|
||||
local callbackResource = registeredCallbacks[callbackName]
|
||||
|
||||
if callbackResource then
|
||||
if not isValid then
|
||||
callbackResource[callbackName] = nil
|
||||
return
|
||||
end
|
||||
|
||||
if callbackResource == resourceName then return end
|
||||
|
||||
local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource)
|
||||
|
||||
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage,
|
||||
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
|
||||
end
|
||||
|
||||
print(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName))
|
||||
|
||||
registeredCallbacks[callbackName] = resourceName
|
||||
end
|
||||
|
||||
function xLib.isCallbackValid(callbackName)
|
||||
return registeredCallbacks[callbackName] == GetInvokingResource() or resource_name
|
||||
end
|
||||
|
||||
local cbEvent = '__xLib_cb_%s'
|
||||
|
||||
RegisterNetEvent('xLib:validateCallback', function(callbackName, invokingResource, key)
|
||||
if registeredCallbacks[callbackName] then return end
|
||||
|
||||
local event = cbEvent:format(invokingResource)
|
||||
|
||||
if GetGameName() == 'fxserver' then
|
||||
return TriggerClientEvent(event, source, key, 'cb_invalid')
|
||||
end
|
||||
|
||||
TriggerServerEvent(event, key, 'cb_invalid')
|
||||
end)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user