-- ====================================================================================== -- System: CitizenFX Thread Scheduler for Multi Theft Auto 1.6 (With Lua Coroutines) -- Author: marlondev -- Description: -- Provides functions to create “threads” (coroutines) that can: -- * Wait X milliseconds (Wait function) -- * Wait X frame ticks (WaitTick function) -- * Wait X game frames (WaitFrames function) -- * Be resumed manually (ResumeThread function) -- * Be consulted if they are waiting (IsWaiting function) -- * Execute a callback in the next game frame (RunNextFrame function) -- ====================================================================================== if type (CreateThread) == 'function' then return true end local CLIENT = isElement(localPlayer); local setTimer, isTimer, killTimer = setTimer, isTimer, killTimer; local coroutine = coroutine; local assert = assert or (function() end) local outputDebugString = outputDebugString or (function() end); local Threads = setmetatable({}, { __mode = 'k' }) local Threads_Tick = setmetatable({}, { __mode = 'k' }) local _coroutine_resume = coroutine.resume function coroutine.resume(...) local state,result = _coroutine_resume(...) if not state then outputDebugString( tostring(result), 1 ) -- Output error message end return state,result end function CreateThread(func, ...) assert(type(func) == 'function', 'Expected a function for the thread.') local co = coroutine.create(function(...) return func(...) end) assert(co, 'Failed to create coroutine.') local ErrorCallback = select(1, ...) if type(ErrorCallback) == 'function' then local args = {...} table.remove(args, 1) local status, err = coroutine.resume(co, unpack(args)) if not status then ErrorCallback(status, err) end return co, status, err end return co, coroutine.resume(co, ...) end function RapidThread(func, ...) assert(type(func) == 'function', 'Expected a function for the thread.') return coroutine.wrap(function(...) return func(...) end)(...) end local THREAD_TICK_RUNNING_RENDER = false; local function onRenderWaitTick() if next(Threads_Tick) == nil then removeEventHandler('onClientPreRender', root, onRenderWaitTick) THREAD_TICK_RUNNING_RENDER = false; return end local NOW = getTickCount(); local NOW_C = os.clock(); local ThreadsToTick = Threads_Tick; local pairs = pairs; local pcall = pcall; local TableToIterate = {} for co, v in pairs(ThreadsToTick) do TableToIterate[#TableToIterate + 1] = {co = co, v = v} end for i = #TableToIterate, 1, -1 do local item = TableToIterate[i] local co = item.co local v = item.v if v.Expires then if v.Type == 'Ticks' then if v.Expires <= NOW then if type (v.Callback) == 'function' then pcall(v.Callback, co); else ThreadsToTick[co] = nil; end end elseif v.Type == 'Frames' then if v.Expires <= 0 then if type (v.Callback) == 'function' then pcall(v.Callback, co); else ThreadsToTick[co] = nil; end else v.Expires = v.Expires - 1; end elseif v.Type == 'Clocks' then if v.Expires <= NOW_C then if type (v.Callback) == 'function' then pcall(v.Callback, co); else ThreadsToTick[co] = nil; end end elseif v.Type == 'Until' then local ok = false if v.Predicate then local okPred, res = pcall(v.Predicate) ok = okPred and res and true or false end if ok or (v.TimeoutAt and NOW >= v.TimeoutAt) then Threads_Tick[co] = nil pcall( function(co, ok) return coroutine.resume(co, ok) -- retorna true/false pro caller end, co, ok ) end end end end if next(ThreadsToTick) == nil then removeEventHandler('onClientPreRender', root, onRenderWaitTick) THREAD_TICK_RUNNING_RENDER = false; end end local function ensureLoop() if not THREAD_TICK_RUNNING_RENDER then addEventHandler('onClientPreRender', root, onRenderWaitTick, true, 'low-999') THREAD_TICK_RUNNING_RENDER = true end end function Wait(ms, errorCallback ,...) local co = coroutine.running() assert(co, 'Wait can only be used in a coroutine!') assert(type(ms) == 'number', 'Expected a number for ms.') if Threads_Tick[co] then Threads_Tick[co] = nil end if isTimer(Threads[co]) then killTimer(Threads[co]) Threads[co] = nil end Threads[co] = setTimer(function(...) local status, err = coroutine.resume(co, ...); if Threads[co] then Threads[co] = nil end if type(errorCallback) == 'function' then errorCallback(co) end if not status then outputDebugString(tostring(err), 4, 255, 0, 0) end end, ms, 1, ...) if not isTimer(Threads[co]) then outputDebugString('Failed to create timer for coroutine.', 4, 255, 0, 0) else coroutine.yield(); end return status, err end function WaitSeconds(Seconds, ...) assert(type(Seconds) == 'number', 'WaitSeconds: Seconds not a number') return Wait(Seconds * 1000) end function WaitTick(TickCount, errorCallback) local co = coroutine.running(); assert(CLIENT, 'WaitTick can only be used in a client-side coroutine!') assert(co, 'WaitTick can only be used in a coroutine!') assert(type(TickCount) == 'number', 'Expected a number for TickCount.') if TickCount <= 0 then TickCount = 1 end if Threads[co] then killTimer(Threads[co]) Threads[co] = nil end if type(Threads_Tick[co]) == 'table' then Threads_Tick[co] = nil end local callback = function(co) local info = Threads_Tick[co]; if not info then return end Threads_Tick[co] = nil local status, err = coroutine.resume(co); if not status then if type(info.ErrorCallback) == 'function' then info.ErrorCallback(co) end end end Threads_Tick[co] = { Expires = getTickCount() + TickCount, Callback = callback, ErrorCallback = errorCallback, Type = 'Ticks' } ensureLoop() coroutine.yield() end function WaitClock(ClockCount, errorCallback) local co = coroutine.running(); assert(CLIENT, 'WaitTick can only be used in a client-side coroutine!') assert(co, 'WaitTick can only be used in a coroutine!') assert(type(ClockCount) == 'number', 'Expected a number for ClockCount.') if ClockCount <= 0 then ClockCount = 1 end if Threads[co] then killTimer(Threads[co]) Threads[co] = nil end if type(Threads_Tick[co]) == 'table' then Threads_Tick[co] = nil end local callback = function(co) local info = Threads_Tick[co]; if not info then return end Threads_Tick[co] = nil local status, err = coroutine.resume(co); if not status then if type(info.ErrorCallback) == 'function' then info.ErrorCallback(co) end end end Threads_Tick[co] = { Expires = os.clock() + ClockCount, Callback = callback, ErrorCallback = errorCallback, Type = 'Clocks' } ensureLoop() coroutine.yield() end function WaitFrames(FrameCount, errorCallback) local co = coroutine.running(); assert(CLIENT, 'WaitTick can only be used in a client-side coroutine!') assert(co, 'WaitTick can only be used in a coroutine!') assert(type(FrameCount) == 'number', 'Expected a number for FrameCount.') if FrameCount <= 0 then FrameCount = 1 end if Threads[co] then killTimer(Threads[co]) Threads[co] = nil end if type(Threads_Tick[co]) == 'table' then Threads_Tick[co] = nil end local callback = function(co) local info = Threads_Tick[co]; if not info then return end Threads_Tick[co] = nil local status, err = coroutine.resume(co); if not status then if type(info.ErrorCallback) == 'function' then info.ErrorCallback(co) end end end Threads_Tick[co] = { Expires = FrameCount, Callback = callback, ErrorCallback = errorCallback, Type = 'Frames' } ensureLoop() coroutine.yield() end -- 1) WaitUntil(predicate[, timeoutMs]) -> retorna true se predicate ficou true, false se expirou function WaitUntil(Predicate, TimeoutMs) local co = coroutine.running() assert(co, 'WaitUntil in coroutine') assert(type(Predicate) == 'function', 'Expected function') local item = { Type='Until', Predicate = predicate } if TimeoutMs and TimeoutMs > 0 then item.TimeoutAt = _getTick() + TimeoutMs end Threads_Tick[co] = item ensureLoop() return coroutine.yield() end -- 2) WaitForEvent(EventName[, Element][, TimeoutMs]) -- Zero polling: usa um handler 1x e retoma a thread function WaitForEvent(EventName, Element, TimeoutMs) local co = coroutine.running() assert(co, 'WaitForEvent in coroutine') assert(type(EventName) == 'string', 'Expected event name') local fired = false local function onceHandler(...) if fired then return end fired = true if Threads_Tick[co] then Threads_Tick[co] = nil end removeEventHandler(EventName, Element or root, onceHandler) coroutine.resume(co, true, ...) end addEventHandler(EventName, (Element or root), onceHandler) if TimeoutMs and TimeoutMs > 0 then Threads_Tick[co] = { Type = 'Ticks', Expires = _getTick() + TimeoutMs, Callback = function() if fired then return end if Threads_Tick[co] then Threads_Tick[co] = nil end fired = true removeEventHandler(EventName, Element or root, onceHandler) coroutine.resume(co, false, 'timeout') end } ensureLoop() end return coroutine.yield() end function ResumeThread(co, ...) assert(co, 'Expected a coroutine.') assert(Threads[co], 'Thread is not waiting.') if Threads_Tick[co] then Threads_Tick[co] = nil end if Threads[co] then if isTimer(Threads[co]) then killTimer(Threads[co]) end Threads[co] = nil end local status, err = coroutine.resume(co, ...) if not status then outputDebugString('Error in thread: ' .. tostring(err), 4, 255, 0, 0) end return status, err end function IsWaiting(co) return (Threads[co] and isTimer(Threads[co])) or (Threads_Tick[co] ~= nil) end function getThread() local Thread = coroutine.running() return type(Thread) == 'thread' and Thread or nil end function isThreadValid(co) if not co then co = getThread() end return type(co) == 'thread' and true or false end function getThreadStatus(co) if not co then co = getThread() end if not co then return false, 'Thread not found' end return coroutine.status(co), IsWaiting(co) end function RunNextFrame(callback) if type(callback) ~= 'function' then return false, 'Invalid callback' end if CLIENT then local wrapper = false local callbackk = callback; wrapper = function () callbackk() removeEventHandler("onClientPreRender", root, wrapper) end addEventHandler('onClientPreRender', root, wrapper) return true else setTimer(function() callback() end, 1, 1) return true; end end