diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07eb70ed..d224187ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,10 +91,3 @@ jobs: env: CI: false GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Publish npm package - uses: JS-DevTools/npm-publish@v1 - with: - token: ${{ secrets.NPM_TOKEN }} - package: './package/package.json' - access: 'public' diff --git a/fxmanifest.lua b/fxmanifest.lua index 2c7e87a83..c1259bce7 100644 --- a/fxmanifest.lua +++ b/fxmanifest.lua @@ -10,7 +10,7 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw -- name 'ox_lib' author 'Overextended' -version '3.19.2' +version '3.20.1' license 'LGPL-3.0-or-later' repository 'https://github.com/overextended/ox_lib' description 'A library of shared functions to utilise in other resources.' diff --git a/imports/array/shared.lua b/imports/array/shared.lua index b39100dcc..44fbf33e7 100644 --- a/imports/array/shared.lua +++ b/imports/array/shared.lua @@ -1,6 +1,8 @@ ---@class Array : OxClass local Array = lib.class('Array') +---@alias ArrayLike Array | { [number]: T } + ---@private function Array:constructor(...) local arr = { ... } @@ -18,7 +20,7 @@ function Array:__newindex(index, value) end ---Create a new array containing the elements from two arrays. ----@param arr Array | any[] +---@param arr ArrayLike function Array:merge(arr) local newArr = table.clone(self) local length = #self @@ -167,6 +169,21 @@ function Array:reduce(reducer, initialValue) return accumulator end +---Returns true if the given table is an instance of array or an array-like table. +---@param tbl ArrayLike +---@return boolean +function Array.isArray(tbl) + if not type(tbl) == 'table' then return false end + + local tableType = table.type(tbl) + + if tableType == 'array' or tableType == 'empty' or Array.instanceOf(tbl, Array) then + return true + end + + return false +end + lib.array = Array return lib.array diff --git a/imports/class/shared.lua b/imports/class/shared.lua index bcbd258a6..1fed0906b 100644 --- a/imports/class/shared.lua +++ b/imports/class/shared.lua @@ -85,7 +85,7 @@ function mixins.new(class, ...) __index = function(self, index) local di = getinfo(2, 'n') - if di.namewhat ~= 'method' then return end + if di.namewhat == 'local' then return end return private[index] end, diff --git a/imports/locale/shared.lua b/imports/locale/shared.lua index 946e34357..02a2c0590 100644 --- a/imports/locale/shared.lua +++ b/imports/locale/shared.lua @@ -1,19 +1,21 @@ ---@type { [string]: string } local dict = {} ----@param prefix string|nil ---@param source { [string]: string } ---@param target { [string]: string } -local function flattenDict(prefix, source, target) +---@param prefix? string +local function flattenDict(source, target, prefix) for key, value in pairs(source) do - local fullKey = prefix and (prefix .. "." .. key) or key + local fullKey = prefix and (prefix .. '.' .. key) or key - if type(value) == "table" then - flattenDict(fullKey, value, target) + if type(value) == 'table' then + flattenDict(value, target, fullKey) else target[fullKey] = value end end + + return target end ---@param str string @@ -37,32 +39,31 @@ function lib.getLocales() return dict end ----Loads the ox_lib locale module. Prefer using fxmanifest instead (see [docs](https://overextended.dev/ox_lib#usage)). -function lib.locale(key) - local lang = key or lib.getLocaleKey() - local locales = json.decode(LoadResourceFile(cache.resource, ('locales/%s.json'):format(lang))) +local function loadLocale(key) + local data = LoadResourceFile(cache.resource, ('locales/%s.json'):format(key)) - table.wipe(dict) + if not data then + warn(("could not load 'locales/%s.json'"):format(key)) + end - if not locales then - local warning = "could not load 'locales/%s.json'" - warn(warning:format(lang)) + return json.decode(data) or {} +end - if lang ~= 'en' then - locales = json.decode(LoadResourceFile(cache.resource, 'locales/en.json')) +local table = lib.table - if not locales then - warn(warning:format('en')) - end - end +---Loads the ox_lib locale module. Prefer using fxmanifest instead (see [docs](https://overextended.dev/ox_lib#usage)). +---@param key string +function lib.locale(key) + local lang = key or lib.getLocaleKey() + local locales = loadLocale('en') - if not locales then return end + if lang ~= 'en' then + table.merge(locales, loadLocale(lang)) end - local flattenedDict = {} - flattenDict(nil, locales, flattenedDict) + table.wipe(dict) - for k, v in pairs(flattenedDict) do + for k, v in pairs(flattenDict(locales, {})) do if type(v) == 'string' then for var in v:gmatch('${[%w%s%p]-}') do local locale = locales[var:sub(3, -2)] diff --git a/imports/playAnim/client.lua b/imports/playAnim/client.lua new file mode 100644 index 000000000..59e223787 --- /dev/null +++ b/imports/playAnim/client.lua @@ -0,0 +1,70 @@ +---@alias AnimationFlags number +---| 0 DEFAULT +---| 1 LOOPING +---| 2 HOLD_LAST_FRAME +---| 4 REPOSITION_WHEN_FINISHED +---| 8 NOT_INTERRUPTABLE +---| 16 UPPERBODY +---| 32 SECONDARY +---| 64 REORIENT_WHEN_FINISHED +---| 128 ABORT_ON_PED_MOVEMENT +---| 256 ADDITIVE +---| 512 TURN_OFF_COLLISION +---| 1024 OVERRIDE_PHYSICS +---| 2048 IGNORE_GRAVITY +---| 4096 EXTRACT_INITIAL_OFFSET +---| 8192 EXIT_AFTER_INTERRUPTED +---| 16384 TAG_SYNC_IN +---| 32768 TAG_SYNC_OUT +---| 65536 TAG_SYNC_CONTINUOUS +---| 131072 FORCE_START +---| 262144 USE_KINEMATIC_PHYSICS +---| 524288 USE_MOVER_EXTRACTION +---| 1048576 HIDE_WEAPON +---| 2097152 ENDS_IN_DEAD_POSE +---| 4194304 ACTIVATE_RAGDOLL_ON_COLLISION +---| 8388608 DONT_EXIT_ON_DEATH +---| 16777216 ABORT_ON_WEAPON_DAMAGE +---| 33554432 DISABLE_FORCED_PHYSICS_UPDATE +---| 67108864 PROCESS_ATTACHMENTS_ON_START +---| 134217728 EXPAND_PED_CAPSULE_FROM_SKELETON +---| 268435456 USE_ALTERNATIVE_FP_ANIM +---| 536870912 BLENDOUT_WRT_LAST_FRAME +---| 1073741824 USE_FULL_BLENDING + +---@alias ControlFlags number +---| 0 NONE +---| 1 DISABLE_LEG_IK +---| 2 DISABLE_ARM_IK +---| 4 DISABLE_HEAD_IK +---| 8 DISABLE_TORSO_IK +---| 16 DISABLE_TORSO_REACT_IK +---| 32 USE_LEG_ALLOW_TAGS +---| 64 USE_LEG_BLOCK_TAGS +---| 128 USE_ARM_ALLOW_TAGS +---| 256 USE_ARM_BLOCK_TAGS +---| 512 PROCESS_WEAPON_HAND_GRIP +---| 1024 USE_FP_ARM_LEFT +---| 2048 USE_FP_ARM_RIGHT +---| 4096 DISABLE_TORSO_VEHICLE_IK +---| 8192 LINKED_FACIAL + +---@param ped number +---@param animDictionary string +---@param animationName string +---@param blendInSpeed? number Defaults to 8.0 +---@param blendOutSpeed? number Defaults to -8.0 +---@param duration? integer Defaults to -1 +---@param animFlags? AnimationFlags +---@param startPhase? number +---@param phaseControlled? boolean +---@param controlFlags? integer +---@param overrideCloneUpdate? boolean +function lib.playAnim(ped, animDictionary, animationName, blendInSpeed, blendOutSpeed, duration, animFlags, startPhase, phaseControlled, controlFlags, overrideCloneUpdate) + lib.requestAnimDict(animDictionary) + ---@diagnostic disable-next-line: param-type-mismatch + TaskPlayAnim(ped, animDictionary, animationName, blendInSpeed or 8.0, blendOutSpeed or -8.0, duration or -1, animFlags or 0, startPhase or 0.0, phaseControlled or false, controlFlags or 0, overrideCloneUpdate or false) + RemoveAnimDict(animDictionary) +end + +return lib.playAnim diff --git a/imports/requestAnimDict/client.lua b/imports/requestAnimDict/client.lua index 622e48883..51ea5fd84 100644 --- a/imports/requestAnimDict/client.lua +++ b/imports/requestAnimDict/client.lua @@ -1,6 +1,6 @@ ---Load an animation dictionary. When called from a thread, it will yield until it has loaded. ---@param animDict string ----@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 10000. ---@return string animDict function lib.requestAnimDict(animDict, timeout) if HasAnimDictLoaded(animDict) then return animDict end diff --git a/imports/requestAnimSet/client.lua b/imports/requestAnimSet/client.lua index 11d4b1c8b..062eab2bd 100644 --- a/imports/requestAnimSet/client.lua +++ b/imports/requestAnimSet/client.lua @@ -1,6 +1,6 @@ ---Load an animation clipset. When called from a thread, it will yield until it has loaded. ---@param animSet string ----@param timeout number? Approximate milliseconds to wait for the clipset to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the clipset to load. Default is 10000. ---@return string animSet function lib.requestAnimSet(animSet, timeout) if HasAnimSetLoaded(animSet) then return animSet end diff --git a/imports/requestModel/client.lua b/imports/requestModel/client.lua index 4994bea17..64ba263f9 100644 --- a/imports/requestModel/client.lua +++ b/imports/requestModel/client.lua @@ -1,6 +1,6 @@ ---Load a model. When called from a thread, it will yield until it has loaded. ---@param model number | string ----@param timeout number? Approximate milliseconds to wait for the model to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the model to load. Default is 10000. ---@return number model function lib.requestModel(model, timeout) if type(model) ~= 'number' then model = joaat(model) end diff --git a/imports/requestNamedPtfxAsset/client.lua b/imports/requestNamedPtfxAsset/client.lua index 450433eaa..ef9451df1 100644 --- a/imports/requestNamedPtfxAsset/client.lua +++ b/imports/requestNamedPtfxAsset/client.lua @@ -1,6 +1,6 @@ ---Load a named particle effect. When called from a thread, it will yield until it has loaded. ---@param ptFxName string ----@param timeout number? Approximate milliseconds to wait for the particle effect to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the particle effect to load. Default is 10000. ---@return string ptFxName function lib.requestNamedPtfxAsset(ptFxName, timeout) if HasNamedPtfxAssetLoaded(ptFxName) then return ptFxName end diff --git a/imports/requestStreamedTextureDict/client.lua b/imports/requestStreamedTextureDict/client.lua index 64cfd44ec..eefe75cd4 100644 --- a/imports/requestStreamedTextureDict/client.lua +++ b/imports/requestStreamedTextureDict/client.lua @@ -1,6 +1,6 @@ ---Load a texture dictionary. When called from a thread, it will yield until it has loaded. ---@param textureDict string ----@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the dictionary to load. Default is 10000. ---@return string textureDict function lib.requestStreamedTextureDict(textureDict, timeout) if HasStreamedTextureDictLoaded(textureDict) then return textureDict end diff --git a/imports/requestWeaponAsset/client.lua b/imports/requestWeaponAsset/client.lua index 349df8c39..61965c0a0 100644 --- a/imports/requestWeaponAsset/client.lua +++ b/imports/requestWeaponAsset/client.lua @@ -17,7 +17,7 @@ ---Load a weapon asset. When called from a thread, it will yield until it has loaded. ---@param weaponType string | number ----@param timeout number? Approximate milliseconds to wait for the asset to load. Default is 1000. +---@param timeout number? Approximate milliseconds to wait for the asset to load. Default is 10000. ---@param weaponResourceFlags WeaponResourceFlags? Default is 31. ---@param extraWeaponComponentFlags ExtraWeaponComponentFlags? Default is 0. ---@return string | number weaponType diff --git a/imports/streamingRequest/client.lua b/imports/streamingRequest/client.lua index 38b748e39..71677cbe3 100644 --- a/imports/streamingRequest/client.lua +++ b/imports/streamingRequest/client.lua @@ -18,7 +18,7 @@ function lib.streamingRequest(request, hasLoaded, assetType, asset, timeout, ... 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) + end, ("failed to load %s '%s' - this is likely caused by unreleased assets"):format(assetType, asset), timeout or 10000) end return lib.streamingRequest diff --git a/imports/timer/shared.lua b/imports/timer/shared.lua new file mode 100644 index 000000000..3f559533f --- /dev/null +++ b/imports/timer/shared.lua @@ -0,0 +1,153 @@ +---@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 +---@field currentTimeLeft number current timer length +---@field paused boolean the pause state of the timer + +---@class OxTimer : OxClass +---@field private private TimerPrivateProps +---@field start fun(self: self, async?: boolean) starts the timer +---@field forceEnd fun(self: self, triggerOnEnd: boolean) end timer early and optionally trigger the onEnd function still +---@field isPaused fun(self: self): boolean returns wether the timer is paused or not +---@field pause fun(self: self) pauses the timer until play method is called +---@field play fun(self: self) resumes the timer if paused +---@field getTimeLeft fun(self: self, format?: 'ms' | 's' | 'm' | 'h'): number | table returns the time left on the timer with the specified format rounded to 2 decimal places (miliseconds, seconds, minutes, hours). returns a table of all if not specified. +local timer = lib.class('OxTimer') + +---@private +---@param time number +---@param onEnd fun(self: OxTimer) +---@param async? boolean +function timer:constructor(time, onEnd, async) + assert(type(time) == "number" and time > 0, "Time must be a positive number") + assert(onEnd == nil or type(onEnd) == "function", "onEnd must be a function or nil") + assert(type(async) == "boolean" or async == nil, "async must be a boolean or nil") + + self.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 + + self.private.startTime = GetGameTimer() + + local function tick(instance) + while true do + while instance.private.paused do + Wait(0) + end + + if instance:getTimeLeft('ms') <= 0 then + break + end + + Wait(0) + end + end + + if async then + Citizen.CreateThreadNow(function() + tick(self) + self:onEnd() + end) + else + tick(self) + self:onEnd() + end +end + +function timer:onEnd() + if self:getTimeLeft('ms') > 0 then return end + + if self.private.triggerOnEnd and self.private.onEnd then + self.private:onEnd() + 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 + Wait(0) +end + +function timer:pause() + if self.private.paused then return end + self.private.currentTimeLeft = self:getTimeLeft('ms') --[[@as number]] + self.private.paused = true +end + +function timer:play() + if not self.private.paused then return end + self.private.startTime = GetGameTimer() + self.private.paused = false +end + +function timer:isPaused() + return self.private.paused +end + +function timer:restart() + self:forceEnd(false) + Wait(0) + self.private.currentTimeLeft = self.private.initialTime + self.private.startTime = 0 + self:start() +end + +function timer:getTimeLeft(format) + local ms = self.private.currentTimeLeft - (GetGameTimer() - self.private.startTime) + + local roundedfloat = function(value) + return tonumber(string.format('%.2f', value)) + end + + if format == 'ms' then + return roundedfloat(ms) + end + + local s = ms / 1000 + + if format == 's' then + return roundedfloat(s) + end + + local m = s / 60 + + if format == 'm' then + return roundedfloat(m) + end + + local h = m / 60 + + if format == 'h' then + return roundedfloat(h) + end + + return { + ms = roundedfloat(ms), + s = roundedfloat(s), + m = roundedfloat(m), + h = roundedfloat(h) + } +end + +---@param time number +---@param onEnd fun(self: OxTimer) +---@param async? boolean +function lib.timer(time, onEnd, async) + return timer:new(time, onEnd, async) +end + +return lib.timer diff --git a/imports/triggerClientEvent/server.lua b/imports/triggerClientEvent/server.lua index 3294339f9..fc49af025 100644 --- a/imports/triggerClientEvent/server.lua +++ b/imports/triggerClientEvent/server.lua @@ -3,13 +3,13 @@ --- ---Provides non-neglibible performance gains due to msgpacking all arguments _once_, instead of per-target. ---@param eventName string ----@param targetIds number | number[] +---@param targetIds number | ArrayLike ---@param ... any function lib.triggerClientEvent(eventName, targetIds, ...) local payload = msgpack.pack_args(...) local payloadLen = #payload - if type(targetIds) == 'table' and table.type(targetIds) == 'array' then + if lib.array.isArray(targetIds) then for i = 1, #targetIds do TriggerClientEventInternal(eventName, targetIds[i] --[[@as string]], payload, payloadLen) end diff --git a/locales/en.json b/locales/en.json index 3790401f3..35f52734f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,5 +1,6 @@ { "language": "English", + "settings": "Settings", "ui": { "cancel": "Cancel", "close": "Close", diff --git a/resource/interface/client/progress.lua b/resource/interface/client/progress.lua index ec404a0f8..e50563eec 100644 --- a/resource/interface/client/progress.lua +++ b/resource/interface/client/progress.lua @@ -28,8 +28,7 @@ local function createProp(ped, prop) lib.requestModel(prop.model) 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, 0, true) + AttachEntityToEntity(object, ped, GetEntityBoneIndexByName(PlayerPedId(), (prop.boneName or "SKEL_R_HAND")), prop.pos.x, prop.pos.y, prop.pos.z, prop.rot.x, prop.rot.y, prop.rot.z, true, true, false, true, 0, true) SetModelAsNoLongerNeeded(prop.model) return object @@ -76,7 +75,6 @@ local function startProgress(data) TaskStartScenarioInPlace(cache.ped, anim.scenario, 0, anim.playEnter ~= nil and anim.playEnter or true) end end - if data.prop then playerState:set('lib:progressProps', data.prop, true) end @@ -205,7 +203,7 @@ RegisterCommand('cancelprogress', function() if progress?.canCancel then progress = false end end) -RegisterKeyMapping('cancelprogress', locale('cancel_progress'), 'keyboard', 'x') +--RegisterKeyMapping('cancelprogress', locale('cancel_progress'), 'keyboard', 'x') local function deleteProgressProps(serverId) local playerProps = createdProps[serverId] diff --git a/resource/settings.lua b/resource/settings.lua index 73a5c4ccb..ccc707150 100644 --- a/resource/settings.lua +++ b/resource/settings.lua @@ -1,9 +1,19 @@ +-- Some users have locale set from ox_lib v2 +if GetResourceKvpInt('reset_locale') ~= 1 then + DeleteResourceKvp('locale') + SetResourceKvpInt('reset_locale', 1) +end + local settings = { - locale = GetResourceKvpString('locale') or GetConvar('ox:locale', 'en'), + default_locale = GetConvar('ox:locale', 'en'), notification_position = GetResourceKvpString('notification_position') or 'top-right', notification_audio = GetResourceKvpInt('notification_audio') == 1 } +local userLocales = GetConvarInt('ox:userLocales', 1) == 1 + +settings.locale = userLocales and GetResourceKvpString('locale') or settings.default_locale + local function set(key, value) if settings[key] == value then return false end @@ -28,16 +38,11 @@ local function set(key, value) end RegisterCommand('ox_lib', function() - local input = lib.inputDialog('Settings', { + local inputSettings = { { - type = 'select', - label = locale('ui.settings.locale'), - searchable = true, - description = locale('ui.settings.locale_description', settings.locale), - options = GlobalState['ox_lib:locales'], - default = settings.locale, - required = true, - icon = 'book', + type = 'checkbox', + label = locale('ui.settings.notification_audio'), + checked = settings.notification_audio, }, { type = 'select', @@ -56,17 +61,28 @@ RegisterCommand('ox_lib', function() required = true, icon = 'message', }, - { - type = 'checkbox', - label = locale('ui.settings.notification_audio'), - checked = settings.notification_audio, - } - }) --[[@as table?]] + } + + if userLocales then + table.insert(inputSettings, + { + type = 'select', + label = locale('ui.settings.locale'), + searchable = true, + description = locale('ui.settings.locale_description', settings.locale), + options = GlobalState['ox_lib:locales'], + default = settings.locale, + required = true, + icon = 'book', + }) + end + + local input = lib.inputDialog(locale('settings'), inputSettings) --[[@as table?]] if not input then return end - ---@type string, string, boolean - local locale, notification_position, notification_audio = table.unpack(input) + ---@type boolean, string, string + local notification_audio, notification_position, locale = table.unpack(input) if set('locale', locale) then lib.setLocale(locale) end diff --git a/resource/vehicleProperties/client.lua b/resource/vehicleProperties/client.lua index b33398569..ead1ce9da 100644 --- a/resource/vehicleProperties/client.lua +++ b/resource/vehicleProperties/client.lua @@ -284,16 +284,10 @@ end ---@param vehicle number ---@param props VehicleProperties ---@param fixVehicle? boolean Fix the vehicle after props have been set. Usually required when adding extras. ----@return boolean? +---@return boolean isEntityOwner True if the entity is networked and the client is the current entity owner. function lib.setVehicleProperties(vehicle, props, fixVehicle) if not DoesEntityExist(vehicle) then - error(("Unable to set vehicle properties for '%s' (entity does not exist)"): - format(vehicle)) - end - - if NetworkGetEntityIsNetworked(vehicle) and NetworkGetEntityOwner(vehicle) ~= cache.playerId then - error(( - "Unable to set vehicle properties for '%s' (client is not entity owner)"):format(vehicle)) + error(("Unable to set vehicle properties for '%s' (entity does not exist)"):format(vehicle)) end local colorPrimary, colorSecondary = GetVehicleColours(vehicle) @@ -643,5 +637,5 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle) SetVehicleFixed(vehicle) end - return true + return not NetworkGetEntityIsNetworked(vehicle) or NetworkGetEntityOwner(vehicle) == cache.playerId end