Skip to content

Architecture

donniedice edited this page May 1, 2026 · 4 revisions

Architecture

RGX-Framework's architecture is built around the WoW ... varargs pattern, a deterministic XML load order, and a central module registry. This page covers every layer from first file load to runtime.


Load Order

RGX-Framework.xml loads 44 files in strict sequence. Every file receives the same local addonName, addonTable = ... varargs — they all share the same RGX table.

1.  core/core.lua                    — creates _G.RGXFramework, module registry, getters
2.  core/systems/config.lua          — default config table
3.  core/systems/database.lua        — InitDatabase, GetDB, DB(), MigrateDB()
4.  core/systems/events.lua          — RegisterEvent, RegisterMessage, CreateEmitter
5.  core/systems/runtime.lua         — After, Every, CancelTimer, Hook, QueueForCombat, Safe*
6.  core/systems/utils.lua           — Trim, Split, TableKeys/Values/Contains, MergeTable, Print/Warn/Error
7.  modules/dropdowns/dropdowns.lua  — CreateNestedDropdown, NormalizeItems, AddInlineButton
8.  modules/fonts/definitions.lua    — font definitions table, unavailableFonts
9.  modules/fonts/init.lua           — module bootstrap, built-in registration
10. modules/fonts/registry.lua       — Register, RegisterAddonFont, RegisterFontPack, RegisterBuiltInFonts
11. modules/fonts/query.lua          — GetPath, Get, GetFont, List, ListAvailable, FindByPath, ResolveName
12. modules/fonts/defaults.lua       — SetDefault, GetDefault, SetDefaultSize, SetDefaultFlags
13. modules/fonts/apply.lua          — Apply, Quick, ApplyChildren, CreateString, FromTemplate
14. modules/fonts/normalize.lua      — SplitFlags, NormalizeFlags, DescribeFlags, NormalizeColorValue, NormalizeShadowOffset
15. modules/fonts/styles.lua         — NormalizeStyle, CreateStyle, GetStyle, ApplyStyle, ApplyStyleMap
16. modules/fonts/grouping.lua       — GetGroupedFonts, BuildGroupedFontItems, GetCategoryLabel
17. modules/fonts/dropdowns.lua      — CreateFontDropdown, CreateSimpleFontSelector
18. modules/fonts/controls.lua       — CreateFontSettingControl, AttachFontSelector, GetOptionValues
19. modules/fonts/menuitems.lua      — CreateFontMenuItems, CreateFlagMenuItems, CreateSizeMenuItems, CreateStyleMenuItems
20. modules/fonts/selectors.lua      — CreateStyleSelector, AttachStyleSelector, CreateStyleEditorFrame
21. modules/fonts/preview.lua        — CreateTestFrame, ToggleTestFrame, _GetPreviewSample, _ApplyPreviewSelection
22. modules/colors/colors.lua        — color registry, Get, GetRGB, GetHex, Wrap, Lerp, OpenPicker, CreateColorPicker
23. modules/colors/colorpicker.lua   — rectangular HSV picker widget (v2.0.0)
24. modules/textures/textures.lua    — RegisterBar, GetBar, CreateBarDropdown, ImportLibSharedMedia
25. modules/design/design.lua        — RGXDesign palette, ApplyBackdrop, CreateFrame, CreateButton, CreateSection
26. modules/ui/controls.lua          — CreateSlider, CreateToggle, CreateLabel, CreateColorPicker, CreateDropdown, CreateSection
27. modules/ui/options.lua           — CreateOptionsPanel, tab system, scroll container, auto-layout helper
28. modules/minimap/minimap.lua      — CreateMinimapButton, angle persistence, drag tracking
29. modules/databroker/databroker.lua — NewDataObject, live proxy, LDB bridge
30. modules/sound/sound.lua          — Register, Handle API, variant playback
31. core/commands.lua                — /rgx slash commands (modules, fonts, debug)
32. core/initialization.lua          — lifecycle: IsReady, OnReady, OnLogin, ADDON_LOADED handler, TryInit

The Varargs Pattern

Every XML <Script> file receives:

local addonName, addonTable = ...
  • addonName = "RGX-Framework"
  • addonTable = the same table reference as _G.RGXFramework

This means every file adds methods directly to the shared RGX table. No require(), no LibStub, no embedding — just direct table extension.

For submodules (Fonts, Colors, etc.), definitions.lua creates a local table and assigns it to both addon._fontsModule and a _G global. Subsequent font files read the module back from addon._fontsModule:

-- definitions.lua
local Fonts = {}
addon._fontsModule = Fonts
_G.RGXFonts = Fonts

-- init.lua
local Fonts = addon._fontsModule

Module Registration

RGX:RegisterModule(name, module, opts)

Registers a module into the central registry. Called by each module during its init.

Parameters:

Parameter Type Description
name string Module name (case-insensitive)
module table Module table
opts table? { global = "RGXFonts" } — global variable name

Behavior:

  1. Normalizes name to lowercase
  2. Stores in RGX.modules[name] and RGX.loadedModules[name]
  3. Sets module.name and module.framework
  4. If opts.global is provided and _G[global] is nil, assigns _G[global] = module
  5. Returns true on success, false on duplicate or invalid input

Module Alias Map

RGX.moduleAliases = {
    fonts       = "RGXFonts",
    colors      = "RGXColors",
    textures    = "RGXTextures",
    dropdowns   = "RGXDropdowns",
    ui          = "RGXUI",
    colorpicker = "RGXColorPicker",
    minimap     = "RGXMinimap",
    petbattles  = "RGXPetBattles",
    sharedmedia = "RGXSharedMedia",
    design      = "RGXDesign",
    combat      = "RGXCombat",
    reputation  = "RGXReputation",
    databroker  = "RGXDataBroker",
    sound       = "RGXSound",
}

RGX:GetModule(name) → module | nil

Looks up by normalized name, then falls back to alias resolution via _G.

RGX:RequireModule(name) → module | nil + error

Same as GetModule, but calls geterrorhandler() if the module is missing.

Shortcut Getters

RGX:GetFonts()        → RGXFonts
RGX:GetColors()       → RGXColors
RGX:GetTextures()     → RGXTextures
RGX:GetDropdowns()    → RGXDropdowns
RGX:GetUI()           → RGXUI
RGX:GetColorPicker()  → RGXColorPicker
RGX:GetMinimap()      → RGXMinimap
RGX:GetPetBattles()   → RGXPetBattles    -- dormant
RGX:GetSharedMedia()  → RGXSharedMedia   -- dormant
RGX:GetDesign()       → RGXDesign
RGX:GetCombat()       → RGXCombat        -- dormant
RGX:GetReputation()   → RGXReputation    -- dormant
RGX:GetDataBroker()   → RGXDataBroker
RGX:GetSound()        → RGXSound

Lifecycle

Phase 1: XML Load (synchronous)

All 44 files execute in order. Modules that self-initialize (Dropdowns, Colors, Textures, UI, Minimap, DataBroker, Design) call their own :Init() at the bottom of their file. This registers them with RGX:RegisterModule() and sets their _G global.

Phase 2: ADDON_LOADED

initialization.lua registers a handler for ADDON_LOADED with the ID "RGX_Init":

  1. Initialize _G.RGXFrameworkDB and RGX.db
  2. Call TryInit("RGXFonts") — calls Fonts:Init() if it hasn't been called yet
  3. Call TryInit("RGXSharedMedia") — no-op if module is dormant
  4. Call TryInit("RGXCombat") — no-op if module is dormant
  5. Call TryInit("RGXReputation") — no-op if module is dormant
  6. Set RGX._ready = true
  7. Fire all queued OnReady callbacks
  8. Unregister the ADDON_LOADED handler

PetBattles self-initializes in its own file and does not use TryInit.

Phase 3: PLAYER_LOGIN

Modules that need post-load state (SharedMedia scanning, PetBattles level scanning) hook PLAYER_LOGIN internally.


OnReady Queue

RGX:OnReady(function()
    -- runs immediately if framework is already ready
    -- otherwise queued until ADDON_LOADED completes
end)

Consumer addons should always wrap their setup in OnReady:

local RGX = assert(_G.RGXFramework, "MyAddon: RGX-Framework not loaded")
RGX:OnReady(function()
    local Fonts = RGX:GetFonts()
    -- safe to use all modules here
end)

Event Dispatch

RGX:RegisterEvent(event, callback, id, owner) uses a single hidden eventFrame with OnEvent script:

  1. WoW fires eventFrame:GetScript("OnEvent") with (frame, event, ...)
  2. The handler calls RGX:FireEvent(event, ...)
  3. FireEvent iterates RGX.events[event] bucket, calling each handler
  4. Errors are caught via pcall and sent to geterrorhandler()
  5. Unit events (RegisterUnitEvent) are filtered by unit token before dispatch

Handler IDs

Every registered handler gets a string ID. If you don't provide one, a default is generated from the callback's tostring(). Use explicit IDs for clean unregistration:

RGX:RegisterEvent("UNIT_AURA", myHandler, "myAddon_auraTracker")
-- later:
RGX:UnregisterEvent("UNIT_AURA", "myAddon_auraTracker")

Timer Driver

Timers are driven by a single OnUpdate frame (RGX.timerFrame). The budget system prevents frame-time spikes:

Setting Value Meaning
maxPerFrame 256 Max timer callbacks per frame
maxSeconds 0.033 Max seconds spent on timers per frame
slowSeconds 0.050 Threshold for slow-callback warnings

When the budget is exceeded, remaining timers are deferred to the next frame and a diagnostic message is printed (rate-limited to once per 2 seconds).

The timer frame's OnUpdate script is set to nil when no timers remain, avoiding per-frame overhead.


Combat Queue

RGX:QueueForCombat(func, ...)

If not in combat, calls func(...) immediately. If in combat, queues the call and fires it when PLAYER_REGEN_ENABLED fires. This is the foundation for all Safe* wrappers:

Method Delegates to
RGX:SafeShow(frame) frame:Show()
RGX:SafeHide(frame) frame:Hide()
RGX:SafeSetPoint(frame, ...) frame:ClearAllPoints() + frame:SetPoint(...)
RGX:SafeSetSize(frame, w, h) frame:SetSize(w, h)
RGX:SafeSetText(region, text) region:SetText(text)
RGX:SafeUIDropDownMenu_SetText(dropdown, text) UIDropDownMenu_SetText()
RGX:SafeUIDropDownMenu_Initialize(dropdown, init, mode) UIDropDownMenu_Initialize()
RGX:SafeUIDropDownMenu_Refresh(dropdown) UIDropDownMenu_Refresh()
RGX:SafeUIDropDownMenu_EnableDropDown(dropdown) UIDropDownMenu_EnableDropDown()
RGX:SafeUIDropDownMenu_DisableDropDown(dropdown) UIDropDownMenu_DisableDropDown()
RGX:SafeToggleDropDownMenu(...) ToggleDropDownMenu()
RGX:SafeCloseDropDownMenus(...) CloseDropDownMenus()

Dormant Modules

Four modules are in-tree but not loaded by RGX-Framework.xml since v1.5.18:

Module Global Why dormant
PetBattles RGXPetBattles Self-initializes; Get*() returns nil
SharedMedia RGXSharedMedia TryInit("RGXSharedMedia") is a no-op
Combat RGXCombat TryInit("RGXCombat") is a no-op
Reputation RGXReputation TryInit("RGXReputation") is a no-op

To re-enable: add the <Script> line to RGX-Framework.xml. TryInit in initialization.lua will automatically call Init() when the global becomes available.


Interdependency Graph

core.lua
  ├─ config.lua
  ├─ database.lua ─── uses config.defaults
  ├─ events.lua ──── uses core for Debug
  ├─ runtime.lua ─── uses core for Debug, After for combat queue
  └─ utils.lua ───── uses core for Clamp

dropdowns.lua ─── uses core for Debug, Safe*

fonts/
  definitions.lua ── standalone
  init.lua ───────── uses core (RegisterModule)
  registry.lua ───── uses core (Debug)
  query.lua ──────── uses core (Debug, Clamp)
  defaults.lua ───── uses core (Debug)
  apply.lua ──────── standalone (uses module refs)
  normalize.lua ──── uses core (Clamp), Colors module
  styles.lua ─────── uses core (Clamp), normalize
  grouping.lua ───── uses core (Debug)
  dropdowns.lua ──── uses Dropdowns module, core (Debug)
  controls.lua ───── uses Dropdowns module, core (Debug, CopyTable)
  menuitems.lua ──── uses grouping, normalize
  selectors.lua ──── uses Dropdowns module, core (CopyTable)
  preview.lua ────── uses core (Debug)

colors.lua ────── uses core (Clamp, Debug)
colorpicker.lua ─ uses core (Debug, RegisterModule)

textures.lua ──── uses core (Debug, RegisterModule), Dropdowns
design.lua ─────── uses core (RegisterModule) — no runtime deps

ui/controls.lua ── uses core, Design, ColorPicker, Textures, Dropdowns
ui/options.lua ─── uses core, Design, UI/controls

minimap.lua ────── uses core (Debug, RegisterModule)
databroker.lua ─── uses core (Debug, RegisterModule)
sound.lua ──────── uses core (Debug, RegisterModule, After)

combat.lua ─────── uses core (Debug, RegisterEvent, After) — dormant
reputation.lua ─── uses core (Debug, RegisterEvent, After) — dormant
petbattles.lua ─── uses core (Debug, RegisterEvent, After) — dormant
sharedmedia.lua ── uses core (Debug, RegisterEvent, After) — dormant

Database System

RGX:DB(name, defaults)

Creates or retrieves a _G[name] SavedVariables table. Deep-merges defaults without overwriting existing keys.

RGX:MigrateDB(db, name, currentVersion, migrations)

Version-based migration runner. Migrations execute from (storedVersion + 1) through currentVersion. On first install (no stored version), no migrations run.

RGX:MigrateDB(db, "MyAddonDB", 3, {
    [1] = function(db) db.newKey = true end,
    [2] = function(db) db.oldKey = nil end,
    [3] = function(db) db.nested = { a = 1 } end,
})

Config Defaults

RGX.defaults = {
    global = {
        debugMode = false,
        version = RGX.version,
    },
    profile = {
        fonts = {
            default = "Inter-Regular",
            defaultSize = 12,
            defaultFlags = "",
        },
    },
}

Clone this wiki locally