adding new mlo removed things

This commit is contained in:
KingMcDonalds
2026-03-30 07:15:37 -07:00
parent aab83be079
commit 863723e991
297 changed files with 11545 additions and 223 deletions
Binary file not shown.
Binary file not shown.
-21
View File
@@ -310,27 +310,6 @@ Config = {
{ x = 1.395, y = 0.445, z = 0.175, rx = -22.750, ry = 0.000, rz = -83.750, depth = 0.895 }
},
},
[`rearmountels`] = {
useBone = false,
bones = {}, -- to add bones do "bone_name" you can have multiple by doing "bonename", "bonename_2"
offsets = {
{x = -1.345, y = 1.095, z = -0.440, rx = -24.750, ry = 0.000, rz = 72.000, depth = 0.670 },
},
},
[`midmountels`] = {
useBone = false,
bones = {}, -- to add bones do "bone_name" you can have multiple by doing "bonename", "bonename_2"
offsets = {
{ x = -1.320, y = 1.370, z = -0.595, rx = -24.250, ry = 0.000, rz = 77.500, depth = 0.860 },
},
},
[`ldfoam`] = {
useBone = false,
bones = {}, -- to add bones do "bone_name" you can have multiple by doing "bonename", "bonename_2"
offsets = {
{ x = 1.375, y = 1.160, z = 0.000, rx = -20.500, ry = 0.000, rz = -91.000, depth = 0.550 },
},
},
[`ldfoamels`] = {
useBone = false,
bones = {}, -- to add bones do "bone_name" you can have multiple by doing "bonename", "bonename_2"
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,26 @@
resource_manifest_version '77731fab-63ca-442c-a67b-abc70f28dfa5'
files {
'vehicles.meta',
'carvariations.meta',
'carcols.meta',
'handling.meta',
'vehiclelayouts.meta',
'peds.meta'
}
data_file 'HANDLING_FILE' 'handling.meta'
data_file 'VEHICLE_METADATA_FILE' 'vehicles.meta'
data_file 'CARCOLS_FILE' 'carcols.meta'
data_file 'VEHICLE_VARIATION_FILE' 'carvariations.meta'
data_file 'VEHICLE_LAYOUTS_FILE' 'vehiclelayouts.META'
data_file 'PED_METADATA_FILE' 'peds.meta'
client_script {
'vehicle_names.lua'
}
@@ -1,19 +0,0 @@
fx_version 'cerulean'
games {'gta5'}
lua54 'yes'
files {
'data/**/*.meta',
}
escrow_ignore {
'data/*',
}
client_script 'data/**/vehicle_names.lua'
data_file 'HANDLING_FILE' 'data/**/*handling.meta'
data_file 'VEHICLE_METADATA_FILE' 'data/**/*vehicles.meta'
data_file 'CARCOLS_FILE' 'data/**/*carcols.meta'
data_file 'VEHICLE_VARIATION_FILE' 'data/**/*carvariations.meta'
data_file 'VEHICLE_LAYOUTS_FILE' 'data/**/*vehiclelayouts.meta'
@@ -0,0 +1,26 @@
resource_manifest_version '77731fab-63ca-442c-a67b-abc70f28dfa5'
files {
'vehicles.meta',
'carvariations.meta',
'carcols.meta',
'handling.meta',
'vehiclelayouts.meta',
'peds.meta'
}
data_file 'HANDLING_FILE' 'handling.meta'
data_file 'VEHICLE_METADATA_FILE' 'vehicles.meta'
data_file 'CARCOLS_FILE' 'carcols.meta'
data_file 'VEHICLE_VARIATION_FILE' 'carvariations.meta'
data_file 'VEHICLE_LAYOUTS_FILE' 'vehiclelayouts.META'
data_file 'PED_METADATA_FILE' 'peds.meta'
client_script {
'vehicle_names.lua'
}
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
@@ -0,0 +1,32 @@
# ox_lib
A FiveM library and resource implementing reusable modules, methods, and UI elements.
![](https://img.shields.io/github/downloads/overextended/ox_lib/total?logo=github)
![](https://img.shields.io/github/downloads/overextended/ox_lib/latest/total?logo=github)
![](https://img.shields.io/github/contributors/overextended/ox_lib?logo=github)
![](https://img.shields.io/github/v/release/overextended/ox_lib?logo=github)
For guidelines to contributing to the project, and to see our Contributor License Agreement, see [CONTRIBUTING.md](./CONTRIBUTING.md)
For additional legal notices, refer to [NOTICE.md](./NOTICE.md).
## 📚 Documentation
https://overextended.dev/ox_lib
## 💾 Download
https://github.com/overextended/ox_lib/releases/latest/download/ox_lib.zip
## 📦 npm package
https://www.npmjs.com/package/@overextended/ox_lib
## 🖥️ Lua Language Server
- Install [Lua Language Server](https://marketplace.visualstudio.com/items?itemName=sumneko.lua) to ease development with annotations, type checking, diagnostics, and more.
- Install [cfxlua-vscode](https://marketplace.visualstudio.com/items?itemName=overextended.cfxlua-vscode) to add natives and cfxlua runtime declarations to LLS.
- You can load ox_lib into your global development environment by modifying workspace/user settings "Lua.workspace.library" with the resource path.
- e.g. "c:/fxserver/resources/ox_lib"
@@ -0,0 +1,48 @@
fx_version 'cerulean'
use_experimental_fxv2_oal 'yes'
lua54 'yes'
games { 'rdr3', 'gta5' }
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
name 'ox_lib'
author 'Overextended'
version '3.30.6'
license 'LGPL-3.0-or-later'
repository 'https://github.com/overextended/ox_lib'
description 'A library of shared functions to utilise in other resources.'
dependencies {
'/server:7290',
'/onesync',
}
ui_page 'web/build/index.html'
files {
'init.lua',
'resource/settings.lua',
'imports/**/client.lua',
'imports/**/shared.lua',
'web/build/index.html',
'web/build/**/*',
'locales/*.json',
}
shared_script 'resource/init.lua'
shared_scripts {
'resource/**/shared.lua',
-- 'resource/**/shared/*.lua'
}
client_scripts {
'resource/**/client.lua',
'resource/**/client/*.lua'
}
server_scripts {
'imports/callback/server.lua',
'imports/getFilesInDirectory/server.lua',
'resource/**/server.lua',
'resource/**/server/*.lua',
}
@@ -0,0 +1,118 @@
-- DO NOT USE! Old syntax for addCommand (prior to v3.0)
---@todo convert input and call standard function?
--[[
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 commands = {}
SetTimeout(1000, function()
TriggerClientEvent('chat:addSuggestions', -1, commands)
end)
AddEventHandler('playerJoining', function()
TriggerClientEvent('chat:addSuggestions', source, commands)
end)
local function chatSuggestion(name, parameters, help)
local params = {}
if parameters then
for i = 1, #parameters do
local arg, argType = string.strsplit(':', parameters[i])
if argType and argType:sub(0, 1) == '?' then
argType = argType:sub(2, #argType)
end
params[i] = {
name = arg,
help = argType
}
end
end
commands[#commands + 1] = {
name = '/' .. name,
help = help,
params = params
}
end
---@deprecated
---@param group string | string[] | false
---@param name string | string[]
---@param callback function
---@param parameters table
function lib.__addCommand(group, name, callback, parameters, help)
if not group then group = 'builtin.everyone' end
if type(name) == 'table' then
for i = 1, #name do
---@diagnostic disable-next-line: deprecated
lib.__addCommand(group, name[i], callback, parameters, help)
end
else
chatSuggestion(name, parameters, help)
RegisterCommand(name, function(source, args, raw)
source = tonumber(source) --[[@as number]]
if parameters then
for i = 1, #parameters do
local arg, argType = string.strsplit(':', parameters[i])
local value = args[i]
if arg == 'target' and value == 'me' then value = source end
if argType then
local optional
if argType:sub(0, 1) == '?' then
argType = argType:sub(2, #argType)
optional = true
end
if argType == 'number' then
value = tonumber(value) or value
end
local type = type(value)
if type ~= argType and (not optional or type ~= 'nil') then
local invalid = ('^1%s expected <%s> for argument %s (%s), received %s^0'):format(name,
argType, i, arg, type)
if source < 1 then
return print(invalid)
else
return TriggerClientEvent('chat:addMessage', source, invalid)
end
end
end
args[arg] = value
args[i] = nil
end
end
callback(source, args, raw)
end, group and true)
name = ('command.%s'):format(name)
if type(group) == 'table' then
for _, v in ipairs(group) do
if not IsPrincipalAceAllowed(v, name) then lib.addAce(v, name) end
end
else
if not IsPrincipalAceAllowed(group, name) then lib.addAce(group, name) end
end
end
end
---@diagnostic disable-next-line: deprecated
return lib.__addCommand
@@ -0,0 +1,164 @@
--[[
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>
]]
---@class OxCommandParams
---@field name string
---@field help? string
---@field type? 'number' | 'playerId' | 'string' | 'longString'
---@field optional? boolean
---@class OxCommandProperties
---@field help string?
---@field params OxCommandParams[]?
---@field restricted boolean | string | string[]?
---@type OxCommandProperties[]
local registeredCommands = {}
local shouldSendCommands = false
SetTimeout(1000, function()
shouldSendCommands = true
TriggerClientEvent('chat:addSuggestions', -1, registeredCommands)
end)
AddEventHandler('playerJoining', function()
TriggerClientEvent('chat:addSuggestions', source, registeredCommands)
end)
---@param source number
---@param args table
---@param raw string
---@param params OxCommandParams[]?
---@return table?
local function parseArguments(source, args, raw, params)
if not params then return args end
local paramsNum = #params
for i = 1, paramsNum do
local arg, param = args[i], params[i]
local value
if param.type == 'number' then
value = tonumber(arg)
elseif param.type == 'string' then
value = not tonumber(arg) and arg
elseif param.type == 'playerId' then
value = arg == 'me' and source or tonumber(arg)
if not value or not DoesPlayerExist(value--[[@as string]]) then
value = false
end
elseif param.type == 'longString' and i == paramsNum then
if arg then
local start = raw:find(arg, 1, true)
value = start and raw:sub(start)
else
value = nil
end
else
value = arg
end
if not value and (not param.optional or param.optional and arg) then
return Citizen.Trace(("^1command '%s' received an invalid %s for argument %s (%s), received '%s'^0\n"):format(string.strsplit(' ', raw) or raw, param.type, i, param.name, arg))
end
arg = value
args[param.name] = arg
args[i] = nil
end
return args
end
---@param commandName string | string[]
---@param properties OxCommandProperties | false
---@param cb fun(source: number, args: table, raw: string)
---@param ... any
function lib.addCommand(commandName, properties, cb, ...)
-- Try to handle backwards-compatibility with the old addCommand syntax (prior to v3.0)
local restricted, params
if properties then
if ... or table.type(properties) ~= 'hash' then
local _commandName = type(properties) == 'table' and properties[1] or properties
local info = debug.getinfo(2, 'Sl')
warn(("command '%s' is using deprecated syntax for lib.addCommand\nupdate the command or use lib.__addCommand to ignore this warning\n> source ^0(^5%s^0:%d)"):format(_commandName, info.short_src, info.currentline))
---@diagnostic disable-next-line: deprecated
return lib.__addCommand(commandName, properties, cb, ...)
end
restricted = properties.restricted
params = properties.params
end
if params then
for i = 1, #params do
local param = params[i]
if param.type then
param.help = param.help and ('%s (type: %s)'):format(param.help, param.type) or ('(type: %s)'):format(param.type)
end
end
end
local commands = type(commandName) ~= 'table' and { commandName } or commandName
local numCommands = #commands
local totalCommands = #registeredCommands
local function commandHandler(source, args, raw)
args = parseArguments(source, args, raw, params)
if not args then return end
local success, resp = pcall(cb, source, args, raw)
if not success then
Citizen.Trace(("^1command '%s' failed to execute!\n%s"):format(string.strsplit(' ', raw) or raw, resp))
end
end
for i = 1, numCommands do
totalCommands += 1
commandName = commands[i]
RegisterCommand(commandName, commandHandler, restricted and true)
if restricted then
local ace = ('command.%s'):format(commandName)
local restrictedType = type(restricted)
if restrictedType == 'string' and not IsPrincipalAceAllowed(restricted, ace) then
lib.addAce(restricted, ace)
elseif restrictedType == 'table' then
for j = 1, #restricted do
if not IsPrincipalAceAllowed(restricted[j], ace) then
lib.addAce(restricted[j], ace)
end
end
end
end
if properties then
---@diagnostic disable-next-line: inject-field
properties.name = ('/%s'):format(commandName)
properties.restricted = nil
registeredCommands[totalCommands] = properties
if i ~= numCommands and numCommands ~= 1 then
properties = table.clone(properties)
end
if shouldSendCommands then TriggerClientEvent('chat:addSuggestions', -1, properties) end
end
end
end
return lib.addCommand
@@ -0,0 +1,91 @@
--[[
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>
]]
if cache.game == 'redm' then return end
---@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 [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
---@param data KeybindProps
---@return CKeybind
function lib.addKeybind(data)
---@cast data CKeybind
data.hash = joaat('+' .. data.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 data.onPressed then data:onPressed() end
end)
RegisterCommand('-' .. data.name, function()
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = false
if data.onReleased then data:onReleased() end
end)
RegisterKeyMapping('+' .. data.name, data.description, data.defaultMapper, data.defaultKey)
if data.secondaryKey then
RegisterKeyMapping('~!+' .. data.name, data.description, data.secondaryMapper or data.defaultMapper, data.secondaryKey)
end
SetTimeout(500, function()
TriggerEvent('chat:removeSuggestion', ('/+%s'):format(data.name))
TriggerEvent('chat:removeSuggestion', ('/-%s'):format(data.name))
end)
return data
end
return lib.addKeybind
@@ -0,0 +1,363 @@
--[[
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>
]]
---@class Array<T> : OxClass, { [number]: T }
lib.array = lib.class('Array')
local table_unpack = table.unpack
local table_remove = table.remove
local table_clone = table.clone
local table_concat = table.concat
local table_type = table.type
---@alias ArrayLike<T> Array | { [number]: T }
---@private
function lib.array:constructor(...)
local arr = { ... }
for i = 1, #arr do
self[i] = arr[i]
end
end
---@private
function lib.array:__newindex(index, value)
if type(index) ~= 'number' then error(("Cannot insert non-number index '%s' into an array."):format(index)) end
rawset(self, index, value)
end
---Creates a new array from an iteratable value.
---@param iter table | function | string
---@return Array
function lib.array:from(iter)
local iterType = type(iter)
if iterType == 'table' then
return lib.array:new(table_unpack(iter))
end
if iterType == 'string' then
return lib.array:new(string.strsplit('', iter))
end
if iterType == 'function' then
local arr = lib.array:new()
local length = 0
for value in iter do
length += 1
arr[length] = value
end
return arr
end
error(('Array.from argument was not a valid iterable value (received %s)'):format(iterType))
end
---Returns the element at the given index, with negative numbers counting backwards from the end of the array.
---@param index number
---@return unknown
function lib.array:at(index)
if index < 0 then
index = #self + index + 1
end
return self[index]
end
---Create a new array containing the elements of two or more arrays.
---@param ... ArrayLike
function lib.array:merge(...)
local newArr = table_clone(self)
local length = #self
local arrays = { ... }
for i = 1, #arrays do
local arr = arrays[i]
for j = 1, #arr do
length += 1
newArr[length] = arr[j]
end
end
return lib.array:new(table_unpack(newArr))
end
---Tests if all elements in an array succeed in passing the provided test function.
---@param testFn fun(element: unknown): boolean
function lib.array:every(testFn)
for i = 1, #self do
if not testFn(self[i]) then
return false
end
end
return true
end
---Sets all elements within a range to the given value and returns the modified array.
---@param value any
---@param start? number
---@param endIndex? number
function lib.array:fill(value, start, endIndex)
local length = #self
start = start or 1
endIndex = endIndex or length
if start < 1 then start = 1 end
if endIndex > length then endIndex = length end
for i = start, endIndex do
self[i] = value
end
return self
end
---Creates a new array containing the elements from an array that pass the test of the provided function.
---@param testFn fun(element: unknown): boolean
function lib.array:filter(testFn)
local newArr = {}
local length = 0
for i = 1, #self do
local element = self[i]
if testFn(element) then
length += 1
newArr[length] = element
end
end
return lib.array:new(table_unpack(newArr))
end
---Returns the first or last element of an array that passes the provided test function.
---@param testFn fun(element: unknown): boolean
---@param last? boolean
function lib.array:find(testFn, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if testFn(element) then
return element
end
end
end
---Returns the first or last index of the first element of an array that passes the provided test function.
---@param testFn fun(element: unknown): boolean
---@param last? boolean
function lib.array:findIndex(testFn, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if testFn(element) then
return i
end
end
end
---Returns the first or last index of the first element of an array that matches the provided value.
---@param value unknown
---@param last? boolean
function lib.array:indexOf(value, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if element == value then
return i
end
end
end
---Executes the provided function for each element in an array.
---@param cb fun(element: unknown)
function lib.array:forEach(cb)
for i = 1, #self do
cb(self[i])
end
end
---Determines if a given element exists inside an array.
---@param element unknown The value to find in the array.
---@param fromIndex? number The position in the array to begin searching from.
function lib.array:includes(element, fromIndex)
for i = (fromIndex or 1), #self do
if self[i] == element then return true end
end
return false
end
---Concatenates all array elements into a string, seperated by commas or the specified seperator.
---@param seperator? string
function lib.array:join(seperator)
return table_concat(self, seperator or ',')
end
---Create a new array containing the results from calling the provided function on every element in an array.
---@param cb fun(element: unknown, index: number, array: self): unknown
function lib.array:map(cb)
local arr = {}
for i = 1, #self do
arr[i] = cb(self[i], i, self)
end
return lib.array:new(table_unpack(arr))
end
---Removes the last element from an array and returns the removed element.
function lib.array:pop()
return table_remove(self)
end
---Adds the given elements to the end of an array and returns the new array length.
---@param ... any
function lib.array:push(...)
local elements = { ... }
local length = #self
for i = 1, #elements do
length += 1
self[length] = elements[i]
end
return length
end
---The "reducer" function is applied to every element within an array, with the previous element's result serving as the accumulator.
---If an initial value is provided, it's used as the accumulator for index 1; otherwise, index 1 itself serves as the initial value, and iteration begins from index 2.
---@generic T
---@param reducer fun(accumulator: T, currentValue: T, index?: number): T
---@param initialValue? T
---@param reverse? boolean Iterate over the array from right-to-left.
---@return T
function lib.array:reduce(reducer, initialValue, reverse)
local length = #self
local initialIndex = initialValue and 1 or 2
local accumulator = initialValue or self[1]
if reverse then
for i = initialIndex, length do
local index = length - i + initialIndex
accumulator = reducer(accumulator, self[index], index)
end
else
for i = initialIndex, length do
accumulator = reducer(accumulator, self[i], i)
end
end
return accumulator
end
---Reverses the elements inside an array.
function lib.array:reverse()
local i, j = 1, #self
while i < j do
self[i], self[j] = self[j], self[i]
i += 1
j -= 1
end
return self
end
---Removes the first element from an array and returns the removed element.
function lib.array:shift()
return table_remove(self, 1)
end
---Creates a shallow copy of a portion of an array as a new array.
---@param start? number
---@param finish? number
function lib.array:slice(start, finish)
local length = #self
start = start or 1
finish = finish or length
if start < 0 then start = length + start + 1 end
if finish < 0 then finish = length + finish + 1 end
if start < 1 then start = 1 end
if finish > length then finish = length end
local arr = lib.array:new()
local index = 0
for i = start, finish do
index += 1
arr[index] = self[i]
end
return arr
end
---Creates a new array with reversed elements from the given array.
function lib.array:toReversed()
local reversed = lib.array:new()
for i = #self, 1, -1 do
reversed:push(self[i])
end
return reversed
end
---Inserts the given elements to the start of an array and returns the new array length.
---@param ... any
function lib.array:unshift(...)
local elements = { ... }
local length = #self
local eLength = #elements
for i = length, 1, -1 do
self[i + eLength] = self[i]
end
for i = 1, #elements do
self[i] = elements[i]
end
return length + eLength
end
---Returns true if the given table is an instance of array or an array-like table.
---@param tbl ArrayLike
---@return boolean
function lib.array.isArray(tbl)
local tableType = table_type(tbl)
if not tableType then return false end
if tableType == 'array' or tableType == 'empty' or lib.array.instanceOf(tbl, lib.array) then
return true
end
return false
end
return lib.array
@@ -0,0 +1,145 @@
--[[
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 = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 300000)
RegisterNetEvent(cbEvent:format(cache.resource), 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 delay and type(delay) == 'number' and delay > 0 then
local time = GetGameTimer()
if (timers[event] or 0) > time then
return false
end
timers[event] = time + delay
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('ox_lib:validateCallback', event, cache.resource, key)
TriggerServerEvent(cbEvent:format(event), cache.resource, 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, ...)
lib.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 lib.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
assert(cbType == 'function', ("expected argument 3 to have type 'function' (received %s)"):format(cbType))
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 lib.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 lib.callback.register(name, cb)
event = cbEvent:format(name)
lib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
end
return lib.callback
@@ -0,0 +1,126 @@
--[[
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 = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 300000)
RegisterNetEvent(cbEvent:format(cache.resource), 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, ...)
assert(DoesPlayerExist(playerId --[[@as string]]), ("target playerId '%s' does not exist"):format(playerId))
local key
repeat
key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId)
until not pendingCallbacks[key]
TriggerClientEvent('ox_lib:validateCallback', playerId, event, cache.resource, key)
TriggerClientEvent(cbEvent:format(event), playerId, cache.resource, 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, ...)
lib.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 lib.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
assert(cbType == 'function', ("expected argument 3 to have type 'function' (received %s)"):format(cbType))
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 lib.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 lib.callback.register(name, cb)
event = cbEvent:format(name)
lib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerClientEvent(cbEvent:format(resource), source, key, callbackResponse(pcall(cb, source, ...)))
end)
end
return lib.callback
@@ -0,0 +1,158 @@
--[[
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>
]]
---@diagnostic disable: invisible
local getinfo = debug.getinfo
---Ensure the given argument or property has a valid type, otherwise throwing an error.
---@param id number | string
---@param var any
---@param expected type
local function assertType(id, var, expected)
local received = type(var)
if received ~= expected then
error(("expected %s %s to have type '%s' (received %s)")
:format(type(id) == 'string' and 'field' or 'argument', id, expected, received), 3)
end
if expected == 'table' and table.type(var) ~= 'hash' then
error(("expected argument %s to have table.type 'hash' (received %s)")
:format(id, table.type(var)), 3)
end
return true
end
---@alias OxClassConstructor<T> fun(self: T, ...: unknown): nil
---@class OxClass
---@field private __index table
---@field protected __name string
---@field protected private? { [string]: unknown }
---@field protected super? OxClassConstructor
---@field protected constructor? OxClassConstructor
local mixins = {}
local constructors = {}
---Somewhat hacky way to remove the constructor from the class.__index.
---Maybe add static fields in the future?
---@param class OxClass
local function getConstructor(class)
local constructor = constructors[class] or class.constructor
if class.constructor then
constructors[class] = class.constructor
class.constructor = nil
end
return constructor
end
local function void() return '' end
---Creates a new instance of the given class.
---@protected
---@generic T
---@param class T | OxClass
---@return T
function mixins.new(class, ...)
local constructor = getConstructor(class)
local private = {}
local obj = setmetatable({ private = private }, class)
if constructor then
local parent = class
rawset(obj, 'super', function(self, ...)
parent = getmetatable(parent)
constructor = getConstructor(parent)
if constructor then return constructor(self, ...) end
end)
constructor(obj, ...)
end
rawset(obj, 'super', nil)
if private ~= obj.private or next(obj.private) then
private = table.clone(obj.private)
table.wipe(obj.private)
setmetatable(obj.private, {
__metatable = 'private',
__tostring = void,
__index = function(self, index)
local di = getinfo(2, 'n')
if di.namewhat ~= 'method' and di.namewhat ~= '' then return end
return private[index]
end,
__newindex = function(self, index, value)
local di = getinfo(2, 'n')
if di.namewhat ~= 'method' and di.namewhat ~= '' then
error(("cannot set value of private field '%s'"):format(index), 2)
end
private[index] = value
end
})
else
obj.private = nil
end
return obj
end
---Checks if an object is an instance of the given class.
---@param class OxClass
function mixins:isClass(class)
return getmetatable(self) == class
end
---Checks if an object is an instance or derivative of the given class.
---@param class OxClass
function mixins:instanceOf(class)
local mt = getmetatable(self)
while mt do
if mt == class then return true end
mt = getmetatable(mt)
end
return false
end
---Creates a new class.
---@generic S : OxClass
---@generic T : string
---@param name `T`
---@param super? S
---@return `T`
function lib.class(name, super)
assertType(1, name, 'string')
local class = table.clone(mixins)
class.__name = name
class.__index = class
if super then
assertType('super', super, 'table')
setmetatable(class, super)
end
---@todo See if there's a way we can auto-create a class using the name and super
return class
end
return lib.class
@@ -0,0 +1,473 @@
--[[
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>
]]
lib.cron = {}
---@alias Date { year: number, month: number, day: number, hour: number, min: number, sec: number, wday: number, yday: number, isdst: boolean }
---@type Date
local currentDate = {}
setmetatable(currentDate, {
__index = function(self, index)
local newDate = os.date('*t') --[[@as Date]]
for k, v in pairs(newDate) do
self[k] = v
end
SetTimeout(1000, function() table.wipe(self) end)
return self[index]
end
})
---@class OxTaskProperties
---@field minute? number|string|function
---@field hour? number|string|function
---@field day? number|string|function
---@field month? number|string|function
---@field year? number|string|function
---@field weekday? number|string|function
---@field job fun(task: OxTask, date: osdate)
---@field isActive boolean
---@field id number
---@field debug? boolean
---@field lastRun? number
---@field maxDelay? number Maximum allowed delay in seconds before skipping (0 to disable)
---@class OxTask : OxTaskProperties
---@field expression string
---@field private scheduleTask fun(self: OxTask): boolean?
local OxTask = {}
OxTask.__index = OxTask
local validRanges = {
min = { min = 0, max = 59 },
hour = { min = 0, max = 23 },
day = { min = 1, max = 31 },
month = { min = 1, max = 12 },
wday = { min = 0, max = 7 },
}
local maxUnits = {
min = 60,
hour = 24,
wday = 7,
day = 31,
month = 12,
}
local weekdayMap = {
sun = 1,
mon = 2,
tue = 3,
wed = 4,
thu = 5,
fri = 6,
sat = 7,
}
local monthMap = {
jan = 1, feb = 2, mar = 3, apr = 4,
may = 5, jun = 6, jul = 7, aug = 8,
sep = 9, oct = 10, nov = 11, dec = 12
}
---Returns the last day of the specified month
---@param month number
---@param year? number
---@return number
local function getMaxDaysInMonth(month, year)
return os.date('*t', os.time({ year = year or currentDate.year, month = month + 1, day = -1 })).day --[[@as number]]
end
---@param value string|number
---@param unit string
---@return boolean
local function isValueInRange(value, unit)
local range = validRanges[unit]
if not range then return true end
return value >= range.min and value <= range.max
end
---@param value string
---@param unit string
---@return number|string|function|nil
local function parseCron(value, unit)
if not value or value == '*' then return end
if unit == 'day' and value:lower() == 'l' then
return function()
return getMaxDaysInMonth(currentDate.month, currentDate.year)
end
end
local num = tonumber(value)
if num then
if not isValueInRange(num, unit) then
error(("^1invalid cron expression. '%s' is out of range for %s^0"):format(value, unit), 3)
end
return num
end
if unit == 'wday' then
local start, stop = value:match('(%a+)-(%a+)')
if start and stop then
start = weekdayMap[start:lower()]
stop = weekdayMap[stop:lower()]
if start and stop then
if stop < start then stop = stop + 7 end
return ('%d-%d'):format(start, stop)
end
end
local day = weekdayMap[value:lower()]
if day then return day end
end
if unit == 'month' then
local months = {}
for month in value:gmatch('[^,]+') do
local monthNum = monthMap[month:lower()]
if monthNum then
months[#months + 1] = tostring(monthNum)
end
end
if #months > 0 then
return table.concat(months, ',')
end
end
local stepMatch = value:match('^%*/(%d+)$')
if stepMatch then
local step = tonumber(stepMatch)
if not step or step == 0 then
error(("^1invalid cron expression. Step value cannot be %s^0"):format(step or 'nil'), 3)
end
return value
end
local start, stop = value:match('^(%d+)-(%d+)$')
if start and stop then
start, stop = tonumber(start), tonumber(stop)
if not start or not stop or not isValueInRange(start, unit) or not isValueInRange(stop, unit) then
error(("^1invalid cron expression. Range '%s' is invalid for %s^0"):format(value, unit), 3)
end
return value
end
local valid = true
for item in value:gmatch('[^,]+') do
local num = tonumber(item)
if not num or not isValueInRange(num, unit) then
valid = false
break
end
end
if valid then return value end
error(("^1invalid cron expression. '%s' is not supported for %s^0"):format(value, unit), 3)
end
---@param value string|number|function|nil
---@param unit string
---@return number|false|nil
local function getTimeUnit(value, unit)
local currentTime = currentDate[unit]
if not value then
return unit == 'min' and currentTime + 1 or currentTime
end
if type(value) == 'function' then
return value()
end
local unitMax = maxUnits[unit]
if type(value) == 'string' then
local stepValue = string.match(value, '*/(%d+)')
if stepValue then
local step = tonumber(stepValue)
for i = currentTime + 1, unitMax do
if i % step == 0 then return i end
end
return step + unitMax
end
local range = string.match(value, '%d+-%d+')
if range then
local min, max = string.strsplit('-', range)
min, max = tonumber(min, 10), tonumber(max, 10)
if unit == 'min' then
if currentTime >= max then
return min + unitMax
end
elseif currentTime > max then
return min + unitMax
end
return currentTime < min and min or currentTime
end
local list = string.match(value, '%d+,%d+')
if list then
local values = {}
for listValue in string.gmatch(value, '%d+') do
values[#values + 1] = tonumber(listValue)
end
table.sort(values)
for i = 1, #values do
local listValue = values[i]
if unit == 'min' then
if currentTime < listValue then
return listValue
end
elseif currentTime <= listValue then
return listValue
end
end
return values[1] + unitMax
end
return false
end
if unit == 'min' then
return value <= currentTime and value + unitMax or value --[[@as number]]
end
return value < currentTime and value + unitMax or value --[[@as number]]
end
---@return number?
function OxTask:getNextTime()
if not self.isActive then return end
local day = getTimeUnit(self.day, 'day')
if day == 0 then
day = getMaxDaysInMonth(currentDate.month)
end
if day ~= currentDate.day then return end
local month = getTimeUnit(self.month, 'month')
if month ~= currentDate.month then return end
local weekday = getTimeUnit(self.weekday, 'wday')
if weekday and weekday ~= currentDate.wday then return end
local minute = getTimeUnit(self.minute, 'min')
if not minute then return end
local hour = getTimeUnit(self.hour, 'hour')
if not hour then return end
if minute >= maxUnits.min then
if not self.hour then
hour += math.floor(minute / maxUnits.min)
end
minute = minute % maxUnits.min
end
if hour >= maxUnits.hour and day then
if not self.day then
day += math.floor(hour / maxUnits.hour)
end
hour = hour % maxUnits.hour
end
local nextTime = os.time({
min = minute,
hour = hour,
day = day or currentDate.day,
month = month or currentDate.month,
year = currentDate.year,
})
if self.lastRun and nextTime - self.lastRun < 60 then
if self.debug then
lib.print.debug(('Preventing duplicate execution of task %s - Last run: %s, Next scheduled: %s'):format(
self.id,
os.date('%c', self.lastRun),
os.date('%c', nextTime)
))
end
return
end
return nextTime
end
---@return number
function OxTask:getAbsoluteNextTime()
local minute = getTimeUnit(self.minute, 'min')
local hour = getTimeUnit(self.hour, 'hour')
local day = getTimeUnit(self.day, 'day')
local month = getTimeUnit(self.month, 'month')
local year = getTimeUnit(self.year, 'year')
if self.day then
if currentDate.hour < hour or (currentDate.hour == hour and currentDate.min < minute) then
day = day - 1
if day < 1 then
day = getMaxDaysInMonth(currentDate.month)
end
end
if currentDate.hour > hour or (currentDate.hour == hour and currentDate.min >= minute) then
day = day + 1
if day > getMaxDaysInMonth(currentDate.month) or day == 1 then
day = 1
month = month + 1
end
end
end
---@diagnostic disable-next-line: assign-type-mismatch
if os.time({ year = year, month = month, day = day, hour = hour, min = minute }) < os.time() then
year = year and year + 1 or currentDate.year + 1
end
return os.time({
min = minute < 60 and minute or 0,
hour = hour < 24 and hour or 0,
day = day or currentDate.day,
month = month or currentDate.month,
year = year or currentDate.year,
})
end
function OxTask:getTimeAsString(timestamp)
return os.date('%A %H:%M, %d %B %Y', timestamp or self:getAbsoluteNextTime())
end
---@type OxTask[]
local tasks = {}
function OxTask:scheduleTask()
local runAt = self:getNextTime()
if not runAt then
return self:stop('getNextTime returned no value')
end
local currentTime = os.time()
local sleep = runAt - currentTime
if sleep < 0 then
if not self.maxDelay or -sleep > self.maxDelay then
return self:stop(self.debug and ('scheduled time expired %s seconds ago'):format(-sleep))
end
if self.debug then
lib.print.debug(('Task %s is %s seconds overdue, executing now due to maxDelay=%s'):format(
self.id,
-sleep,
self.maxDelay
))
end
sleep = 0
end
local timeAsString = self:getTimeAsString(runAt)
if self.debug then
lib.print.debug(('(%s) task %s will run in %d seconds (%0.2f minutes / %0.2f hours)'):format(timeAsString, self.id, sleep,
sleep / 60,
sleep / 60 / 60))
end
if sleep > 0 then
Wait(sleep * 1000)
else
Wait(0)
return true
end
if self.isActive then
if self.debug then
lib.print.debug(('(%s) running task %s'):format(timeAsString, self.id))
end
Citizen.CreateThreadNow(function()
self:job(currentDate)
self.lastRun = os.time()
end)
return true
end
end
function OxTask:run()
if self.isActive then return end
self.isActive = true
CreateThread(function()
while self:scheduleTask() do end
end)
end
function OxTask:stop(msg)
self.isActive = false
if self.debug then
if msg then
return lib.print.debug(('stopping task %s (%s)'):format(self.id, msg))
end
lib.print.debug(('stopping task %s'):format(self.id))
end
end
---@param expression string A cron expression such as `* * * * *` representing minute, hour, day, month, and day of the week.
---@param job fun(task: OxTask, date: osdate)
---@param options? { debug?: boolean }
---Creates a new [cronjob](https://en.wikipedia.org/wiki/Cron), scheduling a task to run at fixed times or intervals.
---Supports numbers, any value `*`, lists `1,2,3`, ranges `1-3`, and steps `*/4`.
---Day of the week is a range of `1-7` starting from Sunday and allows short-names (i.e. sun, mon, tue).
---@note maxDelay: Maximum allowed delay in seconds before skipping (0 to disable)
function lib.cron.new(expression, job, options)
if not job or type(job) ~= 'function' then
error(("expected job to have type 'function' (received %s)"):format(type(job)))
end
local minute, hour, day, month, weekday = string.strsplit(' ', string.lower(expression))
---@type OxTask
local task = setmetatable(options or {}, OxTask)
task.expression = expression
task.minute = parseCron(minute, 'min')
task.hour = parseCron(hour, 'hour')
task.day = parseCron(day, 'day')
task.month = parseCron(month, 'month')
task.weekday = parseCron(weekday, 'wday')
task.id = #tasks + 1
task.job = job
task.lastRun = nil
task.maxDelay = task.maxDelay or 1
tasks[task.id] = task
task:run()
return task
end
-- reschedule any dead tasks on a new day
lib.cron.new('0 0 * * *', function()
for i = 1, #tasks do
local task = tasks[i]
if not task.isActive then
task:run()
end
end
end)
return lib.cron
@@ -0,0 +1,64 @@
--[[
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>
]]
--- Call on frame to disable all stored keys.
--- ```
--- disableControls()
--- ```
local disableControls = {}
---@param ... number | table
function disableControls:Add(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
local key = keys[i]
if self[key] then
self[key] += 1
else
self[key] = 1
end
end
end
---@param ... number | table
function disableControls:Remove(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
local key = keys[i]
local exists = self[key]
if exists and exists > 1 then
self[key] -= 1
else
self[key] = nil
end
end
end
---@param ... number | table
function disableControls:Clear(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
self[keys[i]] = nil
end
end
local keys = {}
local DisableControlAction = DisableControlAction
local pairs = pairs
lib.disableControls = setmetatable(disableControls, {
__index = keys,
__newindex = keys,
__call = function()
for k in pairs(keys) do
DisableControlAction(0, k, true)
end
end
})
return lib.disableControls
@@ -0,0 +1,95 @@
--[[
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>
]]
---@class DuiProperties
---@field url string
---@field width number
---@field height number
---@field debug? boolean
---@class Dui : OxClass
---@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
lib.dui = lib.class('Dui')
---@type table<string, Dui>
local duis = {}
local currentId = 0
---@param data DuiProperties
function lib.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 lib.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 lib.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 lib.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
AddEventHandler('onResourceStop', function(resourceName)
if cache.resource ~= resourceName then return end
for _, dui in pairs(duis) do
dui:remove()
end
end)
return lib.dui
@@ -0,0 +1,34 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return number? object
---@return vector3? objectCoords
function lib.getClosestObject(coords, maxDistance)
local objects = GetGamePool('CObject')
local closestObject, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #objects do
local object = objects[i]
local objectCoords = GetEntityCoords(object)
local distance = #(coords - objectCoords)
if distance < maxDistance then
maxDistance = distance
closestObject = object
closestCoords = objectCoords
end
end
return closestObject, closestCoords
end
return lib.getClosestObject
@@ -0,0 +1,36 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return number? ped
---@return vector3? pedCoords
function lib.getClosestPed(coords, maxDistance)
local peds = GetGamePool('CPed')
local closestPed, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #peds do
local ped = peds[i]
if not IsPedAPlayer(ped) then
local pedCoords = GetEntityCoords(ped)
local distance = #(coords - pedCoords)
if distance < maxDistance then
maxDistance = distance
closestPed = ped
closestCoords = pedCoords
end
end
end
return closestPed, closestCoords
end
return lib.getClosestPed
@@ -0,0 +1,40 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayer? boolean Whether or not to include the current player.
---@return number? playerId
---@return number? playerPed
---@return vector3? playerCoords
function lib.getClosestPlayer(coords, maxDistance, includePlayer)
local players = GetActivePlayers()
local closestId, closestPed, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
if playerId ~= cache.playerId or includePlayer then
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
maxDistance = distance
closestId = playerId
closestPed = playerPed
closestCoords = playerCoords
end
end
end
return closestId, closestPed, closestCoords
end
return lib.getClosestPlayer
@@ -0,0 +1,36 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return number? playerId
---@return number? playerPed
---@return vector3? playerCoords
function lib.getClosestPlayer(coords, maxDistance)
local players = GetActivePlayers()
local closestId, closestPed, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
maxDistance = distance
closestId = playerId
closestPed = playerPed
closestCoords = playerCoords
end
end
return closestId, closestPed, closestCoords
end
return lib.getClosestPlayer
@@ -0,0 +1,37 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayerVehicle? boolean Whether or not to include the player's current vehicle. Ignored on the server.
---@return number? vehicle
---@return vector3? vehicleCoords
function lib.getClosestVehicle(coords, maxDistance, includePlayerVehicle)
local vehicles = GetGamePool('CVehicle')
local closestVehicle, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #vehicles do
local vehicle = vehicles[i]
if lib.context == 'server' or not cache.vehicle or vehicle ~= cache.vehicle or includePlayerVehicle then
local vehicleCoords = GetEntityCoords(vehicle)
local distance = #(coords - vehicleCoords)
if distance < maxDistance then
maxDistance = distance
closestVehicle = vehicle
closestCoords = vehicleCoords
end
end
end
return closestVehicle, closestCoords
end
return lib.getClosestVehicle
@@ -0,0 +1,46 @@
--[[
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>
]]
---@param path string
---@param pattern string
---@return table string[]
---@return integer fileCount
function lib.getFilesInDirectory(path, pattern)
local resource = cache.resource
if path:find('^@') then
resource = path:gsub('^@(.-)/.+', '%1')
path = path:sub(#resource + 3)
end
local files = {}
local fileCount = 0
local windows = string.match(os.getenv('OS') or '', 'Windows')
local command = ('%s%s%s'):format(
windows and 'dir "' or 'ls "',
(GetResourcePath(resource):gsub('//', '/') .. '/' .. path):gsub('\\', '/'),
windows and '/" /b' or '/"'
)
local dir = io.popen(command)
if dir then
for line in dir:lines() do
if line:match(pattern) then
fileCount += 1
files[fileCount] = line
end
end
dir:close()
end
return files, fileCount
end
return lib.getFilesInDirectory
@@ -0,0 +1,36 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { object: number, coords: vector3 }[]
function lib.getNearbyObjects(coords, maxDistance)
local objects = GetGamePool('CObject')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #objects do
local object = objects[i]
local objectCoords = GetEntityCoords(object)
local distance = #(coords - objectCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
object = object,
coords = objectCoords
}
end
end
return nearby
end
return lib.getNearbyObjects
@@ -0,0 +1,38 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { ped: number, coords: vector3 }[]
function lib.getNearbyPeds(coords, maxDistance)
local peds = GetGamePool('CPed')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #peds do
local ped = peds[i]
if not IsPedAPlayer(ped) then
local pedCoords = GetEntityCoords(ped)
local distance = #(coords - pedCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
ped = ped,
coords = pedCoords,
}
end
end
end
return nearby
end
return lib.getNearbyPeds
@@ -0,0 +1,41 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayer? boolean Whether or not to include the current player.
---@return { id: number, ped: number, coords: vector3 }[]
function lib.getNearbyPlayers(coords, maxDistance, includePlayer)
local players = GetActivePlayers()
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
if playerId ~= cache.playerId or includePlayer then
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
id = playerId,
ped = playerPed,
coords = playerCoords,
}
end
end
end
return nearby
end
return lib.getNearbyPlayers
@@ -0,0 +1,37 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { id: number, ped: number, coords: vector3 }[]
function lib.getNearbyPlayers(coords, maxDistance)
local players = GetActivePlayers()
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
id = playerId,
ped = playerPed,
coords = playerCoords,
}
end
end
return nearby
end
return lib.getNearbyPlayers
@@ -0,0 +1,39 @@
--[[
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>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayerVehicle? boolean Whether or not to include the player's current vehicle.
---@return { vehicle: number, coords: vector3 }[]
function lib.getNearbyVehicles(coords, maxDistance, includePlayerVehicle)
local vehicles = GetGamePool('CVehicle')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #vehicles do
local vehicle = vehicles[i]
if lib.context == 'server' or not cache.vehicle or vehicle ~= cache.vehicle or includePlayerVehicle then
local vehicleCoords = GetEntityCoords(vehicle)
local distance = #(coords - vehicleCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
vehicle = vehicle,
coords = vehicleCoords
}
end
end
end
return nearby
end
return lib.getNearbyVehicles
@@ -0,0 +1,56 @@
--[[
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 glm_sincos = require 'glm'.sincos --[[@as fun(n: number): number, number]]
local glm_rad = require 'glm'.rad --[[@as fun(n: number): number]]
---Get the relative coordinates based on heading/rotation and offset
---@overload fun(coords: vector3, heading: number, offset: vector3): vector3
---@overload fun(coords: vector4, offset: vector3): vector4
---@overload fun(coords: vector3, rotation: vector3, offset: vector3): vector3
function lib.getRelativeCoords(coords, rotation, offset)
if type(rotation) == 'vector3' and offset then
local pitch = glm_rad(rotation.x)
local roll = glm_rad(rotation.y)
local yaw = glm_rad(rotation.z)
local sp, cp = glm_sincos(pitch)
local sr, cr = glm_sincos(roll)
local sy, cy = glm_sincos(yaw)
local rotatedX = offset.x * (cy * cr) + offset.y * (cy * sr * sp - sy * cp) + offset.z * (cy * sr * cp + sy * sp)
local rotatedY = offset.x * (sy * cr) + offset.y * (sy * sr * sp + cy * cp) + offset.z * (sy * sr * cp - cy * sp)
local rotatedZ = offset.x * (-sr) + offset.y * (cr * sp) + offset.z * (cr * cp)
return vec3(
coords.x + rotatedX,
coords.y + rotatedY,
coords.z + rotatedZ
)
end
offset = offset or rotation
local x, y, z, w = coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or coords.w
local sin, cos = glm_sincos(glm_rad(w))
local relativeX = offset.x * cos - offset.y * sin
local relativeY = offset.x * sin + offset.y * cos
return coords.w and vec4(
x + relativeX,
y + relativeY,
z + offset.z,
w
) or vec3(
x + relativeX,
y + relativeY,
z + offset.z
)
end
return lib.getRelativeCoords
@@ -0,0 +1,194 @@
--[[
Based on PolyZone's grid system (https://github.com/mkafrin/PolyZone/blob/master/ComboZone.lua)
MIT License
Copyright © 2019-2021 Michael Afrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local mapMinX = -3700
local mapMinY = -4400
local mapMaxX = 4500
local mapMaxY = 8000
local xDelta = (mapMaxX - mapMinX) / 34
local yDelta = (mapMaxY - mapMinY) / 50
local grid = {}
local lastCell = {}
local gridCache = {}
local entrySet = {}
lib.grid = {}
---@class GridEntry
---@field coords vector
---@field length? number
---@field width? number
---@field radius? number
---@field [string] any
---@param point vector
---@param length number
---@param width number
---@return number, number, number, number
local function getGridDimensions(point, length, width)
local minX = (point.x - width - mapMinX) // xDelta
local maxX = (point.x + width - mapMinX) // xDelta
local minY = (point.y - length - mapMinY) // yDelta
local maxY = (point.y + length - mapMinY) // yDelta
return minX, maxX, minY, maxY
end
---@param point vector
---@return number, number
function lib.grid.getCellPosition(point)
local x = (point.x - mapMinX) // xDelta
local y = (point.y - mapMinY) // yDelta
return x, y
end
---@param point vector
---@return GridEntry[]
function lib.grid.getCell(point)
local x, y = lib.grid.getCellPosition(point)
if lastCell.x ~= x or lastCell.y ~= y then
lastCell.x = x
lastCell.y = y
lastCell.cell = grid[y] and grid[y][x] or {}
end
return lastCell.cell
end
---@param point vector
---@param filter? fun(entry: GridEntry): boolean
---@return Array<GridEntry>
function lib.grid.getNearbyEntries(point, filter)
local minX, maxX, minY, maxY = getGridDimensions(point, xDelta, yDelta)
if gridCache.filter == filter and
gridCache.minX == minX and
gridCache.maxX == maxX and
gridCache.minY == minY and
gridCache.maxY == maxY then
return gridCache.entries
end
local entries = lib.array:new()
local n = 0
table.wipe(entrySet)
for y = minY, maxY do
local row = grid[y]
for x = minX, maxX do
local cell = row and row[x]
if cell then
for j = 1, #cell do
local entry = cell[j]
if not entrySet[entry] and (not filter or filter(entry)) then
n = n + 1
entrySet[entry] = true
entries[n] = entry
end
end
end
end
end
gridCache.minX = minX
gridCache.maxX = maxX
gridCache.minY = minY
gridCache.maxY = maxY
gridCache.entries = entries
gridCache.filter = filter
return entries
end
---@param entry { coords: vector, length?: number, width?: number, radius?: number, [string]: any }
function lib.grid.addEntry(entry)
entry.length = entry.length or entry.radius * 2
entry.width = entry.width or entry.radius * 2
local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width)
for y = minY, maxY do
local row = grid[y] or {}
for x = minX, maxX do
local cell = row[x] or {}
cell[#cell + 1] = entry
row[x] = cell
end
grid[y] = row
table.wipe(gridCache)
end
end
---@param entry table A table that was added to the grid previously.
function lib.grid.removeEntry(entry)
local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width)
local success = false
for y = minY, maxY do
local row = grid[y]
if not row then goto continue end
for x = minX, maxX do
local cell = row[x]
if cell then
for i = 1, #cell do
if cell[i] == entry then
table.remove(cell, i)
success = true
break
end
end
if #cell == 0 then
row[x] = nil
end
end
end
if not next(row) then
grid[y] = nil
end
::continue::
end
table.wipe(gridCache)
return success
end
return lib.grid
@@ -0,0 +1,122 @@
--[[
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>
]]
---@type { [string]: string }
local dict = {}
---@param source { [string]: string }
---@param target { [string]: string }
---@param prefix? string
local function flattenDict(source, target, prefix)
for key, value in pairs(source) do
local fullKey = prefix and (prefix .. '.' .. key) or key
if type(value) == 'table' then
flattenDict(value, target, fullKey)
else
target[fullKey] = value
end
end
return target
end
---@param str string
---@param ... string | number
---@return string
function locale(str, ...)
local lstr = dict[str]
if lstr then
if ... then
return lstr and lstr:format(...)
end
return lstr
end
return str
end
function lib.getLocales()
return dict
end
local function loadLocale(key)
local data = LoadResourceFile(cache.resource, ('locales/%s.json'):format(key))
if not data then
warn(("could not load 'locales/%s.json'"):format(key))
end
return json.decode(data) or {}
end
local table = lib.table
---Loads the ox_lib locale module. Prefer using fxmanifest instead (see [docs](https://overextended.dev/ox_lib#usage)).
---@param key? string
function lib.locale(key)
local lang = key or lib.getLocaleKey()
local locales = loadLocale('en')
if lang ~= 'en' then
table.merge(locales, loadLocale(lang))
end
table.wipe(dict)
for k, v in pairs(flattenDict(locales, {})) do
if type(v) == 'string' then
for var in v:gmatch('${[%w%s%p]-}') do
local locale = locales[var:sub(3, -2)]
if locale then
locale = locale:gsub('%%', '%%%%')
v = v:gsub(var, locale)
end
end
end
dict[k] = v
end
end
---Gets a locale string from another resource and adds it to the dict.
---@param resource string
---@param key string
---@return string?
function lib.getLocale(resource, key)
local locale = dict[key]
if locale then
warn(("overwriting existing locale '%s' (%s)"):format(key, locale))
end
locale = exports[resource]:getLocale(key)
dict[key] = locale
if not locale then
warn(("no locale exists with key '%s' in resource '%s'"):format(key, resource))
end
return locale
end
---Backing function for lib.getLocale.
---@param key string
---@return string?
exports('getLocale', function(key)
return dict[key]
end)
AddEventHandler('ox_lib:setLocale', function(key)
lib.locale(key)
end)
return lib.locale

Some files were not shown because too many files have changed in this diff Show More