update libs
This commit is contained in:
parent
ff18b299fd
commit
e4b7186910
@ -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.'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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
|
||||
|
||||
87
resources/[core]/ox_lib/imports/dui/client.lua
Normal file
87
resources/[core]/ox_lib/imports/dui/client.lua
Normal 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
|
||||
48
resources/[core]/ox_lib/imports/getRelativeCoords/shared.lua
Normal file
48
resources/[core]/ox_lib/imports/getRelativeCoords/shared.lua
Normal 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
|
||||
194
resources/[core]/ox_lib/imports/grid/shared.lua
Normal file
194
resources/[core]/ox_lib/imports/grid/shared.lua
Normal 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
|
||||
@ -5,9 +5,6 @@ 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
|
||||
|
||||
@ -152,9 +149,9 @@ function lib.marker.new(options)
|
||||
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.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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
224
resources/[core]/ox_lib/imports/scaleform/client.lua
Normal file
224
resources/[core]/ox_lib/imports/scaleform/client.lua
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
|
||||
34
resources/[core]/ox_lib/locales/el.json
Normal file
34
resources/[core]/ox_lib/locales/el.json
Normal 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": "Προγραμματισμένη επανεκκίνηση"
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"language": "Türkçe",
|
||||
"settings": "Ayarlar",
|
||||
"ui": {
|
||||
"cancel": "İptal",
|
||||
"close": "Kapat",
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
{
|
||||
"language": "简体中文",
|
||||
"settings": "设置",
|
||||
"ui": {
|
||||
"cancel": "取消",
|
||||
"close": "关闭",
|
||||
|
||||
@ -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 警告了",
|
||||
|
||||
@ -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
|
||||
|
||||
57
resources/[core]/ox_lib/resource/callbacks/shared.lua
Normal file
57
resources/[core]/ox_lib/resource/callbacks/shared.lua
Normal 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)
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
@ -243,13 +248,13 @@ AddStateBagChangeHandler('lib:progressProps', nil, function(bagName, key, value,
|
||||
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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
1041
resources/[core]/ox_lib/web/build/assets/index-DA6-Nmx5.js
Normal file
1041
resources/[core]/ox_lib/web/build/assets/index-DA6-Nmx5.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Black.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Black.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-BlackItalic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Bold.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-BoldItalic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ExtraBold.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ExtraBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ExtraLight.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ExtraLight.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Italic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Light.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-LightItalic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-LightItalic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Medium.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Medium.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-MediumItalic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Regular.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Regular.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-SemiBold.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-SemiBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Thin.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-Thin.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ThinItalic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/Roboto-ThinItalic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Bold.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Italic.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Italic.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Light.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Light.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Medium.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Medium.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Regular.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-SemiBold.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-SemiBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Thin.ttf
Normal file
BIN
resources/[core]/ox_lib/web/build/fonts/RobotoMono-Thin.ttf
Normal file
Binary file not shown.
Binary file not shown.
@ -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
Reference in New Issue
Block a user