fix(qb-core): post-update recovery + centralizare notify 17mov_Hud
Restaurat jobs.lua din git (Quasar fork a suprascris joburile 17mov). Adăugat item map în items.lua (lipsea, rupt rv-maphold). Setat licences.driver = false în config.lua. Override QBCore.Functions.Notify + QBCore:Notify event → 17mov_Hud:ShowNotification (toate notificările merg automat prin 17mov_Hud).
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
local id = 0
|
||||
local MugshotsCache = {}
|
||||
local Answers = {}
|
||||
|
||||
function GetMugShotBase64(Ped,Tasparent)
|
||||
if not Ped then return "" end
|
||||
id = id + 1
|
||||
|
||||
local Handle = RegisterPedheadshot(Ped)
|
||||
|
||||
local timer = 2000
|
||||
while ((not Handle or not IsPedheadshotReady(Handle) or not IsPedheadshotValid(Handle)) and timer > 0) do
|
||||
Citizen.Wait(10)
|
||||
timer = timer - 10
|
||||
end
|
||||
|
||||
local MugShotTxd = 'none'
|
||||
if (IsPedheadshotReady(Handle) and IsPedheadshotValid(Handle)) then
|
||||
MugshotsCache[id] = Handle
|
||||
MugShotTxd = GetPedheadshotTxdString(Handle)
|
||||
end
|
||||
|
||||
SendNUIMessage({
|
||||
type = 'convert',
|
||||
pMugShotTxd = MugShotTxd,
|
||||
removeImageBackGround = Tasparent or false,
|
||||
id = id,
|
||||
})
|
||||
|
||||
local p = promise.new()
|
||||
Answers[id] = p
|
||||
|
||||
return Citizen.Await(p)
|
||||
end
|
||||
exports("GetMugShotBase64", GetMugShotBase64)
|
||||
|
||||
RegisterNUICallback('Answer', function(data)
|
||||
if MugshotsCache[data.Id] then
|
||||
UnregisterPedheadshot(MugshotsCache[data.Id])
|
||||
MugshotsCache[data.Id] = nil
|
||||
end
|
||||
Answers[data.Id]:resolve(data.Answer)
|
||||
Answers[data.Id] = nil
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if (GetCurrentResourceName() ~= resourceName) then
|
||||
return
|
||||
end
|
||||
for k,v in pairs(MugshotsCache) do
|
||||
UnregisterPedheadshot(v)
|
||||
end
|
||||
MugshotsCache = {}
|
||||
id = 0
|
||||
end)
|
||||
|
||||
RegisterCommand("base64mugshotNormal",function(source,args,rawCommand)
|
||||
print(GetMugShotBase64(GetPlayerPed(-1),false))
|
||||
end,false)
|
||||
|
||||
RegisterCommand("base64mugshotTrasParent",function(source,args,rawCommand)
|
||||
print(GetMugShotBase64(GetPlayerPed(-1),true))
|
||||
end,false)
|
||||
@@ -0,0 +1,18 @@
|
||||
fx_version 'cerulean'
|
||||
game 'gta5'
|
||||
|
||||
name "MugShotBase64"
|
||||
description 'A script can convert peds mugshot image to Base64 encoding to save that as save and manage that'
|
||||
author "BaziForYou#9907"
|
||||
|
||||
ui_page 'html/index.html'
|
||||
|
||||
files {
|
||||
"html/js/*",
|
||||
"html/js/models/*",
|
||||
"html/img/*",
|
||||
"html/index.html",
|
||||
}
|
||||
client_script {
|
||||
"client.lua",
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 KiB |
@@ -0,0 +1,9 @@
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script src="nui://game/ui/jquery.js" type="text/javascript"></script>
|
||||
<script src="js/tfjs@1.2"></script>
|
||||
<script src="js/body-pix@2.0"></script>
|
||||
<script defer src="js/script.js"></script>
|
||||
</head>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,102 @@
|
||||
|
||||
async function getBase64Image(src, removeImageBackGround, callback, outputFormat) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
img.addEventListener("load", () => loadFunc(), false);
|
||||
async function loadFunc() {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
var convertingCanvas = canvas;
|
||||
if (removeImageBackGround) {
|
||||
var selectedSize = 320
|
||||
canvas.height = selectedSize;
|
||||
canvas.width = selectedSize;
|
||||
ctx.drawImage(img, 0, 0, selectedSize, selectedSize);
|
||||
await removeBackGround(canvas);
|
||||
const canvas2 = document.createElement('canvas');
|
||||
const ctx2 = canvas2.getContext('2d');
|
||||
canvas2.height = 64;
|
||||
canvas2.width = 64;
|
||||
ctx2.drawImage(canvas, 0, 0, selectedSize, selectedSize, 0, 0, img.naturalHeight, img.naturalHeight);
|
||||
convertingCanvas = canvas2;
|
||||
} else {
|
||||
canvas.height = img.naturalHeight;
|
||||
canvas.width = img.naturalWidth;
|
||||
ctx.drawImage(img, 0, 0);
|
||||
}
|
||||
var dataURL = convertingCanvas.toDataURL(outputFormat);
|
||||
canvas.remove();
|
||||
convertingCanvas.remove();
|
||||
img.remove();
|
||||
callback(dataURL);
|
||||
};
|
||||
|
||||
img.src = src;
|
||||
if (img.complete || img.complete === undefined) {
|
||||
img.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACEAAAAkCAIAAACIS8SLAAAAKklEQVRIie3NMQEAAAgDILV/55nBww8K0Enq2XwHDofD4XA4HA6Hw+E4Wwq6A0U+bfCEAAAAAElFTkSuQmCC";
|
||||
img.src = src;
|
||||
}
|
||||
}
|
||||
|
||||
async function Convert(pMugShotTxd, removeImageBackGround, id) {
|
||||
var tempUrl = `https://nui-img/${pMugShotTxd}/${pMugShotTxd}?t=${String(Math.round(new Date().getTime() / 1000))}`;
|
||||
if (pMugShotTxd == 'none') {
|
||||
tempUrl = './img/failSafe.png';
|
||||
}
|
||||
getBase64Image(tempUrl, removeImageBackGround, function(dataUrl) {
|
||||
$.post(`https://${GetParentResourceName()}/Answer`, JSON.stringify({
|
||||
Answer: dataUrl,
|
||||
Id: id,
|
||||
}));
|
||||
})
|
||||
}
|
||||
|
||||
// https://www.youtube.com/watch?v=GV6LSAYzEgc
|
||||
async function removeBackGround(sentCanvas) {
|
||||
const canvas = sentCanvas;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Loading the model
|
||||
const net = await bodyPix.load({
|
||||
architecture: 'MobileNetV1',
|
||||
outputStride: 16,
|
||||
multiplier: 0.75,
|
||||
quantBytes: 2,
|
||||
modelUrl: "./js/models/model-stride16.json"
|
||||
});
|
||||
|
||||
// Segmentation
|
||||
const { data:map } = await net.segmentPerson(canvas, {
|
||||
internalResolution: 'medium',
|
||||
});
|
||||
|
||||
// Extracting image data
|
||||
const { data:imgData } = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Creating new image data
|
||||
const newImg = ctx.createImageData(canvas.width, canvas.height);
|
||||
const newImgData = newImg.data;
|
||||
|
||||
for (var i=0; i<map.length; i++) {
|
||||
//The data array stores four values for each pixel
|
||||
const [r, g, b, a] = [imgData[i*4], imgData[i*4+1], imgData[i*4+2], imgData[i*4+3]];
|
||||
[
|
||||
newImgData[i*4],
|
||||
newImgData[i*4+1],
|
||||
newImgData[i*4+2],
|
||||
newImgData[i*4+3]
|
||||
] = !map[i] ? [255, 255, 255, 0] : [r, g, b, a];
|
||||
}
|
||||
|
||||
// Draw the new image back to canvas
|
||||
ctx.putImageData(newImg, 0, 0);
|
||||
}
|
||||
|
||||
window.addEventListener("message", (e) => GotMessage(e), false);
|
||||
async function GotMessage(e) {
|
||||
var msg = e.data
|
||||
if (msg.type == 'convert') {
|
||||
Convert(msg.pMugShotTxd, msg.removeImageBackGround, msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+29
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**First of all**
|
||||
Did you try disabling `bob74_ipl` to see if the issue is still there? Yes/No
|
||||
Did you use the latest version of `bob74_ipl` ? Yes/No
|
||||
Did you use an up to date server [artifact](https://runtime.fivem.net/artifacts/fivem/build_server_windows/master/) ? Yes/No
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Coordinates or map screenshot of the location.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen, a screenshot from GTA Online if relevant is welcome.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Versions**
|
||||
- Game build version
|
||||
- Resource version
|
||||
@@ -1,32 +0,0 @@
|
||||
# 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://coxdocs.dev/ox_lib
|
||||
|
||||
## 💾 Download
|
||||
|
||||
https://github.com/communityox/ox_lib/releases/latest/download/ox_lib.zip
|
||||
|
||||
## 📦 npm package
|
||||
|
||||
https://www.npmjs.com/package/@communityox/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 IntelliSense](https://marketplace.visualstudio.com/items?itemName=communityox.cfxlua-vscode-cox) 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"
|
||||
@@ -1,48 +0,0 @@
|
||||
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.32.3'
|
||||
license 'LGPL-3.0-or-later'
|
||||
repository 'https://github.com/communityox/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 @@
|
||||
!cache
|
||||
@@ -1,118 +0,0 @@
|
||||
-- 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
|
||||
@@ -1,164 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,91 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,363 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,145 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,126 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,158 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,473 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,64 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,118 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
|
||||
---@param x number
|
||||
---@param y number
|
||||
function lib.dui:sendMouseMove(x, y)
|
||||
SendDuiMouseMove(self.duiObject, x, y)
|
||||
end
|
||||
|
||||
---@param button 'left' | 'middle' | 'right'
|
||||
function lib.dui:sendMouseDown(button)
|
||||
SendDuiMouseDown(self.duiObject, button)
|
||||
end
|
||||
|
||||
---@param button 'left' | 'middle' | 'right'
|
||||
function lib.dui:sendMouseUp(button)
|
||||
SendDuiMouseUp(self.duiObject, button)
|
||||
end
|
||||
|
||||
---@param deltaX number
|
||||
---@param deltaY number
|
||||
function lib.dui:sendMouseWheel(deltaX, deltaY)
|
||||
SendDuiMouseWheel(self.duiObject, deltaY, deltaX)
|
||||
end
|
||||
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if cache.resource ~= resourceName then return end
|
||||
|
||||
for _, dui in pairs(duis) do
|
||||
dui:remove()
|
||||
end
|
||||
end)
|
||||
|
||||
return lib.dui
|
||||
@@ -1,34 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,40 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,40 +0,0 @@
|
||||
--[[
|
||||
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 ignorePlayerId? number|false The player server ID to ignore.
|
||||
---@return number? playerId
|
||||
---@return number? playerPed
|
||||
---@return vector3? playerCoords
|
||||
function lib.getClosestPlayer(coords, maxDistance, ignorePlayerId)
|
||||
local players = GetActivePlayers()
|
||||
local closestId, closestPed, closestCoords
|
||||
maxDistance = maxDistance or 2.0
|
||||
|
||||
for i = 1, #players do
|
||||
local playerId = players[i]
|
||||
|
||||
if not ignorePlayerId or playerId ~= ignorePlayerId 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
|
||||
@@ -1,37 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,46 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,38 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,41 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,37 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,39 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,56 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,194 +0,0 @@
|
||||
--[[
|
||||
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
|
||||
@@ -1,122 +0,0 @@
|
||||
--[[
|
||||
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://coxdocs.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
|
||||
@@ -1,332 +0,0 @@
|
||||
--[[
|
||||
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 service = GetConvar('ox:logger', 'datadog')
|
||||
local buffer
|
||||
local bufferSize = 0
|
||||
|
||||
local function removeColorCodes(str)
|
||||
-- replace ^[0-9] with nothing
|
||||
str = string.gsub(str, "%^%d", "")
|
||||
|
||||
-- replace ^#[0-9A-F]{3,6} with nothing
|
||||
str = string.gsub(str, "%^#[%dA-Fa-f]+", "")
|
||||
|
||||
-- replace ~[a-z]~ with nothing
|
||||
str = string.gsub(str, "~[%a]~", "")
|
||||
|
||||
return str
|
||||
end
|
||||
|
||||
local hostname = removeColorCodes(GetConvar('ox:logger:hostname', GetConvar('sv_projectName', 'fxserver')))
|
||||
|
||||
local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
||||
|
||||
local function base64encode(data)
|
||||
return ((data:gsub(".", function(x)
|
||||
local r, b = "", x:byte()
|
||||
for i = 8, 1, -1 do
|
||||
r = r .. (b % 2 ^ i - b % 2 ^ (i - 1) > 0 and "1" or "0")
|
||||
end
|
||||
return r;
|
||||
end) .. "0000"):gsub("%d%d%d?%d?%d?%d?", function(x)
|
||||
if (#x < 6) then
|
||||
return ""
|
||||
end
|
||||
local c = 0
|
||||
for i = 1, 6 do
|
||||
c = c + (x:sub(i, i) == "1" and 2 ^ (6 - i) or 0)
|
||||
end
|
||||
return b:sub(c + 1, c + 1)
|
||||
end) .. ({"", "==", "="})[#data % 3 + 1])
|
||||
end
|
||||
|
||||
local function getAuthorizationHeader(user, password)
|
||||
return "Basic " .. base64encode(user .. ":" .. password)
|
||||
end
|
||||
|
||||
|
||||
local function badResponse(endpoint, status, response)
|
||||
warn(('unable to submit logs to %s (status: %s)\n%s'):format(endpoint, status, json.encode(response, { indent = true })))
|
||||
end
|
||||
|
||||
local playerData = {}
|
||||
|
||||
AddEventHandler('playerDropped', function()
|
||||
playerData[source] = nil
|
||||
end)
|
||||
|
||||
local function formatTags(source, tags)
|
||||
if type(source) == 'number' and source > 0 then
|
||||
local data = playerData[source]
|
||||
|
||||
if not data then
|
||||
local _data = {
|
||||
('username:%s'):format(GetPlayerName(source))
|
||||
}
|
||||
|
||||
local num = 1
|
||||
|
||||
---@cast source string
|
||||
for i = 0, GetNumPlayerIdentifiers(source) - 1 do
|
||||
local identifier = GetPlayerIdentifier(source, i)
|
||||
|
||||
if not identifier:find('ip') then
|
||||
num += 1
|
||||
_data[num] = identifier
|
||||
end
|
||||
end
|
||||
|
||||
data = table.concat(_data, ',')
|
||||
playerData[source] = data
|
||||
end
|
||||
|
||||
tags = tags and ('%s,%s'):format(tags, data) or data
|
||||
end
|
||||
|
||||
return tags
|
||||
end
|
||||
|
||||
if service == 'fivemanage' then
|
||||
local key = GetConvar('fivemanage:key', '')
|
||||
local dataset = GetConvar('fivemanage:dataset', '')
|
||||
|
||||
if key ~= '' then
|
||||
local endpoint = 'https://api.fivemanage.com/api/logs/batch'
|
||||
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
['Authorization'] = key,
|
||||
['User-Agent'] = 'ox_lib',
|
||||
}
|
||||
|
||||
if dataset ~= "" then
|
||||
headers['X-Fivemanage-Dataset'] = dataset
|
||||
end
|
||||
|
||||
function lib.logger(source, event, message, ...)
|
||||
if not buffer then
|
||||
buffer = {}
|
||||
|
||||
SetTimeout(500, function()
|
||||
PerformHttpRequest(endpoint, function(status, _, _, response)
|
||||
if status ~= 200 then
|
||||
if type(response) == 'string' then
|
||||
response = json.decode(response) or response
|
||||
badResponse(endpoint, status, response)
|
||||
end
|
||||
end
|
||||
end, 'POST', json.encode(buffer), headers)
|
||||
|
||||
buffer = nil
|
||||
bufferSize = 0
|
||||
end)
|
||||
end
|
||||
|
||||
local metadata = {
|
||||
hostname = hostname,
|
||||
service = event,
|
||||
source = source,
|
||||
}
|
||||
|
||||
local playerTags = formatTags(source, nil)
|
||||
if playerTags and type(playerTags) == 'string' then
|
||||
local tempTable = { string.strsplit(',', playerTags) }
|
||||
for _, v in pairs(tempTable) do
|
||||
local key, value = string.strsplit(':', v)
|
||||
if key and value then
|
||||
metadata[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local args = { ... }
|
||||
for _, arg in pairs(args) do
|
||||
if type(arg) == 'table' then
|
||||
for k, v in pairs(arg) do
|
||||
metadata[k] = v
|
||||
end
|
||||
elseif type(arg) == 'string' then
|
||||
local key, value = string.strsplit(':', arg)
|
||||
if key and value then
|
||||
metadata[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
bufferSize += 1
|
||||
buffer[bufferSize] = {
|
||||
level = "info",
|
||||
message = message,
|
||||
resource = cache.resource,
|
||||
metadata = metadata,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if service == 'datadog' then
|
||||
local key = GetConvar('datadog:key', ''):gsub("[\'\"]", '')
|
||||
|
||||
if key ~= '' then
|
||||
local endpoint = ('https://http-intake.logs.%s/api/v2/logs'):format(GetConvar('datadog:site', 'datadoghq.com'))
|
||||
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json',
|
||||
['DD-API-KEY'] = key,
|
||||
}
|
||||
|
||||
function lib.logger(source, event, message, ...)
|
||||
if not buffer then
|
||||
buffer = {}
|
||||
|
||||
SetTimeout(500, function()
|
||||
PerformHttpRequest(endpoint, function(status, _, _, response)
|
||||
if status ~= 202 then
|
||||
if type(response) == 'string' then
|
||||
response = json.decode(response:sub(10)) or response
|
||||
badResponse(endpoint, status, type(response) == 'table' and response.errors[1] or response)
|
||||
end
|
||||
end
|
||||
end, 'POST', json.encode(buffer), headers)
|
||||
|
||||
buffer = nil
|
||||
bufferSize = 0
|
||||
end)
|
||||
end
|
||||
|
||||
bufferSize += 1
|
||||
buffer[bufferSize] = {
|
||||
hostname = hostname,
|
||||
service = event,
|
||||
message = message,
|
||||
resource = cache.resource,
|
||||
ddsource = tostring(source),
|
||||
ddtags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil),
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if service == 'loki' then
|
||||
local lokiUser = GetConvar('loki:user', '')
|
||||
local lokiPassword = GetConvar('loki:password', GetConvar('loki:key', ''))
|
||||
local lokiEndpoint = GetConvar('loki:endpoint', '')
|
||||
local lokiTenant = GetConvar('loki:tenant', '')
|
||||
local startingPattern = '^http[s]?://'
|
||||
local headers = {
|
||||
['Content-Type'] = 'application/json'
|
||||
}
|
||||
|
||||
if lokiUser ~= '' then
|
||||
headers['Authorization'] = getAuthorizationHeader(lokiUser, lokiPassword)
|
||||
end
|
||||
|
||||
if lokiTenant ~= '' then
|
||||
headers['X-Scope-OrgID'] = lokiTenant
|
||||
end
|
||||
|
||||
if not lokiEndpoint:find(startingPattern) then
|
||||
lokiEndpoint = 'https://' .. lokiEndpoint
|
||||
end
|
||||
|
||||
local endpoint = ('%s/loki/api/v1/push'):format(lokiEndpoint)
|
||||
|
||||
-- Converts a string of comma seperated kvp string to a table of kvps
|
||||
-- example `discord:blahblah,fivem:blahblah,license:blahblah` -> `{discord="blahblah",fivem="blahblah",license="blahblah"}`
|
||||
local function convertDDTagsToKVP(tags)
|
||||
if not tags or type(tags) ~= 'string' then
|
||||
return {}
|
||||
end
|
||||
local tempTable = { string.strsplit(',', tags) } -- outputs a number index table wth k:v strings as values
|
||||
local bTable = table.create(0, #tempTable) -- buffer table
|
||||
|
||||
-- Loop through table and grab only values
|
||||
for _, v in pairs(tempTable) do
|
||||
local key, value = string.strsplit(':', v) -- splits string on ':' character
|
||||
bTable[key] = value
|
||||
end
|
||||
|
||||
return bTable -- Return the new table of kvps
|
||||
end
|
||||
|
||||
function lib.logger(source, event, message, ...)
|
||||
if not buffer then
|
||||
buffer = {}
|
||||
|
||||
SetTimeout(500, function()
|
||||
-- Strip string keys from buffer
|
||||
local tempBuffer = {}
|
||||
for _,v in pairs(buffer) do
|
||||
tempBuffer[#tempBuffer+1] = v
|
||||
end
|
||||
|
||||
local postBody = json.encode({streams = tempBuffer})
|
||||
PerformHttpRequest(endpoint, function(status, _, _, _)
|
||||
if status ~= 204 then
|
||||
badResponse(endpoint, status, ("%s"):format(status, postBody))
|
||||
end
|
||||
end, 'POST', postBody, headers)
|
||||
|
||||
buffer = nil
|
||||
end)
|
||||
end
|
||||
|
||||
-- Generates a nanosecond unix timestamp
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
local timestamp = ('%s000000000'):format(os.time(os.date('*t')))
|
||||
|
||||
-- Initializes values table with the message
|
||||
local values = {message = message}
|
||||
|
||||
-- Format the args into strings
|
||||
local tags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil)
|
||||
local tagsTable = convertDDTagsToKVP(tags)
|
||||
|
||||
-- Concatenates tags kvp table to the values table
|
||||
for k,v in pairs(tagsTable) do
|
||||
values[k] = v -- Store the tags in the values table ready for logging
|
||||
end
|
||||
|
||||
-- initialise stream payload
|
||||
local payload = {
|
||||
stream = {
|
||||
server = hostname,
|
||||
resource = cache.resource,
|
||||
event = event
|
||||
},
|
||||
values = {
|
||||
{
|
||||
timestamp,
|
||||
json.encode(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-- Safety check incase it throws index issue
|
||||
if not buffer then
|
||||
buffer = {}
|
||||
end
|
||||
|
||||
-- Checks if the event exists in the buffer and adds to the values if found
|
||||
-- else initialises the stream
|
||||
if not buffer[event] then
|
||||
buffer[event] = payload
|
||||
else
|
||||
local lastIndex = #buffer[event].values
|
||||
lastIndex += 1
|
||||
|
||||
buffer[event].values[lastIndex] = {
|
||||
timestamp,
|
||||
json.encode(values)
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lib.logger
|
||||
@@ -1,116 +0,0 @@
|
||||
--[[
|
||||
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: param-type-mismatch
|
||||
lib.marker = {}
|
||||
|
||||
---@enum (key) MarkerType
|
||||
local markerTypes = {
|
||||
UpsideDownCone = 0,
|
||||
VerticalCylinder = 1,
|
||||
ThickChevronUp = 2,
|
||||
ThinChevronUp = 3,
|
||||
CheckeredFlagRect = 4,
|
||||
CheckeredFlagCircle = 5,
|
||||
VerticleCircle = 6,
|
||||
PlaneModel = 7,
|
||||
LostMCTransparent = 8,
|
||||
LostMC = 9,
|
||||
Number0 = 10,
|
||||
Number1 = 11,
|
||||
Number2 = 12,
|
||||
Number3 = 13,
|
||||
Number4 = 14,
|
||||
Number5 = 15,
|
||||
Number6 = 16,
|
||||
Number7 = 17,
|
||||
Number8 = 18,
|
||||
Number9 = 19,
|
||||
ChevronUpx1 = 20,
|
||||
ChevronUpx2 = 21,
|
||||
ChevronUpx3 = 22,
|
||||
HorizontalCircleFat = 23,
|
||||
ReplayIcon = 24,
|
||||
HorizontalCircleSkinny = 25,
|
||||
HorizontalCircleSkinny_Arrow = 26,
|
||||
HorizontalSplitArrowCircle = 27,
|
||||
DebugSphere = 28,
|
||||
DollarSign = 29,
|
||||
HorizontalBars = 30,
|
||||
WolfHead = 31,
|
||||
QuestionMark = 32,
|
||||
PlaneSymbol = 33,
|
||||
HelicopterSymbol = 34,
|
||||
BoatSymbol = 35,
|
||||
CarSymbol = 36,
|
||||
MotorcycleSymbol = 37,
|
||||
BikeSymbol = 38,
|
||||
TruckSymbol = 39,
|
||||
ParachuteSymbol = 40,
|
||||
Unknown41 = 41,
|
||||
SawbladeSymbol = 42,
|
||||
Unknown43 = 43,
|
||||
}
|
||||
|
||||
---@class MarkerProps
|
||||
---@field type? MarkerType | integer
|
||||
---@field coords { x: number, y: number, z: number }
|
||||
---@field width? number
|
||||
---@field height? number
|
||||
---@field color? { r: integer, g: integer, b: integer, a: integer }
|
||||
---@field rotation? { x: number, y: number, z: number }
|
||||
---@field direction? { x: number, y: number, z: number }
|
||||
---@field bobUpAndDown? boolean
|
||||
---@field faceCamera? boolean
|
||||
---@field rotate? boolean
|
||||
---@field textureDict? string
|
||||
---@field textureName? string
|
||||
---@field invert? boolean
|
||||
|
||||
local vector3_zero = vector3(0, 0, 0)
|
||||
|
||||
local marker_mt = {
|
||||
type = 0,
|
||||
width = 2.,
|
||||
height = 1.,
|
||||
color = {r = 255, g = 100, b = 0, a = 100},
|
||||
rotation = vector3_zero,
|
||||
direction = vector3_zero,
|
||||
bobUpAndDown = false,
|
||||
faceCamera = false,
|
||||
rotate = false,
|
||||
invert = false,
|
||||
}
|
||||
marker_mt.__index = marker_mt
|
||||
|
||||
function marker_mt:draw()
|
||||
DrawMarker(
|
||||
self.type,
|
||||
self.coords.x, self.coords.y, self.coords.z,
|
||||
self.direction.x, self.direction.y, self.direction.z,
|
||||
self.rotation.x, self.rotation.y, self.rotation.z,
|
||||
self.width, self.width, self.height,
|
||||
self.color.r, self.color.g, self.color.b, self.color.a,
|
||||
self.bobUpAndDown, self.faceCamera, 2, self.rotate, self.textureDict, self.textureName, self.invert)
|
||||
end
|
||||
|
||||
---@param options MarkerProps
|
||||
function lib.marker.new(options)
|
||||
options.type =
|
||||
type(options.type) == 'string' and markerTypes[options.type]
|
||||
or type(options.type) == 'number' and options.type or nil
|
||||
|
||||
local self = setmetatable(options, marker_mt)
|
||||
|
||||
self.width += .0
|
||||
self.height += .0
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
return lib.marker
|
||||
@@ -1,229 +0,0 @@
|
||||
--[[
|
||||
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 oxmath : mathlib
|
||||
lib.math = math
|
||||
|
||||
local function parseNumber(input, min, max, round)
|
||||
local n = tonumber(input)
|
||||
|
||||
if not n then
|
||||
error(("value cannot be converted into a number (received %s)"):format(input), 3)
|
||||
end
|
||||
|
||||
n = round and math.floor(n + 0.5) or n
|
||||
|
||||
if min and n < min then
|
||||
error(("value does not meet minimum value of '%s' (received %s)"):format(min, n), 3)
|
||||
end
|
||||
|
||||
if max and n > max then
|
||||
error(("value exceeds maximum value of '%s' (received %s)"):format(max, n), 3)
|
||||
end
|
||||
|
||||
return n
|
||||
end
|
||||
|
||||
---Takes a string and returns a set of scalar values.
|
||||
---@param input string
|
||||
---@param min? number
|
||||
---@param max? number
|
||||
---@param round? boolean
|
||||
---@return number? ...
|
||||
function math.toscalars(input, min, max, round)
|
||||
local arr = {}
|
||||
local i = 0
|
||||
|
||||
for s in string.gmatch(input:gsub('[%w]+%w?%(', ''), '(-?[%w.%w]+)') do
|
||||
local n = parseNumber(s, min, max, round and (round == true or i < round))
|
||||
|
||||
i += 1
|
||||
arr[i] = n
|
||||
end
|
||||
|
||||
return table.unpack(arr)
|
||||
end
|
||||
|
||||
---Tries to convert its argument to a vector.
|
||||
---@param input string | table
|
||||
---@param min? number
|
||||
---@param max? number
|
||||
---@param round? boolean | number If round is a number, only round n values.
|
||||
---@return number | vector2 | vector3 | vector4
|
||||
function math.tovector(input, min, max, round)
|
||||
local inputType = type(input)
|
||||
|
||||
if inputType == 'string' then
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
return vector(math.toscalars(input, min, max, round))
|
||||
end
|
||||
|
||||
if inputType == 'table' then
|
||||
for _, v in pairs(input) do
|
||||
parseNumber(v, min, max, round)
|
||||
end
|
||||
|
||||
if table.type(input) == 'array' then
|
||||
return vector(table.unpack(input))
|
||||
end
|
||||
|
||||
-- vector doesn't accept literal nils
|
||||
return input.w and vector4(input.x, input.y, input.z, input.w)
|
||||
or input.z and vector3(input.x, input.y, input.z)
|
||||
or input.y and vector2(input.x, input.y)
|
||||
or input.x + 0.0
|
||||
end
|
||||
|
||||
error(('cannot convert %s to a vector value'):format(inputType), 2)
|
||||
end
|
||||
|
||||
---Tries to convert a surface Normal to a Rotation.
|
||||
---@param input vector3
|
||||
---@return vector3
|
||||
function math.normaltorotation(input)
|
||||
local inputType = type(input)
|
||||
|
||||
if inputType == 'vector3' then
|
||||
local pitch = -math.asin(input.y) * (180.0 / math.pi)
|
||||
local yaw = math.atan(input.x, input.z) * (180.0 / math.pi)
|
||||
return vec3(pitch, yaw, 0.0)
|
||||
end
|
||||
|
||||
error(('cannot convert type %s to a rotation vector'):format(inputType), 2)
|
||||
end
|
||||
|
||||
---Tries to convert its argument to a vector4.
|
||||
---@param input string | table
|
||||
---@return vector4
|
||||
function math.torgba(input)
|
||||
local res = math.tovector(input, 0, 255, 3)
|
||||
assert(type(res) == 'vector4', 'cannot convert input to rgba')
|
||||
parseNumber(res.a, 0, 1)
|
||||
return res
|
||||
end
|
||||
|
||||
---Takes a hexidecimal string and returns three integers.
|
||||
---@param input string
|
||||
---@return integer
|
||||
---@return integer
|
||||
---@return integer
|
||||
function math.hextorgb(input)
|
||||
local r, g, b = string.match(input, '([^#]+.)(..)(..)')
|
||||
return tonumber(r, 16), tonumber(g, 16), tonumber(b, 16)
|
||||
end
|
||||
|
||||
---Formats a number as a hexadecimal string.
|
||||
---@param n number | string
|
||||
---@param upper? boolean
|
||||
---@return string
|
||||
function math.tohex(n, upper)
|
||||
local formatString = ('0x%s'):format(upper and '%X' or '%x')
|
||||
return formatString:format(n)
|
||||
end
|
||||
|
||||
---Converts input number into grouped digits
|
||||
---@param number number
|
||||
---@param seperator? string
|
||||
---@return string
|
||||
function math.groupdigits(number, seperator) -- credit http://richard.warburton.it
|
||||
local left, num, right = string.match(number, '^([^%d]*%d)(%d*)(.-)$')
|
||||
return left .. (num:reverse():gsub('(%d%d%d)', '%1' .. (seperator or ',')):reverse()) .. right
|
||||
end
|
||||
|
||||
---Clamp a number between 2 other numbers
|
||||
---@param val number
|
||||
---@param lower number
|
||||
---@param upper number
|
||||
---@return number
|
||||
function math.clamp(val, lower, upper) -- credit https://love2d.org/forums/viewtopic.php?t=1856
|
||||
if lower > upper then lower, upper = upper, lower end -- swap if boundaries supplied the wrong way
|
||||
return math.max(lower, math.min(upper, val))
|
||||
end
|
||||
|
||||
---Calculates an intermediate value between `start` and `finish` based on the interpolation `factor`.
|
||||
---@generic T : number | vector2 | vector3 | vector4
|
||||
---@param start T
|
||||
---@param finish T
|
||||
---@param factor integer The interpolation factor between 0 and 1.
|
||||
---@return T
|
||||
function math.interp(start, finish, factor)
|
||||
return start + (finish - start) * factor
|
||||
end
|
||||
|
||||
local function interpolateTable(start, finish, factor)
|
||||
local interp = math.interp
|
||||
local result = {}
|
||||
|
||||
for k, v in pairs(start) do
|
||||
result[k] = interp(v, finish[k], factor)
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---Linearly interpolates between two values over a specified duration, returning an iterator function that will run once per game-frame.
|
||||
---@generic T : number | table | vector2 | vector3 | vector4
|
||||
---@param start T -- The starting value of the interpolation.
|
||||
---@param finish T -- The ending value of the interpolation.
|
||||
---@param duration number -- The duration over which to interpolate over in milliseconds.
|
||||
---@return fun(): T, number
|
||||
function math.lerp(start, finish, duration)
|
||||
local startTime = GetGameTimer()
|
||||
local typeStart = type(start)
|
||||
local typeFinish = type(finish)
|
||||
|
||||
if typeStart ~= 'number' and typeStart ~= 'vector2' and typeStart ~= 'vector3' and typeStart ~= 'vector4' and typeStart ~= 'table' then
|
||||
error(("expected argument 1 to have type '%s' (received %s)"):format('number | table | vector2 | vector3 | vector4', typeStart))
|
||||
end
|
||||
|
||||
assert(typeFinish == typeStart, ("expected argument 2 to have type '%s' (received %s)"):format(typeStart, typeFinish))
|
||||
|
||||
local interpFn = typeStart == 'table' and interpolateTable or math.interp
|
||||
local step
|
||||
|
||||
return function()
|
||||
if not step then
|
||||
step = 0
|
||||
return start, step
|
||||
end
|
||||
|
||||
if step == 1 then return end
|
||||
|
||||
Wait(0)
|
||||
step = math.min((GetGameTimer() - startTime) / duration, 1)
|
||||
|
||||
if step < 1 then
|
||||
return interpFn(start, finish, step), step
|
||||
end
|
||||
|
||||
return finish, step
|
||||
end
|
||||
end
|
||||
|
||||
---Rounds a number to a whole number or to the specified number of decimal places.
|
||||
---@param value number | string
|
||||
---@param places? number | string
|
||||
---@return number
|
||||
function math.round(value, places)
|
||||
if type(value) == 'string' then value = tonumber(value) end
|
||||
if type(value) ~= 'number' then error('Value must be a number') end
|
||||
|
||||
if places then
|
||||
if type(places) == 'string' then places = tonumber(places) end
|
||||
if type(places) ~= 'number' then error('Places must be a number') end
|
||||
|
||||
if places > 0 then
|
||||
local mult = 10 ^ (places or 0)
|
||||
return math.floor(value * mult + 0.5) / mult
|
||||
end
|
||||
end
|
||||
|
||||
return math.floor(value + 0.5)
|
||||
end
|
||||
|
||||
return lib.math
|
||||
@@ -1,78 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---@alias AnimationFlags number
|
||||
---| 0 DEFAULT
|
||||
---| 1 LOOPING
|
||||
---| 2 HOLD_LAST_FRAME
|
||||
---| 4 REPOSITION_WHEN_FINISHED
|
||||
---| 8 NOT_INTERRUPTABLE
|
||||
---| 16 UPPERBODY
|
||||
---| 32 SECONDARY
|
||||
---| 64 REORIENT_WHEN_FINISHED
|
||||
---| 128 ABORT_ON_PED_MOVEMENT
|
||||
---| 256 ADDITIVE
|
||||
---| 512 TURN_OFF_COLLISION
|
||||
---| 1024 OVERRIDE_PHYSICS
|
||||
---| 2048 IGNORE_GRAVITY
|
||||
---| 4096 EXTRACT_INITIAL_OFFSET
|
||||
---| 8192 EXIT_AFTER_INTERRUPTED
|
||||
---| 16384 TAG_SYNC_IN
|
||||
---| 32768 TAG_SYNC_OUT
|
||||
---| 65536 TAG_SYNC_CONTINUOUS
|
||||
---| 131072 FORCE_START
|
||||
---| 262144 USE_KINEMATIC_PHYSICS
|
||||
---| 524288 USE_MOVER_EXTRACTION
|
||||
---| 1048576 HIDE_WEAPON
|
||||
---| 2097152 ENDS_IN_DEAD_POSE
|
||||
---| 4194304 ACTIVATE_RAGDOLL_ON_COLLISION
|
||||
---| 8388608 DONT_EXIT_ON_DEATH
|
||||
---| 16777216 ABORT_ON_WEAPON_DAMAGE
|
||||
---| 33554432 DISABLE_FORCED_PHYSICS_UPDATE
|
||||
---| 67108864 PROCESS_ATTACHMENTS_ON_START
|
||||
---| 134217728 EXPAND_PED_CAPSULE_FROM_SKELETON
|
||||
---| 268435456 USE_ALTERNATIVE_FP_ANIM
|
||||
---| 536870912 BLENDOUT_WRT_LAST_FRAME
|
||||
---| 1073741824 USE_FULL_BLENDING
|
||||
|
||||
---@alias ControlFlags number
|
||||
---| 0 NONE
|
||||
---| 1 DISABLE_LEG_IK
|
||||
---| 2 DISABLE_ARM_IK
|
||||
---| 4 DISABLE_HEAD_IK
|
||||
---| 8 DISABLE_TORSO_IK
|
||||
---| 16 DISABLE_TORSO_REACT_IK
|
||||
---| 32 USE_LEG_ALLOW_TAGS
|
||||
---| 64 USE_LEG_BLOCK_TAGS
|
||||
---| 128 USE_ARM_ALLOW_TAGS
|
||||
---| 256 USE_ARM_BLOCK_TAGS
|
||||
---| 512 PROCESS_WEAPON_HAND_GRIP
|
||||
---| 1024 USE_FP_ARM_LEFT
|
||||
---| 2048 USE_FP_ARM_RIGHT
|
||||
---| 4096 DISABLE_TORSO_VEHICLE_IK
|
||||
---| 8192 LINKED_FACIAL
|
||||
|
||||
---@param ped number
|
||||
---@param animDictionary string
|
||||
---@param animationName string
|
||||
---@param blendInSpeed? number Defaults to 8.0
|
||||
---@param blendOutSpeed? number Defaults to -8.0
|
||||
---@param duration? integer Defaults to -1
|
||||
---@param animFlags? AnimationFlags
|
||||
---@param startPhase? number
|
||||
---@param phaseControlled? boolean
|
||||
---@param controlFlags? integer
|
||||
---@param overrideCloneUpdate? boolean
|
||||
function lib.playAnim(ped, animDictionary, animationName, blendInSpeed, blendOutSpeed, duration, animFlags, startPhase, phaseControlled, controlFlags, overrideCloneUpdate)
|
||||
lib.requestAnimDict(animDictionary)
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
TaskPlayAnim(ped, animDictionary, animationName, blendInSpeed or 8.0, blendOutSpeed or -8.0, duration or -1, animFlags or 0, startPhase or 0.0, phaseControlled or false, controlFlags or 0, overrideCloneUpdate or false)
|
||||
RemoveAnimDict(animDictionary)
|
||||
end
|
||||
|
||||
return lib.playAnim
|
||||
@@ -1,194 +0,0 @@
|
||||
--[[
|
||||
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 PointProperties
|
||||
---@field coords vector3
|
||||
---@field distance number
|
||||
---@field onEnter? fun(self: CPoint)
|
||||
---@field onExit? fun(self: CPoint)
|
||||
---@field nearby? fun(self: CPoint)
|
||||
---@field [string] any
|
||||
|
||||
---@class CPoint : PointProperties
|
||||
---@field id number
|
||||
---@field currentDistance number
|
||||
---@field isClosest? boolean
|
||||
---@field remove fun()
|
||||
|
||||
---@type table<number, CPoint>
|
||||
local points = {}
|
||||
---@type CPoint[]
|
||||
local nearbyPoints = {}
|
||||
local nearbyCount = 0
|
||||
---@type CPoint?
|
||||
local closestPoint
|
||||
local tick
|
||||
|
||||
local function removePoint(self)
|
||||
if closestPoint?.id == self.id then
|
||||
closestPoint = nil
|
||||
end
|
||||
|
||||
lib.grid.removeEntry(self)
|
||||
|
||||
points[self.id] = nil
|
||||
end
|
||||
|
||||
local function hasRemovePoint(entry)
|
||||
return entry.remove == removePoint
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local coords = GetEntityCoords(cache.ped)
|
||||
local newPoints = lib.grid.getNearbyEntries(coords, hasRemovePoint) --[[@as CPoint[] ]]
|
||||
local cellX, cellY = lib.grid.getCellPosition(coords)
|
||||
cache.coords = coords
|
||||
closestPoint = nil
|
||||
|
||||
if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
|
||||
for i = 1, nearbyCount do
|
||||
local point = nearbyPoints[i]
|
||||
|
||||
if point.inside then
|
||||
local distance = #(coords - point.coords)
|
||||
|
||||
if distance > point.radius then
|
||||
if point.onExit then point:onExit() end
|
||||
|
||||
point.inside = nil
|
||||
point.currentDistance = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cache.lastCellX = cellX
|
||||
cache.lastCellY = cellY
|
||||
end
|
||||
|
||||
if nearbyCount ~= 0 then
|
||||
table.wipe(nearbyPoints)
|
||||
nearbyCount = 0
|
||||
end
|
||||
|
||||
for i = 1, #newPoints do
|
||||
local point = newPoints[i]
|
||||
local distance = #(coords - point.coords)
|
||||
|
||||
if distance <= point.radius then
|
||||
point.currentDistance = distance
|
||||
|
||||
if not closestPoint or distance < (closestPoint.currentDistance or point.radius) then
|
||||
if closestPoint then closestPoint.isClosest = nil end
|
||||
|
||||
point.isClosest = true
|
||||
closestPoint = point
|
||||
end
|
||||
|
||||
nearbyCount += 1
|
||||
nearbyPoints[nearbyCount] = point
|
||||
|
||||
if not point.inside then
|
||||
point.inside = true
|
||||
if point.onEnter then
|
||||
point:onEnter()
|
||||
end
|
||||
end
|
||||
elseif point.currentDistance then
|
||||
if point.onExit then point:onExit() end
|
||||
|
||||
point.inside = nil
|
||||
point.currentDistance = nil
|
||||
end
|
||||
end
|
||||
|
||||
if not tick then
|
||||
if nearbyCount ~= 0 then
|
||||
tick = SetInterval(function()
|
||||
for i = nearbyCount, 1, -1 do
|
||||
local point = nearbyPoints[i]
|
||||
|
||||
if point and point.nearby then
|
||||
point:nearby()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
elseif nearbyCount == 0 then
|
||||
tick = ClearInterval(tick)
|
||||
end
|
||||
|
||||
Wait(300)
|
||||
end
|
||||
end)
|
||||
|
||||
local function toVector(coords)
|
||||
local _type = type(coords)
|
||||
|
||||
if _type ~= 'vector3' then
|
||||
if _type == 'table' or _type == 'vector4' then
|
||||
return vec3(coords[1] or coords.x, coords[2] or coords.y, coords[3] or coords.z)
|
||||
end
|
||||
|
||||
error(("expected type 'vector3' or 'table' (received %s)"):format(_type))
|
||||
end
|
||||
|
||||
return coords
|
||||
end
|
||||
|
||||
lib.points = {}
|
||||
|
||||
---@return CPoint
|
||||
---@overload fun(data: PointProperties): CPoint
|
||||
---@overload fun(coords: vector3, distance: number, data?: PointProperties): CPoint
|
||||
function lib.points.new(...)
|
||||
local args = { ... }
|
||||
local id = #points + 1
|
||||
local self
|
||||
|
||||
-- Support sending a single argument containing point data
|
||||
if type(args[1]) == 'table' then
|
||||
self = args[1]
|
||||
self.id = id
|
||||
self.remove = removePoint
|
||||
else
|
||||
-- Backwards compatibility for original implementation (args: coords, distance, data)
|
||||
self = {
|
||||
id = id,
|
||||
coords = args[1],
|
||||
remove = removePoint,
|
||||
}
|
||||
end
|
||||
|
||||
self.coords = toVector(self.coords)
|
||||
self.distance = self.distance or args[2]
|
||||
self.radius = self.distance
|
||||
|
||||
if args[3] then
|
||||
for k, v in pairs(args[3]) do
|
||||
self[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
lib.grid.addEntry(self)
|
||||
points[id] = self
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function lib.points.getAllPoints() return points end
|
||||
|
||||
function lib.points.getNearbyPoints() return nearbyPoints end
|
||||
|
||||
---@return CPoint?
|
||||
function lib.points.getClosestPoint() return closestPoint end
|
||||
|
||||
---@deprecated
|
||||
lib.points.closest = lib.points.getClosestPoint
|
||||
|
||||
return lib.points
|
||||
@@ -1,73 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---@enum PrintLevel
|
||||
local printLevel = {
|
||||
error = 1,
|
||||
warn = 2,
|
||||
info = 3,
|
||||
verbose = 4,
|
||||
debug = 5,
|
||||
}
|
||||
|
||||
local levelPrefixes = {
|
||||
'^1[ERROR]',
|
||||
'^3[WARN]',
|
||||
'^7[INFO]',
|
||||
'^4[VERBOSE]',
|
||||
'^6[DEBUG]',
|
||||
}
|
||||
local convarGlobal = 'ox:printlevel'
|
||||
local convarResource = 'ox:printlevel:' .. cache.resource
|
||||
local function getPrintLevelFromConvar()
|
||||
return printLevel[GetConvar(convarResource, GetConvar(convarGlobal, 'info'))]
|
||||
end
|
||||
local resourcePrintLevel = getPrintLevelFromConvar()
|
||||
local template = ('^5[%s] %%s %%s^7'):format(cache.resource)
|
||||
local function handleException(reason, value)
|
||||
if type(value) == 'function' then return tostring(value) end
|
||||
return reason
|
||||
end
|
||||
local jsonOptions = { sort_keys = true, indent = true, exception = handleException }
|
||||
|
||||
---Prints to console conditionally based on what ox:printlevel is.
|
||||
---Any print with a level more severe will also print. If ox:printlevel is info, then warn and error prints will appear as well, but debug prints will not.
|
||||
---@param level PrintLevel
|
||||
---@param ... any
|
||||
local function libPrint(level, ...)
|
||||
if level > resourcePrintLevel then return end
|
||||
|
||||
local args = { ... }
|
||||
|
||||
for i = 1, #args do
|
||||
local arg = args[i]
|
||||
args[i] = type(arg) == 'table' and json.encode(arg, jsonOptions) or tostring(arg)
|
||||
end
|
||||
|
||||
print(template:format(levelPrefixes[level], table.concat(args, '\t')))
|
||||
end
|
||||
|
||||
lib.print = {
|
||||
error = function(...) libPrint(printLevel.error, ...) end,
|
||||
warn = function(...) libPrint(printLevel.warn, ...) end,
|
||||
info = function(...) libPrint(printLevel.info, ...) end,
|
||||
verbose = function(...) libPrint(printLevel.verbose, ...) end,
|
||||
debug = function(...) libPrint(printLevel.debug, ...) end,
|
||||
}
|
||||
|
||||
-- Update the print level when the convar changes
|
||||
if (AddConvarChangeListener) then
|
||||
AddConvarChangeListener('ox:printlevel*', function(convarName, reserved)
|
||||
if (convarName ~= convarResource and convarName ~= convarGlobal) then return end
|
||||
resourcePrintLevel = getPrintLevelFromConvar()
|
||||
end)
|
||||
else
|
||||
libPrint(printLevel.verbose, 'Convar change listener not available, print level will not update dynamically.')
|
||||
end
|
||||
|
||||
return lib.print
|
||||
@@ -1,79 +0,0 @@
|
||||
--[[
|
||||
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.raycast = {}
|
||||
|
||||
local StartShapeTestLosProbe = StartShapeTestLosProbe
|
||||
local GetShapeTestResultIncludingMaterial = GetShapeTestResultIncludingMaterial
|
||||
local glm_sincos = require 'glm'.sincos
|
||||
local glm_rad = require 'glm'.rad
|
||||
local math_abs = math.abs
|
||||
local GetFinalRenderedCamCoord = GetFinalRenderedCamCoord
|
||||
local GetFinalRenderedCamRot = GetFinalRenderedCamRot
|
||||
|
||||
---@alias ShapetestIgnore
|
||||
---| 1 GLASS
|
||||
---| 2 SEE_THROUGH
|
||||
---| 3 GLASS | SEE_THROUGH
|
||||
---| 4 NO_COLLISION
|
||||
---| 7 GLASS | SEE_THROUGH | NO_COLLISION
|
||||
|
||||
---@alias ShapetestFlags integer
|
||||
---| 1 INCLUDE_MOVER
|
||||
---| 2 INCLUDE_VEHICLE
|
||||
---| 4 INCLUDE_PED
|
||||
---| 8 INCLUDE_RAGDOLL
|
||||
---| 16 INCLUDE_OBJECT
|
||||
---| 32 INCLUDE_PICKUP
|
||||
---| 64 INCLUDE_GLASS
|
||||
---| 128 INCLUDE_RIVER
|
||||
---| 256 INCLUDE_FOLIAGE
|
||||
---| 511 INCLUDE_ALL
|
||||
|
||||
---@param coords vector3
|
||||
---@param destination vector3
|
||||
---@param flags ShapetestFlags? Defaults to 511.
|
||||
---@param ignore ShapetestIgnore? Defaults to 4.
|
||||
---@return boolean hit
|
||||
---@return number entityHit
|
||||
---@return vector3 endCoords
|
||||
---@return vector3 surfaceNormal
|
||||
---@return number materialHash
|
||||
function lib.raycast.fromCoords(coords, destination, flags, ignore)
|
||||
local handle = StartShapeTestLosProbe(coords.x, coords.y, coords.z, destination.x, destination.y,
|
||||
destination.z, flags or 511, cache.ped, ignore or 4)
|
||||
|
||||
while true do
|
||||
Wait(0)
|
||||
local retval, hit, endCoords, surfaceNormal, material, entityHit = GetShapeTestResultIncludingMaterial(handle)
|
||||
|
||||
if retval ~= 1 then
|
||||
return hit, entityHit, endCoords, surfaceNormal, material
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function getForwardVector()
|
||||
local sin, cos = glm_sincos(glm_rad(GetFinalRenderedCamRot(2)))
|
||||
return vec3(-sin.z * math_abs(cos.x), cos.z * math_abs(cos.x), sin.x)
|
||||
end
|
||||
|
||||
---@param flags ShapetestFlags? Defaults to 511.
|
||||
---@param ignore ShapetestIgnore? Defaults to 4.
|
||||
---@param distance number? Defaults to 10.
|
||||
function lib.raycast.fromCamera(flags, ignore, distance)
|
||||
local coords = GetFinalRenderedCamCoord()
|
||||
local destination = coords + getForwardVector() * (distance or 10)
|
||||
|
||||
return lib.raycast.fromCoords(GetFinalRenderedCamCoord(), destination, flags, ignore)
|
||||
end
|
||||
|
||||
---@deprecated
|
||||
lib.raycast.cam = lib.raycast.fromCamera
|
||||
|
||||
return lib.raycast
|
||||
@@ -1,27 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load an animation dictionary. When called from a thread, it will yield until it has loaded.
|
||||
---@param animDict string
|
||||
---@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 10000.
|
||||
---@return string animDict
|
||||
function lib.requestAnimDict(animDict, timeout)
|
||||
if HasAnimDictLoaded(animDict) then return animDict end
|
||||
|
||||
if type(animDict) ~= 'string' then
|
||||
error(("expected animDict to have type 'string' (received %s)"):format(type(animDict)))
|
||||
end
|
||||
|
||||
if not DoesAnimDictExist(animDict) then
|
||||
error(("attempted to load invalid animDict '%s'"):format(animDict))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestAnimDict, HasAnimDictLoaded, 'animDict', animDict, timeout)
|
||||
end
|
||||
|
||||
return lib.requestAnimDict
|
||||
@@ -1,23 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load an animation clipset. When called from a thread, it will yield until it has loaded.
|
||||
---@param animSet string
|
||||
---@param timeout number? Approximate milliseconds to wait for the clipset to load. Default is 10000.
|
||||
---@return string animSet
|
||||
function lib.requestAnimSet(animSet, timeout)
|
||||
if HasAnimSetLoaded(animSet) then return animSet end
|
||||
|
||||
if type(animSet) ~= 'string' then
|
||||
error(("expected animSet to have type 'string' (received %s)"):format(type(animSet)))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestAnimSet, HasAnimSetLoaded, 'animSet', animSet, timeout)
|
||||
end
|
||||
|
||||
return lib.requestAnimSet
|
||||
@@ -1,19 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Loads an audio bank.
|
||||
---@param audioBank string
|
||||
---@param timeout number?
|
||||
---@return string
|
||||
function lib.requestAudioBank(audioBank, timeout)
|
||||
return lib.waitFor(function()
|
||||
if RequestScriptAudioBank(audioBank, false) then return audioBank end
|
||||
end, ("failed to load audiobank '%s' - this may be caused by\n- too many loaded assets\n- oversized, invalid, or corrupted assets"):format(audioBank), timeout or 30000)
|
||||
end
|
||||
|
||||
return lib.requestAudioBank
|
||||
@@ -1,24 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load a model. When called from a thread, it will yield until it has loaded.
|
||||
---@param model number | string
|
||||
---@param timeout number? Approximate milliseconds to wait for the model to load. Default is 10000.
|
||||
---@return number model
|
||||
function lib.requestModel(model, timeout)
|
||||
if type(model) ~= 'number' then model = joaat(model) end
|
||||
if HasModelLoaded(model) then return model end
|
||||
|
||||
if not IsModelValid(model) and not IsModelInCdimage(model) then
|
||||
error(("attempted to load invalid model '%s'"):format(model))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestModel, HasModelLoaded, 'model', model, timeout)
|
||||
end
|
||||
|
||||
return lib.requestModel
|
||||
@@ -1,23 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load a named particle effect. When called from a thread, it will yield until it has loaded.
|
||||
---@param ptFxName string
|
||||
---@param timeout number? Approximate milliseconds to wait for the particle effect to load. Default is 10000.
|
||||
---@return string ptFxName
|
||||
function lib.requestNamedPtfxAsset(ptFxName, timeout)
|
||||
if HasNamedPtfxAssetLoaded(ptFxName) then return ptFxName end
|
||||
|
||||
if type(ptFxName) ~= 'string' then
|
||||
error(("expected ptFxName to have type 'string' (received %s)"):format(type(ptFxName)))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestNamedPtfxAsset, HasNamedPtfxAssetLoaded, 'ptFxName', ptFxName, timeout)
|
||||
end
|
||||
|
||||
return lib.requestNamedPtfxAsset
|
||||
@@ -1,25 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load a scaleform movie. When called from a thread, it will yield until it has loaded.
|
||||
---@param scaleformName string
|
||||
---@param timeout number? Approximate milliseconds to wait for the scaleform movie to load. Default is 1000.
|
||||
---@return number? scaleform
|
||||
function lib.requestScaleformMovie(scaleformName, timeout)
|
||||
if type(scaleformName) ~= 'string' then
|
||||
error(("expected scaleformName to have type 'string' (received %s)"):format(type(scaleformName)))
|
||||
end
|
||||
|
||||
local scaleform = RequestScaleformMovie(scaleformName)
|
||||
|
||||
return lib.waitFor(function()
|
||||
if HasScaleformMovieLoaded(scaleform) then return scaleform end
|
||||
end, ("failed to load scaleformMovie '%s'"):format(scaleformName), timeout)
|
||||
end
|
||||
|
||||
return lib.requestScaleformMovie
|
||||
@@ -1,23 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Load a texture dictionary. When called from a thread, it will yield until it has loaded.
|
||||
---@param textureDict string
|
||||
---@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 10000.
|
||||
---@return string textureDict
|
||||
function lib.requestStreamedTextureDict(textureDict, timeout)
|
||||
if HasStreamedTextureDictLoaded(textureDict) then return textureDict end
|
||||
|
||||
if type(textureDict) ~= 'string' then
|
||||
error(("expected textureDict to have type 'string' (received %s)"):format(type(textureDict)))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestStreamedTextureDict, HasStreamedTextureDictLoaded, 'textureDict', textureDict, timeout)
|
||||
end
|
||||
|
||||
return lib.requestStreamedTextureDict
|
||||
@@ -1,52 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---@alias WeaponResourceFlags
|
||||
---| 1 WRF_REQUEST_BASE_ANIMS
|
||||
---| 2 WRF_REQUEST_COVER_ANIMS
|
||||
---| 4 WRF_REQUEST_MELEE_ANIMS
|
||||
---| 8 WRF_REQUEST_MOTION_ANIMS
|
||||
---| 16 WRF_REQUEST_STEALTH_ANIMS
|
||||
---| 32 WRF_REQUEST_ALL_MOVEMENT_VARIATION_ANIMS
|
||||
---| 31 WRF_REQUEST_ALL_ANIMS
|
||||
|
||||
---@alias ExtraWeaponComponentFlags
|
||||
---| 0 WEAPON_COMPONENT_NONE
|
||||
---| 1 WEAPON_COMPONENT_FLASH
|
||||
---| 2 WEAPON_COMPONENT_SCOPE
|
||||
---| 4 WEAPON_COMPONENT_SUPP
|
||||
---| 8 WEAPON_COMPONENT_SCLIP2
|
||||
---| 16 WEAPON_COMPONENT_GRIP
|
||||
|
||||
---Load a weapon asset. When called from a thread, it will yield until it has loaded.
|
||||
---@param weaponType string | number
|
||||
---@param timeout number? Approximate milliseconds to wait for the asset to load. Default is 10000.
|
||||
---@param weaponResourceFlags WeaponResourceFlags? Default is 31.
|
||||
---@param extraWeaponComponentFlags ExtraWeaponComponentFlags? Default is 0.
|
||||
---@return string | number weaponType
|
||||
function lib.requestWeaponAsset(weaponType, timeout, weaponResourceFlags, extraWeaponComponentFlags)
|
||||
if HasWeaponAssetLoaded(weaponType) then return weaponType end
|
||||
|
||||
local weaponTypeType = type(weaponType) --kekw
|
||||
|
||||
if weaponTypeType ~= 'string' and weaponTypeType ~= 'number' then
|
||||
error(("expected weaponType to have type 'string' or 'number' (received %s)"):format(weaponTypeType))
|
||||
end
|
||||
|
||||
if weaponResourceFlags and type(weaponResourceFlags) ~= 'number' then
|
||||
error(("expected weaponResourceFlags to have type 'number' (received %s)"):format(type(weaponResourceFlags)))
|
||||
end
|
||||
|
||||
if extraWeaponComponentFlags and type(extraWeaponComponentFlags) ~= 'number' then
|
||||
error(("expected extraWeaponComponentFlags to have type 'number' (received %s)"):format(type(extraWeaponComponentFlags)))
|
||||
end
|
||||
|
||||
return lib.streamingRequest(RequestWeaponAsset, HasWeaponAssetLoaded, 'weaponHash', weaponType, timeout, weaponResourceFlags or 31, extraWeaponComponentFlags or 0)
|
||||
end
|
||||
|
||||
return lib.requestWeaponAsset
|
||||
@@ -1,190 +0,0 @@
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local loaded = {}
|
||||
local _require = require
|
||||
|
||||
package = {
|
||||
path = './?.lua;./?/init.lua',
|
||||
preload = {},
|
||||
loaded = setmetatable({}, {
|
||||
__index = loaded,
|
||||
__newindex = noop,
|
||||
__metatable = false,
|
||||
})
|
||||
}
|
||||
|
||||
---@param modName string
|
||||
---@return string
|
||||
---@return string
|
||||
local function getModuleInfo(modName)
|
||||
local resource = modName:match('^@(.-)/.+') --[[@as string?]]
|
||||
|
||||
if resource then
|
||||
return resource, modName:sub(#resource + 3)
|
||||
end
|
||||
|
||||
local idx = 4 -- call stack depth (kept slightly lower than expected depth "just in case")
|
||||
|
||||
while true do
|
||||
local src = debug.getinfo(idx, 'S')?.source
|
||||
|
||||
if not src then
|
||||
return cache.resource, modName
|
||||
end
|
||||
|
||||
resource = src:match('^@@([^/]+)/.+')
|
||||
|
||||
if resource and not src:find('^@@ox_lib/imports/require') then
|
||||
return resource, modName
|
||||
end
|
||||
|
||||
idx += 1
|
||||
end
|
||||
end
|
||||
|
||||
local tempData = {}
|
||||
|
||||
---@param name string
|
||||
---@param path string
|
||||
---@return string? filename
|
||||
---@return string? errmsg
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function package.searchpath(name, path)
|
||||
local resource, modName = getModuleInfo(name:gsub('%.', '/'))
|
||||
local tried = {}
|
||||
|
||||
for template in path:gmatch('[^;]+') do
|
||||
local fileName = template:gsub('^%./', ''):gsub('?', modName:gsub('%.', '/') or modName)
|
||||
local file = LoadResourceFile(resource, fileName)
|
||||
|
||||
if file then
|
||||
tempData[1] = file
|
||||
tempData[2] = resource
|
||||
return fileName
|
||||
end
|
||||
|
||||
tried[#tried + 1] = ("no file '@%s/%s'"):format(resource, fileName)
|
||||
end
|
||||
|
||||
return nil, table.concat(tried, "\n\t")
|
||||
end
|
||||
|
||||
---Attempts to load a module at the given path relative to the resource root directory.\
|
||||
---Returns a function to load the module chunk, or a string containing all tested paths.
|
||||
---@param modName string
|
||||
---@param env? table
|
||||
local function loadModule(modName, env)
|
||||
local fileName, err = package.searchpath(modName, package.path)
|
||||
|
||||
if fileName then
|
||||
local file = tempData[1]
|
||||
local resource = tempData[2]
|
||||
|
||||
table.wipe(tempData)
|
||||
return assert(load(file, ('@@%s/%s'):format(resource, fileName), 't', env or _ENV))
|
||||
end
|
||||
|
||||
return nil, err or 'unknown error'
|
||||
end
|
||||
|
||||
---@alias PackageSearcher
|
||||
---| fun(modName: string): function loader
|
||||
---| fun(modName: string): nil, string errmsg
|
||||
|
||||
---@type PackageSearcher[]
|
||||
package.searchers = {
|
||||
function(modName)
|
||||
local ok, result = pcall(_require, modName)
|
||||
|
||||
if ok then return result end
|
||||
|
||||
return ok, result
|
||||
end,
|
||||
function(modName)
|
||||
if package.preload[modName] ~= nil then
|
||||
return package.preload[modName]
|
||||
end
|
||||
|
||||
return nil, ("no field package.preload['%s']"):format(modName)
|
||||
end,
|
||||
function(modName) return loadModule(modName) end,
|
||||
}
|
||||
|
||||
---@param filePath string
|
||||
---@param env? table
|
||||
---@return unknown
|
||||
---Loads and runs a Lua file at the given path. Unlike require, the chunk is not cached for future use.
|
||||
function lib.load(filePath, env)
|
||||
if type(filePath) ~= 'string' then
|
||||
error(("file path must be a string (received '%s')"):format(filePath), 2)
|
||||
end
|
||||
|
||||
local result, err = loadModule(filePath, env)
|
||||
|
||||
if result then return result() end
|
||||
|
||||
error(("file '%s' not found\n\t%s"):format(filePath, err))
|
||||
end
|
||||
|
||||
---@param filePath string
|
||||
---@return table
|
||||
---Loads and decodes a json file at the given path.
|
||||
function lib.loadJson(filePath)
|
||||
if type(filePath) ~= 'string' then
|
||||
error(("file path must be a string (received '%s')"):format(filePath), 2)
|
||||
end
|
||||
|
||||
local resourceSrc, modPath = getModuleInfo(filePath:gsub('%.', '/'))
|
||||
local resourceFile = LoadResourceFile(resourceSrc, ('%s.json'):format(modPath))
|
||||
|
||||
if resourceFile then
|
||||
return json.decode(resourceFile)
|
||||
end
|
||||
|
||||
error(("json file '%s' not found\n\tno file '@%s/%s.json'"):format(filePath, resourceSrc, modPath))
|
||||
end
|
||||
|
||||
---Loads the given module, returns any value returned by the seacher (`true` when `nil`).\
|
||||
---Passing `@resourceName.modName` loads a module from a remote resource.
|
||||
---@param modName string
|
||||
---@return unknown
|
||||
function lib.require(modName)
|
||||
if type(modName) ~= 'string' then
|
||||
error(("module name must be a string (received '%s')"):format(modName), 3)
|
||||
end
|
||||
|
||||
local module = loaded[modName]
|
||||
|
||||
if module == '__loading' then
|
||||
error(("^1circular-dependency occurred when loading module '%s'^0"):format(modName), 2)
|
||||
end
|
||||
|
||||
if module ~= nil then return module end
|
||||
|
||||
loaded[modName] = '__loading'
|
||||
|
||||
local err = {}
|
||||
|
||||
for i = 1, #package.searchers do
|
||||
local result, errMsg = package.searchers[i](modName)
|
||||
|
||||
if result then
|
||||
if type(result) == 'function' then result = result() end
|
||||
loaded[modName] = result or result == nil
|
||||
|
||||
return loaded[modName]
|
||||
end
|
||||
|
||||
err[#err + 1] = errMsg
|
||||
end
|
||||
|
||||
error(("%s"):format(table.concat(err, "\n\t")))
|
||||
end
|
||||
|
||||
return lib.require
|
||||
@@ -1,232 +0,0 @@
|
||||
--[[
|
||||
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 renderTargetTable
|
||||
---@field name string
|
||||
---@field model string | number
|
||||
|
||||
---@class detailsTable
|
||||
---@field name string
|
||||
---@field fullScreen? boolean
|
||||
---@field x? number
|
||||
---@field y? number
|
||||
---@field width? number
|
||||
---@field height? number
|
||||
---@field renderTarget? renderTargetTable
|
||||
|
||||
---@class Scaleform : OxClass
|
||||
---@field scaleform number
|
||||
---@field draw boolean
|
||||
---@field target number
|
||||
---@field targetName string
|
||||
---@field sfHandle? number
|
||||
---@field fullScreen boolean
|
||||
---@field private private { isDrawing: boolean }
|
||||
lib.scaleform = lib.class('Scaleform')
|
||||
|
||||
--- Converts the arguments into data types usable by scaleform
|
||||
---@param argsTable (number | string | boolean)[]
|
||||
local function convertArgs(argsTable)
|
||||
for i = 1, #argsTable do
|
||||
local arg = argsTable[i]
|
||||
local argType = type(arg)
|
||||
|
||||
if argType == 'string' then
|
||||
ScaleformMovieMethodAddParamPlayerNameString(arg)
|
||||
elseif argType == 'number' then
|
||||
if math.type(arg) == 'integer' then
|
||||
ScaleformMovieMethodAddParamInt(arg)
|
||||
else
|
||||
ScaleformMovieMethodAddParamFloat(arg)
|
||||
end
|
||||
elseif argType == 'boolean' then
|
||||
ScaleformMovieMethodAddParamBool(arg)
|
||||
else
|
||||
error(('Unsupported Parameter type [%s]'):format(argType))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param expectedType 'boolean' | 'integer' | 'string'
|
||||
---@return boolean | integer | string
|
||||
local function retrieveReturnValue(expectedType)
|
||||
local result = EndScaleformMovieMethodReturnValue()
|
||||
|
||||
lib.waitFor(function()
|
||||
if IsScaleformMovieMethodReturnValueReady(result) then
|
||||
return true
|
||||
end
|
||||
end, "Failed to retrieve return value", 1000)
|
||||
|
||||
if expectedType == "integer" then
|
||||
return GetScaleformMovieMethodReturnValueInt(result)
|
||||
elseif expectedType == "boolean" then
|
||||
return GetScaleformMovieMethodReturnValueBool(result)
|
||||
else
|
||||
return GetScaleformMovieMethodReturnValueString(result)
|
||||
end
|
||||
end
|
||||
|
||||
---@param details detailsTable | string
|
||||
---@return nil
|
||||
function lib.scaleform:constructor(details)
|
||||
details = type(details) == 'table' and details or { name = details }
|
||||
|
||||
local scaleform = lib.requestScaleformMovie(details.name)
|
||||
|
||||
self.sfHandle = scaleform
|
||||
self.private.isDrawing = false
|
||||
|
||||
self.fullScreen = details.fullScreen or false
|
||||
self.x = details.x or 0
|
||||
self.y = details.y or 0
|
||||
self.width = details.width or 0
|
||||
self.height = details.height or 0
|
||||
|
||||
if details.renderTarget then
|
||||
self:setRenderTarget(details.renderTarget.name, details.renderTarget.model)
|
||||
end
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param args? (number | string | boolean)[]
|
||||
---@param returnValue? string
|
||||
---@return any
|
||||
function lib.scaleform:callMethod(name, args, returnValue)
|
||||
if not self.sfHandle then
|
||||
return error("attempted to call method with invalid scaleform handle")
|
||||
end
|
||||
|
||||
BeginScaleformMovieMethod(self.sfHandle, name)
|
||||
|
||||
if args and type(args) == 'table' then
|
||||
convertArgs(args)
|
||||
end
|
||||
|
||||
if returnValue then
|
||||
return retrieveReturnValue(returnValue)
|
||||
end
|
||||
|
||||
EndScaleformMovieMethod()
|
||||
end
|
||||
|
||||
---@param isFullscreen boolean
|
||||
---@return nil
|
||||
function lib.scaleform:setFullScreen(isFullscreen)
|
||||
self.fullScreen = isFullscreen
|
||||
end
|
||||
|
||||
---@param x number
|
||||
---@param y number
|
||||
---@param width number
|
||||
---@param height number
|
||||
---@return nil
|
||||
function lib.scaleform:setProperties(x, y, width, height)
|
||||
if self.fullScreen then
|
||||
lib.print.info('Cannot set properties when full screen is enabled')
|
||||
return
|
||||
end
|
||||
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.width = width
|
||||
self.height = height
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@param model string|number
|
||||
---@return nil
|
||||
function lib.scaleform:setRenderTarget(name, model)
|
||||
if self.target then
|
||||
ReleaseNamedRendertarget(self.targetName)
|
||||
end
|
||||
|
||||
if type(model) == 'string' then
|
||||
model = joaat(model)
|
||||
end
|
||||
|
||||
if not IsNamedRendertargetRegistered(name) then
|
||||
RegisterNamedRendertarget(name, false)
|
||||
|
||||
if not IsNamedRendertargetLinked(model) then
|
||||
LinkNamedRendertarget(model)
|
||||
end
|
||||
|
||||
self.target = GetNamedRendertargetRenderId(name)
|
||||
self.targetName = name
|
||||
end
|
||||
end
|
||||
|
||||
function lib.scaleform:isDrawing()
|
||||
return self.private.isDrawing
|
||||
end
|
||||
|
||||
function lib.scaleform:draw()
|
||||
if self.target then
|
||||
SetTextRenderId(self.target)
|
||||
SetScriptGfxDrawOrder(4)
|
||||
SetScriptGfxDrawBehindPausemenu(true)
|
||||
SetScaleformFitRendertarget(self.sfHandle, true)
|
||||
end
|
||||
|
||||
if self.fullScreen then
|
||||
DrawScaleformMovieFullscreen(self.sfHandle, 255, 255, 255, 255, 0)
|
||||
else
|
||||
if not self.x or not self.y or not self.width or not self.height then
|
||||
error('attempted to draw scaleform without setting properties')
|
||||
else
|
||||
DrawScaleformMovie(self.sfHandle, self.x, self.y, self.width, self.height, 255, 255, 255, 255, 0)
|
||||
end
|
||||
end
|
||||
|
||||
if self.target then
|
||||
SetTextRenderId(1)
|
||||
end
|
||||
end
|
||||
|
||||
function lib.scaleform:startDrawing()
|
||||
if self.private.isDrawing then
|
||||
return
|
||||
end
|
||||
|
||||
self.private.isDrawing = true
|
||||
|
||||
CreateThread(function()
|
||||
while self:isDrawing() do
|
||||
self:draw()
|
||||
Wait(0)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function lib.scaleform:stopDrawing()
|
||||
if not self.private.isDrawing then
|
||||
return
|
||||
end
|
||||
|
||||
self.private.isDrawing = false
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function lib.scaleform:dispose()
|
||||
if self.sfHandle then
|
||||
SetScaleformMovieAsNoLongerNeeded(self.sfHandle)
|
||||
end
|
||||
|
||||
if self.target then
|
||||
ReleaseNamedRendertarget(self.targetName)
|
||||
end
|
||||
|
||||
self.sfHandle = nil
|
||||
self.target = nil
|
||||
self.private.isDrawing = false
|
||||
end
|
||||
|
||||
---@return Scaleform
|
||||
return lib.scaleform
|
||||
@@ -1,175 +0,0 @@
|
||||
---@alias OxSelectorItem {[1]: number, [2]: any}
|
||||
---@alias OxSelectorSet OxSelectorItem[]
|
||||
|
||||
---@class OxSelector: OxClass
|
||||
---@field private sets OxSelectorSet | table<string, OxSelectorItem[]>
|
||||
---@field private totalWeights table<string, number>
|
||||
local OxSelector = lib.class("OxSelector")
|
||||
|
||||
local DEFAULT_SET = 'default'
|
||||
local deepClone = lib.table.deepclone
|
||||
|
||||
|
||||
local function calculateTotalWeight(set)
|
||||
local total = 0
|
||||
for i = 1, #set do
|
||||
local item = set[i]
|
||||
assert(type(item) == "table", "Each OxSelectorItem must be a table")
|
||||
local weight = item[1]
|
||||
assert(type(weight) == "number" and weight >= 0, "weight must be 0 or more")
|
||||
total += weight
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
|
||||
---@param sets OxSelectorSet | table<string, OxSelectorItem[]>
|
||||
function OxSelector:constructor(sets)
|
||||
if type(sets) ~= "table" then
|
||||
lib.print.error("Invalid sets provided to OxSelector constructor")
|
||||
end
|
||||
|
||||
if lib.table.type(sets) == "array" then
|
||||
sets = { [DEFAULT_SET] = sets }
|
||||
end
|
||||
|
||||
self.private.totalWeights = {}
|
||||
self.private.sets = {}
|
||||
|
||||
for setName, set in pairs(sets) do
|
||||
assert(type(set) == "table" and lib.table.type(set) == "array", "Each set must be an array of OxSelectorItem")
|
||||
assert(#set > 0, "Each set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(set)
|
||||
end
|
||||
|
||||
self.private.sets = sets
|
||||
end
|
||||
|
||||
--- Get a random non-weighted item from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem?
|
||||
function OxSelector:getRandom(setName)
|
||||
local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET]
|
||||
if not set then return nil end
|
||||
local item = set[math.random(#set)][2]
|
||||
|
||||
return type(item) == "table" and deepClone(item) or item
|
||||
end
|
||||
|
||||
--- Get a random weighted item from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem?
|
||||
function OxSelector:getRandomWeighted(setName)
|
||||
local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET]
|
||||
if not set then return nil end
|
||||
|
||||
local totalWeight = self.private.totalWeights[setName or DEFAULT_SET]
|
||||
if totalWeight == 0 then return nil end
|
||||
|
||||
local randomWeight = math.random() * totalWeight
|
||||
local cumulativeWeight = 0
|
||||
|
||||
for i = 1, #set do
|
||||
cumulativeWeight = cumulativeWeight + set[i][1]
|
||||
if randomWeight <= cumulativeWeight then
|
||||
local item = set[i][2]
|
||||
return type(item) == "table" and deepClone(item) or item
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- get multiple non-weighted random items from a specific set
|
||||
---@param setName? string
|
||||
---@param count number
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getRandomAmount(setName, count)
|
||||
assert(type(count) == "number" and count > 0, "Count must be a positive number")
|
||||
local items = {}
|
||||
for _ = 1, count do
|
||||
local item = self:getRandom(setName)
|
||||
if item then
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
--- get multiple weighted random items from a specific set
|
||||
---@param setName? string
|
||||
---@param count number
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getRandomWeightedAmount(setName, count)
|
||||
assert(type(count) == "number" and count > 0, "Count must be a positive number")
|
||||
local items = {}
|
||||
for _ = 1, count do
|
||||
local item = self:getRandomWeighted(setName)
|
||||
if item then
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
--- get all items from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getSet(setName)
|
||||
return deepClone((setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET])
|
||||
end
|
||||
|
||||
--- get all sets
|
||||
---@return table<string, OxSelectorItem[]>
|
||||
function OxSelector:getAllSets()
|
||||
return deepClone(self.private.sets)
|
||||
end
|
||||
|
||||
--- add a new set
|
||||
---@param setName string
|
||||
---@param items OxSelectorItem[]
|
||||
function OxSelector:addSet(setName, items)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
if self.private.sets[setName] then
|
||||
lib.print.error("Selector set '" .. setName .. "' already exists.")
|
||||
return
|
||||
end
|
||||
|
||||
assert(type(items) == "table" and lib.table.type(items) == "array", "items must be an array")
|
||||
assert(#items > 0, "set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(items)
|
||||
self.private.sets[setName] = items
|
||||
end
|
||||
|
||||
--- update an existing set
|
||||
---@param setName string
|
||||
---@param newItems OxSelectorItem[]
|
||||
function OxSelector:updateSet(setName, newItems)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
if not self.private.sets[setName] then
|
||||
lib.print.error("Selector set '" .. setName .. "' does not exist.")
|
||||
return
|
||||
end
|
||||
|
||||
assert(type(newItems) == "table" and lib.table.type(newItems) == "array", "newItems must be an array")
|
||||
assert(#newItems > 0, "set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(newItems)
|
||||
self.private.sets[setName] = newItems
|
||||
end
|
||||
|
||||
--- remove a set
|
||||
---@param setName string
|
||||
function OxSelector:removeSet(setName)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
self.private.totalWeights[setName] = nil
|
||||
self.private.sets[setName] = nil
|
||||
end
|
||||
|
||||
lib.selector = OxSelector
|
||||
return lib.selector
|
||||
@@ -1,29 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---@async
|
||||
---@generic T : string | number
|
||||
---@param request function
|
||||
---@param hasLoaded function
|
||||
---@param assetType string
|
||||
---@param asset T
|
||||
---@param timeout? number
|
||||
---@param ... any
|
||||
---Used internally.
|
||||
function lib.streamingRequest(request, hasLoaded, assetType, asset, timeout, ...)
|
||||
if hasLoaded(asset) then return asset end
|
||||
|
||||
request(asset, ...)
|
||||
|
||||
return lib.waitFor(function()
|
||||
if hasLoaded(asset) then return asset end
|
||||
end, ("failed to load %s '%s' - this may be caused by\n- too many loaded assets\n- oversized, invalid, or corrupted assets"):format(assetType, asset),
|
||||
timeout or 30000)
|
||||
end
|
||||
|
||||
return lib.streamingRequest
|
||||
@@ -1,66 +0,0 @@
|
||||
--[[
|
||||
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 oxstring : stringlib
|
||||
lib.string = string
|
||||
|
||||
local string_char = string.char
|
||||
local math_random = math.random
|
||||
|
||||
local function getLetter() return string_char(math_random(65, 90)) end
|
||||
local function getLowerLetter() return string_char(math_random(97, 122)) end
|
||||
local function getInt() return math_random(0, 9) end
|
||||
local function getAlphanumeric() return math_random(0, 1) == 1 and getLetter() or getInt() end
|
||||
|
||||
local formatChar = {
|
||||
['1'] = getInt,
|
||||
['A'] = getLetter,
|
||||
['a'] = getLowerLetter,
|
||||
['.'] = getAlphanumeric,
|
||||
}
|
||||
|
||||
---Creates a random string based on a given pattern.
|
||||
---`1` will output a random number from 0-9.
|
||||
---`A` will output a random letter from A-Z.
|
||||
---`a` will output a random letter from a-z.
|
||||
---`.` will output a random letter or number.
|
||||
---`^` will output the following character literally.
|
||||
---Any other character will output said character.
|
||||
---@param pattern string
|
||||
---@param length? integer Sets the length of the returned string, either padding it or omitting characters.
|
||||
---@return string
|
||||
function string.random(pattern, length)
|
||||
local len = length or #pattern:gsub('%^', '')
|
||||
local arr = table.create(len, 0)
|
||||
local size = 0
|
||||
local i = 0
|
||||
|
||||
while size < len do
|
||||
i += 1
|
||||
---@type string | integer
|
||||
local char = pattern:sub(i, i)
|
||||
|
||||
if char == '' then
|
||||
arr[size + 1] = string.rep(' ', len - size)
|
||||
break
|
||||
elseif char == '^' then
|
||||
i += 1
|
||||
char = pattern:sub(i, i)
|
||||
else
|
||||
local fn = formatChar[char]
|
||||
char = fn and fn() or char
|
||||
end
|
||||
|
||||
size += 1
|
||||
arr[size] = char
|
||||
end
|
||||
|
||||
return table.concat(arr)
|
||||
end
|
||||
|
||||
return lib.string
|
||||
@@ -1,185 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
-- Add additional functions to the standard table library
|
||||
|
||||
---@class oxtable : tablelib
|
||||
lib.table = table
|
||||
local pairs = pairs
|
||||
|
||||
---@param tbl table
|
||||
---@param value any
|
||||
---@return boolean
|
||||
---Checks if tbl contains the given values. Only intended for simple values and unnested tables.
|
||||
local function contains(tbl, value)
|
||||
if type(value) ~= 'table' then
|
||||
for _, v in pairs(tbl) do
|
||||
if v == value then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
else
|
||||
local set = {}
|
||||
|
||||
for _, v in pairs(tbl) do
|
||||
set[v] = true
|
||||
end
|
||||
|
||||
for _, v in pairs(value) do
|
||||
if not set[v] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
---@param t1 any
|
||||
---@param t2 any
|
||||
---@return boolean
|
||||
---Compares if two values are equal, iterating over tables and matching both keys and values.
|
||||
local function table_matches(t1, t2)
|
||||
local type1, type2 = type(t1), type(t2)
|
||||
|
||||
if type1 ~= type2 then return false end
|
||||
if type1 ~= 'table' and type2 ~= 'table' then return t1 == t2 end
|
||||
|
||||
for k, v1 in pairs(t1) do
|
||||
local v2 = t2[k]
|
||||
if v2 == nil or not table_matches(v1, v2) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for k in pairs(t2) do
|
||||
if t1[k] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
---@generic T
|
||||
---@param tbl T
|
||||
---@return T
|
||||
---Recursively clones a table to ensure no table references.
|
||||
local function table_deepclone(tbl)
|
||||
tbl = table.clone(tbl)
|
||||
|
||||
for k, v in pairs(tbl) do
|
||||
if type(v) == 'table' then
|
||||
tbl[k] = table_deepclone(v)
|
||||
end
|
||||
end
|
||||
|
||||
return tbl
|
||||
end
|
||||
|
||||
---@param t1 table
|
||||
---@param t2 table
|
||||
---@param addDuplicateNumbers boolean? add duplicate number keys together if true, replace if false. Defaults to true.
|
||||
---@return table
|
||||
---Merges two tables together. Defaults to adding duplicate keys together if they are numbers, otherwise they are overriden.
|
||||
local function table_merge(t1, t2, addDuplicateNumbers)
|
||||
addDuplicateNumbers = addDuplicateNumbers == nil or addDuplicateNumbers
|
||||
for k, v2 in pairs(t2) do
|
||||
local v1 = t1[k]
|
||||
local type1 = type(v1)
|
||||
local type2 = type(v2)
|
||||
|
||||
if type1 == 'table' and type2 == 'table' then
|
||||
table_merge(v1, v2, addDuplicateNumbers)
|
||||
elseif addDuplicateNumbers and (type1 == 'number' and type2 == 'number') then
|
||||
t1[k] = v1 + v2
|
||||
else
|
||||
t1[k] = v2
|
||||
end
|
||||
end
|
||||
|
||||
return t1
|
||||
end
|
||||
|
||||
---@param tbl table
|
||||
---@return table
|
||||
---Shuffles the elements of a table randomly using the Fisher-Yates algorithm.
|
||||
local function shuffle(tbl)
|
||||
local len = #tbl
|
||||
for i = len, 2, -1 do
|
||||
local j = math.random(i)
|
||||
tbl[i], tbl[j] = tbl[j], tbl[i]
|
||||
end
|
||||
return tbl
|
||||
end
|
||||
|
||||
|
||||
---@param tbl table
|
||||
---@param fn function(value: any, key: any): any
|
||||
---@return table
|
||||
local function map(tbl, fn)
|
||||
local result = {}
|
||||
for k, v in pairs(tbl) do
|
||||
result[k] = fn(v, k)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
table.contains = contains
|
||||
table.matches = table_matches
|
||||
table.deepclone = table_deepclone
|
||||
table.merge = table_merge
|
||||
table.shuffle = shuffle
|
||||
table.map = map
|
||||
|
||||
local frozenNewIndex = function(self) error(('cannot set values on a frozen table (%s)'):format(self), 2) end
|
||||
local _rawset = rawset
|
||||
|
||||
---@param tbl table
|
||||
---@param index any
|
||||
---@param value any
|
||||
---@return table
|
||||
function rawset(tbl, index, value)
|
||||
if table.isfrozen(tbl) then
|
||||
frozenNewIndex(tbl)
|
||||
end
|
||||
|
||||
return _rawset(tbl, index, value)
|
||||
end
|
||||
|
||||
---Makes a table read-only, preventing further modification. Unfrozen tables stored within `tbl` are still mutable.
|
||||
---@generic T : table
|
||||
---@param tbl T
|
||||
---@return T
|
||||
function table.freeze(tbl)
|
||||
local copy = table.clone(tbl)
|
||||
local metatbl = getmetatable(tbl)
|
||||
|
||||
table.wipe(tbl)
|
||||
setmetatable(tbl, {
|
||||
__index = metatbl and setmetatable(copy, metatbl) or copy,
|
||||
__metatable = 'readonly',
|
||||
__newindex = frozenNewIndex,
|
||||
__len = function() return #copy end,
|
||||
---@diagnostic disable-next-line: redundant-return-value
|
||||
__pairs = function() return next, copy end,
|
||||
})
|
||||
|
||||
return tbl
|
||||
end
|
||||
|
||||
---Return true if `tbl` is set as read-only.
|
||||
---@param tbl table
|
||||
---@return boolean
|
||||
function table.isfrozen(tbl)
|
||||
return getmetatable(tbl) == 'readonly'
|
||||
end
|
||||
|
||||
return lib.table
|
||||
@@ -1,151 +0,0 @@
|
||||
--[[
|
||||
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 TimerPrivateProps
|
||||
---@field initialTime number the initial duration of the timer.
|
||||
---@field async? boolean wether the timer should run asynchronously or not
|
||||
---@field startTime number the gametimer stamp of when the timer starts. changes when paused and played
|
||||
---@field triggerOnEnd boolean set in the forceEnd method using the optional param. wether or not the onEnd function is triggered when force ending the timer early
|
||||
---@field currentTimeLeft number current timer length
|
||||
---@field paused boolean the pause state of the timer
|
||||
|
||||
---@class OxTimer : OxClass
|
||||
---@field private private TimerPrivateProps
|
||||
---@field start fun(self: self, async?: boolean) starts the timer
|
||||
---@field onEnd? fun() cb function triggered when the timer finishes
|
||||
---@field forceEnd fun(self: self, triggerOnEnd: boolean) end timer early and optionally trigger the onEnd function still
|
||||
---@field isPaused fun(self: self): boolean returns wether the timer is paused or not
|
||||
---@field pause fun(self: self) pauses the timer until play method is called
|
||||
---@field play fun(self: self) resumes the timer if paused
|
||||
---@field getTimeLeft fun(self: self, format?: 'ms' | 's' | 'm' | 'h'): number | table returns the time left on the timer with the specified format rounded to 2 decimal places (miliseconds, seconds, minutes, hours). returns a table of all if not specified.
|
||||
local timer = lib.class('OxTimer')
|
||||
|
||||
---@private
|
||||
---@param time number
|
||||
---@param onEnd fun(self: OxTimer)
|
||||
---@param async? boolean
|
||||
function timer:constructor(time, onEnd, async)
|
||||
assert(type(time) == "number" and time > 0, "Time must be a positive number")
|
||||
assert(onEnd == nil or type(onEnd) == "function", "onEnd must be a function or nil")
|
||||
assert(type(async) == "boolean" or async == nil, "async must be a boolean or nil")
|
||||
|
||||
self.onEnd = onEnd
|
||||
self.private.initialTime = time
|
||||
self.private.currentTimeLeft = time
|
||||
self.private.startTime = 0
|
||||
self.private.paused = false
|
||||
self.private.triggerOnEnd = true
|
||||
|
||||
self:start(async)
|
||||
end
|
||||
|
||||
---@protected
|
||||
function timer:run()
|
||||
while self:isPaused() or self:getTimeLeft('ms') > 0 do
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
if self.private.triggerOnEnd then
|
||||
self:onEnd()
|
||||
end
|
||||
|
||||
self.private.triggerOnEnd = true
|
||||
end
|
||||
|
||||
function timer:start(async)
|
||||
if self.private.startTime > 0 then error('Cannot start a timer that is already running') end
|
||||
|
||||
self.private.startTime = GetGameTimer()
|
||||
|
||||
if not async then return self:run() end
|
||||
|
||||
Citizen.CreateThreadNow(function()
|
||||
self:run()
|
||||
end)
|
||||
end
|
||||
|
||||
function timer:forceEnd(triggerOnEnd)
|
||||
if self:getTimeLeft('ms') <= 0 then return end
|
||||
|
||||
self.private.paused = false
|
||||
self.private.currentTimeLeft = 0
|
||||
self.private.triggerOnEnd = triggerOnEnd
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
function timer:pause()
|
||||
if self.private.paused then return end
|
||||
|
||||
self.private.currentTimeLeft = self:getTimeLeft('ms') --[[@as number]]
|
||||
self.private.paused = true
|
||||
end
|
||||
|
||||
function timer:play()
|
||||
if not self.private.paused then return end
|
||||
self.private.startTime = GetGameTimer()
|
||||
self.private.paused = false
|
||||
end
|
||||
|
||||
function timer:isPaused()
|
||||
return self.private.paused
|
||||
end
|
||||
|
||||
function timer:restart(async)
|
||||
self:forceEnd(false)
|
||||
Wait(0)
|
||||
self.private.currentTimeLeft = self.private.initialTime
|
||||
self.private.startTime = 0
|
||||
self:start(async)
|
||||
end
|
||||
|
||||
function timer:getTimeLeft(format)
|
||||
local ms = self.private.currentTimeLeft - (GetGameTimer() - self.private.startTime)
|
||||
|
||||
local roundedfloat = function(value)
|
||||
return tonumber(string.format('%.2f', value))
|
||||
end
|
||||
|
||||
if format == 'ms' then
|
||||
return roundedfloat(ms)
|
||||
end
|
||||
|
||||
local s = ms / 1000
|
||||
|
||||
if format == 's' then
|
||||
return roundedfloat(s)
|
||||
end
|
||||
|
||||
local m = s / 60
|
||||
|
||||
if format == 'm' then
|
||||
return roundedfloat(m)
|
||||
end
|
||||
|
||||
local h = m / 60
|
||||
|
||||
if format == 'h' then
|
||||
return roundedfloat(h)
|
||||
end
|
||||
|
||||
return {
|
||||
ms = roundedfloat(ms),
|
||||
s = roundedfloat(s),
|
||||
m = roundedfloat(m),
|
||||
h = roundedfloat(h)
|
||||
}
|
||||
end
|
||||
|
||||
---@param time number
|
||||
---@param onEnd fun(self: OxTimer)
|
||||
---@param async? boolean
|
||||
function lib.timer(time, onEnd, async)
|
||||
return timer:new(time, onEnd, async)
|
||||
end
|
||||
|
||||
return lib.timer
|
||||
@@ -1,31 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Triggers an event for the given playerIds, sending additional parameters as arguments.\
|
||||
---Implements functionality from [this pending pull request](https://github.com/citizenfx/fivem/pull/1210) and may be deprecated.
|
||||
---
|
||||
---Provides non-neglibible performance gains due to msgpacking all arguments _once_, instead of per-target.
|
||||
---@param eventName string
|
||||
---@param targetIds number | ArrayLike<number>
|
||||
---@param ... any
|
||||
function lib.triggerClientEvent(eventName, targetIds, ...)
|
||||
local payload = msgpack.pack_args(...)
|
||||
local payloadLen = #payload
|
||||
|
||||
if lib.array.isArray(targetIds) then
|
||||
for i = 1, #targetIds do
|
||||
TriggerClientEventInternal(eventName, targetIds[i] --[[@as string]], payload, payloadLen)
|
||||
end
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
TriggerClientEventInternal(eventName, targetIds --[[@as string]], payload, payloadLen)
|
||||
end
|
||||
|
||||
return lib.triggerClientEvent
|
||||
@@ -1,42 +0,0 @@
|
||||
--[[
|
||||
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>
|
||||
]]
|
||||
|
||||
---Yields the current thread until a non-nil value is returned by the function.
|
||||
---@generic T
|
||||
---@param cb fun(): T?
|
||||
---@param errMessage string?
|
||||
---@param timeout? number | false Error out after `~x` ms. Defaults to 1000, unless set to `false`.
|
||||
---@return T
|
||||
---@async
|
||||
function lib.waitFor(cb, errMessage, timeout)
|
||||
local value = cb()
|
||||
|
||||
if value ~= nil then return value end
|
||||
|
||||
if timeout or timeout == nil then
|
||||
if type(timeout) ~= 'number' then timeout = 1000 end
|
||||
end
|
||||
|
||||
local start = timeout and GetGameTimer()
|
||||
|
||||
while value == nil do
|
||||
Wait(0)
|
||||
|
||||
local elapsed = timeout and GetGameTimer() - start
|
||||
|
||||
if elapsed and elapsed > timeout then
|
||||
return error(('%s (waited %.1fms)'):format(errMessage or 'failed to resolve callback', elapsed), 2)
|
||||
end
|
||||
|
||||
value = cb()
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
return lib.waitFor
|
||||
@@ -1,498 +0,0 @@
|
||||
--[[
|
||||
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 = require 'glm'
|
||||
|
||||
---@class ZoneProperties
|
||||
---@field debug? boolean
|
||||
---@field debugColour? vector4
|
||||
---@field onEnter fun(self: CZone)?
|
||||
---@field onExit fun(self: CZone)?
|
||||
---@field inside fun(self: CZone)?
|
||||
---@field [string] any
|
||||
|
||||
---@class CZone : PolyZone, BoxZone, SphereZone
|
||||
---@field id number
|
||||
---@field __type 'poly' | 'sphere' | 'box'
|
||||
---@field remove fun(self: self)
|
||||
---@field setDebug fun(self: CZone, enable?: boolean, colour?: vector)
|
||||
---@field contains fun(self: CZone, coords?: vector3, updateDistance?: boolean): boolean
|
||||
|
||||
---@type table<number, CZone>
|
||||
local Zones = {}
|
||||
_ENV.Zones = Zones
|
||||
|
||||
local function nextFreePoint(points, b, len)
|
||||
for i = 1, len do
|
||||
local n = (i + b) % len
|
||||
|
||||
n = n ~= 0 and n or len
|
||||
|
||||
if points[n] then
|
||||
return n
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function unableToSplit(polygon)
|
||||
print('The following polygon is malformed and has failed to be split into triangles for debug')
|
||||
|
||||
for k, v in pairs(polygon) do
|
||||
print(k, v)
|
||||
end
|
||||
end
|
||||
|
||||
local function getTriangles(polygon)
|
||||
local triangles = {}
|
||||
|
||||
if polygon:isConvex() then
|
||||
for i = 2, #polygon - 1 do
|
||||
triangles[#triangles + 1] = mat(polygon[1], polygon[i], polygon[i + 1])
|
||||
end
|
||||
|
||||
return triangles
|
||||
end
|
||||
|
||||
if not polygon:isSimple() then
|
||||
unableToSplit(polygon)
|
||||
|
||||
return triangles
|
||||
end
|
||||
|
||||
local points = {}
|
||||
local polygonN = #polygon
|
||||
|
||||
for i = 1, polygonN do
|
||||
points[i] = polygon[i]
|
||||
end
|
||||
|
||||
local a, b, c = 1, 2, 3
|
||||
local zValue = polygon[1].z
|
||||
local count = 0
|
||||
|
||||
while polygonN - #triangles > 2 do
|
||||
local a2d = polygon[a].xy
|
||||
local c2d = polygon[c].xy
|
||||
|
||||
if polygon:containsSegment(vec3(glm.segment2d.getPoint(a2d, c2d, 0.01), zValue), vec3(glm.segment2d.getPoint(a2d, c2d, 0.99), zValue)) then
|
||||
triangles[#triangles + 1] = mat(polygon[a], polygon[b], polygon[c])
|
||||
points[b] = false
|
||||
|
||||
b = c
|
||||
c = nextFreePoint(points, b, polygonN)
|
||||
else
|
||||
a = b
|
||||
b = c
|
||||
c = nextFreePoint(points, b, polygonN)
|
||||
end
|
||||
|
||||
count += 1
|
||||
|
||||
if count > polygonN and #triangles == 0 then
|
||||
unableToSplit(polygon)
|
||||
|
||||
return triangles
|
||||
end
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
|
||||
return triangles
|
||||
end
|
||||
|
||||
local insideZones = lib.context == 'client' and {} --[[@as table<number, CZone>]]
|
||||
local exitingZones = lib.context == 'client' and lib.array:new() --[[@as Array<CZone>]]
|
||||
local enteringZones = lib.context == 'client' and lib.array:new() --[[@as Array<CZone>]]
|
||||
local nearbyZones = lib.array:new() --[[@as Array<CZone>]]
|
||||
local glm_polygon_contains = glm.polygon.contains
|
||||
local tick
|
||||
|
||||
---@param zone CZone
|
||||
local function removeZone(zone)
|
||||
Zones[zone.id] = nil
|
||||
|
||||
lib.grid.removeEntry(zone)
|
||||
|
||||
if lib.context == 'server' then return end
|
||||
|
||||
insideZones[zone.id] = nil
|
||||
|
||||
local exitingIndex = exitingZones:indexOf(zone)
|
||||
if exitingIndex then
|
||||
table.remove(exitingZones, exitingIndex)
|
||||
end
|
||||
|
||||
local enteringIndex = enteringZones:indexOf(zone)
|
||||
if enteringIndex then
|
||||
table.remove(enteringZones, enteringIndex)
|
||||
end
|
||||
end
|
||||
|
||||
CreateThread(function()
|
||||
if lib.context == 'server' then return end
|
||||
|
||||
while true do
|
||||
local coords = GetEntityCoords(cache.ped)
|
||||
local zones = lib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removeZone end) --[[@as Array<CZone>]]
|
||||
local cellX, cellY = lib.grid.getCellPosition(coords)
|
||||
cache.coords = coords
|
||||
|
||||
if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
|
||||
for i = 1, #nearbyZones do
|
||||
local zone = nearbyZones[i]
|
||||
|
||||
if zone.insideZone then
|
||||
local contains = zone:contains(coords, true)
|
||||
|
||||
if not contains then
|
||||
zone.insideZone = false
|
||||
insideZones[zone.id] = nil
|
||||
|
||||
if zone.onExit then
|
||||
exitingZones:push(zone)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cache.lastCellX = cellX
|
||||
cache.lastCellY = cellY
|
||||
end
|
||||
|
||||
nearbyZones = zones
|
||||
|
||||
for i = 1, #zones do
|
||||
local zone = zones[i]
|
||||
local contains = zone:contains(coords, true)
|
||||
|
||||
if contains then
|
||||
if not zone.insideZone then
|
||||
zone.insideZone = true
|
||||
|
||||
if zone.onEnter then
|
||||
enteringZones:push(zone)
|
||||
end
|
||||
|
||||
if zone.inside or zone.debug then
|
||||
insideZones[zone.id] = zone
|
||||
end
|
||||
end
|
||||
else
|
||||
if zone.insideZone then
|
||||
zone.insideZone = false
|
||||
insideZones[zone.id] = nil
|
||||
|
||||
if zone.onExit then
|
||||
exitingZones:push(zone)
|
||||
end
|
||||
end
|
||||
|
||||
if zone.debug then
|
||||
insideZones[zone.id] = zone
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local exitingSize = #exitingZones
|
||||
local enteringSize = #enteringZones
|
||||
|
||||
if exitingSize > 0 then
|
||||
table.sort(exitingZones, function(a, b)
|
||||
return a.distance < b.distance
|
||||
end)
|
||||
|
||||
for i = exitingSize, 1, -1 do
|
||||
exitingZones[i]:onExit()
|
||||
end
|
||||
|
||||
table.wipe(exitingZones)
|
||||
end
|
||||
|
||||
if enteringSize > 0 then
|
||||
table.sort(enteringZones, function(a, b)
|
||||
return a.distance < b.distance
|
||||
end)
|
||||
|
||||
for i = 1, enteringSize do
|
||||
enteringZones[i]:onEnter()
|
||||
end
|
||||
|
||||
table.wipe(enteringZones)
|
||||
end
|
||||
|
||||
if not tick then
|
||||
if next(insideZones) then
|
||||
tick = SetInterval(function()
|
||||
for _, zone in pairs(insideZones) do
|
||||
if zone.debug then
|
||||
zone:debug()
|
||||
|
||||
if zone.inside and zone.insideZone then
|
||||
zone:inside()
|
||||
end
|
||||
else
|
||||
zone:inside()
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
elseif not next(insideZones) then
|
||||
tick = ClearInterval(tick)
|
||||
end
|
||||
|
||||
Wait(300)
|
||||
end
|
||||
end)
|
||||
|
||||
local DrawLine = DrawLine
|
||||
local DrawPoly = DrawPoly
|
||||
|
||||
local function debugPoly(self)
|
||||
for i = 1, #self.triangles do
|
||||
local triangle = self.triangles[i]
|
||||
DrawPoly(triangle[1].x, triangle[1].y, triangle[1].z, triangle[2].x, triangle[2].y, triangle[2].z, triangle[3].x, triangle[3].y, triangle[3].z,
|
||||
self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
DrawPoly(triangle[2].x, triangle[2].y, triangle[2].z, triangle[1].x, triangle[1].y, triangle[1].z, triangle[3].x, triangle[3].y, triangle[3].z,
|
||||
self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
end
|
||||
for i = 1, #self.polygon do
|
||||
local thickness = vec(0, 0, self.thickness / 2)
|
||||
local a = self.polygon[i] + thickness
|
||||
local b = self.polygon[i] - thickness
|
||||
local c = (self.polygon[i + 1] or self.polygon[1]) + thickness
|
||||
local d = (self.polygon[i + 1] or self.polygon[1]) - thickness
|
||||
DrawLine(a.x, a.y, a.z, b.x, b.y, b.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
||||
DrawLine(a.x, a.y, a.z, c.x, c.y, c.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
||||
DrawLine(b.x, b.y, b.z, d.x, d.y, d.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, 225)
|
||||
DrawPoly(a.x, a.y, a.z, b.x, b.y, b.z, c.x, c.y, c.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
DrawPoly(c.x, c.y, c.z, b.x, b.y, b.z, a.x, a.y, a.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
DrawPoly(b.x, b.y, b.z, c.x, c.y, c.z, d.x, d.y, d.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
DrawPoly(d.x, d.y, d.z, c.x, c.y, c.z, b.x, b.y, b.z, self.debugColour.r, self.debugColour.g, self.debugColour.b, self.debugColour.a)
|
||||
end
|
||||
end
|
||||
|
||||
local function debugSphere(self)
|
||||
DrawMarker(28, self.coords.x, self.coords.y, self.coords.z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, self.radius, self.radius, self.radius, self.debugColour.r,
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
self.debugColour.g, self.debugColour.b, self.debugColour.a, false, false, 0, false, false, false, false)
|
||||
end
|
||||
|
||||
local function contains(self, coords, updateDistance)
|
||||
if updateDistance then self.distance = #(self.coords - coords) end
|
||||
|
||||
return glm_polygon_contains(self.polygon, coords, self.thickness / 4)
|
||||
end
|
||||
|
||||
local function insideSphere(self, coords, updateDistance)
|
||||
local distance = #(self.coords - coords)
|
||||
|
||||
if updateDistance then self.distance = distance end
|
||||
|
||||
return distance < self.radius
|
||||
end
|
||||
|
||||
local function convertToVector(coords)
|
||||
local _type = type(coords)
|
||||
|
||||
if _type ~= 'vector3' then
|
||||
if _type == 'table' or _type == 'vector4' then
|
||||
return vec3(coords[1] or coords.x, coords[2] or coords.y, coords[3] or coords.z)
|
||||
end
|
||||
|
||||
error(("expected type 'vector3' or 'table' (received %s)"):format(_type))
|
||||
end
|
||||
|
||||
return coords
|
||||
end
|
||||
|
||||
local function setDebug(self, bool, colour)
|
||||
if not bool and insideZones[self.id] then
|
||||
insideZones[self.id] = nil
|
||||
end
|
||||
|
||||
self.debugColour = bool and
|
||||
{
|
||||
r = glm.tointeger(colour?.r or self.debugColour?.r or 255),
|
||||
g = glm.tointeger(colour?.g or self.debugColour?.g or 42),
|
||||
b = glm.tointeger(colour?.b or
|
||||
self.debugColour?.b or 24),
|
||||
a = glm.tointeger(colour?.a or self.debugColour?.a or 100)
|
||||
} or nil
|
||||
|
||||
if not bool and self.debug then
|
||||
self.triangles = nil
|
||||
self.debug = nil
|
||||
return
|
||||
end
|
||||
|
||||
if bool and self.debug and self.debug ~= true then return end
|
||||
|
||||
self.triangles = self.__type == 'poly' and getTriangles(self.polygon) or
|
||||
self.__type == 'box' and { mat(self.polygon[1], self.polygon[2], self.polygon[3]), mat(self.polygon[1], self.polygon[3], self.polygon[4]) } or nil
|
||||
self.debug = self.__type == 'sphere' and debugSphere or debugPoly or nil
|
||||
end
|
||||
|
||||
---@param data ZoneProperties
|
||||
---@return CZone
|
||||
local function setZone(data)
|
||||
---@cast data CZone
|
||||
data.remove = removeZone
|
||||
data.contains = data.contains or contains
|
||||
|
||||
if lib.context == 'client' then
|
||||
local coords = cache.coords or GetEntityCoords(cache.ped)
|
||||
data.distance = #(data.coords - coords)
|
||||
data.setDebug = setDebug
|
||||
|
||||
if data.debug then
|
||||
data.debug = nil
|
||||
|
||||
data:setDebug(true, data.debugColour)
|
||||
end
|
||||
else
|
||||
data.debug = nil
|
||||
end
|
||||
|
||||
Zones[data.id] = data
|
||||
lib.grid.addEntry(data)
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
lib.zones = {}
|
||||
|
||||
---@class PolyZone : ZoneProperties
|
||||
---@field points vector3[]
|
||||
---@field thickness? number
|
||||
|
||||
---@param data PolyZone
|
||||
---@return CZone
|
||||
function lib.zones.poly(data)
|
||||
data.id = #Zones + 1
|
||||
data.thickness = data.thickness or 4
|
||||
|
||||
local pointN = #data.points
|
||||
local points = table.create(pointN, 0)
|
||||
|
||||
for i = 1, pointN do
|
||||
points[i] = convertToVector(data.points[i])
|
||||
end
|
||||
|
||||
data.polygon = glm.polygon.new(points)
|
||||
|
||||
if not data.polygon:isPlanar() then
|
||||
local zCoords = {}
|
||||
|
||||
for i = 1, pointN do
|
||||
local zCoord = points[i].z
|
||||
|
||||
if zCoords[zCoord] then
|
||||
zCoords[zCoord] += 1
|
||||
else
|
||||
zCoords[zCoord] = 1
|
||||
end
|
||||
end
|
||||
|
||||
local coordsArray = {}
|
||||
|
||||
for coord, count in pairs(zCoords) do
|
||||
coordsArray[#coordsArray + 1] = {
|
||||
coord = coord,
|
||||
count = count
|
||||
}
|
||||
end
|
||||
|
||||
table.sort(coordsArray, function(a, b)
|
||||
return a.count > b.count
|
||||
end)
|
||||
|
||||
local zCoord = coordsArray[1].coord
|
||||
local averageTo = 1
|
||||
|
||||
for i = 1, #coordsArray do
|
||||
if coordsArray[i].count < coordsArray[1].count then
|
||||
averageTo = i - 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if averageTo > 1 then
|
||||
for i = 2, averageTo do
|
||||
zCoord += coordsArray[i].coord
|
||||
end
|
||||
|
||||
zCoord /= averageTo
|
||||
end
|
||||
|
||||
for i = 1, pointN do
|
||||
---@diagnostic disable-next-line: param-type-mismatch
|
||||
points[i] = vec3(data.points[i].xy, zCoord)
|
||||
end
|
||||
|
||||
data.polygon = glm.polygon.new(points)
|
||||
end
|
||||
|
||||
data.coords = data.polygon:centroid()
|
||||
data.__type = 'poly'
|
||||
data.radius = lib.array.reduce(data.polygon, function(acc, point)
|
||||
local distance = #(point - data.coords)
|
||||
return distance > acc and distance or acc
|
||||
end, 0)
|
||||
|
||||
return setZone(data)
|
||||
end
|
||||
|
||||
---@class BoxZone : ZoneProperties
|
||||
---@field coords vector3
|
||||
---@field size? vector3
|
||||
---@field rotation? number | vector3 | vector4 | matrix
|
||||
|
||||
---@param data BoxZone
|
||||
---@return CZone
|
||||
function lib.zones.box(data)
|
||||
data.id = #Zones + 1
|
||||
data.coords = convertToVector(data.coords)
|
||||
data.size = data.size and convertToVector(data.size) / 2 or vec3(2)
|
||||
data.thickness = data.size.z * 2
|
||||
data.rotation = quat(data.rotation or 0, vec3(0, 0, 1))
|
||||
data.__type = 'box'
|
||||
data.width = data.size.x * 2
|
||||
data.length = data.size.y * 2
|
||||
data.polygon = (data.rotation * glm.polygon.new({
|
||||
vec3(data.size.x, data.size.y, 0),
|
||||
vec3(-data.size.x, data.size.y, 0),
|
||||
vec3(-data.size.x, -data.size.y, 0),
|
||||
vec3(data.size.x, -data.size.y, 0),
|
||||
}) + data.coords)
|
||||
|
||||
return setZone(data)
|
||||
end
|
||||
|
||||
---@class SphereZone : ZoneProperties
|
||||
---@field coords vector3
|
||||
---@field radius? number
|
||||
|
||||
---@param data SphereZone
|
||||
---@return CZone
|
||||
function lib.zones.sphere(data)
|
||||
data.id = #Zones + 1
|
||||
data.coords = convertToVector(data.coords)
|
||||
data.radius = (data.radius or 2) + 0.0
|
||||
data.__type = 'sphere'
|
||||
data.contains = insideSphere
|
||||
|
||||
return setZone(data)
|
||||
end
|
||||
|
||||
function lib.zones.getAllZones() return Zones end
|
||||
|
||||
function lib.zones.getCurrentZones() return insideZones end
|
||||
|
||||
function lib.zones.getNearbyZones() return nearbyZones end
|
||||
|
||||
return lib.zones
|
||||
@@ -1,299 +0,0 @@
|
||||
---@meta
|
||||
--[[
|
||||
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 not _VERSION:find('5.4') then
|
||||
error('Lua 5.4 must be enabled in the resource manifest!', 2)
|
||||
end
|
||||
|
||||
local resourceName = GetCurrentResourceName()
|
||||
local ox_lib = 'ox_lib'
|
||||
|
||||
-- Some people have decided to load this file as part of ox_lib's fxmanifest?
|
||||
if resourceName == ox_lib then return end
|
||||
|
||||
if lib and lib.name == ox_lib then
|
||||
error(("Cannot load ox_lib more than once.\n\tRemove any duplicate entries from '@%s/fxmanifest.lua'"):format(resourceName))
|
||||
end
|
||||
|
||||
local export = exports[ox_lib]
|
||||
|
||||
if GetResourceState(ox_lib) ~= 'started' then
|
||||
error('^1ox_lib must be started before this resource.^0', 0)
|
||||
end
|
||||
|
||||
local status = export.hasLoaded()
|
||||
|
||||
if status ~= true then error(status, 2) end
|
||||
|
||||
-- Ignore invalid types during msgpack.pack (e.g. userdata)
|
||||
msgpack.setoption('ignore_invalid', true)
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
-- Module
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
local LoadResourceFile = LoadResourceFile
|
||||
local context = IsDuplicityVersion() and 'server' or 'client'
|
||||
|
||||
function noop() end
|
||||
|
||||
local function loadModule(self, module)
|
||||
local dir = ('imports/%s'):format(module)
|
||||
local chunk = LoadResourceFile(ox_lib, ('%s/%s.lua'):format(dir, context))
|
||||
local shared = LoadResourceFile(ox_lib, ('%s/shared.lua'):format(dir))
|
||||
|
||||
if shared then
|
||||
chunk = (chunk and ('%s\n%s'):format(shared, chunk)) or shared
|
||||
end
|
||||
|
||||
if chunk then
|
||||
local fn, err = load(chunk, ('@@ox_lib/imports/%s/%s.lua'):format(module, context))
|
||||
|
||||
if not fn or err then
|
||||
if shared then
|
||||
lib.print.warn(("An error occurred when importing '@ox_lib/imports/%s'.\nThis is likely caused by improperly updating ox_lib.\n%s'")
|
||||
:format(module, err))
|
||||
fn, err = load(shared, ('@@ox_lib/imports/%s/shared.lua'):format(module))
|
||||
end
|
||||
|
||||
if not fn or err then
|
||||
return error(('\n^1Error importing module (%s): %s^0'):format(dir, err), 3)
|
||||
end
|
||||
end
|
||||
|
||||
local result = fn()
|
||||
self[module] = result or noop
|
||||
return self[module]
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------------------------
|
||||
-- API
|
||||
-----------------------------------------------------------------------------------------------
|
||||
|
||||
local function call(self, index, ...)
|
||||
local module = rawget(self, index)
|
||||
|
||||
if not module then
|
||||
self[index] = noop
|
||||
module = loadModule(self, index)
|
||||
|
||||
if not module then
|
||||
local function method(...)
|
||||
return export[index](nil, ...)
|
||||
end
|
||||
|
||||
if not ... then
|
||||
self[index] = method
|
||||
end
|
||||
|
||||
return method
|
||||
end
|
||||
end
|
||||
|
||||
return module
|
||||
end
|
||||
|
||||
local lib = setmetatable({
|
||||
name = ox_lib,
|
||||
context = context,
|
||||
}, {
|
||||
__index = call,
|
||||
__call = call,
|
||||
})
|
||||
|
||||
local intervals = {}
|
||||
--- Dream of a world where this PR gets accepted.
|
||||
---@param callback function | number
|
||||
---@param interval? number
|
||||
---@param ... any
|
||||
function SetInterval(callback, interval, ...)
|
||||
interval = interval or 0
|
||||
|
||||
if type(interval) ~= 'number' then
|
||||
return error(('Interval must be a number. Received %s'):format(json.encode(interval --[[@as unknown]])))
|
||||
end
|
||||
|
||||
local cbType = type(callback)
|
||||
|
||||
if cbType == 'number' and intervals[callback] then
|
||||
intervals[callback] = interval or 0
|
||||
return
|
||||
end
|
||||
|
||||
if cbType ~= 'function' then
|
||||
return error(('Callback must be a function. Received %s'):format(cbType))
|
||||
end
|
||||
|
||||
local args, id = { ... }
|
||||
|
||||
Citizen.CreateThreadNow(function(ref)
|
||||
id = ref
|
||||
intervals[id] = interval or 0
|
||||
repeat
|
||||
interval = intervals[id]
|
||||
Wait(interval)
|
||||
|
||||
if interval < 0 then break end
|
||||
callback(table.unpack(args))
|
||||
until false
|
||||
intervals[id] = nil
|
||||
end)
|
||||
|
||||
return id
|
||||
end
|
||||
|
||||
---@param id number
|
||||
function ClearInterval(id)
|
||||
if type(id) ~= 'number' then
|
||||
return error(('Interval id must be a number. Received %s'):format(json.encode(id --[[@as unknown]])))
|
||||
end
|
||||
|
||||
if not intervals[id] then
|
||||
return error(('No interval exists with id %s'):format(id))
|
||||
end
|
||||
|
||||
intervals[id] = -1
|
||||
end
|
||||
|
||||
--[[
|
||||
lua language server doesn't support generics when using @overload
|
||||
see https://github.com/LuaLS/lua-language-server/issues/723
|
||||
this function stub allows the following to work
|
||||
|
||||
local key = cache('key', function() return 'abc' end) -- fff: 'abc'
|
||||
local game = cache.game -- game: string
|
||||
]]
|
||||
|
||||
---@generic T
|
||||
---@param key string
|
||||
---@param func fun(...: any): T
|
||||
---@param timeout? number
|
||||
---@return T
|
||||
---Caches the result of a function, optionally clearing it after timeout ms.
|
||||
function cache(key, func, timeout) end
|
||||
|
||||
local cacheEvents = {}
|
||||
|
||||
local cache = setmetatable({ game = GetGameName(), resource = resourceName }, {
|
||||
__index = function(self, key)
|
||||
cacheEvents[key] = {}
|
||||
|
||||
AddEventHandler(('ox_lib:cache:%s'):format(key), function(value)
|
||||
local oldValue = self[key]
|
||||
local events = cacheEvents[key]
|
||||
|
||||
for i = 1, #events do
|
||||
Citizen.CreateThreadNow(function()
|
||||
events[i](value, oldValue)
|
||||
end)
|
||||
end
|
||||
|
||||
self[key] = value
|
||||
end)
|
||||
|
||||
return rawset(self, key, export.cache(nil, key) or false)[key]
|
||||
end,
|
||||
|
||||
__call = function(self, key, func, timeout)
|
||||
local value = rawget(self, key)
|
||||
|
||||
if value == nil then
|
||||
value = func()
|
||||
|
||||
rawset(self, key, value)
|
||||
|
||||
if timeout then SetTimeout(timeout, function() self[key] = nil end) end
|
||||
end
|
||||
|
||||
return value
|
||||
end,
|
||||
})
|
||||
|
||||
function lib.onCache(key, cb)
|
||||
if not cacheEvents[key] then
|
||||
getmetatable(cache).__index(cache, key)
|
||||
end
|
||||
|
||||
table.insert(cacheEvents[key], cb)
|
||||
end
|
||||
|
||||
_ENV.lib = lib
|
||||
_ENV.cache = cache
|
||||
_ENV.require = lib.require
|
||||
|
||||
local notifyEvent = ('__ox_notify_%s'):format(cache.resource)
|
||||
|
||||
if context == 'client' then
|
||||
RegisterNetEvent(notifyEvent, function(data)
|
||||
if locale then
|
||||
if data.title then
|
||||
data.title = locale(data.title) or data.title
|
||||
end
|
||||
|
||||
if data.description then
|
||||
data.description = locale(data.description) or data.description
|
||||
end
|
||||
end
|
||||
|
||||
return export:notify(data)
|
||||
end)
|
||||
|
||||
cache.playerId = PlayerId()
|
||||
cache.serverId = GetPlayerServerId(cache.playerId)
|
||||
else
|
||||
---`server`\
|
||||
---Trigger a notification on the target playerId from the server.\
|
||||
---If locales are loaded, the title and description will be formatted automatically.\
|
||||
---Note: No support for locale placeholders when using this function.
|
||||
---@param playerId number
|
||||
---@param data NotifyProps
|
||||
---@deprecated
|
||||
---@diagnostic disable-next-line: duplicate-set-field
|
||||
function lib.notify(playerId, data)
|
||||
TriggerClientEvent(notifyEvent, playerId, data)
|
||||
end
|
||||
|
||||
local poolNatives = {
|
||||
CPed = GetAllPeds,
|
||||
CObject = GetAllObjects,
|
||||
CVehicle = GetAllVehicles,
|
||||
}
|
||||
|
||||
---@param poolName 'CPed' | 'CObject' | 'CVehicle'
|
||||
---@return number[]
|
||||
---Server-side parity for the `GetGamePool` client native.
|
||||
function GetGamePool(poolName)
|
||||
local fn = poolNatives[poolName]
|
||||
return fn and fn() --[[@as number[] ]]
|
||||
end
|
||||
|
||||
---@return number[]
|
||||
---Server-side parity for the `GetPlayers` client native.
|
||||
function GetActivePlayers()
|
||||
local playerNum = GetNumPlayerIndices()
|
||||
local players = table.create(playerNum, 0)
|
||||
|
||||
for i = 1, playerNum do
|
||||
players[i] = tonumber(GetPlayerFromIndex(i - 1))
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
end
|
||||
|
||||
for i = 1, GetNumResourceMetadata(cache.resource, 'ox_lib') do
|
||||
local name = GetResourceMetadata(cache.resource, 'ox_lib', i - 1)
|
||||
|
||||
if not rawget(lib, name) then
|
||||
local module = loadModule(lib, name)
|
||||
|
||||
if type(module) == 'function' then pcall(module) end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Shqip",
|
||||
"ui": {
|
||||
"cancel": "Cancel",
|
||||
"close": "Mbylle",
|
||||
"confirm": "Konfirmo",
|
||||
"more": "More...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "العربية",
|
||||
"ui": {
|
||||
"cancel": "إلغاء",
|
||||
"close": "إغلاق",
|
||||
"confirm": "تأكيد",
|
||||
"more": "المزيد ...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "إعلان من قبل %s",
|
||||
"txadmin_dm": "رسالة من %s",
|
||||
"txadmin_warn": "تم تحذيرك من قبل %s",
|
||||
"txadmin_warn_content": "%s \nأيدي: %s",
|
||||
"txadmin_scheduledrestart": "تحديد ريستارت للسرفر"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Čeština",
|
||||
"ui": {
|
||||
"cancel": "Zrušit",
|
||||
"close": "Zavřít",
|
||||
"confirm": "Potvrdit",
|
||||
"more": "Více...",
|
||||
"settings": {
|
||||
"locale": "Změnit jazyk",
|
||||
"locale_description": "Aktuální jazyk: ${language} (%s)",
|
||||
"notification_audio": "Zvuk notifikací",
|
||||
"notification_position": "Pozice notifikací"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Dole",
|
||||
"bottom-left": "Vlevo dole",
|
||||
"bottom-right": "Vpravo dole",
|
||||
"center-left": "Vlevo uprostřed",
|
||||
"center-right": "Vpravo uprostřed",
|
||||
"top": "Nahoře",
|
||||
"top-left": "Vlevo nahoře",
|
||||
"top-right": "Vpravo nahoře"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Otevřít kruhové menu",
|
||||
"cancel_progress": "Zrušit aktuální progress bar",
|
||||
"txadmin_announcement": "Serverové oznámení od %s",
|
||||
"txadmin_dm": "Soukromá zpráva od %s",
|
||||
"txadmin_warn": "Byl jsi varován od %s",
|
||||
"txadmin_warn_content": "%s \nID akce: %s",
|
||||
"txadmin_scheduledrestart": "Plánovaný restart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Dansk",
|
||||
"settings": "Indstillinger",
|
||||
"ui": {
|
||||
"cancel": "Annuller",
|
||||
"close": "Luk",
|
||||
"confirm": "Bekræft",
|
||||
"more": "Mere...",
|
||||
"settings": {
|
||||
"locale": "Skift Sprog",
|
||||
"locale_description": "Nuværende sprog: ${language} (%s)",
|
||||
"notification_audio": "Notifikations lyd",
|
||||
"notification_position": "Notifikations position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Nederst",
|
||||
"bottom-left": "Nederst til venstre",
|
||||
"bottom-right": "Nederst til højre",
|
||||
"center-left": "Center til venstre",
|
||||
"center-right": "Center til højre",
|
||||
"top": "Øverst",
|
||||
"top-left": "Øverst til venstre",
|
||||
"top-right": "Øverst til højre"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Åbn radial menu",
|
||||
"cancel_progress": "Annuller den aktuelle progress bar",
|
||||
"txadmin_announcement": "Servermeddelelse af %s",
|
||||
"txadmin_dm": "Direkte besked fra %s",
|
||||
"txadmin_warn": "Du er blevet advaret af %s",
|
||||
"txadmin_warn_content": "%s \nHandlings-id: %s",
|
||||
"txadmin_scheduledrestart": "Planlagt genstart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Deutsch",
|
||||
"settings": "Einstellungen",
|
||||
"ui": {
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"confirm": "Bestätigen",
|
||||
"more": "Mehr...",
|
||||
"settings": {
|
||||
"locale": "Sprache ändern",
|
||||
"locale_description": "Aktuelle Sprache: ${language} (%s)",
|
||||
"notification_audio": "Benachrichtigungs-Sound",
|
||||
"notification_position": "Benachrichtigungs-Position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Unten",
|
||||
"bottom-left": "Unten-Links",
|
||||
"bottom-right": "Unten-Rechts",
|
||||
"center-left": "Mittig-Links",
|
||||
"center-right": "Mittig-Rechts",
|
||||
"top": "Oben",
|
||||
"top-left": "Oben-Links",
|
||||
"top-right": "Oben-Rechts"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Radial Menu öffnen",
|
||||
"cancel_progress": "Aktuelle Tätigkeit abbrechen",
|
||||
"txadmin_announcement": "Serverankündigung von %s",
|
||||
"txadmin_dm": "Direktnachricht von %s",
|
||||
"txadmin_warn": "Du wurdest von %s verwarnt",
|
||||
"txadmin_warn_content": "%s \nVerwarn ID: %s",
|
||||
"txadmin_scheduledrestart": "Geplanter Neustart"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"language": "Ελληνικά",
|
||||
"settings": "Ρυθμίσεις",
|
||||
"ui": {
|
||||
"cancel": "Ακύρωση",
|
||||
"close": "Κλείσιμο",
|
||||
"confirm": "Επιβεβαίωση",
|
||||
"more": "Περισσότερα...",
|
||||
"settings": {
|
||||
"locale": "Αλλαγή γλώσσας",
|
||||
"locale_description": "Τρέχουσα γλώσσα: ${language} (%s)",
|
||||
"notification_audio": "Ήχος ειδοποίησης",
|
||||
"notification_position": "Θέση ειδοποίησης"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Κάτω",
|
||||
"bottom-left": "Κάτω αριστερά",
|
||||
"bottom-right": "Κάτω δεξιά",
|
||||
"center-left": "Κέντρο αριστερά",
|
||||
"center-right": "Κέντρο δεξιά",
|
||||
"top": "Πάνω",
|
||||
"top-left": "Πάνω αριστερά",
|
||||
"top-right": "Πάνω δεξιά"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Άνοιγμα κυκλικού μενού",
|
||||
"cancel_progress": "Ακύρωση τρέχουσας γραμμής προόδου",
|
||||
"txadmin_announcement": "Ανακοίνωση διακομιστή από τον %s",
|
||||
"txadmin_dm": "Άμεσο μήνυμα από τον %s",
|
||||
"txadmin_warn": "Έχετε προειδοποιηθεί από τον %s",
|
||||
"txadmin_warn_content": "%s \nID Ενέργειας: %s",
|
||||
"txadmin_scheduledrestart": "Προγραμματισμένη επανεκκίνηση"
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "English",
|
||||
"settings": "Settings",
|
||||
"ui": {
|
||||
"cancel": "Cancel",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"more": "More...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Español",
|
||||
"settings": "Ajustes",
|
||||
"ui": {
|
||||
"cancel": "Cancelar",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"more": "Más...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Anuncio de servidor de %s",
|
||||
"txadmin_dm": "Mensaje directo de %s",
|
||||
"txadmin_warn": "Has sido advertido por %s",
|
||||
"txadmin_warn_content": "%s \nID de acción: %s",
|
||||
"txadmin_scheduledrestart": "Reinicio programado"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Eesti",
|
||||
"ui": {
|
||||
"cancel": "Tühista",
|
||||
"close": "Sulge",
|
||||
"confirm": "Kinnita",
|
||||
"more": "Rohkem...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Ava rullikmenüü",
|
||||
"cancel_progress": "Katkesta praegune edenemisriba",
|
||||
"txadmin_announcement": "Serveri teadaanne kasutajalt %s",
|
||||
"txadmin_dm": "Sulle saadeti sõnum kasutajalt %s",
|
||||
"txadmin_warn": "Sind hoiatati kasutaja %s poolt",
|
||||
"txadmin_warn_content": "%s \nTegevuse ID: %s",
|
||||
"txadmin_scheduledrestart": "Planeeritud taaskäivitus"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Suomi",
|
||||
"settings": "Asetukset",
|
||||
"ui": {
|
||||
"cancel": "Peruuta",
|
||||
"close": "Sulje",
|
||||
"confirm": "Vahvista",
|
||||
"more": "Lisää...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Ilmoitus palvelimen ylläpitäjältä %s",
|
||||
"txadmin_dm": "Sait viestin henkilöltä %s",
|
||||
"txadmin_warn": "Sinua on varoitettu henkilön %s toimesta",
|
||||
"txadmin_warn_content": "%s \nTunniste: %s",
|
||||
"txadmin_scheduledrestart": "Ajoitettu uudelleenkäynnistyminen"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Français",
|
||||
"settings": "Paramètres",
|
||||
"ui": {
|
||||
"cancel": "Annuler",
|
||||
"close": "Fermer",
|
||||
"confirm": "Confirmer",
|
||||
"more": "Plus...",
|
||||
"settings": {
|
||||
"locale": "Changer la langue",
|
||||
"locale_description": "Langue actuelle : ${language} (%s)",
|
||||
"notification_audio": "Audio des notifications",
|
||||
"notification_position": "Position des notifications"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bas",
|
||||
"bottom-left": "Bas-gauche",
|
||||
"bottom-right": "Bas-droite",
|
||||
"center-left": "Centre-gauche",
|
||||
"center-right": "Centre-droite",
|
||||
"top": "Haut",
|
||||
"top-left": "Haut-gauche",
|
||||
"top-right": "Haut-droite"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Ouvrir le menu radial",
|
||||
"cancel_progress": "Annuler la barre de progression actuelle",
|
||||
"txadmin_announcement": "Annonce serveur par %s",
|
||||
"txadmin_dm": "Message privé de %s",
|
||||
"txadmin_warn": "Vous avez été averti par %s",
|
||||
"txadmin_warn_content": "%s\nID de l'action : %s",
|
||||
"txadmin_scheduledrestart": "Redémarrage programmé"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "עברית",
|
||||
"ui": {
|
||||
"cancel": "ביטול",
|
||||
"close": "סגירה",
|
||||
"confirm": "אישור",
|
||||
"more": "...עוד",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Hrvatski",
|
||||
"ui": {
|
||||
"cancel": "Odustani",
|
||||
"close": "Zatvori",
|
||||
"confirm": "Potvrdi",
|
||||
"more": "Više...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Magyar",
|
||||
"ui": {
|
||||
"cancel": "Mégse",
|
||||
"close": "Bezárás",
|
||||
"confirm": "Megerősít",
|
||||
"more": "Tovább...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Szerver bejelentés által %s",
|
||||
"txadmin_dm": "Közvetlen üzenet a %s",
|
||||
"txadmin_warn": "Figyelmeztetett %s",
|
||||
"txadmin_warn_content": "%s \nMűvelet ID: %s",
|
||||
"txadmin_scheduledrestart": "Ütemezett újraindítás"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Indonesian",
|
||||
"ui": {
|
||||
"cancel": "Batal",
|
||||
"close": "Tutup",
|
||||
"confirm": "Konfirmasi",
|
||||
"more": "More...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Italiano",
|
||||
"settings": "Impostazioni",
|
||||
"ui": {
|
||||
"cancel": "Annulla",
|
||||
"close": "Chiudi",
|
||||
"confirm": "Conferma",
|
||||
"more": "Altro...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Annuncio server da %s",
|
||||
"txadmin_dm": "Messaggio diretto da %s",
|
||||
"txadmin_warn": "Sei stato richiamato da %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Riavvio Programmato"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Lietuvių",
|
||||
"settings": "Nustatymai",
|
||||
"ui": {
|
||||
"cancel": "Atšaukti",
|
||||
"close": "Uždaryti",
|
||||
"confirm": "Patvirtinti",
|
||||
"more": "Daugiau...",
|
||||
"settings": {
|
||||
"locale": "Pakeisti kalbą",
|
||||
"locale_description": "Dabartinė: ${language} (%s)",
|
||||
"notification_audio": "Pranešimo garsas",
|
||||
"notification_position": "Pranešimo pozicija"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Apačioje",
|
||||
"bottom-left": "Apačioje-kairėje",
|
||||
"bottom-right": "Apačioje-dešinėje",
|
||||
"center-left": "Centre-kairėje",
|
||||
"center-right": "Centre-dešinėje",
|
||||
"top": "Viršuje",
|
||||
"top-left": "Viršuje-kairėje",
|
||||
"top-right": "Viršuje-dešinėje"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Atidaryti radialinį meniu",
|
||||
"cancel_progress": "Atšaukti dabartinę eigos juostą",
|
||||
"txadmin_announcement": "Serverio pranešimas nuo %s",
|
||||
"txadmin_dm": "Tiesioginis pranešimas nuo %s",
|
||||
"txadmin_warn": "Buvote įspėtas nuo %s",
|
||||
"txadmin_warn_content": "%s \nVeiksmo ID: %s",
|
||||
"txadmin_scheduledrestart": "Suplanuotas paleidimas iš naujo"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Nederlands",
|
||||
"settings": "Instellingen",
|
||||
"ui": {
|
||||
"cancel": "Annuleren",
|
||||
"close": "Sluiten",
|
||||
"confirm": "Bevestigen",
|
||||
"more": "Meer...",
|
||||
"settings": {
|
||||
"locale": "Taal wijzigen",
|
||||
"locale_description": "Huidige taal: ${language} (%s)",
|
||||
"notification_audio": "Meldingsgeluid",
|
||||
"notification_position": "Meldingspositie"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Onder",
|
||||
"bottom-left": "Linksonder",
|
||||
"bottom-right": "Rechtsonder",
|
||||
"center-left": "Linksmidden",
|
||||
"center-right": "Rechtsmidden",
|
||||
"top": "Boven",
|
||||
"top-left": "Linksboven",
|
||||
"top-right": "Rechtsboven"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Radiaal menu openen",
|
||||
"cancel_progress": "Huidige voortgangsbalk annuleren",
|
||||
"txadmin_announcement": "Server mededeling door %s",
|
||||
"txadmin_dm": "Bericht van %s",
|
||||
"txadmin_warn": "Je hebt een waarschuwing gekregen van %s",
|
||||
"txadmin_warn_content": "%s \nActie ID: %s",
|
||||
"txadmin_scheduledrestart": "Geplande Server Restart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Norsk",
|
||||
"settings": "Innstillinger",
|
||||
"ui": {
|
||||
"cancel": "Avbryt",
|
||||
"close": "Lukk",
|
||||
"confirm": "Bekreft",
|
||||
"more": "Mer...",
|
||||
"settings": {
|
||||
"locale": "Endre språk",
|
||||
"locale_description": "Nåværende språk: ${language} (%s)",
|
||||
"notification_audio": "Varslingslyd",
|
||||
"notification_position": "Varslingsposisjon"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Nederst",
|
||||
"bottom-left": "Nederst til venstre",
|
||||
"bottom-right": "Nederst til høyre",
|
||||
"center-left": "Midten venstre",
|
||||
"center-right": "Midten høyre",
|
||||
"top": "Øverst",
|
||||
"top-left": "Øverst til venstre",
|
||||
"top-right": "Øverst til høyre"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Åpne radialmenyen",
|
||||
"cancel_progress": "Avbryt den nåværende progresjonsbaren",
|
||||
"txadmin_announcement": "Serverannonsering fra %s",
|
||||
"txadmin_dm": "Direktemelding fra %s",
|
||||
"txadmin_warn": "Du har fått en advarsel fra %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Planlagt omstart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Polski",
|
||||
"ui": {
|
||||
"cancel": "Anuluj",
|
||||
"close": "Zamknij",
|
||||
"confirm": "Potwierdź",
|
||||
"more": "Więcej...",
|
||||
"settings": {
|
||||
"locale": "Zmień język",
|
||||
"locale_description": "Aktualny język: ${language} (%s)",
|
||||
"notification_audio": "Powiadomienie dźwiękowe",
|
||||
"notification_position": "Pozycja powiadomień"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Dół",
|
||||
"bottom-left": "Lewy dolny róg",
|
||||
"bottom-right": "Prawy dolny róg",
|
||||
"center-left": "Środek po lewej",
|
||||
"center-right": "Środek po prawej",
|
||||
"top": "Góra",
|
||||
"top-left": "Lewy górny róg",
|
||||
"top-right": "Prawy górny róg"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Otwórz menu promieniowe",
|
||||
"cancel_progress": "Anulowanie bieżącego paska postępu",
|
||||
"txadmin_announcement": "Ogłoszenie serwerowe od %s",
|
||||
"txadmin_dm": "Prywatna wiadomość od %s",
|
||||
"txadmin_warn": "Otrzymano ostrzeżenie od %s",
|
||||
"txadmin_warn_content": "%s \nId akcji: %s",
|
||||
"txadmin_scheduledrestart": "Zaplanowany restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Português",
|
||||
"ui": {
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"more": "Mais...",
|
||||
"settings": {
|
||||
"locale": "Alterar idioma",
|
||||
"locale_description": "Idioma atual: ${language} (%s)",
|
||||
"notification_audio": "Áudio de notificação",
|
||||
"notification_position": "Posição da notificação"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Inferior",
|
||||
"bottom-left": "Inferior esquerdo",
|
||||
"bottom-right": "Inferior direito",
|
||||
"center-left": "Centro-esquerdo",
|
||||
"center-right": "Centro-direito",
|
||||
"top": "Superior",
|
||||
"top-left": "Superior esquerdo",
|
||||
"top-right": "Superior direito"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Abrir menu radial",
|
||||
"cancel_progress": "Cancelar barra de progresso atual",
|
||||
"txadmin_announcement": "Anúncio por %s",
|
||||
"txadmin_dm": "Mensagem de %s",
|
||||
"txadmin_warn": "Você foi alertado por %s",
|
||||
"txadmin_warn_content": "%s \nID do aviso: %s",
|
||||
"txadmin_scheduledrestart": "Reinício agendado"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Português",
|
||||
"ui": {
|
||||
"cancel": "Cancelar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"more": "Mais...",
|
||||
"settings": {
|
||||
"locale": "Mudar idioma",
|
||||
"locale_description": "Idioma atual: ${language} (%s)",
|
||||
"notification_audio": "Áudio de notificações",
|
||||
"notification_position": "Posição das notificações"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Em baixo",
|
||||
"bottom-left": "Em baixo à esquerda",
|
||||
"bottom-right": "Em baixo à direita",
|
||||
"center-left": "Centro-esquerda",
|
||||
"center-right": "Centro-direita",
|
||||
"top": "Em cima",
|
||||
"top-left": "Em cima à esquerda",
|
||||
"top-right": "Em cima à direita"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Abrir menu radial",
|
||||
"cancel_progress": "Cancelar barra de progresso atual",
|
||||
"txadmin_announcement": "Anúncio do servidor por %s",
|
||||
"txadmin_dm": "Mensagem direta de %s",
|
||||
"txadmin_warn": "Foste avisado por %s",
|
||||
"txadmin_warn_content": "%s \nID da ação: %s",
|
||||
"txadmin_scheduledrestart": "Reinício agendado"
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
{
|
||||
"language": "Romana",
|
||||
"settings": "Setari",
|
||||
"ui": {
|
||||
"cancel": "Anuleaza",
|
||||
"close": "Inchide",
|
||||
"confirm": "Confirma",
|
||||
"more": "Mai multe...",
|
||||
"settings": {
|
||||
"locale": "Schimba limba",
|
||||
"locale_description": "Limba actuala: ${language} (%s)",
|
||||
"notification_audio": "Audio notificari",
|
||||
"notification_position": "Pozitie notificari"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Jos",
|
||||
"bottom-left": "Jos-stanga",
|
||||
"bottom-right": "Jos-dreapta",
|
||||
"center-left": "Centru-stanga",
|
||||
"center-right": "Centru-dreapta",
|
||||
"top": "Sus",
|
||||
"top-left": "Sus-stanga",
|
||||
"top-right": "Sus-dreapta"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Deschide meniul radial",
|
||||
"cancel_progress": "Anuleaza bara de progres actuala",
|
||||
"txadmin_announcement": "Anunt de server dat de %s",
|
||||
"txadmin_dm": "Mesaj Direct de la %s",
|
||||
"txadmin_warn": "Ai fost avertizat de %s",
|
||||
"txadmin_warn_content": "%s \nID Actiune: %s",
|
||||
"txadmin_scheduledrestart": "Restart Programat"
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Русский",
|
||||
"ui": {
|
||||
"cancel": "Отменить",
|
||||
"close": "Закрыть",
|
||||
"confirm": "Подтвердить",
|
||||
"more": "Ещё...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Slovenčina",
|
||||
"ui": {
|
||||
"cancel": "Zrušiť",
|
||||
"close": "Zavrieť",
|
||||
"confirm": "Potvrdiť",
|
||||
"more": "More...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Slovenski",
|
||||
"ui": {
|
||||
"cancel": "Cancel",
|
||||
"close": "Zapri",
|
||||
"confirm": "Potrdi",
|
||||
"more": "More...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Svenska",
|
||||
"ui": {
|
||||
"cancel": "Avbryt",
|
||||
"close": "Stäng",
|
||||
"confirm": "Acceptera",
|
||||
"more": "Mer...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"language": "Thailand",
|
||||
"ui": {
|
||||
"cancel": "ยกเลิก",
|
||||
"close": "ปิด",
|
||||
"confirm": "ยืนยัน",
|
||||
"more": "เพิ่มเติม...",
|
||||
"settings": {
|
||||
"locale": "Change locale",
|
||||
"locale_description": "Current language: ${language} (%s)",
|
||||
"notification_audio": "Notification audio",
|
||||
"notification_position": "Notification position"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Bottom",
|
||||
"bottom-left": "Bottom-left",
|
||||
"bottom-right": "Bottom-right",
|
||||
"center-left": "Center-left",
|
||||
"center-right": "Center-right",
|
||||
"top": "Top",
|
||||
"top-left": "Top-left",
|
||||
"top-right": "Top-right"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Open radial menu",
|
||||
"cancel_progress": "Cancel current progress bar",
|
||||
"txadmin_announcement": "Server announcement by %s",
|
||||
"txadmin_dm": "Direct Message from %s",
|
||||
"txadmin_warn": "You have been warned by %s",
|
||||
"txadmin_warn_content": "%s \nAction ID: %s",
|
||||
"txadmin_scheduledrestart": "Scheduled Restart"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "Türkçe",
|
||||
"settings": "Ayarlar",
|
||||
"ui": {
|
||||
"cancel": "İptal",
|
||||
"close": "Kapat",
|
||||
"confirm": "Onayla",
|
||||
"more": "Daha Fazla...",
|
||||
"settings": {
|
||||
"locale": "Dili değiştir",
|
||||
"locale_description": "Mevcut dil: ${language} (%s)",
|
||||
"notification_audio": "Bildirim Sesi",
|
||||
"notification_position": "Bildirim Pozisyonu"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "Alt",
|
||||
"bottom-left": "Alt-sol",
|
||||
"bottom-right": "Alt-sağ",
|
||||
"center-left": "Merkez-sol",
|
||||
"center-right": "Merkez-sağ",
|
||||
"top": "Üst",
|
||||
"top-left": "Üst-sol",
|
||||
"top-right": "Üst-sağ"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "Radyal menüyü aç",
|
||||
"cancel_progress": "Mevcut ilerleme çubuğunu iptal et",
|
||||
"txadmin_announcement": "%s tarafından sunucu duyurusu",
|
||||
"txadmin_dm": "%s tarafından direkt mesaj",
|
||||
"txadmin_warn": "%s tarafından uyarıldınız",
|
||||
"txadmin_warn_content": "%s \nİşlem Kimliği: %s",
|
||||
"txadmin_scheduledrestart": "Planlı Yeniden Başlatma"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "简体中文",
|
||||
"settings": "设置",
|
||||
"ui": {
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"more": "更多...",
|
||||
"settings": {
|
||||
"locale": "更改语言",
|
||||
"locale_description": "当前语言: ${language} (%s)",
|
||||
"notification_audio": "通知提示音",
|
||||
"notification_position": "通知位置"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "底部",
|
||||
"bottom-left": "左下",
|
||||
"bottom-right": "右下",
|
||||
"center-left": "左侧居中",
|
||||
"center-right": "右侧居中",
|
||||
"top": "顶部",
|
||||
"top-left": "左上",
|
||||
"top-right": "右上"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "打开轮盘菜单",
|
||||
"cancel_progress": "取消当前进度条",
|
||||
"txadmin_announcement": "来自 %s 的服务器公告",
|
||||
"txadmin_dm": "来自 %s 的信息",
|
||||
"txadmin_warn": "您被 %s 警告了",
|
||||
"txadmin_warn_content": "%s \n操作 ID: %s",
|
||||
"txadmin_scheduledrestart": "计划内重启"
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"language": "繁體中文",
|
||||
"settings": "設置",
|
||||
"ui": {
|
||||
"cancel": "取消",
|
||||
"close": "關閉",
|
||||
"confirm": "確認",
|
||||
"more": "更多...",
|
||||
"settings": {
|
||||
"locale": "更改語言",
|
||||
"locale_description": "當前語言: ${language} (%s)",
|
||||
"notification_audio": "通知提示音",
|
||||
"notification_position": "通知位置"
|
||||
},
|
||||
"position": {
|
||||
"bottom": "底部",
|
||||
"bottom-left": "左下",
|
||||
"bottom-right": "右下",
|
||||
"center-left": "左側居中",
|
||||
"center-right": "右側居中",
|
||||
"top": "頂部",
|
||||
"top-left": "左上",
|
||||
"top-right": "右上"
|
||||
}
|
||||
},
|
||||
"open_radial_menu": "打開輪盤菜單",
|
||||
"cancel_progress": "取消當前進度條",
|
||||
"txadmin_announcement": "來自 %s 的伺服器通告",
|
||||
"txadmin_dm": "來自 %s 的訊息",
|
||||
"txadmin_warn": "您被 %s 警告了",
|
||||
"txadmin_warn_content": "%s \n操作 ID: %s",
|
||||
"txadmin_scheduledrestart": "計劃內重啟"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
!cache
|
||||
@@ -1,51 +0,0 @@
|
||||
--[[
|
||||
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 function allowAce(allow)
|
||||
return allow == false and 'deny' or 'allow'
|
||||
end
|
||||
|
||||
-- Adds the ace to the principal.
|
||||
function lib.addAce(principal, ace, allow)
|
||||
if type(principal) == 'number' then
|
||||
principal = 'player.'..principal
|
||||
end
|
||||
|
||||
ExecuteCommand(('add_ace %s %s %s'):format(principal, ace, allowAce(allow)))
|
||||
end
|
||||
|
||||
-- Removes the ace from the principal.
|
||||
function lib.removeAce(principal, ace, allow)
|
||||
if type(principal) == 'number' then
|
||||
principal = 'player.'..principal
|
||||
end
|
||||
|
||||
ExecuteCommand(('remove_ace %s %s %s'):format(principal, ace, allowAce(allow)))
|
||||
end
|
||||
|
||||
-- Adds the child principal to the parent principal.
|
||||
function lib.addPrincipal(child, parent)
|
||||
if type(child) == 'number' then
|
||||
child = 'player.'..child
|
||||
end
|
||||
|
||||
ExecuteCommand(('add_principal %s %s'):format(child, parent))
|
||||
end
|
||||
|
||||
-- Removes the child principal from the parent principal.
|
||||
function lib.removePrincipal(child, parent)
|
||||
if type(child) == 'number' then
|
||||
child = 'player.'..child
|
||||
end
|
||||
|
||||
ExecuteCommand(('remove_principal %s %s'):format(child, parent))
|
||||
end
|
||||
|
||||
lib.callback.register('ox_lib:checkPlayerAce', function(source, command)
|
||||
return IsPlayerAceAllowed(source, command)
|
||||
end)
|
||||
@@ -1,75 +0,0 @@
|
||||
--[[
|
||||
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 cache = _ENV.cache
|
||||
cache.playerId = PlayerId()
|
||||
cache.serverId = GetPlayerServerId(cache.playerId)
|
||||
|
||||
function cache:set(key, value)
|
||||
if value ~= self[key] then
|
||||
TriggerEvent(('ox_lib:cache:%s'):format(key), value, self[key])
|
||||
self[key] = value
|
||||
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local GetVehiclePedIsIn = GetVehiclePedIsIn
|
||||
local GetPedInVehicleSeat = GetPedInVehicleSeat
|
||||
local GetVehicleMaxNumberOfPassengers = GetVehicleMaxNumberOfPassengers
|
||||
local GetMount = GetMount
|
||||
local IsPedOnMount = IsPedOnMount
|
||||
local GetCurrentPedWeapon = GetCurrentPedWeapon
|
||||
|
||||
CreateThread(function()
|
||||
while true do
|
||||
local ped = PlayerPedId()
|
||||
|
||||
-- If the player's ped is changed, the ped value may return 0 during the transition. If the value is 0, skip all checks.
|
||||
if ped ~= 0 then
|
||||
cache:set('ped', ped)
|
||||
|
||||
local vehicle = GetVehiclePedIsIn(ped, false)
|
||||
|
||||
if vehicle > 0 then
|
||||
if vehicle ~= cache.vehicle then
|
||||
cache:set('seat', false)
|
||||
end
|
||||
|
||||
cache:set('vehicle', vehicle)
|
||||
|
||||
if not cache.seat or GetPedInVehicleSeat(vehicle, cache.seat) ~= ped then
|
||||
for i = -1, GetVehicleMaxNumberOfPassengers(vehicle) - 1 do
|
||||
if GetPedInVehicleSeat(vehicle, i) == ped then
|
||||
cache:set('seat', i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
cache:set('vehicle', false)
|
||||
cache:set('seat', false)
|
||||
end
|
||||
|
||||
if cache.game == 'redm' then
|
||||
local mount = GetMount(ped)
|
||||
local onMount = IsPedOnMount(ped)
|
||||
cache:set('mount', onMount and mount or false)
|
||||
end
|
||||
|
||||
local hasWeapon, currentWeapon = GetCurrentPedWeapon(ped, true)
|
||||
cache:set('weapon', (hasWeapon and currentWeapon ~= 0) and currentWeapon or false)
|
||||
end
|
||||
|
||||
Wait(100)
|
||||
end
|
||||
end)
|
||||
|
||||
function lib.cache(key)
|
||||
return cache[key]
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
--[[
|
||||
https://github.com/overextended/ox_lib
|
||||
|
||||
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
|
||||
|
||||
Copyright © 2025 Linden <https://github.com/thelindat>
|
||||
]]
|
||||
|
||||
local registeredCallbacks = {}
|
||||
|
||||
AddEventHandler('onResourceStop', function(resourceName)
|
||||
if cache.resource == resourceName then return end
|
||||
|
||||
for callbackName, resource in pairs(registeredCallbacks) do
|
||||
if resource == resourceName then
|
||||
registeredCallbacks[callbackName] = nil
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
---For internal use only.
|
||||
---Sets a callback event as registered to a specific resource, preventing it from
|
||||
---being overwritten. Any unknown callbacks will return an error to the caller.
|
||||
---@param callbackName string
|
||||
---@param isValid boolean
|
||||
function lib.setValidCallback(callbackName, isValid)
|
||||
local resourceName = GetInvokingResource() or cache.resource
|
||||
local callbackResource = registeredCallbacks[callbackName]
|
||||
|
||||
if callbackResource then
|
||||
if not isValid then
|
||||
callbackResource[callbackName] = nil
|
||||
return
|
||||
end
|
||||
|
||||
if callbackResource == resourceName then return end
|
||||
|
||||
local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource)
|
||||
|
||||
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage,
|
||||
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
|
||||
end
|
||||
|
||||
lib.print.verbose(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName))
|
||||
|
||||
registeredCallbacks[callbackName] = resourceName
|
||||
end
|
||||
|
||||
function lib.isCallbackValid(callbackName)
|
||||
return registeredCallbacks[callbackName] == GetInvokingResource() or cache.resource
|
||||
end
|
||||
|
||||
local cbEvent = '__ox_cb_%s'
|
||||
|
||||
RegisterNetEvent('ox_lib:validateCallback', function(callbackName, invokingResource, key)
|
||||
if registeredCallbacks[callbackName] then return end
|
||||
|
||||
local event = cbEvent:format(invokingResource)
|
||||
|
||||
if cache.game == 'fxserver' then
|
||||
return TriggerClientEvent(event, source, key, 'cb_invalid')
|
||||
end
|
||||
|
||||
TriggerServerEvent(event, key, 'cb_invalid')
|
||||
end)
|
||||
@@ -1,27 +0,0 @@
|
||||
--[[
|
||||
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 _registerCommand = RegisterCommand
|
||||
|
||||
---@param commandName string
|
||||
---@param callback fun(source, args, raw)
|
||||
---@param restricted boolean?
|
||||
function RegisterCommand(commandName, callback, restricted)
|
||||
_registerCommand(commandName, function(source, args, raw)
|
||||
if not restricted or lib.callback.await('ox_lib:checkPlayerAce', 100, ('command.%s'):format(commandName)) then
|
||||
callback(source, args, raw)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
RegisterNUICallback('getConfig', function(_, cb)
|
||||
cb({
|
||||
primaryColor = GetConvar('ox:primaryColor', 'blue'),
|
||||
primaryShade = GetConvarInt('ox:primaryShade', 8)
|
||||
})
|
||||
end)
|
||||
@@ -1,55 +0,0 @@
|
||||
local debug_getinfo = debug.getinfo
|
||||
|
||||
function noop() end
|
||||
|
||||
lib = setmetatable({
|
||||
name = 'ox_lib',
|
||||
context = IsDuplicityVersion() and 'server' or 'client',
|
||||
}, {
|
||||
__newindex = function(self, key, fn)
|
||||
rawset(self, key, fn)
|
||||
|
||||
if debug_getinfo(2, 'S').short_src:find('@ox_lib/resource') then
|
||||
exports(key, fn)
|
||||
end
|
||||
end,
|
||||
|
||||
__index = function(self, key)
|
||||
local dir = ('imports/%s'):format(key)
|
||||
local chunk = LoadResourceFile(self.name, ('%s/%s.lua'):format(dir, self.context))
|
||||
local shared = LoadResourceFile(self.name, ('%s/shared.lua'):format(dir))
|
||||
|
||||
if shared then
|
||||
chunk = (chunk and ('%s\n%s'):format(shared, chunk)) or shared
|
||||
end
|
||||
|
||||
if chunk then
|
||||
local fn, err = load(chunk, ('@@ox_lib/%s/%s.lua'):format(key, self.context))
|
||||
|
||||
if not fn or err then
|
||||
return error(('\n^1Error importing module (%s): %s^0'):format(dir, err), 3)
|
||||
end
|
||||
|
||||
rawset(self, key, fn() or noop)
|
||||
|
||||
return self[key]
|
||||
end
|
||||
end
|
||||
})
|
||||
|
||||
cache = {
|
||||
resource = lib.name,
|
||||
game = GetGameName(),
|
||||
}
|
||||
|
||||
if not LoadResourceFile(lib.name, 'web/build/index.html') then
|
||||
local err =
|
||||
'^1Unable to load UI. Build ox_lib or download the latest release.\n ^3https://github.com/communityox/ox_lib/releases/latest/download/ox_lib.zip^0'
|
||||
function lib.hasLoaded() return err end
|
||||
|
||||
error(err)
|
||||
end
|
||||
|
||||
function lib.hasLoaded() return true end
|
||||
|
||||
require = lib.require
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user