Everyday helper code my add-ons share, kept in one place so none of them has to carry its own copy. Any functions are subject to change.
- Window & Theme - resizable status/scroll-list windows with a shared flat theme
- Console Support - right-stick window drag/resize, console text-entry and picker dialogs
- Context Menu & Search Bar - multi-level context menu, movable expanding search bar, key-hint icons
- Module Manager - soft-disables individual add-on files and rebuilds the Client Info file list
- Wizard Helper - schedules a first-run setup wizard, auto-unloads once completed
- Messaging & Dialogs - chained/window-hiding dialogs, safe CSAs, a chat logger
- Player State - crafting/interacting/menu busy checks, movement and teleport trackers
- SavedVariables - disk-usage reporting, throttled priority saves, unused-SV cleanup
- Library & Version Checks - checks an optional library's version and builds a shared warning message
- Bug Reporting - a shared
/xxxbugreportpopup format used by every add-on - Activity Triggers - a shared table of "is the player doing X" checks
- Scheduler - spreads work across frames within a time budget, staged add-on init
- Memory Cleanup - the Auto Lua Memory Cleaner cleanup methods for any add-on, shown in Auto Lua Memory Cleaner when it is installed
Extract LibAPH into your AddOns directory, then declare it as a dependency in your add-on manifest:
## DependsOn: LibAPH>=<version>
It loads as a global table; no require or manual initialization needed:
LibAPH.GetPlatformString()LibAPH's functions hang off the single global LibAPH table, organized internally by folder (UI/, UTILS/, MODULES/, MENU/, HELPERS/, DATA/). Call whatever you need directly:
local status = LibAPH.CreateStatusWindow({ name = "MyAddonUI", title = "My Addon" })
LibAPH.RegisterAddonDependencies("MyAddon", { "LibAPH" }, { "LibAddonMenu-2.0" })
LibAPH.Schedule("MyAddon_Cleanup", function() --[[ heavy work, one chunk per frame ]] end)Window & Theme (UI/Window.lua, UI/Theme.lua, UI/Position.lua)
MakeWindowResizable(control, opts): adds corner-drag resizing to a windowCreateStatusWindow(opts): builds a movable, resizable status window with the shared themeCreateRowList(parent, opts): builds a themed scroll list of rowsCreateScrollListWindow(opts): builds a full window wrapping a themed scroll listCreateCopyTextBox(opts): builds a read-only, selectable copy-text box (used by every bug report popup)OpenPastebin()/ConfirmOpenPastebin(win): opens (or confirms opening) an external pastebin linkStepActiveSearch(direction): cycles a search box's matches forward/backwardAddButtonHoverEffects(control, baseColor): adds the shared hover/press color states to a buttonAddGhostText(editBox, ghostText): shows greyed-out placeholder text in an empty edit boxHandleKeybindButtonKey(keybind)/RegisterKeybindDefaults(namespace, store, defaults, modifiers)/CreateKeybindLabelButton(parent, opts): shared keybind-button handling and default-binding registrationSetWindowActive(window, label, isActive, opts): shows/hides a window based on whether it has anything to displayAddFragmentToScenes(fragment, sceneNames)/RemoveFragmentFromScenes(fragment, sceneNames): batch scene-fragment registrationCreateCogwheelButton(parent, name, onClicked, tooltipText): builds the shared settings-cogwheel buttonIsScrollableMenuAvailable()/ShowScrollableMenu(control, entries, opts)/CloseScrollableMenu()/RefreshScrollableMenu(): LibScrollableMenu integration with a safe fallback when it's missingCreateToggleArrowButton(parent, name, onToggle, tooltipText)/SetToggleArrowOpen(button, open): an expand/collapse arrow controlDockWindowBeside(win, other, gap, prefer): anchors one window beside anotherPixelSize(): returns the current UI pixel scaleAddPixelBorder(control, name, color): adds the shared one-pixel borderApplyPanelBackdrop(win, name, fill): applies the shared flat panel backgroundCreateHeaderStrip(win, name, height): builds the shared header stripCreateThemedCloseButton(win, name, onClick, right, top): builds the themed X close buttonStyleRowBackground(texture, index, is_section)/StyleScrollList(list, name): shared row striping and scrollbar stylingUseGreenSelection(combo): colors a dropdown's selected entry greenUseTextSelection(combo): shows a dropdown's selected entries as colored text with no background, as in the bug report windowsCreateStickyFragment(control, scenes): keeps a window's own close state sticky across scene changesCreateWindowPosition(win, opts): saves and restores a movable window's spot, resets it to a default, and runs the console right-stick move- optional
opts.onMoving(win)runs everyopts.watchMs(default 50 ms) while the window is dragged with the mouse or moved with the right stick;opts.onMoveStop(win)runs after the new spot is saved;opts.onMoveEnd(win)runs whenever a right-stick move ends, moved or not pos:WatchMove()starts theonMovingloop by hand (returns false withoutonMoving);pos:StopWatch()stops it
- optional
GetScreenThirdAlign(control)/GetScreenThirdAlignAt(centerX, screenWidth): returnsTEXT_ALIGN_LEFT,TEXT_ALIGN_CENTERorTEXT_ALIGN_RIGHTfor whichever third of the screen a control's center sits in, so text can hug the nearest edgeEnableUndoRedo(edit, opts): keeps an undo and redo history for an edit box, with typing grouped into steps; call:Undo()and:Redo()from your own buttons
Show a full Window & Theme example
local DemoWindow = { saved = {} }
local PAD = 12
local function Font(size)
return string.format("$(MEDIUM_FONT)|%d|soft-shadow-thin", size)
end
local function MakeLabel(parent, text, size, color)
local label = WINDOW_MANAGER:CreateControl(nil, parent, CT_LABEL)
label:SetFont(Font(size or 14))
label:SetColor(unpack(color or LibAPH.THEME.TEXT))
label:SetText(text)
return label
end
local function MakeButton(name, parent, text, width, onClick)
local button = WINDOW_MANAGER:CreateControlFromVirtual(name, parent, "ZO_DefaultButton")
button:SetDimensions(width, 26)
button:SetText(text)
button:SetHandler("OnClicked", onClick)
return button
end
local function MakeTipLabel(parent, text, onEnter, onExit)
local label = MakeLabel(parent, text, 14, LibAPH.THEME.ACCENT)
label:SetMouseEnabled(true)
label:SetHandler("OnMouseEnter", onEnter)
label:SetHandler("OnMouseExit", onExit)
return label
end
local function BuildDemoWindow()
local win = WINDOW_MANAGER:CreateTopLevelWindow("LibAPHDemoWindow")
win:SetDimensions(460, 520)
win:SetClampedToScreen(true)
win:SetMouseEnabled(true)
win:SetMovable(true)
win:SetDrawTier(DT_MEDIUM)
win:SetDrawLayer(DL_OVERLAY)
win:SetDrawLevel(9100)
DemoWindow.window = win
LibAPH.ApplyPanelBackdrop(win, "LibAPHDemoWindowBG", LibAPH.THEME.BG)
LibAPH.CreateHeaderStrip(win, "LibAPHDemoWindowHeader", 30)
LibAPH.CreateThemedCloseButton(win, "LibAPHDemoWindowClose", function() win:SetHidden(true) end, 12, 10)
LibAPH.MakeWindowResizable(win, { minWidth = 440, minHeight = 500 })
DemoWindow.position = LibAPH.CreateWindowPosition(win, {
get = function() return DemoWindow.saved.left, DemoWindow.saved.top end,
set = function(x, y) DemoWindow.saved.left, DemoWindow.saved.top = x, y end,
placeDefault = function(control) control:SetAnchor(CENTER, GuiRoot, CENTER, 0, 0) end,
mover = LibAPH.CreateGamepadMover(win),
})
DemoWindow.position:Apply()
local title = MakeLabel(win, "LibAPH Demo", 16)
title:SetAnchor(TOPLEFT, win, TOPLEFT, PAD, 7)
local gear = LibAPH.CreateCogwheelButton(win, "LibAPHDemoGear", function()
d("LibAPH Demo: gear clicked")
end, "Cogwheel tooltip, built into the button")
gear:SetAnchor(TOPRIGHT, win, TOPRIGHT, -40, 1)
local y = 42
local function Place(control, height)
control:SetAnchor(TOPLEFT, win, TOPLEFT, PAD, y)
y = y + height + 8
end
local function Section(text)
Place(MakeLabel(win, text, 13, LibAPH.THEME.MUTED), 16)
end
Section("Buttons")
local hover = MakeLabel(win, "Hover Me", 14, LibAPH.THEME.ACCENT)
hover:SetDimensions(90, 26)
hover:SetVerticalAlignment(TEXT_ALIGN_CENTER)
hover:SetMouseEnabled(true)
hover.libaph_click_action = function() d("LibAPH Demo: hover label clicked") end
LibAPH.AddButtonHoverEffects(hover, LibAPH.THEME.ACCENT)
Place(hover, 26)
local copy_box = LibAPH.CreateCopyTextBox({ name = "LibAPHDemoCopyBox", titleText = "Demo Copy Box" })
local copy_button = MakeButton("LibAPHDemoCopyButton", win, "Copy Box", 100, function()
copy_box:Show("Select-and-copy text lives here.")
end)
copy_button:SetAnchor(LEFT, hover, RIGHT, 8, 0)
local pastebin_button = MakeButton("LibAPHDemoPastebinButton", win, "Pastebin", 100, function()
LibAPH.ConfirmOpenPastebin(win) -- LibAPH.OpenPastebin() skips the confirm dialog
end)
pastebin_button:SetAnchor(LEFT, copy_button, RIGHT, 8, 0)
Section("Checkbox")
local check = WINDOW_MANAGER:CreateControlFromVirtual("LibAPHDemoCheck", win, "ZO_CheckButton")
ZO_CheckButton_SetLabelText(check, "Show the row list")
ZO_CheckButton_SetCheckState(check, true)
Place(check, 20)
Section("Dropdowns")
local combo_container = WINDOW_MANAGER:CreateControlFromVirtual("LibAPHDemoCombo", win, "ZO_ComboBox")
combo_container:SetDimensions(180, 26)
Place(combo_container, 26)
local combo = ZO_ComboBox_ObjectFromContainer(combo_container)
combo:SetSortsItems(false)
for _, size_name in ipairs({ "Small", "Medium", "Large" }) do
combo:AddItem(combo:CreateItemEntry(size_name, function(_, choice)
d("LibAPH Demo: picked " .. choice)
end))
end
combo:SelectFirstItem(true)
LibAPH.UseGreenSelection(combo)
local menu_combo_container = WINDOW_MANAGER:CreateControlFromVirtual("LibAPHDemoMenuCombo", win, "ZO_ComboBox")
menu_combo_container:SetDimensions(180, 26)
menu_combo_container:SetAnchor(LEFT, combo_container, RIGHT, 12, 0)
local menu_combo = ZO_ComboBox_ObjectFromContainer(menu_combo_container)
menu_combo:SetSelectedItemText("Opens a context menu")
LibAPH.UseContextMenuForCombo(menu_combo_container, function()
return {
{ text = "Option A", onClick = function() menu_combo:SetSelectedItemText("Option A") end },
{ text = "Option B", onClick = function() menu_combo:SetSelectedItemText("Option B") end },
}
end)
Section("Context menu: right-click anywhere in this window")
local show_hints = true
win:SetHandler("OnMouseUp", function(control, button, upInside)
if not upInside or button ~= MOUSE_BUTTON_INDEX_RIGHT then return end
LibAPH.ShowContextMenu(control, {
{ header = true, text = "LibAPH Demo" },
{ text = "Say hello", hint = show_hints and "d()" or nil, onClick = function() d("LibAPH Demo: hello") end },
{ text = "Show hints", checkbox = true, checked = show_hints, onToggle = function(on) show_hints = on end },
{ divider = true },
{ text = "More", submenu = function()
return {
{ text = "Sub item", onClick = function() d("LibAPH Demo: sub item") end },
{ text = "Disabled item", enabled = false },
}
end },
})
end)
y = y + 4
Section("Tooltips: hover each word")
local text_tip = MakeTipLabel(win, "Text", function(self)
InitializeTooltip(InformationTooltip, self, BOTTOM, 0, -4)
SetTooltipText(InformationTooltip, "InformationTooltip with plain text")
end, function() ClearTooltip(InformationTooltip) end)
Place(text_tip, 20)
local side_tip = MakeTipLabel(win, "Side", function(self)
ZO_Tooltips_ShowTextTooltip(self, RIGHT, "ZO_Tooltips_ShowTextTooltip, anchored to one side")
end, function() ZO_Tooltips_HideTextTooltip() end)
side_tip:SetAnchor(LEFT, text_tip, RIGHT, 24, 0)
local item_tip = MakeTipLabel(win, "Item", function(self)
local link = GetItemLink(BAG_WORN, EQUIP_SLOT_CHEST)
InitializeTooltip(ItemTooltip, self, BOTTOM, 0, -4)
if link ~= "" then
ItemTooltip:SetLink(link)
else
SetTooltipText(ItemTooltip, "No chest piece equipped")
end
end, function() ClearTooltip(ItemTooltip) end)
item_tip:SetAnchor(LEFT, side_tip, RIGHT, 24, 0)
local ability_tip = MakeTipLabel(win, "Ability", function(self)
local ability_id = GetSlotBoundId(ACTION_BAR_FIRST_NORMAL_SLOT_INDEX + 1, HOTBAR_CATEGORY_PRIMARY)
InitializeTooltip(AbilityTooltip, self, BOTTOM, 0, -4)
if ability_id ~= 0 then
AbilityTooltip:SetAbilityId(ability_id)
else
SetTooltipText(AbilityTooltip, "First front-bar slot is empty")
end
end, function() ClearTooltip(AbilityTooltip) end)
ability_tip:SetAnchor(LEFT, item_tip, RIGHT, 24, 0)
Section("Search box")
local search_bg = WINDOW_MANAGER:CreateControlFromVirtual("LibAPHDemoSearchBG", win, "ZO_EditBackdrop")
search_bg:SetDimensions(220, 26)
Place(search_bg, 26)
local search_box = WINDOW_MANAGER:CreateControlFromVirtual("LibAPHDemoSearch", search_bg, "ZO_DefaultEditForBackdrop")
LibAPH.AddGhostText(search_box, "Search rows...")
search_box:SetHandler("OnEnter", function()
LibAPH.StepActiveSearch(1) -- -1 steps to the previous match instead
end)
Section("Row list")
local row_area = WINDOW_MANAGER:CreateControl("LibAPHDemoRowArea", win, CT_CONTROL)
row_area:SetDimensions(200, 66)
Place(row_area, 66)
LibAPH.CreateRowList(row_area, { spacing = 2 }):SetRows({ "Row one", "Row two", "Row three" })
ZO_CheckButton_SetToggleFunction(check, function(_, checked)
row_area:SetHidden(not checked)
end)
LibAPH.RegisterKeybindDefaults("LibAPHDemo", DemoWindow.saved, { LibAPHDemo_Toggle = KEY_F5 })
local keybind_button = LibAPH.CreateKeybindLabelButton(win, {
action = "LibAPHDemo_Toggle",
name = "Toggle Demo",
callback = function() win:SetHidden(not win:IsHidden()) end,
})
keybind_button:SetAnchor(BOTTOMLEFT, win, BOTTOMLEFT, PAD, -PAD)
local arrow, list_sticky
local list_api = LibAPH.CreateScrollListWindow({
name = "LibAPHDemoListWindow",
titleText = "Demo List Window",
rowHeight = 20,
onClose = function()
list_sticky:Close()
LibAPH.SetToggleArrowOpen(arrow, false)
end,
})
list_api:SetRows({ { text = "Row one", tooltip = "Rows can carry their own tooltip" }, { text = "Row two" } })
list_api:Hide()
LibAPH.MakeWindowResizable(list_api.window, { minWidth = 150, minHeight = 100 })
list_sticky = LibAPH.CreateStickyFragment(list_api.window, { "hud", "hudui" })
arrow = LibAPH.CreateToggleArrowButton(win, "LibAPHDemoArrow", function(wants_open)
if wants_open then
LibAPH.DockWindowBeside(list_api.window, win, 6)
list_sticky:Open()
else
list_sticky:Close()
end
return wants_open
end, "Show/hide the list window")
arrow:SetAnchor(BOTTOMRIGHT, win, BOTTOMRIGHT, -8, -8)
LibAPH.SetToggleArrowOpen(arrow, false)
local demo_fragment = ZO_SimpleSceneFragment:New(win)
LibAPH.AddFragmentToScenes(demo_fragment, { "hud", "hudui" })
d(string.format("LibAPH Demo: UI pixel scale is %.3f", LibAPH.PixelSize()))
end
SLASH_COMMANDS["/demowindow"] = function()
if not DemoWindow.window then
BuildDemoWindow()
else
DemoWindow.window:SetHidden(not DemoWindow.window:IsHidden())
end
endConsole (UI/Gamepad.lua, UI/GamepadDialogs.lua)
CreateGamepadMover(target): right-analog-stick window draggingCreateGamepadResizer(target, opts): right-analog-stick window resizingForceControllerKeybindIcons(): forces controller-button icons instead of keyboard keysShowGamepadTextEntry(opts)/ShowGamepadPicker(opts): console text entry and picker dialogs
Context Menu (UI/ContextMenu.lua)
ShowContextMenu(control, entries, opts)/CloseContextMenu()/CloseContextMenuLevel(): opens/closes the multi-level menuIsContextMenuOpen()/GetContextMenuDepth()/GetContextMenuRowCount(level)/GetContextMenuPlacement(level): menu-state queriesRefreshContextMenu(): rebuilds the currently open menu in placeContextMenuRowClicked(row)/ContextMenuRowEntered(row)/MoveContextMenuFocus(direction)/ActivateContextMenuFocus(): row interaction and console focus movementUseContextMenuForCombo(container, build, opts): adapts a combo box to open through this context menuGroupedChoiceEntries(groups, opts)/ShowGroupedChoiceMenu(control, groups, opts): a long list of choices split into one scrollable submenu per group (e.g. Default Categories / User Categories), with the current choice ticked and its group colored
Search Bar & Key Hints (UI/SearchBar.lua, UI/KeyHints.lua)
CreateSearchBar(opts): a movable search bar that expands into a match palette, with a console routeCreateKeyHints(parent, name, hints): shows the game's key icons, drawn key caps, or controller button hints
Status Icons (UI/StatusIcons.lua)
CreateStatusIconStrip(parentControl, selfHandled, slots, size): builds a colored status-icon strip for a rowUpdateStatusIconStrip(icons, anchorControl, statusIcons, growLeftward, offsetX, relativePoint): repositions/refreshes an icon stripSetStatusIconFocused(icon, focused, focusScale): highlights an icon on console focus
Module Manager (MODULES/ModuleManager.lua)
ToggleModuleDisabled(store, moduleFileFuncs, modKey, notifyFn, silent): soft-disables/re-enables one moduleApplyModuleDisableOverrides(store, moduleFileFuncs, modulesTable, nilOutFn, getFn): nils out a disabled module's functions at loadRegisterModuleLifecycle(modKey, hooks)/HasModuleLifecycle(modKey)/SyncModuleLifecycle(modulesTable, modKey, isDisabled): onLoad/onUnload hooks for modules that can apply liveStashFunc(modKey, fname, fn)/GetStashedFunc(modKey, fname): keeps a disabled module's function retrievable without re-enabling itCallOptional(warnedTable, tag, unavailableNote, fn, label, ...): calls a possibly-nil (module-disabled) function, warning once if it's missingBuildModuleFileList(moduleOrder, moduleFiles, getState, labels, sep)/FormatModuleFileLine(filename, state, labels): builds the Client Info "Files" listBuildModuleLoadButton(opts): builds the settings-menu unload/reload button for one module
Wizard (MODULES/Wizard.lua)
ScheduleWizardIfNeeded(isCompleted, runFn, delayMs): runs the first-time setup wizard once, after a short delayAutoUnloadWizardModule(settings, toggleFn): soft-disables the wizard module once it's been completed
Menu State (MENU/MenuState.lua, MENU/MenuRefresh.lua)
TrackSubmenuOpenState(savedTable, reference)/RestoreSubmenuOpenState(savedTable, reference)/PersistSubmenuOpenState(savedTable, reference): remembers a LAM2 submenu's open/closed state, PC onlyCreateMenuLabelRefresher(addonPrefix, panelGetter): forces a settings panel's labels to redraw on language change
Messaging & Dialogs (HELPERS/Messaging.lua)
ShowDialogChained(dialogId, title, body, buttons, delayMs, onClosed): shows one dialog after another in sequenceShowDialogHidingWindows(windows, dialogId, title, body, buttons, delayMs, onClosed): hides given windows while a dialog is up, restores them after, ref-counted across chained stepsSafeCSA(enabled, title, body, lifespanMs): shows a center-screen announcement only if the setting allows itCreateChatLogger(shortTag, colorHex): builds a tagged, colored chat print functionSendRawChatLine(msg): prints a raw line to chat, working on console whereCHAT_SYSTEM:AddMessagedoesn'tSetSlashCommandsShown(names, shown): adds or removes slash commands and clears the chat autocomplete, so a command only exists while it can actIsSafeToReloadUI()/ReloadUIWhenSafe(namespace, opts): defers aReloadUI()until it's safe to do (not mid-combat, etc.)
Player State (HELPERS/PlayerState.lua)
IsPlayerCrafting()/IsPlayerInteracting()/IsPlayerInMenu(): quick busy-state checksCheckBusyReason(checks): runs a list of busy checks and returns which one (if any) is trueCreateMovementTracker(opts)/CreateTeleportTracker(opts): tracks player movement/teleport state over timeRunWhenPlayerActivated(namespace, fn): defers a function untilEVENT_PLAYER_ACTIVATEDGetClientStartTime()/IsSameClientSession(recordedStart, toleranceSec): detects whether the client has restarted since a saved timestamp
SavedVariables (HELPERS/SavedVariables.lua)
IsPrioritySaveSupported()/RequestPrioritySave(addonName)/RequestPrioritySaveForRunningAddons(onSaved): forces an early SavedVariables writeRequestThrottledPrioritySave(namespace, addonName, throttleMs, force)/RequestThrottledPrioritySaveSweep(namespace, throttleMs, force, onSaved): same, rate-limitedGetSavedVariablesDiskCapacityMB()/GetSavedVariablesDiskUsageMB(addonIndex)/GetTotalSavedVariablesDiskUsageMB()/GetUnusedSavedVariablesDiskUsageMB(): disk-usage reportingClearUnusedSavedVariables()/DeleteSavedVariablesForAddon(addonIndex): frees disk space from disabled/removed add-ons
Library & Self Version Checks (UTILS/LibraryVersion.lua, UTILS/SelfVersion.lua)
CheckLibraryVersion(addonName): returns an optional library's installed version and whether it's enabledGetLibraryDriftColor(installedVer, tableVer)/FormatLibraryVersion(ver, enabled, requiredVer, formatters): colors/formats a version for displayBuildLibraryWarning(templates, fullName, shortName, ver, enabled, requiredVer, consequence)/BuildLibraryWarningFromData(...): builds the shared missing/disabled/outdated-library warning text (dialog + CSA + chat)CheckSelfVersion(store, currentVersion, opts): detects if this add-on's own AddOnVersion changed since last loadCheckAddonVersions(knownVersions, warnedTable, onMismatch): compares known dependent add-ons' versions against a reference table
Bug Reporting (UTILS/BugReport.lua)
SetAddonMetadataProvider(provider): registers how to fetch an add-on's own metadata for reportsCreateAddonBugReporter(opts): builds a ready-to-use/xxxbugreportpopup for one add-on/libaphbugreport: opens LibAPH's own bug report popup (PC)GetLiveApiLine(): the current live API version, formattedBuildEnabledAddonsReport()/BuildEnvironmentReport(): lists every enabled add-on/library with Version, AddOnVersion, APIDefaultBugReportSections(hasErrors)/BuildBugReportSections(opts)/BuildBugReportText(opts)/RenderBugReport(sections, enabled): assembles the final report textFitBugReportText(text): trims a report to fit the SavedVariables character limit
Dependency Registration (UTILS/DependencyRegistry.lua)
RegisterAddonDependencies(addonName, requiredLibs, optionalLibs): declares an add-on's real dependencies for the Client Info panel
Activity Triggers (HELPERS/ActivityTriggers.lua)
GetActivityTriggers()/GetActivityTriggerGroups()/GetActivityTriggersInGroup(group_id)/GetActivityTrigger(id): reads the shared trigger tableRegisterActivityTrigger(definition): adds a new "is the player doing X" triggerIsActivityTriggerActive(id, context)/CountActiveActivityTriggers(ids, context): evaluates triggersRegisterActivityTriggerWatcher(namespace, callback, delayMs)/UnregisterActivityTriggerWatcher(namespace): watches for trigger state changesGetPlayerStatusIcon(playerStatus)/GetFriendList()/GetIgnoredList()/GetGroupDisplayNames()/GetLootWindowSeconds(): related player/social lookups
Add-on Manager Helpers (HELPERS/AddonManager.lua, UTILS/Platform.lua)
EnableRequiredDependencies(index, setEnabled, onEnabled)/EnableAddonWithDependencies(index, setEnabled, onEnabled): enables an add-on along with its dependenciesGetPlatformString()/GetPlatformServiceName(): platform and storefront detectionGetKeybindMarkup(action): the bound key for an action as inline icon markup, or an empty string when nothing is boundIsGameScreenShown(): true only while the base game scene is fully shown (no menu, no transition)ShowMenuScene(scene): opens a main-menu scene through the keyboard main menu when it knows the scene, otherwise throughSCENE_MANAGERAfterSceneShown(scene, fn): runsfnoncesceneis shown, checking every 100 ms for up to a secondIsAddonActiveAndRunning(addonName)/IsLibraryAddonByName(am, addonName): add-on state queries
Error Capture (HELPERS/ErrorCapture.lua)
HookErrorCapture(addonName, onCaptured): hooks Lua errors for one add-onRecordCapturedBug(bugList, text, maxTracked): records a captured error, cappedFormatCapturedBugBlocks(title, bugs)/FormatCapturedBugsSection(bugLines, promptText, noneCapturedText, describeInsteadText): formats captured errors for a bug report
Format Helpers (UTILS/Format.lua)
FormatVersionParen(version)/FormatVersionBare(version)/FormatVersionHistory(history, currentVersion, sep): version-string formattingStripColors(text, fallback): strips ESO color markup from a stringPickDiskUnit(usageMB)/FormatSizeMB(sizeMB, decimals, subMegabyteUnit)/FormatDiskUsageMB(usageMB, short)/FormatDiskUsageRangeMB(usedMB, capacityMB)/FormatMemoryMB(sizeMB): disk/memory-size formattingFormatInstallDateLine(installedDate, todayStr)/GetTodayDateString(): install-date formatting
Profile Store (UTILS/ProfileStore.lua)
CreateProfileStore(opts): a saved-profile manager (create/switch/delete named settings profiles); opts.perCharacter keeps a separate active profile for each character while the profiles stay account-wideCopyMap(source)/CopyList(source): shallow-copy helpers for saved tablesCaptureAddonEnabledState(): snapshots which add-ons are currently enabled
Packed Tables (DATA/PackedTable.lua)
CreatePackedTable(rows, parseRow)/CreatePackedConsoleTable(rows)/CreatePackedListTable(rows)/CreatePackedValueTable(rows): unpacks a compact static data table into a lookup tableGetPackedTableNames(packed): lists the keys of a packed table
Scheduler (UTILS/Scheduler.lua)
Schedule(name, step, opts)/RunOrSchedule(name, step, opts): spreads a function's work across frames within a time budgetScheduleLoop(name, items, body, opts): schedules a per-item loop across framesScheduleWait(name, condition, onDone, opts): waits for a condition across frames before continuingRunInitStages(name, stages): runs a list of init functions one per frame (staged add-on load)InitOnFirstShow(sceneObject, fn): defers init until a scene is first shownGetScheduledJob(name)/IsScheduled(name)/GetScheduledCount(): scheduler-state queriesGetSchedulerBudgetMs()/SetSchedulerBudgetMs(ms): reads/sets the per-frame time budgetStepCleanup(passes, onDone)/IsStepCleanupRunning(): the shared stepped garbage-collection job every add-on's cleanup routes through
Memory Cleanup (UTILS/MemoryCleanup.lua)
RunCleanup(opts): runs one garbage collection with the Auto Lua Memory Cleaner methods:opts.methodis"automatic"(default),"background","aggressive","deep"or"vanilla"(runs only withopts.force),opts.sourcenames your add-on andopts.onDone(result)gets the result. A call while another add-on's cleanup is running joins it instead of cleaning twice. Returnstrue, methodorfalse, reasonGetCleanupMethods()/PickCleanupMethod(force, reason): the method names, and what Automatic would pick right nowRegisterCleanupListener(name, fn)/UnregisterCleanupListener(name): callsfn(result)after every cleanup from any add-on;resulthassource,method,pickedandbeforeLua/afterLua/freedLua,beforePool/afterPool/freedPoolin MBIsCleanupRunning()/GetLastCleanup()/GetMemoryUsageMB(): state queries; the last returns Lua and add-on pool MB
When Auto Lua Memory Cleaner is installed, every cleanup run through RunCleanup shows in its window, chat logs and announcements.
Show a full Memory Cleanup example
/democlean shows the status; /democlean automatic, background, aggressive, deep or vanilla runs that method. Every other add-on's cleanup shows in chat too.
local DemoCleanup = { name = "DemoCleanup" }
local LUA_LIMIT_MB = 300
local CHECK_EVERY_MS = 2000
local chat = LibAPH.CreateChatLogger("DemoCleanup", "00FFFF")
local function Describe(result)
local how = result.picked and ("automatic, picked " .. result.picked) or result.method
return string.format("%s by %s: Lua %.1f MB (-%.1f), pool %.1f MB (-%.1f)",
how, result.source, result.afterLua, result.freedLua, result.afterPool, result.freedPool)
end
local function BusyChecks()
return {
{ reasonKey = "in combat", delayMs = 5000, check = function() return IsUnitInCombat("player") end },
{ reasonKey = "dead", delayMs = 5000, check = function() return IsUnitDead("player") end },
{ reasonKey = "crafting", delayMs = 3000, check = LibAPH.IsPlayerCrafting },
{ reasonKey = "talking to an NPC", delayMs = 3000, check = LibAPH.IsPlayerInteracting },
{ reasonKey = "travelling", delayMs = 3000, check = function() return DemoCleanup.teleport:IsTeleporting() end },
}
end
function DemoCleanup.PickMethod(lua_mb)
if LibAPH.IsPlayerInMenu() then return "deep" end
if DemoCleanup.movement:IsMoving() then return "background" end
if lua_mb >= LUA_LIMIT_MB * 1.5 then return "aggressive" end
return "automatic"
end
function DemoCleanup.Run(method, force)
local started, how = LibAPH.RunCleanup({
method = method,
source = DemoCleanup.name,
force = force,
onDone = function(result)
DemoCleanup.saved.freedMB = DemoCleanup.saved.freedMB + result.freedLua
end,
})
if not started and how == "vanilla" then
chat:Print("Vanilla: nothing to clean yet, the game handles it.")
elseif not started then
chat:Print("Unknown method. Use one of: " .. table.concat(LibAPH.GetCleanupMethods(), ", "))
elseif how == "joined" then
chat:Print("Another add-on is already cleaning; joined it.")
end
return started
end
function DemoCleanup.Check()
DemoCleanup.movement:Update()
if GetGameTimeMilliseconds() < DemoCleanup.waitUntil or LibAPH.IsCleanupRunning() then return end
local lua_mb = LibAPH.GetMemoryUsageMB()
if lua_mb < LUA_LIMIT_MB then return end
local busy, reason, delay_ms = LibAPH.CheckBusyReason(DemoCleanup.busy)
if busy then
DemoCleanup.waitReason = reason
DemoCleanup.waitUntil = GetGameTimeMilliseconds() + delay_ms
return
end
DemoCleanup.waitReason = nil
DemoCleanup.Run(DemoCleanup.PickMethod(lua_mb))
end
function DemoCleanup.Status()
local lua_mb, pool_mb = LibAPH.GetMemoryUsageMB()
chat:Print(string.format("Lua %.1f MB, pool %.1f MB, freed this game session %.1f MB", lua_mb, pool_mb, DemoCleanup.saved.freedMB))
chat:Print("Automatic would pick: " .. LibAPH.PickCleanupMethod(true))
if DemoCleanup.waitReason then chat:Print("Waiting: " .. DemoCleanup.waitReason) end
local last = LibAPH.GetLastCleanup()
if last then chat:Print("Last cleanup: " .. Describe(last)) end
end
local function Init()
DemoCleanup.saved = ZO_SavedVars:NewAccountWide("DemoCleanupSaved", 1, nil, { freedMB = 0, clientStart = 0 })
if not LibAPH.IsSameClientSession(DemoCleanup.saved.clientStart) then
DemoCleanup.saved.freedMB = 0
DemoCleanup.saved.clientStart = LibAPH.GetClientStartTime()
end
DemoCleanup.movement = LibAPH.CreateMovementTracker({ throttleMs = 500, threshold = 0.5 })
DemoCleanup.teleport = LibAPH.CreateTeleportTracker({ staleMs = 5000 })
DemoCleanup.busy = BusyChecks()
DemoCleanup.waitUntil = 0
LibAPH.RegisterCleanupListener(DemoCleanup.name, function(result)
chat:Print(Describe(result))
end)
SLASH_COMMANDS["/democlean"] = function(args)
local method = zo_strlower(zo_strtrim(args or ""))
if method == "" then return DemoCleanup.Status() end
DemoCleanup.Run(method, true)
end
LibAPH.RunWhenPlayerActivated(DemoCleanup.name .. "_Start", function()
EVENT_MANAGER:RegisterForUpdate(DemoCleanup.name .. "_Watch", CHECK_EVERY_MS, DemoCleanup.Check)
end)
end
EVENT_MANAGER:RegisterForEvent(DemoCleanup.name, EVENT_ADD_ON_LOADED, function(_, addon_name)
if addon_name ~= DemoCleanup.name then return end
EVENT_MANAGER:UnregisterForEvent(DemoCleanup.name, EVENT_ADD_ON_LOADED)
Init()
end)Settings Helpers (HELPERS/Settings.lua)
FormatSettingsSnapshot(settings, fields, onLabel, offLabel): formats a settings table for display (Client Info, etc.)ResetToDefaults(settings, defaults, excludeKeys, postFn): the shared reset-to-defaults loop every add-on's Reset button calls
Warning
Console Testing Notes: This addon was developed and tested on PC / Steam Deck (using Force Console Flow for console testing).
Copyright © 2026 @APHONlC. All rights reserved. See LICENSE.md
Note
This add-on is not created by, affiliated with, or sponsored by ZeniMax Media Inc. or its affiliates. The Elder Scrolls® and related logos are registered trademarks or trademarks of ZeniMax Media Inc. in the United States and/or other countries. All rights reserved.
For permissions or inquiries, contact @APHONlC on ESOUI.
I would like to thank the following, for providing resources and their awesome projects:
- ESOUI Wiki
- ESO Forums
- @sirinsidiator
- @Flat-Badger-1971
- @sirinsidiator & @Seerah (LibAddonMenu-2.0)
- @Harven & @votan (LibHarvensAddonSettings)
- @SinusPi, @merlight, @Rhyono, @Dolgubon (Zgoo High Isle)
- @Baertram (Mer Torchbug - Fixed and Improved "Variable inspector/Scripts/Events/and more")
- @Baertram, @IceHeart, @Masteroshi430 (Circonians TextureIt)
Testers & Suggestions:
- @phlupp89
- @Drakius192
Check out my other addons/projects:
- Auto Lua Memory Cleaner
- Permanent Memento
- Tamriel Trade Center, HarvestMap, ESO-Hub, ESOUI Auto-Updater (Linux, macOS, SteamDeck, & Windows)
If you like the addon and are considering donating, here's a link. Thank you!
If you encounter any issues, please submit a report here