Details

Addon ID: 26001
Addon Version: 1.1.12
Expansion: 1.60
Upload date: Oct 06, 2026
Last Updated: Oct 06, 2026
Downloads: Less than 100
Website: Source Link

Expansion



Developers

Kurkidentron
Kurkidentron Creator

LibMasterAddons

A lightweight framework for building WoW Classic addons.

Embeddable LibStub library with dependency injection, an addon container, client detection for every Classic client, logging, locales, timers and a complete UI toolkit in a purple and gold style. Register models, services and views and build features instead of boilerplate.

The library is embedded in the addons using it, so players never install it themselves. When several addons ship a copy, the newest one wins and serves them all.

Supported clients: Classic Era, Season of Discovery, Anniversary, TBC, Wrath, Cataclysm and Mists of Pandaria Classic.


Dependency injection

The heart of the library. Instead of globals and load order juggling you register three kinds of building blocks by name and let the container wire them together.

KindRegisterResolveLives
Serviceaddon:CreateService(name)addon:GetService(name)One lazy singleton per addon
Viewaddon:CreateView(name)addon:NewView(name)A fresh instance per call
Modeladdon:CreateModel(name[, data])addon:GetModel(name)One shared table

Registration order does not matter. Nothing runs until something actually asks for it, so a service that is never used costs nothing but its file.

What gets injected

Every service, view and class model is created with these members already on it:

self.addon            -- the addon container (name, version, client flags, Log, ...)
self:GetService(name) -- resolve another service
self:GetModel(name)   -- resolve a model
self:HandleEvent(...) -- register a game event (services only)

You never pass dependencies around and you never store references at file scope. Ask for what you need at the moment you need it.

Services

A service is a lazy singleton. The first GetService call creates it and calls Initialize once, which is the place for constants, storage and events.

local PriceService = _G.myAddon:CreateService("price");

function PriceService:Initialize()
    -- constants belong here, not at file scope
    self.MaxAge = 3600;
    self.prices = self.addon:GetSavedTable("Prices");

    -- events are registered through the container's dispatcher
    self:HandleEvent("AUCTION_HOUSE_SHOW", function()
        self:Scan();
    end);

    self.addon:Log("PriceService", "Initialize", "ready, %d cached prices", self:Count());
end

function PriceService:GetPrice(itemId)
    local entry = self.prices[itemId];
    if (not entry or time() - entry.timestamp > self.MaxAge) then
        return nil;
    end
    return entry.price;
end

Services may use each other freely, because resolution happens at call time and not at load time:

function PriceService:Scan()
    local locale = self:GetService("locale");
    print(locale:Get("ScanStarted"));
end

Views

A view is a type you can instantiate more than once, which is what you want for a dialog or list that exists per tab. NewView returns a fresh table each time.

local ListView = _G.myAddon:CreateView("list");

function ListView:Setup(parent, filterType)
    self.filterType = filterType;
    self.frame = self:GetService("ui"):CreatePanel(parent);
    self:Refresh();
end

function ListView:Refresh()
    local entries = self:GetService("price"):GetAll(self.filterType);
    -- ... render rows
end
-- two independent instances of the same type
local requests = _G.myAddon:NewView("list");
local offers = _G.myAddon:NewView("list");
requests:Setup(window, "request");
offers:Setup(window, "offer");

Views get self.addon, self:GetService and self:GetModel. They deliberately have no HandleEvent: let a service own the events and call into the view, so views can be created and dropped freely.

Models

Models hold data. Pass a table to CreateModel and it is stored as is:

_G.myAddon:CreateModel("profession-icons", {
    [171] = "Interface\Icons\Trade_BrewPoison",
    [164] = "Interface\Icons\Trade_BlackSmithing",
});

local icon = _G.myAddon:GetModel("profession-icons")[171];

Leave the second argument out and you get a class model with self.addon, self:GetService and self:GetModel injected, which suits data that has to be built at runtime:

local LocalesModel = _G.myAddon:CreateModel("locales");

function LocalesModel:Create()
    return {
        ["en"] = { ["Greeting"] = "Hello %s" },
        ["de"] = { ["Greeting"] = "Hallo %s" },
    };
end

Overriding a built-in service

log, locale, timer and ui are resolved by name like everything else. Register your own service under one of those names and your addon uses yours, while other addons sharing the embedded library keep the original.

local UiService = _G.myAddon:CreateService("ui");   -- replaces the built-in ui service

Embedding

Copy the single LibMasterAddons-<version>.lua file from the package straight into your libs folder. No subfolder, no XML, no extra files: the whole library is that one compiled file.

MyAddon/
  libs/
    LibStub.lua
    CallbackHandler-1.0.lua
    LibDataBroker-1.1.lua
    LibDBIcon-1.0.lua
    LibMasterAddons-1.0.1.lua
  MyAddon.lua
  MyAddon.toc

Load it after LibStub and the LibDBIcon stack. The file name carries the library version, so updating means dropping in the new file and changing that one TOC line:

## OptionalDeps: LibMasterAddons
## X-Embeds: LibMasterAddons

libsLibStub.lua
libsCallbackHandler-1.0.lua
libsLibDataBroker-1.1.lua
libsLibDBIcon-1.0.lua
libsLibMasterAddons-1.0.1.lua
MyAddon.lua

Because the version sits in the file name, two addons can never overwrite each other's copy, and LibStub hands the newest of all loaded copies to everyone.


Quick start

local Lib = LibStub("LibMasterAddons-1.0");

local addon = Lib:CreateAddon({
    id = "MyAddon",                 -- folder and TOC name
    name = "My Addon",
    shortcut = "|cffDA8CFF[MA]|r ", -- prefix of window titles and chat output
    storagePrefix = "MA",           -- MA_Logs, MA_Frames, MA_Settings
});

-- SavedVariables are available from here on
addon:OnLoaded(function()
    addon:GetService("startup");
end);

_G.myAddon = addon;

The container also gives you:

addon.version                     -- from the TOC
addon.devMode / addon.isDebug     -- true while the TOC still says 0.0.1
addon.inCombat                    -- kept current by the container
addon:GetSavedTable("Settings")   -- MA_Settings, created when missing
addon:GenerateString(10)          -- random id, e.g. for frame names
addon:HandleEvent("BAG_UPDATE", function(bagId) ... end)
addon:OnCombatStart(function() myWindow:Hide(); end)
addon:OnCombatEnd(function() myWindow:Show(); end)

Client detection

Every container carries the flags, so you never parse a build string yourself:

if (addon.isSod) then ... end
if (addon.isWrathAtLeast) then ... end   -- Wrath, Cata and MoP
local id = addon.expansionId;            -- 1 to 5, Season of Discovery = 102

Available: isVanilla, isSod, isBcc, isWrath, isCata, isMop, plus isBccAtLeast, isWrathAtLeast, isCataAtLeast and isMopAtLeast.


Built-in services

Log service

A persistent log grouped by day in <storagePrefix>_Logs, purged after 7 days. Log through the container, read it back for a support window.

self.addon:Log("SyncService", "SendData", "sent %d skills to %s", count, playerName);
local log = addon:GetService("log");
log:GetLogText();   -- whole log as text, newest day first
log:ClearLogs();    -- for a clear button
log:GetLogs();      -- raw table, keyed by "2026-08-31"

Locale service

Add a model named locales whose Create() returns one table per language. The service picks the full client locale first (zhCN), then the short one (de), then English.

local LocalesModel = _G.myAddon:CreateModel("locales");

function LocalesModel:Create()
    return {
        ["en"] = { ["ItemsFound"] = "%d items found", ["Title"] = "My Addon" },
        ["de"] = { ["ItemsFound"] = "%d Gegenstände gefunden", ["Title"] = "Mein Addon" },
    };
end
local locale = self:GetService("locale");

locale:Get("Title");            -- "My Addon"
locale:Get("ItemsFound", 12);   -- "12 items found"
locale:GetBare("Title");        -- nil when the key is missing
locale:Has("Title");            -- true
locale.localeName;              -- "deDE"

Get returns the key itself when it is missing, so a forgotten translation shows up in the UI instead of erroring. Without format arguments the text is returned untouched, which keeps placeholders like %player% intact.

Timer service

Named countdowns on a one second ticker, so you need no OnUpdate handler of your own.

local timer = self:GetService("timer");

-- countdown, called immediately and then every second
timer:Start("scan", 30, function(secondsLeft)
    button:SetText(timer:FormatTime(secondsLeft));
end);

timer:IsRunning("scan");   -- true
timer:Stop("scan");        -- stop early

-- one shot, replaces a pending wait of the same name
timer:Wait("refresh", 5, function()
    self:Refresh();
end);

FormatTime renders durations the way cooldown lists want them:

timer:FormatTime(45);            -- "45s"
timer:FormatTime(300);           -- "5m"
timer:FormatTime(180000);        -- "2d 2h 0m"
timer:FormatTime(180000, true);  -- "2d 2h"   (zero parts left out)
timer:FormatTime(0);             -- green "Ready" (locale key CooldownReady)

UI service

The complete toolkit behind the Master addon windows. Everything below is themed, so a window built from these parts looks finished without a single texture of your own.

Windows

local ui = self:GetService("ui");

-- name, width, height, title, prefix the title with addon.shortcut, resizable
local window = ui:CreateView("main", 800, 500, "My Addon", true, true);
ui:CreateFlatCloseButton(window, function() window:Hide(); end)
    :SetPoint("TOPRIGHT", -12, -8);

Movable, resizable from every edge and corner, with a gradient border, ElvUI scale awareness and an automatic clamp back onto the screen. Position and size are stored per window name in <storagePrefix>_Frames and restored on the next login. Optional arguments after resizable are minWidth, minHeight and a table of allowed resize directions. The frame exposes titleLabel and SetMinSize(width, height).

Tabs

CreateTabStrip builds a real tab control that sits on the top border of a content frame. Tabs are clipped to the window width, and when they no longer fit, two arrows appear on the right and the mouse wheel scrolls through them.

local content = ui:CreatePanel(window);
content:SetPoint("TOPLEFT", 12, -70);
content:SetPoint("BOTTOMRIGHT", -12, 12);

local strip = ui:CreateTabStrip(window, content);

local guildTab = strip:AddTab("Guild", function() self:ShowGuild(); end);
strip:AddTab("Requests", function() self:ShowRequests(); end);
strip:AddTab("Offers", function() self:ShowOffers(); end);

strip:SetActive(guildTab);
strip:Layout();

The strip takes an optional config table: tabWidth (125), tabGap (4), activeHeight (26), inactiveHeight (24), sideInset (6) and glowTexture for the glow behind the active tab. strip:ScrollToTab(button) brings a tab into view, for example after activating it from code. ui:CreateTab(container, caption) is the plain single tab button when you do not want a whole strip.

Dropdowns

local dropdown = ui:CreateDropdown(window, 200, {
    { value = 0,   text = "All professions" },
    { value = 333, text = "Enchanting" },
    { value = 755, text = "Jewelcrafting" },
}, function(value, text)
    self.professionId = value;
    self:Refresh();
end);
dropdown:SetPoint("TOPLEFT", 12, -40);

dropdown:SetValue(333);      -- select from code
dropdown:GetValue();         -- 333
dropdown:SetItems(newItems); -- rebuild the list, e.g. after a filter change

Item text may contain escape sequences, so an icon in front of an entry is simply "|T" .. icon .. ":16|t " .. name. Opening a dropdown closes any other open one automatically.

Tooltips

-- static text
ui:BindTooltip(button, "Scan the auction house");

-- computed on hover, so it always shows the current state
ui:BindTooltip(button, function()
    return self.isScanning and "Scanning..." or "Scan the auction house";
end);

-- custom anchor
ui:BindTooltip(icon, "Requires Mining", "ANCHOR_RIGHT");

BindTooltip hooks the scripts instead of replacing them, so existing OnEnter and OnLeave handlers keep working. Returning nil or an empty string suppresses the tooltip for that hover.

Buttons

ui:CreateButton(window, "Apply", function() self:Apply(); end);            -- classic
ui:CreateFlatButton(window, "Cancel", function() window:Hide(); end);      -- flat, themed
ui:CreateFlatSquareButton(box, "OK", function() self:Confirm(); end, 18);  -- small square
ui:CreateFlatCloseButton(window, function() window:Hide(); end);           -- X
ui:CreateHeaderIconButton(window, "Interface\Icons\INV_Misc_Note_01",
    "Settings", function() self:OpenSettings(); end);                      -- icon with tooltip

Lists and scrolling

local panel, content, scrollFrame = ui:CreateScrollFrame(window);
panel:SetPoint("TOPLEFT", 12, -70);
panel:SetPoint("BOTTOMRIGHT", -12, 12);

for index, entry in ipairs(entries) do
    local row = ui:CreatePanel(content);
    row:SetHeight(20);
    ui:SetRowColor(row, index);      -- alternating background
    ui:AttachHoverGlow(row);         -- purple hover border and backdrop
end

AttachHoverGlow takes a second argument with extra mouse enabled child frames that should keep the glow alive, for example per column hover regions. Set row.hoverGlowDisabled = true on rows without a click action.

Input

local search = ui:CreateEditBox(window, 200);
search:SetPlaceholder("Search");
search:SetText("");
search:GetText();
search.editBox:SetScript("OnEnterPressed", function(box) box:ClearFocus(); end);

local quantity = ui:CreateNumberEditBox(window, 60);  -- right aligned, for numbers

Minimap button

ui:CreateMinimapIcon({
    icon = "Interface\Icons\INV_Misc_Book_09",
    overlayText = "MA",
    onClick = function(button)
        if (button == "LeftButton" and IsShiftKeyDown()) then
            self:OpenSettings();
        elseif (button == "LeftButton") then
            self.addon.mainView:Toggle();
        end
    end,
    onTooltip = { "My Addon", "|cff999999Left Click:|r toggle the window" },
    onHidden = function() settingsCheckbox:SetChecked(false); end,
});

ui:SetMinimapIconShown(false);   -- from a settings checkbox
ui:IsMinimapIconShown();

Ctrl and right click hides the button, and position and hide state persist in <storagePrefix>_Settings.minimapButton. onTooltip also accepts a function receiving the tooltip.


Compatibility

LibMasterAddons-1.0 is the API contract: members are only ever added, never removed or changed in meaning. The minor version in the file name is stamped by the build, and the newest embedded copy serves every addon that ships one. Breaking changes would ship as a new major that can be embedded side by side.

Feedback

Comments on the CurseForge project page are welcome.

You must be logged in to leave a comment.

Comments (0)