mirror of
https://github.com/CommunityOx/ox_lib.git
synced 2026-08-16 22:46:03 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8bf225b3d | ||
|
|
1e02469d9a | ||
|
|
4b2446f7a8 | ||
|
|
f33c613b6d | ||
|
|
e66b6805bc | ||
|
|
fdc1674f4f | ||
|
|
c8db003cbc | ||
|
|
ae16b78bc4 | ||
|
|
3ea020cc50 | ||
|
|
db0bd16f0f |
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -5,6 +5,10 @@ on:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
if: github.actor_id != 210085057
|
||||
@@ -86,8 +90,12 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.ref_name }}
|
||||
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Publish package to npm registry
|
||||
run: bun publish --access public
|
||||
run: npm publish --access public
|
||||
working-directory: package
|
||||
env:
|
||||
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
@@ -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.32.1'
|
||||
version '3.32.3'
|
||||
license 'LGPL-3.0-or-later'
|
||||
repository 'https://github.com/communityox/ox_lib'
|
||||
description 'A library of shared functions to utilise in other resources.'
|
||||
|
||||
@@ -128,17 +128,43 @@ if service == 'fivemanage' then
|
||||
end)
|
||||
end
|
||||
|
||||
local metadata = {
|
||||
hostname = hostname,
|
||||
service = event,
|
||||
source = source,
|
||||
}
|
||||
|
||||
local playerTags = formatTags(source, nil)
|
||||
if playerTags and type(playerTags) == 'string' then
|
||||
local tempTable = { string.strsplit(',', playerTags) }
|
||||
for _, v in pairs(tempTable) do
|
||||
local key, value = string.strsplit(':', v)
|
||||
if key and value then
|
||||
metadata[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local args = { ... }
|
||||
for _, arg in pairs(args) do
|
||||
if type(arg) == 'table' then
|
||||
for k, v in pairs(arg) do
|
||||
metadata[k] = v
|
||||
end
|
||||
elseif type(arg) == 'string' then
|
||||
local key, value = string.strsplit(':', arg)
|
||||
if key and value then
|
||||
metadata[key] = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
bufferSize += 1
|
||||
buffer[bufferSize] = {
|
||||
level = "info",
|
||||
message = message,
|
||||
resource = cache.resource,
|
||||
metadata = {
|
||||
hostname = hostname,
|
||||
service = event,
|
||||
source = source,
|
||||
tags = formatTags(source, ... and string.strjoin(',', string.tostringall(...)) or nil),
|
||||
}
|
||||
metadata = metadata,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
175
imports/selector/shared.lua
Normal file
175
imports/selector/shared.lua
Normal file
@@ -0,0 +1,175 @@
|
||||
---@alias OxSelectorItem {[1]: number, [2]: any}
|
||||
---@alias OxSelectorSet OxSelectorItem[]
|
||||
|
||||
---@class OxSelector: OxClass
|
||||
---@field private sets OxSelectorSet | table<string, OxSelectorItem[]>
|
||||
---@field private totalWeights table<string, number>
|
||||
local OxSelector = lib.class("OxSelector")
|
||||
|
||||
local DEFAULT_SET = 'default'
|
||||
local deepClone = lib.table.deepclone
|
||||
|
||||
|
||||
local function calculateTotalWeight(set)
|
||||
local total = 0
|
||||
for i = 1, #set do
|
||||
local item = set[i]
|
||||
assert(type(item) == "table", "Each OxSelectorItem must be a table")
|
||||
local weight = item[1]
|
||||
assert(type(weight) == "number" and weight >= 0, "weight must be 0 or more")
|
||||
total += weight
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
|
||||
---@param sets OxSelectorSet | table<string, OxSelectorItem[]>
|
||||
function OxSelector:constructor(sets)
|
||||
if type(sets) ~= "table" then
|
||||
lib.print.error("Invalid sets provided to OxSelector constructor")
|
||||
end
|
||||
|
||||
if lib.table.type(sets) == "array" then
|
||||
sets = { [DEFAULT_SET] = sets }
|
||||
end
|
||||
|
||||
self.private.totalWeights = {}
|
||||
self.private.sets = {}
|
||||
|
||||
for setName, set in pairs(sets) do
|
||||
assert(type(set) == "table" and lib.table.type(set) == "array", "Each set must be an array of OxSelectorItem")
|
||||
assert(#set > 0, "Each set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(set)
|
||||
end
|
||||
|
||||
self.private.sets = sets
|
||||
end
|
||||
|
||||
--- Get a random non-weighted item from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem?
|
||||
function OxSelector:getRandom(setName)
|
||||
local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET]
|
||||
if not set then return nil end
|
||||
local item = set[math.random(#set)][2]
|
||||
|
||||
return type(item) == "table" and deepClone(item) or item
|
||||
end
|
||||
|
||||
--- Get a random weighted item from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem?
|
||||
function OxSelector:getRandomWeighted(setName)
|
||||
local set = (setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET]
|
||||
if not set then return nil end
|
||||
|
||||
local totalWeight = self.private.totalWeights[setName or DEFAULT_SET]
|
||||
if totalWeight == 0 then return nil end
|
||||
|
||||
local randomWeight = math.random() * totalWeight
|
||||
local cumulativeWeight = 0
|
||||
|
||||
for i = 1, #set do
|
||||
cumulativeWeight = cumulativeWeight + set[i][1]
|
||||
if randomWeight <= cumulativeWeight then
|
||||
local item = set[i][2]
|
||||
return type(item) == "table" and deepClone(item) or item
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- get multiple non-weighted random items from a specific set
|
||||
---@param setName? string
|
||||
---@param count number
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getRandomAmount(setName, count)
|
||||
assert(type(count) == "number" and count > 0, "Count must be a positive number")
|
||||
local items = {}
|
||||
for _ = 1, count do
|
||||
local item = self:getRandom(setName)
|
||||
if item then
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
--- get multiple weighted random items from a specific set
|
||||
---@param setName? string
|
||||
---@param count number
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getRandomWeightedAmount(setName, count)
|
||||
assert(type(count) == "number" and count > 0, "Count must be a positive number")
|
||||
local items = {}
|
||||
for _ = 1, count do
|
||||
local item = self:getRandomWeighted(setName)
|
||||
if item then
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
return items
|
||||
end
|
||||
|
||||
--- get all items from a specific set
|
||||
---@param setName? string
|
||||
---@return OxSelectorItem[]
|
||||
function OxSelector:getSet(setName)
|
||||
return deepClone((setName and self.private.sets[setName]) or self.private.sets[DEFAULT_SET])
|
||||
end
|
||||
|
||||
--- get all sets
|
||||
---@return table<string, OxSelectorItem[]>
|
||||
function OxSelector:getAllSets()
|
||||
return deepClone(self.private.sets)
|
||||
end
|
||||
|
||||
--- add a new set
|
||||
---@param setName string
|
||||
---@param items OxSelectorItem[]
|
||||
function OxSelector:addSet(setName, items)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
if self.private.sets[setName] then
|
||||
lib.print.error("Selector set '" .. setName .. "' already exists.")
|
||||
return
|
||||
end
|
||||
|
||||
assert(type(items) == "table" and lib.table.type(items) == "array", "items must be an array")
|
||||
assert(#items > 0, "set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(items)
|
||||
self.private.sets[setName] = items
|
||||
end
|
||||
|
||||
--- update an existing set
|
||||
---@param setName string
|
||||
---@param newItems OxSelectorItem[]
|
||||
function OxSelector:updateSet(setName, newItems)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
if not self.private.sets[setName] then
|
||||
lib.print.error("Selector set '" .. setName .. "' does not exist.")
|
||||
return
|
||||
end
|
||||
|
||||
assert(type(newItems) == "table" and lib.table.type(newItems) == "array", "newItems must be an array")
|
||||
assert(#newItems > 0, "set must contain at least one OxSelectorItem")
|
||||
|
||||
self.private.totalWeights[setName] = calculateTotalWeight(newItems)
|
||||
self.private.sets[setName] = newItems
|
||||
end
|
||||
|
||||
--- remove a set
|
||||
---@param setName string
|
||||
function OxSelector:removeSet(setName)
|
||||
assert(type(setName) == "string", "setName must be a string")
|
||||
|
||||
self.private.totalWeights[setName] = nil
|
||||
self.private.sets[setName] = nil
|
||||
end
|
||||
|
||||
lib.selector = OxSelector
|
||||
return lib.selector
|
||||
@@ -47,7 +47,7 @@ export function triggerServerCallback<T = unknown>(
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pendingCallbacks[key] = (args) => {
|
||||
if (args[0] === 'cb_invalid') reject(`callback '${eventName} does not exist`);
|
||||
if (Array.isArray(args) && args[0] === 'cb_invalid') reject(`callback '${eventName} does not exist`);
|
||||
|
||||
resolve(args);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@communityox/ox_lib",
|
||||
"author": "Overextended",
|
||||
"version": "3.32.1",
|
||||
"version": "3.32.3",
|
||||
"description": "JS/TS wrapper for ox_lib exports",
|
||||
"main": "./shared/index.js",
|
||||
"types": "./shared/index.d.ts",
|
||||
@@ -21,10 +21,10 @@
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/communityox/ox_lib.git"
|
||||
"url": "git+https://github.com/CommunityOx/ox_lib.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/communityox/ox_lib/issues"
|
||||
"url": "https://github.com/CommunityOx/ox_lib/issues"
|
||||
},
|
||||
"license": "LGPL-3.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -29,7 +29,7 @@ export function triggerClientCallback<T = unknown>(
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
pendingCallbacks[key] = (args) => {
|
||||
if (args[0] === 'cb_invalid') reject(`callback '${eventName} does not exist`);
|
||||
if (Array.isArray(args) && args[0] === 'cb_invalid') reject(`callback '${eventName} does not exist`);
|
||||
|
||||
resolve(args);
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ local openMenu
|
||||
|
||||
---@alias MenuPosition 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
|
||||
---@alias MenuChangeFunction fun(selected: number, scrollIndex?: number, args?: any, checked?: boolean)
|
||||
---@alias MenuScrollSelectChangeFunction fun(selected: number, scrollIndex?: number, args?: any)
|
||||
|
||||
---@class MenuOptions
|
||||
---@field label string
|
||||
@@ -35,9 +36,9 @@ local openMenu
|
||||
---@field disableInput? boolean
|
||||
---@field canClose? boolean
|
||||
---@field onClose? fun(keyPressed?: 'Escape' | 'Backspace')
|
||||
---@field onSelected? MenuChangeFunction
|
||||
---@field onSideScroll? MenuChangeFunction
|
||||
---@field onCheck? MenuChangeFunction
|
||||
---@field onSelected? MenuScrollSelectChangeFunction
|
||||
---@field onSideScroll? MenuScrollSelectChangeFunction
|
||||
---@field onCheck? fun(selected: number, checked: boolean, args?: any)
|
||||
---@field cb? MenuChangeFunction
|
||||
|
||||
---@param data MenuProps
|
||||
|
||||
@@ -12,6 +12,7 @@ if cache.game == 'redm' then return end
|
||||
---@field model? number
|
||||
---@field plate? string
|
||||
---@field plateIndex? number
|
||||
---@field lockState? number
|
||||
---@field bodyHealth? number
|
||||
---@field engineHealth? number
|
||||
---@field tankHealth? number
|
||||
@@ -202,6 +203,7 @@ function lib.getVehicleProperties(vehicle)
|
||||
model = GetEntityModel(vehicle),
|
||||
plate = GetVehicleNumberPlateText(vehicle),
|
||||
plateIndex = GetVehicleNumberPlateTextIndex(vehicle),
|
||||
lockState = GetVehicleDoorLockStatus(vehicle),
|
||||
bodyHealth = math.floor(GetVehicleBodyHealth(vehicle) + 0.5),
|
||||
engineHealth = math.floor(GetVehicleEngineHealth(vehicle) + 0.5),
|
||||
tankHealth = math.floor(GetVehiclePetrolTankHealth(vehicle) + 0.5),
|
||||
@@ -322,6 +324,10 @@ function lib.setVehicleProperties(vehicle, props, fixVehicle)
|
||||
SetVehicleNumberPlateTextIndex(vehicle, props.plateIndex)
|
||||
end
|
||||
|
||||
if props.lockState ~= nil then
|
||||
SetVehicleDoorsLocked(vehicle, props.lockState)
|
||||
end
|
||||
|
||||
if props.bodyHealth then
|
||||
SetVehicleBodyHealth(vehicle, props.bodyHealth + 0.0)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { SkillCheckProps } from '../../typings';
|
||||
import { useInterval } from '@mantine/hooks';
|
||||
|
||||
interface Props {
|
||||
angle: number;
|
||||
@@ -11,15 +10,58 @@ interface Props {
|
||||
handleComplete: (success: boolean) => void;
|
||||
}
|
||||
|
||||
const Indicator: React.FC<Props> = ({ angle, offset, multiplier, handleComplete, skillCheck, className }) => {
|
||||
const BASE_DURATION_MS = 2000;
|
||||
|
||||
const Indicator: React.FC<Props> = ({
|
||||
angle,
|
||||
offset,
|
||||
multiplier,
|
||||
handleComplete,
|
||||
skillCheck,
|
||||
className,
|
||||
}) => {
|
||||
const [indicatorAngle, setIndicatorAngle] = useState(-90);
|
||||
const [keyPressed, setKeyPressed] = useState<false | string>(false);
|
||||
const interval = useInterval(
|
||||
() =>
|
||||
setIndicatorAngle((prevState) => {
|
||||
return (prevState += multiplier);
|
||||
}),
|
||||
1
|
||||
|
||||
const rafIdRef = useRef<number | null>(null);
|
||||
const startTimeRef = useRef<number | null>(null);
|
||||
const completedRef = useRef(false);
|
||||
|
||||
const stopAnimation = () => {
|
||||
if (rafIdRef.current !== null) {
|
||||
cancelAnimationFrame(rafIdRef.current);
|
||||
rafIdRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
const animate = useCallback(
|
||||
(time: number) => {
|
||||
if (completedRef.current) return;
|
||||
|
||||
if (startTimeRef.current === null) {
|
||||
startTimeRef.current = time;
|
||||
}
|
||||
|
||||
const elapsed = time - startTimeRef.current;
|
||||
|
||||
const speed = Math.max(multiplier || 0, 0.0001);
|
||||
const duration = BASE_DURATION_MS / speed;
|
||||
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const newAngle = -90 + progress * 360;
|
||||
|
||||
setIndicatorAngle(newAngle);
|
||||
|
||||
if (newAngle + 90 >= 360) {
|
||||
completedRef.current = true;
|
||||
stopAnimation();
|
||||
handleComplete(false);
|
||||
return;
|
||||
}
|
||||
|
||||
rafIdRef.current = requestAnimationFrame(animate);
|
||||
},
|
||||
[multiplier, handleComplete]
|
||||
);
|
||||
const keyHandler = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
@@ -42,32 +84,43 @@ const Indicator: React.FC<Props> = ({ angle, offset, multiplier, handleComplete,
|
||||
|
||||
useEffect(() => {
|
||||
setIndicatorAngle(-90);
|
||||
startTimeRef.current = null;
|
||||
completedRef.current = false;
|
||||
|
||||
window.addEventListener('keydown', keyHandler);
|
||||
interval.start();
|
||||
}, [skillCheck]);
|
||||
rafIdRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
stopAnimation();
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
startTimeRef.current = null;
|
||||
completedRef.current = true;
|
||||
};
|
||||
}, [skillCheck, keyHandler, animate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (indicatorAngle + 90 >= 360) {
|
||||
interval.stop();
|
||||
handleComplete(false);
|
||||
}
|
||||
}, [indicatorAngle]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!keyPressed) return;
|
||||
if (!keyPressed || completedRef.current) return;
|
||||
|
||||
if (skillCheck.keys && !skillCheck.keys?.includes(keyPressed)) return;
|
||||
|
||||
interval.stop();
|
||||
|
||||
stopAnimation();
|
||||
window.removeEventListener('keydown', keyHandler);
|
||||
completedRef.current = true;
|
||||
|
||||
if (keyPressed !== skillCheck.key || indicatorAngle < angle || indicatorAngle > angle + offset)
|
||||
handleComplete(false);
|
||||
else handleComplete(true);
|
||||
|
||||
setKeyPressed(false);
|
||||
}, [keyPressed]);
|
||||
}, [
|
||||
keyPressed,
|
||||
angle,
|
||||
offset,
|
||||
indicatorAngle,
|
||||
skillCheck,
|
||||
keyHandler,
|
||||
handleComplete,
|
||||
]);
|
||||
|
||||
return <circle transform={`rotate(${indicatorAngle}, 250, 250)`} className={className} />;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user