mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 14:47:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39dcfc73de | ||
|
|
6e5f7c3e43 | ||
|
|
2e05e86362 | ||
|
|
bcc631c4ed | ||
|
|
f9fb32186d | ||
|
|
60adbba7c3 | ||
|
|
c35d883af0 | ||
|
|
c202c1000f |
@@ -38,4 +38,8 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
if C_AddOns.DoesAddOnExist("SpecialTalentUI") then
|
||||
EventUtil.ContinueOnAddOnLoaded("SpecialTalentUI", CAPI_ApplyStandardColorGlobals)
|
||||
end
|
||||
|
||||
@@ -108,11 +108,14 @@ for _, v in pairs(ITEM_QUALITY_COLORS) do
|
||||
v.color = CreateColor(v.r, v.g, v.b, 1)
|
||||
end
|
||||
|
||||
do
|
||||
function CAPI_ApplyStandardColorGlobals()
|
||||
local envTbl = _G
|
||||
for _, dbColor in ipairs(C_UIColor.GetColors()) do
|
||||
local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
|
||||
envTbl[dbColor.baseTag] = color
|
||||
envTbl[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
if not envTbl[dbColor.baseTag] or not envTbl[dbColor.baseTag].GetRGB then
|
||||
local color = CreateColor(dbColor.color.r, dbColor.color.g, dbColor.color.b, dbColor.color.a)
|
||||
envTbl[dbColor.baseTag] = color
|
||||
envTbl[dbColor.baseTag.."_CODE"] = color:GenerateHexColorMarkup()
|
||||
end
|
||||
end
|
||||
end
|
||||
CAPI_ApplyStandardColorGlobals()
|
||||
|
||||
@@ -248,7 +248,7 @@ end
|
||||
local function CreateFrame_TradeSkillLink()
|
||||
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
|
||||
f:SetSize(FRAME_W, FRAME_H);
|
||||
f:SetPoint("CENTER", 0, 0);
|
||||
f:SetPoint("CENTER");
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
@@ -259,6 +259,9 @@ local function CreateFrame_TradeSkillLink()
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
f:SetScript("OnShow", function() PlaySound("igCharacterInfoOpen"); end);
|
||||
f:SetScript("OnHide", function() PlaySound("igCharacterInfoClose"); end);
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
@@ -296,7 +299,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT or "Exit");
|
||||
exit:SetText(EXIT);
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
@@ -393,29 +396,13 @@ local function CreateFrame_TradeSkillLink()
|
||||
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
|
||||
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
|
||||
detailScroll.scrollBarHideable = 1; -- template sets this too; belt-and-suspenders
|
||||
f.detailScroll = detailScroll;
|
||||
-- Off until a recipe overflows the pane (toggled in RenderDetail).
|
||||
-- The template shows its slider and enables the wheel by default; on
|
||||
-- the roomy Turtle pane that leaves a dead scrollbar that still eats
|
||||
-- wheel events and over-scrolls a hair. Start both disabled.
|
||||
-- Bar visibility + wheel are driven per-render by SetDetailBarShown (from
|
||||
-- the computed overflow). `scrollBarHideable` still lets the template's
|
||||
-- ScrollFrame_OnScrollRangeChanged manage the slider on range changes.
|
||||
-- Start with the wheel off; the first RenderDetail/ClearDetail sets it.
|
||||
detailScroll.scrollBarHideable = 1;
|
||||
detailScroll:EnableMouseWheel(false);
|
||||
local detailSB = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if detailSB then
|
||||
detailSB:Hide();
|
||||
end
|
||||
-- The template re-shows its slider when the frame transitions to shown
|
||||
-- (on first open, before the auto-selected recipe's RenderDetail runs),
|
||||
-- leaving a dead scrollbar until the first click. Re-assert the hidden
|
||||
-- state on show unless the current recipe genuinely overflows.
|
||||
detailScroll:SetScript("OnShow", function()
|
||||
f:UpdateDetailScrollbar();
|
||||
RunNextFrame(function()
|
||||
if f:IsShown() then
|
||||
f:UpdateDetailScrollbar();
|
||||
end
|
||||
end);
|
||||
end);
|
||||
f.detailScroll = detailScroll;
|
||||
|
||||
local detailChild = CreateFrame("Frame",
|
||||
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
|
||||
@@ -514,18 +501,20 @@ local function CreateFrame_TradeSkillLink()
|
||||
return btn;
|
||||
end
|
||||
|
||||
-- Show the detail slider + enable the wheel only when the current
|
||||
-- recipe overflows the pane (tracked by `detailNeedScroll`).
|
||||
function f:UpdateDetailScrollbar()
|
||||
local need = self.detailNeedScroll and true or false;
|
||||
self.detailScroll:EnableMouseWheel(need);
|
||||
-- Show the detail bar/borders only when the content overflows the pane.
|
||||
-- Driven by our own computed overflow, NOT the engine: scrollBarHideable's
|
||||
-- auto-hide only fires on a range CHANGE (so content that fits from the
|
||||
-- start never hides), and GetVerticalScrollRange isn't reliably readable
|
||||
-- the same tick as UpdateScrollChildRect.
|
||||
function f:SetDetailBarShown(show)
|
||||
self.detailScroll:EnableMouseWheel(show);
|
||||
local base = "ClassicAPITradeSkillLinkDetailScroll";
|
||||
local sb = getglobal(base .. "ScrollBar");
|
||||
if sb then sb:SetShown(need) end
|
||||
if sb then sb:SetShown(show) end
|
||||
local top = getglobal(base .. "Top");
|
||||
if top then top:SetShown(need) end
|
||||
if top then top:SetShown(show) end
|
||||
local bottom = getglobal(base .. "Bottom");
|
||||
if bottom then bottom:SetShown(need) end
|
||||
if bottom then bottom:SetShown(show) end
|
||||
end
|
||||
|
||||
function f:RenderDetail(entry)
|
||||
@@ -571,19 +560,21 @@ local function CreateFrame_TradeSkillLink()
|
||||
local total = table.getn(reagents);
|
||||
for i = 1, total do
|
||||
local btn = AcquireReagent(i);
|
||||
local r = reagents[i];
|
||||
local name, tex, link = itemInfo(r.itemID);
|
||||
local reagent = reagents[i];
|
||||
local name, tex, link = itemInfo(reagent.itemID);
|
||||
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
btn.itemLink = link;
|
||||
btn.nameText:SetText(name or ("item:" .. r.itemID));
|
||||
local need = r.count or 1;
|
||||
local have = C_Item.GetItemCount(r.itemID) or 0;
|
||||
btn.nameText:SetText(name or ("item:" .. reagent.itemID));
|
||||
local need = reagent.count or 1;
|
||||
local have = C_Item.GetItemCount(reagent.itemID) or 0;
|
||||
if have >= need then
|
||||
btn:GetNormalTexture():SetVertexColor(1, 1, 1);
|
||||
btn.nameText:SetTextColor(HIGHLIGHT_FONT_COLOR:GetRGB());
|
||||
local r, g, b = HIGHLIGHT_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
else
|
||||
btn:GetNormalTexture():SetVertexColor(0.5, 0.5, 0.5);
|
||||
btn.nameText:SetTextColor(GRAY_FONT_COLOR:GetRGB());
|
||||
local r, g, b = GRAY_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
@@ -594,29 +585,32 @@ local function CreateFrame_TradeSkillLink()
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
|
||||
-- Grow the scroll child to the content height so the pane scrolls
|
||||
-- once the header + reagent rows exceed the visible area. Reagent
|
||||
-- row 0 sits ~67px below the child top (icon + label); add the
|
||||
-- wrapped description height for spell recipes.
|
||||
local descH = (descText ~= "") and self.prodDesc:GetHeight() or 0;
|
||||
local rows = math.ceil(total / 2);
|
||||
local contentH = 67 + descH + rows * REAGENT_ROW_HEIGHT + 8;
|
||||
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
|
||||
-- Reset to the top and manage the slider explicitly: a range that
|
||||
-- stays 0 (content fits, e.g. Turtle's roomy pane) never fires the
|
||||
-- range-changed event that `scrollBarHideable` relies on, so the
|
||||
-- slider would otherwise sit visible-but-useless. Show it only when
|
||||
-- the content actually overflows.
|
||||
self.detailNeedScroll = contentH > LO.detail[4];
|
||||
local function applyContentHeight()
|
||||
local contentH = LO.detail[4];
|
||||
if total > 0 then
|
||||
local childTop = self.detailChild:GetTop();
|
||||
local lastBtn = self.reagents[total];
|
||||
local lastBottom = lastBtn and lastBtn:GetBottom();
|
||||
if childTop and lastBottom then
|
||||
-- +22: ~12px name-plate overhang below the icon + ~10px pad.
|
||||
contentH = (childTop - lastBottom) + 22;
|
||||
end
|
||||
end
|
||||
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
|
||||
-- Recompute the scroll range from the child's new height, then
|
||||
-- show the bar only if the content actually overflows the pane.
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(contentH > LO.detail[4] + 0.5);
|
||||
end
|
||||
applyContentHeight();
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
self:UpdateDetailScrollbar();
|
||||
RunNextFrame(function()
|
||||
if self:IsShown() then
|
||||
self:UpdateDetailScrollbar();
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
applyContentHeight();
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -639,8 +633,8 @@ local function CreateFrame_TradeSkillLink()
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
self.detailChild:SetHeight(LO.detail[4]);
|
||||
self.detailNeedScroll = false;
|
||||
self:UpdateDetailScrollbar();
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(false);
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
end
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
@@ -29,7 +30,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
@@ -61,6 +62,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
|
||||
+137
@@ -95,6 +95,7 @@ build instructions.
|
||||
|
||||
- [Events](#events)
|
||||
- [`C_EventUtils.IsEventValid(eventName)`](#c_eventutilsiseventvalideventname)
|
||||
- [`GetFramesRegisteredForEvent(event)`](#getframesregisteredforeventevent)
|
||||
- [`PLAYER_ENTERING_WORLD` payload (`isInitialLogin`, `isReloadingUi`)](#player_entering_world-payload-isinitiallogin-isreloadingui)
|
||||
- [`PLAYER_TOTEM_UPDATE` event](#player_totem_update-event)
|
||||
- [`BAG_UPDATE_DELAYED` event](#bag_update_delayed-event)
|
||||
@@ -334,6 +335,9 @@ build instructions.
|
||||
- [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot)
|
||||
- [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled)
|
||||
|
||||
- [ChatBubbles](#chatbubbles)
|
||||
- [`C_ChatBubbles.GetAllChatBubbles([includeForbidden])`](#c_chatbubblesgetallchatbubblesincludeforbidden)
|
||||
|
||||
- [NamePlate](#nameplate)
|
||||
- [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates)
|
||||
- [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids)
|
||||
@@ -445,6 +449,14 @@ build instructions.
|
||||
- [`GetTalentSpellID(tabIndex, talentIndex, [rank])`](#gettalentspellidtabindex-talentindex-rank)
|
||||
- [`GetTalentIDByIndex(tabIndex, talentIndex)`](#gettalentidbyindextabindex-talentindex)
|
||||
|
||||
- [Targeting](#targeting)
|
||||
- [`GetPlayerFacing()`](#getplayerfacing)
|
||||
- [`TargetDirectionEnemy(facing [, coneAngle])`](#targetdirectionenemyfacing--coneangle)
|
||||
- [`TargetDirectionFriend(facing [, coneAngle])`](#targetdirectionfriendfacing--coneangle)
|
||||
- [`TargetNearest([reverse])`](#targetnearestreverse)
|
||||
- [`TargetNearestEnemyPlayer([reverse])`](#targetnearestenemyplayerreverse)
|
||||
- [`TargetNearestFriendPlayer([reverse])`](#targetnearestfriendplayerreverse)
|
||||
|
||||
- [TaxiMap](#taximap)
|
||||
- [`C_TaxiMap.GetTaxiNodesForMap([mapID])`](#c_taximapgettaxinodesformapmapid)
|
||||
- [`C_TaxiMap.GetAllTaxiNodes([uiMapID])`](#c_taximapgetalltaxinodesuimapid)
|
||||
@@ -2235,6 +2247,27 @@ SuperWoWhook patches the binary's event names with its own. This matches
|
||||
what `frame:RegisterEvent` will actually accept, which is what addon code
|
||||
needs to know.
|
||||
|
||||
### `GetFramesRegisteredForEvent(event)`
|
||||
|
||||
Returns the frames currently registered for `event`, as multiple return
|
||||
values (matching the modern signature — not a table). Returns nothing when
|
||||
no frame is registered or the event name is unknown.
|
||||
|
||||
```lua
|
||||
local f1, f2 = GetFramesRegisteredForEvent("PLAYER_LOGIN")
|
||||
for i = 1, select("#", GetFramesRegisteredForEvent("BAG_UPDATE")) do
|
||||
local frame = select(i, GetFramesRegisteredForEvent("BAG_UPDATE"))
|
||||
print(frame:GetName())
|
||||
end
|
||||
```
|
||||
|
||||
Reads the engine's own subscriber chain — the exact list
|
||||
`frame:RegisterEvent` appends to — so it stays in sync with real
|
||||
registrations, including our `AutoReserve`-backed custom events. Frames
|
||||
created purely in C++ (nameplates, etc.) come back as their canonical
|
||||
wrapper, so `:GetName()` and other methods work; an anonymous frame's
|
||||
`:GetName()` is `nil`, same as on retail.
|
||||
|
||||
### `PLAYER_ENTERING_WORLD` payload (`isInitialLogin`, `isReloadingUi`)
|
||||
|
||||
Backports the 8.0.1 payload onto vanilla's (natively arg-less)
|
||||
@@ -8012,6 +8045,42 @@ No retail equivalent (retail ships only the explored getter; its inverse,
|
||||
`C_Map.GetMapOverlays`, returns *everything*). Same table shape and
|
||||
`areaID` semantics as the explored getter.
|
||||
|
||||
## ChatBubbles
|
||||
|
||||
### `C_ChatBubbles.GetAllChatBubbles([includeForbidden])`
|
||||
|
||||
Returns a 1-based table of the currently-active chat-bubble `Frame`
|
||||
objects (the speech bubbles above characters who `/say` or `/yell`).
|
||||
Modern WoW added this in 7.2.5 to replace the old "iterate over
|
||||
`WorldFrame` children and guess which are bubbles" idiom — we hand you
|
||||
the exact set instead.
|
||||
|
||||
```lua
|
||||
for _, bubble in ipairs(C_ChatBubbles.GetAllChatBubbles()) do
|
||||
-- The spoken text lives in the bubble's FontString region.
|
||||
for _, region in ipairs({bubble:GetRegions()}) do
|
||||
if region.GetText and region:GetText() then
|
||||
print(region:GetText())
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Vanilla 1.12 chat bubbles are `CGChatBubbleFrame`s — full frames the
|
||||
engine creates in C++ without ever calling `CreateFrame` (same as
|
||||
default nameplates). The returned frames are the engine's **canonical
|
||||
wrappers**: real, method-capable (`:GetRegions()`, `:IsShown()`,
|
||||
`:GetPoint()`, …), and identical to the object any other addon that
|
||||
touches the bubble sees, so decorations survive. The spoken-text
|
||||
FontString is parented to the bubble, so it shows up in
|
||||
`:GetRegions()`.
|
||||
|
||||
`includeForbidden` is accepted for signature parity and ignored —
|
||||
vanilla has no forbidden frames, so every active bubble is returned
|
||||
regardless. Returns an empty table when no bubbles are showing. A
|
||||
bubble whose owner just despawned can linger for one frame before the
|
||||
engine prunes it; filter on `bubble:IsShown()` if that matters.
|
||||
|
||||
## NamePlate
|
||||
|
||||
Modern `C_NamePlate.*` returns nameplate `Frame` objects keyed off
|
||||
@@ -10935,6 +11004,74 @@ a different field.
|
||||
Equivalent to the talentID return slot of `GetTalentInfo` in modern
|
||||
WoW (5.0+; not exposed at all in 1.12).
|
||||
|
||||
## Targeting
|
||||
|
||||
Backports of the post-vanilla `TargetScript` selection functions. Vanilla
|
||||
already ships `TargetNearestEnemy/Friend/PartyMember/RaidMember`,
|
||||
`TargetLastEnemy/Target`, `AssistUnit`, `TargetUnit`, `ClearTarget`; these
|
||||
fill in the ones added in later patches. They're built on the engine's own
|
||||
tab-targeting core — the same visible-unit enumeration, per-mode validity
|
||||
predicate (reaction/attackability/alive), and selection commit the native
|
||||
`TargetNearestEnemy` uses — so hostility semantics match the client exactly.
|
||||
|
||||
Not backported (no vanilla equivalent — soft-target / action-camera era):
|
||||
`TargetPriorityHighlightStart/End`, `IsTargetLoose`, `TargetToggle`.
|
||||
|
||||
### `GetPlayerFacing()`
|
||||
|
||||
Returns the player's facing as an angle in radians (`0` … `2*pi`,
|
||||
increasing counter-clockwise), or nothing off-world (glue / loading).
|
||||
Vanilla never shipped this (only character-create/select facing exist), but
|
||||
it's the companion the direction-target functions below need — pass it to
|
||||
aim "in front of me."
|
||||
|
||||
```lua
|
||||
local facing = GetPlayerFacing() -- e.g. 1.65
|
||||
```
|
||||
|
||||
Read from the player unit's movement-block orientation field
|
||||
(`player + 0x9C4`).
|
||||
|
||||
### `TargetDirectionEnemy(facing [, coneAngle])`
|
||||
|
||||
Targets the nearest **attackable** unit within a cone centered on `facing`
|
||||
(an absolute world angle in radians, as returned by `GetPlayerFacing()`).
|
||||
`coneAngle` is the full cone width in radians; omitted, it defaults to
|
||||
`pi/2` (90° — 45° to either side).
|
||||
|
||||
```lua
|
||||
-- Target whatever enemy you're looking at:
|
||||
TargetDirectionEnemy(GetPlayerFacing())
|
||||
|
||||
-- Wider 180° sweep in front:
|
||||
TargetDirectionEnemy(GetPlayerFacing(), math.pi)
|
||||
```
|
||||
|
||||
Does nothing if no enemy falls inside the cone. "Attackable" uses the
|
||||
engine's own reaction check, so it matches what `TargetNearestEnemy` would
|
||||
consider a valid enemy (hostile, alive, not a critter).
|
||||
|
||||
### `TargetDirectionFriend(facing [, coneAngle])`
|
||||
|
||||
Same as `TargetDirectionEnemy`, but selects the nearest **friendly**
|
||||
(assistable) unit in the cone.
|
||||
|
||||
### `TargetNearest([reverse])`
|
||||
|
||||
Cycles the nearest unit you can attack or assist. Repeated calls within ~1s
|
||||
step through candidates nearest→farthest (a frozen snapshot, so the order
|
||||
is stable mid-cycle); a longer gap or an externally-changed target rebuilds
|
||||
and re-targets the nearest. `reverse` (truthy) steps the other way.
|
||||
|
||||
### `TargetNearestEnemyPlayer([reverse])`
|
||||
|
||||
Like `TargetNearest`, but restricted to hostile **players** (PvP targeting).
|
||||
Same cycle behavior and `reverse` flag.
|
||||
|
||||
### `TargetNearestFriendPlayer([reverse])`
|
||||
|
||||
Like `TargetNearestEnemyPlayer`, but restricted to friendly **players**.
|
||||
|
||||
## TaxiMap
|
||||
|
||||
Backports of the retail taxi-map node enumerators, reading
|
||||
|
||||
@@ -463,6 +463,30 @@ enum Offsets {
|
||||
// (lo) and `guid[1]` (hi) through the pointer.
|
||||
FUN_TARGET_BY_GUID = 0x00489A40,
|
||||
|
||||
// Tab-targeting internals, shared with our backported TargetNearest* /
|
||||
// TargetDirection* family (`target/Nearest.cpp`).
|
||||
//
|
||||
// `FUN_TARGET_CANDIDATE_VALID` (0x00493E40) — the engine's per-mode
|
||||
// candidate validity predicate used by the native `TargetNearestEnemy`
|
||||
// / `TargetNearestFriend` cycle. `__fastcall(CGUnit *player /*ecx*/,
|
||||
// CGUnit *candidate /*edx*/, int mode) → int` (nonzero = valid). Modes:
|
||||
// 1 = enemy (attackable + alive + not-critter), 2 = friend (assistable +
|
||||
// alive), 3 = party, 4 = raid. Wraps the reaction cores
|
||||
// `FUN_00606980` (can-attack) / `FUN_006066f0` (can-assist), so calling
|
||||
// it gives us the engine's exact hostility semantics for free.
|
||||
FUN_TARGET_CANDIDATE_VALID = 0x00493E40,
|
||||
// `FUN_SET_SELECTION_BY_GUID` (0x00493540) — canonical target setter the
|
||||
// native tab-cycle commits through. `__cdecl(uint32_t guidLo, uint32_t
|
||||
// guidHi)`. Validates + fires CMSG_SET_SELECTION + client target +
|
||||
// event 0xC3, and maintains the current-selection globals below.
|
||||
// Passing {0,0} clears the target.
|
||||
FUN_SET_SELECTION_BY_GUID = 0x00493540,
|
||||
// Current target-selection GUID (lo/hi), written by
|
||||
// `FUN_SET_SELECTION_BY_GUID`. We read it to detect whether the player's
|
||||
// target changed out from under an in-progress tab-cycle.
|
||||
VAR_CURRENT_SELECTION_GUID_LO = 0x00B4E2D8,
|
||||
VAR_CURRENT_SELECTION_GUID_HI = 0x00B4E2DC,
|
||||
|
||||
// Local-player CGObject-like global. Not the same pointer as
|
||||
// ResolveUnitToken("player") returns — that one's the canonical
|
||||
// CGPlayer_C used by inventory etc. This pointer's +0xC0 field
|
||||
@@ -1476,6 +1500,15 @@ enum Offsets {
|
||||
// `0x0048BACF` / `0x0048BADC`.
|
||||
OFF_CGOBJECT_VTBL_GET_POSITION = 0x14,
|
||||
|
||||
// CGUnit movement-block orientation (facing) in radians, 0..2*pi. Lives
|
||||
// right after the {x,y,z} position block (at +0x9B8), i.e. pos+0xC. The
|
||||
// `GetPosition` virtual copies only x/y/z into the caller's buffer, so
|
||||
// read facing off the object directly. Backs `GetPlayerFacing`. Empirically
|
||||
// located by probing the player object across a known 180-degree turn
|
||||
// (value flipped by ~pi, stayed in range); mirrored at +0x9F8 / +0xC98
|
||||
// (last-sent / render copies) — +0x9C4 is the authoritative one.
|
||||
OFF_UNIT_FACING = 0x9C4,
|
||||
|
||||
// CGObject `m_objectType` field — 0 = (base) object, 1 = item,
|
||||
// 2 = container, 3 = unit, 4 = player, 5 = gameobject, … Read as
|
||||
// `*(int *)(obj + 0x14)`. (Distinct from GET_POSITION above, which
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_ChatBubbles.GetAllChatBubbles([includeForbidden])` — returns an
|
||||
// array of the currently-active chat-bubble Frame objects, matching
|
||||
// modern WoW's 7.2.5 signature. Addons use it to recolor / translate /
|
||||
// reposition speech bubbles by reading the bubble's FontString region.
|
||||
//
|
||||
// Vanilla 1.12 chat bubbles are `CGChatBubbleFrame`s — full
|
||||
// `CSimpleFrame`s created purely in C++ (never via `CreateFrame`), so
|
||||
// they have no name and no Lua wrapper until something pushes them. This
|
||||
// is exactly the default-nameplate case: we push each via the shared
|
||||
// `UI::FrameObject::Push`, which delegates to the engine's own
|
||||
// `FrameScript_Object::ScriptRegister` to lazily build the canonical
|
||||
// wrapper. The returned frames respond to real frame methods — the
|
||||
// FontString holding the spoken text (bubble + 0x31c, created with the
|
||||
// bubble as its parent) shows up in `bubble:GetRegions()`, so the modern
|
||||
// "iterate GetRegions, find the FontString, read GetText()" idiom works
|
||||
// unchanged. This is the pre-7.2.5 "walk WorldFrame children" pattern the
|
||||
// wiki describes, but handed the exact set instead of the whole world.
|
||||
//
|
||||
// The engine keeps active bubbles on an intrusive Storm `TSList` whose
|
||||
// head is `VAR_CHAT_BUBBLE_LIST_HEAD` (`0x00B6F038`); each node's forward
|
||||
// link sits at `+0x318` and the list terminator carries the low-bit
|
||||
// sentinel. We mirror the engine's own walk in the per-frame update
|
||||
// `FUN_004b1060` / teardown `FUN_004b0f80` verbatim. Bubbles whose owner
|
||||
// unit has despawned are pruned on the engine's next update tick, so a
|
||||
// just-orphaned bubble can linger one frame — harmless, and addons that
|
||||
// care filter on `bubble:IsShown()` (a real method here) anyway.
|
||||
//
|
||||
// `includeForbidden` is accepted and ignored: vanilla has no forbidden
|
||||
// frames, so every bubble is returned regardless.
|
||||
|
||||
#include "Game.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace ChatBubble::Info {
|
||||
|
||||
namespace {
|
||||
|
||||
// Storm TSList of live CGChatBubbleFrames. Head pointer, per-node
|
||||
// forward link, and the end-of-list low-bit sentinel — see the engine
|
||||
// walks at 0x004b1060 / 0x004b0f80.
|
||||
constexpr uintptr_t kListHead = 0x00B6F038;
|
||||
constexpr int kOffNextLink = 0x318;
|
||||
|
||||
} // namespace
|
||||
|
||||
static int __fastcall Script_GetAllChatBubbles(void *L) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(kListHead);
|
||||
int nextIndex = 1;
|
||||
while (node != 0 && (node & 1) == 0) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextIndex++));
|
||||
UI::FrameObject::Push(L, reinterpret_cast<void *>(node));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
node = *reinterpret_cast<const uintptr_t *>(node + kOffNextLink);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_ChatBubbles", "GetAllChatBubbles",
|
||||
&Script_GetAllChatBubbles);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace ChatBubble::Info
|
||||
@@ -0,0 +1,98 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetFramesRegisteredForEvent(event)` → the Frame objects currently
|
||||
// subscribed to `event`, returned as multiple return values (matching
|
||||
// modern WoW's vararg signature — not a table). Empty result set returns
|
||||
// nothing.
|
||||
//
|
||||
// The engine already keeps exactly this list: every `frame:RegisterEvent`
|
||||
// appends the frame to a subscriber chain hanging off the matching event
|
||||
// table entry. We find the entry by name (same table `Event::Custom`
|
||||
// walks), then walk the chain the same way `Frame::RegisterEvent`
|
||||
// (`FUN_00702140`) does — node next at `+0x04`, subscriber frame at
|
||||
// `+0x08`, with a low-bit-set / null value marking the end sentinel — and
|
||||
// push each frame via the shared `UI::FrameObject::Push` so callers get
|
||||
// the canonical wrapper table (real frame methods, `:GetName()`, etc.).
|
||||
//
|
||||
// Event names resolve through `Event::Custom::LookupByName`, so this also
|
||||
// works for our `AutoReserve`-claimed custom events, not just engine ones.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Event::Registered {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetFramesRegisteredForEvent(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return 0;
|
||||
const char *event = Game::Lua::ToString(L, 1);
|
||||
if (event == nullptr)
|
||||
return 0;
|
||||
|
||||
const int slot = Event::Custom::LookupByName(event);
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr)
|
||||
return 0;
|
||||
|
||||
const uint8_t *entry = base + slot * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const uintptr_t head = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
|
||||
// First pass: count subscribers so we can reserve stack up front — an
|
||||
// event with many registered frames would otherwise risk overflowing
|
||||
// the default Lua stack slack as we push.
|
||||
int count = 0;
|
||||
for (uintptr_t node = head; (node & 1) == 0 && node != 0;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
if (*reinterpret_cast<void *const *>(node + Offsets::OFF_EVENT_NODE_FRAME))
|
||||
++count;
|
||||
}
|
||||
if (count == 0)
|
||||
return 0;
|
||||
Game::Lua::CheckStack(L, count);
|
||||
|
||||
int pushed = 0;
|
||||
for (uintptr_t node = head; (node & 1) == 0 && node != 0;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT)) {
|
||||
void *frame = *reinterpret_cast<void *const *>(
|
||||
node + Offsets::OFF_EVENT_NODE_FRAME);
|
||||
if (frame != nullptr) {
|
||||
UI::FrameObject::Push(L, frame);
|
||||
++pushed;
|
||||
}
|
||||
}
|
||||
return pushed;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetFramesRegisteredForEvent",
|
||||
&Script_GetFramesRegisteredForEvent);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Event::Registered
|
||||
+7
-66
@@ -56,6 +56,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -66,9 +67,6 @@ namespace {
|
||||
using NamePlate::Walk::ForEachNamePlatedUnit;
|
||||
using NamePlate::Walk::kOffUnitNamePlate;
|
||||
|
||||
using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using ScriptRegister_t = void(__fastcall *)(void *this_, void *edx_unused,
|
||||
const char *name);
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
@@ -115,70 +113,13 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) {
|
||||
// frame-push path. Internal callers (Script_GetNamePlates etc.) call
|
||||
// it through the unqualified name (they live in the same namespace).
|
||||
//
|
||||
// Delegate wrapper construction to the engine's own
|
||||
// `FrameScript_Object::ScriptRegister`. First call for a never-seen
|
||||
// nameplate builds a `{[0] = lightuserdata}` table with the
|
||||
// framescript metatable, `luaL_ref`s it into the registry, and
|
||||
// writes the refkey to `nameplate + 0x08` (`OFF_COBJECT_LUA_REGISTRY_REF`).
|
||||
// Subsequent calls find a populated refkey and just rawgeti.
|
||||
//
|
||||
// Why delegate instead of building our own wrapper:
|
||||
//
|
||||
// - **Single source of truth.** Every code path that pushes this
|
||||
// frame — ours via this function, addons' `CreateFrame("Type",
|
||||
// "name", plate)` extracting the parent, the engine's own internal
|
||||
// pushers — funnels through `rawgeti(REGISTRY, plate+0x08)` and
|
||||
// gets the same Lua table. Earlier "build our own wrapper +
|
||||
// `luaL_ref` it" approach kept a side cache, but pfUI's
|
||||
// `CreateFrame("Button", "name", parent)` inside `NAME_PLATE_CREATED`
|
||||
// ends up causing the engine to register the parent itself —
|
||||
// the engine's wrapper diverges from ours, and later
|
||||
// `GetNamePlateForUnit` calls return the engine's bare wrapper
|
||||
// instead of our decorated one. Addon fields on the wrapper
|
||||
// (`plate.nameplate = styledButton`) disappear from the API's
|
||||
// perspective even though they still exist on the orphaned
|
||||
// table.
|
||||
//
|
||||
// - **Refcount-pinning is benign here.** `ScriptRegister`
|
||||
// unconditionally increments `nameplate + 0x04`, which normally
|
||||
// keeps the engine from GC-ing the CObject. Nameplates are
|
||||
// pool-managed and never destroyed during a session, so pinning
|
||||
// doesn't actually leak anything. We call `ScriptRegister` at
|
||||
// most once per nameplate pointer (gated by `refKey <= 0`), so
|
||||
// the refcount tops out at one increment per pool slot.
|
||||
//
|
||||
// Defensive fallback: if the registry slot exists but isn't a table
|
||||
// any more (some other code freed it), we re-register. This shouldn't
|
||||
// happen in practice — engines only unref on frame destruction —
|
||||
// but a stale type check keeps callers from ever seeing a non-table.
|
||||
// Thin wrapper over the shared `UI::FrameObject::Push` — nameplates and
|
||||
// chat bubbles are both engine-created-in-C++ `CSimpleFrame`s that need
|
||||
// the same lazy `ScriptRegister` → `rawgeti` push so addons get the
|
||||
// engine's canonical wrapper (see that helper for the pfUI-divergence
|
||||
// rationale that motivated delegating to the engine).
|
||||
void PushNamePlateFrame(void *L, void *nameplate) {
|
||||
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
|
||||
auto scriptRegister = reinterpret_cast<ScriptRegister_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER));
|
||||
|
||||
int refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
if (refKey <= 0) {
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
}
|
||||
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
|
||||
return;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
|
||||
// Slot got freed somewhere — re-register to rebuild a fresh
|
||||
// wrapper and slot. Zero the refcount so `ScriptRegister`
|
||||
// re-enters its build branch.
|
||||
*reinterpret_cast<int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0;
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
UI::FrameObject::Push(L, nameplate);
|
||||
}
|
||||
|
||||
// GUID → nameplate Frame pushed on stack. Pushes nil if the GUID
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetPlayerFacing()` → facing angle of the player in radians (0 =
|
||||
// north/+Y in WoW's world convention, increasing counter-clockwise), or
|
||||
// nothing when there's no player (glue / loading). Vanilla 1.12 doesn't
|
||||
// ship it (only character-create/select facing exist), but it's the
|
||||
// missing half that makes our backported `TargetDirectionEnemy/Friend`
|
||||
// usable — a caller passes `GetPlayerFacing()` to target what's in front.
|
||||
//
|
||||
// The orientation is a field in the player's CGUnit movement block
|
||||
// (`OFF_UNIT_FACING` = +0x9C4), sitting just past the {x,y,z} position at
|
||||
// +0x9B8. The `GetPosition` virtual only copies x/y/z into a caller buffer,
|
||||
// so the facing is read off the object directly.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Player::Facing {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_GetPlayerFacing(void *L) {
|
||||
void *player = Unit::Position::ResolveToken("player");
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
const float facing = *reinterpret_cast<const float *>(
|
||||
static_cast<const uint8_t *>(player) + Offsets::OFF_UNIT_FACING);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(facing));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetPlayerFacing", &Script_GetPlayerFacing);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Player::Facing
|
||||
@@ -0,0 +1,328 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Backports the post-vanilla TargetScript selection functions that 1.12.1
|
||||
// doesn't ship, built on the engine's own tab-targeting internals:
|
||||
//
|
||||
// TargetNearest() — cycle nearest attackable/assistable unit
|
||||
// TargetNearestEnemyPlayer([rev]) — cycle nearest hostile player (PvP)
|
||||
// TargetNearestFriendPlayer([rev]) — cycle nearest friendly player
|
||||
// TargetDirectionEnemy(facing[,cone]) — nearest hostile in a facing cone
|
||||
// TargetDirectionFriend(facing[,cone]) — nearest friendly in a facing cone
|
||||
//
|
||||
// Vanilla already ships `TargetNearestEnemy/Friend/PartyMember/RaidMember`;
|
||||
// they run through `FUN_00493f60(reverse, mode)` — enumerate visible units,
|
||||
// keep those `FUN_00493e40(player, cand, mode)` accepts, sort by distance,
|
||||
// cycle, and commit via `FUN_00493540(guidLo, guidHi)`. We reuse the two
|
||||
// load-bearing pieces of that machinery directly:
|
||||
//
|
||||
// * `FUN_00493e40` (Offsets::FUN_TARGET_CANDIDATE_VALID) — the exact
|
||||
// per-mode validity predicate (mode 1 = enemy: attackable + alive +
|
||||
// not-critter; mode 2 = friend: assistable + alive). Calling it gives
|
||||
// us the engine's real reaction/hostility semantics with zero
|
||||
// reimplementation.
|
||||
// * `FUN_00493540` (Offsets::FUN_SET_SELECTION_BY_GUID) — the canonical
|
||||
// setter the native cycle commits through (CMSG_SET_SELECTION + client
|
||||
// target + event 0xC3), which also maintains the current-selection
|
||||
// globals we read back for cycle continuity.
|
||||
//
|
||||
// So the only genuinely new logic here is (a) an extra player-GUID filter
|
||||
// for the `*Player` variants and (b) the cone geometry for the direction
|
||||
// variants — everything reaction-related defers to the engine.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Target::Nearest {
|
||||
|
||||
namespace {
|
||||
|
||||
// Candidate class each function selects from.
|
||||
enum class Filter { Enemy, Friend, EnemyPlayer, FriendPlayer, Any };
|
||||
constexpr int kFilterCount = 5;
|
||||
|
||||
// Default cone (full width, radians) when `coneAngle` is omitted — 90°, so
|
||||
// 45° to either side of the facing. The engine's own default constant
|
||||
// wasn't cleanly recoverable from the 3.3.5 reference binary; this is a
|
||||
// deliberate, documented choice that matches the "roughly in front of me"
|
||||
// intent of a direction-target keybind.
|
||||
constexpr float kDefaultConeAngle = 1.5707963f; // pi/2
|
||||
|
||||
// Re-cycle window: repeated presses within this many ms advance through the
|
||||
// frozen candidate snapshot (no ping-pong as distances shift); a longer gap
|
||||
// rebuilds and re-targets the nearest. Mirrors the ~1s reset the native
|
||||
// cycle uses.
|
||||
constexpr uint32_t kCycleWindowMs = 1000;
|
||||
|
||||
constexpr int kMaxCandidates = 64;
|
||||
|
||||
// Engine entry points ─────────────────────────────────────────────────
|
||||
using Predicate_t = int(__fastcall *)(void *player, void *cand, int mode);
|
||||
using SetTargetByGuid_t = void(__fastcall *)(const uint64_t *guid);
|
||||
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
|
||||
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
|
||||
using ObjectPtr_t = void *(__fastcall *)(uint32_t typeMask, const char *dbg,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int dbgCode);
|
||||
using TickMs_t = uint32_t(__fastcall *)();
|
||||
|
||||
bool PassesFilter(void *player, void *cand, uint64_t guid, Filter f) {
|
||||
auto valid = reinterpret_cast<Predicate_t>(Offsets::FUN_TARGET_CANDIDATE_VALID);
|
||||
switch (f) {
|
||||
case Filter::Enemy: return valid(player, cand, 1) != 0;
|
||||
case Filter::Friend: return valid(player, cand, 2) != 0;
|
||||
case Filter::EnemyPlayer: return Guid::IsPlayer(guid) && valid(player, cand, 1) != 0;
|
||||
case Filter::FriendPlayer: return Guid::IsPlayer(guid) && valid(player, cand, 2) != 0;
|
||||
case Filter::Any: return valid(player, cand, 1) != 0 ||
|
||||
valid(player, cand, 2) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
uint64_t guid;
|
||||
float x, y; // world XY (for the direction cone)
|
||||
float distSq; // 3D squared distance from the player (for nearest)
|
||||
};
|
||||
|
||||
struct CollectCtx {
|
||||
void *player;
|
||||
Filter filter;
|
||||
float px, py, pz;
|
||||
Candidate list[kMaxCandidates];
|
||||
int count;
|
||||
};
|
||||
|
||||
int __fastcall CollectCb(CollectCtx *ctx, void * /*edx*/, uint64_t guid) {
|
||||
if (ctx->count >= kMaxCandidates)
|
||||
return 1;
|
||||
// Players and creatures both resolve under TYPEMASK_UNIT (CGPlayer
|
||||
// derives from CGUnit); non-unit objects resolve to null and drop out.
|
||||
auto ObjectPtr =
|
||||
reinterpret_cast<ObjectPtr_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *cand = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (cand == nullptr || cand == ctx->player)
|
||||
return 1;
|
||||
if (!PassesFilter(ctx->player, cand, guid, ctx->filter))
|
||||
return 1;
|
||||
|
||||
float pos[3];
|
||||
if (!Unit::Position::Read(cand, pos))
|
||||
return 1;
|
||||
const float dx = pos[0] - ctx->px;
|
||||
const float dy = pos[1] - ctx->py;
|
||||
const float dz = pos[2] - ctx->pz;
|
||||
Candidate &c = ctx->list[ctx->count++];
|
||||
c.guid = guid;
|
||||
c.x = pos[0];
|
||||
c.y = pos[1];
|
||||
c.distSq = dx * dx + dy * dy + dz * dz;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Enumerate visible units matching `f` into `out`. Returns false (empty)
|
||||
// pre-world or on a player-position miss.
|
||||
bool Collect(Filter f, CollectCtx *out) {
|
||||
out->count = 0;
|
||||
// The enumerator derefs the object manager unconditionally; null on the
|
||||
// glue / character-select screen.
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) ==
|
||||
nullptr)
|
||||
return false;
|
||||
void *player = Unit::Position::ResolveToken("player");
|
||||
float ppos[3];
|
||||
if (!Unit::Position::Read(player, ppos))
|
||||
return false;
|
||||
|
||||
out->player = player;
|
||||
out->filter = f;
|
||||
out->px = ppos[0];
|
||||
out->py = ppos[1];
|
||||
out->pz = ppos[2];
|
||||
auto Enum = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
Enum(reinterpret_cast<EnumCallback_t>(&CollectCb), out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Commit the selection through the engine's `Script_TargetUnit` helper
|
||||
// rather than the tab-cycle's raw `FUN_00493540`: this one first checks the
|
||||
// GUID resolves to a live unit and bails otherwise, so a stale GUID left in
|
||||
// a cycle snapshot (unit despawned / went out of range between presses)
|
||||
// gets rejected instead of committed. Committing a dead GUID would leave
|
||||
// `"target"` pointing at freed unit data and crash the next handler that
|
||||
// reads it. `FUN_TARGET_BY_GUID` still routes through `FUN_00493540`
|
||||
// internally, so the current-selection globals we read back are maintained.
|
||||
void SetTarget(uint64_t guid) {
|
||||
reinterpret_cast<SetTargetByGuid_t>(Offsets::FUN_TARGET_BY_GUID)(&guid);
|
||||
}
|
||||
|
||||
uint64_t CurrentSelection() {
|
||||
const uint32_t lo = *reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_HI);
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
}
|
||||
|
||||
uint32_t NowMs() {
|
||||
return reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
|
||||
}
|
||||
|
||||
// Ascending insertion sort by 3D distance (candidate counts are tiny).
|
||||
void SortByDistance(Candidate *a, int n) {
|
||||
for (int i = 1; i < n; ++i) {
|
||||
Candidate key = a[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && a[j].distSq > key.distSq) {
|
||||
a[j + 1] = a[j];
|
||||
--j;
|
||||
}
|
||||
a[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-filter tab-cycle state. A frozen snapshot of GUIDs (sorted nearest→
|
||||
// farthest) plus a cursor; presses within `kCycleWindowMs` that haven't had
|
||||
// the target changed out from under them advance the cursor over the frozen
|
||||
// list, so the order stays stable through a cycle burst.
|
||||
struct CycleState {
|
||||
bool valid = false;
|
||||
uint64_t snapshot[kMaxCandidates];
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
uint32_t lastPressMs = 0;
|
||||
uint64_t lastSetGuid = 0;
|
||||
};
|
||||
CycleState g_cycle[kFilterCount];
|
||||
|
||||
void DoCycle(void *L, Filter f) {
|
||||
const bool reverse = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
CycleState &st = g_cycle[static_cast<int>(f)];
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
const bool cont = st.valid && st.count > 0 &&
|
||||
(now - st.lastPressMs) <= kCycleWindowMs &&
|
||||
CurrentSelection() == st.lastSetGuid;
|
||||
|
||||
if (cont) {
|
||||
st.index = reverse ? (st.index + st.count - 1) % st.count
|
||||
: (st.index + 1) % st.count;
|
||||
} else {
|
||||
CollectCtx c;
|
||||
if (!Collect(f, &c) || c.count == 0) {
|
||||
st.valid = false;
|
||||
return;
|
||||
}
|
||||
SortByDistance(c.list, c.count);
|
||||
st.count = c.count;
|
||||
for (int i = 0; i < c.count; ++i)
|
||||
st.snapshot[i] = c.list[i].guid;
|
||||
st.index = 0;
|
||||
st.valid = true;
|
||||
}
|
||||
|
||||
const uint64_t guid = st.snapshot[st.index];
|
||||
SetTarget(guid);
|
||||
st.lastSetGuid = guid;
|
||||
st.lastPressMs = now;
|
||||
}
|
||||
|
||||
void DoDirection(void *L, bool hostile, const char *usage) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return;
|
||||
}
|
||||
const float facing = static_cast<float>(Game::Lua::ToNumber(L, 1));
|
||||
float cone = Game::Lua::IsNumber(L, 2)
|
||||
? static_cast<float>(Game::Lua::ToNumber(L, 2))
|
||||
: kDefaultConeAngle;
|
||||
if (cone <= 0.0f)
|
||||
cone = kDefaultConeAngle;
|
||||
const float cosHalf = std::cos(cone * 0.5f);
|
||||
const float dirX = std::cos(facing);
|
||||
const float dirY = std::sin(facing);
|
||||
|
||||
CollectCtx c;
|
||||
if (!Collect(hostile ? Filter::Enemy : Filter::Friend, &c) || c.count == 0)
|
||||
return;
|
||||
|
||||
int best = -1;
|
||||
float bestDistSq = 0.0f;
|
||||
for (int i = 0; i < c.count; ++i) {
|
||||
const float vx = c.list[i].x - c.px;
|
||||
const float vy = c.list[i].y - c.py;
|
||||
const float horizSq = vx * vx + vy * vy;
|
||||
if (horizSq <= 0.0001f)
|
||||
continue;
|
||||
// cos(angle between facing and the horizontal vector to the unit).
|
||||
const float cosAngle = (dirX * vx + dirY * vy) / std::sqrt(horizSq);
|
||||
if (cosAngle < cosHalf)
|
||||
continue; // outside the cone
|
||||
if (best < 0 || c.list[i].distSq < bestDistSq) {
|
||||
best = i;
|
||||
bestDistSq = c.list[i].distSq;
|
||||
}
|
||||
}
|
||||
if (best >= 0)
|
||||
SetTarget(c.list[best].guid);
|
||||
}
|
||||
|
||||
// Lua entry points ─────────────────────────────────────────────────────
|
||||
int __fastcall Script_TargetNearest(void *L) {
|
||||
DoCycle(L, Filter::Any);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetNearestEnemyPlayer(void *L) {
|
||||
DoCycle(L, Filter::EnemyPlayer);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetNearestFriendPlayer(void *L) {
|
||||
DoCycle(L, Filter::FriendPlayer);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetDirectionEnemy(void *L) {
|
||||
DoDirection(L, /*hostile*/ true,
|
||||
"Usage: TargetDirectionEnemy(facing [, coneAngle])");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetDirectionFriend(void *L) {
|
||||
DoDirection(L, /*hostile*/ false,
|
||||
"Usage: TargetDirectionFriend(facing [, coneAngle])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearest", &Script_TargetNearest);
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearestEnemyPlayer",
|
||||
&Script_TargetNearestEnemyPlayer);
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearestFriendPlayer",
|
||||
&Script_TargetNearestFriendPlayer);
|
||||
Game::Lua::RegisterGlobalFunction("TargetDirectionEnemy",
|
||||
&Script_TargetDirectionEnemy);
|
||||
Game::Lua::RegisterGlobalFunction("TargetDirectionFriend",
|
||||
&Script_TargetDirectionFriend);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Target::Nearest
|
||||
@@ -0,0 +1,86 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace UI::FrameObject {
|
||||
|
||||
namespace {
|
||||
|
||||
using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using ScriptRegister_t = void(__fastcall *)(void *this_, void *edx_unused,
|
||||
const char *name);
|
||||
|
||||
} // namespace
|
||||
|
||||
// Delegate wrapper construction to the engine's own
|
||||
// `FrameScript_Object::ScriptRegister`. First call for a never-seen frame
|
||||
// builds a `{[0] = lightuserdata}` table with the framescript metatable,
|
||||
// `luaL_ref`s it into the registry, and writes the refkey to `frame +
|
||||
// 0x08` (`OFF_COBJECT_LUA_REGISTRY_REF`). Subsequent calls find a
|
||||
// populated refkey and just rawgeti.
|
||||
//
|
||||
// Why delegate instead of building our own wrapper: single source of
|
||||
// truth. Every code path that pushes this frame — ours, addons'
|
||||
// `CreateFrame(..., parent)` extracting the frame, the engine's own
|
||||
// pushers — funnels through `rawgeti(REGISTRY, frame + 0x08)` and gets the
|
||||
// same Lua table, so addon fields set on the wrapper don't get orphaned
|
||||
// onto a divergent copy. See the long note in `nameplate/Info.cpp`'s
|
||||
// original implementation for the pfUI-divergence story that motivated
|
||||
// this.
|
||||
//
|
||||
// `ScriptRegister` unconditionally bumps the CObject refcount at `+0x04`,
|
||||
// which pins the frame against GC. That's benign for the pooled/permanent
|
||||
// frames we push here (nameplates, chat bubbles) — we call it at most
|
||||
// once per frame pointer (gated by `refKey <= 0`), so the refcount tops
|
||||
// out at one increment per pool slot.
|
||||
//
|
||||
// Defensive fallback: if the registry slot exists but isn't a table any
|
||||
// more (freed elsewhere), re-register to rebuild a fresh wrapper. This
|
||||
// shouldn't happen in practice — engines only unref on frame destruction.
|
||||
void Push(void *L, void *frame) {
|
||||
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
|
||||
auto scriptRegister = reinterpret_cast<ScriptRegister_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER));
|
||||
|
||||
auto *cobj = static_cast<uint8_t *>(frame);
|
||||
int refKey = *reinterpret_cast<const int *>(
|
||||
cobj + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
if (refKey <= 0) {
|
||||
scriptRegister(frame, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
cobj + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
}
|
||||
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
|
||||
return;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
|
||||
// Slot got freed somewhere — re-register to rebuild a fresh wrapper
|
||||
// and slot. Zero the refcount so `ScriptRegister` re-enters its build
|
||||
// branch.
|
||||
*reinterpret_cast<int *>(cobj + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0;
|
||||
scriptRegister(frame, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
cobj + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
}
|
||||
|
||||
} // namespace UI::FrameObject
|
||||
@@ -0,0 +1,34 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace UI::FrameObject {
|
||||
|
||||
// Pushes a C++ `CFrameScriptObject`-derived frame onto the Lua stack as
|
||||
// its canonical wrapper table — the same object every other engine path
|
||||
// resolves via `rawgeti(REGISTRY, frame + 0x08)`, so addon-set fields on
|
||||
// the wrapper survive roundtrips through our API and method dispatch
|
||||
// (`:GetRegions()`, `:GetName()`, …) works normally.
|
||||
//
|
||||
// For frames the engine created purely in C++ without ever going through
|
||||
// `CreateFrame` (default nameplates, chat bubbles), the registry ref at
|
||||
// `+0x08` starts at 0; the first push delegates to the engine's own
|
||||
// `FrameScript_Object::ScriptRegister` to lazily build the wrapper +
|
||||
// registry slot. Subsequent pushes just `rawgeti` the cached slot.
|
||||
//
|
||||
// Leaves exactly one value (the wrapper table, or the re-registered
|
||||
// fallback) on the stack. `frame` must be non-null.
|
||||
void Push(void *L, void *frame);
|
||||
|
||||
} // namespace UI::FrameObject
|
||||
Reference in New Issue
Block a user