--@TODO: maybe update_function_hook_chain() pass entry instead of name local vmf = get_mod("VMF") HOOKED_FUNCTIONS = HOOKED_FUNCTIONS or {} -- global, because 'loadstring' doesn't see local variables @TODO: or just HOOKED_FUNCTIONS = {} -- #################################################################################################################### -- ##### Private functions ############################################################################################ -- #################################################################################################################### local function get_function_by_name(function_name) local _, value = pcall(loadstring("return " .. function_name)) -- no need to check status of 'pcall' - if there will be error, it's gonna be string instead of function -- also, it can be anything else instead of function, even if 'loadstring' run will be successful, so check it if type(value) == "function" then return value else return nil end end local function create_hooked_function_entry(hooked_function_name) local hooked_function = get_function_by_name(hooked_function_name) if not hooked_function then return nil end local hooked_function_entry = {} hooked_function_entry.name = hooked_function_name hooked_function_entry.original_function = hooked_function hooked_function_entry.exec_function = hooked_function hooked_function_entry.hooks = {} table.insert(HOOKED_FUNCTIONS, hooked_function_entry) return hooked_function_entry end local function create_hook_entry(mod, hooked_function_entry, hook_function) local hook_entry = {} hook_entry.mod = mod hook_entry.hook_function = hook_function hook_entry.exec_function = nil hook_entry.is_enabled = true table.insert(hooked_function_entry.hooks, hook_entry) --return hook_entry -- @TODO: do I need this return? end -- Pick already existing function entry if it's already being hooked by some mod local function get_hooked_function_entry(hooked_function_name) for i, hooked_function_entry in ipairs(HOOKED_FUNCTIONS) do if hooked_function_entry.name == hooked_function_name then return hooked_function_entry, i end end return nil end -- Pick already existing hook entry if there is one local function get_hook_entry(mod, hooked_function_entry) for i, hook_entry in ipairs(hooked_function_entry.hooks) do if hook_entry.mod == mod then return hook_entry, i end end return nil end local function update_function_hook_chain(hooked_function_name) local hooked_function_entry, hooked_function_entry_index = get_hooked_function_entry(hooked_function_name) for i, hook_entry in ipairs(hooked_function_entry.hooks) do if i == 1 then if hook_entry.is_enabled then hook_entry.exec_function = function(...) return hook_entry.hook_function(hooked_function_entry.original_function, ...) end else hook_entry.exec_function = hooked_function_entry.original_function end else if hook_entry.is_enabled then hook_entry.exec_function = function(...) return hook_entry.hook_function(hooked_function_entry.hooks[i - 1].exec_function, ...) end else hook_entry.exec_function = hooked_function_entry.hooks[i - 1].exec_function end end end if #hooked_function_entry.hooks > 0 then hooked_function_entry.exec_function = hooked_function_entry.hooks[#hooked_function_entry.hooks].exec_function else hooked_function_entry.exec_function = hooked_function_entry.original_function end assert(loadstring(hooked_function_name .. " = HOOKED_FUNCTIONS[" .. hooked_function_entry_index .. "].exec_function"))() --table.dump(HOOKED_FUNCTIONS, "HOOKED_FUNCTIONS", 3) end local function modify_hook(mod, hooked_function_name, action) if not get_function_by_name(hooked_function_name) then mod:echo("ERROR: 'hook_".. action .. "' - function [" .. hooked_function_name .. "] doesn't exist", true) return end local hooked_function_entry, hooked_function_entry_index = get_hooked_function_entry(hooked_function_name) if not hooked_function_entry then return end local hook_entry, hook_entry_index = get_hook_entry(mod, hooked_function_entry) if hook_entry then if action == "remove" then table.remove(hooked_function_entry.hooks, hook_entry_index) elseif action == "enable" then hook_entry.is_enabled = true elseif action == "disable" then hook_entry.is_enabled = false end update_function_hook_chain(hooked_function_name) end if #hooked_function_entry.hooks == 0 then table.remove(HOOKED_FUNCTIONS, hooked_function_entry_index) end end local function modify_all_hooks(mod, action) local no_hooks_functions_indexes = {} for i, hooked_function_entry in ipairs(HOOKED_FUNCTIONS) do for j, hook_entry in ipairs(hooked_function_entry.hooks) do if hook_entry.mod == mod then if action == "remove" then table.remove(hooked_function_entry.hooks, j) elseif action == "enable" then hook_entry.is_enabled = true elseif action == "disable" then hook_entry.is_enabled = false end update_function_hook_chain(hooked_function_entry.name) break end end -- can't delete functions entries right away -- because next function entry will be skiped by 'for' -- so it have to be done later if #hooked_function_entry.hooks == 0 then table.insert(no_hooks_functions_indexes, 1, i) end end for _, no_hooks_function_index in ipairs(no_hooks_functions_indexes) do table.remove(HOOKED_FUNCTIONS, no_hooks_function_index) end end -- #################################################################################################################### -- ##### VMFMod ####################################################################################################### -- #################################################################################################################### VMFMod.hook = function (self, hooked_function_name, hook_function) local hooked_function_entry = get_hooked_function_entry(hooked_function_name) or create_hooked_function_entry(hooked_function_name) if not hooked_function_entry then self:echo("ERROR: 'hook' - function [" .. hooked_function_name .. "] doesn't exist", true) return end local hook_entry = get_hook_entry(self, hooked_function_entry) -- overwrite existing hook if hook_entry then hook_entry.hook_function = hook_function hook_entry.is_enabled = true -- create the new one else create_hook_entry(self, hooked_function_entry, hook_function) end update_function_hook_chain(hooked_function_name) end VMFMod.hook_remove = function (self, hooked_function_name) modify_hook(self, hooked_function_name, "remove") end VMFMod.hook_disable = function (self, hooked_function_name) modify_hook(self, hooked_function_name, "disable") end VMFMod.hook_enable = function (self, hooked_function_name) modify_hook(self, hooked_function_name, "enable") end VMFMod.remove_all_hooks = function (self) modify_all_hooks(self, "remove") end VMFMod.disable_all_hooks = function (self) modify_all_hooks(self, "disable") end VMFMod.enable_all_hooks = function (self) modify_all_hooks(self, "enable") end -- #################################################################################################################### -- ##### Event functions ############################################################################################## -- #################################################################################################################### -- removes all hooks when VMF is about to be reloaded vmf.hooks_unload = function() for _, hooked_function_entry in ipairs(HOOKED_FUNCTIONS) do hooked_function_entry.hooks = {} update_function_hook_chain(hooked_function_entry.name) end HOOKED_FUNCTIONS = {} end