promotional bannermobile promotional banner

Project Roth

Project Roth is the revival of an old classic wow UI mod called Roth_UI modeled after Diablo UI. Revival has tried to keep it as close to original with new features and the ability to work with new interfce standards.

File Details

Roth_UI_v3.1.1.zip

  • R
  • May 3, 2026
  • 4.91 MB
  • 49
  • 2.5.5+4
  • Classic TBC

File Name

Roth_UI_v3.1.1.zip

Supported Versions

  • 2.5.5
  • 2.5.4
  • 2.5.3
  • 2.5.2
  • 2.5.1

Version 3.1.1 - Taint Regression Hotfix & Codebase Audit Pass
--------------------------------------------------------------

OVERVIEW — WHAT SHIPPED IN 3.1.1:
• Buff/debuff positioning (modules/MovableBuffs.lua,
  modules/MovableDebuffs.lua): restored the hooksecurefunc + dirty-flag +
  OnUpdate design (no SetPoint/ClearAllPoints replacement — avoids GameMenu /
  CompactRaidFrame taint). Lazy-created AuraContainer now gets hooks installed
  on retry so icons stay on Roth anchors across instances and BGs.
• Roth_UI_Protection.isLoggingOut (embeds/rLib/core.lua, FixBlizzBarNil.lua,
  embeds/rMinimap/core.lua): flag now clears on PLAYER_ENTERING_WORLD so it
  does not latch true forever after the first load screen (was blocking
  movable aura re-anchor, oUF_Smooth, framefader, rMinimap re-apply).
• Mid-combat aura drift: removed InCombatLockdown gates from the movable
  aura ApplyAnchor / OnUpdate path so Blizzard grid layout cannot leave
  debuffs at default TOPRIGHT until combat ends.
• ShinyBuffs (embeds/Roth_ShinyBuffs/Roth_ShinyBuffs.lua): single "Font Sizes"
  UI drives buff + debuff duration/count text; SyncFontSizes migration;
  aligned defaults.
• rInfoStrings (embeds/rInfostrings/core/core.lua): Level Time / Total Time
  below minimap button bar; XP/rep line hidden at max level (rep math /
  MAXED OUT audit documented in-file); sub-max XP/rep hangs below time block.
• rChat (embeds/rChat/core/core.lua): reclaimChatBottomGap removes dead strip
  below chat when editbox is anchored above the frame (absolute floor pin,
  idempotent across /reload).
• rMinimap Alt+Z / minimap fixes; codebase audit pass (compat, tooltip typo,
  zoom hooks, init_coordinator errors, ShinyBuffs debug gating, etc.);
  BugSack triage narrative unchanged below.

DEFERRED / FUTURE WORK (NOT IN THIS RELEASE):
• Hook Blizzard Edit Mode together with Roth's custom unlock (/roth unlock*,
  drag overlays) so chat bottom-pin vs manual placement, cast bar drag frames,
  and buff/debuff anchors can be tuned from one workflow — pending follow-up.

RCHAT — RECLAIM EMPTY EDITBOX-RESERVATION SPACE BELOW CHAT FRAME:
• Symptom: Empty vertical strip between the bottom of the chat frame
  and the bottom of the screen. Blizzard's default layout reserves
  ~25-30 px there for the editbox, but rChat moves the editbox to
  ABOVE the chat frame (eb:SetPoint("BOTTOM", self, "TOP", 0, 22)) so
  the bottom strip became dead space.
• Fix: New reclaimChatBottomGap helper called at the end of skinChat
  for every chat frame. Pins the chat frame's bottom edge to an
  absolute floor (CHAT_BOTTOM_FLOOR = 5 px above UIParent.BOTTOM)
  rather than subtracting a relative delta — this is critical because
  Blizzard saves chat dimensions across /reload via
  FCF_SavePositionAndDimensions, and a relative delta would
  accumulate height every session. The absolute-floor approach makes
  subsequent skinChat passes no-ops once the frame is already at the
  floor.
• Behavior: For bottom-anchored chat frames the bottom anchor's y is
  shifted down to the floor and height grows by the same delta so the
  top edge (and the editbox above it) doesn't move. For top-anchored
  frames, height alone grows so the bottom extends down toward the
  floor. Wrapped in pcall and guarded against missing GetPoint /
  GetBottom returns.

INFOSTRINGS — XP/REP LINE RELOCATED BELOW LEVEL TIME / TOTAL TIME:
• Anchor chain reorganized so the f3 XP/rep line hangs from f4's
  bottom edge instead of stacking inside the minimap above f4. New
  vertical order under the minimap is:
    f1 zone           (inside minimap, top)
    f2 coords/lat/fps (inside minimap, below f1)
    [button bar]      (below minimap)
    f4 Level / Total Time
    f3 XP / rep       (only renders below max level)
• f3 still renders empty at max level (returns "" from rsiExpRep), so
  there's no visible gap there for capped characters.

INFOSTRINGS — XP/REP LINE HIDDEN AT MAX LEVEL + REP-LOGIC AUDIT:
• rsiExpRep now returns "" once the player hits MAX_PLAYER_LEVEL,
  removing the green reputation line entirely once you're at cap. The
  XP branch (sub-max levels, with rested chunk in gold) is unchanged.
• Audit notes left as a code comment (legacy max-level rep branch
  preserved in --[[ ]] form for future reference). Two long-standing
  bugs were identified in the disabled rep code:
    1. MAXED OUT detection compared `(value-minimum)==999 and
       (maximum-minimum)==1000`, which only matches when the bar sits
       at exactly 999/1000. For Exalted in TBC GetWatchedFactionInfo
       returns minimum=42000, maximum=43000, value sweeps 42000..43000,
       so value-minimum is 0..1000 and never 999 except by coincidence
       — MAXED OUT effectively never fired.
    2. The `_, _, minimum, ...` destructure threw away standingId, so
       even fixing the test required re-capturing it.
  Recommended fix if max-level rep is ever re-enabled in this slot:
  capture standingId and use `standingId == 8 and value >= maximum`.

INFOSTRINGS — LEVEL TIME / TOTAL TIME RELOCATED BELOW BUTTON BAR:
• The f4 container (Level Time + Total Time, populated from
  TIME_PLAYED_MSG) used to stack vertically with the rest of the info
  strings INSIDE the minimap, which caused it to overlap the map view
  on smaller minimap setups.
• Re-anchored f4 directly to TOP, Minimap, BOTTOM, 0, -40 so it sits
  cleanly below rMinimap's bottom button bar (bar lives at y=[-35,-2]
  given BUTTON_SIZE=33 / BOTTOM_OFFSET=-2). Falls back to the old
  stacked anchor if Minimap isn't available when rInfoStrings loads.
• f1 (zone) / f2 (coords+latency+fps) / f3 (xp/rep) still stack inside
  the minimap as before — only the Level Time / Total Time pair moved.

SHINYBUFFS — UNIFIED FONT SIZES FOR BUFFS + DEBUFFS:
• Options panel "Buff Font Sizes" and "Debuff Font Sizes" sections
  collapsed into a single "Font Sizes" group. Duration size and Count
  size sliders now drive both buff and debuff text. The Font selector
  and Font Flag selector were already shared.
• Implementation keeps the dual storage (db.buffs.dfsize /
  db.debuffs.dfsize, etc.) because the Skinning pipeline keys font
  sizes off db[svtable].* per aura type. The unified slider's set
  function mirrors the value into both tables and reskins both sides.
• One-time migration on PEW (SyncFontSizes) converges existing saved
  variables that had mismatched buff vs debuff sizes — both tables are
  set to the buff side's value (which is what the unified slider's get
  reads from), so what you see in the slider matches what's rendered.
• Defaults updated so a fresh install starts both tables at the same
  values (dfsize=14, cfsize=17 for both) and won't reintroduce the
  mismatch on a SavedVariables wipe.

MOVABLE BUFFS/DEBUFFS — DRIFT DURING COMBAT (esp. DEBUFFS):
• Symptom: During combat, debuffs (and to a lesser extent buffs) would
  visibly drift to Blizzard's grid-default position whenever an aura
  churned (took damage / ticking poisons / ramping debuffs). They would
  snap back to the user's anchor only after combat ended.
• Root cause: ApplyAnchor and the OnUpdate processor in MovableBuffs.lua
  / MovableDebuffs.lua both gated on InCombatLockdown(). Each time
  Blizzard's UpdateGridLayout called AuraContainer:SetPoint (which fires
  on every aura add/remove — extremely frequent in combat for debuffs),
  our hook flipped dirty=true, but the OnUpdate processor's combat
  check made ApplyAnchor a no-op until combat ended. The container sat
  at Blizzard's grid position for the duration of the fight.
• Fix: Removed the InCombatLockdown gates from ApplyAnchor and the
  OnUpdate processor in both modules. BuffFrame / DebuffFrame /
  AuraContainer are EditModeSystem / GridLayoutFrame frames — none of
  them are SecureTemplate-protected, so their SetPoint /
  ClearAllPoints can safely be called during combat. Blizzard's own
  UpdateGridLayout calls these methods mid-combat constantly; our
  re-anchor doing the same is benign. The Roth_UI_Protection.isLoggingOut
  gate and the existing pcall around the re-anchor body remain as the
  safety net.

PROTECTION-FLAG REGRESSION — isLoggingOut STUCK TRUE AFTER FIRST ZONE CHANGE:
• Symptom: Buffs/debuffs would snap back to the upper-right Blizzard
  default position when loading into a battleground (and any other
  load-screen transition), and would NOT recover even after the
  AuraContainer-hook fix. /reload was the only workaround per session.
• Root cause: rLib/core.lua, FixBlizzBarNil.lua, and rMinimap/core.lua
  all set their `isLoggingOut` protection flag on PLAYER_LEAVING_WORLD
  AND PLAYER_LOGOUT, but never cleared the flag back to false.
  PLAYER_LEAVING_WORLD fires on every zone/instance/BG transition (just
  before the load screen) — not only on actual logout. So the very first
  time the player crossed any load screen in a session, the global
  Roth_UI_Protection.isLoggingOut (and the file-local equivalents in
  FixBlizzBarNil + rMinimap) latched true and stayed true forever.
  Every consumer that gates on it then silently bailed for the rest of
  the session: MovableBuffs/MovableDebuffs OnUpdate stopped processing
  the dirty flag (so re-anchor never ran), oUF_Smooth's bar driver
  stopped updating, framefader stopped, and rMinimap stopped re-applying
  its settings on subsequent zone changes.
• Fix in embeds/rLib/core.lua, FixBlizzBarNil.lua, and
  embeds/rMinimap/core.lua: each logout-tracking frame now also
  registers PLAYER_ENTERING_WORLD and clears its isLoggingOut flag back
  to false when that event fires. PLAYER_ENTERING_WORLD doesn't fire
  during real logout, so the protection during shutdown is preserved
  while zone/instance/BG loads correctly clear the flag.
• Frame ordering: in all three files the logout frame is created and
  registers PLAYER_ENTERING_WORLD BEFORE any consumer event frame in
  the same file, so WoW's registration-order event dispatch guarantees
  the flag is cleared before any consumer handler observes it.

MOVABLE BUFFS/DEBUFFS — AURACONTAINER NOT RE-ANCHORED ON INSTANCE CHANGE:
• Symptom: After zoning into an instance the buff icons snapped back to
  the upper-right of the screen (Blizzard's default TOPRIGHT position),
  ignoring the user's saved Roth_UIBuffAnchor / Roth_UIDebuffAnchor
  position. /reload fixed it; next instance change broke it again.
• Root cause: BuffFrame.AuraContainer (and DebuffFrame.AuraContainer)
  is created LAZILY by Blizzard — it is often nil at PLAYER_LOGIN and
  only materializes on the first PEW into a populated zone or the first
  UNIT_AURA dispatch. The initial InstallHooks() routine bundled both
  the BuffFrame and AuraContainer hook installs behind a single
  `hooksInstalled` boolean. If AuraContainer was nil the first time
  InstallHooks ran (overwhelmingly the case at PLAYER_LOGIN), the AC
  hooks were silently skipped AND the boolean was set to true,
  permanently locking out any subsequent install attempt. Because
  AuraContainer is what visually positions the aura buttons, skipping
  its hook meant Blizzard's EditModeManager / UpdateGridLayout could
  freely reset its position on every PEW and our addon never noticed.
  ApplyAnchor's AC branch was also a no-op because origAC_SetPoint /
  origAC_ClearAllPoints were never captured.
• Fix in modules/MovableBuffs.lua and modules/MovableDebuffs.lua:
  - Split InstallHooks into InstallBFHooks / InstallDFHooks (frame
    hooks, idempotent, runs once) and InstallACHooks (container hooks,
    re-attempted on every Attach call and on every OnUpdate tick).
  - Track the hooked AuraContainer reference (`hookedAuraContainer`) so
    InstallACHooks can detect both first-time installation AND a
    Blizzard-side container swap (defensive — in practice the container
    instance stays the same).
  - InstallACHooks sets dirty = true the moment it finishes installing,
    so a re-anchor is scheduled immediately rather than waiting for the
    next Blizzard layout pass to dirty the flag.
  - ApplyAnchor now falls back to the live AuraContainer SetPoint /
    ClearAllPoints methods when origAC_* aren't captured yet.
    hooksecurefunc never replaces the underlying method, so the live
    refs remain the genuine Blizzard implementations — safe to call.
  - OnUpdate processor now invokes InstallACHooks() every tick (cheap:
    early-exits when the container is already hooked) as a safety net
    for the case where Blizzard creates the container between events.
  - Same pattern mirrored on the Debuff side for DebuffFrame /
    DebuffFrame.AuraContainer.

TAINT — MOVABLE BUFFS / DEBUFFS REGRESSION FIX:
• Root cause: GameMenuFrame Logout / Exit Game buttons would stop
  responding after enough time online, requiring a /reload to restore
  them. MovableBuffs.lua and MovableDebuffs.lua had regressed to
  replacing BuffFrame.SetPoint / BuffFrame.ClearAllPoints (and the
  same on DebuffFrame + their AuraContainers) with empty no-op
  closures. Every Blizzard secure code path that later invoked
  BuffFrame:SetPoint (aura updates, party changes, group config,
  etc.) executed addon-owned tainted code, propagating taint through
  the secure call graph until it polluted GameMenuFrame's button
  handlers — which is exactly the failure mode the original 3.1.0
  notes warned about ("hooks now only set a boolean, zero frame
  modifications inside hook bodies").
• MovableBuffs / MovableDebuffs rewritten to the documented design:
  - NEVER replace SetPoint or ClearAllPoints on BuffFrame /
    DebuffFrame / their AuraContainers
  - Save references to the original Blizzard methods at first attach
    so re-anchoring always uses the genuine implementation
  - hooksecurefunc on SetPoint + ClearAllPoints; hook body only flips
    a `dirty` boolean (zero frame mutation inside the secure hook)
  - Single OnUpdate handler processes the dirty flag; gated on
    Roth_UI_Protection.isLoggingOut only (follow-up in this release:
    InCombatLockdown gate removed — see "MOVABLE BUFFS/DEBUFFS — DRIFT
    DURING COMBAT" above — those frames are not SecureTemplate-protected)
  - `reanchoring` guard suppresses the hook during our own re-anchor
    SetPoint call to prevent infinite re-trigger loops

MINIMAP — ALT+Z / UI HIDE-SHOW VISUAL FIX:
• Symptom: Hiding the UI with Alt+Z then restoring it left a
  duplicate "Thunder Ridge"-style zone header bar floating above the
  minimap (Blizzard's MinimapZoneTextButton re-shown by default
  layout) and the map texture appeared stretched until the next zone
  change.
• MinimapZoneTextButton is now also hidden alongside MinimapBorderTop
  in ApplyMinimapSettings (was the "duplicate Thunder Ridge")
• Both MinimapBorderTop and MinimapZoneTextButton get a one-time
  HookScript("OnShow", Hide) so Blizzard re-showing them after a
  UIParent hide/show cycle is immediately reverted (visual frames,
  no taint risk)
• Added DISPLAY_SIZE_CHANGED + UI_SCALE_CHANGED to rMinimap event
  list so resolution/scale changes trigger a debounced re-apply
• Hooked UIParent:OnShow to flip the pendingApply flag on the
  rMinimap debounce frame — Alt+Z restoring the UI now triggers an
  automatic minimap re-apply on the next OnUpdate tick, restoring
  the square mask, sizing, and child-frame hiding

BUGSACK / !BUGGRABBER TRIAGE (no additional code changes required):
• Triaged the cached errors attributed to Roth_UI in BugGrabberDB
  across both account profiles. All ADDON_ACTION_BLOCKED /
  ADDON_ACTION_FORBIDDEN errors below trace to the same root cause
  and are expected to be resolved by the MovableBuffs / MovableDebuffs
  rewrite above:
  - GameMenuFrame.lua:67 callback() ADDON_ACTION_FORBIDDEN
    (Logout / Exit Game stop responding)
  - CompactPartyFrameMember1:SetSize() / CompactPartyFrameMember2:
    SetSize() ADDON_ACTION_BLOCKED via CompactRaidFrameContainer:
    TryUpdate -> RefreshMembers -> CompactUnitFrame_SetUpFrame
    (most prevalent recurring error, counter 11 in the last session)
  - CompactRaidFrame{3,11,12,28}:Show() / Hide() ADDON_ACTION_BLOCKED
    via CompactUnitFrame_UpdateVisible -> CompactUnitFrame_UpdateAll
  - ArenaEnemyFrame2:SetPoint() ADDON_ACTION_BLOCKED via
    ArenaEnemyFrame_UpdatePlayer
  Mechanism: Once BuffFrame.SetPoint / ClearAllPoints (and the same
  on DebuffFrame and AuraContainer) were replaced with addon-owned
  no-op closures, every Blizzard secure code path that called those
  methods executed under our taint. GROUP_ROSTER_UPDATE,
  ARENA_PREP_OPPONENT_SPECIALIZATIONS, and aura update events all
  share secure handler chains with CompactRaidFrame /
  CompactPartyFrame / ArenaEnemyFrame layout, so the taint
  propagated to their SetSize / Show / Hide / SetPoint calls and to
  GameMenuFrame button callbacks.
• Out-of-scope errors (NOT Roth_UI bugs, no fix attempted):
  - C_UnitAuras.GetAuraSlots called with unit=nil from Blizzard's
    CompactUnitFrame_UpdateAuras (counter 1472) — pure Blizzard bug
    in CompactUnitFrame.lua. We deliberately removed the
    AuraUtil.ForEachAura wrapper in 3.0.0 because the wrapper itself
    propagated taint to arena/party frames; the resulting Lua error
    is cosmetic (visible in BugSack only, no functional impact).
  - GameTooltip:SetUnit usage error in Blizzard_UnitFrame/Classic/
    UnitFrame.lua:453 (UnitFrame_UpdateTooltip) — Blizzard bug.
  - TacoTip InterfaceOptionsFrame_OpenToCategory nil — TacoTip bug.
  - Details!/AtlasLootClassic DecompressString internal error —
    Details!/AtlasLootClassic comm bug.

CODEBASE AUDIT — BUGS, PERFORMANCE & QUALITY PASS:

BUGS:
• rInfostrings memory tooltip: `local enry, memory` declared the
  wrong name, so the loop body's `entry = { name = ..., memory = ...}`
  was a stray global. Fixed the typo; entries now stay local to the
  tooltip build (no cross-tooltip leakage, no global pollution).
• ShinyBuffs SkinTench_Legacy: `hooksecurefunc(TemporaryEnchantFrame,
  "SetWidth", ...)` was being registered INSIDE the per-slot for-loop
  on the first skin pass — registering the same hook up to
  NUM_TEMP_ENCHANT_FRAMES times and calling it that many times per
  width change. Hoisted out of the loop with a module-level
  `tenchWidthHookInstalled` guard so the hook is registered exactly
  once.
• rMinimap zoom hooks: copy-paste duplication had Minimap_ZoomIn and
  Minimap_ZoomOut each wrapped with `hooksecurefunc` TWICE, firing
  pendingApply twice per zoom event. Consolidated into a single named
  FlagPendingApply helper that's registered once on each of
  Minimap_ZoomIn / Minimap_ZoomOut / Minimap_Update.
• oUF_Reputation: gated everything on the legacy GetWatchedFactionInfo
  global only, so a client that exposes reputation only via
  C_Reputation.GetWatchedFactionData (path that already exists in
  core/compat.lua) would always Hide() the bar at max level.
  Routed all five tags + the Update path through ns.Compat.
  GetWatchedFactionInfo with proper nil guards. Also guarded
  FACTION_BAR_COLORS[standing] indexing — out-of-range standings
  returned nil and `color.r` would error.
• core/bars.lua createRepBar tooltip: same C_Reputation gap as above
  + same unguarded FACTION_BAR_COLORS[standing] indexing + division
  by (maxrep - minrep) without checking for zero. Routed through the
  compat shim, guarded the color lookup with an r,g,b = 1,1,1
  fallback, guarded the percentage math against a 0 span.
• rInfostrings rsiExpRep: same C_Reputation gap on the max-level
  branch (audit pass). Routed through the compat shim with full nil
  guards on `value / minimum / maximum`. Subsequently superseded for
  UI purposes: max-level branch hidden entirely — see INFOSTRINGS —
  XP/REP LINE HIDDEN AT MAX LEVEL above (rep display removed at cap).

PERFORMANCE:
• FixBlizzBarNil.lua: was attaching ArtFrameOnUpdate to the event
  frame at file load (`f:SetScript("OnUpdate", ArtFrameOnUpdate)`).
  Even though the handler early-exits when needsArtFrameCheck is
  false, the closure ran every single frame for the whole session.
  Removed the unconditional attach — the handler is now only
  installed by the PLAYER_ENTERING_WORLD branch and self-detaches
  after the retry intervals exhaust (which it already did).

CODE QUALITY / DEAD CODE:
• core/init_coordinator.lua: the executionFrame OnUpdate wrapped
  every callback in pcall and silently dropped errors — a broken
  module would never load and never explain why. Errors are now
  routed through geterrorhandler() so BugSack / !BugGrabber / the
  default error frame can show them, and the failed callback is
  marked executed so the same error doesn't re-fire on every event
  tick.
• embeds/oUF/ouf.lua: removed leftover `print("handling custom")` /
  `print("handling condition")` debug spam in the `visibility`
  branch of the header builder.
• embeds/Roth_ShinyBuffs/Roth_ShinyBuffs.lua: gated the seven
  unconditional login-time chat prints (API detection, hook setup,
  initial-skin notice, legacy-API detection) behind a new SB_Debug
  flag. /sbdiag remains unconditional as the user-facing diagnostic
  command, and the "No buff API detected" error stays unconditional
  because in that case the addon won't function at all.
• embeds/Roth_ShinyBuffs/Roth_ShinyBuffs.lua: removed
  `hooksecurefunc("BuffFrame_UpdateAllBuffAnchors", function() end)`
  — was a no-op hook with zero behavior; legacy skinning is driven
  entirely by SkinAuras_Legacy via UNIT_AURA.
• embeds/RothFont/Config.lua: removed leftover `print("setting")`
  debug from the headerFont set callback.