From 30a8c0fd481ff571e1bfb0bae2bbf1f52a2db383 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 1 Aug 2026 16:02:15 -0700 Subject: [PATCH] feat: a frame's id, which FrameXML builds names out of MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit id="1" is how a frame in a numbered set knows which one it is, and neither the emitter nor the widget API supported it at all — GetID and SetID simply did not exist. FrameXML concatenates the result straight into a name: PartyMemberFrame_RefreshPetDebuffs reaches for _G["PartyMemberFrame" .. self:GetID() .. "PetFrame"], so every one of the 848 ids declared across 57 files was looking up the wrong frame. Zero when unset, which is what the real client answers and what FrameXML concatenates without checking. --- src/addons/lua_engine.cpp | 4 ++++ src/ui/framexml_emitter.cpp | 8 ++++++++ tests/test_framexml.cpp | 9 +++++++++ 3 files changed, 21 insertions(+) diff --git a/src/addons/lua_engine.cpp b/src/addons/lua_engine.cpp index 5e5b2f1f..55543d4e 100644 --- a/src/addons/lua_engine.cpp +++ b/src/addons/lua_engine.cpp @@ -1397,6 +1397,10 @@ void LuaEngine::registerCoreAPI() { "function mt:GetHorizontalScrollRange() return 0 end\n" "function mt:GetVerticalScroll() return 0 end\n" "function mt:GetHorizontalScroll() return 0 end\n" + // Zero when unset, which is what the real client answers and what + // FrameXML concatenates into a name without checking. + "function mt:SetID(id) self.__id = id end\n" + "function mt:GetID() return self.__id or 0 end\n" "function mt:SetScrollChild(child) self.__scrollChild = child end\n" "function mt:GetScrollChild() return self.__scrollChild end\n" "function mt:SetFontString(fs) self.__fontString = fs end\n" diff --git a/src/ui/framexml_emitter.cpp b/src/ui/framexml_emitter.cpp index efa062a5..19417fdf 100644 --- a/src/ui/framexml_emitter.cpp +++ b/src/ui/framexml_emitter.cpp @@ -408,6 +408,14 @@ struct Emitter { if (const std::string* strata = node.attr("frameStrata")) { line(var + ":SetFrameStrata(" + quote(*strata) + ")"); } + // How a frame in a numbered set knows which one it is, and FrameXML + // builds names out of it: PartyMemberFrame_RefreshPetDebuffs reaches + // for _G["PartyMemberFrame" .. self:GetID() .. "PetFrame"]. Ignoring + // the attribute left every one of the 848 declared across 57 files + // answering zero. + if (const std::string* id = node.attr("id"); id && !id->empty()) { + line(var + ":SetID(" + *id + ")"); + } if (node.attr("enableMouse")) { line(var + ":EnableMouse(" + (node.attrBool("enableMouse") ? "true" : "false") + ")"); } diff --git a/tests/test_framexml.cpp b/tests/test_framexml.cpp index 87c273d0..4ef9999d 100644 --- a/tests/test_framexml.cpp +++ b/tests/test_framexml.cpp @@ -229,6 +229,15 @@ TEST_CASE("A template installs OnLoad but does not run it", "[framexml][emit]") REQUIRE(has(r.lua, "SetScript(\"OnLoad\"")); } +TEST_CASE("A frame's id becomes SetID", "[framexml][emit]") { + // How a frame in a numbered set knows which one it is. FrameXML builds + // names out of it — PartyMemberFrame_RefreshPetDebuffs reaches for + // _G["PartyMemberFrame" .. self:GetID() .. "PetFrame"] — and 848 of these + // are declared across 57 files. + XmlNode root = parseOrFail(""); + REQUIRE(has(emitFrameXml(root).lua, ":SetID(3)")); +} + TEST_CASE("parentKey binds a region to a field on its owner", "[framexml][emit]") { // How FrameXML's handlers reach their own pieces: QuestHonorFrameTemplate's // OnLoad opens with self.icon:SetTexture(...), and the icon is bound only