Architecture - RGXMods/RGX-Framework GitHub Wiki
Internals, load order, module registration, and conventions.
RGX has a modular registry, but implementation ownership is uneven. Modularity means a coherent responsibility behind a small interface, not a directory per helper or a new public module for every file.
| Area | Verified current structure | Ownership / next correction |
|---|---|---|
| Core systems | events, runtime/timers, DB and compatibility have dedicated implementations | Keep one owner per mechanic; addon-scoped methods delegate to these systems |
| Fonts | registry/query/apply/styles/selectors are separate files sharing one module | Useful internal decomposition; retain one public Fonts interface |
| UI | controls/options/layout/profiles are separate files sharing one UI module; the local follow-on moves declarative tab rendering into options | Controls own interaction, layout owns geometry, options owns panel/tab composition; keep one set of factories |
| Addon factory |
core/core.lua retains declaration compilation, scoped methods, lifecycle/storage wiring and panel configuration |
The local follow-on delegates rendering to UI's private compositor; the grammar stays in core and consumers still use RGXAddon
|
| Design/media | theme colors and frame primitives in Design; registries in media modules | Extend existing presentation contracts for skins; keep geometry/state in UI, not per-skin consumer forks |
| Game modules | registered with category = "game", capability/flavor gates and lifecycle initialization |
Keep domain state separate; verify callback contracts and missing capabilities before new DSL routes |
| Contract/MCP | local validation/preflight now delegate to the shared engine; availability comes from schema annotations | Shared vectors verify MCP/direct reports; remaining normalization/generation/editor scope stays in #8 |
| Runtime tests |
tools/ci/*-check.mjs plus tests/database_test.lua; the runtime DB diagnostic harness relocated out of the runtime package (#79), now covered headless via tools/ci/database-runtime-test.mjs
|
Diagnostic harness no longer XML-loaded; in-game /rgx dbtest reports the relocation |
- Preserve and reconcile current correctness fixes before moving their owners. Several local feature slices touch the same addon factory; a parked change is not integrated behavior.
- Integrate the local shared-validation correction in
contract/engine/and MCP; retain runtime Lua validation and conformance against the same supported forms. - Verify/integrate the local options-rendering separation.
library/ui/options.luacomposes tabs and calls existing scroll/column/flow/control factories through a private seam; core supplies resolved declarations and retains lifecycle. This follow-on is separate from the committed beta.3 snapshot and introduces no consumer API, module, global or package-file expansion. - Complete page/card composition and modular skin resolution through their existing owners, with consumer/client evidence. Column distribution alone is not a card model; palette selection alone is not a native frame skin.
- Relocate the runtime DB diagnostic only with an explicit supported replacement
for
/rgx dbtest. Directory taxonomy moves come after dependency checks, not before correctness or as a substitute for responsibility separation.
These are scoped follow-ups under #32, contract engine #8, layout #11 and themes #21. This review records implementation seams, not completed refactors.
RGX-Framework is one WoW addon with one loaded instance, one compatibility boundary, and one public contract. Internally it separates eight layers with a one-way dependency direction:
Consumer addons
โ RequiredDeps: RGX-Framework
Public RGX API (RGXFramework / RGXAddon / Get* / RegisterModule)
โ
Runtime (core โ modules/libraries โ modules/game)
โ conforms to
Canonical contract (contract/schemas + contract/engine)
โ consumed by
MCP adapter ยท editor ยท CI ยท future Studio
| Layer | Lives in | Ships to players | Rules |
|---|---|---|---|
| Runtime kernel | core/ |
Yes | Registry, events, timers, DB, compat, lifecycle only; feature systems belong in modules |
| Framework libraries | modules/<library> |
Yes |
category = "library"; reusable by any consumer |
| Game adapters | modules/<game> |
Yes |
category = "game"; WoW-domain events gated by flavor capability |
| Public API |
RGXFramework, RGXAddon, Get*
|
Yes | Frozen 31-global surface โ npm run arch-check fails any new _G write |
| Contract |
contract/schemas, contract/engine
|
No | Machine form of the declarative API; runtime remains authoritative for behavior |
| Tooling |
tools/rgx-mcp, tools/ci, tools/reference, tools/release, tools/wiki
|
No | Adapters over the contract; never own RGX semantics |
| Tests |
tools/ci/*-check.mjs, modules/*/tests/
|
Normally no | Headless checks stay outside runtime; XML still explicitly loads the /rgx dbtest diagnostic |
| WoW reference |
.reference/ (untracked trees + tracked manifests/provenance) |
No | Generated cache; sync via tools/reference/sync-*.mjs
|
Two machine-enforced invariants (npm run arch-check, wired into shared CI):
-
Module graph (
tools/ci/module-graph-check.mjs): registered names are unique, declareddependsexist and load earlier in the XML, the dependency graph is acyclic,flavorsdeclarations have matching central gates incore/compat.lua, every module declares acategory, and the public global surface stays frozen. -
Runtime boundary (
tools/ci/runtime-boundary-check.mjs): nocore/ormodules/Lua may referencetools/,contract/,.reference/,rgx-mcp, ornode_modules, and may never userequire/dofile/loadfileโ WoW addons load exclusively through the TOC/XML manifest.
RGX-Framework exposes a single global table:
_G.RGXFrameworkEvery module and every consumer addon references this same table. There is no LibStub, no version negotiation, and no embedding โ one addon, one instance.
RGX-Framework.xml loads in this exact order. The compatibility layer is now
first: every later file can consult flavor flags and RGX:ModuleSupported at
load time, and RGX:RegisterModule refuses gated registrations centrally. Both
compat files preserve the shared RGX.API table (RGX.API = RGX.API or {}) โ
earlier revisions silently wiped predicates when the second file loaded, which
is a load-order bug class we now test for (tools/ci/compat-loader-check.mjs).
Every RGX.API name has exactly one defining file: core/compat.lua owns the
normalized implementations and core/compat_api.lua only adds names it does
not define, enforced across the XML load list by
tools/ci/api-ownership-check.mjs.
WoW loads files in the order declared in RGX-Framework.xml. The framework uses this sequence:
0. core/compat.lua, core/compat_api.lua โ capability foundation before registration
1. core/core.lua โ global object, module registry, Mixin, CopyTable, Clamp, Lerp, TableCount, Print/Warn/Error/Debug
2. core/systems/config.lua โ framework defaults (debugMode, default font, size, flags)
3. core/systems/database.luaโ RGX:DB(name, defaults), RGX:InitDatabase()
3b. `tests/database_test.lua` (relocated, #79) โ the DB diagnostic harness now lives out of the runtime package and is exercised headless via `tools/ci/database-runtime-test.mjs`
4. core/systems/events.lua โ RegisterEvent, RegisterMessage, CreateEmitter, ADDON_ACTION_BLOCKED monitor
5. core/systems/runtime.lua โ After, Every, CancelTimer, Hook, RegisterSlashCommand, combat queue, Safe* helpers
6. core/systems/utils.lua โ Trim, Split, TableKeys/Values/Contains/Map/Filter/Find, MergeTable, Round, Format, Clamp, StartsWith, EndsWith
6b. library/locale/locale.lua, overrides.lua โ locale registry and framework translations
7. library/dropdowns/dropdowns.lua โ CreateNestedDropdown, CopyItem, NormalizeItems, ForceWidth, AddInlineButton
8. library/fonts/definitions.lua โ 36 font definitions, unavailableFonts blocklist
9. library/fonts/init.lua โ Fonts:Init(), RegisterModule("fonts")
10. library/fonts/registry.lua โ Register, RegisterAddonFont, RegisterFontPack
11. library/fonts/query.lua โ GetPath, Get, Exists, IsAvailable, List, ListAvailable, FindByPath
12. library/fonts/defaults.lua โ SetDefault, GetDefault, SetDefaultSize, SetDefaultFlags, SetAutoScale
13. library/fonts/apply.lua โ Apply, Quick, ApplyChildren, CreateString, FromTemplate
14. library/fonts/normalize.lua โ SplitFlags, NormalizeFlags, DescribeFlags, GetFlagPresets, NormalizeFontPath
15. library/fonts/styles.lua โ NormalizeStyle, NormalizeColorValue, CreateStyle, ApplyStyle/ApplyTextStyle
16. library/fonts/grouping.lua โ BuildGroupedFontItems, _groupedFontsCache
17. library/fonts/dropdowns.lua โ CreateFontDropdown, buildItems
18. library/fonts/controls.lua โ CreateFontSettingControl
19. library/fonts/menuitems.lua โ CreateFontMenuItems, CreateFlagMenuItems, CreateSizeMenuItems, CreateStyleMenuItems
20. library/fonts/selectors.lua โ CreateStyleSelector, CreateSimpleFontSelector, AttachStyleSelector, AttachFontSelector
21. library/fonts/preview.lua โ FontPreview:Create, _ApplyPreviewSelection
22. library/colors/colors.lua โ full color API (lookup, math, wrapping, apply, picker)
23. library/colors/colorpicker.lua โ HSV color picker widget
24. library/textures/textures.lua โ statusbar texture registry, LSM import
25. library/design/design.lua โ Design.Colors static palette, visual building blocks
25b. library/display/display.lua โ positioned/scalable display elements, drag edit mode
26. library/ui/controls.lua โ UI control factory (slider, toggle, label, dropdown, etc.)
27. library/ui/options.lua โ CreateOptionsPanel (tabbed settings window)
27b. library/ui/layout.lua, profiles.lua, guide.lua โ composition, profiles and guide
28. library/minimap/minimap.lua โ circular-drag minimap button
29. library/sharedmedia/sharedmedia.lua โ multi-type media registry, DBM/known-addon/generic scanners
30. modules/combat/combat.lua โ combat enter/leave/kill/crit/low-health/encounter callbacks
31. modules/petbattles/petbattles.lua โ pet battle level/capture/state callbacks
32. modules/reputation/reputation.lua โ reputation and renown tracking callbacks
32b. modules/auras/auras.lua, library/tooltip/tooltip.lua โ aura boundary and tooltip composition
33. library/databroker/databroker.lua โ NewDataObject, LDB bridge
34. library/sound/sound.lua โ Sound:Register, variant playback, SavedVar integration
35. modules/achievement/achievement.lua โ achievement unlock callbacks
36. modules/levelup/levelup.lua โ level-up callbacks
37. modules/quest/quest.lua โ quest lifecycle and progress callbacks
38. modules/honor/honor.lua โ honor level callbacks
39. modules/delves/delves.lua โ delve companion/lives callbacks
40. modules/housing/housing.lua โ housing progression/decor callbacks
41. modules/tradingpost/tradingpost.lua โ Trading Post activity callbacks
42. modules/prey/prey.lua โ prey hunt callbacks
43. modules/collectibles/collectibles.lua โ mount/pet/toy unlock callbacks
44. modules/loot/loot.lua โ loot and currency callbacks
45. core/commands.lua โ /rgx slash command handler (modules, fonts, debug)
46. core/initialization.lua โ ADDON_LOADED handler, database init, module TryInit, OnReady lifecycle
Exact load order is authoritative in
RGX-Framework.xml; the labels above summarize it and are not manifest indices. File existence alone does not prove registration, initialization or availability on every flavor.
Required dependencies load before the consumer's Lua files. This does not make
gameplay data or consumer SavedVariables ready at chunk load: use the proper
readiness/ADDON_LOADED lifecycle and capability gates.
Every Lua file loaded via WoW's <Script> tag receives the addon name and the addon table through the ... varargs:
local addonName, RGX = ...Critical subtlety: WoW passes the same private table to every file. local _, MyModule = ... does not create a unique module table โ MyModule is the same table as _G.RGXFramework. This means:
- Generic field names like
Init,name, ordbcan collide across split-module files - The fonts sub-module files (definitions, registry, query, etc.) all populate the same
Fontstable โ they use specific, non-colliding field names - If two files both define
function MyModule:Init(), the second one overwrites the first
Convention: Module files should either use unique field names or be organized so that only one file defines any given method.
Modules register themselves at load time:
RGX:RegisterModule(name, moduleTable, { global = "RGXFoo" })This stores the module in RGX.modules[name] and optionally publishes it to _G["RGXFoo"].
RGX:GetModule(name) resolves in two steps:
- Check
RGX.modules[normalizedName](wherenormalizedName = string.lower(name)) - Fall back to
ResolveModuleAliasโ look up_G[self.moduleAliases[normalizedName]]
This means a module that sets its own global (e.g. RGXFonts = Fonts) before the framework processes it will still be found via the alias fallback.
The framework provides typed convenience wrappers:
RGX:GetFonts() -- "fonts" โ RGXFonts
RGX:GetColors() -- "colors" โ RGXColors
RGX:GetTextures() -- "textures" โ RGXTextures
RGX:GetDropdowns() -- "dropdowns" โ RGXDropdowns
RGX:GetUI() -- "ui" โ RGXUI
RGX:GetColorPicker()-- "colorpicker" โ RGXColorPicker
RGX:GetMinimap() -- "minimap" โ RGXMinimap
RGX:GetDesign() -- "design" โ RGXDesign
RGX:GetDisplay() -- "display" โ RGXDisplay
RGX:GetDataBroker() -- "databroker" โ RGXDataBroker
RGX:GetSound() -- "sound" โ RGXSoundAs of v2.1.0, there are no dormant modules. All in-tree modules are loaded by the XML loader.
Feature modules that can only exist on specific client families skip registration via RGX:ModuleSupported(name) at file scope, decided by the client's own global surface (the namespace the module drives), not the product string. The support map is evidence-coded in core/compat.lua from the synced client dumps; the module's flavors registration metadata records the same list so the taxonomy and gate stay congruent. Blizzard's misspelled CURRENT_HOUSE_INFO_RECIEVED event exists on every flavor and never substitutes for a namespace check. Registration tests live in tools/ci/module-gating-check.mjs.
Every module declares its architectural class at registration:
category = "library" for framework capabilities consumed by any addon
(fonts, colors, textures, dropdowns, ui, design, display, minimap, tooltip, databroker,
sharedmedia, sound, locale) and category = "game" for WoW-domain adapters
(auras, quest, combat, levelup, housing, and the rest). depends lists must
load earlier in RGX-Framework.xml โ the checker proves it. stability
(stable | experimental | deprecated | internal) is recorded alongside; the
machine-readable API catalog milestone formalizes it further. The global
namespace is frozen at 32 names: module-graph-check.mjs fails both when a
new _G.RGX* write appears and when an allowlisted global stops being
written.
Previously dormant modules and when they were re-enabled:
| Module | Global | Re-enabled |
|---|---|---|
| SharedMedia | RGXSharedMedia |
v2.0.0 |
| PetBattles | RGXPetBattles |
v2.0.0 |
| Reputation | RGXReputation |
v2.0.0 |
| Combat | RGXCombat |
v2.1.0 |
| Achievement, LevelUp, Quest, Honor, Delves, Housing, TradingPost, Prey | various | v2.1.0 |
When WoW fires ADDON_LOADED for "RGX-Framework":
- Initialize
_G.RGXFrameworkDB(or reuse existing) - Set
RGX.db = _G.RGXFrameworkDB - Call
TryInit("RGXFonts")โ runsFonts:Init() - Call
TryInitfor each active module: SharedMedia, Combat, PetBattles, Reputation, Achievement, LevelUp, Quest, Honor, Delves, Housing, TradingPost, Prey, Collectibles, Loot - Set
RGX._ready = true - Fire all queued
OnReadycallbacks - Unregister the ADDON_LOADED handler
RGX:OnReady(fn)If the framework is already initialized, fn runs immediately. Otherwise it is queued and fired during step 6 above.
Consumer addons should use OnReady when they need initialized modules (fonts, colors, etc.). For core-only APIs (events, timers, hooks, slash commands), _G.RGXFramework is available immediately โ no OnReady needed.
RGX runs its own tick-based timer driver on a hidden OnUpdate frame. Timers are plain tables:
timer = {
id, label, duration, callback, repeating, elapsed, active,
owner?, name?, declarativeName?
}-
RGX:After(dur, cb)โ one-shot, returns timer ref -
RGX:Every(dur, cb)โ repeating, cb receivestimeras first arg so it can cancel itself -
RGX:CancelTimer(timer)โ markstimer.active = false; removed on next tick
RGXAddon can declare every = { name = { seconds, handler } }. These timers
start after the consumer's matching ADDON_LOADED, carry owner/name metadata,
and use a stable AddonName:every:name label. Definitions are sorted and
registered in reverse because the driver walks newest-to-oldest; timers from one
declaration that become due on the same update therefore dispatch in lexical
name order. A persistent scan cursor resumes budget-deferred work on the next
update so a large due set cannot starve later names. Callback errors retain the
timer label and remain failure-isolated.
Budget: timerBudget = { maxPerFrame = 256, maxSeconds = 0.033, slowSeconds = 0.250, slowByLabel = { ["SharedMedia:QueueScan"] = 0.500 } }. Slow callbacks (>250ms by default) are reported via [RGX:timer-slow]; known-heavy labels get per-label overrides instead of raising the global threshold. The driver pauses OnUpdate when no active timers remain. (The threshold was raised from 50ms in v2.0.0-alpha.1 โ media scanning is normal I/O, not a fault.)
Two dispatch channels share the same internal handler registry:
| Channel | API | Scope |
|---|---|---|
| Events |
RegisterEvent, UnregisterEvent, FireEvent
|
WoW C events via OnEvent frame |
| Messages |
RegisterMessage, UnregisterMessage, SendMessage
|
Internal addon-to-addon / module-to-module |
Both support id (for targeted unregistration) and owner (for method-name callbacks). Dispatch is pcall-wrapped with error reporting.
RegisterCallback / UnregisterCallback are aliases for RegisterMessage / UnregisterMessage.
Module-local callback emitters:
local emitter = RGX:CreateEmitter("MyModule")
emitter:RegisterCallback("DATA_CHANGED", fn, id)
emitter:Fire("DATA_CHANGED", data)RGX:QueueForCombat(func, ...)If not in combat lockdown, func runs immediately. Otherwise it is queued and processed when PLAYER_REGEN_ENABLED fires.
The Safe* helpers (SafeShow, SafeHide, SafeSetPoint, SafeSetSize, SafeSetText, and the UIDropDownMenu variants) all use this queue internally.
Framework DB is RGXFrameworkDB (declared in TOC as SavedVariables). Consumer addons use their own SavedVariables managed via RGX:NewDatabase(name, defaults, opts), which returns a profile-aware proxy with metamethod access (shipped in v1.9.0, hardened in v2.0.0).
The framework's config.lua provides defaults:
defaults = {
global = {
debugMode = false,
},
profile = {
fonts = {
default = "Inter-Regular",
defaultSize = 12,
defaultFlags = "",
},
},
}Core (events, runtime, utils, config, database)
โโโ Dropdowns (no deps beyond core)
โโโ Fonts (depends on Dropdowns for CreateFontDropdown)
โ โโโ uses RGXDropdowns.CreateNestedDropdown internally
โโโ Colors (no deps beyond core)
โ โโโ ColorPicker (no deps beyond core + Colors)
โโโ Textures (no deps beyond core)
โโโ Design (depends on Colors for palette)
โโโ UI (depends on Fonts, Colors, Textures, Dropdowns for control factories)
โโโ Minimap (no deps beyond core)
โโโ DataBroker (no deps beyond core)
โโโ Sound (no deps beyond core)
-
No C_Timer โ all deferred work uses
RGX:After/RGX:Every. Inside the framework itself, four call sites keep a guardedelseif C_Timer.Afterfallback for the edge case where the timer driver is unavailable (options.lua x2, sharedmedia.lua, reputation.lua); consumer-facing code has no such exception. -
No manual event frames โ use
RGX:RegisterEvent -
No raw SLASH_X patterns โ use
RGX:RegisterSlashCommand -
assert(_G.RGXFramework, ...)โ consumer addons fail fast if RGX is missing -
RequiredDeps: RGX-Frameworkโ TOC dependency, not optional embedding -
Module methods are colon-call โ
Fonts:GetPath("Inter-Regular"), notFonts.GetPath(Fonts, ...) -
Font paths are absolute โ
"Interface\\AddOns\\RGX-Framework\\media\\fonts\\Inter-Regular.otf" -
Unavailable fonts are in-tree but blocked (corrupted assets) โ
unavailableFontslist in definitions.lua;IsAvailable()returns false;ListAvailable()excludes them; they cannot be selected in dropdowns