refactor: move the embedded addons into src/embed/AddOns

Users saw AddOns/!!!ClassicAPI at the repository root and downloaded it,
thinking the mod is only an addon. The addons are source code for the
DLL, so they now live next to the resource loader in src/embed/. The
README says that the DLL contains them and that the folder is not for
copying into the game.

Remove the AoELootTest dev addon.
This commit is contained in:
Brues
2026-10-27 16:44:30 -05:00
parent 091d2204a6
commit 1f3ce9229d
82 changed files with 19 additions and 254 deletions
+1 -1
View File
@@ -47,4 +47,4 @@
CLAUDE.md
/AddOns/*/.classicapi-dev
/src/embed/AddOns/*/.classicapi-dev
-217
View File
@@ -1,217 +0,0 @@
-- AoE Loot Test
--
-- /aoeloot opens a picker window listing every item in every lootable
-- corpse within click range. Clicking a row hands the (guid, itemID)
-- pair to C_Loot.LootUnitItem and re-scans so the picker refreshes
-- with whatever's left.
--
-- Known visual: each scan opens / closes loot windows in rapid
-- succession, so the Blizzard LootFrame flickers during the scan. This
-- addon intentionally doesn't suppress that — the goal is to exercise
-- the C++ surface, not to ship polished UI.
local MAX_ROWS = 24
local RESCAN_DELAY = 0.3 -- seconds; lets LootUnitItem complete before re-scan
-- ---------- UI scaffolding ----------
-- Frame geometry. Height is recomputed dynamically per scan so the
-- window only takes up as much screen real estate as the result count
-- needs — see `ResizeForRows`.
local FRAME_WIDTH = 320
local HEADER_HEIGHT = 64 -- title + status + insets; minimum frame height
local ROW_HEIGHT = 22
local frame = CreateFrame("Frame", "AoELootTestFrame", UIParent)
frame:SetWidth(FRAME_WIDTH)
frame:SetHeight(HEADER_HEIGHT)
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
frame:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 },
})
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
-- Vanilla 1.12 fires script handlers with the frame set as the global
-- `this`, not as a `self` arg to the function. Passing `frame.StartMoving`
-- directly (the modern shortcut) breaks because the C method has no
-- `self` to operate on. Wrap in closures so `this:method()` resolves
-- correctly inside the handler.
frame:SetScript("OnDragStart", function() this:StartMoving() end)
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
frame:Hide()
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
title:SetPoint("TOP", frame, "TOP", 0, -16)
title:SetText("AoE Loot")
local status = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
status:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -36)
status:SetText("")
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
-- ---------- Row pool ----------
local rows = {}
local function CreateRow(parent, idx)
local row = CreateFrame("Button", nil, parent)
row:SetWidth(290)
row:SetHeight(20)
row:SetPoint("TOPLEFT", parent, "TOPLEFT", 15,
-54 - (idx - 1) * ROW_HEIGHT)
local icon = row:CreateTexture(nil, "ARTWORK")
icon:SetWidth(18); icon:SetHeight(18)
icon:SetPoint("LEFT", row, "LEFT", 0, 0)
row.icon = icon
local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
text:SetPoint("LEFT", icon, "RIGHT", 6, 0)
text:SetPoint("RIGHT", row, "RIGHT", 0, 0)
text:SetJustifyH("LEFT")
row.text = text
local hl = row:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
hl:SetBlendMode("ADD")
hl:SetAllPoints(row)
-- Tooltip on hover. The engine's native `SetHyperlink` does a
-- literal `SStrCmpI` of the input's first 5 chars against the
-- string `"item:"` at `.rdata` VA `0x00842D60` — anything that
-- starts with `|c...` (full link with color) or `|Hitem` (full
-- link without color) fails the prefix check with "Unknown link
-- type". Only the bare payload form survives.
--
-- We extract the payload from the engine's full link rather than
-- composing `item:ID:0:0:0` ourselves, so per-instance fields
-- (enchant / random-suffix / unique) survive round-trip and
-- "Stringy Wolf Meat of the Bear" tooltips correctly instead of
-- collapsing to the un-suffixed itemID.
row:SetScript("OnEnter", function()
if this.payload then
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
GameTooltip:SetHyperlink(this.payload)
GameTooltip:Show()
end
end)
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
row:Hide()
return row
end
for i = 1, MAX_ROWS do
rows[i] = CreateRow(frame, i)
end
-- ---------- Render ----------
-- Tail of a GUID string — last 4 hex chars of the low dword. Lets the
-- user tell apart two rows of "Stringy Wolf Meat x1" coming from
-- different corpses, without occupying a whole column with the full
-- 16-digit GUID.
local function ShortGuidSuffix(guid)
return "[.." .. string.sub(guid, -4) .. "]"
end
-- Extract the `item:ID:enchant:suffix:unique` payload out of the
-- engine's full link string (`|cff...|Hitem:...|h[Name]|h|r`).
-- Vanilla `SetHyperlink` requires the input to start literally with
-- `"item:"` — so we can't pass the full link, but we also can't drop
-- the per-instance fields (random suffix is what distinguishes "of
-- the Bear" from "of the Eagle" on otherwise identical itemIDs).
-- Returns the payload, or nil if extraction fails.
local function PayloadFromLink(link)
if not link then return nil end
return string.match(link, "|H(item:[^|]+)|h")
end
local function ClearRows()
for i = 1, MAX_ROWS do
rows[i]:Hide()
rows[i]:SetScript("OnClick", nil)
rows[i].payload = nil
end
end
local function PopulateRows(results)
local idx = 0
for _, corpse in ipairs(results) do
for _, item in ipairs(corpse.items) do
idx = idx + 1
if idx > MAX_ROWS then break end
local row = rows[idx]
local name, _, _, _, _, _, _, _, icon = GetItemInfo(item.itemID)
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
local label = name or ("Item #" .. item.itemID)
if item.count and item.count > 1 then
label = label .. " x" .. item.count
end
label = label .. " " .. ShortGuidSuffix(corpse.guid)
row.text:SetText(label)
row.payload = PayloadFromLink(item.link) -- for OnEnter tooltip
local guid, itemID = corpse.guid, item.itemID
row:SetScript("OnClick", function()
C_Loot.LootUnitItem(guid, itemID)
C_Timer.After(RESCAN_DELAY, function()
if frame:IsShown() then C_Loot.ScanNearbyLoot() end
end)
end)
row:Show()
end
if idx > MAX_ROWS then break end
end
for i = idx + 1, MAX_ROWS do rows[i]:Hide() end
local shown
if idx == 0 then
status:SetText("Nothing lootable in range.")
shown = 0
elseif idx > MAX_ROWS then
status:SetText(MAX_ROWS .. "+ items (showing first " .. MAX_ROWS .. ")")
shown = MAX_ROWS
else
status:SetText(idx .. " item(s) available")
shown = idx
end
frame:SetHeight(HEADER_HEIGHT + shown * ROW_HEIGHT)
end
-- ---------- Scan trigger ----------
local function StartScan()
status:SetText("Scanning...")
ClearRows()
frame:SetHeight(HEADER_HEIGHT) -- collapse while we wait for results
if not C_Loot.ScanNearbyLoot() then
status:SetText("Scan busy or not in world.")
end
end
-- ---------- Event wiring ----------
local evt = CreateFrame("Frame")
evt:RegisterEvent("LOOT_SCAN_COMPLETED")
evt:SetScript("OnEvent", function()
-- Race guard: a delayed scan may complete after the user closed
-- the picker. Just drop the result rather than re-showing.
if not frame:IsShown() then return end
PopulateRows(C_Loot.GetLastScanResults())
end)
-- ---------- Slash command ----------
RegisterNewSlashCommand(function()
if frame:IsShown() then
StartScan() -- already open → just refresh
else
frame:Show()
StartScan()
end
end, "aoeloot", "aoe")
-7
View File
@@ -1,7 +0,0 @@
## Interface: 11200
## Title: AoE Loot Test
## Notes: Test bed for ClassicAPI's C_Loot.ScanNearbyLoot / LootUnitItem. /aoeloot
## Dependencies: !!!ClassicAPI
## Version: DEV
AoELootTest.lua
+2 -2
View File
@@ -76,7 +76,7 @@ endif()
add_library(${PROJECT_NAME} SHARED
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
# Embed the addons under `AddOns/` and `src/bindings/Bindings.xml` as RCDATA
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
# addon files when the engine asks for paths under
# `Interface\AddOns\<addon>\`, so the addons load even when the user
@@ -86,7 +86,7 @@ add_library(${PROJECT_NAME} SHARED
include(cmake/embed_resources.cmake)
embed_resources(
TARGET ${PROJECT_NAME}
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/AddOns"
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools"
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
+12 -23
View File
@@ -409,13 +409,15 @@ Download the prebuilt `ClassicAPI.dll` from the
3. Add `ClassicAPI.dll` to `dlls.txt`.
4. Launch the game with `VanillaFixes.exe`.
The bundled `!!!ClassicAPI` addon ships *inside* the DLL — no separate
addon download or install step needed.
The `!!!ClassicAPI` and `ClassicAPI_DebugTools` addons are *inside* the
DLL. You do not download or install them. The `src/embed/AddOns/` folder
in this repository is source code for the DLL. Do not copy it into your
game.
## Bundled addon: !!!ClassicAPI
The Lua-side companion library lives in
[`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/). It's a 1.12.1 /
[`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/). It's a 1.12.1 /
Lua 5.0 backport of the modern Blizzard helpers that aren't engine
functions but that consumer code still expects to find as globals —
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
@@ -426,7 +428,7 @@ commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
and `@unit` for them.
**You don't have to install the addon manually** — the DLL embeds
the contents of [`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/) and
the contents of [`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/) and
registers them with the engine as a synthetic addon on startup, so
the library is always available when the DLL is loaded. It fires
`ADDON_LOADED`, supports `SavedVariables`, and is resolvable by name —
@@ -443,15 +445,12 @@ can't be switched off. Being hidden means it's omitted from the
index-based `GetNumAddOns()` / `GetAddOnInfo(i)` enumeration, but every
by-name lookup and dependency reference still resolves normally.
If you drop the folder into your `Interface/AddOns/` directory, **that
copy's files win** — the DLL's embedded version only serves files when
the engine's normal scan doesn't already have a newer entry under that
name. Useful for editing the Lua locally without rebuilding the DLL.
The hidden + force-enabled treatment applies either way (it's keyed to
the addon name, not to which copy serves the files), so the local copy
is also absent from the AddOns list. The dispatch is transparent:
addons consuming `Mixin`, `ColorMixin`, `TableUtil`, etc. behave
identically in both cases.
For development, you can put a copy of the addon folder in
`Interface/AddOns/` and add an empty `.classicapi-dev` file to it. The
game then reads the Lua files from that copy, so you can edit them
without a DLL rebuild. Without this file, the game uses the copy with the
newer `## Version:`. The copy is hidden and always enabled, the same as
the embedded addon.
## Debug tools
@@ -470,16 +469,6 @@ that you use one of these commands:
| `/tinspect <expr>` (or `/tins`) | Opens the table inspector on the result of `<expr>`. |
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
### Print functions
`!!!ClassicAPI` adds these globals:
| Global | Purpose |
|--------|---------|
| `print(...)` | Backport of Blizzard's `print`. It joins the arguments with `" "` and sends the result to `DEFAULT_CHAT_FRAME`. It uses the handler from `setprinthandler`. If that handler fails, it uses `geterrorhandler`. |
| `setprinthandler(func)` / `getprinthandler()` | Sets or gets the print handler. Use it to send print output to a different place in tests. |
| `tostringall(...)` | Applies `tostring()` to each argument, and keeps the number of arguments. |
## Building
Requires CMake (3.10+) and an MSVC toolchain that can target 32-bit Windows.
+1 -1
View File
@@ -3809,7 +3809,7 @@ of descriptors feeds the in-game browser, a page generator, and a drift check.
registration on the first pass of each Lua state, derives systems, and
exports `C_APIDocumentation.GetSystems()` / `GetSystem(name)` lazily in
Blizzard's exact table shape, plus `_classicapi_UndocumentedAPI()`.
- `AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
- `src/embed/AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
`Blizzard_APIDocumentation`, driven by `/classicapi` (short `/capi`).
Named for what it documents: the browser holds only ClassicAPI's own
surface, so `/api` would have over-promised.
+2 -2
View File
@@ -13719,7 +13719,7 @@ end)
the next `step` after that many seconds. `yield(0)` is the
"resume on the next frame" form (since `C_Timer.After(0, ...)`
fires on the next OnUpdate tick). `RunNextFrame(cb)` in
[`AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
[`src/embed/AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../src/embed/AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
wraps the same primitive for the non-coroutine case.
## Macros
@@ -22066,7 +22066,7 @@ Scent of Blood to its own length — live in `src/turtle/DurationMods.cpp`, and
Carnage's roll-gated Rip/Rake refresh in `src/turtle/Carnage.cpp`.
The one rule registered from Lua
([Util/AuraDurationModifiers.lua](../AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua))
([Util/AuraDurationModifiers.lua](../src/embed/AddOns/!!!ClassicAPI/Util/AuraDurationModifiers.lua))
is Shadow Weaving: for a 5/5 priest (talent `15334`, where the proc is 100%),
any shadow-school priest cast refreshes the target's Shadow Vulnerability
(`15258`). Below 5/5 it's roll-gated and deliberately not inferred — the client
+1 -1
View File
@@ -18,7 +18,7 @@
// basenames; returns nothing (mutation is the contract).
//
// Modern WoW's `IconDataProviderMixin` (see
// `AddOns/!!!ClassicAPI/Util/IconDataProvider.lua`) calls all four
// `src/embed/AddOns/!!!ClassicAPI/Util/IconDataProvider.lua`) calls all four
// in this order to assemble its `BaseIconFilenames` cache:
//
// GetLooseMacroIcons(spellList)