Files
esx_core/[core]/esx_progressbar/Progress.lua
T
Selt 07b9253401 fix(esx_progressbar): honour the requested duration and ignore cancelled runs
The processing thread counted the remaining time down in 1000 ms steps and read
the global CurrentProgress on every wake, which caused two problems.

Any duration that is not a whole number of seconds finished late, since the
thread only checked after another full second. The NUI bar runs on wall clock
time, so the bar completed and the player then stood frozen waiting for onFinish.

Cancelling did not stop the thread either. CancelProgressbar clears
CurrentProgress, but the sleeping thread only notices up to a second later, and if
a new bar was started in that window it saw a non-nil CurrentProgress and kept
decrementing the new one alongside its own thread. The new bar then finished at
roughly half its length, firing onFinish early.

The run now carries an id, so a thread stops as soon as its run is no longer the
current one, and the end is an absolute timestamp rather than a countdown.

Measured in game on artifact 25770, requested versus actual onFinish:
  before: 500 -> 1011, 4200 -> 5042, cancel then 3000 -> 1427
  after:  500 ->  519, 4200 -> 4222, cancel then 3000 -> 3058
2026-07-27 21:51:15 +02:00

93 lines
2.9 KiB
Lua

---@class ProgressBarAnimation
---@field public type "anim" | "Scenario"
---@field public dict? string
---@field public lib? string
---@class ProgressBarOptions
---@field public animation ProgressBarAnimation
---@field public FreezePlayer? boolean
---@field public onFinish? function
local CurrentProgress = nil
local progressCount = 0
local function startProcessing(id)
while CurrentProgress ~= nil and CurrentProgress.id == id do
if GetGameTimer() < CurrentProgress.finishAt then
Wait(50)
else
ClearPedTasks(ESX.PlayerData.ped)
if CurrentProgress.FreezePlayer then
FreezeEntityPosition(ESX.PlayerData.ped, false)
end
if CurrentProgress.onFinish then
CurrentProgress.onFinish()
end
CurrentProgress = nil
end
end
end
---@param message string
---@param length? number Timeout in milliseconds
---@param Options? ProgressBarOptions
---@return boolean Success
local function Progressbar(message, length, Options)
if CurrentProgress then
return false
end
CurrentProgress = Options or {}
if CurrentProgress.animation then
if CurrentProgress.animation.type == "anim" then
ESX.Streaming.RequestAnimDict(CurrentProgress.animation.dict, function()
TaskPlayAnim(ESX.PlayerData.ped, CurrentProgress.animation.dict, CurrentProgress.animation.lib, 1.0, 1.0, length, 1, 1.0, false, false, false)
RemoveAnimDict(CurrentProgress.animation.dict)
end)
elseif CurrentProgress.animation.type == "Scenario" then
TaskStartScenarioInPlace(ESX.PlayerData.ped, CurrentProgress.animation.Scenario, 0, true)
end
end
if CurrentProgress.FreezePlayer then
FreezeEntityPosition(ESX.PlayerData.ped, CurrentProgress.FreezePlayer)
end
SendNUIMessage({
type = "Progressbar",
length = length or 3000,
message = message or "ESX-Framework",
})
progressCount = progressCount + 1
CurrentProgress.id = progressCount
CurrentProgress.length = length or 3000
CurrentProgress.finishAt = GetGameTimer() + CurrentProgress.length
local id = progressCount
CreateThread(function()
startProcessing(id)
end)
return true;
end
local function CancelProgressbar()
if not CurrentProgress then
return
end
SendNUIMessage({
type = "Close",
})
ClearPedTasks(ESX.PlayerData.ped)
if CurrentProgress.FreezePlayer then
FreezeEntityPosition(ESX.PlayerData.ped, false)
end
if CurrentProgress.onCancel then
CurrentProgress.onCancel()
end
CurrentProgress.canceled = true
CurrentProgress.length = 0
CurrentProgress = nil
end
ESX.RegisterInput("cancelprog", "[ProgressBar] Cancel Progressbar", "keyboard", "BACK", CancelProgressbar)
exports("Progressbar", Progressbar)
exports("CancelProgressbar", CancelProgressbar)