-
Notifications
You must be signed in to change notification settings - Fork 0
SFUtils_HookManager
SFUtils.HookManager is a lightweight registry for managing multiple ESO API hooks from a single object.
Rather than tracking hook handles throughout your addon, HookManager stores each hook in a central registry, allowing you to enable, disable, retrieve, or remove hooks individually or as a group.
Hooks created by HookManager remain permanently registered with the game just as the ESO hooks do. ESO's hook APIs do not provide any mechanism to unregister or remove a hook once it has been installed.
The HookManager-created hook wraps the hook function with an enable/disable capability. Enabling or disabling a hook simply determines whether its callback can execute.
- Centralized hook registry
- Unique hook identifiers
- Pre-hooks
- Post-hooks
- Secure post-hooks
- Enable or disable individual hooks
- Enable or disable all hooks simultaneously
- Toggle hook state without re-registering
- Safe execution for secure hooks using
LibSFUtils.safeCall10
| Feature | Benefit |
|---|---|
| Batch Control | Enable/disable all hooks at once (ideal for feature toggles) |
| Dynamic State | Toggle individual hooks on/off without re-registration |
| Safety | Wraps callbacks with sfutil.safeCall to prevent UI-breaking errors |
| Identification | Unique ID per hook for easy retrieval and manipulation |
| Centralized Tracking | Single source of truth for all addon hooks |
-- Assuming LibSFUtils is already loaded
local HookManager = LibSFUtils.HookManager
-- Or using the global SF reference (common in ESO addons)
local HookManager = SF.HookManager-- Create a new HookManager instance
local myHooks = HookManager:New("MyAddon_Hooks")
-- Register a pre-hook
local mailHook = myHooks:PreHook(MAIL_INBOX, "SendMail", function(...)
zo_dlog("Intercepting mail send...")
return false -- Continue with original function
end)
-- Later: disable hook temporarily
myHooks:disable(mailHook.id)
-- Re-enable
myHooks:enable(mailHook.id)
-- On addon unload
myHooks:disableAll() -- Pause all hooks
myHooks:disableAll() -- Clean uplocal manager = HookManager:New(baseName)
ParameterTypeDefaultDescriptionbaseNamestring"HookManager"Prefix for generating unique hook IDs
Returns: New HookManager instance with internal state:{
base = "MyAddon_Hooks", -- Your custom prefix
cnt = 1, -- Auto-incrementing counter
hooks = {} -- Internal registry (id → hook table)
}Example:
-- Different base names allow multiple managers in same addon
local combatHooks = HookManager:New("CombatModule")
local uiHooks = HookManager:New("UI_Module")All registration methods return a hook table object that represents the registered hook. All hooks are created with enabled = true by default.
Runs before the original function. Returning true from the callback cancels the original function execution.
local hookObj = manager:PreHook(target, method, fn)| Parameter | Type | Description |
|---|---|---|
| target | table | Object containing the method (e.g., MAIL_INBOX, _G) |
| method | string | Method name (case-sensitive) |
| fn | function | Callback function (signature matches original) |
Returns: Hook table or nil if ID collision detected. Usage Example:
local validateHook = myHooks:PreHook(MAIL_INBOX, "SendMail", function(self, ...)
-- Validate mail before sending
local subject = ...
if #subject > 50 then
uierror("Subject too long!")
return true -- Cancel original
end
return false -- Allow original to run
end)Runs after the original function. The callback's return value is ignored (cannot cancel the original).
local hookObj = manager:PostHook(target, method, fn)| Parameter | Type | Description |
|---|---|---|
| target | table | Object containing the method |
| method | string | Method name (case-sensitive) |
| fn | function | Callback function |
Usage Example:
local logHook = myHooks:PostHook(SKILL_BAR, "RefreshSkills", function(...)
zo_dlog("Skill bar refreshed at " .. ZO_GetTimeString())
end)Uses SecurePostHook for combat/security-critical functions. Errors in the callback are swallowed via sfutil.safeCall10.
local hookObj = manager:SecurePostHook(target, method, fn)| Parameter | Type | Description |
|---|---|---|
| target | table | Object containing the method (e.g., COMBAT) |
| method | string | Method name (case-sensitive) |
| fn | function | Callback function |
Important: Secure hooks cannot be cancelled and use sfutil.safeCall10 for error handling.
Usage Example:
local combatHook = myHooks:SecurePostHook(_G, "CastAbility", function(...)
-- Track ability casts (secure context)
LogAbilityUsed(...)
end)The object returned by registration methods contains:
| Property | Type | Description |
|---|---|---|
| id | string | Unique identifier (e.g., "MyAddon_Hooks_1") |
| target | table | The object that owns the method |
| method | string | Method name (case-sensitive) |
| fn | function | Original callback function |
| kind | string | Hook type ("pre", "post", or "secure") |
| enabled | boolean | Whether hook is currently active |
Metatable: Hook objects inherit from HookManager, allowing method calls:
-- You can call manager methods passing the hook table directly
-- (though typically you'll use the id)
local hook = myHooks:PreHook(...)
d(hook.id) -- "MyAddon_Hooks_1"
d(hook.kind) -- "pre"
d(hook.target) -- MAIL_INBOX (table)
d(hook.method) -- "SendMail" (string)| Method | Description |
|---|---|
| manager:get(id) | Retrieve hook table by ID (returns nil if not found) |
| manager:enable(id) | Activate specific hook |
| manager:disable(id) | Deactivate specific hook (callback skipped, but registered) |
| manager:toggle(id) | Flip hook state (active ↔ inactive) |
| manager:remove(id) | Completely remove hook from registry |
Examples:
-- Get hook info
local hook = myHooks:get("MyAddon_Hooks_1")
if hook then
d(hook.kind, hook.method, hook.enabled)
end
-- Toggle debug logging
myHooks:toggle(logHook.id)
-- Remove hook permanently
myHooks:remove(logHook.id)| Method | Description |
|---|---|
| manager:enableAll() | Activate all registered hooks |
| manager:disableAll() | Deactivate all registered hooks |
| manager:toggleAll() | Flip state of all registered hooks |
Use Cases:
-- Pause all hooks when addon is disabled
local function OnAddonUnload(...)
myHooks:disableAll() -- Stop all callbacks
-- Optionally remove if cleanup needed
end
-- Resume all hooks when addon reloads
local function OnAddonLoaded(...)
myHooks:enableAll() -- Reactivate all
end
-- Toggle all debugging at once
local function ToggleDebug()
myHooks:toggleAll()
end-- =============================================================================
-- MyAddon Main File (EsoMain.txt / Init.lua)
-- =============================================================================
local MY_ADDON_NAME = "MyAddon"
local myHooks = nil
-- Event Handler for Addon Loading
local function OnAddonLoaded(eventCode, addonName)
if addonName ~= MY_ADDON_NAME then return end
-- Initialize HookManager
myHooks = HookManager:New(MY_ADDON_NAME .. "_Hooks")
-- Register hooks
myHooks:PreHook(MAIL_INBOX, "SendMail", OnMailValidate)
myHooks:PostHook(INVENTORY_MANAGER, "UpdateSlot", OnInventoryUpdated)
myHooks:SecurePostHook(_G, "CastAbility", OnAbilityUsed)
zo_dlog("MyAddon: All hooks registered")
end
-- Event Handler for Addon Unloading
local function OnAddonUnload(eventCode, addonName)
if addonName ~= MY_ADDON_NAME then return end
-- Disable all hooks before unload
if myHooks then
myHooks:disableAll()
myHooks = nil
end
zo_dlog("MyAddon: Hooks cleaned up")
end
-- Hook Callbacks
local function OnMailValidate(self, ...)
zo_dlog("Validating mail send...")
return false -- Allow original
end
local function OnInventoryUpdated(...)
zo_dlog("Inventory slot updated")
end
local function OnAbilityUsed(...)
zo_dlog("Player cast an ability")
end
-- Register for addon events
EVENT_MANAGER:RegisterForEvent(
MY_ADDON_NAME,
EVENT_ADD_ON_LOADED,
OnAddonLoaded
)
EVENT_MANAGER:RegisterForEvent(
MY_ADDON_NAME,
EVENT_ADD_ON_UNLOADING,
OnAddonUnload
)While there's no built-in describeAll method in the current implementation, you can add debugging utilities:
-- Add to your addon code
function InspectHooks(manager)
d("=== Hook Manager: " .. manager.base .. " ===")
for id, hook in pairs(manager.hooks) do
local status = hook.enabled and "ACTIVE" or "INACTIVE"
d(string.format(
"%s - %s.%s [%s]",
id,
hook.kind,
hook.method,
status
))
end
d("Total hooks: " .. #manager.hooks)
end
-- Usage
InspectHooks(myHooks)✅ Do
- Use unique base names for different modules (CombatModule_Hooks, UIModule_Hooks)
- Disable hooks on unload to prevent ghost callbacks
- Store hook IDs if you need to reference them later
- Test hooks with errors to ensure safeCall protects your game UI
-- Good: Store reference for later use
local inventoryHook = myHooks:PostHook(INVENTORY_MANAGER, "UpdateSlot", OnUpdate)
inventoryHookId = inventoryHook.id -- Save for later
-- Later...
myHooks:toggle(inventoryHookId)❌ Don't
- Register the same hook twice with same ID (will return nil)
- Assume hooks are always active (check enabled property)
- Forget to clean up on addon unload (memory leak risk)
- Mix hook types (pre/post/secure) without understanding behavior differences
| Issue | Workaround |
|---|---|
| Hook IDs must be unique per manager | Use different base names for separate managers |
| Hooks remain registered even when disabled | Use remove(id) if you need to truly unregister |
| No built-in hook description/debug output | Add your own inspection utility (see above) |
| Cannot retrieve original function (only callback) | Store original function reference separately if needed |
| Signature | Purpose |
|---|---|
| HookManager:New(baseName?) | Create new instance |
| manager:PreHook(target, method, fn) | Register pre-hook |
| manager:PostHook(target, method, fn) | Register post-hook |
| manager:SecurePostHook(target, method, fn) | Register secure hook |
| Signature | Purpose |
|---|---|
| manager:get(id) | Get hook by ID |
| manager:enable(id) | Activate hook |
| manager:disable(id) | Deactivate hook |
| manager:toggle(id) | Flip hook state |
| manager:remove(id) | Remove hook permanently |
| manager:enableAll() | Activate all hooks |
| manager:disableAll() | Deactivate all hooks |
| manager:toggleAll() | Flip all hook states |
| Property | Type | Read/Write |
|---|---|---|
| id | string | Read-only |
| target | table | Read-only |
| method | string | Read-only |
| fn | function | Read-only |
| kind | string | Read-only |
| enabled | boolean | Read/Write |
Full LibSFUtils Documentation - Complete library reference ESO API Reference - Official ESO addon documentation HookManager Source - Raw implementation
Generated from SFUtils_HookManager.lua. Last updated: July 2026