7 Commits

Author SHA1 Message Date
ANTOND.
a8bf225b3d ci(create-release): run publish in package folder 2026-02-20 01:44:50 +01:00
ANTOND.
1e02469d9a ci(create-release): use npm trusted publisher 2026-02-20 01:35:10 +01:00
github-actions
4b2446f7a8 chore: bump version to v3.32.3 2026-02-19 21:34:58 +00:00
Senlar
f33c613b6d Fix: Skillcheck animation speed inconsistencies by switching to time-… (#66)
* Fix: Skillcheck animation speed inconsistencies by switching to time-based RAF loop

This PR fixes long-standing issues with the skillcheck mini-game animation speed being inconsistent across machines and after long play sessions. Some players reported extremely slow indicator movement, others extremely fast, and many noticed that the speed changed unpredictably the longer they stayed logged in.

The root cause was that the skillcheck relied on a useInterval tick (setInterval-like behavior) and assumed it fired every 1ms. In reality, browser timer clamping and throttling make interval timing highly unpredictable — especially in embedded CEF browsers like FiveM’s NUI.

This PR replaces tick-based animation with a time-based requestAnimationFrame loop using performance.now(), ensuring perfectly consistent animation timing across all hardware and browser states. I have used this method in other NUI based skillcheck scripts to address this same behavior.

Signed-off-by: Senlar <brandonrhue@gmail.com>

* Refactor keyHandler and clean up code

Signed-off-by: Senlar <brandonrhue@gmail.com>

* Refactor keyHandler and cleanup useEffect logic again

Signed-off-by: Senlar <brandonrhue@gmail.com>

---------

Signed-off-by: Senlar <brandonrhue@gmail.com>
2026-02-11 18:22:54 +01:00
Maximus7474
e66b6805bc fix(package): added an array check on callback args (#85)
-> Issue is that if callaback returns a null value it errors and fails
2026-02-11 18:07:42 +01:00
BerkieBb
fdc1674f4f fix(interface/menu): incosistent menu function types (#80)
Signed-off-by: BerkieBb <82737367+BerkieBb@users.noreply.github.com>
2026-01-19 23:47:42 +01:00
Angelo Rodrigues De Morais Barroso
c8db003cbc Add lockState support to vehicle properties (#83)
Hello,

I’d like to propose a small improvement to vehicle properties handling.

This change adds support for the vehicle door lock state (lockState) by:
- Including lockState in the VehicleProperties type
- Reading the value using GetVehicleDoorLockStatus in getVehicleProperties
- Applying the value using SetVehicleDoorsLocked in setVehicleProperties

This allows vehicle lock status to be properly saved and restored alongside other vehicle properties.

Let me know if this fits the project direction or if any adjustments are needed.
This allows the vehicle lock status to be properly saved and restored alongside other vehicle properties.

Note: Two additional commits and pull requests were accidentally opened during this process and have already been closed. Sorry for the noise.

Thank you!

Signed-off-by: Angelo Rodrigues De Morais Barroso <115129101+Angelo90810@users.noreply.github.com>
2026-01-19 23:46:37 +01:00
8 changed files with 102 additions and 34 deletions

View File

@@ -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 }}

View File

@@ -6,7 +6,7 @@ rdr3_warning 'I acknowledge that this is a prerelease build of RedM, and I am aw
name 'ox_lib'
author 'Overextended'
version '3.32.2'
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.'

View File

@@ -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);
};

View File

@@ -1,7 +1,7 @@
{
"name": "@communityox/ox_lib",
"author": "Overextended",
"version": "3.32.2",
"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": {

View File

@@ -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);
};

View File

@@ -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

View File

@@ -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

View File

@@ -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} />;
};