Skip to content

ModuleManager

APHONIC edited this page Sep 15, 2026 · 3 revisions

Module Manager

ModuleManager.lua: a soft-disable system for optional feature files, with opt-in live load/unload.

ESO gives addons no API to stop a manifest-listed file from loading: every file always executes once EVENT_ADD_ON_LOADED fires. To let a player "unload" an optional module (e.g. a wizard or settings panel they don't want consuming CPU usage), an addon has to nil out that module's functions itself and gate every call site behind a check.

Core functions

-- once, in init(): applies any modules the player previously unloaded
LibAPH.ApplyModuleDisableOverrides(store, moduleFileFuncs, modulesTable, nilOutFn, getFn)

-- when the player toggles a module (a slash command, a menu checkbox):
local now_disabled = LibAPH.ToggleModuleDisabled(store, moduleFileFuncs, modKey, notifyFn, silent)
LibAPH.SyncModuleLifecycle(modulesTable, modKey, now_disabled)

-- anywhere you'd otherwise call an optional module's function directly:
LibAPH.CallOptional(warnedTable, "[MyAddon]", "is unloaded, skipping.", MyAddon.SomeFn, "Some feature (SomeFn)", arg1, arg2)
  • store: your settings table, passed fresh on every call not cached. If your addon ever swaps settings tables at runtime (an account/character profile switch), a cached reference inside the library would go stale.
  • moduleFileFuncs: { modKey = {"FuncName1", "FuncName2", ...} }, listing which global functions live in each optional file.
  • modulesTable: your own MyAddon._modules table, flipped true/false per module.
  • nilOutFn(fname): your closure that actually does MyAddon[fname] = nil. The library can't reach into your addon's namespace directly.
  • getFn(fname) (optional): your closure reading MyAddon[fname], used to stash a reference before nil-ing, so the module's function can be restored on a later re-enable.
  • CallOptional's warnedTable is caller-owned (one per addon) so two addons' warning states never mix; it groups by the text before "(" so multiple functions in the same module only warn once between them.

Live load/unload

LibAPH.RegisterModuleLifecycle("sync", {
    onLoad = function() MyAddon.sync_engine.initialize() end,
    onUnload = function()
        EVENT_MANAGER:UnregisterForEvent("MyAddon_Sync", EVENT_CHAT_MESSAGE_CHANNEL)
    end,
})

This is opt-in, not automatic: most optional modules (a one-shot wizard, a settings panel builder) don't own any running state to tear down or restart, so nil-and-reload is already correct for them. A module worth registering a lifecycle for is one that owns real running state: an EVENT_MANAGER registration, a poller. A sync module that holds a chat-channel event registration and slash commands is one example: toggling it takes effect immediately instead of requiring /reloadui.

A modKey that never registers a lifecycle just falls through SyncModuleLifecycle untouched (it returns false, meaning tell the player to reload. Only register a lifecycle for a module you've actually verified is safe to tear down and rebuild live.

HasModuleLifecycle(modKey), StashFunc(modKey, fname, fn), and GetStashedFunc(modKey, fname) are the supporting pieces ApplyModuleDisableOverrides/SyncModuleLifecycle use internally: you generally don't call these directly unless you're building something more custom than the standard toggle flow above.

LibAPH.BuildModuleLoadButton(opts)

Returns a ready-made LAM2 checkbox control definition for a module toggle.

opts: displayName, moduleLabel, modKey, settings, toggleFn, isMissing (optional, disables the checkbox and appends opts.missingText to the label when true).

build_data[#build_data + 1] = LibAPH.BuildModuleLoadButton({
    displayName = "Sync",
    moduleLabel = "Module",
    modKey = "sync",
    settings = MyAddon.settings,
    toggleFn = MyAddon.toggle_module_disabled,
})

Home · Getting Started

Clone this wiki locally