update libs

This commit is contained in:
UIRP.Hetzner 2025-03-17 15:15:40 +00:00
parent ff18b299fd
commit e4b7186910
71 changed files with 2810 additions and 1855 deletions

View File

@ -6,7 +6,7 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw
name 'ox_lib'
author 'Overextended'
version '3.26.0'
version '3.30.3'
license 'LGPL-3.0-or-later'
repository 'https://github.com/overextended/ox_lib'
description 'A library of shared functions to utilise in other resources.'

View File

@ -1,6 +1,12 @@
---@class Array : OxClass
---@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
@ -19,18 +25,63 @@ function lib.array:__newindex(index, value)
rawset(self, index, value)
end
---Create a new array containing the elements from two arrays.
---@param arr ArrayLike
function lib.array:merge(arr)
local newArr = table.clone(self)
local length = #self
---Creates a new array from an iteratable value.
---@param iter table | function | string
---@return Array
function lib.array:from(iter)
local iterType = type(iter)
for i = 1, #arr do
length += 1
newArr[length] = arr[i]
if iterType == 'table' then
return lib.array:new(table_unpack(iter))
end
return lib.array:new(table.unpack(newArr))
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.
@ -45,7 +96,26 @@ function lib.array:every(testFn)
return true
end
---Creates a new array containing the elements from an array thtat pass the test of the provided function.
---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 = {}
@ -60,7 +130,7 @@ function lib.array:filter(testFn)
end
end
return lib.array:new(table.unpack(newArr))
return lib.array:new(table_unpack(newArr))
end
---Returns the first or last element of an array that passes the provided test function.
@ -109,7 +179,7 @@ function lib.array:indexOf(value, last)
local element = self[i]
if element == value then
return element
return i
end
end
end
@ -122,15 +192,38 @@ function lib.array:forEach(cb)
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 ',')
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)
return table_remove(self)
end
---Adds the given elements to the end of an array and returns the new array length.
@ -147,35 +240,110 @@ function lib.array:push(...)
return length
end
---Removes the first element from an array and returns the removed element.
function lib.array:shift()
return table.remove(self, 1)
end
---The "reducer" function is applied to every element within an array, with the previous element's result serving as the accumulator.\
---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)
function lib.array:reduce(reducer, initialValue, reverse)
local length = #self
local initialIndex = initialValue and 1 or 2
local accumulator = initialValue or self[1]
for i = initialIndex, #self do
accumulator = reducer(accumulator, self[i], i)
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)
if not type(tbl) == 'table' then return false end
local tableType = table_type(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

View File

@ -5,9 +5,12 @@ 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
return cb and cb(...)
cb(...)
end)
---@param event string
@ -41,12 +44,19 @@ local function triggerServerCallback(_, event, delay, cb, ...)
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
@ -74,6 +84,10 @@ lib.callback = setmetatable({}, {
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
@ -109,7 +123,11 @@ local pcall = pcall
---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)
RegisterNetEvent(cbEvent:format(name), function(resource, key, ...)
event = cbEvent:format(name)
lib.setValidCallback(name, true)
RegisterNetEvent(event, function(resource, key, ...)
TriggerServerEvent(cbEvent:format(resource), key, callbackResponse(pcall(cb, ...)))
end)
end

View File

@ -4,9 +4,12 @@ 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
return cb and cb(...)
cb(...)
end)
---@param _ any
@ -24,12 +27,19 @@ local function triggerClientCallback(_, event, playerId, cb, ...)
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
@ -57,6 +67,10 @@ lib.callback = setmetatable({}, {
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
@ -92,7 +106,11 @@ local pcall = pcall
---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)
RegisterNetEvent(cbEvent:format(name), function(resource, key, ...)
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

View File

@ -55,10 +55,8 @@ local function void() return '' end
---@return T
function mixins.new(class, ...)
local constructor = getConstructor(class)
local obj = setmetatable({
private = {}
}, class)
local private = {}
local obj = setmetatable({ private = private }, class)
if constructor then
local parent = class
@ -75,8 +73,8 @@ function mixins.new(class, ...)
rawset(obj, 'super', nil)
if next(obj.private) then
local private = table.clone(obj.private)
if private ~= obj.private or next(obj.private) then
private = table.clone(obj.private)
table.wipe(obj.private)
setmetatable(obj.private, {

View File

@ -7,28 +7,27 @@ 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
---@field hour? number | string
---@field day? number | string
---@field month? number | string
---@field year? number | string
---@field weekday? number | string
---@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
@ -36,6 +35,14 @@ setmetatable(currentDate, {
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,
@ -44,7 +51,23 @@ local maxUnits = {
month = 12,
}
--- Gets the amount of days in certain month
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
@ -52,9 +75,96 @@ 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 | nil
---@param value string|number
---@param unit string
---@return string | number | false | nil
---@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]
@ -62,25 +172,24 @@ local function getTimeUnit(value, unit)
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
-- */10 * * * * is equal to a list of 0,10,20,30,40,50
-- best suited to factors of unitMax (excluding the highest and lowest numbers)
-- i.e. for minutes - 2, 3, 4, 5, 6, 10, 12, 15, 20, 30
local step = tonumber(stepValue)
for i = currentTime + 1, unitMax do
-- if i is divisible by stepValue
if i % stepValue == 0 then return i end
if i % step == 0 then return i end
end
return stepValue + unitMax
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)
@ -97,12 +206,15 @@ local function getTimeUnit(value, unit)
end
local list = string.match(value, '%d+,%d+')
if list then
for listValue in string.gmatch(value, '%d+') --[[@as number]] do
listValue = tonumber(listValue)
local values = {}
for listValue in string.gmatch(value, '%d+') do
values[#values + 1] = tonumber(listValue)
end
table.sort(values)
-- e.g. if current time is less than in the expression 0,10,20,45 * * * *
for i = 1, #values do
local listValue = values[i]
if unit == 'min' then
if currentTime < listValue then
return listValue
@ -112,56 +224,47 @@ local function getTimeUnit(value, unit)
end
end
-- if iterator failed, return the first value in the list
return tonumber(string.match(value, '%d+')) + unitMax
return values[1] + unitMax
end
return false
end
if unit == 'min' then
return value <= currentTime and value + unitMax or value
return value <= currentTime and value + unitMax or value --[[@as number]]
end
return value < currentTime and value + unitMax or value
return value < currentTime and value + unitMax or value --[[@as number]]
end
---Get a timestamp for the next time to run the task today.
---@return number?
function OxTask:getNextTime()
if not self.isActive then return end
local day = getTimeUnit(self.day, 'day')
-- If current day is the last day of the month, and the task is scheduled for the last day of the month, then the task should run.
if day == 0 then
-- Should probably be used month from getTimeUnit, but don't want to reorder this code.
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 ~= currentDate.wday then return end
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
@ -169,20 +272,31 @@ function OxTask:getNextTime()
if not self.day then
day += math.floor(hour / maxUnits.hour)
end
hour = hour % maxUnits.hour
end
return os.time({
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
---Get timestamp for next time to run task at any day.
---@return number
function OxTask:getAbsoluteNextTime()
local minute = getTimeUnit(self.minute, 'min')
@ -191,7 +305,6 @@ function OxTask:getAbsoluteNextTime()
local month = getTimeUnit(self.month, 'month')
local year = getTimeUnit(self.year, 'year')
-- To avoid modifying getTimeUnit function, the day is adjusted here if needed.
if self.day then
if currentDate.hour < hour or (currentDate.hour == hour and currentDate.min < minute) then
day = day - 1
@ -209,7 +322,6 @@ function OxTask:getAbsoluteNextTime()
end
end
-- Check if time will be in next year.
---@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
@ -242,40 +354,50 @@ function OxTask:scheduleTask()
local sleep = runAt - currentTime
if sleep < 0 then
return self:stop(self.debug and ('scheduled time expired %s seconds ago'):format(-sleep))
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
print(('(%s) task %s will run in %d seconds (%0.2f minutes / %0.2f hours)'):format(timeAsString, self.id, sleep,
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 -- will this even happen?
Wait(1000)
else
Wait(0)
return true
end
if self.isActive then
if self.debug then
print(('(%s) running task %s'):format(timeAsString, self.id))
lib.print.debug(('(%s) running task %s'):format(timeAsString, self.id))
end
Citizen.CreateThreadNow(function()
self:job(currentDate)
self.lastRun = os.time()
end)
-- Wait(30000)
return true
end
end
---Start an inactive task.
function OxTask:run()
if self.isActive then return end
@ -291,58 +413,20 @@ function OxTask:stop(msg)
if self.debug then
if msg then
return print(('stopping task %s (%s)'):format(self.id, msg))
return lib.print.debug(('stopping task %s (%s)'):format(self.id, msg))
end
print(('stopping task %s'):format(self.id))
lib.print.debug(('stopping task %s'):format(self.id))
end
end
---@param value string
---@return number | string | nil
local function parseCron(value, unit)
if not value or value == '*' then return end
local num = tonumber(value)
if num then return num end
if unit == 'wday' then
if value == 'sun' then return 1 end
if value == 'mon' then return 2 end
if value == 'tue' then return 3 end
if value == 'wed' then return 4 end
if value == 'thu' then return 5 end
if value == 'fri' then return 6 end
if value == 'sat' then return 7 end
end
if unit == 'month' then
if value == 'jan' then return 1 end
if value == 'feb' then return 2 end
if value == 'mar' then return 3 end
if value == 'apr' then return 4 end
if value == 'may' then return 5 end
if value == 'jun' then return 6 end
if value == 'jul' then return 7 end
if value == 'aug' then return 8 end
if value == 'sep' then return 9 end
if value == 'oct' then return 10 end
if value == 'nov' then return 11 end
if value == 'dec' then return 12 end
end
if getTimeUnit(value, unit) then return value end
error(("^1invalid cron expression. '%s' is not supported for %s^0"):format(value, unit), 3)
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)))
@ -360,6 +444,8 @@ function lib.cron.new(expression, job, options)
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()
@ -370,7 +456,6 @@ end
lib.cron.new('0 0 * * *', function()
for i = 1, #tasks do
local task = tasks[i]
if not task.isActive then
task:run()
end

View File

@ -0,0 +1,87 @@
---@class DuiProperties
---@field url string
---@field width number
---@field height number
---@field debug? boolean
---@class Dui : OxClass
---@field private private { id: string, debug: boolean }
---@field url string
---@field duiObject number
---@field duiHandle string
---@field runtimeTxd number
---@field txdObject number
---@field dictName string
---@field txtName string
lib.dui = lib.class('Dui')
---@type table<string, Dui>
local duis = {}
local currentId = 0
---@param data DuiProperties
function lib.dui:constructor(data)
local time = GetGameTimer()
local id = ("%s_%s_%s"):format(cache.resource, time, currentId)
currentId = currentId + 1
local dictName = ('ox_lib_dui_dict_%s'):format(id)
local txtName = ('ox_lib_dui_txt_%s'):format(id)
local duiObject = CreateDui(data.url, data.width, data.height)
local duiHandle = GetDuiHandle(duiObject)
local runtimeTxd = CreateRuntimeTxd(dictName)
local txdObject = CreateRuntimeTextureFromDuiHandle(runtimeTxd, txtName, duiHandle)
self.private.id = id
self.private.debug = data.debug or false
self.url = data.url
self.duiObject = duiObject
self.duiHandle = duiHandle
self.runtimeTxd = runtimeTxd
self.txdObject = txdObject
self.dictName = dictName
self.txtName = txtName
duis[id] = self
if self.private.debug then
print(('Dui %s created'):format(id))
end
end
function lib.dui:remove()
SetDuiUrl(self.duiObject, 'about:blank')
DestroyDui(self.duiObject)
duis[self.private.id] = nil
if self.private.debug then
print(('Dui %s removed'):format(self.private.id))
end
end
---@param url string
function lib.dui:setUrl(url)
self.url = url
SetDuiUrl(self.duiObject, url)
if self.private.debug then
print(('Dui %s url set to %s'):format(self.private.id, url))
end
end
---@param message table
function lib.dui:sendMessage(message)
SendDuiMessage(self.duiObject, json.encode(message))
if self.private.debug then
print(('Dui %s message sent with data :'):format(self.private.id), json.encode(message, { indent = true }))
end
end
AddEventHandler('onResourceStop', function(resourceName)
if cache.resource ~= resourceName then return end
for _, dui in pairs(duis) do
dui:remove()
end
end)
return lib.dui

View File

@ -0,0 +1,48 @@
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

View File

@ -0,0 +1,194 @@
--[[
Based on PolyZone's grid system (https://github.com/mkafrin/PolyZone/blob/master/ComboZone.lua)
MIT License
Copyright (c) 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

View File

@ -1,168 +1,165 @@
---@diagnostic disable: param-type-mismatch
lib.marker = {}
local defaultRotation = vector3(0, 0, 0)
local defaultDirection = vector3(0, 0, 0)
local defaultColor = { r = 255, g = 255, b = 255, a = 100 }
local defaultSize = { width = 2, height = 1 }
local defaultBobUpAndDown = false
local defaultFaceCamera = true
local defaultRotate = false
local defaultTextureDict = nil
local defaultTextureName = nil
local markerTypesMap = {
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,
}
---@alias MarkerType
---| "UpsideDownCone"
---| "VerticalCylinder"
---| "ThickChevronUp"
---| "ThinChevronUp"
---| "CheckeredFlagRect"
---| "CheckeredFlagCircle"
---| "VerticleCircle"
---| "PlaneModel"
---| "LostMCTransparent"
---| "LostMC"
---| "Number0"
---| "Number1"
---| "Number2"
---| "Number3"
---| "Number4"
---| "Number5"
---| "Number6"
---| "Number7"
---| "Number8"
---| "Number9"
---| "ChevronUpx1"
---| "ChevronUpx2"
---| "ChevronUpx3"
---| "HorizontalCircleFat"
---| "ReplayIcon"
---| "HorizontalCircleSkinny"
---| "HorizontalCircleSkinny_Arrow"
---| "HorizontalSplitArrowCircle"
---| "DebugSphere"
---| "DollarSign"
---| "HorizontalBars"
---| "WolfHead"
---| "QuestionMark"
---| "PlaneSymbol"
---| "HelicopterSymbol"
---| "BoatSymbol"
---| "CarSymbol"
---| "MotorcycleSymbol"
---| "BikeSymbol"
---| "TruckSymbol"
---| "ParachuteSymbol"
---| "Unknown41"
---| "SawbladeSymbol"
---| "Unknown43"
---@class MarkerProps
---@field type MarkerType | number
---@field coords { x: number, y: number, z: number }
---@field width? number
---@field height? number
---@field color? { r: number, g: number, b: number, a: number }
---@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
---@param self MarkerProps
local function drawMarker(self)
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, false)
end
---@param options MarkerProps
function lib.marker.new(options)
local markerType
if type(options.type) == "string" then
markerType = markerTypesMap[options.type]
if markerType == nil then
error(("unknown marker type '%s'"):format(options.type))
end
elseif type(options.type) == "number" then
markerType = options.type
else
error(("expected marker type to have type 'string' or 'number' (received %s)"):format(type(options.type)))
end
local self = {}
self.type = markerType
self.coords = options.coords
self.color = options.color or defaultColor
self.width = options.width or defaultSize.width
self.height = options.height or defaultSize.height
self.rotation = options.rotation or defaultRotation
self.direction = options.direction or defaultDirection
self.bobUpAndDown = options.bobUpAndDown or defaultBobUpAndDown
self.faceCamera = options.faceCamera or defaultFaceCamera
self.rotate = options.rotate or defaultRotate
self.textureDict = options.textureDict or defaultTextureDict
self.textureName = options.textureName or defaultTextureName
self.draw = drawMarker
self.width += 0.0
self.height += 0.0
return self
end
return lib.marker
---@diagnostic disable: param-type-mismatch
lib.marker = {}
local defaultRotation = vector3(0, 0, 0)
local defaultDirection = vector3(0, 0, 0)
local defaultColor = { r = 255, g = 255, b = 255, a = 100 }
local defaultSize = { width = 2, height = 1 }
local defaultTextureDict = nil
local defaultTextureName = nil
local markerTypesMap = {
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,
}
---@alias MarkerType
---| "UpsideDownCone"
---| "VerticalCylinder"
---| "ThickChevronUp"
---| "ThinChevronUp"
---| "CheckeredFlagRect"
---| "CheckeredFlagCircle"
---| "VerticleCircle"
---| "PlaneModel"
---| "LostMCTransparent"
---| "LostMC"
---| "Number0"
---| "Number1"
---| "Number2"
---| "Number3"
---| "Number4"
---| "Number5"
---| "Number6"
---| "Number7"
---| "Number8"
---| "Number9"
---| "ChevronUpx1"
---| "ChevronUpx2"
---| "ChevronUpx3"
---| "HorizontalCircleFat"
---| "ReplayIcon"
---| "HorizontalCircleSkinny"
---| "HorizontalCircleSkinny_Arrow"
---| "HorizontalSplitArrowCircle"
---| "DebugSphere"
---| "DollarSign"
---| "HorizontalBars"
---| "WolfHead"
---| "QuestionMark"
---| "PlaneSymbol"
---| "HelicopterSymbol"
---| "BoatSymbol"
---| "CarSymbol"
---| "MotorcycleSymbol"
---| "BikeSymbol"
---| "TruckSymbol"
---| "ParachuteSymbol"
---| "Unknown41"
---| "SawbladeSymbol"
---| "Unknown43"
---@class MarkerProps
---@field type MarkerType | number
---@field coords { x: number, y: number, z: number }
---@field width? number
---@field height? number
---@field color? { r: number, g: number, b: number, a: number }
---@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
---@param self MarkerProps
local function drawMarker(self)
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, false)
end
---@param options MarkerProps
function lib.marker.new(options)
local markerType
if type(options.type) == "string" then
markerType = markerTypesMap[options.type]
if markerType == nil then
error(("unknown marker type '%s'"):format(options.type))
end
elseif type(options.type) == "number" then
markerType = options.type
else
error(("expected marker type to have type 'string' or 'number' (received %s)"):format(type(options.type)))
end
local self = {}
self.type = markerType
self.coords = options.coords
self.color = options.color or defaultColor
self.width = options.width or defaultSize.width
self.height = options.height or defaultSize.height
self.rotation = options.rotation or defaultRotation
self.direction = options.direction or defaultDirection
self.bobUpAndDown = type(options.bobUpAndDown) == 'boolean' and options.bobUpAndDown
self.faceCamera = type(options.faceCamera) ~= 'boolean' or options.faceCamera
self.rotate = type(options.rotate) == 'boolean' and options.rotate
self.textureDict = options.textureDict or defaultTextureDict
self.textureName = options.textureName or defaultTextureName
self.draw = drawMarker
self.width += 0.0
self.height += 0.0
return self
end
return lib.marker

View File

@ -26,74 +26,93 @@ local function removePoint(self)
closestPoint = nil
end
points[self.id] = nil
lib.grid.removeEntry(self)
points[self.id] = nil
end
CreateThread(function()
while true do
while true do
if nearbyCount ~= 0 then
table.wipe(nearbyPoints)
nearbyCount = 0
end
local coords = GetEntityCoords(cache.ped)
cache.coords = coords
if closestPoint and #(coords - closestPoint.coords) > closestPoint.distance then
closestPoint = nil
table.wipe(nearbyPoints)
nearbyCount = 0
end
for _, point in pairs(points) do
local distance = #(coords - point.coords)
local coords = GetEntityCoords(cache.ped)
local newPoints = lib.grid.getNearbyEntries(coords, function(entry) return entry.remove == removePoint end) --[[@as CPoint[] ]]
local cellX, cellY = lib.grid.getCellPosition(coords)
cache.coords = coords
closestPoint = nil
if distance <= point.distance then
point.currentDistance = distance
if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
for i = 1, #nearbyPoints do
local point = nearbyPoints[i]
if closestPoint then
if distance < closestPoint.currentDistance then
closestPoint.isClosest = nil
point.isClosest = true
closestPoint = point
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
elseif distance < point.distance then
end
end
cache.lastCellX = cellX
cache.lastCellY = cellY
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
if point.nearby then
if point.nearby then
nearbyCount += 1
nearbyPoints[nearbyCount] = point
end
if point.onEnter and not point.inside then
point.inside = true
point:onEnter()
end
elseif point.currentDistance then
if point.onExit then point:onExit() end
point.inside = nil
point.currentDistance = nil
end
end
if point.onEnter and not point.inside then
point.inside = true
point:onEnter()
end
elseif point.currentDistance then
if point.onExit then point:onExit() end
if not tick then
if nearbyCount ~= 0 then
tick = SetInterval(function()
for i = 1, nearbyCount do
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 then
if point and point.nearby then
point:nearby()
end
end
end)
end
elseif nearbyCount == 0 then
tick = ClearInterval(tick)
end
end
end)
end
elseif nearbyCount == 0 then
tick = ClearInterval(tick)
end
Wait(300)
end
Wait(300)
end
end)
local function toVector(coords)
@ -110,50 +129,52 @@ local function toVector(coords)
return coords
end
lib.points = {
---@return CPoint
---@overload fun(data: PointProperties): CPoint
---@overload fun(coords: vector3, distance: number, data?: PointProperties): CPoint
new = function(...)
local args = {...}
local id = #points + 1
local self
lib.points = {}
-- 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
---@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
self.coords = toVector(self.coords)
self.distance = self.distance or args[2]
-- 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
if args[3] then
for k, v in pairs(args[3]) do
self[k] = v
end
end
self.coords = toVector(self.coords)
self.distance = self.distance or args[2]
self.radius = self.distance
points[id] = self
if args[3] then
for k, v in pairs(args[3]) do
self[k] = v
end
end
return self
end,
lib.grid.addEntry(self)
points[id] = self
getAllPoints = function() return points end,
return self
end
getNearbyPoints = function() return nearbyPoints end,
function lib.points.getAllPoints() return points end
---@return CPoint?
getClosestPoint = function() return closestPoint end,
}
function lib.points.getNearbyPoints() return nearbyPoints end
---@return CPoint?
function lib.points.getClosestPoint() return closestPoint end
---@deprecated
lib.points.closest = lib.points.getClosestPoint

View File

@ -14,8 +14,12 @@ local levelPrefixes = {
'^4[VERBOSE]',
'^6[DEBUG]',
}
local resourcePrintLevel = printLevel[GetConvar('ox:printlevel:' .. cache.resource, GetConvar('ox:printlevel', 'info'))]
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
@ -48,4 +52,14 @@ lib.print = {
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

View File

@ -5,7 +5,7 @@
function lib.requestAudioBank(audioBank, timeout)
return lib.waitFor(function()
if RequestScriptAudioBank(audioBank, false) then return audioBank end
end, ("failed to load audiobank '%s'"):format(audioBank), timeout or 500)
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

View File

@ -0,0 +1,224 @@
---@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

View File

@ -12,12 +12,10 @@ function lib.streamingRequest(request, hasLoaded, assetType, asset, timeout, ...
request(asset, ...)
-- i hate fivem developers
lib.print.verbose(("Loading %s '%s' - remember to release it when done."):format(assetType, asset))
return lib.waitFor(function()
if hasLoaded(asset) then return asset end
end, ("failed to load %s '%s' - this is likely caused by unreleased assets"):format(assetType, asset), timeout or 10000)
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

View File

@ -9,24 +9,29 @@ local pairs = pairs
---@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
else
local matched_values = 0
local values = 0
for _, v1 in pairs(value) do
values += 1
if type(value) ~= 'table' then
for _, v in pairs(tbl) do
if v == value then
return true
end
end
for _, v2 in pairs(tbl) do
if v1 == v2 then matched_values += 1 end
end
end
if matched_values == values then return true end
end
return false
else
local set = {}
return false
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
@ -34,22 +39,28 @@ end
---@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)
local tabletype1 = table.type(t1)
if type1 ~= type2 then return false end
if type1 ~= 'table' and type2 ~= 'table' then return t1 == t2 end
if not tabletype1 then return t1 == t2 end
for k1,v1 in pairs(t1) do
local v2 = t2[k1]
if v2 == nil or not table_matches(v1,v2) then return false end
end
if tabletype1 ~= table.type(t2) or (tabletype1 == 'array' and #t1 ~= #t2) then
return false
end
for k2,v2 in pairs(t2) do
local v1 = t1[k2]
if v1 == nil or not table_matches(v1,v2) then return false end
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
return true
for k in pairs(t2) do
if t1[k] == nil then
return false
end
end
return true
end
---@generic T
@ -57,15 +68,15 @@ end
---@return T
---Recursively clones a table to ensure no table references.
local function table_deepclone(tbl)
tbl = table.clone(tbl)
tbl = table.clone(tbl)
for k, v in pairs(tbl) do
if type(v) == 'table' then
tbl[k] = table_deepclone(v)
end
end
for k, v in pairs(tbl) do
if type(v) == 'table' then
tbl[k] = table_deepclone(v)
end
end
return tbl
return tbl
end
---@param t1 table
@ -74,27 +85,41 @@ end
---@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)
if addDuplicateNumbers == nil then addDuplicateNumbers = true end
for k, v in pairs(t2) do
local type1 = type(t1[k])
local type2 = type(v)
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(t1[k], v, addDuplicateNumbers)
if type1 == 'table' and type2 == 'table' then
table_merge(v1, v2, addDuplicateNumbers)
elseif addDuplicateNumbers and (type1 == 'number' and type2 == 'number') then
t1[k] += v
else
t1[k] = v
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
table.contains = contains
table.matches = table_matches
table.deepclone = table_deepclone
table.merge = table_merge
table.shuffle = shuffle
local frozenNewIndex = function(self) error(('cannot set values on a frozen table (%s)'):format(self), 2) end
local _rawset = rawset

View File

@ -1,6 +1,5 @@
---@class TimerPrivateProps
---@field initialTime number the initial duration of the timer.
---@field onEnd? fun() cb function triggered when the timer finishes
---@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
@ -10,6 +9,7 @@
---@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
@ -26,58 +26,54 @@ function timer:constructor(time, onEnd, async)
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.onEnd = onEnd
self.private.triggerOnEnd = true
self:start(async)
end
function timer:start(async)
if self.private.startTime > 0 then return end
---@protected
function timer:run()
while self:isPaused() or self:getTimeLeft('ms') > 0 do
Wait(0)
end
self.private.startTime = GetGameTimer()
local function tick()
while self:getTimeLeft('ms') > 0 do
while self:isPaused() do
Wait(0)
end
Wait(0)
end
if self.private.triggerOnEnd then
self:onEnd()
end
if async then
Citizen.CreateThreadNow(function()
tick()
end)
else
tick()
end
self.private.triggerOnEnd = true
end
function timer:onEnd()
if self:getTimeLeft('ms') > 0 then return end
function timer:start(async)
if self.private.startTime > 0 then error('Cannot start a timer that is already running') end
if self.private.triggerOnEnd and self.private.onEnd then
self.private:onEnd()
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.triggerOnEnd = triggerOnEnd
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

View File

@ -1,110 +0,0 @@
local glm = require 'glm'
---@type table<number, CZone>
local Zones = {}
_ENV.Zones = Zones
local function removeZone(self)
Zones[self.id] = nil
end
local glm_polygon_contains = glm.polygon.contains
local function contains(self, coords)
return glm_polygon_contains(self.polygon, coords, self.thickness / 4)
end
local function insideSphere(self, coords)
return #(self.coords - coords) < 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
lib.zones = {
---@return CZone
poly = function(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)
data.coords = data.polygon:centroid()
data.type = 'poly'
data.remove = removeZone
data.contains = contains
data.debug = nil
data.debugColour = nil
data.inside = nil
data.onEnter = nil
data.onExit = nil
Zones[data.id] = data
return data
end,
---@return CZone
box = function(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 or 4
data.rotation = quat(data.rotation or 0, vec3(0, 0, 1))
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)
data.type = 'box'
data.remove = removeZone
data.contains = contains
data.debug = nil
data.debugColour = nil
data.inside = nil
data.onEnter = nil
data.onExit = nil
Zones[data.id] = data
return data
end,
---@return CZone
sphere = function(data)
data.id = #Zones + 1
data.coords = convertToVector(data.coords)
data.radius = (data.radius or 2) + 0.0
data.type = 'sphere'
data.remove = removeZone
data.contains = insideSphere
data.debug = nil
data.debugColour = nil
data.inside = nil
data.onEnter = nil
data.onExit = nil
Zones[data.id] = data
return data
end,
getAllZones = function() return Zones end,
}
return lib.zones

View File

@ -1,19 +1,20 @@
local glm = require 'glm'
---@class CZone
---@field id number
---@field coords vector3
---@field distance number
---@field __type 'poly' | 'sphere' | 'box'
---@field debugColour vector4?
---@field setDebug fun(self: CZone, enable?: boolean, colour?: vector)
---@field remove fun()
---@field contains fun(self: CZone, coords?: vector3): boolean
---@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
@ -96,46 +97,66 @@ local function getTriangles(polygon)
return triangles
end
---@type table<number, CZone>
local insideZones = {}
---@type table<number, CZone>
local enteringZones = {}
---@type table<number, CZone>
local exitingZones = {}
local enteringSize = 0
local exitingSize = 0
local tick
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
local function removeZone(self)
Zones[self.id] = nil
insideZones[self.id] = nil
enteringZones[self.id] = nil
exitingZones[self.id] = nil
---@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
table.remove(exitingZones, exitingZones:indexOf(zone))
table.remove(enteringZones, enteringZones:indexOf(zone))
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
for _, zone in pairs(Zones) do
zone.distance = #(zone.coords - coords)
local radius, contains = zone.radius, nil
if cellX ~= cache.lastCellX or cellY ~= cache.lastCellY then
for i = 1, #nearbyZones do
local zone = nearbyZones[i]
if radius then
contains = zone.distance < radius
else
contains = glm_polygon_contains(zone.polygon, coords, zone.thickness / 4)
if zone.insideZone then
local contains = zone:contains(coords, true)
if not contains then
zone.insideZone = false
insideZones[zone.id] = nil
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
enteringSize += 1
enteringZones[enteringSize] = zone
enteringZones:push(zone)
end
if zone.inside or zone.debug then
@ -148,8 +169,7 @@ CreateThread(function()
insideZones[zone.id] = nil
if zone.onExit then
exitingSize += 1
exitingZones[exitingSize] = zone
exitingZones:push(zone)
end
end
@ -159,16 +179,18 @@ CreateThread(function()
end
end
local exitingSize = #exitingZones
local enteringSize = #enteringZones
if exitingSize > 0 then
table.sort(exitingZones, function(a, b)
return a.distance > b.distance
return a.distance < b.distance
end)
for i = 1, exitingSize do
for i = exitingSize, 1, -1 do
exitingZones[i]:onExit()
end
exitingSize = 0
table.wipe(exitingZones)
end
@ -181,7 +203,6 @@ CreateThread(function()
enteringZones[i]:onEnter()
end
enteringSize = 0
table.wipe(enteringZones)
end
@ -242,12 +263,18 @@ local function debugSphere(self)
self.debugColour.g, self.debugColour.b, self.debugColour.a, false, false, 0, false, false, false, false)
end
local function contains(self, coords)
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)
return #(self.coords - coords) < self.radius
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)
@ -291,137 +318,160 @@ local function setDebug(self, bool, colour)
self.debug = self.__type == 'sphere' and debugSphere or debugPoly or nil
end
lib.zones = {
---@return CZone
poly = function(data)
data.id = #Zones + 1
data.thickness = data.thickness or 4
---@param data ZoneProperties
---@return CZone
local function setZone(data)
---@cast data CZone
data.remove = removeZone
data.contains = data.contains or contains
local pointN = #data.points
local points = table.create(pointN, 0)
if lib.context == 'client' then
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
points[i] = convertToVector(data.points[i])
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
if not data.polygon:isPlanar() then
local zCoords = {}
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)
for i = 1, pointN do
local zCoord = points[i].z
return setZone(data)
end
if zCoords[zCoord] then
zCoords[zCoord] += 1
else
zCoords[zCoord] = 1
end
end
---@class BoxZone : ZoneProperties
---@field coords vector3
---@field size? vector3
---@field rotation? number | vector3 | vector4 | matrix
local coordsArray = {}
---@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)
for coord, count in pairs(zCoords) do
coordsArray[#coordsArray + 1] = {
coord = coord,
count = count
}
end
return setZone(data)
end
table.sort(coordsArray, function(a, b)
return a.count > b.count
end)
---@class SphereZone : ZoneProperties
---@field coords vector3
---@field radius? number
local zCoord = coordsArray[1].coord
local averageTo
---@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
for i = 1, #coordsArray do
if coordsArray[i].count < coordsArray[1].count then
averageTo = i - 1
break
end
end
return setZone(data)
end
if averageTo > 1 then
for i = 2, averageTo do
zCoord += coordsArray[i].coord
end
function lib.zones.getAllZones() return Zones end
zCoord /= averageTo
end
function lib.zones.getCurrentZones() return insideZones end
for i = 1, pointN do
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.remove = removeZone
data.contains = contains
data.setDebug = setDebug
if data.debug then
data.debug = nil
data:setDebug(true, data.debugColour)
end
Zones[data.id] = data
return data
end,
---@return CZone
box = function(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 or 4
data.rotation = quat(data.rotation or 0, vec3(0, 0, 1))
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)
data.__type = 'box'
data.remove = removeZone
data.contains = contains
data.setDebug = setDebug
if data.debug then
data.debug = nil
data:setDebug(true, data.debugColour)
end
Zones[data.id] = data
return data
end,
---@return CZone
sphere = function(data)
data.id = #Zones + 1
data.coords = convertToVector(data.coords)
data.radius = (data.radius or 2) + 0.0
data.__type = 'sphere'
data.remove = removeZone
data.contains = insideSphere
data.setDebug = setDebug
if data.debug then
data:setDebug(true, data.debugColour)
end
Zones[data.id] = data
return data
end,
getAllZones = function() return Zones end,
getCurrentZones = function() return insideZones end,
}
function lib.zones.getNearbyZones() return nearbyZones end
return lib.zones

View File

@ -96,11 +96,6 @@ local lib = setmetatable({
__call = call,
})
_ENV.lib = lib
-- Override standard Lua require with our own.
require = lib.require
local intervals = {}
--- Dream of a world where this PR gets accepted.
---@param callback function | number
@ -215,7 +210,9 @@ function lib.onCache(key, cb)
table.insert(cacheEvents[key], cb)
end
_ENV.lib = lib
_ENV.cache = cache
_ENV.require = lib.require
local notifyEvent = ('__ox_notify_%s'):format(cache.resource)

View File

@ -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": "Προγραμματισμένη επανεκκίνηση"
}

View File

@ -1,5 +1,6 @@
{
"language": "Lietuvių",
"settings": "Nustatymai",
"ui": {
"cancel": "Atšaukti",
"close": "Uždaryti",
@ -9,17 +10,17 @@
"locale": "Pakeisti kalbą",
"locale_description": "Dabartinė: ${language} (%s)",
"notification_audio": "Pranešimo garsas",
"notification_position": "Notification position"
"notification_position": "Pranešimo pozicija"
},
"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"
"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",

View File

@ -1,29 +1,30 @@
{
"language": "Nederlands",
"settings": "Instellingen",
"ui": {
"cancel": "Annuleren",
"close": "Sluiten",
"confirm": "Bevestigen",
"more": "Meer...",
"settings": {
"locale": "Change locale",
"locale_description": "Current language: ${language} (%s)",
"notification_audio": "Notification audio",
"notification_position": "Notification position"
"locale": "Taal wijzigen",
"locale_description": "Huidige taal: ${language} (%s)",
"notification_audio": "Meldingsgeluid",
"notification_position": "Meldingspositie"
},
"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"
"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": "Open radial menu",
"cancel_progress": "Cancel current progress bar",
"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",

View File

@ -6,27 +6,27 @@
"confirm": "Confirmar",
"more": "Mais...",
"settings": {
"locale": "Change locale",
"locale_description": "Current language: ${language} (%s)",
"notification_audio": "Notification audio",
"notification_position": "Notification position"
"locale": "Alterar idioma",
"locale_description": "Idioma atual: ${language} (%s)",
"notification_audio": "Áudio de notificação",
"notification_position": "Posição da notificação"
},
"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"
"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": "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"
"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"
}

View File

@ -1,5 +1,6 @@
{
"language": "Türkçe",
"settings": "Ayarlar",
"ui": {
"cancel": "İptal",
"close": "Kapat",

View File

@ -1,5 +1,6 @@
{
"language": "简体中文",
"settings": "设置",
"ui": {
"cancel": "取消",
"close": "关闭",

View File

@ -1,29 +1,30 @@
{
"language": "繁體中文",
"settings": "設置",
"ui": {
"cancel": "取消",
"close": "關閉",
"confirm": "確認",
"more": "更多...",
"settings": {
"locale": "Change locale",
"locale_description": "Current language: ${language} (%s)",
"notification_audio": "Notification audio",
"notification_position": "Notification position"
"locale": "更改語言",
"locale_description": "當前語言: ${language} (%s)",
"notification_audio": "通知提示音",
"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"
"bottom": "底部",
"bottom-left": "左下",
"bottom-right": "右下",
"center-left": "左側居中",
"center-right": "右側居中",
"top": "頂部",
"top-left": "左上",
"top-right": "右上"
}
},
"open_radial_menu": "Open radial menu",
"cancel_progress": "Cancel current progress bar",
"open_radial_menu": "打開輪盤菜單",
"cancel_progress": "取消當前進度條",
"txadmin_announcement": "來自 %s 的伺服器通告",
"txadmin_dm": "來自 %s 的訊息",
"txadmin_warn": "您被 %s 警告了",

View File

@ -26,6 +26,10 @@ CreateThread(function()
local vehicle = GetVehiclePedIsIn(ped, false)
if vehicle > 0 then
if vehicle ~= cache.vehicle then
cache:set('seat', false)
end
cache:set('vehicle', vehicle)
if not cache.seat or GetPedInVehicleSeat(vehicle, cache.seat) ~= ped then

View File

@ -0,0 +1,57 @@
local registeredCallbacks = {}
AddEventHandler('onResourceStop', function(resourceName)
if cache.resource == resourceName then return end
for callbackName, resource in pairs(registeredCallbacks) do
if resource == resourceName then
registeredCallbacks[callbackName] = nil
end
end
end)
---For internal use only.
---Sets a callback event as registered to a specific resource, preventing it from
---being overwritten. Any unknown callbacks will return an error to the caller.
---@param callbackName string
---@param isValid boolean
function lib.setValidCallback(callbackName, isValid)
local resourceName = GetInvokingResource() or cache.resource
local callbackResource = registeredCallbacks[callbackName]
if callbackResource then
if not isValid then
callbackResource[callbackName] = nil
return
end
if callbackResource == resourceName then return end
local errMessage = ("^1resource '%s' attempted to overwrite callback '%s' owned by resource '%s'^0"):format(resourceName, callbackName, callbackResource)
return print(('^1SCRIPT ERROR: %s^0\n%s'):format(errMessage,
Citizen.InvokeNative(`FORMAT_STACK_TRACE` & 0xFFFFFFFF, nil, 0, Citizen.ResultAsString()) or ''))
end
lib.print.verbose(("set valid callback '%s' for resource '%s'"):format(callbackName, resourceName))
registeredCallbacks[callbackName] = resourceName
end
function lib.isCallbackValid(callbackName)
return registeredCallbacks[callbackName] == GetInvokingResource() or cache.resource
end
local cbEvent = '__ox_cb_%s'
RegisterNetEvent('ox_lib:validateCallback', function(callbackName, invokingResource, key)
if registeredCallbacks[callbackName] then return end
local event = cbEvent:format(invokingResource)
if cache.game == 'fxserver' then
return TriggerClientEvent(event, source, key, 'cb_invalid')
end
TriggerServerEvent(event, key, 'cb_invalid')
end)

View File

@ -49,6 +49,11 @@ function lib.showMenu(id, startIndex)
if not menu then
error(('No menu with id %s was found'):format(id))
end
if table.type(menu.options) == 'empty' then
error(('Can\'t open empty menu with id %s'):format(id))
end
if not openMenu then
local control = cache.game == 'fivem' and 140 or 0xE30CD707

View File

@ -30,7 +30,8 @@ local function createProp(ped, prop)
local coords = GetEntityCoords(ped)
local object = CreateObject(prop.model, coords.x, coords.y, coords.z, false, false, false)
AttachEntityToEntity(object, ped, GetPedBoneIndex(ped, prop.bone or 60309), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, prop.rotOrder or 0, true)
AttachEntityToEntity(object, ped, GetPedBoneIndex(ped, prop.bone or 60309), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true,
true, false, true, prop.rotOrder or 0, true)
SetModelAsNoLongerNeeded(prop.model)
return object
@ -62,6 +63,7 @@ local controls = {
INPUT_VEH_MOUSE_CONTROL_OVERRIDE = isFivem and 106 or 0x39CCABD5
}
---@param data ProgressProps
local function startProgress(data)
playerState.invBusy = true
progress = data
@ -71,10 +73,11 @@ local function startProgress(data)
if anim.dict then
lib.requestAnimDict(anim.dict)
TaskPlayAnim(cache.ped, anim.dict, anim.clip, anim.blendIn or 3.0, anim.blendOut or 1.0, anim.duration or -1, anim.flag or 49, anim.playbackRate or 0, anim.lockX, anim.lockY, anim.lockZ)
TaskPlayAnim(cache.ped, anim.dict, anim.clip, anim.blendIn or 3.0, anim.blendOut or 1.0, anim.duration or -1, anim.flag or 49, anim.playbackRate or 0,
anim.lockX, anim.lockY, anim.lockZ)
RemoveAnimDict(anim.dict)
elseif anim.scenario then
TaskStartScenarioInPlace(cache.ped, anim.scenario, 0, anim.playEnter ~= nil and anim.playEnter or true)
TaskStartScenarioInPlace(cache.ped, anim.scenario, 0, anim.playEnter == nil or anim.playEnter --[[@as boolean]])
end
end
@ -83,6 +86,7 @@ local function startProgress(data)
end
local disable = data.disable
local startTime = GetGameTimer()
while progress do
if disable then
@ -138,8 +142,9 @@ local function startProgress(data)
end
playerState.invBusy = false
local duration = progress ~= false and GetGameTimer() - startTime + 100 -- give slight leeway
if progress == false then
if progress == false or duration <= data.duration then
SendNUIMessage({ action = 'progressCancel' })
return false
end
@ -234,22 +239,22 @@ AddStateBagChangeHandler('lib:progressProps', nil, function(bagName, key, value,
local ped = GetPlayerPed(ply)
local serverId = GetPlayerServerId(ply)
if not value then
return deleteProgressProps(serverId)
end
createdProps[serverId] = {}
local playerProps = createdProps[serverId]
if value.model then
playerProps[#playerProps+1] = createProp(ped, value)
playerProps[#playerProps + 1] = createProp(ped, value)
else
for i = 1, #value do
local prop = value[i]
if prop then
playerProps[#playerProps+1] = createProp(ped, prop)
playerProps[#playerProps + 1] = createProp(ped, prop)
end
end
end

View File

@ -80,6 +80,7 @@ if cache.game == 'redm' then return end
---@field modLivery? number
---@field modRoofLivery? number
---@field modLightbar? number
---@field livery? number
---@field windows? number[]
---@field doors? number[]
---@field tyres? table<number | string, 1 | 2>
@ -151,13 +152,6 @@ function lib.getVehicleProperties(vehicle)
end
end
local modLiveryCount = GetVehicleLiveryCount(vehicle)
local modLivery = GetVehicleLivery(vehicle)
if modLiveryCount == -1 or modLivery == -1 then
modLivery = GetVehicleMod(vehicle, 48)
end
local damage = {
windows = {},
doors = {},
@ -273,9 +267,10 @@ function lib.getVehicleProperties(vehicle)
modTank = GetVehicleMod(vehicle, 45),
modWindows = GetVehicleMod(vehicle, 46),
modDoorR = GetVehicleMod(vehicle, 47),
modLivery = modLivery,
modLivery = GetVehicleMod(vehicle, 48),
modRoofLivery = GetVehicleRoofLivery(vehicle),
modLightbar = GetVehicleMod(vehicle, 49),
livery = GetVehicleLivery(vehicle),
windows = damage.windows,
doors = damage.doors,
tyres = damage.tyres,
@ -348,7 +343,7 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle)
ClearVehicleCustomPrimaryColour(vehicle)
SetVehicleColours(vehicle, props.color1 --[[@as number]], colorSecondary --[[@as number]])
else
if props.paintType1 then SetVehicleModColor_1(vehicle, props.paintType1, colorPrimary, pearlescentColor) end
if props.paintType1 then SetVehicleModColor_1(vehicle, props.paintType1, 0, props.pearlescentColor or 0) end
SetVehicleCustomPrimaryColour(vehicle, props.color1[1], props.color1[2], props.color1[3])
end
@ -359,7 +354,7 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle)
ClearVehicleCustomSecondaryColour(vehicle)
SetVehicleColours(vehicle, props.color1 or colorPrimary --[[@as number]], props.color2 --[[@as number]])
else
if props.paintType2 then SetVehicleModColor_2(vehicle, props.paintType2, colorSecondary) end
if props.paintType2 then SetVehicleModColor_2(vehicle, props.paintType2, 0) end
SetVehicleCustomSecondaryColour(vehicle, props.color2[1], props.color2[2], props.color2[3])
end
@ -623,7 +618,6 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle)
if props.modLivery then
SetVehicleMod(vehicle, 48, props.modLivery, false)
SetVehicleLivery(vehicle, props.modLivery)
end
if props.modRoofLivery then
@ -634,6 +628,10 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle)
SetVehicleMod(vehicle, 49, props.modLightbar, false)
end
if props.livery then
SetVehicleLivery(vehicle, props.livery)
end
if props.bulletProofTyres ~= nil then
SetVehicleTyresCanBurst(vehicle, props.bulletProofTyres)
end

View File

@ -37,6 +37,10 @@ exports.ox_target:addPolyZone({
})
]]
local function formatNumber(num)
return tostring(num):gsub(",", ".")
end
local parse = {
poly = function(data)
local points = {}
@ -82,18 +86,34 @@ local parse = {
pattern = {
'local box = lib.zones.box({\n',
('\tname = "%s",\n'):format(data.name),
('\tcoords = vec3(%s, %s, %s),\n'):format(data.xCoord, data.yCoord, data.zCoord),
('\tsize = vec3(%s, %s, %s),\n'):format(data.width, data.length, data.height),
('\trotation = %s,\n'):format(data.heading),
('\tcoords = vec3(%s, %s, %s),\n'):format(
formatNumber(data.xCoord),
formatNumber(data.yCoord),
formatNumber(data.zCoord)
),
('\tsize = vec3(%s, %s, %s),\n'):format(
formatNumber(data.width),
formatNumber(data.length),
formatNumber(data.height)
),
('\trotation = %s,\n'):format(formatNumber(data.heading)),
'})\n',
}
elseif data.format == 'array' then
pattern = {
'{\n',
('\tname = "%s",\n'):format(data.name),
('\tcoords = vec3(%s, %s, %s),\n'):format(data.xCoord, data.yCoord, data.zCoord),
('\tsize = vec3(%s, %s, %s),\n'):format(data.width, data.length, data.height),
('\trotation = %s,\n'):format(data.heading),
('\tcoords = vec3(%s, %s, %s),\n'):format(
formatNumber(data.xCoord),
formatNumber(data.yCoord),
formatNumber(data.zCoord)
),
('\tsize = vec3(%s, %s, %s),\n'):format(
formatNumber(data.width),
formatNumber(data.length),
formatNumber(data.height)
),
('\trotation = %s,\n'):format(formatNumber(data.heading)),
'},\n',
}
elseif data.format == 'target' then

View File

@ -1 +0,0 @@
@import"https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;500;700&display=swap";@import"https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500&display=swap";@import"https://fonts.googleapis.com/css2?family=Fira+Mono&display=swap";@import"https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap";@import"https://fonts.googleapis.com/css2?family=Nunito:wght@300;400;500;600;700&display=swap";@import"https://use.typekit.net/wxh5ury.css";@import"https://use.typekit.net/qgr5ebd.css";html{color-scheme:normal!important}body{background:none!important;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;height:100vh;user-select:none;overflow:hidden!important}p{margin:0}#root{height:100%}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}@keyframes progress-bar{0%{width:0%}to{width:100%}}::-webkit-scrollbar{background-color:transparent;padding:0;margin:0;width:0;height:0}.toast-inform{background-color:#2980b9}.toast-success{background-color:#27ae60}.toast-error{background-color:#c0392b}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,8 +5,8 @@
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NUI React Boilerplate</title>
<script type="module" crossorigin src="./assets/index-K9iBfj89.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-3x7-Y31P.css">
<script type="module" crossorigin src="./assets/index-DA6-Nmx5.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BgkLwDpx.css">
</head>
<body>
<div id="root"></div>

File diff suppressed because one or more lines are too long