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:
2026-04-03 02:47:59 +03:00
parent 06414ed181
commit e756e29294
1539 changed files with 51926 additions and 39806 deletions
@@ -0,0 +1,17 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
end_of_line = lf
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = false
indent_size = 2
indent_style = space
max_line_length = 120
[*.lua]
indent_size = 4
max_line_length = 160
@@ -0,0 +1 @@
ko_fi: thelindat
@@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve
title: "[Bug] "
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Additional context**
Add any other context about the problem here.
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Suggestion] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,10 @@
const fs = require('fs')
const version = process.env.TGT_RELEASE_VERSION
const newVersion = version.replace('v', '')
const manifestFile = fs.readFileSync('fxmanifest.lua', {encoding: 'utf8'})
const newFileContent = manifestFile.replace(/\bversion\s+(.*)$/gm, `version '${newVersion}'`)
fs.writeFileSync('fxmanifest.lua', newFileContent)
@@ -0,0 +1,100 @@
name: Release
on:
push:
tags:
- 'v*.*.*'
jobs:
create-release:
name: Build and Create Tagged Release
runs-on: ubuntu-latest
steps:
- name: Install archive tools
run: sudo apt install zip
- name: Checkout source code
uses: actions/checkout@v2
with:
fetch-depth: 0
ref: ${{ github.event.repository.default_branch }}
- uses: pnpm/action-setup@v2.0.1
with:
version: 8.6.1
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: 'pnpm'
cache-dependency-path: 'web/pnpm-lock.yaml'
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
- name: Install dependencies
run: pnpm install
working-directory: web
- name: Install package dependencies
run: pnpm install
working-directory: package
- name: Bump package version
run: pnpm version ${{ github.ref_name }}
working-directory: package
- name: Run build
run: pnpm build
working-directory: web
env:
CI: false
- name: Bump manifest version
run: node .github/actions/bump-manifest-version.js
env:
TGT_RELEASE_VERSION: ${{ github.ref_name }}
- name: Push manifest change
uses: EndBug/add-and-commit@v8
with:
add: fxmanifest.lua
push: true
author_name: Manifest Bumper
author_email: 41898282+github-actions[bot]@users.noreply.github.com
message: 'chore: bump manifest version to ${{ github.ref_name }}'
- name: Update tag ref
uses: EndBug/latest-tag@latest
with:
tag-name: ${{ github.ref_name }}
- name: Bundle files
run: |
mkdir -p ./temp/ox_lib
mkdir -p ./temp/ox_lib/web/
cp ./{LICENSE,README.md,fxmanifest.lua,init.lua} ./temp/ox_lib
cp -r ./{imports,resource,locales} ./temp/ox_lib
cp -r ./web/build ./temp/ox_lib/web/
cd ./temp && zip -r ../ox_lib.zip ./ox_lib
- name: Create Release
uses: 'marvinpinto/action-automatic-releases@v1.2.1'
id: auto_release
with:
repo_token: '${{ secrets.GITHUB_TOKEN }}'
title: ${{ env.RELEASE_VERSION }}
prerelease: false
files: ox_lib.zip
env:
CI: false
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish npm package
uses: JS-DevTools/npm-publish@v1
with:
token: ${{ secrets.NPM_TOKEN }}
package: './package/package.json'
access: 'public'
@@ -0,0 +1,12 @@
.idea
created_zones.lua
node_modules
pnpm-debug.log*
/package/**/*.js
/package/**/*.js.map
/package/**/*.d.ts
/web/node_modules
/web/build
@@ -0,0 +1,74 @@
# Contributing to our projects
We welcome you to contribute to our projects, whether reporting a bug, suggesting an improvement, or submitting fixes, improvements, or new code. To ensure a smooth collaboration process please follow these guidelines.
## Found a bug?
### Search for existing issues
Before creating a new issue, please search existing [issues](https://github.com/overextended/ox_lib/issues) to see if it has already been reported.
### Add details to existing issues
If you have found an **active** issue which matches your own, please provide any additional details or context that might help us to resolve it.
### Create a new issue
If there is no **open** issue addresing your bug, please create a new issue - you can also reference a **closed** issue if you are certain they are related. Make sure to:
- Use the bug report template when creating an issue.
- Include a descriptive title and a clear and detailed description of the problem.
- Provide simple steps to reproduce the bug.
- Include all relevant code samples.
- Provide any additional information that could help diagnose the problem.
## Patched a bug?
- Submit a new pull request that includes **only the changes related to the fix**.
- Clearly explain the problem being addressed and how your solution fixes it.
- Reference any open issues or pull requests that your changes will resolve.
## Suggesting improvements or new features
Not all suggested improvements or features will be accepted.
Changes may be declined if they do not align with our design philosophy, are incomplete, not well-planned, or introduce unnecessary complexity or breaking changes to the codebase.
### Open an issue
Open a new issue to discuss your suggestion, and wait for feedback from the community and maintainers.
### Submit a draft
If you have started working on your idea, feel free to submit a **draft pull request** to gather initial feedback.
### Cosmetic changes
Pull requests that do not contribute meaningful improvements to the project's stability or functionality, such as changes to formatting or unused variables, will not be accepted.
## How to submit a pull request
- Fork the repository and, optionally, create a new branch for your changes.
- If applicable, include example code to demonstrate your changes.
- Ensure your code's style is consistent with the project, e.g. uses the same indentation and string quotations.
- If you have modified or introduced new APIs, open a pull request to our [documentation](https://github.com/overextended/overextended.github.io). We will not accept undocumented code.
## Contributor License Agreement
This Contributor License Agreement ("Agreement") is entered into between the contributor ("Contributor") and Overextended ("Owner") for the purpose of contributing to this repository (the "Project").
By submitting any code, documentation, or other materials (collectively, the "Contribution") to this project, the Contributor agrees to the following terms:
1. **License Grant**
Subject to the Contributor retaining all ownership rights in their Contribution, the Contributor hereby grants the Owner a perpetual, irrevocable, worldwide, non-exclusive license to:
- Use, reproduce, modify, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the Contribution,
- Incorporate the Contribution into the Project or any derivative works thereof, **under the terms of the Project's License**.
2. **Representation and Warranties**
The Contributor represents and warrants that:
- The Contribution is original and is the Contributor's own work.
- The Contribution does not violate any third-party rights, including copyright, patent, trademark, or trade secret rights.
- The Contributor has full power and authority to enter into this Agreement and grant the rights herein.
By submitting a Contribution to the Project, the Contributor acknowledges that they have read, understood, and agree to be bound by the terms of this Agreement.
+165
View File
@@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.
@@ -0,0 +1,23 @@
## Copyright
Copyright © 2025 Overextended
Use of this code is subject to the terms and conditions of the license, as stated below.
## Licensing and Attribution
This project is licensed under the [LGPL3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html) or any later version.
A complete copy of the license is included in the [LICENSE](./LICENSE) file.
When incorporating this work into your own project, you must:
- Clearly credit the original authors and provide a link to the original project.
- Document any modifications made to the original work.
- Include the full text of the LGPL3.0 license with your distribution.
- Ensure that your project remains available under the LGPL3.0 or a compatible open-source license.
- Preserve all existing copyright and licensing notices.
## Source Code
The original source code is available at: [https://github.com/overextended/ox_lib](https://github.com/overextended/ox_lib)
@@ -0,0 +1,32 @@
# ox_lib
A FiveM library and resource implementing reusable modules, methods, and UI elements.
![](https://img.shields.io/github/downloads/communityox/ox_lib/total?logo=github)
![](https://img.shields.io/github/downloads/communityox/ox_lib/latest/total?logo=github)
![](https://img.shields.io/github/contributors/communityox/ox_lib?logo=github)
![](https://img.shields.io/github/v/release/communityox/ox_lib?logo=github)
For guidelines to contributing to the project, and to see our Contributor License Agreement, see [CONTRIBUTING.md](./CONTRIBUTING.md)
For additional legal notices, refer to [NOTICE.md](./NOTICE.md).
## 📚 Documentation
https://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"
@@ -0,0 +1,48 @@
fx_version 'cerulean'
use_experimental_fxv2_oal 'yes'
lua54 'yes'
games { 'rdr3', 'gta5' }
rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aware my resources *will* become incompatible once RedM ships.'
name 'ox_lib'
author 'Overextended'
version '3.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',
}
@@ -0,0 +1 @@
!cache
@@ -0,0 +1,118 @@
-- DO NOT USE! Old syntax for addCommand (prior to v3.0)
---@todo convert input and call standard function?
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local commands = {}
SetTimeout(1000, function()
TriggerClientEvent('chat:addSuggestions', -1, commands)
end)
AddEventHandler('playerJoining', function()
TriggerClientEvent('chat:addSuggestions', source, commands)
end)
local function chatSuggestion(name, parameters, help)
local params = {}
if parameters then
for i = 1, #parameters do
local arg, argType = string.strsplit(':', parameters[i])
if argType and argType:sub(0, 1) == '?' then
argType = argType:sub(2, #argType)
end
params[i] = {
name = arg,
help = argType
}
end
end
commands[#commands + 1] = {
name = '/' .. name,
help = help,
params = params
}
end
---@deprecated
---@param group string | string[] | false
---@param name string | string[]
---@param callback function
---@param parameters table
function lib.__addCommand(group, name, callback, parameters, help)
if not group then group = 'builtin.everyone' end
if type(name) == 'table' then
for i = 1, #name do
---@diagnostic disable-next-line: deprecated
lib.__addCommand(group, name[i], callback, parameters, help)
end
else
chatSuggestion(name, parameters, help)
RegisterCommand(name, function(source, args, raw)
source = tonumber(source) --[[@as number]]
if parameters then
for i = 1, #parameters do
local arg, argType = string.strsplit(':', parameters[i])
local value = args[i]
if arg == 'target' and value == 'me' then value = source end
if argType then
local optional
if argType:sub(0, 1) == '?' then
argType = argType:sub(2, #argType)
optional = true
end
if argType == 'number' then
value = tonumber(value) or value
end
local type = type(value)
if type ~= argType and (not optional or type ~= 'nil') then
local invalid = ('^1%s expected <%s> for argument %s (%s), received %s^0'):format(name,
argType, i, arg, type)
if source < 1 then
return print(invalid)
else
return TriggerClientEvent('chat:addMessage', source, invalid)
end
end
end
args[arg] = value
args[i] = nil
end
end
callback(source, args, raw)
end, group and true)
name = ('command.%s'):format(name)
if type(group) == 'table' then
for _, v in ipairs(group) do
if not IsPrincipalAceAllowed(v, name) then lib.addAce(v, name) end
end
else
if not IsPrincipalAceAllowed(group, name) then lib.addAce(group, name) end
end
end
end
---@diagnostic disable-next-line: deprecated
return lib.__addCommand
@@ -0,0 +1,164 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@class OxCommandParams
---@field name string
---@field help? string
---@field type? 'number' | 'playerId' | 'string' | 'longString'
---@field optional? boolean
---@class OxCommandProperties
---@field help string?
---@field params OxCommandParams[]?
---@field restricted boolean | string | string[]?
---@type OxCommandProperties[]
local registeredCommands = {}
local shouldSendCommands = false
SetTimeout(1000, function()
shouldSendCommands = true
TriggerClientEvent('chat:addSuggestions', -1, registeredCommands)
end)
AddEventHandler('playerJoining', function()
TriggerClientEvent('chat:addSuggestions', source, registeredCommands)
end)
---@param source number
---@param args table
---@param raw string
---@param params OxCommandParams[]?
---@return table?
local function parseArguments(source, args, raw, params)
if not params then return args end
local paramsNum = #params
for i = 1, paramsNum do
local arg, param = args[i], params[i]
local value
if param.type == 'number' then
value = tonumber(arg)
elseif param.type == 'string' then
value = not tonumber(arg) and arg
elseif param.type == 'playerId' then
value = arg == 'me' and source or tonumber(arg)
if not value or not DoesPlayerExist(value--[[@as string]]) then
value = false
end
elseif param.type == 'longString' and i == paramsNum then
if arg then
local start = raw:find(arg, 1, true)
value = start and raw:sub(start)
else
value = nil
end
else
value = arg
end
if not value and (not param.optional or param.optional and arg) then
return Citizen.Trace(("^1command '%s' received an invalid %s for argument %s (%s), received '%s'^0\n"):format(string.strsplit(' ', raw) or raw, param.type, i, param.name, arg))
end
arg = value
args[param.name] = arg
args[i] = nil
end
return args
end
---@param commandName string | string[]
---@param properties OxCommandProperties | false
---@param cb fun(source: number, args: table, raw: string)
---@param ... any
function lib.addCommand(commandName, properties, cb, ...)
-- Try to handle backwards-compatibility with the old addCommand syntax (prior to v3.0)
local restricted, params
if properties then
if ... or table.type(properties) ~= 'hash' then
local _commandName = type(properties) == 'table' and properties[1] or properties
local info = debug.getinfo(2, 'Sl')
warn(("command '%s' is using deprecated syntax for lib.addCommand\nupdate the command or use lib.__addCommand to ignore this warning\n> source ^0(^5%s^0:%d)"):format(_commandName, info.short_src, info.currentline))
---@diagnostic disable-next-line: deprecated
return lib.__addCommand(commandName, properties, cb, ...)
end
restricted = properties.restricted
params = properties.params
end
if params then
for i = 1, #params do
local param = params[i]
if param.type then
param.help = param.help and ('%s (type: %s)'):format(param.help, param.type) or ('(type: %s)'):format(param.type)
end
end
end
local commands = type(commandName) ~= 'table' and { commandName } or commandName
local numCommands = #commands
local totalCommands = #registeredCommands
local function commandHandler(source, args, raw)
args = parseArguments(source, args, raw, params)
if not args then return end
local success, resp = pcall(cb, source, args, raw)
if not success then
Citizen.Trace(("^1command '%s' failed to execute!\n%s"):format(string.strsplit(' ', raw) or raw, resp))
end
end
for i = 1, numCommands do
totalCommands += 1
commandName = commands[i]
RegisterCommand(commandName, commandHandler, restricted and true)
if restricted then
local ace = ('command.%s'):format(commandName)
local restrictedType = type(restricted)
if restrictedType == 'string' and not IsPrincipalAceAllowed(restricted, ace) then
lib.addAce(restricted, ace)
elseif restrictedType == 'table' then
for j = 1, #restricted do
if not IsPrincipalAceAllowed(restricted[j], ace) then
lib.addAce(restricted[j], ace)
end
end
end
end
if properties then
---@diagnostic disable-next-line: inject-field
properties.name = ('/%s'):format(commandName)
properties.restricted = nil
registeredCommands[totalCommands] = properties
if i ~= numCommands and numCommands ~= 1 then
properties = table.clone(properties)
end
if shouldSendCommands then TriggerClientEvent('chat:addSuggestions', -1, properties) end
end
end
end
return lib.addCommand
@@ -0,0 +1,91 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
if cache.game == 'redm' then return end
---@class KeybindProps
---@field name string
---@field description string
---@field defaultMapper? string (see: https://docs.fivem.net/docs/game-references/input-mapper-parameter-ids/)
---@field defaultKey? string
---@field disabled? boolean
---@field disable? fun(self: CKeybind, toggle: boolean)
---@field onPressed? fun(self: CKeybind)
---@field onReleased? fun(self: CKeybind)
---@field [string] any
---@class CKeybind : KeybindProps
---@field currentKey string
---@field disabled boolean
---@field isPressed boolean
---@field hash number
---@field getCurrentKey fun(): string
---@field isControlPressed fun(): boolean
local keybinds = {}
local IsPauseMenuActive = IsPauseMenuActive
local GetControlInstructionalButton = GetControlInstructionalButton
local keybind_mt = {
disabled = false,
isPressed = false,
defaultKey = '',
defaultMapper = 'keyboard',
}
function keybind_mt:__index(index)
return index == 'currentKey' and self:getCurrentKey() or keybind_mt[index]
end
function keybind_mt:getCurrentKey()
return GetControlInstructionalButton(0, self.hash, true):sub(3)
end
function keybind_mt:isControlPressed()
return self.isPressed
end
function keybind_mt:disable(toggle)
self.disabled = toggle
end
---@param data KeybindProps
---@return CKeybind
function lib.addKeybind(data)
---@cast data CKeybind
data.hash = joaat('+' .. data.name) | 0x80000000
keybinds[data.name] = setmetatable(data, keybind_mt)
RegisterCommand('+' .. data.name, function()
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = true
if data.onPressed then data:onPressed() end
end)
RegisterCommand('-' .. data.name, function()
if data.disabled or IsPauseMenuActive() then return end
data.isPressed = false
if data.onReleased then data:onReleased() end
end)
RegisterKeyMapping('+' .. data.name, data.description, data.defaultMapper, data.defaultKey)
if data.secondaryKey then
RegisterKeyMapping('~!+' .. data.name, data.description, data.secondaryMapper or data.defaultMapper, data.secondaryKey)
end
SetTimeout(500, function()
TriggerEvent('chat:removeSuggestion', ('/+%s'):format(data.name))
TriggerEvent('chat:removeSuggestion', ('/-%s'):format(data.name))
end)
return data
end
return lib.addKeybind
@@ -0,0 +1,363 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@class Array<T> : OxClass, { [number]: T }
lib.array = lib.class('Array')
local table_unpack = table.unpack
local table_remove = table.remove
local table_clone = table.clone
local table_concat = table.concat
local table_type = table.type
---@alias ArrayLike<T> Array | { [number]: T }
---@private
function lib.array:constructor(...)
local arr = { ... }
for i = 1, #arr do
self[i] = arr[i]
end
end
---@private
function lib.array:__newindex(index, value)
if type(index) ~= 'number' then error(("Cannot insert non-number index '%s' into an array."):format(index)) end
rawset(self, index, value)
end
---Creates a new array from an iteratable value.
---@param iter table | function | string
---@return Array
function lib.array:from(iter)
local iterType = type(iter)
if iterType == 'table' then
return lib.array:new(table_unpack(iter))
end
if iterType == 'string' then
return lib.array:new(string.strsplit('', iter))
end
if iterType == 'function' then
local arr = lib.array:new()
local length = 0
for value in iter do
length += 1
arr[length] = value
end
return arr
end
error(('Array.from argument was not a valid iterable value (received %s)'):format(iterType))
end
---Returns the element at the given index, with negative numbers counting backwards from the end of the array.
---@param index number
---@return unknown
function lib.array:at(index)
if index < 0 then
index = #self + index + 1
end
return self[index]
end
---Create a new array containing the elements of two or more arrays.
---@param ... ArrayLike
function lib.array:merge(...)
local newArr = table_clone(self)
local length = #self
local arrays = { ... }
for i = 1, #arrays do
local arr = arrays[i]
for j = 1, #arr do
length += 1
newArr[length] = arr[j]
end
end
return lib.array:new(table_unpack(newArr))
end
---Tests if all elements in an array succeed in passing the provided test function.
---@param testFn fun(element: unknown): boolean
function lib.array:every(testFn)
for i = 1, #self do
if not testFn(self[i]) then
return false
end
end
return true
end
---Sets all elements within a range to the given value and returns the modified array.
---@param value any
---@param start? number
---@param endIndex? number
function lib.array:fill(value, start, endIndex)
local length = #self
start = start or 1
endIndex = endIndex or length
if start < 1 then start = 1 end
if endIndex > length then endIndex = length end
for i = start, endIndex do
self[i] = value
end
return self
end
---Creates a new array containing the elements from an array that pass the test of the provided function.
---@param testFn fun(element: unknown): boolean
function lib.array:filter(testFn)
local newArr = {}
local length = 0
for i = 1, #self do
local element = self[i]
if testFn(element) then
length += 1
newArr[length] = element
end
end
return lib.array:new(table_unpack(newArr))
end
---Returns the first or last element of an array that passes the provided test function.
---@param testFn fun(element: unknown): boolean
---@param last? boolean
function lib.array:find(testFn, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if testFn(element) then
return element
end
end
end
---Returns the first or last index of the first element of an array that passes the provided test function.
---@param testFn fun(element: unknown): boolean
---@param last? boolean
function lib.array:findIndex(testFn, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if testFn(element) then
return i
end
end
end
---Returns the first or last index of the first element of an array that matches the provided value.
---@param value unknown
---@param last? boolean
function lib.array:indexOf(value, last)
local a = last and #self or 1
local b = last and 1 or #self
local c = last and -1 or 1
for i = a, b, c do
local element = self[i]
if element == value then
return i
end
end
end
---Executes the provided function for each element in an array.
---@param cb fun(element: unknown)
function lib.array:forEach(cb)
for i = 1, #self do
cb(self[i])
end
end
---Determines if a given element exists inside an array.
---@param element unknown The value to find in the array.
---@param fromIndex? number The position in the array to begin searching from.
function lib.array:includes(element, fromIndex)
for i = (fromIndex or 1), #self do
if self[i] == element then return true end
end
return false
end
---Concatenates all array elements into a string, seperated by commas or the specified seperator.
---@param seperator? string
function lib.array:join(seperator)
return table_concat(self, seperator or ',')
end
---Create a new array containing the results from calling the provided function on every element in an array.
---@param cb fun(element: unknown, index: number, array: self): unknown
function lib.array:map(cb)
local arr = {}
for i = 1, #self do
arr[i] = cb(self[i], i, self)
end
return lib.array:new(table_unpack(arr))
end
---Removes the last element from an array and returns the removed element.
function lib.array:pop()
return table_remove(self)
end
---Adds the given elements to the end of an array and returns the new array length.
---@param ... any
function lib.array:push(...)
local elements = { ... }
local length = #self
for i = 1, #elements do
length += 1
self[length] = elements[i]
end
return length
end
---The "reducer" function is applied to every element within an array, with the previous element's result serving as the accumulator.
---If an initial value is provided, it's used as the accumulator for index 1; otherwise, index 1 itself serves as the initial value, and iteration begins from index 2.
---@generic T
---@param reducer fun(accumulator: T, currentValue: T, index?: number): T
---@param initialValue? T
---@param reverse? boolean Iterate over the array from right-to-left.
---@return T
function lib.array:reduce(reducer, initialValue, reverse)
local length = #self
local initialIndex = initialValue and 1 or 2
local accumulator = initialValue or self[1]
if reverse then
for i = initialIndex, length do
local index = length - i + initialIndex
accumulator = reducer(accumulator, self[index], index)
end
else
for i = initialIndex, length do
accumulator = reducer(accumulator, self[i], i)
end
end
return accumulator
end
---Reverses the elements inside an array.
function lib.array:reverse()
local i, j = 1, #self
while i < j do
self[i], self[j] = self[j], self[i]
i += 1
j -= 1
end
return self
end
---Removes the first element from an array and returns the removed element.
function lib.array:shift()
return table_remove(self, 1)
end
---Creates a shallow copy of a portion of an array as a new array.
---@param start? number
---@param finish? number
function lib.array:slice(start, finish)
local length = #self
start = start or 1
finish = finish or length
if start < 0 then start = length + start + 1 end
if finish < 0 then finish = length + finish + 1 end
if start < 1 then start = 1 end
if finish > length then finish = length end
local arr = lib.array:new()
local index = 0
for i = start, finish do
index += 1
arr[index] = self[i]
end
return arr
end
---Creates a new array with reversed elements from the given array.
function lib.array:toReversed()
local reversed = lib.array:new()
for i = #self, 1, -1 do
reversed:push(self[i])
end
return reversed
end
---Inserts the given elements to the start of an array and returns the new array length.
---@param ... any
function lib.array:unshift(...)
local elements = { ... }
local length = #self
local eLength = #elements
for i = length, 1, -1 do
self[i + eLength] = self[i]
end
for i = 1, #elements do
self[i] = elements[i]
end
return length + eLength
end
---Returns true if the given table is an instance of array or an array-like table.
---@param tbl ArrayLike
---@return boolean
function lib.array.isArray(tbl)
local tableType = table_type(tbl)
if not tableType then return false end
if tableType == 'array' or tableType == 'empty' or lib.array.instanceOf(tbl, lib.array) then
return true
end
return false
end
return lib.array
@@ -0,0 +1,145 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local pendingCallbacks = {}
local timers = {}
local cbEvent = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 300000)
RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...)
if source == '' then return end
local cb = pendingCallbacks[key]
if not cb then return end
pendingCallbacks[key] = nil
cb(...)
end)
---@param event string
---@param delay? number | false prevent the event from being called for the given time
local function eventTimer(event, delay)
if delay and type(delay) == 'number' and delay > 0 then
local time = GetGameTimer()
if (timers[event] or 0) > time then
return false
end
timers[event] = time + delay
end
return true
end
---@param _ any
---@param event string
---@param delay number | false | nil
---@param cb function | false
---@param ... any
---@return ...
local function triggerServerCallback(_, event, delay, cb, ...)
if not eventTimer(event, delay) then return end
local key
repeat
key = ('%s:%s'):format(event, math.random(0, 100000))
until not pendingCallbacks[key]
TriggerServerEvent('ox_lib:validateCallback', event, cache.resource, key)
TriggerServerEvent(cbEvent:format(event), cache.resource, key, ...)
---@type promise | false
local promise = not cb and promise.new()
pendingCallbacks[key] = function(response, ...)
if response == 'cb_invalid' then
response = ("callback '%s' does not exist"):format(event)
return promise and promise:reject(response) or error(response)
end
response = { response, ... }
if promise then
return promise:resolve(response)
end
if cb then
cb(table.unpack(response))
end
end
if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
return table.unpack(Citizen.Await(promise))
end
end
---@overload fun(event: string, delay: number | false, cb: function, ...)
lib.callback = setmetatable({}, {
__call = function(_, event, delay, cb, ...)
if not cb then
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse lib.callback.await or a regular event to remove this warning")
:format(event))
else
local cbType = type(cb)
if cbType == 'table' and getmetatable(cb)?.__call then
cbType = 'function'
end
assert(cbType == 'function', ("expected argument 3 to have type 'function' (received %s)"):format(cbType))
end
return triggerServerCallback(_, event, delay, cb, ...)
end
})
---@param event string
---@param delay? number | false prevent the event from being called for the given time.
---Sends an event to the server and halts the current thread until a response is returned.
---@diagnostic disable-next-line: duplicate-set-field
function lib.callback.await(event, delay, ...)
return triggerServerCallback(nil, event, delay, false, ...)
end
local function callbackResponse(success, result, ...)
if not success then
if result then
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return false
end
return result, ...
end
local pcall = pcall
---@param name string
---@param cb function
---Registers an event handler and callback function to respond to server requests.
---@diagnostic disable-next-line: duplicate-set-field
function lib.callback.register(name, cb)
event = cbEvent:format(name)
lib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
end
return lib.callback
@@ -0,0 +1,126 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local pendingCallbacks = {}
local cbEvent = '__ox_cb_%s'
local callbackTimeout = GetConvarInt('ox:callbackTimeout', 300000)
RegisterNetEvent(cbEvent:format(cache.resource), function(key, ...)
local cb = pendingCallbacks[key]
if not cb then return end
pendingCallbacks[key] = nil
cb(...)
end)
---@param _ any
---@param event string
---@param playerId number
---@param cb function|false
---@param ... any
---@return ...
local function triggerClientCallback(_, event, playerId, cb, ...)
assert(DoesPlayerExist(playerId --[[@as string]]), ("target playerId '%s' does not exist"):format(playerId))
local key
repeat
key = ('%s:%s:%s'):format(event, math.random(0, 100000), playerId)
until not pendingCallbacks[key]
TriggerClientEvent('ox_lib:validateCallback', playerId, event, cache.resource, key)
TriggerClientEvent(cbEvent:format(event), playerId, cache.resource, key, ...)
---@type promise | false
local promise = not cb and promise.new()
pendingCallbacks[key] = function(response, ...)
if response == 'cb_invalid' then
response = ("callback '%s' does not exist"):format(event)
return promise and promise:reject(response) or error(response)
end
response = { response, ... }
if promise then
return promise:resolve(response)
end
if cb then
cb(table.unpack(response))
end
end
if promise then
SetTimeout(callbackTimeout, function() promise:reject(("callback event '%s' timed out"):format(key)) end)
return table.unpack(Citizen.Await(promise))
end
end
---@overload fun(event: string, playerId: number, cb: function, ...)
lib.callback = setmetatable({}, {
__call = function(_, event, playerId, cb, ...)
if not cb then
warn(("callback event '%s' does not have a function to callback to and will instead await\nuse lib.callback.await or a regular event to remove this warning")
:format(event))
else
local cbType = type(cb)
if cbType == 'table' and getmetatable(cb)?.__call then
cbType = 'function'
end
assert(cbType == 'function', ("expected argument 3 to have type 'function' (received %s)"):format(cbType))
end
return triggerClientCallback(_, event, playerId, cb, ...)
end
})
---@param event string
---@param playerId number
--- Sends an event to a client and halts the current thread until a response is returned.
---@diagnostic disable-next-line: duplicate-set-field
function lib.callback.await(event, playerId, ...)
return triggerClientCallback(nil, event, playerId, false, ...)
end
local function callbackResponse(success, result, ...)
if not success then
if result then
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(result,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
return false
end
return result, ...
end
local pcall = pcall
---@param name string
---@param cb function
---Registers an event handler and callback function to respond to client requests.
---@diagnostic disable-next-line: duplicate-set-field
function lib.callback.register(name, cb)
event = cbEvent:format(name)
lib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerClientEvent(cbEvent:format(resource), source, key, callbackResponse(pcall(cb, source, ...)))
end)
end
return lib.callback
@@ -0,0 +1,158 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@diagnostic disable: invisible
local getinfo = debug.getinfo
---Ensure the given argument or property has a valid type, otherwise throwing an error.
---@param id number | string
---@param var any
---@param expected type
local function assertType(id, var, expected)
local received = type(var)
if received ~= expected then
error(("expected %s %s to have type '%s' (received %s)")
:format(type(id) == 'string' and 'field' or 'argument', id, expected, received), 3)
end
if expected == 'table' and table.type(var) ~= 'hash' then
error(("expected argument %s to have table.type 'hash' (received %s)")
:format(id, table.type(var)), 3)
end
return true
end
---@alias OxClassConstructor<T> fun(self: T, ...: unknown): nil
---@class OxClass
---@field private __index table
---@field protected __name string
---@field protected private? { [string]: unknown }
---@field protected super? OxClassConstructor
---@field protected constructor? OxClassConstructor
local mixins = {}
local constructors = {}
---Somewhat hacky way to remove the constructor from the class.__index.
---Maybe add static fields in the future?
---@param class OxClass
local function getConstructor(class)
local constructor = constructors[class] or class.constructor
if class.constructor then
constructors[class] = class.constructor
class.constructor = nil
end
return constructor
end
local function void() return '' end
---Creates a new instance of the given class.
---@protected
---@generic T
---@param class T | OxClass
---@return T
function mixins.new(class, ...)
local constructor = getConstructor(class)
local private = {}
local obj = setmetatable({ private = private }, class)
if constructor then
local parent = class
rawset(obj, 'super', function(self, ...)
parent = getmetatable(parent)
constructor = getConstructor(parent)
if constructor then return constructor(self, ...) end
end)
constructor(obj, ...)
end
rawset(obj, 'super', nil)
if private ~= obj.private or next(obj.private) then
private = table.clone(obj.private)
table.wipe(obj.private)
setmetatable(obj.private, {
__metatable = 'private',
__tostring = void,
__index = function(self, index)
local di = getinfo(2, 'n')
if di.namewhat ~= 'method' and di.namewhat ~= '' then return end
return private[index]
end,
__newindex = function(self, index, value)
local di = getinfo(2, 'n')
if di.namewhat ~= 'method' and di.namewhat ~= '' then
error(("cannot set value of private field '%s'"):format(index), 2)
end
private[index] = value
end
})
else
obj.private = nil
end
return obj
end
---Checks if an object is an instance of the given class.
---@param class OxClass
function mixins:isClass(class)
return getmetatable(self) == class
end
---Checks if an object is an instance or derivative of the given class.
---@param class OxClass
function mixins:instanceOf(class)
local mt = getmetatable(self)
while mt do
if mt == class then return true end
mt = getmetatable(mt)
end
return false
end
---Creates a new class.
---@generic S : OxClass
---@generic T : string
---@param name `T`
---@param super? S
---@return `T`
function lib.class(name, super)
assertType(1, name, 'string')
local class = table.clone(mixins)
class.__name = name
class.__index = class
if super then
assertType('super', super, 'table')
setmetatable(class, super)
end
---@todo See if there's a way we can auto-create a class using the name and super
return class
end
return lib.class
@@ -0,0 +1,473 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
lib.cron = {}
---@alias Date { year: number, month: number, day: number, hour: number, min: number, sec: number, wday: number, yday: number, isdst: boolean }
---@type Date
local currentDate = {}
setmetatable(currentDate, {
__index = function(self, index)
local newDate = os.date('*t') --[[@as Date]]
for k, v in pairs(newDate) do
self[k] = v
end
SetTimeout(1000, function() table.wipe(self) end)
return self[index]
end
})
---@class OxTaskProperties
---@field minute? number|string|function
---@field hour? number|string|function
---@field day? number|string|function
---@field month? number|string|function
---@field year? number|string|function
---@field weekday? number|string|function
---@field job fun(task: OxTask, date: osdate)
---@field isActive boolean
---@field id number
---@field debug? boolean
---@field lastRun? number
---@field maxDelay? number Maximum allowed delay in seconds before skipping (0 to disable)
---@class OxTask : OxTaskProperties
---@field expression string
---@field private scheduleTask fun(self: OxTask): boolean?
local OxTask = {}
OxTask.__index = OxTask
local validRanges = {
min = { min = 0, max = 59 },
hour = { min = 0, max = 23 },
day = { min = 1, max = 31 },
month = { min = 1, max = 12 },
wday = { min = 0, max = 7 },
}
local maxUnits = {
min = 60,
hour = 24,
wday = 7,
day = 31,
month = 12,
}
local weekdayMap = {
sun = 1,
mon = 2,
tue = 3,
wed = 4,
thu = 5,
fri = 6,
sat = 7,
}
local monthMap = {
jan = 1, feb = 2, mar = 3, apr = 4,
may = 5, jun = 6, jul = 7, aug = 8,
sep = 9, oct = 10, nov = 11, dec = 12
}
---Returns the last day of the specified month
---@param month number
---@param year? number
---@return number
local function getMaxDaysInMonth(month, year)
return os.date('*t', os.time({ year = year or currentDate.year, month = month + 1, day = -1 })).day --[[@as number]]
end
---@param value string|number
---@param unit string
---@return boolean
local function isValueInRange(value, unit)
local range = validRanges[unit]
if not range then return true end
return value >= range.min and value <= range.max
end
---@param value string
---@param unit string
---@return number|string|function|nil
local function parseCron(value, unit)
if not value or value == '*' then return end
if unit == 'day' and value:lower() == 'l' then
return function()
return getMaxDaysInMonth(currentDate.month, currentDate.year)
end
end
local num = tonumber(value)
if num then
if not isValueInRange(num, unit) then
error(("^1invalid cron expression. '%s' is out of range for %s^0"):format(value, unit), 3)
end
return num
end
if unit == 'wday' then
local start, stop = value:match('(%a+)-(%a+)')
if start and stop then
start = weekdayMap[start:lower()]
stop = weekdayMap[stop:lower()]
if start and stop then
if stop < start then stop = stop + 7 end
return ('%d-%d'):format(start, stop)
end
end
local day = weekdayMap[value:lower()]
if day then return day end
end
if unit == 'month' then
local months = {}
for month in value:gmatch('[^,]+') do
local monthNum = monthMap[month:lower()]
if monthNum then
months[#months + 1] = tostring(monthNum)
end
end
if #months > 0 then
return table.concat(months, ',')
end
end
local stepMatch = value:match('^%*/(%d+)$')
if stepMatch then
local step = tonumber(stepMatch)
if not step or step == 0 then
error(("^1invalid cron expression. Step value cannot be %s^0"):format(step or 'nil'), 3)
end
return value
end
local start, stop = value:match('^(%d+)-(%d+)$')
if start and stop then
start, stop = tonumber(start), tonumber(stop)
if not start or not stop or not isValueInRange(start, unit) or not isValueInRange(stop, unit) then
error(("^1invalid cron expression. Range '%s' is invalid for %s^0"):format(value, unit), 3)
end
return value
end
local valid = true
for item in value:gmatch('[^,]+') do
local num = tonumber(item)
if not num or not isValueInRange(num, unit) then
valid = false
break
end
end
if valid then return value end
error(("^1invalid cron expression. '%s' is not supported for %s^0"):format(value, unit), 3)
end
---@param value string|number|function|nil
---@param unit string
---@return number|false|nil
local function getTimeUnit(value, unit)
local currentTime = currentDate[unit]
if not value then
return unit == 'min' and currentTime + 1 or currentTime
end
if type(value) == 'function' then
return value()
end
local unitMax = maxUnits[unit]
if type(value) == 'string' then
local stepValue = string.match(value, '*/(%d+)')
if stepValue then
local step = tonumber(stepValue)
for i = currentTime + 1, unitMax do
if i % step == 0 then return i end
end
return step + unitMax
end
local range = string.match(value, '%d+-%d+')
if range then
local min, max = string.strsplit('-', range)
min, max = tonumber(min, 10), tonumber(max, 10)
if unit == 'min' then
if currentTime >= max then
return min + unitMax
end
elseif currentTime > max then
return min + unitMax
end
return currentTime < min and min or currentTime
end
local list = string.match(value, '%d+,%d+')
if list then
local values = {}
for listValue in string.gmatch(value, '%d+') do
values[#values + 1] = tonumber(listValue)
end
table.sort(values)
for i = 1, #values do
local listValue = values[i]
if unit == 'min' then
if currentTime < listValue then
return listValue
end
elseif currentTime <= listValue then
return listValue
end
end
return values[1] + unitMax
end
return false
end
if unit == 'min' then
return value <= currentTime and value + unitMax or value --[[@as number]]
end
return value < currentTime and value + unitMax or value --[[@as number]]
end
---@return number?
function OxTask:getNextTime()
if not self.isActive then return end
local day = getTimeUnit(self.day, 'day')
if day == 0 then
day = getMaxDaysInMonth(currentDate.month)
end
if day ~= currentDate.day then return end
local month = getTimeUnit(self.month, 'month')
if month ~= currentDate.month then return end
local weekday = getTimeUnit(self.weekday, 'wday')
if weekday and weekday ~= currentDate.wday then return end
local minute = getTimeUnit(self.minute, 'min')
if not minute then return end
local hour = getTimeUnit(self.hour, 'hour')
if not hour then return end
if minute >= maxUnits.min then
if not self.hour then
hour += math.floor(minute / maxUnits.min)
end
minute = minute % maxUnits.min
end
if hour >= maxUnits.hour and day then
if not self.day then
day += math.floor(hour / maxUnits.hour)
end
hour = hour % maxUnits.hour
end
local nextTime = os.time({
min = minute,
hour = hour,
day = day or currentDate.day,
month = month or currentDate.month,
year = currentDate.year,
})
if self.lastRun and nextTime - self.lastRun < 60 then
if self.debug then
lib.print.debug(('Preventing duplicate execution of task %s - Last run: %s, Next scheduled: %s'):format(
self.id,
os.date('%c', self.lastRun),
os.date('%c', nextTime)
))
end
return
end
return nextTime
end
---@return number
function OxTask:getAbsoluteNextTime()
local minute = getTimeUnit(self.minute, 'min')
local hour = getTimeUnit(self.hour, 'hour')
local day = getTimeUnit(self.day, 'day')
local month = getTimeUnit(self.month, 'month')
local year = getTimeUnit(self.year, 'year')
if self.day then
if currentDate.hour < hour or (currentDate.hour == hour and currentDate.min < minute) then
day = day - 1
if day < 1 then
day = getMaxDaysInMonth(currentDate.month)
end
end
if currentDate.hour > hour or (currentDate.hour == hour and currentDate.min >= minute) then
day = day + 1
if day > getMaxDaysInMonth(currentDate.month) or day == 1 then
day = 1
month = month + 1
end
end
end
---@diagnostic disable-next-line: assign-type-mismatch
if os.time({ year = year, month = month, day = day, hour = hour, min = minute }) < os.time() then
year = year and year + 1 or currentDate.year + 1
end
return os.time({
min = minute < 60 and minute or 0,
hour = hour < 24 and hour or 0,
day = day or currentDate.day,
month = month or currentDate.month,
year = year or currentDate.year,
})
end
function OxTask:getTimeAsString(timestamp)
return os.date('%A %H:%M, %d %B %Y', timestamp or self:getAbsoluteNextTime())
end
---@type OxTask[]
local tasks = {}
function OxTask:scheduleTask()
local runAt = self:getNextTime()
if not runAt then
return self:stop('getNextTime returned no value')
end
local currentTime = os.time()
local sleep = runAt - currentTime
if sleep < 0 then
if not self.maxDelay or -sleep > self.maxDelay then
return self:stop(self.debug and ('scheduled time expired %s seconds ago'):format(-sleep))
end
if self.debug then
lib.print.debug(('Task %s is %s seconds overdue, executing now due to maxDelay=%s'):format(
self.id,
-sleep,
self.maxDelay
))
end
sleep = 0
end
local timeAsString = self:getTimeAsString(runAt)
if self.debug then
lib.print.debug(('(%s) task %s will run in %d seconds (%0.2f minutes / %0.2f hours)'):format(timeAsString, self.id, sleep,
sleep / 60,
sleep / 60 / 60))
end
if sleep > 0 then
Wait(sleep * 1000)
else
Wait(0)
return true
end
if self.isActive then
if self.debug then
lib.print.debug(('(%s) running task %s'):format(timeAsString, self.id))
end
Citizen.CreateThreadNow(function()
self:job(currentDate)
self.lastRun = os.time()
end)
return true
end
end
function OxTask:run()
if self.isActive then return end
self.isActive = true
CreateThread(function()
while self:scheduleTask() do end
end)
end
function OxTask:stop(msg)
self.isActive = false
if self.debug then
if msg then
return lib.print.debug(('stopping task %s (%s)'):format(self.id, msg))
end
lib.print.debug(('stopping task %s'):format(self.id))
end
end
---@param expression string A cron expression such as `* * * * *` representing minute, hour, day, month, and day of the week.
---@param job fun(task: OxTask, date: osdate)
---@param options? { debug?: boolean }
---Creates a new [cronjob](https://en.wikipedia.org/wiki/Cron), scheduling a task to run at fixed times or intervals.
---Supports numbers, any value `*`, lists `1,2,3`, ranges `1-3`, and steps `*/4`.
---Day of the week is a range of `1-7` starting from Sunday and allows short-names (i.e. sun, mon, tue).
---@note maxDelay: Maximum allowed delay in seconds before skipping (0 to disable)
function lib.cron.new(expression, job, options)
if not job or type(job) ~= 'function' then
error(("expected job to have type 'function' (received %s)"):format(type(job)))
end
local minute, hour, day, month, weekday = string.strsplit(' ', string.lower(expression))
---@type OxTask
local task = setmetatable(options or {}, OxTask)
task.expression = expression
task.minute = parseCron(minute, 'min')
task.hour = parseCron(hour, 'hour')
task.day = parseCron(day, 'day')
task.month = parseCron(month, 'month')
task.weekday = parseCron(weekday, 'wday')
task.id = #tasks + 1
task.job = job
task.lastRun = nil
task.maxDelay = task.maxDelay or 1
tasks[task.id] = task
task:run()
return task
end
-- reschedule any dead tasks on a new day
lib.cron.new('0 0 * * *', function()
for i = 1, #tasks do
local task = tasks[i]
if not task.isActive then
task:run()
end
end
end)
return lib.cron
@@ -0,0 +1,64 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
--- Call on frame to disable all stored keys.
--- ```
--- disableControls()
--- ```
local disableControls = {}
---@param ... number | table
function disableControls:Add(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
local key = keys[i]
if self[key] then
self[key] += 1
else
self[key] = 1
end
end
end
---@param ... number | table
function disableControls:Remove(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
local key = keys[i]
local exists = self[key]
if exists and exists > 1 then
self[key] -= 1
else
self[key] = nil
end
end
end
---@param ... number | table
function disableControls:Clear(...)
local keys = type(...) == 'table' and ... or {...}
for i=1, #keys do
self[keys[i]] = nil
end
end
local keys = {}
local DisableControlAction = DisableControlAction
local pairs = pairs
lib.disableControls = setmetatable(disableControls, {
__index = keys,
__newindex = keys,
__call = function()
for k in pairs(keys) do
DisableControlAction(0, k, true)
end
end
})
return lib.disableControls
@@ -0,0 +1,118 @@
--[[
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
@@ -0,0 +1,34 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return number? object
---@return vector3? objectCoords
function lib.getClosestObject(coords, maxDistance)
local objects = GetGamePool('CObject')
local closestObject, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #objects do
local object = objects[i]
local objectCoords = GetEntityCoords(object)
local distance = #(coords - objectCoords)
if distance < maxDistance then
maxDistance = distance
closestObject = object
closestCoords = objectCoords
end
end
return closestObject, closestCoords
end
return lib.getClosestObject
@@ -0,0 +1,36 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return number? ped
---@return vector3? pedCoords
function lib.getClosestPed(coords, maxDistance)
local peds = GetGamePool('CPed')
local closestPed, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #peds do
local ped = peds[i]
if not IsPedAPlayer(ped) then
local pedCoords = GetEntityCoords(ped)
local distance = #(coords - pedCoords)
if distance < maxDistance then
maxDistance = distance
closestPed = ped
closestCoords = pedCoords
end
end
end
return closestPed, closestCoords
end
return lib.getClosestPed
@@ -0,0 +1,40 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayer? boolean Whether or not to include the current player.
---@return number? playerId
---@return number? playerPed
---@return vector3? playerCoords
function lib.getClosestPlayer(coords, maxDistance, includePlayer)
local players = GetActivePlayers()
local closestId, closestPed, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
if playerId ~= cache.playerId or includePlayer then
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
maxDistance = distance
closestId = playerId
closestPed = playerPed
closestCoords = playerCoords
end
end
end
return closestId, closestPed, closestCoords
end
return lib.getClosestPlayer
@@ -0,0 +1,40 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param 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
@@ -0,0 +1,37 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayerVehicle? boolean Whether or not to include the player's current vehicle. Ignored on the server.
---@return number? vehicle
---@return vector3? vehicleCoords
function lib.getClosestVehicle(coords, maxDistance, includePlayerVehicle)
local vehicles = GetGamePool('CVehicle')
local closestVehicle, closestCoords
maxDistance = maxDistance or 2.0
for i = 1, #vehicles do
local vehicle = vehicles[i]
if lib.context == 'server' or not cache.vehicle or vehicle ~= cache.vehicle or includePlayerVehicle then
local vehicleCoords = GetEntityCoords(vehicle)
local distance = #(coords - vehicleCoords)
if distance < maxDistance then
maxDistance = distance
closestVehicle = vehicle
closestCoords = vehicleCoords
end
end
end
return closestVehicle, closestCoords
end
return lib.getClosestVehicle
@@ -0,0 +1,46 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param path string
---@param pattern string
---@return table string[]
---@return integer fileCount
function lib.getFilesInDirectory(path, pattern)
local resource = cache.resource
if path:find('^@') then
resource = path:gsub('^@(.-)/.+', '%1')
path = path:sub(#resource + 3)
end
local files = {}
local fileCount = 0
local windows = string.match(os.getenv('OS') or '', 'Windows')
local command = ('%s%s%s'):format(
windows and 'dir "' or 'ls "',
(GetResourcePath(resource):gsub('//', '/') .. '/' .. path):gsub('\\', '/'),
windows and '/" /b' or '/"'
)
local dir = io.popen(command)
if dir then
for line in dir:lines() do
if line:match(pattern) then
fileCount += 1
files[fileCount] = line
end
end
dir:close()
end
return files, fileCount
end
return lib.getFilesInDirectory
@@ -0,0 +1,36 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { object: number, coords: vector3 }[]
function lib.getNearbyObjects(coords, maxDistance)
local objects = GetGamePool('CObject')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #objects do
local object = objects[i]
local objectCoords = GetEntityCoords(object)
local distance = #(coords - objectCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
object = object,
coords = objectCoords
}
end
end
return nearby
end
return lib.getNearbyObjects
@@ -0,0 +1,38 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { ped: number, coords: vector3 }[]
function lib.getNearbyPeds(coords, maxDistance)
local peds = GetGamePool('CPed')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #peds do
local ped = peds[i]
if not IsPedAPlayer(ped) then
local pedCoords = GetEntityCoords(ped)
local distance = #(coords - pedCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
ped = ped,
coords = pedCoords,
}
end
end
end
return nearby
end
return lib.getNearbyPeds
@@ -0,0 +1,41 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayer? boolean Whether or not to include the current player.
---@return { id: number, ped: number, coords: vector3 }[]
function lib.getNearbyPlayers(coords, maxDistance, includePlayer)
local players = GetActivePlayers()
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
if playerId ~= cache.playerId or includePlayer then
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
id = playerId,
ped = playerPed,
coords = playerCoords,
}
end
end
end
return nearby
end
return lib.getNearbyPlayers
@@ -0,0 +1,37 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@return { id: number, ped: number, coords: vector3 }[]
function lib.getNearbyPlayers(coords, maxDistance)
local players = GetActivePlayers()
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #players do
local playerId = players[i]
local playerPed = GetPlayerPed(playerId)
local playerCoords = GetEntityCoords(playerPed)
local distance = #(coords - playerCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
id = playerId,
ped = playerPed,
coords = playerCoords,
}
end
end
return nearby
end
return lib.getNearbyPlayers
@@ -0,0 +1,39 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@param coords vector3 The coords to check from.
---@param maxDistance? number The max distance to check.
---@param includePlayerVehicle? boolean Whether or not to include the player's current vehicle.
---@return { vehicle: number, coords: vector3 }[]
function lib.getNearbyVehicles(coords, maxDistance, includePlayerVehicle)
local vehicles = GetGamePool('CVehicle')
local nearby = {}
local count = 0
maxDistance = maxDistance or 2.0
for i = 1, #vehicles do
local vehicle = vehicles[i]
if lib.context == 'server' or not cache.vehicle or vehicle ~= cache.vehicle or includePlayerVehicle then
local vehicleCoords = GetEntityCoords(vehicle)
local distance = #(coords - vehicleCoords)
if distance < maxDistance then
count += 1
nearby[count] = {
vehicle = vehicle,
coords = vehicleCoords
}
end
end
end
return nearby
end
return lib.getNearbyVehicles
@@ -0,0 +1,56 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
local glm_sincos = require 'glm'.sincos --[[@as fun(n: number): number, number]]
local glm_rad = require 'glm'.rad --[[@as fun(n: number): number]]
---Get the relative coordinates based on heading/rotation and offset
---@overload fun(coords: vector3, heading: number, offset: vector3): vector3
---@overload fun(coords: vector4, offset: vector3): vector4
---@overload fun(coords: vector3, rotation: vector3, offset: vector3): vector3
function lib.getRelativeCoords(coords, rotation, offset)
if type(rotation) == 'vector3' and offset then
local pitch = glm_rad(rotation.x)
local roll = glm_rad(rotation.y)
local yaw = glm_rad(rotation.z)
local sp, cp = glm_sincos(pitch)
local sr, cr = glm_sincos(roll)
local sy, cy = glm_sincos(yaw)
local rotatedX = offset.x * (cy * cr) + offset.y * (cy * sr * sp - sy * cp) + offset.z * (cy * sr * cp + sy * sp)
local rotatedY = offset.x * (sy * cr) + offset.y * (sy * sr * sp + cy * cp) + offset.z * (sy * sr * cp - cy * sp)
local rotatedZ = offset.x * (-sr) + offset.y * (cr * sp) + offset.z * (cr * cp)
return vec3(
coords.x + rotatedX,
coords.y + rotatedY,
coords.z + rotatedZ
)
end
offset = offset or rotation
local x, y, z, w = coords.x, coords.y, coords.z, type(rotation) == 'number' and rotation or coords.w
local sin, cos = glm_sincos(glm_rad(w))
local relativeX = offset.x * cos - offset.y * sin
local relativeY = offset.x * sin + offset.y * cos
return coords.w and vec4(
x + relativeX,
y + relativeY,
z + offset.z,
w
) or vec3(
x + relativeX,
y + relativeY,
z + offset.z
)
end
return lib.getRelativeCoords
@@ -0,0 +1,194 @@
--[[
Based on PolyZone's grid system (https://github.com/mkafrin/PolyZone/blob/master/ComboZone.lua)
MIT License
Copyright © 2019-2021 Michael Afrin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
]]
local mapMinX = -3700
local mapMinY = -4400
local mapMaxX = 4500
local mapMaxY = 8000
local xDelta = (mapMaxX - mapMinX) / 34
local yDelta = (mapMaxY - mapMinY) / 50
local grid = {}
local lastCell = {}
local gridCache = {}
local entrySet = {}
lib.grid = {}
---@class GridEntry
---@field coords vector
---@field length? number
---@field width? number
---@field radius? number
---@field [string] any
---@param point vector
---@param length number
---@param width number
---@return number, number, number, number
local function getGridDimensions(point, length, width)
local minX = (point.x - width - mapMinX) // xDelta
local maxX = (point.x + width - mapMinX) // xDelta
local minY = (point.y - length - mapMinY) // yDelta
local maxY = (point.y + length - mapMinY) // yDelta
return minX, maxX, minY, maxY
end
---@param point vector
---@return number, number
function lib.grid.getCellPosition(point)
local x = (point.x - mapMinX) // xDelta
local y = (point.y - mapMinY) // yDelta
return x, y
end
---@param point vector
---@return GridEntry[]
function lib.grid.getCell(point)
local x, y = lib.grid.getCellPosition(point)
if lastCell.x ~= x or lastCell.y ~= y then
lastCell.x = x
lastCell.y = y
lastCell.cell = grid[y] and grid[y][x] or {}
end
return lastCell.cell
end
---@param point vector
---@param filter? fun(entry: GridEntry): boolean
---@return Array<GridEntry>
function lib.grid.getNearbyEntries(point, filter)
local minX, maxX, minY, maxY = getGridDimensions(point, xDelta, yDelta)
if gridCache.filter == filter and
gridCache.minX == minX and
gridCache.maxX == maxX and
gridCache.minY == minY and
gridCache.maxY == maxY then
return gridCache.entries
end
local entries = lib.array:new()
local n = 0
table.wipe(entrySet)
for y = minY, maxY do
local row = grid[y]
for x = minX, maxX do
local cell = row and row[x]
if cell then
for j = 1, #cell do
local entry = cell[j]
if not entrySet[entry] and (not filter or filter(entry)) then
n = n + 1
entrySet[entry] = true
entries[n] = entry
end
end
end
end
end
gridCache.minX = minX
gridCache.maxX = maxX
gridCache.minY = minY
gridCache.maxY = maxY
gridCache.entries = entries
gridCache.filter = filter
return entries
end
---@param entry { coords: vector, length?: number, width?: number, radius?: number, [string]: any }
function lib.grid.addEntry(entry)
entry.length = entry.length or entry.radius * 2
entry.width = entry.width or entry.radius * 2
local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width)
for y = minY, maxY do
local row = grid[y] or {}
for x = minX, maxX do
local cell = row[x] or {}
cell[#cell + 1] = entry
row[x] = cell
end
grid[y] = row
table.wipe(gridCache)
end
end
---@param entry table A table that was added to the grid previously.
function lib.grid.removeEntry(entry)
local minX, maxX, minY, maxY = getGridDimensions(entry.coords, entry.length, entry.width)
local success = false
for y = minY, maxY do
local row = grid[y]
if not row then goto continue end
for x = minX, maxX do
local cell = row[x]
if cell then
for i = 1, #cell do
if cell[i] == entry then
table.remove(cell, i)
success = true
break
end
end
if #cell == 0 then
row[x] = nil
end
end
end
if not next(row) then
grid[y] = nil
end
::continue::
end
table.wipe(gridCache)
return success
end
return lib.grid
@@ -0,0 +1,122 @@
--[[
https://github.com/overextended/ox_lib
This file is licensed under LGPL-3.0 or higher <https://www.gnu.org/licenses/lgpl-3.0.en.html>
Copyright © 2025 Linden <https://github.com/thelindat>
]]
---@type { [string]: string }
local dict = {}
---@param source { [string]: string }
---@param target { [string]: string }
---@param prefix? string
local function flattenDict(source, target, prefix)
for key, value in pairs(source) do
local fullKey = prefix and (prefix .. '.' .. key) or key
if type(value) == 'table' then
flattenDict(value, target, fullKey)
else
target[fullKey] = value
end
end
return target
end
---@param str string
---@param ... string | number
---@return string
function locale(str, ...)
local lstr = dict[str]
if lstr then
if ... then
return lstr and lstr:format(...)
end
return lstr
end
return str
end
function lib.getLocales()
return dict
end
local function loadLocale(key)
local data = LoadResourceFile(cache.resource, ('locales/%s.json'):format(key))
if not data then
warn(("could not load 'locales/%s.json'"):format(key))
end
return json.decode(data) or {}
end
local table = lib.table
---Loads the ox_lib locale module. Prefer using fxmanifest instead (see [docs](https://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
@@ -0,0 +1,332 @@
--[[
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
@@ -0,0 +1,116 @@
--[[
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
@@ -0,0 +1,229 @@
--[[
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
@@ -0,0 +1,78 @@
--[[
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
@@ -0,0 +1,194 @@
--[[
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
@@ -0,0 +1,73 @@
--[[
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
@@ -0,0 +1,79 @@
--[[
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
@@ -0,0 +1,27 @@
--[[
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
@@ -0,0 +1,23 @@
--[[
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
@@ -0,0 +1,19 @@
--[[
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
@@ -0,0 +1,24 @@
--[[
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
@@ -0,0 +1,23 @@
--[[
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
@@ -0,0 +1,25 @@
--[[
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
@@ -0,0 +1,23 @@
--[[
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
@@ -0,0 +1,52 @@
--[[
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
@@ -0,0 +1,190 @@
--[[
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
@@ -0,0 +1,232 @@
--[[
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
@@ -0,0 +1,175 @@
---@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
@@ -0,0 +1,29 @@
--[[
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
@@ -0,0 +1,66 @@
--[[
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
@@ -0,0 +1,185 @@
--[[
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
@@ -0,0 +1,151 @@
--[[
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
@@ -0,0 +1,31 @@
--[[
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
@@ -0,0 +1,42 @@
--[[
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
@@ -0,0 +1,498 @@
--[[
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
@@ -0,0 +1,299 @@
---@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
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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": "تحديد ريستارت للسرفر"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,34 @@
{
"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": "Προγραμματισμένη επανεκκίνηση"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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é"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,34 @@
{
"language": "Română",
"settings": "Setări",
"ui": {
"cancel": "Anulează",
"close": "Închide",
"confirm": "Confirmă",
"more": "Mai multe...",
"settings": {
"locale": "Schimbă limba",
"locale_description": "Limba actuală: ${language} (%s)",
"notification_audio": "Audio notificări",
"notification_position": "Poziţie notificări"
},
"position": {
"bottom": "Jos",
"bottom-left": "Jos-stânga",
"bottom-right": "Jos-dreapta",
"center-left": "Centru-stânga",
"center-right": "Centru-dreapta",
"top": "Sus",
"top-left": "Sus-stânga",
"top-right": "Sus-dreapta"
}
},
"open_radial_menu": "Deschide meniul radial",
"cancel_progress": "Anuleaza bara de progres actuala",
"txadmin_announcement": "Anunţ de server dat de %s",
"txadmin_dm": "Mesaj Direct de la %s",
"txadmin_warn": "Ai fost avertizat de %s",
"txadmin_warn_content": "%s \nID Acţiune: %s",
"txadmin_scheduledrestart": "Restart Programat"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,32 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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"
}
@@ -0,0 +1,33 @@
{
"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": "计划内重启"
}
@@ -0,0 +1,33 @@
{
"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": "計劃內重啟"
}
@@ -0,0 +1,3 @@
.prettierrc
*.ts
!*.d.ts
@@ -0,0 +1,9 @@
{
"printWidth": 120,
"singleQuote": true,
"useTabs": false,
"tabWidth": 2,
"semi": true,
"bracketSpacing": true,
"trailingComma": "es5"
}
@@ -0,0 +1,38 @@
# ox_lib JS/TS wrapper
Not all ox_lib functions found in Lua are supported, the ones that are will have a JS/TS example
on the documentation.
You still need to use and have the ox_lib resource included into the resource you are using the npm package in.
## Installation
```yaml
# With pnpm
pnpm add @overextended/ox_lib
# With Yarn
yarn add @overextended/ox_lib
# With npm
npm install @overextended/ox_lib
```
## Usage
You can either import the lib from client or server files or deconstruct the object and import only certain functions
you may require.
```ts
import lib from '@overextended/ox_lib/client'
```
```ts
import lib from '@overextended/ox_lib/server'
```
```ts
import { checkDependency } from '@overextended/ox_lib/shared';
```
## Documentation
[View documentation](https://overextended.github.io/docs/ox_lib)
@@ -0,0 +1,5 @@
import * as lib from './resource';
export * from './resource';
export * from '../shared';
export default lib;
@@ -0,0 +1,74 @@
import { cache } from '../cache';
const pendingCallbacks: Record<string, (...args: any[]) => void> = {};
const callbackTimeout = GetConvarInt('ox:callbackTimeout', 300000);
onNet(`__ox_cb_${cache.resource}`, (key: string, ...args: any) => {
if (!source) return;
const resolve = pendingCallbacks[key];
if (!resolve) return;
delete pendingCallbacks[key];
resolve(...args);
});
const eventTimers: Record<string, number> = {};
export function eventTimer(eventName: string, delay: number | null) {
if (delay && delay > 0) {
const currentTime = GetGameTimer();
if ((eventTimers[eventName] || 0) > currentTime) return false;
eventTimers[eventName] = currentTime + delay;
}
return true;
}
export function triggerServerCallback<T = unknown>(
eventName: string,
delay: number | null,
...args: any
): Promise<T> | void {
if (!eventTimer(eventName, delay)) return;
let key: string;
do {
key = `${eventName}:${Math.floor(Math.random() * (100000 + 1))}`;
} while (pendingCallbacks[key]);
emitNet(`ox_lib:validateCallback`, eventName, cache.resource, key);
emitNet(`__ox_cb_${eventName}`, cache.resource, key, ...args);
return new Promise<T>((resolve, reject) => {
pendingCallbacks[key] = (args) => {
if (args[0] === 'cb_invalid') reject(`callback '${eventName} does not exist`);
resolve(args);
};
setTimeout(reject, callbackTimeout, `callback event '${key}' timed out`);
});
}
export function onServerCallback(eventName: string, cb: (...args: any[]) => any) {
exports.ox_lib.setValidCallback(eventName, true)
onNet(`__ox_cb_${eventName}`, async (resource: string, key: string, ...args: any[]) => {
let response: any;
try {
response = await cb(...args);
} catch (e: any) {
console.error(`an error occurred while handling callback event ${eventName}`);
console.log(`^3${e.stack}^0`);
}
emitNet(`__ox_cb_${resource}`, key, response);
});
}
@@ -0,0 +1,70 @@
import { cache } from '../cache';
const duis: Record<string, Dui> = {};
let currentId = 0;
interface DuiProperties {
url: string;
width: number;
height: number;
debug?: boolean;
}
export class Dui {
private id: string = '';
private debug: boolean = false;
url: string = '';
duiObject: number = 0;
duiHandle: string = '';
runtimeTxd: number = 0;
txdObject: number = 0;
dictName: string = '';
txtName: string = '';
constructor(data: DuiProperties) {
const time = GetGameTimer();
const id = `${cache.resource}_${time}_${currentId}`;
currentId++;
this.id = id;
this.debug = data.debug || false;
this.url = data.url;
this.dictName = `ox_lib_dui_dict_${id}`;
this.txtName = `ox_lib_dui_txt_${id}`;
this.duiObject = CreateDui(data.url, data.width, data.height);
this.duiHandle = GetDuiHandle(this.duiObject);
this.runtimeTxd = CreateRuntimeTxd(this.dictName);
this.txdObject = CreateRuntimeTextureFromDuiHandle(this.runtimeTxd, this.txtName, this.duiHandle);
duis[id] = this;
if (this.debug) console.log(`Dui ${this.id} created`);
}
remove() {
SetDuiUrl(this.duiObject, 'about:blank');
DestroyDui(this.duiObject);
delete duis[this.id];
if (this.debug) console.log(`Dui ${this.id} removed`);
}
setUrl(url: string) {
this.url = url;
SetDuiUrl(this.duiObject, url);
if (this.debug) console.log(`Dui ${this.id} url set to ${url}`);
}
sendMessage(data: object) {
SendDuiMessage(this.duiObject, JSON.stringify(data));
if (this.debug) console.log(`Dui ${this.id} message sent with data :`, data);
}
}
on('onResourceStop', (resourceName: string) => {
if (cache.resource !== resourceName) return;
for (const dui in duis) {
duis[dui].remove();
}
});

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