Compare commits

...

10 commits

Author SHA1 Message Date
066a39aa8c chore: pin Ace3.toc to 3.3.5 client and upstream revision
Interface: retail multi-client list -> 30300; Version: unsubstituted
@project-version@ packager placeholder -> master-52e5f2c (the
WoWUIDev/Ace3 commit this bundle was lifted from, per README)
2026-06-10 02:17:23 +02:00
baf855666d ci(release): sync release.yml from coa-template
hide_archive_links is only honored by Gitea on release edit, not create —
add the PATCH step after create/lookup so auto-generated source archive
links actually stay hidden (coa-template 90874c5).
2026-06-10 02:11:46 +02:00
cdc3429882 fix(AceGUI-3.0): pcall the OnGamePadButtonDown SetScript (retail-only script type, crashes on 3.3.5)
All checks were successful
release / release (push) Successful in 2s
2026-05-29 20:23:32 +02:00
28b13a85fa ci: respect GITHUB_REPOSITORY + tolerate per-asset upload failures
All checks were successful
release / release (push) Successful in 7s
2026-05-25 12:16:03 +02:00
edcf9bb0aa ci: add Gitea Actions release workflow (per-addon git-archive zip) 2026-05-25 12:00:24 +02:00
827a5bdc60 chore: move bundle into Ace3/ subfolder + standard .gitignore
Matches the Exiles fork-layout convention (one folder per addon). README
and tools/sweep.py updated for the new path; tools/sweep.py stays at repo
root since it's dev tooling, not part of the shipped bundle.
2026-05-25 10:59:24 +02:00
a96308ff2c tools: add canonical sweep.py with explicit coa-elvui exclusion
Two failures that compounded in the last round of ElvUI breakage:

  1. The sweep script only lived in /tmp during a session — re-derived
     from scratch each time, so the EXCLUDE_FORKS knowledge wasn't
     anywhere reviewable.
  2. The old filename-only filter ('if -ElvUI in name skip') missed
     ElvUI's customizations inside otherwise-stock-named files AND
     rsync --delete killed -ElvUI suffixed widgets that exist only in
     ElvUI's bundle (e.g. AceGUIWidget-Button-ElvUI.lua).

This tool fixes both:

  - Lives in the repo (tools/sweep.py), so the exclusion list is
    visible in version control and reviewable.
  - EXCLUDED_FORKS = {'coa-elvui'} hardcoded with an in-source comment
    explaining why.
  - --exclude='*-ElvUI*' passed to every rsync as belt-and-braces, so
    even if a future fork accidentally carries an ElvUI-namespaced
    file we never wanted to overwrite, the sweep won't touch it.
  - Refuses to add new lib dirs — only updates ones already present
    in the fork.
  - --dry-run flag for safe verification.

README updated with a 'Forks excluded from sweep' section documenting
the same.
2026-05-25 10:06:41 +02:00
9583952806 fix(AceDB): backport PR #10 — falsy defaults read back as themselves
Upstream AceDB-3.0's simple-value defaults metatable uses
`function(t,k2) return k2~=nil and v or nil end` — when `v` is itself
falsy (`false`, `0`, `""`), the `and` short-circuits to that falsy
value and the trailing `or nil` then collapses it to nil, so any
`['*'] = false` (or similar) default silently reads back as nil.

Backport of https://github.com/WoWUIDev/Ace3/pull/10 (open upstream since
2023-11-04, not merged). Documented as CoA-compat patch #4 in README;
drop it when upstream finally merges.
2026-05-24 19:30:28 +02:00
3ec2009f54 fix(compat): guard retail-only InterfaceOptions globals and Settings.* API
AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua: the Constructor
parented its frame to the global InterfaceOptionsFramePanelContainer, which
is nil at AceGUI widget-construction time on the CoA reworked FrameXML.
Guard with 'local _parent = InterfaceOptionsFramePanelContainer or UIParent'
and pass _parent to CreateFrame so addons that register a Blizzard Interface
Options panel don't error out during load.

AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua: :AddToBlizOptions
used the Dragonflight+ Settings.* API (GetCategory, RegisterCanvasLayoutCategory,
RegisterCanvasLayoutSubcategory, RegisterAddOnCategory). The Settings table
doesn't exist on the 3.3.5-based CoA client, so every AceConfig-driven options
panel errored the moment it was registered. Wrap the whole Settings.* block in
'if Settings and Settings.GetCategory then ... else ... end' and fall back to
the WotLK-era InterfaceOptions_AddCategory(group.frame) after stamping the
category name via group:SetName(name or appName, parent).

Both retail and CoA paths now work; behaviour on retail is unchanged. luac -p
passes on both files. README CoA-compat patches table updated (entries 2 and 3).

This bundle is the source-of-truth for the Exiles/coa-* forks; the propagation
to bartender / quartz / shadowedunitframes / ai-voiceover / etc. bundled Ace
copies is handled separately.
2026-05-24 17:38:30 +02:00
d422ad36b8 fix(compat): convert FileDataID Set*Texture() calls to string paths
WoW 3.3.5 / CoA does not support numeric FileDataIDs in Set(*)Texture —
only string paths. Upstream Ace3 uses FDIDs in 42 places across
AceGUI-3.0/widgets and AceConfigDialog-3.0, which silently fail and
render as red placeholders (visible as solid-red squares where color
swatches, checkboxes, and window chrome should be).

Substituted each FDID with the string path documented in the trailing
comment. Files touched:

  AceGUI-3.0/widgets/AceGUIContainer-Frame.lua             (5)
  AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua         (4)
  AceGUI-3.0/widgets/AceGUIContainer-Window.lua           (12)
  AceGUI-3.0/widgets/AceGUIWidget-CheckBox.lua             (9)
  AceGUI-3.0/widgets/AceGUIWidget-ColorPicker.lua          (3)
  AceGUI-3.0/widgets/AceGUIWidget-DropDown-Items.lua       (3)
  AceGUI-3.0/widgets/AceGUIWidget-Heading.lua              (2)
  AceGUI-3.0/widgets/AceGUIWidget-Icon.lua                 (1)
  AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua (3)

Documented as patch #1 in README.md.
2026-05-23 14:03:09 +02:00
67 changed files with 446 additions and 76 deletions

View file

@ -0,0 +1,75 @@
name: release
on:
push:
tags:
- '*-coa.*' # Asc-1.1.6-coa.2, 9.1.40-coa.3, etc.
- 'v*' # v0.3.0 for repos without an upstream version
jobs:
release:
runs-on: linux-amd64
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # build_zip uses git archive HEAD; full history is fine
- name: Build per-addon zip(s)
run: bash tools/build_zip.sh
- name: Publish release (Gitea API direct; no action dependency)
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
API: ${{ github.server_url }}/api/v1
# Gitea attachment ceiling is 200 MiB (see roles/gitea config).
# Skip anything larger so one oversized asset doesn't fail the job.
MAX_BYTES: 209715200
run: |
set -euo pipefail
# Create the release (or reuse if it already exists for this tag).
RID=$(curl -s -H "Authorization: token $GITEA_TOKEN" \
"$API/repos/$REPO/releases/tags/$TAG" 2>/dev/null \
| jq -r '.id // empty')
if [ -z "$RID" ]; then
RID=$(curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"$API/repos/$REPO/releases" \
-d "$(jq -nc --arg t "$TAG" '{tag_name:$t,name:$t,draft:false,prerelease:false,hide_archive_links:true}')" \
| jq -r '.id')
fi
echo "release id: $RID"
# Gitea honors hide_archive_links only on edit, not create — PATCH it
# so the auto-generated Source Code (zip/tar.gz) links stay hidden.
curl -sf -X PATCH -H "Authorization: token $GITEA_TOKEN" -H "Content-Type: application/json" \
"$API/repos/$REPO/releases/$RID" -d '{"hide_archive_links":true}' >/dev/null || true
# Upload every dist/*.zip. Per-asset failures don't fail the job —
# we want partial releases to still publish rather than block the
# whole pipeline on one big file.
failed=0
uploaded=0
for f in dist/*.zip; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
if [ "$size" -gt "$MAX_BYTES" ]; then
echo "::warning::skip $name (${size} B > ${MAX_BYTES} B Gitea limit; host on CDN instead)"
failed=$((failed+1))
continue
fi
echo "uploading $name ($(numfmt --to=iec "$size"))"
if curl -sf -X POST -H "Authorization: token $GITEA_TOKEN" \
-F "attachment=@$f" \
"$API/repos/$REPO/releases/$RID/assets?name=$name" \
| jq -r '" -> " + .browser_download_url'; then
uploaded=$((uploaded+1))
else
echo "::warning::upload failed for $name"
failed=$((failed+1))
fi
done
echo "release published: $uploaded uploaded, $failed skipped/failed"
# Only fail the job if NO assets uploaded — a release with zero
# attachments isn't useful to anyone.
[ "$uploaded" -gt 0 ]

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
.env
.DS_Store
.release
.install
.lua/*
.vscode
.idea
dist/

View file

@ -1,4 +1,4 @@
## Interface: 11508, 11507, 20505, 30405, 38001, 40402, 50503, 50504, 120001, 120005, 120007
## Interface: 30300
## Title: Lib: Ace3
## Notes: AddOn development framework
@ -6,7 +6,7 @@
## X-Website: http://www.wowace.com
## X-Category: Library
## X-License: Limited BSD
## Version: @project-version@
## Version: master-52e5f2c
LibStub\LibStub.lua
CallbackHandler-1.0\CallbackHandler-1.0.xml

View file

@ -585,11 +585,11 @@ do
button:SetSize(128, 21)
button:SetNormalFontObject(GameFontNormal)
button:SetHighlightFontObject(GameFontHighlight)
button:SetNormalTexture(130763) -- "Interface\\Buttons\\UI-DialogBox-Button-Up"
button:SetNormalTexture("Interface\\Buttons\\UI-DialogBox-Button-Up")
button:GetNormalTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
button:SetPushedTexture(130761) -- "Interface\\Buttons\\UI-DialogBox-Button-Down"
button:SetPushedTexture("Interface\\Buttons\\UI-DialogBox-Button-Down")
button:GetPushedTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
button:SetHighlightTexture(130762) -- "Interface\\Buttons\\UI-DialogBox-Button-Highlight"
button:SetHighlightTexture("Interface\\Buttons\\UI-DialogBox-Button-Highlight")
button:GetHighlightTexture():SetTexCoord(0.0, 1.0, 0.0, 0.71875)
button:SetText(newText)
return button
@ -2014,28 +2014,37 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...)
group:SetCallback("OnHide", ClearBlizPanel)
local categoryName = name or appName
if parent then
local parentID = BlizOptionsIDMap[parent] or parent
local category = Settings.GetCategory(parentID)
if not category then
error(("The parent category '%s' was not found"):format(parent), 2)
end
local subcategory = Settings.RegisterCanvasLayoutSubcategory(category, group.frame, categoryName)
group:SetName(subcategory.ID, parentID)
else
if BlizOptionsIDMap[categoryName] then
error(("%s has already been added to the Blizzard Options Window with the given name: %s"):format(appName, categoryName), 2)
end
-- CoA-compat: the Settings.* API (GetCategory / RegisterCanvasLayoutCategory /
-- RegisterCanvasLayoutSubcategory / RegisterAddOnCategory) is a retail-only
-- (Dragonflight+) replacement for the WotLK-era InterfaceOptions_AddCategory.
-- On the 3.3.5-based CoA client Settings is nil, so fall back to the legacy API.
if Settings and Settings.GetCategory then
if parent then
local parentID = BlizOptionsIDMap[parent] or parent
local category = Settings.GetCategory(parentID)
if not category then
error(("The parent category '%s' was not found"):format(parent), 2)
end
local subcategory = Settings.RegisterCanvasLayoutSubcategory(category, group.frame, categoryName)
group:SetName(subcategory.ID, parentID)
else
if BlizOptionsIDMap[categoryName] then
error(("%s has already been added to the Blizzard Options Window with the given name: %s"):format(appName, categoryName), 2)
end
local category = Settings.RegisterCanvasLayoutCategory(group.frame, categoryName)
if not (C_SettingsUtil and C_SettingsUtil.OpenSettingsPanel) then
-- override the ID so the name can be used in Settings.OpenToCategory
-- unfortunately with incoming API changes in 12.0 (and likely classic at some point) this override is no longer possible
category.ID = categoryName
local category = Settings.RegisterCanvasLayoutCategory(group.frame, categoryName)
if not (C_SettingsUtil and C_SettingsUtil.OpenSettingsPanel) then
-- override the ID so the name can be used in Settings.OpenToCategory
-- unfortunately with incoming API changes in 12.0 (and likely classic at some point) this override is no longer possible
category.ID = categoryName
end
group:SetName(category.ID)
BlizOptionsIDMap[categoryName] = category.ID
Settings.RegisterAddOnCategory(category)
end
group:SetName(category.ID)
BlizOptionsIDMap[categoryName] = category.ID
Settings.RegisterAddOnCategory(category)
else
group:SetName(name or appName, parent)
InterfaceOptions_AddCategory(group.frame)
end
return group.frame, group.frame.name

View file

@ -111,7 +111,15 @@ local function copyDefaults(dest, src)
end
else
-- Values are not tables, so this is just a simple return
local mt = {__index = function(t,k2) return k2~=nil and v or nil end}
-- (PR #10 backport: the old `k2~=nil and v or nil` short-circuits to
-- nil whenever the default `v` itself is falsy — so `["*"] = false`
-- defaults silently became nil. Make the read explicit instead.)
local mt = {
__index = function(t,k2)
if k2 == nil then return nil end
return v
end,
}
setmetatable(dest, mt)
end
elseif type(v) == "table" then

View file

@ -99,7 +99,11 @@ local methods = {
Constructor
-------------------------------------------------------------------------------]]
local function Constructor()
local frame = CreateFrame("Frame", nil, InterfaceOptionsFramePanelContainer)
-- CoA-compat: InterfaceOptionsFramePanelContainer is a global from the stock 3.3.5
-- Interface Options frame; on the CoA reworked FrameXML it can be nil at the time
-- AceGUI widgets are constructed. Fall back to UIParent so CreateFrame doesn't blow up.
local _parent = InterfaceOptionsFramePanelContainer or UIParent
local frame = CreateFrame("Frame", nil, _parent)
frame:Hide()
-- support functions for the Blizzard Interface Options

View file

@ -221,7 +221,7 @@ local function Constructor()
statustext:SetText("")
local titlebg = frame:CreateTexture(nil, "OVERLAY")
titlebg:SetTexture(131080) -- Interface\\DialogFrame\\UI-DialogBox-Header
titlebg:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg:SetTexCoord(0.31, 0.67, 0, 0.63)
titlebg:SetPoint("TOP", 0, 12)
titlebg:SetWidth(100)
@ -237,14 +237,14 @@ local function Constructor()
titletext:SetPoint("TOP", titlebg, "TOP", 0, -14)
local titlebg_l = frame:CreateTexture(nil, "OVERLAY")
titlebg_l:SetTexture(131080) -- Interface\\DialogFrame\\UI-DialogBox-Header
titlebg_l:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg_l:SetTexCoord(0.21, 0.31, 0, 0.63)
titlebg_l:SetPoint("RIGHT", titlebg, "LEFT")
titlebg_l:SetWidth(30)
titlebg_l:SetHeight(40)
local titlebg_r = frame:CreateTexture(nil, "OVERLAY")
titlebg_r:SetTexture(131080) -- Interface\\DialogFrame\\UI-DialogBox-Header
titlebg_r:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
titlebg_r:SetTexCoord(0.67, 0.77, 0, 0.63)
titlebg_r:SetPoint("LEFT", titlebg, "RIGHT")
titlebg_r:SetWidth(30)
@ -262,7 +262,7 @@ local function Constructor()
line1:SetWidth(14)
line1:SetHeight(14)
line1:SetPoint("BOTTOMRIGHT", -8, 8)
line1:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
line1:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
local x = 0.1 * 14/17
line1:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)
@ -270,7 +270,7 @@ local function Constructor()
line2:SetWidth(8)
line2:SetHeight(8)
line2:SetPoint("BOTTOMRIGHT", -8, 8)
line2:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
line2:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
x = 0.1 * 8/17
line2:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)

View file

@ -105,11 +105,11 @@ local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
if canExpand then
if not isExpanded then
toggle:SetNormalTexture(130838) -- Interface\\Buttons\\UI-PlusButton-UP
toggle:SetPushedTexture(130836) -- Interface\\Buttons\\UI-PlusButton-DOWN
toggle:SetNormalTexture("Interface\\Buttons\\UI-PlusButton-UP")
toggle:SetPushedTexture("Interface\\Buttons\\UI-PlusButton-DOWN")
else
toggle:SetNormalTexture(130821) -- Interface\\Buttons\\UI-MinusButton-UP
toggle:SetPushedTexture(130820) -- Interface\\Buttons\\UI-MinusButton-DOWN
toggle:SetNormalTexture("Interface\\Buttons\\UI-MinusButton-UP")
toggle:SetPushedTexture("Interface\\Buttons\\UI-MinusButton-DOWN")
end
toggle:Show()
else

View file

@ -190,67 +190,67 @@ do
frame:SetToplevel(true)
local titlebg = frame:CreateTexture(nil, "BACKGROUND")
titlebg:SetTexture(251966) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Title-Background
titlebg:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Title-Background")
titlebg:SetPoint("TOPLEFT", 9, -6)
titlebg:SetPoint("BOTTOMRIGHT", frame, "TOPRIGHT", -28, -24)
local dialogbg = frame:CreateTexture(nil, "BACKGROUND")
dialogbg:SetTexture(137056) -- Interface\\Tooltips\\UI-Tooltip-Background
dialogbg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
dialogbg:SetPoint("TOPLEFT", 8, -24)
dialogbg:SetPoint("BOTTOMRIGHT", -6, 8)
dialogbg:SetVertexColor(0, 0, 0, .75)
local topleft = frame:CreateTexture(nil, "BORDER")
topleft:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
topleft:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
topleft:SetWidth(64)
topleft:SetHeight(64)
topleft:SetPoint("TOPLEFT")
topleft:SetTexCoord(0.501953125, 0.625, 0, 1)
local topright = frame:CreateTexture(nil, "BORDER")
topright:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
topright:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
topright:SetWidth(64)
topright:SetHeight(64)
topright:SetPoint("TOPRIGHT")
topright:SetTexCoord(0.625, 0.75, 0, 1)
local top = frame:CreateTexture(nil, "BORDER")
top:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
top:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
top:SetHeight(64)
top:SetPoint("TOPLEFT", topleft, "TOPRIGHT")
top:SetPoint("TOPRIGHT", topright, "TOPLEFT")
top:SetTexCoord(0.25, 0.369140625, 0, 1)
local bottomleft = frame:CreateTexture(nil, "BORDER")
bottomleft:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
bottomleft:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
bottomleft:SetWidth(64)
bottomleft:SetHeight(64)
bottomleft:SetPoint("BOTTOMLEFT")
bottomleft:SetTexCoord(0.751953125, 0.875, 0, 1)
local bottomright = frame:CreateTexture(nil, "BORDER")
bottomright:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
bottomright:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
bottomright:SetWidth(64)
bottomright:SetHeight(64)
bottomright:SetPoint("BOTTOMRIGHT")
bottomright:SetTexCoord(0.875, 1, 0, 1)
local bottom = frame:CreateTexture(nil, "BORDER")
bottom:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
bottom:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
bottom:SetHeight(64)
bottom:SetPoint("BOTTOMLEFT", bottomleft, "BOTTOMRIGHT")
bottom:SetPoint("BOTTOMRIGHT", bottomright, "BOTTOMLEFT")
bottom:SetTexCoord(0.376953125, 0.498046875, 0, 1)
local left = frame:CreateTexture(nil, "BORDER")
left:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
left:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
left:SetWidth(64)
left:SetPoint("TOPLEFT", topleft, "BOTTOMLEFT")
left:SetPoint("BOTTOMLEFT", bottomleft, "TOPLEFT")
left:SetTexCoord(0.001953125, 0.125, 0, 1)
local right = frame:CreateTexture(nil, "BORDER")
right:SetTexture(251963) -- Interface\\PaperDollInfoFrame\\UI-GearManager-Border
right:SetTexture("Interface\\PaperDollInfoFrame\\UI-GearManager-Border")
right:SetWidth(64)
right:SetPoint("TOPRIGHT", topright, "BOTTOMRIGHT")
right:SetPoint("BOTTOMRIGHT", bottomright, "TOPRIGHT")
@ -290,7 +290,7 @@ do
line1:SetWidth(14)
line1:SetHeight(14)
line1:SetPoint("BOTTOMRIGHT", -8, 8)
line1:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
line1:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
local x = 0.1 * 14/17
line1:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)
@ -299,7 +299,7 @@ do
line2:SetWidth(8)
line2:SetHeight(8)
line2:SetPoint("BOTTOMRIGHT", -8, 8)
line2:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
line2:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
x = 0.1 * 8/17
line2:SetTexCoord(0.05 - x, 0.5, 0.05, 0.5 + x, 0.05, 0.5 - x, 0.5 + x, 0.5)

View file

@ -151,21 +151,21 @@ local methods = {
local size
if type == "radio" then
size = 16
checkbg:SetTexture(130843) -- Interface\\Buttons\\UI-RadioButton
checkbg:SetTexture("Interface\\Buttons\\UI-RadioButton")
checkbg:SetTexCoord(0, 0.25, 0, 1)
check:SetTexture(130843) -- Interface\\Buttons\\UI-RadioButton
check:SetTexture("Interface\\Buttons\\UI-RadioButton")
check:SetTexCoord(0.25, 0.5, 0, 1)
check:SetBlendMode("ADD")
highlight:SetTexture(130843) -- Interface\\Buttons\\UI-RadioButton
highlight:SetTexture("Interface\\Buttons\\UI-RadioButton")
highlight:SetTexCoord(0.5, 0.75, 0, 1)
else
size = 24
checkbg:SetTexture(130755) -- Interface\\Buttons\\UI-CheckBox-Up
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
checkbg:SetTexCoord(0, 1, 0, 1)
check:SetTexture(130751) -- Interface\\Buttons\\UI-CheckBox-Check
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
check:SetTexCoord(0, 1, 0, 1)
check:SetBlendMode("BLEND")
highlight:SetTexture(130753) -- Interface\\Buttons\\UI-CheckBox-Highlight
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
highlight:SetTexCoord(0, 1, 0, 1)
end
checkbg:SetHeight(size)
@ -251,11 +251,11 @@ local function Constructor()
checkbg:SetWidth(24)
checkbg:SetHeight(24)
checkbg:SetPoint("TOPLEFT")
checkbg:SetTexture(130755) -- Interface\\Buttons\\UI-CheckBox-Up
checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
local check = frame:CreateTexture(nil, "OVERLAY")
check:SetAllPoints(checkbg)
check:SetTexture(130751) -- Interface\\Buttons\\UI-CheckBox-Check
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
local text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
text:SetJustifyH("LEFT")
@ -264,7 +264,7 @@ local function Constructor()
text:SetPoint("RIGHT")
local highlight = frame:CreateTexture(nil, "HIGHLIGHT")
highlight:SetTexture(130753) -- Interface\\Buttons\\UI-CheckBox-Highlight
highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
highlight:SetBlendMode("ADD")
highlight:SetAllPoints(checkbg)

View file

@ -180,7 +180,7 @@ local function Constructor()
local colorSwatch = frame:CreateTexture(nil, "OVERLAY")
colorSwatch:SetWidth(19)
colorSwatch:SetHeight(19)
colorSwatch:SetTexture(130939) -- Interface\\ChatFrame\\ChatFrameColorSwatch
colorSwatch:SetTexture("Interface\\ChatFrame\\ChatFrameColorSwatch")
colorSwatch:SetPoint("LEFT")
local texture = frame:CreateTexture(nil, "BACKGROUND")
@ -195,7 +195,7 @@ local function Constructor()
colorSwatch.checkers = checkers
checkers:SetWidth(14)
checkers:SetHeight(14)
checkers:SetTexture(188523) -- Tileset\\Generic\\Checkers
checkers:SetTexture("Tileset\\Generic\\Checkers")
checkers:SetTexCoord(.25, 0, 0.5, .25)
checkers:SetDesaturated(true)
checkers:SetVertexColor(1, 1, 1, 0.75)
@ -210,7 +210,7 @@ local function Constructor()
text:SetPoint("RIGHT")
--local highlight = frame:CreateTexture(nil, "HIGHLIGHT")
--highlight:SetTexture(136810) -- Interface\\QuestFrame\\UI-QuestTitleHighlight
--highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
--highlight:SetBlendMode("ADD")
--highlight:SetAllPoints(frame)

View file

@ -169,7 +169,7 @@ function ItemBase.Create(type)
self.text = text
local highlight = frame:CreateTexture(nil, "OVERLAY")
highlight:SetTexture(136810) -- Interface\\QuestFrame\\UI-QuestTitleHighlight
highlight:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
highlight:SetBlendMode("ADD")
highlight:SetHeight(14)
highlight:ClearAllPoints()
@ -182,7 +182,7 @@ function ItemBase.Create(type)
check:SetWidth(16)
check:SetHeight(16)
check:SetPoint("LEFT",frame,"LEFT",3,-1)
check:SetTexture(130751) -- Interface\\Buttons\\UI-CheckBox-Check
check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
check:Hide()
self.check = check
@ -190,7 +190,7 @@ function ItemBase.Create(type)
sub:SetWidth(16)
sub:SetHeight(16)
sub:SetPoint("RIGHT",frame,"RIGHT",-3,-1)
sub:SetTexture(130940) -- Interface\\ChatFrame\\ChatFrameExpandArrow
sub:SetTexture("Interface\\ChatFrame\\ChatFrameExpandArrow")
sub:Hide()
self.sub = sub

View file

@ -51,14 +51,14 @@ local function Constructor()
left:SetHeight(8)
left:SetPoint("LEFT", 3, 0)
left:SetPoint("RIGHT", label, "LEFT", -5, 0)
left:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
left:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
left:SetTexCoord(0.81, 0.94, 0.5, 1)
local right = frame:CreateTexture(nil, "BACKGROUND")
right:SetHeight(8)
right:SetPoint("RIGHT", -3, 0)
right:SetPoint("LEFT", label, "RIGHT", 5, 0)
right:SetTexture(137057) -- Interface\\Tooltips\\UI-Tooltip-Border
right:SetTexture("Interface\\Tooltips\\UI-Tooltip-Border")
right:SetTexCoord(0.81, 0.94, 0.5, 1)
local widget = {

View file

@ -118,7 +118,7 @@ local function Constructor()
local highlight = frame:CreateTexture(nil, "HIGHLIGHT")
highlight:SetAllPoints(image)
highlight:SetTexture(136580) -- Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight
highlight:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight")
highlight:SetTexCoord(0, 1, 0.23, 0.77)
highlight:SetBlendMode("ADD")

View file

@ -199,7 +199,7 @@ local function Constructor()
button:SetScript("OnKeyDown", Keybinding_OnKeyDown)
button:SetScript("OnMouseDown", Keybinding_OnMouseDown)
button:SetScript("OnMouseWheel", Keybinding_OnMouseWheel)
button:SetScript("OnGamePadButtonDown", Keybinding_OnKeyDown)
pcall(button.SetScript, button, "OnGamePadButtonDown", Keybinding_OnKeyDown)
button:SetPoint("BOTTOMLEFT")
button:SetPoint("BOTTOMRIGHT")
button:SetHeight(24)

View file

@ -2,10 +2,20 @@
Canonical [Ace3](https://www.wowace.com/projects/ace3) bundle for the CoA Guild 'Exiles' addon forks.
Lifted verbatim from upstream [WoWUIDev/Ace3](https://github.com/WoWUIDev/Ace3) at commit
Lifted from upstream [WoWUIDev/Ace3](https://github.com/WoWUIDev/Ace3) at commit
[`52e5f2c`](https://github.com/WoWUIDev/Ace3/commit/52e5f2c7101b6edb02b48ea232bdda2df09d2960)
(2026-05-17). Every fork in the `Exiles` org should converge on this bundle so the runtime
LibStub resolution is predictable and addons can't quietly regress when one of them is disabled.
(2026-05-17), with a small stack of CoA-compat patches on top (see below). Every fork in the `Exiles`
org should converge on this bundle so the runtime LibStub resolution is predictable and addons
can't quietly regress when one of them is disabled.
## CoA-compat patches on top of upstream
| # | Issue | Fix |
|---|-------|-----|
| 1 | Upstream Ace3 calls `Texture:Set*Texture(<FileDataID>)` with numeric FileDataIDs in 42 places across `AceGUI-3.0/widgets/*` and `AceConfigDialog-3.0`. FileDataIDs are a retail-only API (post WoD/Legion). On the WoW 3.3.5-based CoA client, `SetTexture` only accepts string paths — passing a number silently fails and the engine renders a red placeholder. Symptom: solid-red squares where color swatches / checkboxes / window chrome should be. | Each FDID call was substituted with the string path that already lived in the trailing comment, e.g. `colorSwatch:SetTexture(130939)``colorSwatch:SetTexture("Interface\\ChatFrame\\ChatFrameColorSwatch")`. |
| 2 | `AceGUI-3.0/widgets/AceGUIContainer-BlizOptionsGroup.lua` Constructor parents its frame to the global `InterfaceOptionsFramePanelContainer`. On the CoA reworked FrameXML that global is nil at AceGUI widget-construction time, and `CreateFrame("Frame", nil, nil)` is fine, but downstream code that calls `:SetPoint` against the parent / `:Show` it via the options tree relies on a real parent. Symptom: every addon that registers a Blizzard Interface Options panel via AceConfigDialog errors during load. | Guard at line 102: `local _parent = InterfaceOptionsFramePanelContainer or UIParent` and pass `_parent` to `CreateFrame`. Widget behaviour is unchanged on retail; on CoA the panel parents to `UIParent` so the rest of the widget works. |
| 3 | `AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua` `:AddToBlizOptions` uses the Dragonflight+ `Settings.*` API (`Settings.GetCategory`, `Settings.RegisterCanvasLayoutCategory`, `Settings.RegisterCanvasLayoutSubcategory`, `Settings.RegisterAddOnCategory`). The `Settings` table doesn't exist on the 3.3.5-based CoA client, so every AceConfig-driven options panel errors out the moment it's registered. | Wrap the whole `Settings.*` block in `if Settings and Settings.GetCategory then … else … end`. The `else` branch falls back to the WotLK-era `InterfaceOptions_AddCategory(group.frame)` after stamping the category name via `group:SetName(name or appName, parent)`. |
| 4 | `AceDB-3.0/AceDB-3.0.lua:114` — the simple-value `__index` metatable for defaults is `function(t,k2) return k2~=nil and v or nil end`. Whenever the default value `v` is itself falsy (`false`, `0`, `""`), the `and` short-circuits and the `or nil` resolves to `nil`, so `["*"] = false` and similar falsy defaults are silently lost when read. | Replaces the one-liner with an explicit `if k2 == nil then return nil end; return v` so falsy defaults round-trip correctly. Backport of [WoWUIDev/Ace3 PR #10](https://github.com/WoWUIDev/Ace3/pull/10) (open since 2023-11-04, not merged upstream). Drop this patch if/when upstream merges. |
## Versions
@ -47,14 +57,35 @@ Or replace the entire `Libs/Ace3/` tree in one go.
### Option 2: standalone addon
Drop the contents (except `README.md` / `.gitattributes`) into
`Interface/AddOns/Ace3/` and the loadable `Ace3.toc` will register every library at top
Drop the **`Ace3/`** directory from this repo straight into
`Interface/AddOns/` and the loadable `Ace3.toc` will register every library at top
priority via LibStub. Useful for non-bundling forks (`chatter`, `sexymap`, `clique`, …) to
get Ace without each one carrying its own copy.
(The canonical bundle lives under `Ace3/` at the repo root so this repo follows the same
"each addon in its own folder" layout as every other `Exiles/coa-*` fork.)
## Sync policy
Bumping upstream means a single PR here, then a sweep across every fork that embeds these
libs. Note the new upstream commit in the README's commit-pin line above. Never edit a
library file in place; if a CoA-specific patch is genuinely needed, ship it as a separate
loadable addon that registers a higher MINOR via LibStub, leaving this bundle pristine.
Bumping upstream means a single commit here, then a sweep across every fork that embeds these
libs. Note the new upstream commit in the README's commit-pin line above, and re-apply
the CoA-compat patches listed above against the new revision (the FDID one is mechanical —
see `/tmp/fix_fdid.py` history). Keep patches **minimal and documented in this README**;
prefer fixing them upstream where reasonable.
Run the sweep via `tools/sweep.py` from this repo — it walks every sibling `coa-*` fork
under `/home/sub/repos/coa`, finds each fork's bundled `LibStub` / `CallbackHandler-1.0` /
`Ace*-3.0` dirs, and rsyncs them from this bundle. Use `--dry-run` first.
### Forks excluded from sweep
`coa-elvui` is excluded from the sweep — see `EXCLUDED_FORKS` in `tools/sweep.py`. ElvUI
ships its own bundled Ace3 stack with ElvUI-specific patches inside otherwise-stock-named
files (`AceLocale-3.0.lua`, `AceConfigDialog-3.0.lua`, every `AceGUI-3.0/widgets/*.lua`)
**plus** `-ElvUI`-suffixed widgets that don't exist in canonical at all (e.g.
`AceGUIWidget-Button-ElvUI.lua`). `rsync --delete` obliterates the latter; an in-place
sync overwrites the former. Either failure breaks `/ec` and floods locale errors. ElvUI
maintains its own bundle on its own cadence and must never be touched by this tool.
The sweep also passes `--exclude='*-ElvUI*'` to every rsync as a belt-and-braces guard
against future forks that happen to carry an ElvUI-namespaced file we didn't anticipate.

71
tools/build_zip.sh Executable file
View file

@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Build per-addon zip artefacts from HEAD via git-archive.
#
# - Discovers top-level addon folders (Foo/Foo.toc).
# - Re-creates dist/ each run.
# - Always archives HEAD, so the working tree state is irrelevant.
# - If more than one addon folder is present, also emits <repo>-all.zip
# with every addon folder side-by-side at the zip root.
# - When run inside Gitea Actions the working tree lives under a
# per-job dir like /var/lib/act_runner/work/.../hostexecutor, so the
# repo name comes from $GITHUB_REPOSITORY (set by the runner) and
# only falls back to the toplevel basename for local invocations.
set -euo pipefail
root=$(git rev-parse --show-toplevel)
cd "$root"
# Gitea Actions sets GITHUB_REPOSITORY=owner/repo. The basename of
# `git rev-parse --show-toplevel` inside the runner is the worker dir
# (e.g. `hostexecutor`), which would name the bundle wrong.
if [ -n "${GITHUB_REPOSITORY:-}" ]; then
repo_name="${GITHUB_REPOSITORY##*/}"
else
repo_name=$(basename "$root")
fi
dist="$root/dist"
# Find Foo/Foo.toc pairs at depth 1; ignore libs nested deeper.
addons=()
while IFS= read -r toc; do
dir=$(dirname "$toc")
folder=$(basename "$dir")
base=$(basename "$toc" .toc)
# Accept Foo.toc and Foo_Wrath.toc style flavour variants; folder must match
# at least one toc basename prefix (Foo).
case "$base" in
"$folder"|"$folder"_*) addons+=("$folder") ;;
esac
done < <(command find . -mindepth 2 -maxdepth 2 -type f -name '*.toc' | sed 's|^\./||' | sort)
# Dedupe (a folder with Foo.toc + Foo_Wrath.toc shows up twice).
if [ ${#addons[@]} -gt 0 ]; then
mapfile -t addons < <(printf '%s\n' "${addons[@]}" | awk '!seen[$0]++')
fi
if [ ${#addons[@]} -eq 0 ]; then
echo "no addon folders found (looking for */Foo.toc with matching folder name)" >&2
exit 1
fi
rm -rf "$dist"
mkdir -p "$dist"
for folder in "${addons[@]}"; do
out="$dist/$folder.zip"
# No --prefix: the folder already sits at the repo root, so git-archive
# emits entries as <folder>/... which is exactly what
# Interface/AddOns/ expects after extraction.
git archive HEAD --format=zip -o "$out" -- "$folder"
echo "built dist/$folder.zip"
done
# Combined bundle only makes sense when there are multiple addons.
if [ ${#addons[@]} -gt 1 ]; then
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
git archive HEAD --format=tar -- "${addons[@]}" | tar -x -C "$tmp"
out="$dist/$repo_name-all.zip"
( cd "$tmp" && zip -qr "$out" "${addons[@]}" )
echo "built dist/$repo_name-all.zip"
fi

163
tools/sweep.py Executable file
View file

@ -0,0 +1,163 @@
#!/usr/bin/env python3
"""Sweep this coa-ace3 bundle into every Exiles fork that embeds Ace3 libs.
Run from anywhere; the script locates itself and walks sibling repos at
$REPOS_ROOT (default /home/sub/repos/coa). For each fork that bundles
any AceXxx-3.0 library, the matching directory is rsynced from this
bundle with --delete so the fork stays byte-identical to canonical.
USAGE
tools/sweep.py # apply sweep
tools/sweep.py --dry-run # show what would change without writing
WHY THIS SCRIPT EXISTS
Previous sweeps were ad-hoc scripts living in /tmp during a session
and re-derived by hand each time. That made it easy to forget the
ElvUI exclusion (see EXCLUDED_FORKS below) and re-clobber ElvUI's
customized lib stack on every sync. Committing the canonical script
here keeps the exclusion list visible and reviewable.
EXCLUDED_FORKS
coa-elvui ships its OWN bundled Ace3 with ElvUI-specific patches
inside otherwise-stock-named files (AceLocale, AceConfigDialog, the
AceGUI widgets) PLUS files that don't exist in canonical at all
(AceGUIWidget-Button-ElvUI.lua). rsync --delete obliterates the
latter; an in-place sync overwrites the former. Either failure
breaks /ec and floods locale errors. ElvUI maintains its own bundle
on its own cadence and must never be swept by this tool.
"""
import argparse
import os
import re
import subprocess
import sys
from pathlib import Path
# --- configuration -----------------------------------------------------------
REPOS_ROOT = Path(os.environ.get("REPOS_ROOT", "/home/sub/repos/coa"))
BUNDLE = Path(__file__).resolve().parent.parent / "Ace3" # canonical Ace3 lives under <repo>/Ace3/
# Forks that this sweep MUST NOT touch. Document the reason inline.
EXCLUDED_FORKS = {
"coa-elvui", # ships its own ElvUI-patched Ace3 stack; sweep would clobber it
}
# Lib names whose directories we sync from the bundle.
LIB_NAMES = re.compile(r"^(LibStub|CallbackHandler-1\.0|Ace\w*-3\.0)$")
# rsync excludes — repo metadata never deployed, plus a belt-and-braces
# guard against deleting any -ElvUI file that might exist in destinations we
# didn't intend to touch (e.g. a future fork that pulls in some ElvUI widget).
RSYNC_EXCLUDES = [
".git", ".gitattributes", ".gitignore", ".github", ".idea",
".editorconfig", ".luacheckrc", ".pkgmeta",
"*-ElvUI*", # never delete or overwrite ElvUI-namespaced files
]
# --- discovery ---------------------------------------------------------------
def bundle_lib_sources():
"""Return {libname: absolute path inside bundle} for every shippable lib.
Handles the nested AceConfig children (AceConfigCmd/Dialog/Registry).
"""
out = {}
for path in BUNDLE.rglob("*"):
if not path.is_dir():
continue
if path.name in {".git", "tools"}:
continue
if LIB_NAMES.match(path.name):
out.setdefault(path.name, path)
return out
def fork_lib_targets(fork: Path, src_names):
"""Return {libname: absolute path inside fork} for each lib the fork bundles.
A lib is "bundled" when there's a top-level directory containing
<libname>/<libname>.lua. We don't add new libs to forks that didn't
already ship them.
"""
targets = {}
for path in fork.rglob("*.lua"):
if "/.git/" in str(path):
continue
if "-ElvUI" in path.name:
continue
name = path.stem
if name not in src_names:
continue
parent = path.parent
if parent.name != name:
continue
targets.setdefault(name, parent)
return targets
# --- sweep -------------------------------------------------------------------
def sweep(dry_run: bool) -> int:
src_map = bundle_lib_sources()
if not src_map:
print(f"no libs found inside {BUNDLE}; nothing to sweep", file=sys.stderr)
return 2
forks = sorted(p for p in REPOS_ROOT.iterdir()
if p.is_dir() and p.name.startswith("coa-")
and p.name != "coa-ace3"
and (p / ".git").exists())
print(f"bundle: {BUNDLE}")
print(f"forks scan: {REPOS_ROOT}")
print(f"excluded: {sorted(EXCLUDED_FORKS)}")
print(f"libs: {sorted(src_map)}")
print()
total_synced = total_skipped = 0
for fork in forks:
if fork.name in EXCLUDED_FORKS:
print(f" skip {fork.name} (excluded — ships its own customized Ace3)")
total_skipped += 1
continue
targets = fork_lib_targets(fork, src_map.keys())
if not targets:
print(f" no-libs {fork.name}")
continue
for libname in sorted(targets):
src = src_map[libname]
dst = targets[libname]
cmd = ["rsync", "-a", "--delete"]
if dry_run:
cmd += ["--dry-run", "--itemize-changes"]
for exc in RSYNC_EXCLUDES:
cmd += ["--exclude", exc]
cmd += [f"{src}/", f"{dst}/"]
res = subprocess.run(cmd, capture_output=True, text=True)
if res.returncode != 0:
print(f" ! rsync failed for {fork.name}/{libname}: {res.stderr.strip()}")
continue
tag = "would-sync" if dry_run else "synced"
print(f" {tag:<9} {fork.name}/{dst.relative_to(fork)}")
total_synced += 1
print()
print(f"summary: {total_synced} lib dirs {'would be synced' if dry_run else 'synced'}, "
f"{total_skipped} forks excluded")
return 0
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--dry-run", action="store_true",
help="show what would change without writing")
args = ap.parse_args()
sys.exit(sweep(args.dry_run))
if __name__ == "__main__":
main()