Skip to content

Instantly share code, notes, and snippets.

@Earu
Last active August 22, 2021 10:34
Show Gist options
  • Save Earu/c0bc385077cd166ee4fa8c4f93fbb5b6 to your computer and use it in GitHub Desktop.
Save Earu/c0bc385077cd166ee4fa8c4f93fbb5b6 to your computer and use it in GitHub Desktop.
POC for a Garry's Mod asynchronous event library.
--[[
MAIN DIFFERENCES WITH THE SYNCHRONOUS HOOK LIBRARY
- You cannot add a hook callback to an event that is being executed
example:
hook.Add("Think", "example", function()
hook.Add("Think", "example2", function() end)
end)
In this case the hook would be added from the next call of the Think event
and not in the callback.
- Because of the nature of async workflows and coroutines, you cannot run game loop dependent
event like "Think", "Tick" or "HUDPaint". They would end up stacking, and stacking, anything
lower than the CALL_INTERVAL will never be ran.
- This is probably slower than the vanilla hook library.
]]--
module ("hook_async", package.seeall)
CALL_INTERVAL = 0.1 -- This can be changed depending on how much time you want to pass between each calls
local hooks = {}
function GetTable()
return hooks
end
local add_queue = {}
local mutex = {}
local function unlock(event_name)
mutex[event_name] = mutex[event_name] - 1
if mutex[event_name] > 0 then return end
mutex[event_name] = nil -- this can be here because this part of the code is synchronous,
-- meaning the rest of this function will always be run accordingly
-- when it happens
if not add_queue[event_name] then return end
hooks[event_name] = hooks[event_name] or {}
for _, callback_data in ipairs(add_queue[event_name]) do
hooks[event_name][callback_data.identifier] = callback_data.callback
end
add_queue[event_name] = nil
end
local isstring = isstring
local isfunction = isfunction
function Add(event_name, callback_identifier, callback)
if not isstring(event_name) or not isfunction(callback) then return end
if mutex[event_name] then
add_queue[event_name] = add_queue[event_name] or {}
table.insert(add_queue[event_name], { identifier = callback_identifier, callback = callback })
else
hooks[event_name] = hooks[event_name] or {}
hooks[event_name][callback_identifier] = callback
end
end
function Remove(event_name, callback_identifier)
if not isstring(event_name) then return end
if hooks[event_name] then
hooks[event_name][callback_identifier] = nil
end
end
function Run(event_name, on_finished, ...)
return Call(event_name, gmod and gmod.GetGamemode(), on_finished, ...)
end
local co_create = coroutine.create
local co_yield = coroutine.yield
local co_resume = coroutine.resume
local co_status = coroutine.status
local pairs = pairs
local IsValid = IsValid
function Call(event_name, gm, on_finished, ...)
on_finished = isfunction(on_finished) and on_finished or function() end
local hook_callbacks = hooks[event_name]
if not hook_callbacks then return on_finished() end
mutex[event_name] = (mutex[event_name] or 0) + 1
local co = co_create(function(hook_callbacks, ...)
local a, b, c, d, e, f
for identifier, callback in pairs(hook_callbacks) do
if isstring(identifier) then
a, b, c, d, e, f = callback(...)
else
if IsValid(identifier) then
a, b, c, d, e, f = callback(identifier, ...)
else
hook_callbacks[identifier] = nil
end
end
if a ~= nil then
on_finished(a, b, c, d, e, f)
unlock(event_name)
return
else
co_yield()
end
end
if not gm then
unlock(event_name)
return on_finished()
end
local gm_func = gm[event_name]
if not gm_func then
unlock(event_name)
return on_finished()
end
on_finished(gm_func(gm, ...))
unlock(event_name)
end)
local function resume(...)
local ok, err = co_resume(co, hook_callbacks, ...)
if not ok then
error(debug.traceback(co, err))
else
if co_status(co) == "dead" then return end
timer.Simple(CALL_INTERVAL, resume)
end
end
resume(...)
return co
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment