adding new mlo removed things
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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'
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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',
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
!cache
|
||||
+118
@@ -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
|
||||
+164
@@ -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
|
||||
+91
@@ -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
|
||||
+363
@@ -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
|
||||
+145
@@ -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
|
||||
+126
@@ -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
|
||||
+158
@@ -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
|
||||
+473
@@ -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
|
||||
+64
@@ -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
|
||||
+95
@@ -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
|
||||
+34
@@ -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
|
||||
+36
@@ -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
|
||||
+40
@@ -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
|
||||
+36
@@ -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
|
||||
+37
@@ -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
|
||||
+46
@@ -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
|
||||
+36
@@ -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
|
||||
+38
@@ -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
|
||||
+41
@@ -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
|
||||
+37
@@ -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
|
||||
+39
@@ -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
|
||||
+56
@@ -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
|
||||
+194
@@ -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
|
||||
+122
@@ -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
Reference in New Issue
Block a user