mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-14 07:00:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f13c63c99 | ||
|
|
afa1f62720 | ||
|
|
c369373efe | ||
|
|
1c86d0dead | ||
|
|
cedbd04553 | ||
|
|
3b1bba2131 | ||
|
|
786428c9fb | ||
|
|
f5ecdc69c5 | ||
|
|
7531223b9a | ||
|
|
cb4e428592 | ||
|
|
e7e2fe05dc | ||
|
|
d7078827fd | ||
|
|
c571c1a852 | ||
|
|
a7b205cd56 | ||
|
|
a3f224876f | ||
|
|
a40236eb84 | ||
|
|
538beda499 | ||
|
|
af01a4b408 | ||
|
|
33c8c3e37c | ||
|
|
c5d5415afe | ||
|
|
39dcfc73de | ||
|
|
6e5f7c3e43 | ||
|
|
2e05e86362 | ||
|
|
bcc631c4ed | ||
|
|
f9fb32186d | ||
|
|
60adbba7c3 | ||
|
|
c35d883af0 | ||
|
|
c202c1000f | ||
|
|
98b4ae14e7 | ||
|
|
8d957eb424 | ||
|
|
78618ea2cc | ||
|
|
1a4583e16e | ||
|
|
a20e1206fb | ||
|
|
9f67cade87 | ||
|
|
8629e13e1f | ||
|
|
ca9bfbab5e | ||
|
|
8fdc767cc7 | ||
|
|
aa31c25be5 | ||
|
|
29aafe88f8 | ||
|
|
461f15a4a6 |
@@ -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,19 +30,19 @@ 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` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem` (script) |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemClassInfo`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
@@ -61,11 +62,13 @@ 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` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
| [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` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
@@ -144,6 +147,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_STOPPED_MOVING` | *(none)* |
|
||||
| `PLAYER_STARTED_TURNING` | *(none)* |
|
||||
| `PLAYER_STOPPED_TURNING` | *(none)* |
|
||||
| `PLAYER_TOTEM_UPDATE` | `totemSlot` |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
@@ -166,6 +170,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Addon security | `Enum.AddOnSecurityStatus.{Secure,Insecure,Banned,NotAvailable}` |
|
||||
| Power type | `Enum.PowerType.{HealthCost,None,Mana,Rage,Focus,Energy,Happiness}` |
|
||||
| Inventory type | `Enum.InventoryType.Index*Type` (0–34, e.g. `IndexHeadType`=1 … `IndexRelicType`=28) |
|
||||
| Item class | `Enum.ItemClass.{Consumable,Container,Weapon,Gem,Armor,Reagent,Projectile,Tradegoods,ItemEnhancement,Recipe,Quiver,Questitem,Key,Miscellaneous,…}` (0–19) |
|
||||
| Item quality | `Enum.ItemQuality.{Poor,Common,Uncommon,Rare,Epic,Legendary,Artifact}` (0–6) |
|
||||
|
||||
### Unit tokens
|
||||
|
||||
|
||||
+689
-41
@@ -95,7 +95,9 @@ 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)
|
||||
- [`PLAYER_EQUIPMENT_CHANGED` event](#player_equipment_changed-event)
|
||||
- [`UPDATE_INVENTORY_DURABILITY` event](#update_inventory_durability-event)
|
||||
@@ -155,9 +157,11 @@ build instructions.
|
||||
- [GameObject](#gameobject)
|
||||
- [`C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`](#c_gameobjectinfogetgameobjectinfobyidgameobjectid)
|
||||
- [`C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)`](#c_gameobjectinforequestloadgameobjectbyidgameobjectid)
|
||||
- [`ClosestGameObjectPosition(gameObjectID)`](#closestgameobjectpositiongameobjectid)
|
||||
|
||||
- [GameTooltip](#gametooltip)
|
||||
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
|
||||
- [`GameTooltip:AddSpellByID(spellID)`](#gametooltipaddspellbyidspellid)
|
||||
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
|
||||
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
|
||||
- [`GameTooltip:SetHyperlinkCompareItem("itemLink" [, offset, shiftButton, comparisonTooltip])`](#gametooltipsethyperlinkcompareitemitemlink--offset-shiftbutton-comparisontooltip)
|
||||
@@ -165,6 +169,7 @@ build instructions.
|
||||
- [`OnTooltipSetItem` script](#ontooltipsetitem-script)
|
||||
- [`GameTooltip:SetItemByGUID(itemGUID)`](#gametooltipsetitembyguiditemguid)
|
||||
- [`GameTooltip:SetEquipmentSet(name)`](#gametooltipsetequipmentsetname)
|
||||
- [`GameTooltip:SetTotem(slot)`](#gametooltipsettotemslot)
|
||||
- [`GameTooltip:GetItem()`](#gametooltipgetitem)
|
||||
- [`GameTooltip:GetSpell()`](#gametooltipgetspell)
|
||||
- [`GameTooltip:HasItem()` / `GameTooltip:HasSpell()`](#gametooltiphasitem--gametooltiphasspell)
|
||||
@@ -179,6 +184,8 @@ build instructions.
|
||||
- [`LE_UNIT_STAT_*`](#le_unit_stat_)
|
||||
- [`Enum.AddOnSecurityStatus`](#enumaddonsecuritystatus)
|
||||
- [`Enum.InventoryType`](#enuminventorytype)
|
||||
- [`Enum.ItemClass`](#enumitemclass)
|
||||
- [`Enum.ItemQuality`](#enumitemquality)
|
||||
- [`Enum.PowerType`](#enumpowertype)
|
||||
|
||||
- [Gossip](#gossip)
|
||||
@@ -231,7 +238,8 @@ build instructions.
|
||||
- [`C_Item.GetItemSpell(item)`](#c_itemgetitemspellitem)
|
||||
- [`C_Item.GetItemStatDelta(itemLink1, itemLink2)`](#c_itemgetitemstatdeltaitemlink1-itemlink2)
|
||||
- [`C_Item.GetItemStats(itemLink)`](#c_itemgetitemstatsitemlink)
|
||||
- [`C_Item.GetItemSubClassInfo(classID, subClassID)`](#c_itemgetitemsubclassinfoclassid-subclassid)
|
||||
- [`GetItemClassInfo(classID)`](#getitemclassinfoclassid)
|
||||
- [`GetItemSubClassInfo(classID, subClassID)` / `C_Item.GetItemSubClassInfo(classID, subClassID)`](#getitemsubclassinfoclassid-subclassid--c_itemgetitemsubclassinfoclassid-subclassid)
|
||||
- [`C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`](#c_itemgetitemuniquenessitemlocation--c_itemgetitemuniquenessbyiditem)
|
||||
- [`C_Item.GetStackCount(itemLocation)`](#c_itemgetstackcountitemlocation)
|
||||
- [`C_Item.IsBound(itemLocation)`](#c_itemisbounditemlocation)
|
||||
@@ -329,6 +337,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)
|
||||
@@ -440,9 +451,20 @@ 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)
|
||||
- [`C_TaxiMap.GetTaxiPaths()`](#c_taximapgettaxipaths)
|
||||
- [`C_TaxiMap.GetTaxiPathWaypoints(pathID)`](#c_taximapgettaxipathwaypointspathid)
|
||||
- [`C_TaxiMap.GetTaxiRoute(slotIndex)`](#c_taximapgettaxiroutepslotindex)
|
||||
|
||||
- [Time](#time)
|
||||
- [`GetServerTime()`](#getservertime)
|
||||
@@ -457,6 +479,12 @@ build instructions.
|
||||
- [`C_DateAndTime.GetServerTimeLocal()`](#c_dateandtimegetservertimelocal)
|
||||
- [`C_DateAndTime.GetSecondsUntilDailyReset()`](#c_dateandtimegetsecondsuntildailyreset)
|
||||
|
||||
- [Totem](#totem)
|
||||
- [`GetTotemInfo(slot)`](#gettoteminfoslot)
|
||||
- [`GetTotemTimeLeft(slot)`](#gettotemtimeleftslot)
|
||||
- [`GetTotemDuration(slot)`](#gettotemdurationslot)
|
||||
- [`TargetTotem(slot)`](#targettotemslot)
|
||||
|
||||
- [TradeSkillUI](#tradeskillui)
|
||||
- [`C_TradeSkillUI.GetTradeSkillListLink()`](#c_tradeskilluigettradeskilllistlink)
|
||||
- [`C_TradeSkillUI.GetCraftListLink()`](#c_tradeskilluigetcraftlistlink)
|
||||
@@ -482,12 +510,17 @@ build instructions.
|
||||
- [`UnitIsMinion(unit)`](#unitisminionunit)
|
||||
- [`UnitIsPet(unit)`](#unitispetunit)
|
||||
- [`UnitIsOtherPlayersPet(unit)`](#unitisotherplayerspetunit)
|
||||
- [`UnitOwnerGUID(unit)`](#unitownerguidunit)
|
||||
- [`UnitStandState(unit)`](#unitstandstateunit)
|
||||
- [`UnitInRange(unit)`](#unitinrangeunit)
|
||||
- [`UnitDistanceSquared(unit)`](#unitdistancesquaredunit)
|
||||
- [`UnitInLineOfSight(unit)`](#unitinlineofsightunit)
|
||||
- [`UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`](#unitpowerunit--powertype--unitpowermaxunit--powertype)
|
||||
- [`ClosestUnitPosition(creatureID)`](#closestunitpositioncreatureid)
|
||||
- [`UnitHealthMissing(unit)`](#unithealthmissingunit)
|
||||
- [`UnitPower(unit [, powerType [, unmodified]])` / `UnitPowerMax(unit [, powerType [, unmodified]])`](#unitpowerunit--powertype--unmodified--unitpowermaxunit--powertype--unmodified)
|
||||
- [`UnitPowerMissing(unit [, powerType [, unmodified]])`](#unitpowermissingunit--powertype--unmodified)
|
||||
- [`UnitPowerType(unit)`](#unitpowertypeunit)
|
||||
- [`UnitSpellHaste(unit)`](#unitspellhasteunit)
|
||||
|
||||
- [UnitAuras](#unitauras)
|
||||
- [`C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])`](#c_unitaurasgetauradatabyindexunit-index--filter)
|
||||
@@ -1381,17 +1414,18 @@ for bag = 0, 4 do
|
||||
end
|
||||
```
|
||||
|
||||
> **Vanilla `bagType` is sparse.** On Turtle WoW (1.12.1), only Light
|
||||
> Quiver actually carries a populated BagFamily field among the bags
|
||||
> we tested. Soul Pouch, Enchanting Bag, Herb Pouch, etc. all return
|
||||
> 0 here even though their classification clearly implies a family —
|
||||
> the vanilla server data simply doesn't fill the field for those.
|
||||
> For bag-category discrimination, prefer reading `(class, subClass)`
|
||||
> from [`C_Item.GetItemInfoInstant`](#c_itemgetiteminfoinstantitem).
|
||||
> The same field on **individual loot items** (arrows, bullets,
|
||||
> shards, herbs) IS populated reliably — use
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) for routing
|
||||
> decisions.
|
||||
> **`bagType` is derived for specialty bags.** Bags leave the raw
|
||||
> BagFamily field empty in Turtle's data (Soul Bag, Herb Bag, quiver,
|
||||
> ammo pouch, … all read 0). We fall back to deriving the family from the
|
||||
> bag's class + subclass, so a Soul Bag reports `0x4` here, a quiver
|
||||
> `0x1`, an ammo pouch `0x2` — the same values
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) reports for both
|
||||
> the bag and the items it holds. This also means
|
||||
> `CalculateTotalNumberOfFreeBagSlots` now correctly excludes specialty
|
||||
> bags from the general-purpose free-slot total (it previously counted
|
||||
> them, since the raw field was 0). Note the Turtle custom families
|
||||
> (Meat/Fish/Leather/Mining, `0x200`–`0x1000`) collide with retail's
|
||||
> bit meanings — see the `GetItemFamily` family table for the caveat.
|
||||
|
||||
> **The bitmask encoding matches modern.** `bagType` is the bit
|
||||
> position (`1 << (familyID - 1)`), not the raw 1.12-stored familyID,
|
||||
@@ -2221,6 +2255,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)
|
||||
@@ -2256,6 +2311,30 @@ the payload. The flags are derived from whether the in-game Lua state was
|
||||
just (re)built (login/`reload`) and whether we just came through the
|
||||
glue/login screen — no `PLAYER_ENTERING_WORLD`-specific engine hook.
|
||||
|
||||
### `PLAYER_TOTEM_UPDATE` event
|
||||
|
||||
Backports the TBC event (absent from vanilla's table, so reserved as a
|
||||
custom event). Fires with `arg1 = totemSlot` (1 Fire, 2 Earth, 3 Water,
|
||||
4 Air) whenever a totem is dropped, expires, or is destroyed early
|
||||
(killed / Totemic Recall) — the companion event to
|
||||
[`GetTotemInfo`](#gettoteminfoslot).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_TOTEM_UPDATE")
|
||||
f:SetScript("OnEvent", function()
|
||||
local slot = arg1
|
||||
local have, name, start, duration, icon = GetTotemInfo(slot)
|
||||
-- re-read the changed slot
|
||||
end)
|
||||
```
|
||||
|
||||
Driven by the same [`GetTotemInfo`](#gettoteminfoslot) tracker: the drop
|
||||
is detected from `SMSG_SPELL_GO`, removal from the WorldTick object-manager
|
||||
scan, and the event is flushed from the tick (so handlers run in a safe
|
||||
context, not mid-packet). Removal detection carries up to the tracker's
|
||||
scan interval (~250 ms) of latency; drops fire on the next frame.
|
||||
|
||||
### `BAG_UPDATE_DELAYED` event
|
||||
|
||||
Fires (with no payload) once per frame in which any `BAG_UPDATE`
|
||||
@@ -3356,6 +3435,17 @@ so addons get the engine semantics they expect.
|
||||
Returns `nil` cleanly when no focus is set; doesn't raise the
|
||||
"Unknown unit name" error.
|
||||
|
||||
**Unit events fire for `"focus"`.** `UNIT_HEALTH`, `UNIT_MANA`,
|
||||
`UNIT_AURA`, `UNIT_LEVEL`, `UNIT_MODEL_CHANGED`, … fire with
|
||||
`arg1 == "focus"` whenever the focused unit's corresponding descriptor
|
||||
field changes — even when it isn't your target, so a focus frame keeps
|
||||
updating while you fight something else. Backed by `Unit::TokenObserver`,
|
||||
which registers the engine's own unit-event descriptor-field observers
|
||||
for the focus unit (the exact watch the engine gives target/party/raid).
|
||||
A unit that is simultaneously your target and focus fires both
|
||||
`"target"` and `"focus"`. (Vanilla `OnEvent` handlers read the `arg1`
|
||||
global, not a function parameter.)
|
||||
|
||||
[`UnitTokenFromGUID`](#unittokenfromguidguid) scans `"focus"` right
|
||||
after `"target"` (matching retail order), so a focused unit's GUID
|
||||
reverse-resolves to `"focus"` only if it isn't already addressable
|
||||
@@ -3588,6 +3678,26 @@ end)
|
||||
C_GameObjectInfo.RequestLoadGameObjectByID(7000032)
|
||||
```
|
||||
|
||||
### `ClosestGameObjectPosition(gameObjectID)`
|
||||
|
||||
Returns `xPos, yPos, distance` — the world position of the nearest game
|
||||
object with the given GO-template ID, and its distance from the player in
|
||||
yards. Returns nothing (nil) when no matching object is in range.
|
||||
|
||||
```lua
|
||||
local x, y, dist = ClosestGameObjectPosition(1617) -- nearest Herb Bush, etc.
|
||||
if x then
|
||||
-- x, y = world coords; dist = yards from the player
|
||||
end
|
||||
```
|
||||
|
||||
The game-object counterpart of
|
||||
[`ClosestUnitPosition`](#closestunitpositioncreatureid) — same visible-
|
||||
object-manager scan, filtered to game objects (GUID prefix `0xF110`).
|
||||
Same retail-vs-vanilla caveat: this finds the nearest **currently-visible**
|
||||
object of that entry rather than reading retail's static starting-zone
|
||||
database (which vanilla ships no equivalent of).
|
||||
|
||||
## GameTooltip
|
||||
|
||||
### `GameTooltip:SetItemByID(itemID)`
|
||||
@@ -3693,6 +3803,32 @@ GameTooltip:SetSpellByID(133) -- Fireball
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
### `GameTooltip:AddSpellByID(spellID)`
|
||||
|
||||
The append counterpart to [`SetSpellByID`](#gametooltipsetspellbyidspellid):
|
||||
adds a spell's full tooltip (name, cast time, cost, range, description) to
|
||||
the *current* tooltip **without clearing** the existing lines. Works for any
|
||||
`spellID`, learned or not. Lets you compose tooltips — e.g. a header line
|
||||
plus a spell block.
|
||||
|
||||
```lua
|
||||
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
||||
GameTooltip:AddLine("Rank 3 grants:")
|
||||
GameTooltip:AddSpellByID(133) -- Fireball, appended below the header
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
Unlike `SetSpellByID`, this does **not** update what
|
||||
[`GetSpell`](#gametooltipgetspell) reports — the appended spell isn't the
|
||||
tooltip's "primary" spell, so `GetSpell` keeps reflecting whatever `SetX`
|
||||
call (if any) built the base tooltip. Silent no-op for `spellID <= 0` or an
|
||||
unknown spell.
|
||||
|
||||
> Vanilla's tooltip builder only exposes "append" via its internal talent
|
||||
> "next rank" preview, which emits a `Next rank:` header instead of the
|
||||
> spell name; we overwrite that header line with the real name so the
|
||||
> appended block reads normally.
|
||||
|
||||
### `GameTooltip:GetItem()`
|
||||
|
||||
Returns `(name, link, itemID)` for whichever item the tooltip is
|
||||
@@ -4129,6 +4265,41 @@ before the itemID field was added load fine but render their
|
||||
missing slots with the count summary `%d missing` instead of named
|
||||
lines; re-saving a set repopulates itemIDs.
|
||||
|
||||
### `GameTooltip:SetTotem(slot)`
|
||||
|
||||
Fills the tooltip with the shaman totem currently active in `slot`
|
||||
(`1` Fire, `2` Earth, `3` Water, `4` Air). A TBC (2.4.0) tooltip method
|
||||
backported to 1.12; mirrors retail's `Script_GameTooltip_SetTotem`.
|
||||
|
||||
```lua
|
||||
GameTooltip:SetOwner(TotemButton, "ANCHOR_BOTTOMRIGHT")
|
||||
GameTooltip:SetTotem(1) -- Fire slot; shows itself, no :Show() needed
|
||||
```
|
||||
|
||||
```
|
||||
Searing Totem (name, yellow)
|
||||
45 Sec (time remaining, white)
|
||||
```
|
||||
|
||||
Two lines, matching retail exactly: the totem name (`NORMAL_FONT_COLOR`)
|
||||
and the time remaining (white). Built natively with the engine's own
|
||||
per-tooltip clear and raw add-line — the same path `SetSpellByID` and
|
||||
`SetHyperlinkCompareItem` use — and **shows itself** at the end, so no
|
||||
trailing `:Show()` is required (retail's `TotemFrame.xml` OnEnter calls
|
||||
`SetOwner` then `SetTotem` with no show, and the C method shows). Set the
|
||||
owner/anchor before the call.
|
||||
|
||||
Silent no-op when the slot has no active totem, so a totem-button
|
||||
`OnEnter` can call it unconditionally.
|
||||
|
||||
**Localization.** The time line mirrors retail's `SecondsToTimeAbbrev`
|
||||
(raw seconds under a minute, minutes rounded up above) and formats
|
||||
through the FrameXML GlobalStrings `SPELL_TIME_REMAINING_SEC` /
|
||||
`SPELL_TIME_REMAINING_MIN` (English `%d Sec` / `%d Min` fallbacks for
|
||||
servers stripped of those keys). The totem name comes from the summon
|
||||
spell's localized `Spell.dbc` name. Slot data is the same tracker behind
|
||||
[`GetTotemInfo`](#gettoteminfoslot).
|
||||
|
||||
## Globals
|
||||
|
||||
### `CLASSIC_API_VERSION`
|
||||
@@ -4299,6 +4470,53 @@ post-vanilla and included for parity — vanilla items never report them.
|
||||
if C_Item.GetItemInventoryType(loc) == Enum.InventoryType.IndexHeadType then ...
|
||||
```
|
||||
|
||||
### `Enum.ItemClass`
|
||||
|
||||
The item-class enum — the numeric `classID` reported as the 12th return
|
||||
of [`GetItemInfo`](#c_itemgetiteminfoiteminfo) and taken by
|
||||
[`GetItemClassInfo`](#getitemclassinfoclassid) /
|
||||
[`GetItemSubClassInfo`](#getitemsubclassinfoclassid-subclassid). The
|
||||
numeric values match retail; the obsolete slots keep their modern key
|
||||
names even though `ItemClass.dbc` labels them `"…(OBSOLETE)"`.
|
||||
|
||||
| Value | Field | Value | Field |
|
||||
|------:|-------|------:|-------|
|
||||
| 0 | `Consumable` | 8 | `ItemEnhancement` |
|
||||
| 1 | `Container` | 9 | `Recipe` |
|
||||
| 2 | `Weapon` | 10 | `CurrencyTokenObsolete` |
|
||||
| 3 | `Gem` | 11 | `Quiver` |
|
||||
| 4 | `Armor` | 12 | `Questitem` |
|
||||
| 5 | `Reagent` | 13 | `Key` |
|
||||
| 6 | `Projectile` | 14 | `PermanentObsolete` |
|
||||
| 7 | `Tradegoods` | 15 | `Miscellaneous` |
|
||||
|
||||
Values `16..19` (`Glyph`, `Battlepet`, `WoWToken`, `Profession`) are
|
||||
post-vanilla and included for parity — vanilla items never report them.
|
||||
|
||||
```lua
|
||||
if select(12, GetItemInfo(id)) == Enum.ItemClass.Weapon then ...
|
||||
```
|
||||
|
||||
### `Enum.ItemQuality`
|
||||
|
||||
The item-quality enum — the numeric `quality` reported by
|
||||
[`GetItemInfo`](#c_itemgetiteminfoiteminfo) /
|
||||
[`C_Item.GetItemQuality`](#c_itemgetitemqualityitemlocation--c_itemgetitemqualitybyiditem).
|
||||
|
||||
| Value | Field | Value | Field |
|
||||
|------:|-------|------:|-------|
|
||||
| 0 | `Poor` | 4 | `Epic` |
|
||||
| 1 | `Common` | 5 | `Legendary` |
|
||||
| 2 | `Uncommon` | 6 | `Artifact` |
|
||||
| 3 | `Rare` | | |
|
||||
|
||||
The post-vanilla `Heirloom` (7) and `WoWToken` (8) tiers are omitted —
|
||||
no such items exist on 1.12.
|
||||
|
||||
```lua
|
||||
if select(3, GetItemInfo(id)) == Enum.ItemQuality.Epic then ...
|
||||
```
|
||||
|
||||
### `Enum.PowerType`
|
||||
|
||||
The integer enum `UnitPowerType` returns and `UnitPower` /
|
||||
@@ -4946,15 +5164,37 @@ C_Item.GetItemFamily(2447) -- 32 (Peacebloom → herb bag-family)
|
||||
C_Item.GetItemFamily(6948) -- 0 (Hearthstone → general-purpose)
|
||||
```
|
||||
|
||||
Bitmask values match modern WoW's encoding (`1 << (familyID - 1)`):
|
||||
Bitmask values follow the vanilla `ItemBagFamily.dbc` IDs converted to
|
||||
the modern bitmask form (`1 << (familyID - 1)`). The IDs 1–9 match
|
||||
retail exactly; **10–13 are Turtle WoW custom families** (retail vanilla
|
||||
1.12 stops at Keys):
|
||||
|
||||
| Bit | Value | Family |
|
||||
|-----|-------|--------|
|
||||
| 0 | 1 | Quiver (arrows) |
|
||||
| 1 | 2 | Ammo Pouch (bullets) |
|
||||
| 2 | 4 | Soul Shard Bag |
|
||||
| 5 | 32 | Herb Bag |
|
||||
| 6 | 64 | Enchanting Bag (modern) |
|
||||
| Bit | Value | Raw ID | Family | Notes |
|
||||
|-----|--------|--------|-----------------------|-------|
|
||||
| 0 | 1 | 1 | Quiver (arrows) | |
|
||||
| 1 | 2 | 2 | Ammo Pouch (bullets) | |
|
||||
| 2 | 4 | 3 | Soul Bag | |
|
||||
| 5 | 32 | 6 | Herb Bag | |
|
||||
| 6 | 64 | 7 | Enchanting Bag | |
|
||||
| 7 | 128 | 8 | Engineering Bag | |
|
||||
| 8 | 256 | 9 | Keyring | |
|
||||
| 9 | 512 | 10 | **Meat Bag** (Turtle) | ⚠ collides with retail's Gem Bag (`0x200`) |
|
||||
| 10 | 1024 | 11 | **Fish Bag** (Turtle) | ⚠ collides with retail's Mining Bag (`0x400`) |
|
||||
| 11 | 2048 | 12 | **Leather Bag** (Turtle) | no retail equivalent bit |
|
||||
| 12 | 4096 | 13 | **Mining Bag** (Turtle) | ⚠ not retail's Mining Bag bit (`0x400`) |
|
||||
|
||||
> **Turtle custom families & the retail collision.** Turtle added four
|
||||
> cooking/gathering bag families (Meat, Fish, Leather, Mining) as
|
||||
> `ItemBagFamily.dbc` rows 10–13, rather than reusing retail's later
|
||||
> IDs 4 (Leatherworking) / 5 (Inscription), which vanilla left unused.
|
||||
> Because we convert the raw ID straight through `1 << (rawID - 1)`,
|
||||
> these land on bits 9–12 — the same numeric values retail assigned to
|
||||
> **Gem Bag** (`0x200`) and **Mining Bag** (`0x400`). So an addon
|
||||
> backported from retail that hard-codes `family == 0x200` to mean "Gem
|
||||
> Bag" will misread a Turtle Meat Bag. The values are self-consistent
|
||||
> *within* Turtle (a Meat Bag and the meat items it holds both report
|
||||
> `0x200`, which is all the "does this item fit this bag" test needs) —
|
||||
> just don't assume retail's bit meanings for anything above `0x100`.
|
||||
|
||||
> **Encoding deviation under the hood.** 1.12 actually stores the raw
|
||||
> 1-based BagFamily ID (`arrow=1, bullet=2, soul shard=3, herb=6, …`).
|
||||
@@ -4963,16 +5203,20 @@ Bitmask values match modern WoW's encoding (`1 << (familyID - 1)`):
|
||||
> callers backporting from modern see the encoding they expect — addons
|
||||
> can `band(family, FAMILY_BAG_HERB_BAG)` directly.
|
||||
|
||||
> **Item-level data is reliable; bag-level is sparse.** Individual
|
||||
> loot items (arrows, bullets, shards, herbs) carry the field
|
||||
> reliably, so `C_Item.GetItemFamily` works for the auto-routing use
|
||||
> case. **Bag containers** themselves (Soul Pouch, Enchanting Bag,
|
||||
> Herb Pouch, etc.) leave the field at 0 in vanilla server data — at
|
||||
> least on Turtle WoW. If you need to know "is this bag specialty",
|
||||
> read the bag's `subClass` via
|
||||
> [`C_Item.GetItemInfoInstant`](#c_itemgetiteminfoinstantitem)
|
||||
> instead. The Quiver class is fully populated; everything else needs
|
||||
> the subClass fallback.
|
||||
> **Legacy Blizzard bags derive from `(class, subClass)`.** The stock
|
||||
> vanilla bags shipped with an empty `m_bagFamily` field — the four
|
||||
> profession bags (Soul Bag, Herb Bag, Enchanting Bag, Engineering Bag,
|
||||
> all Container class) *and* quivers/ammo pouches (Quiver class). For a
|
||||
> bag whose field is `0` we recover the family from its class + subclass
|
||||
> — the same signal the `"24 Slot Soul Bag"` tooltip line is built from.
|
||||
> A Felcloth Bag reports `0x4` (Soul Shards), a quiver `0x1` (arrows), an
|
||||
> ammo pouch `0x2` (bullets) — each matching the items it holds. Keyed on
|
||||
> class because subclass numbers repeat across classes (Container
|
||||
> subclass 2 = Herb Bag, Quiver subclass 2 = Quiver). This is automatic;
|
||||
> you no longer need to read `subClass` yourself. (Turtle's own custom
|
||||
> bags — Meat/Fish/Leather/Mining — populate the field directly; a
|
||||
> Turtle-specific resolver module supplies their mapping as a safety net,
|
||||
> kept separate from the stock table.)
|
||||
|
||||
> **`nil` vs `0`.** Modern WoW returns `0` for items the cache lookup
|
||||
> fails on; we return `nil` so callers can distinguish "item not
|
||||
@@ -5513,13 +5757,31 @@ what the item actually grants — not just its stored stat slots:
|
||||
bare itemID (a superset of retail, which is link-only). Returns `nil` if
|
||||
the item isn't cached yet — warm it via `GetItemInfo` and retry.
|
||||
|
||||
### `C_Item.GetItemSubClassInfo(classID, subClassID)`
|
||||
### `GetItemClassInfo(classID)`
|
||||
|
||||
Returns `subClassName, subClassUsesInvType` for an item class/subclass pair.
|
||||
Returns `className` — the localized `ItemClass.dbc` name for an item
|
||||
class ID (the `classID` from `Enum.ItemClass` / the 12th `GetItemInfo`
|
||||
return). Also available as `C_Item.GetItemClassInfo(classID)`.
|
||||
|
||||
```lua
|
||||
C_Item.GetItemSubClassInfo(2, 7) -- "One-Handed Swords", false
|
||||
C_Item.GetItemSubClassInfo(4, 1) -- "Cloth", true
|
||||
GetItemClassInfo(2) -- "Weapon"
|
||||
GetItemClassInfo(4) -- "Armor"
|
||||
GetItemClassInfo(1) -- "Container"
|
||||
```
|
||||
|
||||
Returns `nil` for an unknown class. Obsolete slots return the client's
|
||||
literal string (`GetItemClassInfo(3)` → `"Jewelry(OBSOLETE)"`), which is
|
||||
the real vanilla data — use [`Enum.ItemClass`](#enumitemclass) for the
|
||||
modern key names.
|
||||
|
||||
### `GetItemSubClassInfo(classID, subClassID)` / `C_Item.GetItemSubClassInfo(classID, subClassID)`
|
||||
|
||||
Returns `subClassName, subClassUsesInvType` for an item class/subclass pair
|
||||
(the global form documents the second value as `isArmorType` — same value).
|
||||
|
||||
```lua
|
||||
GetItemSubClassInfo(2, 7) -- "One-Handed Swords", false
|
||||
GetItemSubClassInfo(4, 1) -- "Cloth", true
|
||||
```
|
||||
|
||||
- `subClassName` (string) — the localized `ItemSubClass.dbc` name (verbose
|
||||
@@ -7863,6 +8125,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
|
||||
@@ -8007,11 +8305,14 @@ for i = 1, 40 do
|
||||
end
|
||||
```
|
||||
|
||||
Ordering is **creation-order** — each new plate appends to the end of
|
||||
the list and stays at its index until the unit goes out of range or
|
||||
the plate is removed, at which point later indices shift down.
|
||||
Stable for the lifetime of a single plate; no mid-frame reordering.
|
||||
Same semantics as modern WoW.
|
||||
Indices are **assigned slots**, matching modern WoW exactly: a plate keeps
|
||||
its slot for its entire lifetime, so surviving plates are **never
|
||||
renumbered** when another plate is removed. A removed plate frees its slot,
|
||||
and the next new plate reuses the lowest free slot. So a middle plate
|
||||
vanishing leaves the others' `nameplateN` tokens unchanged (that slot simply
|
||||
becomes vacant until a new plate reuses it) — `UnitExists("nameplateN")`
|
||||
returns `false` for a currently-free slot. Stable for the lifetime of a
|
||||
single plate; no reordering, ever.
|
||||
|
||||
Token chains work too — `"nameplate1target"`, `"nameplate1targettarget"`,
|
||||
etc. — by mirroring the engine's own `targettarget`-style suffix
|
||||
@@ -8022,6 +8323,15 @@ own walker either, so they don't compose.
|
||||
Out-of-range indices return `nil` cleanly without raising "Unknown
|
||||
unit name" — `UnitExists("nameplate99")` just returns `false`.
|
||||
|
||||
**Unit events fire for `"nameplateN"`.** Like party/raid tokens,
|
||||
`UNIT_HEALTH` / `UNIT_AURA` / `UNIT_LEVEL` / … fire with
|
||||
`arg1 == "nameplateN"` when a nameplated unit's descriptor field changes
|
||||
(vanilla only watched its own target/party/raid units). Backed by
|
||||
`Unit::TokenObserver` — observers are registered per plate on
|
||||
`NAME_PLATE_UNIT_ADDED` and torn down on `_REMOVED`; the changed GUID is
|
||||
resolved to its *current* index at fire time, since indices shift as
|
||||
plates vanish. Note the per-plate observer cost in very large scenes.
|
||||
|
||||
**Implementation note.** We hook `FUN_TOKEN_TO_GUID` (the central
|
||||
token→GUID resolver) so the entire `Script_Unit*` surface gains the
|
||||
new token form transparently. The hook is gated by an `SStrCmpI`
|
||||
@@ -10786,6 +11096,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
|
||||
@@ -10900,6 +11278,74 @@ and player-relative (its whole point is the open master's reachable set),
|
||||
which is why a static database like pfQuest uses `GetTaxiNodesForMap`
|
||||
instead.
|
||||
|
||||
### `C_TaxiMap.GetTaxiPaths()`
|
||||
|
||||
Returns every flight path (directed edge) in `TaxiPath.dbc` as an array of
|
||||
tables — the taxi routing graph. No flight master needed; reads the DBC
|
||||
directly.
|
||||
|
||||
```lua
|
||||
local paths = C_TaxiMap.GetTaxiPaths()
|
||||
-- paths[i] = { pathID = 94, fromNodeID = 8, toNodeID = 16, cost = 110 }
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `pathID` | number | `TaxiPath.dbc` id — pass to `GetTaxiPathWaypoints` |
|
||||
| `fromNodeID` | number | source `TaxiNodes.dbc` id |
|
||||
| `toNodeID` | number | destination `TaxiNodes.dbc` id |
|
||||
| `cost` | number | flight cost in copper (the routing weight) |
|
||||
|
||||
Directed — `A→B` and `B→A` are two separate entries with their own path IDs
|
||||
and geometry. Combine with `GetTaxiNodesForMap` (the node table) to build a
|
||||
weighted graph, and with `GetTaxiPathWaypoints` to measure real distances.
|
||||
|
||||
### `C_TaxiMap.GetTaxiPathWaypoints(pathID)`
|
||||
|
||||
Returns the waypoint polyline of one flight path as an array of world
|
||||
coordinates, ordered by node index (flight order).
|
||||
|
||||
```lua
|
||||
local wp = C_TaxiMap.GetTaxiPathWaypoints(94)
|
||||
-- wp[i] = { x = -8851.66, y = 498.55, z = 111.26, mapID = 0 }
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|-------|------|---------|
|
||||
| `x`, `y`, `z` | number | world position (yards) |
|
||||
| `mapID` | number | continent (`Map.dbc` id) |
|
||||
|
||||
Returns nothing for an unknown `pathID` or an empty path. Sum the 3D distances
|
||||
between consecutive waypoints for the path's length; vanilla taxi flight
|
||||
speed is 32 yd/s, so `length / 32 ≈ flight seconds` for a single hop.
|
||||
(Multi-hop trips are shorter than the sum of their segments — the server cuts
|
||||
the corner at each intermediate flight master via data not present in any
|
||||
client file, so a client-side estimate of a chained route is an upper bound.)
|
||||
|
||||
### `C_TaxiMap.GetTaxiRoute(slotIndex)`
|
||||
|
||||
Returns the node chain the client would actually fly to a taxi-map slot, as an
|
||||
array of `TaxiNodes.dbc` IDs in flight order (`{ currentNodeID, …, destNodeID }`).
|
||||
`slotIndex` is the legacy `1..NumTaxiNodes()` index (the same one
|
||||
`TaxiNodeName`/`TakeTaxiNode` take). **Only meaningful while the taxi map is
|
||||
open.**
|
||||
|
||||
```lua
|
||||
local route = C_TaxiMap.GetTaxiRoute(slotIndex)
|
||||
-- route = { 8, 16, 43, 66 } (Thelsamar -> Refuge Pointe -> Aerie Peak -> Chillwind)
|
||||
```
|
||||
|
||||
This is the exact chain `TakeTaxiNode(slotIndex)` would send to the server —
|
||||
mirrors the client's own routing decision: a single direct hop when a `TaxiPath`
|
||||
edge exists (`{ current, dest }`), otherwise the client's precomputed multi-hop
|
||||
route (which, per character, may detour around flight points you haven't
|
||||
discovered). Returns nothing when the map is closed, the slot is out of range,
|
||||
it's the current node, or no route exists.
|
||||
|
||||
Pairs with `GetTaxiPaths` (each consecutive `(from, to)` names a `pathID`) and
|
||||
`GetTaxiPathWaypoints` (that path's geometry) to measure a chained trip without
|
||||
matching flight-map pixel coordinates back to nodes.
|
||||
|
||||
## Time
|
||||
|
||||
### `GetServerTime()`
|
||||
@@ -11079,6 +11525,97 @@ wall-clock time). Returns 0 if called before login.
|
||||
> midnight, not UTC midnight (though they happen to coincide for
|
||||
> Turtle WoW since the server reports UTC-aligned components).
|
||||
|
||||
## Totem
|
||||
|
||||
### `GetTotemInfo(slot)`
|
||||
|
||||
Returns `haveTotem, totemName, startTime, duration, icon, modRate, spellID`
|
||||
for one of the four shaman totem slots (`1` Fire, `2` Earth, `3` Water,
|
||||
`4` Air).
|
||||
|
||||
```lua
|
||||
local _, name, start, duration, icon = GetTotemInfo(1) -- Fire slot
|
||||
if name ~= "" then -- a totem IS active
|
||||
local remaining = duration - (GetTime() - start)
|
||||
-- name = "Searing Totem", icon = "Interface\\Icons\\...", etc.
|
||||
end
|
||||
```
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `haveTotem` | `true` if the player carries the slot's **totem tool** (Fire/Earth/Water/Air Totem item) — the documented meaning, **not** whether a totem is summoned. |
|
||||
| `totemName` | The active totem spell's localized name (`""` when no totem is out — this is how you test "is a totem active"). |
|
||||
| `startTime` | `GetTime()` value when the totem was cast (`0` when none). |
|
||||
| `duration` | Total duration in seconds (`0` when none). |
|
||||
| `icon` | The active totem spell's icon texture path, or `nil` when none. |
|
||||
| `modRate` | Always `1` — vanilla has no per-totem haste. |
|
||||
| `spellID` | The active summon spell's ID (`0` when none). |
|
||||
|
||||
> **`haveTotem` is tool presence, not summon state.** Matching the live
|
||||
> API: `haveTotem` reflects whether you hold the slot's totem **tool** item
|
||||
> (`Fire Totem` 5176, `Earth Totem` 5175, `Water Totem` 5177, `Air Totem`
|
||||
> 5178) in bags/equipped, independent of whether a totem is currently
|
||||
> deployed. The tool item is the totem spell's `Totem[0]` requirement (a
|
||||
> *tool*, not a consumed reagent), read from the spell — not hardcoded. To
|
||||
> check for an *active* totem, test `totemName ~= ""` (or `startTime > 0`).
|
||||
|
||||
Vanilla 1.12 has no client-side totem tracking (the totem bar,
|
||||
`PLAYER_TOTEM_UPDATE`, and `GetTotemInfo` are all TBC additions), so this
|
||||
is self-tracked but **data-driven**: the slot is read from the summon
|
||||
spell's `Spell.dbc` effect (`SUMMON_TOTEM_SLOT1..4` = Fire/Earth/Water/Air),
|
||||
duration from `SpellDuration.dbc`, the totem's creature entry (for death
|
||||
detection) from the effect's `EffectMiscValue`, and the `haveTotem` tool
|
||||
item from the spell's `Totem` field (the totem the spell requires) —
|
||||
resolved from the player's spellbook, not a hardcoded table. Casts are observed
|
||||
via the `SMSG_SPELL_GO` hook; a slot clears when its duration elapses OR
|
||||
when the totem creature disappears early (killed, Totemic Recall, or
|
||||
destroyed) — detected by scanning the object manager for the player-owned
|
||||
totem creature. Because it's data-driven, Turtle-custom totems are tracked
|
||||
automatically. Slot changes fire
|
||||
[`PLAYER_TOTEM_UPDATE`](#player_totem_update-event).
|
||||
|
||||
### `GetTotemTimeLeft(slot)`
|
||||
|
||||
Returns the seconds remaining before the slot's totem is auto-destroyed,
|
||||
or `0` when no totem is active in that slot. Slots: `1` Fire, `2` Earth,
|
||||
`3` Water, `4` Air.
|
||||
|
||||
```lua
|
||||
local remaining = GetTotemTimeLeft(1) -- Fire slot, e.g. 12.4
|
||||
```
|
||||
|
||||
Computed from the same tracker as [`GetTotemInfo`](#gettoteminfoslot)
|
||||
(`startTime + duration − now`), so it shares that data's caveats — most
|
||||
notably it reflects the totem's *full* duration from cast, unaffected by
|
||||
any server-side early expiry the client isn't told about (the WorldTick
|
||||
scan still clears the slot when the totem creature actually disappears).
|
||||
|
||||
### `GetTotemDuration(slot)`
|
||||
|
||||
Returns the slot's active totem's **total** duration in seconds (its full
|
||||
lifetime from cast), or `0` when no totem is active. The companion to
|
||||
[`GetTotemTimeLeft`](#gettotemtimeleftslot) (remaining time) — same value
|
||||
as `GetTotemInfo`'s 4th return.
|
||||
|
||||
```lua
|
||||
local total = GetTotemDuration(1) -- e.g. 60
|
||||
local frac = GetTotemTimeLeft(1) / GetTotemDuration(1) -- bar fill
|
||||
```
|
||||
|
||||
### `TargetTotem(slot)`
|
||||
|
||||
Targets the player's totem in the given slot (`1` Fire, `2` Earth, `3`
|
||||
Water, `4` Air). No-op when the slot has no active totem or the totem
|
||||
creature isn't currently visible in the client.
|
||||
|
||||
```lua
|
||||
TargetTotem(2) -- target your Earth totem
|
||||
```
|
||||
|
||||
Looks up the totem creature's live GUID (object-manager scan for the
|
||||
player-owned creature of the slot's tracked entry) and sets the target
|
||||
through the same engine path `TargetUnit` uses (`CMSG_SET_SELECTION`).
|
||||
|
||||
## TradeSkillUI
|
||||
|
||||
Backports the 2.x+ **trade-skill list link** — the shareable
|
||||
@@ -11561,6 +12098,20 @@ players (no owner) return `false`; your own minions (owner is you) return
|
||||
UnitIsOtherPlayersPet("target") -- a party member's pet → true; your own pet → false
|
||||
```
|
||||
|
||||
### `UnitOwnerGUID(unit)`
|
||||
|
||||
Returns the GUID string (`"0x…"`) of the unit's owner — the summoner of a
|
||||
pet/guardian/totem, or the charmer of a charmed unit. `nil` for an
|
||||
unresolved unit and for anything with no owner (players, world creatures).
|
||||
|
||||
```lua
|
||||
local owner = UnitOwnerGUID("pettarget") -- e.g. the enemy hunter's GUID
|
||||
if owner == UnitGUID("player") then ... end -- is it my minion?
|
||||
```
|
||||
|
||||
Reads the same owner field the pet predicates use (`CharmedBy`, else
|
||||
`CreatedBy` in the unit's `m_objectFields`).
|
||||
|
||||
### `UnitStandState(unit)`
|
||||
|
||||
Returns the unit's standstate as an integer, matching the modern
|
||||
@@ -11771,7 +12322,58 @@ from the DBC). Reads byte 0 of `UNIT_FIELD_BYTES_0` (descriptor
|
||||
Returns `(nil, nil)` for unresolvable units and non-player units
|
||||
(creature race bytes don't index `ChrRaces.dbc`).
|
||||
|
||||
### `UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`
|
||||
### `ClosestUnitPosition(creatureID)`
|
||||
|
||||
Returns `xPos, yPos, distance` — the world position of the nearest
|
||||
creature with the given NPC (creature-template) ID, and its distance from
|
||||
the player in yards. Returns nothing (nil) when no matching creature is in
|
||||
range.
|
||||
|
||||
```lua
|
||||
local x, y, dist = ClosestUnitPosition(3098) -- nearest Kobold Vermin
|
||||
if x then
|
||||
-- x, y = world coords; dist = yards from the player
|
||||
end
|
||||
```
|
||||
|
||||
- `creatureID` (number) — the NPC ID (the value
|
||||
[`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
|
||||
returns, and the entry packed into a creature's GUID).
|
||||
|
||||
Walks the client's visible-object manager, matches creatures whose GUID
|
||||
encodes `creatureID` (bits 24–47), and returns the closest one's position
|
||||
+ center-to-center distance.
|
||||
|
||||
> **Differs from retail.** Modern WoW's `ClosestUnitPosition` reads a
|
||||
> static client-side spawn database and only works for starting-zone mobs.
|
||||
> Vanilla 1.12 ships no such database, so this returns the nearest
|
||||
> **currently-visible** creature of that entry (anywhere, not just
|
||||
> starting zones). It can't point at un-synced spawns elsewhere in the
|
||||
> zone the way retail's database can — but for "where's the nearest `<mob>`
|
||||
> I can see" it's more general than retail.
|
||||
|
||||
### `UnitHealthMissing(unit)`
|
||||
|
||||
The health deficit — `UnitHealthMax(unit) - UnitHealth(unit)` — in one call.
|
||||
A convenience for healing addons (overheal math, "missing health" bars) that
|
||||
would otherwise call both engine functions and subtract in Lua every frame.
|
||||
|
||||
```lua
|
||||
local missing = UnitHealthMissing("player") -- 0 at full health
|
||||
```
|
||||
|
||||
Returns `0` at full health and for a valid-but-absent unit (e.g. `"target"`
|
||||
with nothing targeted), matching `UnitHealth`'s 0-for-missing convention;
|
||||
clamped so a transient `current > max` never returns negative.
|
||||
|
||||
Tracks the engine's own `UnitHealth` / `UnitHealthMax` exactly, including
|
||||
vanilla's percentage form for non-grouped units (where `UnitHealthMax` is
|
||||
`100`): a target at `85/100` reports `15`.
|
||||
|
||||
Direct descriptor reads — `desc[+0x40]` (HEALTH), `desc[+0x58]` (MAXHEALTH).
|
||||
No engine call, no Lua-stack roundtrip.
|
||||
|
||||
### `UnitPower(unit [, powerType [, unmodified]])` / `UnitPowerMax(unit [, powerType [, unmodified]])`
|
||||
|
||||
Modern multi-power-type getters. Vanilla 1.12 only ships
|
||||
`UnitMana(unit)` / `UnitManaMax(unit)` which return whichever
|
||||
@@ -11828,10 +12430,35 @@ the divisor table at `0x0086F978`:
|
||||
So a fresh warrior reads `UnitPower("player", 1)` = `0..100`, not
|
||||
`0..1000`. Matches retail Classic.
|
||||
|
||||
Pass a truthy `unmodified` (third arg) to bypass the divisor and get the raw
|
||||
internal value — `UnitPower("player", 1, true)` returns rage as `0..1000`.
|
||||
Same third argument retail's `UnitPower` / `UnitPowerMax` take.
|
||||
|
||||
Direct descriptor reads — `desc[+0x44 + type*4]` for current power,
|
||||
`desc[+0x5C + type*4]` for max, divided by the table entry for the
|
||||
type. No engine call, no Lua-stack roundtrip.
|
||||
|
||||
### `UnitPowerMissing(unit [, powerType [, unmodified]])`
|
||||
|
||||
The power deficit — `UnitPowerMax(...) - UnitPower(...)` for the given type —
|
||||
in one call. The power analogue of [`UnitHealthMissing`](#unithealthmissingunit),
|
||||
for energy/mana/rage "resource needed" checks. Same `unit` / `powerType`
|
||||
handling and invalid-unit-returns-`0` semantics as `UnitPower`.
|
||||
|
||||
```lua
|
||||
local energyNeeded = UnitPowerMissing("player", Enum.PowerType.Energy)
|
||||
local rageMissing = UnitPowerMissing("player") -- primary power
|
||||
```
|
||||
|
||||
`unmodified` (arg 3, any truthy value) skips the display divisor and returns
|
||||
the raw internal deficit — the same third argument retail's `UnitPower` takes.
|
||||
|
||||
Computed as the difference of the two **display** values (each integer-divided
|
||||
by the per-type divisor), not by dividing the raw difference, so it equals
|
||||
`UnitPowerMax(...) - UnitPower(...)` exactly. Those diverge for rage: raw
|
||||
`1000/55` displays as `100/5`, a deficit of `95`, whereas `(1000-55)/10`
|
||||
truncates to `94`. Clamped at 0.
|
||||
|
||||
### `UnitPowerType(unit)`
|
||||
|
||||
Vanilla 1.12 ships this returning just the integer power type;
|
||||
@@ -11865,6 +12492,27 @@ Chains to the engine's original `Script_UnitPowerType` at
|
||||
manager lookup with pet / totem / vehicle fallbacks), then reads
|
||||
the just-pushed integer and appends the token string.
|
||||
|
||||
### `UnitSpellHaste(unit)`
|
||||
|
||||
Backport of the TBC+ spell-haste getter — vanilla 1.12 has no haste API.
|
||||
Returns the spell haste **percentage**: `0` for an unhasted unit, positive
|
||||
when casting is sped up, negative when slowed (Curse of Tongues).
|
||||
|
||||
```lua
|
||||
local haste = UnitSpellHaste("player") -- 0 for most vanilla casters
|
||||
```
|
||||
|
||||
Reads `UNIT_MOD_CAST_SPEED` — the cast-time multiplier at descriptor `+0x22c`
|
||||
that the server folds into `SpellEntry::GetCastTime`
|
||||
(`castTime *= modCastSpeed`) — and converts it to a percentage:
|
||||
`haste% = (1 / modCastSpeed - 1) * 100`. So `modCastSpeed` `1.0` → `0`,
|
||||
`0.5` (half cast time) → `100`, `>1.0` → negative.
|
||||
|
||||
This is the Blizzard-shaped surface over the field nampower exposes raw as
|
||||
`GetUnitField(unit, "modCastSpeed")`, so addons can drop that dependency.
|
||||
Consumers that need the raw multiplier back can recover it as
|
||||
`1 / (1 + UnitSpellHaste(unit) / 100)`. Returns `0` for invalid units.
|
||||
|
||||
## UnitAuras
|
||||
|
||||
Backport of the modern `C_UnitAuras` namespace. Returns
|
||||
|
||||
+136
@@ -454,6 +454,39 @@ enum Offsets {
|
||||
// pointer the inventory routines expect.
|
||||
FUN_RESOLVE_UNIT_TOKEN = 0x00515940,
|
||||
|
||||
// Set the player's current target to a GUID —
|
||||
// `__fastcall(uint64_t *guid)`. Validates the GUID resolves to a unit
|
||||
// (typemask 8) then commits the selection (CMSG_SET_SELECTION +
|
||||
// client-side target). This is what `Script_TargetUnit` (0x004899D0)
|
||||
// calls after resolving its token→GUID; we call it directly with a
|
||||
// GUID we already have (e.g. a totem creature). NOTE: reads `guid[0]`
|
||||
// (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
|
||||
@@ -631,6 +664,25 @@ enum Offsets {
|
||||
// The live (new) value is NOT passed — read it from the object (the
|
||||
// engine's bag callback reads the invMgr GUID array).
|
||||
|
||||
// ---- Unit-event token observers (Unit::TokenObserver) ---------------
|
||||
// The engine makes target/party/raid/pet/mouseover units fire per-token
|
||||
// unit events (UNIT_HEALTH, UNIT_MANA, UNIT_AURA, …) by watching bank-UNIT
|
||||
// descriptor fields: FUN_0051bbb0 loops event index i in
|
||||
// [0, EVENT_NAME_UNIT_MAX); for each whose static name slot is non-null it
|
||||
// registers a bank-3 observer on field i*4 (per-index size below) with
|
||||
// callback FUN_0051bd50 → the broadcast FUN_00515e50(guid, i), which fires
|
||||
// event i once per token referencing the guid. The FIELD INDEX IS THE EVENT
|
||||
// ID (health field +0x40 → event 16 = UNIT_HEALTH). Unit::TokenObserver
|
||||
// replicates that exact loop with a caller's own callback to make synthetic
|
||||
// tokens (focus, nameplateN) first-class; the registrar always appends a
|
||||
// fresh node (FUN_00467f00, no dedup), so ours coexists with the engine's —
|
||||
// a unit that is also the target fires both "target" and our token. The
|
||||
// broadcast always fires with format "%s" + the token (DAT_0082e280).
|
||||
// Per-index watched size (FUN_0051bbb0's switch): i∈{0,2,4,10}=8,
|
||||
// 0x29=0xD8 (UNIT_FIELD_AURA), 0x6B=0x30, 0xA7/0xAE=0x1C, else 4.
|
||||
VAR_EVENT_NAME_TABLE_STATIC = 0x00BE1198, // char*[] boot name array; slot i (base+i*4) non-null ⇒ real event i
|
||||
EVENT_NAME_UNIT_MAX = 0xB6, // FUN_0051bbb0's loop bound
|
||||
|
||||
// The engine's inventory observer setup — registers the bag-slot
|
||||
// GUID-field observers above, once per enter-world (sole caller is the
|
||||
// enter-world initializer FUN_004908C0, latched by DAT_00B4B424).
|
||||
@@ -646,6 +698,7 @@ enum Offsets {
|
||||
OFF_DESC_PLAYER_EQUIP_FIRST = 0x4A8,
|
||||
DESC_PLAYER_EQUIP_SLOTS = 19,
|
||||
DESC_OBSERVER_BANK_PLAYER = 4,
|
||||
DESC_OBSERVER_BANK_UNIT = 3, // CGUnit descriptor bank (FUN_0051bbb0's watch loop)
|
||||
|
||||
// ITEM_FIELD_DURABILITY, ITEM-bank-relative: descriptor-absolute
|
||||
// +0xA0 (field index 0x28, which counts the 6 OBJECT fields) minus
|
||||
@@ -773,6 +826,15 @@ enum Offsets {
|
||||
// mana/rage/focus/energy/happiness order as POWER1..5). Vanilla
|
||||
// 1.12 has 5 power types; the WotLK additions (Runes / Runic Power)
|
||||
// don't exist in this descriptor layout.
|
||||
|
||||
// UNIT_MOD_CAST_SPEED — the cast-time multiplier float (1.0 = normal,
|
||||
// <1.0 = faster). The server folds it into `SpellEntry::GetCastTime`
|
||||
// (`castTime *= GetFloatValue(UNIT_MOD_CAST_SPEED)`); the client reads it
|
||||
// in `FUN_006e3340` at descriptor `+0x22c`. Backs `UnitSpellHaste`
|
||||
// (`haste% = (1/mult - 1) * 100`). nampower surfaces the raw float as its
|
||||
// `modCastSpeed` unit field.
|
||||
OFF_UNIT_MOD_CAST_SPEED = 0x22C,
|
||||
|
||||
UNIT_POWER_MIN_TYPE = 0,
|
||||
UNIT_POWER_MAX_TYPE = 4, // happiness; types 5/6 only valid post-WotLK
|
||||
|
||||
@@ -1467,6 +1529,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
|
||||
@@ -2396,6 +2467,29 @@ enum Offsets {
|
||||
// "DISTANT" / other) — the node's reachability for the open session.
|
||||
FUN_TAXI_NODE_TYPE = 0x004DC4E0,
|
||||
|
||||
// Per-slot computed route (the chain that TakeTaxiNode sends to the
|
||||
// server). Derived from FUN_004dc8d0 (the TakeTaxiNode worker) and the
|
||||
// route builder FUN_004dbce0:
|
||||
// +0x0C (byte) nonzero when the multi-hop route array below is built
|
||||
// +0x14 (int*) array of TaxiNodes.dbc IDs, current->dest in flight
|
||||
// order, length at +0x18
|
||||
// +0x18 (int) route length (node count; hops = count - 1)
|
||||
// A direct hop (a TaxiPath edge exists current->dest) is flown as just
|
||||
// {current, dest} and does NOT populate +0x14 — see VAR_TAXI_ADJACENCY.
|
||||
OFF_TAXI_SLOT_MULTIHOP = 0x0C,
|
||||
OFF_TAXI_SLOT_ROUTE = 0x14,
|
||||
OFF_TAXI_SLOT_ROUTE_COUNT = 0x18,
|
||||
|
||||
// Pointer to the CURRENT flight master's TaxiNodes.dbc record (id @ +0);
|
||||
// the origin of every route. Used by FUN_004dc8d0 / FUN_004dbce0.
|
||||
VAR_TAXI_CURRENT_REC = 0x00BB4A80,
|
||||
|
||||
// Node x node -> pathID adjacency matrix (256-wide rows, int cells; cell
|
||||
// [from*0x100 + to] is the direct TaxiPath id, or 0 if none). Backs the
|
||||
// direct-hop test FUN_004dbad0. IDs are 1..256.
|
||||
VAR_TAXI_ADJACENCY = 0x00B7365C,
|
||||
TAXI_ADJACENCY_DIM = 0x100,
|
||||
|
||||
// TaxiPath.dbc (4 fields, 0x10 bytes): {id, fromNode@+0x04, toNode@+0x08,
|
||||
// cost}. A real flight master is a node some flight path connects to;
|
||||
// Northshire Abbey / spurious duplicate rows / standalone markers are not
|
||||
@@ -2404,6 +2498,23 @@ enum Offsets {
|
||||
VAR_TAXIPATH_COUNT = 0x00C0D69C,
|
||||
OFF_TAXIPATH_FROM = 0x04,
|
||||
OFF_TAXIPATH_TO = 0x08,
|
||||
OFF_TAXIPATH_COST = 0x0C,
|
||||
|
||||
// TaxiPathNode.dbc — the per-path flight waypoints. 1-based
|
||||
// record-pointer array (records/count slots per docs/DBCs.md). Record
|
||||
// (verified against the extracted DBC + the loading-screen spline
|
||||
// reader FUN_004075A0, which reads mapID@+0xC, X@+0x10, Y@+0x14):
|
||||
// {ID@+0, pathID@+4, nodeIndex@+8, mapID@+0xC,
|
||||
// X@+0x10, Y@+0x14, Z@+0x18 (float), flags@+0x1C, delay@+0x20}
|
||||
// Waypoints of a path share pathID and order by nodeIndex.
|
||||
VAR_TAXIPATHNODE_RECORDS = 0x00C0D6AC,
|
||||
VAR_TAXIPATHNODE_COUNT = 0x00C0D6B0,
|
||||
OFF_TAXIPATHNODE_PATH_ID = 0x04,
|
||||
OFF_TAXIPATHNODE_INDEX = 0x08,
|
||||
OFF_TAXIPATHNODE_MAP = 0x0C,
|
||||
OFF_TAXIPATHNODE_X = 0x10,
|
||||
OFF_TAXIPATHNODE_Y = 0x14,
|
||||
OFF_TAXIPATHNODE_Z = 0x18,
|
||||
|
||||
// Engine player-info cache — populated by the
|
||||
// SMSG_NAME_QUERY_RESPONSE handler (opcode 0x51) at 0x005551A0,
|
||||
@@ -3905,6 +4016,10 @@ enum Offsets {
|
||||
// the frame into the matching entry's chain.
|
||||
VAR_EVENT_TABLE_BASE_PTR = 0x00CEEF68,
|
||||
VAR_EVENT_TABLE_COUNT = 0x00CEEF64,
|
||||
// The FrameScript_EventObject is a `{cap@+0, count@+4, data@+8}` at
|
||||
// 0x00CEEF60, so COUNT/BASE above are its +4/+8. `Event::Custom::Grow`
|
||||
// writes all three (base, cap, count) when it enlarges the table.
|
||||
VAR_EVENT_TABLE_CAP = 0x00CEEF60,
|
||||
EVENT_ENTRY_STRIDE = 0x10,
|
||||
OFF_EVENT_ENTRY_NAME = 0x00,
|
||||
OFF_EVENT_ENTRY_HEAD = 0x0C,
|
||||
@@ -3945,6 +4060,17 @@ enum Offsets {
|
||||
// table after the rebuild settles. Kept for reference.
|
||||
FUN_REBUILD_EVENT_TABLE = 0x00703D90,
|
||||
|
||||
// Event-table allocator — `__thiscall(EventObject *this, uint32 count)`.
|
||||
// Called by `RebuildEventTable` to (re)size the entry array. SuperWoW and
|
||||
// nampower co-hook it to destructively clamp the FrameXML count (`> 200`;
|
||||
// glue is smaller) to 700. We deliberately DON'T hook it: our default is
|
||||
// claim-NULL into the engine's ~200 low gaps (which always suffices), and
|
||||
// when a grow IS needed `Event::Custom::Grow` reallocates the buffer +
|
||||
// swaps the globals directly (bypassing this clamp) rather than routing a
|
||||
// size through here. Also documents the empty-entry init the grow mirrors
|
||||
// ({name=0, +4=0, +8=&self, +0xC=(&self)|1}). Kept for reference.
|
||||
FUN_FRAMESCRIPT_SET_EVENT_COUNT = 0x007053B0,
|
||||
|
||||
// `FireEvent` — the engine's event dispatcher. 149 callers in the
|
||||
// binary. `__cdecl void(int eventID, const char *format, ...)`.
|
||||
// Indexes into `[VAR_EVENT_TABLE_BASE_PTR] + eventID * 0x10` and walks
|
||||
@@ -4158,6 +4284,16 @@ enum Offsets {
|
||||
// from `SMemAlloc` (which it requires).
|
||||
FUN_STORM_SSTRDUP = 0x0064A620,
|
||||
|
||||
// Storm allocator/free — the pair the engine uses for the event-table
|
||||
// buffer, so its reload teardown `SMemFree`s our grown buffer as its own.
|
||||
// Both `__stdcall`, 4 args (`ret 0x10`); SMemAlloc flag bit 8 (`& 8`)
|
||||
// zero-fills. `Event::Custom::Grow` uses them to reallocate the event
|
||||
// buffer. (Same addresses as FUN_STORM_SMEM_ALLOC / _FREE above.)
|
||||
// void* __stdcall SMemAlloc(uint size, const char *file, int line, int flags)
|
||||
// char __stdcall SMemFree (void *ptr, const char *file, int line, int flags)
|
||||
FUN_STORM_SMEMALLOC = 0x006462E0,
|
||||
FUN_STORM_SMEMFREE = 0x00646430,
|
||||
|
||||
// Talent system — per-player talent state populated at login from
|
||||
// (class, race) + Talent.dbc / TalentTab.dbc. The engine maintains
|
||||
// one `TabInfo *` per talent tab in the array at
|
||||
|
||||
+21
-28
@@ -132,30 +132,19 @@ const Override *FindOverride(uint32_t spellId) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Known values for spells whose duration row DANGLES — a non-zero
|
||||
// DurationIndex pointing at a SpellDuration row the client doesn't have.
|
||||
// Consulted ONLY when that exact condition holds, so the gate is the bug
|
||||
// itself, not a server fingerprint: on a stock client every one of these
|
||||
// resolves to a valid row and this table is never reached, and if the
|
||||
// broken client data is ever fixed the DBC row takes over automatically.
|
||||
//
|
||||
// Turtle's reworked Rip is the known case: all six ranks point at
|
||||
// SpellDuration row 87, which Turtle added on the SERVER only
|
||||
// ({8000, 0, 18000} — live server data shows 10/12/14/16/18s at 1..5 CP,
|
||||
// i.e. 8s + 2s per combo point); their client patch never shipped the
|
||||
// row (the client table jumps 86 -> 105). Verified the only dangling
|
||||
// duration index in the entire client Spell.dbc.
|
||||
constexpr Override kDanglingRowKnown[] = {
|
||||
{1079, 8000, 18000}, {9492, 8000, 18000}, {9493, 8000, 18000},
|
||||
{9752, 8000, 18000}, {9894, 8000, 18000}, {9896, 8000, 18000},
|
||||
};
|
||||
// Resolver chain for dangling-duration-row spells (client DBC missing the
|
||||
// row). The KNOWN VALUES themselves are server-custom data and live in
|
||||
// their own module (e.g. `src/turtle/ComboDuration.cpp` for Turtle's
|
||||
// reworked Rip); this stock module only owns the mechanism and the
|
||||
// dangling-condition gate. Chained at static-init by `AutoDanglingResolver`.
|
||||
AutoDanglingResolver *g_danglingResolvers = nullptr;
|
||||
|
||||
const Override *FindDanglingRowKnown(uint32_t spellId) {
|
||||
for (const auto &k : kDanglingRowKnown) {
|
||||
if (k.spellId == spellId)
|
||||
return &k;
|
||||
}
|
||||
return nullptr;
|
||||
bool ResolveDangling(uint32_t spellId, int32_t *baseMs, int32_t *maxMs) {
|
||||
for (AutoDanglingResolver *r = g_danglingResolvers; r != nullptr;
|
||||
r = r->next)
|
||||
if (r->fn(spellId, baseMs, maxMs))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// `C_UnitAuras.RegisterComboDuration(spellID, baseSeconds, maxSeconds)`
|
||||
@@ -203,6 +192,11 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoDanglingResolver::AutoDanglingResolver(DanglingResolver f)
|
||||
: fn(f), next(g_danglingResolvers) {
|
||||
g_danglingResolvers = this;
|
||||
}
|
||||
|
||||
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
|
||||
if (spellRecord == nullptr || spellId == 0)
|
||||
return 0;
|
||||
@@ -227,12 +221,11 @@ uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
|
||||
maxMs = *reinterpret_cast<const int32_t *>(row + 0xC);
|
||||
} else if (idx != 0) {
|
||||
// Dangling duration index — the client data is broken for this
|
||||
// spell. Fall back to the built-in known values (Turtle Rip).
|
||||
const Override *k = FindDanglingRowKnown(spellId);
|
||||
if (k == nullptr)
|
||||
// spell. Defer to a registered resolver (the known values are
|
||||
// server-custom data owned by a platform module, e.g.
|
||||
// src/turtle for Turtle's reworked Rip).
|
||||
if (!ResolveDangling(spellId, &baseMs, &maxMs))
|
||||
return 0;
|
||||
baseMs = k->baseMs;
|
||||
maxMs = k->maxMs;
|
||||
} else {
|
||||
return 0; // no duration row at all
|
||||
}
|
||||
|
||||
@@ -47,4 +47,22 @@ namespace Aura::ComboDuration {
|
||||
// duration path".
|
||||
uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId);
|
||||
|
||||
// Extension point for finishers whose SpellDuration row DANGLES — a
|
||||
// non-zero DurationIndex pointing at a row the client DBC doesn't have.
|
||||
// A resolver returns true and fills *baseMs/*maxMs if it knows the spell,
|
||||
// false otherwise. Consulted by `TryComboScaledMs` ONLY in that dangling
|
||||
// condition (after any Lua-registered override), so the activation gate is
|
||||
// the client-data bug itself, not a server fingerprint — the deliberate
|
||||
// design choice (a realm-sniffing approach was rejected). Server-custom
|
||||
// data (e.g. Turtle's reworked Rip) registers its values here instead of
|
||||
// living in this stock module: declare a file-scope
|
||||
// `static const Aura::ComboDuration::AutoDanglingResolver`.
|
||||
using DanglingResolver = bool (*)(uint32_t spellId, int32_t *baseMs,
|
||||
int32_t *maxMs);
|
||||
struct AutoDanglingResolver {
|
||||
explicit AutoDanglingResolver(DanglingResolver fn);
|
||||
DanglingResolver fn;
|
||||
AutoDanglingResolver *next;
|
||||
};
|
||||
|
||||
} // namespace Aura::ComboDuration
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "totem/Tracker.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -230,6 +231,11 @@ void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
if (caster == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
// Feed the totem tracker BEFORE the aura gate below — a totem summon
|
||||
// applies no aura, so it would otherwise be dropped. Player casts only.
|
||||
if (caster == Unit::Identity::PlayerGuid())
|
||||
Totem::Tracker::OnPlayerSpellGo(spellId);
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
@@ -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
|
||||
+21
-22
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
|
||||
@@ -73,15 +74,14 @@ BagInfo ResolveBagInfo(int bagID) {
|
||||
|
||||
const int slots = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS));
|
||||
// Convert 1.12's raw BagFamily ID to the modern bitmask. See
|
||||
// `Offsets::OFF_ITEMSTATS_BAG_FAMILY` for the encoding-shift story
|
||||
// and `Item::Info::BagFamilyIdToBitmask` for the canonical helper —
|
||||
// we inline the one-liner here to avoid a header dance for a
|
||||
// single-instruction conversion.
|
||||
const uint32_t rawFamily = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
// BagFamily bitmask: the item's field if populated, else derived from
|
||||
// the container subclass — Turtle leaves bags' m_bagFamily at 0, so a
|
||||
// Soul Bag would otherwise report family 0 and get miscounted as a
|
||||
// general-purpose bag by CalculateTotalNumberOfFreeBagSlots. Shared
|
||||
// with C_Item.GetItemFamily so a bag and its contents agree. See
|
||||
// `Item::BagFamily`.
|
||||
const int family =
|
||||
(rawFamily == 0) ? 0 : static_cast<int>(1u << (rawFamily - 1));
|
||||
static_cast<int>(Item::BagFamily::BitmaskForRecord(record));
|
||||
return {slots, family};
|
||||
}
|
||||
|
||||
@@ -106,14 +106,13 @@ int CountFreeSlotsInBag(void *L, int bagID, int slotCount) {
|
||||
// `(0, 0)` — matching 3.3.5's behavior of always pushing two values
|
||||
// rather than nil/nothing.
|
||||
//
|
||||
// `bagType` is the `BagFamily` bitmask in modern encoding (raw 1.12
|
||||
// ID converted via `1 << (ID-1)`). Empirically on Turtle WoW, only
|
||||
// Light Quiver actually carries a non-zero family for bags themselves
|
||||
// — Soul Pouch, Enchanting Bag, etc. all return 0 because the
|
||||
// vanilla-era server data doesn't populate the field. **Individual
|
||||
// items** (arrows, bullets, shards, herbs) reliably return their
|
||||
// family, so addons that auto-route loot work correctly via
|
||||
// `C_Item.GetItemFamily(itemID)` even though `bagType` is sparse.
|
||||
// `bagType` is the `BagFamily` bitmask in modern encoding. Bags leave the
|
||||
// raw family field empty in Turtle's data (Soul/Herb/quiver/ammo pouch/…
|
||||
// all read 0), so it's derived from the bag's class + subclass via
|
||||
// `Item::BagFamily`: a Soul Bag reports `0x4`, a quiver `0x1`, an ammo
|
||||
// pouch `0x2` — matching what `C_Item.GetItemFamily` reports for both the
|
||||
// bag and the items it holds. See that helper for the Turtle custom-family
|
||||
// caveat (Meat/Fish/Leather/Mining, IDs 10–13).
|
||||
int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -139,12 +138,12 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Caveat inherited from `GetContainerNumFreeSlots`:
|
||||
// vanilla-era server data leaves most bags' BagFamily at 0, so specialty
|
||||
// bags that retail *would* exclude (Soul Pouch, Enchanting Bag, …) get
|
||||
// counted here — the client just doesn't carry the family bit for them. The
|
||||
// result stays consistent with what `GetContainerNumFreeSlots` reports for
|
||||
// those same bags.
|
||||
// Takes no arguments. Specialty bags are correctly excluded: their family
|
||||
// comes from `Item::BagFamily` (container-subclass fallback), so a Soul
|
||||
// Bag / Herb Bag / Meat Bag reports a non-zero family and its slots are
|
||||
// left out of the general-purpose total — matching retail's FrameXML
|
||||
// behavior. (An earlier version miscounted them because Turtle leaves the
|
||||
// raw family field at 0 for bags.)
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
|
||||
@@ -88,12 +88,27 @@ void Persist() {
|
||||
} // namespace
|
||||
|
||||
void EnsureLoaded() {
|
||||
if (g_loaded)
|
||||
return;
|
||||
g_path = Storage::ResolveFilePath();
|
||||
if (g_path.empty())
|
||||
return; // session globals not yet populated; retry on next call
|
||||
// Re-resolve the per-character path on every call rather than latching
|
||||
// once. The injected DLL persists across a logout→character-select→login
|
||||
// (no process restart), so a permanent `g_loaded` latch would leave the
|
||||
// previous character's file path and cached sets in place — the next
|
||||
// character would then read, and save into, character A's data. Detecting
|
||||
// a path change here makes the cache self-heal on any character/account
|
||||
// switch. Every mutation Persist()s immediately, so dropping the old
|
||||
// in-memory sets never loses unsaved work.
|
||||
std::string path = Storage::ResolveFilePath();
|
||||
if (path.empty())
|
||||
return; // session globals not yet populated; keep current, retry later
|
||||
if (g_loaded && path == g_path)
|
||||
return; // already loaded for this character
|
||||
|
||||
g_path = std::move(path);
|
||||
g_sets.clear();
|
||||
Storage::Load(g_path, &g_sets);
|
||||
// Per-session "ignore this slot on next save" state belongs to the
|
||||
// character it was set on — clear it when switching characters.
|
||||
for (int i = 0; i < SLOT_COUNT; ++i)
|
||||
g_ignoredForSave[i] = false;
|
||||
g_loaded = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,11 @@ namespace EquipmentSet::Storage {
|
||||
// Resolves the per-character file path. Returns an empty string until
|
||||
// the engine has populated all three session globals (account, realm,
|
||||
// character). Path format: `WTF\Account\<acct>\<realm>\<char>\
|
||||
// ClassicAPI_EquipmentSets.txt`. Once the engine has the player in
|
||||
// world the values are stable for the rest of the session, so callers
|
||||
// can cache after the first non-empty return.
|
||||
// ClassicAPI_EquipmentSets.txt`. The values are stable only while the
|
||||
// current character is in world — they change on a logout→login to a
|
||||
// different character (the DLL is NOT reloaded across that transition),
|
||||
// so callers must re-resolve and compare rather than caching the first
|
||||
// non-empty return permanently. See `Data::EnsureLoaded`.
|
||||
std::string ResolveFilePath();
|
||||
|
||||
// Reads the file at `path` and overwrites `outSets` with the parsed
|
||||
|
||||
@@ -40,11 +40,12 @@
|
||||
// `C_EquipmentSet.GetItemIDs` + `C_EquipmentSet.GetItemLocations`
|
||||
// themselves.
|
||||
//
|
||||
// Tooltip filling is done by calling back through Lua (`self:AddLine`
|
||||
// etc.) rather than hitting the engine's tooltip-line internals
|
||||
// directly. The Lua dispatch goes through the engine's own frame-
|
||||
// method registry, so any addon-installed hook on AddLine (DBM,
|
||||
// pfUI's tooltip skin, etc.) still runs.
|
||||
// Tooltip lines are built with the engine's own primitives — the
|
||||
// per-tooltip clear (FUN_GAMETOOLTIP_CLEAR) and the raw add-line
|
||||
// (FUN_GAMETOOLTIP_ADD_LINE), the same native path SetTotem and
|
||||
// SetHyperlinkCompareItem use; no Lua method dispatch. It shows itself
|
||||
// at the end (Script_Show), matching retail: PaperDollFrame.lua sets the
|
||||
// anchor then calls SetEquipmentSet with no trailing :Show().
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -58,57 +59,41 @@ namespace EquipmentSet::Tooltip {
|
||||
|
||||
namespace {
|
||||
|
||||
// Calls `self:Method()` with no Lua args. Stack at entry must have
|
||||
// the tooltip at index 1 (the standard `frame:method()` self slot).
|
||||
// Leaves the stack unchanged.
|
||||
void CallSelfNoArgs(void *L, const char *methodName) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::PushString(L, methodName);
|
||||
Game::Lua::GetTable(L, -2); // method = self[methodName]
|
||||
Game::Lua::Insert(L, -2); // [method, self]
|
||||
Game::Lua::Call(L, 1, 0);
|
||||
using AddLine_t = void(__thiscall *)(void *self, const char *left, const char *right,
|
||||
const void *leftColor, const void *rightColor, int wrap);
|
||||
using ClearTooltip_t = void(__fastcall *)(void *self);
|
||||
using ShowScript_t = int(__fastcall *)(void *L); // engine's Script_Show
|
||||
|
||||
// Packs r,g,b (0..1) into the 0xAARRGGBB value FUN_GAMETOOLTIP_ADD_LINE's
|
||||
// color arg wants — its little-endian bytes are the {b,g,r,a} the engine's
|
||||
// line-color setter reads. Alpha is always opaque.
|
||||
uint32_t Pack(double r, double g, double b) {
|
||||
auto ch = [](double v) {
|
||||
const int i = static_cast<int>(v * 255.0 + 0.5);
|
||||
return static_cast<uint32_t>(i < 0 ? 0 : (i > 255 ? 255 : i));
|
||||
};
|
||||
return 0xFF000000u | (ch(r) << 16) | (ch(g) << 8) | ch(b);
|
||||
}
|
||||
|
||||
// Calls `self:AddLine(text, r, g, b)`. Same self-on-stack precondition.
|
||||
void AddLine(void *L, const char *text, double r, double g, double b) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::PushString(L, "AddLine");
|
||||
Game::Lua::GetTable(L, -2);
|
||||
Game::Lua::Insert(L, -2);
|
||||
Game::Lua::PushString(L, text);
|
||||
Game::Lua::PushNumber(L, r);
|
||||
Game::Lua::PushNumber(L, g);
|
||||
Game::Lua::PushNumber(L, b);
|
||||
Game::Lua::Call(L, 5, 0);
|
||||
// Left-aligned colored line via the engine's raw add-line. The packed
|
||||
// color is a local so its address stays valid across the call.
|
||||
void AddColoredLine(void *self, const char *text, double r, double g, double b) {
|
||||
const uint32_t color = Pack(r, g, b);
|
||||
reinterpret_cast<AddLine_t>(Offsets::FUN_GAMETOOLTIP_ADD_LINE)(
|
||||
self, text, nullptr, &color, nullptr, 0);
|
||||
}
|
||||
|
||||
// Calls `self:SetText(text, r, g, b)`. Same precondition. Used for
|
||||
// the header line — `SetText` is the standard vanilla entry for
|
||||
// "first line of a fresh tooltip" and handles internal redraw state
|
||||
// `AddLine` skips when it's the first content after a clear.
|
||||
void SetText(void *L, const char *text, double r, double g, double b) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::PushString(L, "SetText");
|
||||
Game::Lua::GetTable(L, -2);
|
||||
Game::Lua::Insert(L, -2);
|
||||
Game::Lua::PushString(L, text);
|
||||
Game::Lua::PushNumber(L, r);
|
||||
Game::Lua::PushNumber(L, g);
|
||||
Game::Lua::PushNumber(L, b);
|
||||
Game::Lua::Call(L, 5, 0); // self + text + r + g + b
|
||||
}
|
||||
|
||||
// Format-and-add one localized count line. Calls
|
||||
// `Game::Lua::PushLocalizedFormatInt` to resolve the format string
|
||||
// (Blizzard's FrameXML globals like `ITEMS_EQUIPPED` first, with the
|
||||
// C `fallback` for stripped-down servers), then dispatches `AddLine`.
|
||||
void AddLocalizedLineInt(void *L, const char *globalName, const char *fallback, int n,
|
||||
double r, double g, double b) {
|
||||
// Adds one localized "N unit" line: `string.format(_G[globalName] or
|
||||
// fallback, n)` (the project's GlobalString localize-or-fallback helper,
|
||||
// with the C `fallback` for servers stripped of the standard strings),
|
||||
// then the native colored add-line.
|
||||
void AddLocalizedLineInt(void *L, void *self, const char *globalName,
|
||||
const char *fallback, int n, double r, double g, double b) {
|
||||
const int savedTop = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedFormatInt(L, globalName, fallback, n);
|
||||
const char *text = Game::Lua::ToString(L, -1);
|
||||
if (text != nullptr)
|
||||
AddLine(L, text, r, g, b);
|
||||
AddColoredLine(self, text, r, g, b);
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
@@ -185,39 +170,36 @@ int __fastcall Script_GameTooltipSetEquipmentSet(void *L) {
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
if (self == nullptr)
|
||||
return 0;
|
||||
|
||||
const Tally t = TallySet(*s);
|
||||
const int totalSlots = t.equipped + t.inInventory + t.missing;
|
||||
|
||||
// Trim the Lua stack to just the tooltip self-arg so our internal
|
||||
// method-call sequences don't accumulate the original args past
|
||||
// their useful lifetime. `setName` was a `const char *` lifted out
|
||||
// by ToString — Lua's strings are pinned by reference count, so
|
||||
// dropping the string from the stack doesn't invalidate the
|
||||
// pointer for the lifetime of this call.
|
||||
Game::Lua::SetTop(L, 1);
|
||||
|
||||
// Header — set name, white. Doubles as the "ClearLines" boundary;
|
||||
// SetText replaces line 1 and clears subsequent lines in vanilla.
|
||||
SetText(L, s->name.c_str(), 1.0, 1.0, 1.0);
|
||||
// Header — set name, white. Clear first so it lands as line 0
|
||||
// (the clear resets the line count the add-line body indexes from).
|
||||
reinterpret_cast<ClearTooltip_t>(Offsets::FUN_GAMETOOLTIP_CLEAR)(self);
|
||||
AddColoredLine(self, s->name.c_str(), 1.0, 1.0, 1.0);
|
||||
|
||||
// Body lines — try Blizzard's localized FORMAT_* globals first.
|
||||
// Same key names retail FrameXML uses (`ITEMS_VARIABLE_QUANTITY`,
|
||||
// `ITEMS_EQUIPPED`, ...); addons that need their own wording can
|
||||
// override by reassigning `_G[name]` at runtime. The fallbacks
|
||||
// only fire on servers stripped of the standard GlobalStrings.
|
||||
AddLocalizedLineInt(L, "ITEMS_VARIABLE_QUANTITY", "%d items",
|
||||
AddLocalizedLineInt(L, self, "ITEMS_VARIABLE_QUANTITY", "%d items",
|
||||
totalSlots, 1.0, 1.0, 1.0);
|
||||
|
||||
if (t.equipped > 0) {
|
||||
AddLocalizedLineInt(L, "ITEMS_EQUIPPED", "%d equipped",
|
||||
AddLocalizedLineInt(L, self, "ITEMS_EQUIPPED", "%d equipped",
|
||||
t.equipped, 0.0, 1.0, 0.0); // green
|
||||
}
|
||||
if (t.inInventory > 0) {
|
||||
AddLocalizedLineInt(L, "ITEMS_IN_INVENTORY", "%d in inventory",
|
||||
AddLocalizedLineInt(L, self, "ITEMS_IN_INVENTORY", "%d in inventory",
|
||||
t.inInventory, 1.0, 1.0, 1.0);
|
||||
}
|
||||
if (t.ignored > 0) {
|
||||
AddLocalizedLineInt(L, "ITEM_SLOTS_IGNORED", "%d slots ignored",
|
||||
AddLocalizedLineInt(L, self, "ITEM_SLOTS_IGNORED", "%d slots ignored",
|
||||
t.ignored, 0.5, 0.5, 0.5); // gray
|
||||
}
|
||||
// Missing slots: list each by name using the itemID we stored at
|
||||
@@ -255,18 +237,20 @@ int __fastcall Script_GameTooltipSetEquipmentSet(void *L) {
|
||||
Game::Lua::Call(L, 2, 1);
|
||||
const char *line = Game::Lua::ToString(L, -1);
|
||||
if (line != nullptr)
|
||||
AddLine(L, line, 1.0, 0.0, 0.0); // red
|
||||
AddColoredLine(self, line, 1.0, 0.0, 0.0); // red
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
if (unnamedMissing > 0) {
|
||||
// Pre-itemID-format files or items the cache has no record of.
|
||||
// No Blizzard format global maps cleanly here; addons can
|
||||
// override with `_G.CLASSICAPI_EQUIPMENTSET_MISSING`.
|
||||
AddLocalizedLineInt(L, "CLASSICAPI_EQUIPMENTSET_MISSING",
|
||||
AddLocalizedLineInt(L, self, "CLASSICAPI_EQUIPMENTSET_MISSING",
|
||||
"%d missing", unnamedMissing, 1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
CallSelfNoArgs(L, "Show");
|
||||
// Show ourselves — self is still at Lua stack index 1 (Script_Show
|
||||
// reads it there). Retail's SetEquipmentSet shows internally too.
|
||||
reinterpret_cast<ShowScript_t>(Offsets::FUN_SCRIPT_FRAME_SHOW)(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+178
-7
@@ -46,6 +46,22 @@ struct ReservedName {
|
||||
ReservedName g_reserved[MAX_RESERVED];
|
||||
int g_reservedCount = 0;
|
||||
bool g_writesEnabled = false;
|
||||
// One-shot latch so a genuinely full event table (no NULL slot left to
|
||||
// claim) is logged once per session instead of silently dropping the
|
||||
// event — the failure mode that would otherwise rot unnoticed. Re-armed on
|
||||
// `/reload` (the table is rebuilt fresh). In practice this never triggers:
|
||||
// SuperWoW/nampower clamp the FrameXML table to 700 and the engine leaves
|
||||
// ~200 NULL gaps in the low range, dwarfing our ~46 reservations. The log is
|
||||
// purely a backstop if some future install somehow drains the gap pool.
|
||||
bool g_warnedTableFull = false;
|
||||
|
||||
// One grow attempt per in-game table build. Reset in `PrepareForReload`
|
||||
// (the table is rebuilt fresh on every /reload + logout). See `Grow`.
|
||||
bool g_growLatched = false;
|
||||
// Debug: force a grow on the next table build regardless of headroom, so
|
||||
// the (otherwise dormant) grow path can actually be exercised in-game.
|
||||
// Armed by `_classicapi_ForceEventGrow`, consumed by the next `Grow`.
|
||||
bool g_forceGrowNext = false;
|
||||
|
||||
// Engine's SStrDup at `FUN_STORM_SSTRDUP` — uses `SMemAlloc` internally
|
||||
// and is exactly what the engine itself does when filling event table
|
||||
@@ -61,14 +77,38 @@ char *SStrDup(const char *s) {
|
||||
return fn(s, __FILE__, __LINE__);
|
||||
}
|
||||
|
||||
// Walk the engine's event table from the END looking for a NULL-name
|
||||
// Storm allocator/free — the same pair the engine uses for the event-table
|
||||
// buffer (see RebuildEventTable), so its reload teardown `SMemFree`s our
|
||||
// grown buffer exactly as its own. Both `__stdcall`, 4 args (`ret 0x10`).
|
||||
// SMemAlloc flag bit 8 zero-fills (matching SetEventCount's fresh entries).
|
||||
void *SMemAlloc(unsigned size) {
|
||||
using SMemAlloc_t = void *(__stdcall *)(unsigned size, const char *file,
|
||||
int line, int flags);
|
||||
auto fn = reinterpret_cast<SMemAlloc_t>(Offsets::FUN_STORM_SMEMALLOC);
|
||||
return fn(size, __FILE__, __LINE__, 8 /* zero-fill */);
|
||||
}
|
||||
void SMemFree(void *ptr) {
|
||||
using SMemFree_t = int(__stdcall *)(void *ptr, const char *file, int line,
|
||||
int flags);
|
||||
auto fn = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEMFREE);
|
||||
fn(ptr, __FILE__, __LINE__, 0);
|
||||
}
|
||||
|
||||
// Walk the engine's event table from the LOW end looking for a NULL-name
|
||||
// slot. The bulk-init at `0x0051AB30` leaves ~200 NULL gaps scattered
|
||||
// through the 548-slot input names array (it only writes ~350 of the
|
||||
// slots explicitly; the rest stay zero-initialized from .data). UP
|
||||
// and DOWN walks both find these gaps — we go DOWN for convention with
|
||||
// VanillaMinimapTracking and so the dump in `_classicapi_DumpAllEvents`
|
||||
// shows our events grouped at the tail, visually distinct from engine
|
||||
// events.
|
||||
// slots explicitly; the rest stay zero-initialized from .data) — a big
|
||||
// contiguous hole around 42..106 alone dwarfs our reservation count.
|
||||
//
|
||||
// We go LOW (ascending) deliberately: SuperWoW and nampower own the HIGH
|
||||
// range (fixed event IDs 549..655, and they destructively clamp the table
|
||||
// to 700), and other claim-based DLLs (VanillaMinimapTracking) also walk
|
||||
// from the tail — so the top is the contested end. Claiming from the low
|
||||
// engine gaps keeps us entirely out of their way and independent of their
|
||||
// clamp / hook order. The engine's ~200 low NULL gaps dwarf our ~46
|
||||
// reservations, so a free slot is essentially always available; the
|
||||
// skip-occupied invariant keeps this safe against future engine additions
|
||||
// regardless of direction.
|
||||
int TryClaim(const char *eventName) {
|
||||
if (!g_writesEnabled)
|
||||
return -1;
|
||||
@@ -76,7 +116,7 @@ int TryClaim(const char *eventName) {
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
for (int i = count - 1; i >= 0; --i) {
|
||||
for (int i = 0; i < count; ++i) {
|
||||
auto **namePtr = reinterpret_cast<const char **>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
@@ -88,9 +128,119 @@ int TryClaim(const char *eventName) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Writes are enabled and the table is valid, but every slot is taken —
|
||||
// real capacity exhaustion despite `Grow`. Warn once so it's diagnosable.
|
||||
if (!g_warnedTableFull) {
|
||||
g_warnedTableFull = true;
|
||||
Debug::Log::Printf(
|
||||
"[event] table full (%d slots, no NULL left) — cannot claim '%s'"
|
||||
" and any further custom events; even Grow couldn't make room.",
|
||||
count, eventName ? eventName : "?");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// On-demand event-table grow. Correctly TIMED: called once per in-game
|
||||
// table build from `RetryClaims` (the `frame:RegisterEvent` hook), on the
|
||||
// FIRST such call — which is AFTER `RebuildEventTable` has built the fresh
|
||||
// table (frames can only register once it exists) but BEFORE any frame has
|
||||
// joined a chain (it's the first registration, and we run before the
|
||||
// original registers it). So every chain is still an empty self-sentinel,
|
||||
// and the buffer can be moved safely.
|
||||
//
|
||||
// (This is what the removed `EnsureCapacity` got wrong: it ran from
|
||||
// `LoadScriptFunctions_h`, BEFORE `RebuildEventTable`, so it saw the stale
|
||||
// previous/glue table and always aborted — it never actually grew. The
|
||||
// realloc logic itself was fine; only the timing was broken.)
|
||||
//
|
||||
// Grows only when the engine's low NULL gaps are fewer than our
|
||||
// reservations — which never happens in practice (~200 gaps vs ~46
|
||||
// reservations), so this is a dormant durability valve. `g_forceGrowNext`
|
||||
// (debug) forces it so the path can be exercised. It grows by reallocating
|
||||
// the entry buffer with the engine's own Storm allocator and swapping the
|
||||
// `{cap@0, count@4, base@8}` globals at `0x00CEEF60` — bypassing
|
||||
// SetEventCount's destructive `= 700` clamp. All consumers (RegisterEvent,
|
||||
// the dispatcher, SuperWoW, nampower) read the base pointer fresh, so the
|
||||
// swap is picked up everywhere; the engine's reload teardown `SMemFree`s
|
||||
// our buffer and frees the (shared) name strings via its per-entry helper,
|
||||
// exactly as its own.
|
||||
void Grow() {
|
||||
if (g_growLatched || !g_writesEnabled)
|
||||
return;
|
||||
g_growLatched = true; // one attempt per build, whether or not it grows
|
||||
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
if (base == nullptr || count <= 0 || g_reservedCount <= 0)
|
||||
return;
|
||||
|
||||
int nulls = 0;
|
||||
for (int i = 0; i < count; ++i)
|
||||
if (*reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
Offsets::OFF_EVENT_ENTRY_NAME) == nullptr)
|
||||
++nulls;
|
||||
|
||||
const bool force = g_forceGrowNext;
|
||||
g_forceGrowNext = false;
|
||||
if (nulls >= g_reservedCount && !force)
|
||||
return; // plenty of room — the common (only) real case
|
||||
|
||||
// Growing moves the buffer, invalidating every entry's self-referential
|
||||
// list anchor, so it's safe ONLY while all chains are empty. We run
|
||||
// before the first registration, so that holds — but VERIFY it (a live
|
||||
// chain means someone registered earlier via a path we didn't intercept;
|
||||
// moving the buffer would corrupt their subscription). Abort if so and
|
||||
// fall back to claim-NULL.
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint32_t head = *reinterpret_cast<const uint32_t *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
if (head != 0 && (head & 1) == 0) { // populated chain (not 0 / sentinel)
|
||||
Debug::Log::Printf(
|
||||
"[event] grow aborted — chain live at slot %d (not first-reg)", i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int kMargin = 16;
|
||||
const int deficit = (nulls < g_reservedCount) ? (g_reservedCount - nulls) : 0;
|
||||
const int newCount = count + deficit + kMargin;
|
||||
auto *newBuf = static_cast<uint8_t *>(
|
||||
SMemAlloc(static_cast<unsigned>(newCount) * Offsets::EVENT_ENTRY_STRIDE));
|
||||
if (newBuf == nullptr)
|
||||
return; // alloc failed — keep the old table; claiming still works
|
||||
|
||||
// Rebuild each entry at its NEW address, mirroring SetEventCount's own
|
||||
// per-entry init: copy the name pointer (shared string, not duplicated);
|
||||
// the `+0x04` field stays 0 (zero-filled, matching the engine); re-init
|
||||
// the anchor as an empty self-sentinel at the new location. Old entries
|
||||
// past the copy and all new entries are already zero-filled + get fresh
|
||||
// anchors. Safe because every chain is empty.
|
||||
for (int i = 0; i < newCount; ++i) {
|
||||
auto *e = reinterpret_cast<uint32_t *>(
|
||||
newBuf + i * Offsets::EVENT_ENTRY_STRIDE);
|
||||
if (i < count) // copy name; new slots stay null
|
||||
e[0] = *reinterpret_cast<const uint32_t *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE);
|
||||
const uint32_t anchor = static_cast<uint32_t>(
|
||||
reinterpret_cast<uintptr_t>(&e[2])); // entry+0x08
|
||||
e[2] = anchor; // self-referential list anchor
|
||||
e[3] = anchor | 1; // empty-chain sentinel head (+0x0C)
|
||||
}
|
||||
|
||||
// Publish base BEFORE count so the globals never advertise more entries
|
||||
// than the buffer holds. Then free the OLD buffer only — the name
|
||||
// strings live on in newBuf and are freed by the reload teardown.
|
||||
*reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR) = newBuf;
|
||||
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_CAP) = newCount;
|
||||
*reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT) = newCount;
|
||||
SMemFree(base);
|
||||
|
||||
Debug::Log::Printf("[event] grew table %d -> %d (%d NULL, %d reserved%s)",
|
||||
count, newCount, nulls, g_reservedCount,
|
||||
force ? ", FORCED" : "");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoReserve::AutoReserve(const char *name) {
|
||||
@@ -133,6 +283,11 @@ int LookupByName(const char *name) {
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
// Grow the table first if the low gap pool is short (dormant valve).
|
||||
// Runs at most once per table build; on the first call every chain is
|
||||
// still empty (this is the first registration), which is the only safe
|
||||
// time to move the buffer.
|
||||
Grow();
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
g_reserved[i].slot = TryClaim(g_reserved[i].name);
|
||||
@@ -145,6 +300,8 @@ void PrepareForReload() {
|
||||
for (int i = 0; i < g_reservedCount; ++i)
|
||||
g_reserved[i].slot = -1;
|
||||
g_writesEnabled = false;
|
||||
g_warnedTableFull = false; // fresh table after the rebuild — re-arm
|
||||
g_growLatched = false; // re-arm the one-shot grow for the new table
|
||||
}
|
||||
|
||||
// Diagnostic Lua functions — registered via the auto-register pattern
|
||||
@@ -190,10 +347,24 @@ int __fastcall Script_DumpAllEvents(void *) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `_classicapi_ForceEventGrow()` — arms a one-shot forced table grow for
|
||||
// the NEXT in-game table build, so the (otherwise dormant) grow path can be
|
||||
// exercised: call it, then `/reload`. The grow fires at the first
|
||||
// `RegisterEvent` after the rebuild (chains still empty) and logs
|
||||
// `[event] grew table N -> M (..., FORCED)`. Diagnostic only.
|
||||
int __fastcall Script_ForceEventGrow(void *) {
|
||||
g_forceGrowNext = true;
|
||||
Debug::Log::Printf(
|
||||
"[event] force-grow armed — /reload to trigger it on the next build");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterDiagnostics() {
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_DumpSlot", &Script_DumpSlot);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_DumpAllEvents",
|
||||
&Script_DumpAllEvents);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_ForceEventGrow",
|
||||
&Script_ForceEventGrow);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterDiagnostics};
|
||||
|
||||
@@ -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
|
||||
@@ -282,7 +282,25 @@ void RegisterLuaFunctions() {
|
||||
&Script_C_FriendList_IsWhoQueryPending);
|
||||
}
|
||||
|
||||
// Clear transient /who state on every in-game Lua (re)build — login OR
|
||||
// /reload. The DLL survives a logout→character-select→login, so a query
|
||||
// whose SMSG_WHO response never crossed that boundary would otherwise leave
|
||||
// g_inFlight stuck (IsWhoQueryPending() wrongly true for the next character)
|
||||
// and our VAR_WHO_TO_UI_FLAG override forced on (routing the next
|
||||
// character's unrelated /who to the WhoList). ResetIfStuck only heals this
|
||||
// lazily on a *subsequent* send; a fresh-init reset makes every character
|
||||
// start clean. RestoreFlagIfOwned puts the engine flag back to the value we
|
||||
// snapshotted before overriding. ModuleAutoRegister fires per login and per
|
||||
// /reload, before any addon runs, so this always precedes a new burst.
|
||||
void ResetState() {
|
||||
g_inFlight = 0;
|
||||
RestoreFlagIfOwned();
|
||||
g_haveSent = false;
|
||||
g_lastSendTick = 0;
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::ModuleAutoRegister _resetOnInit{&ResetState};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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/>.
|
||||
|
||||
// `ClosestGameObjectPosition(gameObjectID)` → `xPos, yPos, distance` — the
|
||||
// world position of the nearest game object with the given
|
||||
// game-object-template ID, and its distance from the player in yards.
|
||||
// Returns nothing when none is found (the `MayReturnNothing` contract).
|
||||
//
|
||||
// The game-object analog of `ClosestUnitPosition`; same shared scan
|
||||
// (Object::ClosestByEntry), just filtering on the GameObject GUID type
|
||||
// (`0xF110`) and `TYPEMASK_GAMEOBJECT`. Same retail-vs-vanilla caveat:
|
||||
// this returns the nearest CURRENTLY-VISIBLE object of that entry rather
|
||||
// than reading retail's static starting-zone database (which vanilla
|
||||
// doesn't have).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "object/ClosestByEntry.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace GameObject::ClosestPosition {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_ClosestGameObjectPosition(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: ClosestGameObjectPosition(gameObjectID)");
|
||||
return 0;
|
||||
}
|
||||
const auto entry = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const Object::ClosestByEntry::Result r = Object::ClosestByEntry::Find(
|
||||
Offsets::TYPEMASK_GAMEOBJECT, Guid::Type::GameObject, entry);
|
||||
if (!r.found)
|
||||
return 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.x));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.y));
|
||||
Game::Lua::PushNumber(L, std::sqrt(static_cast<double>(r.distSq)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ClosestGameObjectPosition",
|
||||
&Script_ClosestGameObjectPosition);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace GameObject::ClosestPosition
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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 "item/BagFamily.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Item::BagFamily {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init by each `AutoResolver` constructor, before
|
||||
// `DllMain`. Zero-initialized (namespace static) ahead of any dynamic
|
||||
// init, so the head is valid when resolvers link in.
|
||||
AutoResolver *g_resolvers = nullptr;
|
||||
|
||||
// Stock vanilla bags/quivers that shipped with an empty `m_bagFamily`
|
||||
// field, recovered from (class, subclass). Correspondence cross-referenced
|
||||
// by name between ItemSubClass.dbc and ItemBagFamily.dbc. Everything else
|
||||
// in stock vanilla either carries the field directly or has no family, so
|
||||
// it isn't listed — server-custom bags come in through the resolver chain.
|
||||
uint32_t StockSubclassToId(uint32_t itemClass, uint32_t subclass) {
|
||||
if (itemClass == kItemClassContainer) {
|
||||
switch (subclass) {
|
||||
case 1: return 3; // Soul Bag → Soul Shards
|
||||
case 2: return 6; // Herb Bag → Herbs
|
||||
case 3: return 7; // Enchanting Bag → Enchanting Supplies
|
||||
case 4: return 8; // Engineering Bag → Engineering Supplies
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
if (itemClass == kItemClassQuiver) {
|
||||
switch (subclass) {
|
||||
case 2: return 1; // Quiver → Arrows
|
||||
case 3: return 2; // Ammo Pouch → Bullets
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoResolver::AutoResolver(Resolver f) : fn(f), next(g_resolvers) {
|
||||
g_resolvers = this;
|
||||
}
|
||||
|
||||
uint32_t SubclassToId(uint32_t itemClass, uint32_t subclass) {
|
||||
if (const uint32_t id = StockSubclassToId(itemClass, subclass))
|
||||
return id;
|
||||
for (AutoResolver *r = g_resolvers; r != nullptr; r = r->next)
|
||||
if (const uint32_t id = r->fn(itemClass, subclass))
|
||||
return id;
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint32_t BitmaskForRecord(const uint8_t *record) {
|
||||
uint32_t rawId = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
if (rawId == 0) {
|
||||
const uint32_t cls = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t sub = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
rawId = SubclassToId(cls, sub);
|
||||
}
|
||||
return IdToBitmask(rawId);
|
||||
}
|
||||
|
||||
} // namespace Item::BagFamily
|
||||
@@ -0,0 +1,72 @@
|
||||
// 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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared BagFamily derivation for a cached `ItemStats_C` record. Used by
|
||||
// `C_Item.GetItemFamily` and `C_Container.GetContainerNumFreeSlots` /
|
||||
// `CalculateTotalNumberOfFreeBagSlots` so the family a bag reports matches
|
||||
// the family the items it holds report.
|
||||
//
|
||||
// This module knows only STOCK vanilla data. Server-custom container
|
||||
// families (e.g. Turtle's cooking/gathering bags) plug in through the
|
||||
// `AutoResolver` extension point below, so non-stock hardcoded data lives
|
||||
// in its own module (see `src/turtle/`) rather than here.
|
||||
namespace Item::BagFamily {
|
||||
|
||||
// Item.dbc `m_class` values. Containers hold the profession/soul bags;
|
||||
// Quivers hold arrows (subclass 2) and bullets/ammo pouches (subclass 3).
|
||||
// See ItemClass.dbc.
|
||||
constexpr uint32_t kItemClassContainer = 1;
|
||||
constexpr uint32_t kItemClassQuiver = 11;
|
||||
|
||||
// Convert 1.12's raw 1-based `ItemBagFamily.dbc` ID to the modern bitmask
|
||||
// encoding (`1 << (id - 1)`); `0` stays `0`. Vanilla stores the raw ID
|
||||
// (`arrow=1, bullet=2, soul shard=3, herb=6, …`); Wrath+ flipped the same
|
||||
// field to the bitmask form, which is what backported addons expect.
|
||||
inline uint32_t IdToBitmask(uint32_t rawId) {
|
||||
return (rawId == 0) ? 0 : (1u << (rawId - 1));
|
||||
}
|
||||
|
||||
// (class, subclass) → `ItemBagFamily.dbc` raw ID, for bags/quivers whose
|
||||
// `m_bagFamily` field shipped empty. Covers the stock vanilla profession
|
||||
// bags (Container class: Soul/Herb/Enchanting/Engineering) and quivers
|
||||
// (Quiver class: Quiver→arrows, Ammo Pouch→bullets); for anything else it
|
||||
// consults the registered custom resolvers (see `AutoResolver`) before
|
||||
// giving up with `0`. Keyed on class because subclass numbers repeat
|
||||
// across classes (Container subclass 2 = Herb Bag, Quiver subclass 2 =
|
||||
// Quiver). Returns a raw family ID, not a bitmask.
|
||||
uint32_t SubclassToId(uint32_t itemClass, uint32_t subclass);
|
||||
|
||||
// BagFamily bitmask for a cached `ItemStats_C` record: the item's
|
||||
// `m_bagFamily` field if populated, otherwise — for a container whose
|
||||
// field is empty — derived from the container subclass. Pass a non-null
|
||||
// record.
|
||||
uint32_t BitmaskForRecord(const uint8_t *record);
|
||||
|
||||
// Extension point for server-custom bag families. A resolver maps a
|
||||
// (class, subclass) pair to an `ItemBagFamily.dbc` raw ID, or returns `0`
|
||||
// if it doesn't recognize it. Consulted by `SubclassToId` only after the
|
||||
// stock table misses. Declare a file-scope
|
||||
// `static const Item::BagFamily::AutoResolver` in a platform module to
|
||||
// register one at static-init time.
|
||||
using Resolver = uint32_t (*)(uint32_t itemClass, uint32_t subclass);
|
||||
struct AutoResolver {
|
||||
explicit AutoResolver(Resolver fn);
|
||||
Resolver fn;
|
||||
AutoResolver *next;
|
||||
};
|
||||
|
||||
} // namespace Item::BagFamily
|
||||
@@ -11,6 +11,8 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "item/Count.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/Arg.h"
|
||||
@@ -251,6 +253,15 @@ int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
|
||||
} // namespace
|
||||
|
||||
int InInventory(void *L, int itemID) {
|
||||
if (itemID <= 0)
|
||||
return 0;
|
||||
int total = CountEquipped(itemID, /*includeUses*/ false);
|
||||
for (int bag = 0; bag <= 4; bag++)
|
||||
total += CountInBag(L, bag, itemID, /*includeUses*/ false);
|
||||
return total;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemCount",
|
||||
&Script_C_Item_GetItemCount);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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 Item::Count {
|
||||
|
||||
// Total plain stack count of `itemID` across the player's equipped slots
|
||||
// and bags 0..4 (no bank, no charge multiplier) — the "carried on me"
|
||||
// count. Needs the Lua state `L`: the bag walk uses the engine's slot
|
||||
// packer, which reads and clears the Lua stack, so the caller must have
|
||||
// already consumed any stack arguments it still needs before calling.
|
||||
int InInventory(void *L, int itemID);
|
||||
|
||||
} // namespace Item::Count
|
||||
+13
-18
@@ -15,6 +15,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/Data.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
@@ -192,17 +193,6 @@ static int __fastcall Script_C_Item_GetItemIconByID(void *L) {
|
||||
return PushIconForItemID(L, Item::Arg::ResolveItemID(L, 1));
|
||||
}
|
||||
|
||||
// Convert 1.12's raw `BagFamily` ID to the modern bitmask encoding.
|
||||
// Vanilla stores `arrow=1, bullet=2, soul shard=3, herb=6, ...`; Wrath
|
||||
// flipped to `1 << (ID-1)` (`arrow=0x1, bullet=0x2, soul shard=0x4,
|
||||
// herb=0x20, ...`) and addons backported from Wrath+ expect the
|
||||
// bitmask. We always surface the modern shape so callers don't have to
|
||||
// branch on client version. See `Offsets::OFF_ITEMSTATS_BAG_FAMILY`
|
||||
// for the empirical verification.
|
||||
uint32_t BagFamilyIdToBitmask(uint32_t rawId) {
|
||||
return (rawId == 0) ? 0 : (1u << (rawId - 1));
|
||||
}
|
||||
|
||||
// `C_Item.GetItemFamily(item)` — returns the BagFamily bitmask for an
|
||||
// item (modern encoding: `1 << (ID-1)`), or `nil` if the item isn't
|
||||
// in the cache. Used by auto-routing addons that decide which
|
||||
@@ -227,10 +217,14 @@ uint32_t BagFamilyIdToBitmask(uint32_t rawId) {
|
||||
// but has family 0 (general-purpose)". Both are safe to treat as "no
|
||||
// preference" but keeping them distinct helps debugging.
|
||||
//
|
||||
// 1.12 vanilla server data sparsely populates the field for **bags**
|
||||
// themselves (only quivers seem to carry it on Turtle WoW). Individual
|
||||
// loot items do carry the field reliably (arrows, bullets, shards,
|
||||
// herbs all return their proper raw IDs).
|
||||
// 1.12 vanilla server data leaves the `m_bagFamily` field empty on the
|
||||
// **bags/quivers** themselves — Soul/Herb/Enchanting/Engineering bags AND
|
||||
// quivers/ammo pouches all read 0. Individual loot items do carry the
|
||||
// field reliably (arrows, bullets, shards, herbs return their proper raw
|
||||
// IDs). For a bag whose field is empty we recover the family from its
|
||||
// class + subclass (the same signal the "Soul Bag" tooltip line uses), so
|
||||
// a Felcloth Bag reports the Soul Shard family (0x4), a quiver 0x1, an
|
||||
// ammo pouch 0x2 — see `Item::BagFamily`.
|
||||
static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
const int itemID = Item::Arg::ResolveItemID(L, 1);
|
||||
if (itemID <= 0)
|
||||
@@ -240,9 +234,10 @@ static int __fastcall Script_C_Item_GetItemFamily(void *L) {
|
||||
Item::Data::WarmCache(static_cast<uint32_t>(itemID));
|
||||
return 0;
|
||||
}
|
||||
const uint32_t rawId = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(BagFamilyIdToBitmask(rawId)));
|
||||
// Family field if populated, else derived from the container subclass
|
||||
// (Turtle leaves bags' m_bagFamily at 0 — see Item::BagFamily).
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(Item::BagFamily::BitmaskForRecord(record)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+89
-1
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -25,6 +26,18 @@ int LocaleIndex() {
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
}
|
||||
|
||||
// Localized ItemClass.dbc name for a class ID (records indexed by ID, the
|
||||
// standard DBC shape — same lookup `Script_GetItemInfo` uses). Returns ""
|
||||
// for an unknown class. Obsolete slots keep the client's literal string
|
||||
// (classID 3 → "Jewelry(OBSOLETE)", 8 → "Generic(OBSOLETE)", …) — that's
|
||||
// the real vanilla data; Enum.ItemClass supplies the modern key names.
|
||||
const char *ClassName(uint32_t classID) {
|
||||
const char *s = DBC::LocalizedField(Offsets::VAR_ITEMCLASS_RECORDS,
|
||||
Offsets::VAR_ITEMCLASS_COUNT, classID,
|
||||
Offsets::OFF_ITEMCLASS_NAMES);
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// Linear-scan ItemSubClass.dbc for the (classID, subClassID) compound-key
|
||||
// row — same scan Item::GetData uses. Returns the record or nullptr.
|
||||
const uint8_t *FindSubClass(uint32_t classID, uint32_t subClassID) {
|
||||
@@ -72,8 +85,27 @@ const char *InvTypeKey(int invType) {
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// `GetItemClassInfo(classID)` → `className` (localized). `nil` for an
|
||||
// unknown class. Backport of the modern global (also exposed as
|
||||
// `C_Item.GetItemClassInfo`).
|
||||
int __fastcall Script_GetItemClassInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetItemClassInfo(classID)");
|
||||
return 0;
|
||||
}
|
||||
const char *name = ClassName(static_cast<uint32_t>(Game::Lua::ToNumber(L, 1)));
|
||||
if (name[0] == '\0') {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushString(L, name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemSubClassInfo(classID, subClassID)` →
|
||||
// `subClassName, subClassUsesInvType`.
|
||||
// `subClassName, subClassUsesInvType`. Also exposed as the global
|
||||
// `GetItemSubClassInfo(classID, subClassID)` → `name, isArmorType`, the
|
||||
// same two values (the second is true for the armor material subclasses).
|
||||
//
|
||||
// `subClassName` is the localized ItemSubClass.dbc name (verbose form, e.g.
|
||||
// "One-Handed Swords"; falls back to the short form for subclasses that
|
||||
@@ -184,7 +216,57 @@ constexpr Game::Lua::EnumIntegerEntry kInventoryTypeEntries[] = {
|
||||
{"IndexEquipablespellWeaponType", 34},
|
||||
};
|
||||
|
||||
// `Enum.ItemClass` — the modern item-class enum. Values 0..15 are the
|
||||
// classes vanilla items actually report (the numeric IDs match retail; the
|
||||
// obsolete slots 3/8/10/14 keep their modern key names — Gem /
|
||||
// ItemEnhancement / CurrencyTokenObsolete / PermanentObsolete — even
|
||||
// though ItemClass.dbc labels them "…(OBSOLETE)"). 16..19 are post-vanilla
|
||||
// and never appear in 1.12 item data, included so backport code that
|
||||
// references those keys resolves (comparisons just never match).
|
||||
constexpr Game::Lua::EnumIntegerEntry kItemClassEntries[] = {
|
||||
{"Consumable", 0},
|
||||
{"Container", 1},
|
||||
{"Weapon", 2},
|
||||
{"Gem", 3},
|
||||
{"Armor", 4},
|
||||
{"Reagent", 5},
|
||||
{"Projectile", 6},
|
||||
{"Tradegoods", 7},
|
||||
{"ItemEnhancement", 8},
|
||||
{"Recipe", 9},
|
||||
{"CurrencyTokenObsolete", 10},
|
||||
{"Quiver", 11},
|
||||
{"Questitem", 12},
|
||||
{"Key", 13},
|
||||
{"PermanentObsolete", 14},
|
||||
{"Miscellaneous", 15},
|
||||
{"Glyph", 16},
|
||||
{"Battlepet", 17},
|
||||
{"WoWToken", 18},
|
||||
{"Profession", 19},
|
||||
};
|
||||
|
||||
// `Enum.ItemQuality` — the numeric `quality` reported by `GetItemInfo` /
|
||||
// `C_Item.GetItemQuality`. Values 0..6 are all vanilla ever uses; the
|
||||
// post-vanilla Heirloom (7) and WoWToken (8) tiers are omitted (no such
|
||||
// items exist on 1.12).
|
||||
constexpr Game::Lua::EnumIntegerEntry kItemQualityEntries[] = {
|
||||
{"Poor", 0},
|
||||
{"Common", 1},
|
||||
{"Uncommon", 2},
|
||||
{"Rare", 3},
|
||||
{"Epic", 4},
|
||||
{"Legendary", 5},
|
||||
{"Artifact", 6},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetItemClassInfo",
|
||||
&Script_GetItemClassInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetItemSubClassInfo",
|
||||
&Script_GetItemSubClassInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemClassInfo",
|
||||
&Script_GetItemClassInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemSubClassInfo",
|
||||
&Script_GetItemSubClassInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemInventorySlotKey",
|
||||
@@ -194,6 +276,12 @@ void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "InventoryType", kInventoryTypeEntries,
|
||||
sizeof(kInventoryTypeEntries) / sizeof(kInventoryTypeEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemClass", kItemClassEntries,
|
||||
sizeof(kItemClassEntries) / sizeof(kItemClassEntries[0]));
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "ItemQuality", kItemQualityEntries,
|
||||
sizeof(kItemQualityEntries) / sizeof(kItemQualityEntries[0]));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+96
-35
@@ -30,17 +30,23 @@
|
||||
// (dispatcher leaves `_G.arg<N>` alone when no codes are parsed),
|
||||
// restore.
|
||||
// - `NAME_PLATE_UNIT_ADDED` / `_REMOVED` — `arg1` is the
|
||||
// `"nameplateN"` unit token (formatted from the plate's index in
|
||||
// `g_orderedGUIDs` at fire time). The token resolves to the unit
|
||||
// via the `nameplateN`-aware token resolver in `unit/TokenExtensions.cpp`
|
||||
// — addons can pass it straight to `UnitName`, `UnitGUID`, etc.,
|
||||
// or to `GetNamePlateForUnit` for the frame.
|
||||
// `"nameplateN"` unit token (`N` = the plate's assigned slot). The token
|
||||
// resolves to the unit via the `nameplateN`-aware token resolver in
|
||||
// `unit/TokenExtensions.cpp` — addons can pass it straight to `UnitName`,
|
||||
// `UnitGUID`, etc., or to `GetNamePlateForUnit` for the frame.
|
||||
//
|
||||
// Slot assignment (retail-exact): a plate keeps its slot for its whole
|
||||
// lifetime, so surviving plates are NEVER renumbered when another is removed
|
||||
// — a removal frees the slot and the next new plate reuses the lowest free
|
||||
// one. `g_slots` is therefore a sparse array (freed middle slots read back as
|
||||
// the empty GUID 0 until reused), not a compacting list.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/TokenObserver.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -84,13 +90,27 @@ std::unordered_map<uint64_t, const void *> g_currentTickPlates;
|
||||
// then tops out as pool reuse covers all subsequent shows.
|
||||
std::unordered_set<const void *> g_seenPlates;
|
||||
|
||||
// Ordered list of currently-visible nameplate GUIDs, in
|
||||
// creation-order. Append on UNIT_ADDED, erase on UNIT_REMOVED. Backs
|
||||
// the `nameplateN` unit-token resolver in `unit/TokenExtensions.cpp`. Order
|
||||
// matches modern WoW semantics: stable for the lifetime of each
|
||||
// plate, gaps when middle plates vanish (until the next REMOVED
|
||||
// shifts later entries down).
|
||||
std::vector<uint64_t> g_orderedGUIDs;
|
||||
// Nameplate token slots: slot `i` (0-based) holds the GUID assigned to
|
||||
// `nameplate(i+1)`, or 0 when free. Retail-exact slot assignment — a plate
|
||||
// keeps its slot for its whole lifetime (survivors are never renumbered),
|
||||
// UNIT_REMOVED frees the slot (sets 0), and a new plate reuses the lowest
|
||||
// free slot. Sparse: freed middle slots stay as gaps until reused. Backs the
|
||||
// `nameplateN` unit-token resolver in `unit/TokenExtensions.cpp`.
|
||||
std::vector<uint64_t> g_slots;
|
||||
|
||||
// Assign `guid` the lowest free slot, reusing a vacated one when present and
|
||||
// growing the array only when every slot is occupied. Returns the 0-based
|
||||
// slot. Never shifts an existing entry.
|
||||
int AssignSlot(uint64_t guid) {
|
||||
for (size_t i = 0; i < g_slots.size(); ++i) {
|
||||
if (g_slots[i] == 0) {
|
||||
g_slots[i] = guid;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
g_slots.push_back(guid);
|
||||
return static_cast<int>(g_slots.size() - 1);
|
||||
}
|
||||
|
||||
// Fire `eventName` with a pre-formatted string as `arg1`. The engine
|
||||
// dispatcher's `%s` format code pushes the C string into `_G.arg1`
|
||||
@@ -114,6 +134,32 @@ const char *FormatNamePlateToken(char *buf, size_t bufSize, int oneBasedIndex) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Descriptor-field observer callback — makes unit events (UNIT_HEALTH,
|
||||
// UNIT_AURA, …) fire with the `"nameplateN"` token, so a nameplated unit is a
|
||||
// first-class unit-event source exactly like a party member. Registered per
|
||||
// nameplate GUID via `Unit::TokenObserver` on UNIT_ADDED, unregistered on
|
||||
// UNIT_REMOVED. We reverse-look-up the changed GUID's slot at fire time
|
||||
// (cheap; the array is small) so the token reflects its current assignment.
|
||||
// The event id is the field index (`fieldOffset >> 2`); the engine fires every
|
||||
// unit event as "%s" + token, so we match. A GUID no longer in any slot
|
||||
// (removal raced this field change) simply drops the fire.
|
||||
int __fastcall NamePlateFieldCb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const uint32_t * /*oldValue*/, void * /*userArg*/) {
|
||||
const uint64_t guid =
|
||||
(static_cast<uint64_t>(guidHi) << 32) | static_cast<uint64_t>(guidLo);
|
||||
auto it = std::find(g_slots.begin(), g_slots.end(), guid);
|
||||
if (it == g_slots.end())
|
||||
return 1;
|
||||
const int oneBased = static_cast<int>(it - g_slots.begin()) + 1;
|
||||
char tokenBuf[24];
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased);
|
||||
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
|
||||
reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT))(
|
||||
static_cast<int>(fieldOffset >> 2), "%s", tokenBuf);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fire `eventName` with the nameplate `Frame` set as `_G.arg1`.
|
||||
// `FUN_FIRE_EVENT`'s format-string parser only handles primitive
|
||||
// types, but it only mutates `_G.arg<N>` for codes it actually
|
||||
@@ -178,38 +224,42 @@ void OnWorldTick() {
|
||||
|
||||
// Fire CREATED (with the Frame as arg1) for never-before-seen
|
||||
// frame pointers; ADDED (with `"nameplateN"` token as arg1) for
|
||||
// GUIDs not in last tick's snapshot. New entries are appended to
|
||||
// the ordered list *before* firing so the token resolves to the
|
||||
// newly-added plate during the event handler.
|
||||
// GUIDs not in last tick's snapshot. The slot is assigned *before*
|
||||
// firing so the token resolves to the newly-added plate during the
|
||||
// event handler.
|
||||
for (const auto &kv : g_currentTickPlates) {
|
||||
if (g_seenPlates.insert(kv.second).second)
|
||||
FireWithFrame(kEventCreated, const_cast<void *>(kv.second));
|
||||
if (g_lastTickPlates.find(kv.first) == g_lastTickPlates.end()) {
|
||||
g_orderedGUIDs.push_back(kv.first);
|
||||
const int slot = AssignSlot(kv.first);
|
||||
// Watch this unit's fields so UNIT_HEALTH/UNIT_AURA/… fire with
|
||||
// its "nameplateN" token (the engine only watches its own
|
||||
// target/party/raid tokens, not nameplates).
|
||||
Unit::TokenObserver::Register(kv.first, &NamePlateFieldCb);
|
||||
char tokenBuf[24];
|
||||
FireWithString(kEventUnitAdded,
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf,
|
||||
static_cast<int>(g_orderedGUIDs.size())));
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, slot + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Fire REMOVED for GUIDs in last tick's snapshot but not current.
|
||||
// We compute the token from the position *before* erasing so the
|
||||
// event payload reflects the slot the unit just vacated; the
|
||||
// handler can still resolve the token to the unit via
|
||||
// `g_orderedGUIDs[slot]` during dispatch. Later plates shift down
|
||||
// when we erase — matches modern semantics.
|
||||
// Fire REMOVED for GUIDs in last tick's snapshot but not current. The
|
||||
// slot is *freed* (set 0), not erased, so surviving plates keep their
|
||||
// index (retail-exact — no shift); the next ADD reuses it. Token is
|
||||
// computed before freeing so the payload names the slot just vacated.
|
||||
// Trailing empties are trimmed so the array tracks the high-water mark.
|
||||
for (const auto &kv : g_lastTickPlates) {
|
||||
if (g_currentTickPlates.find(kv.first) == g_currentTickPlates.end()) {
|
||||
auto it = std::find(g_orderedGUIDs.begin(), g_orderedGUIDs.end(),
|
||||
kv.first);
|
||||
if (it == g_orderedGUIDs.end())
|
||||
auto it = std::find(g_slots.begin(), g_slots.end(), kv.first);
|
||||
if (it == g_slots.end())
|
||||
continue;
|
||||
const int oneBased = static_cast<int>(it - g_orderedGUIDs.begin()) + 1;
|
||||
const int oneBased = static_cast<int>(it - g_slots.begin()) + 1;
|
||||
char tokenBuf[24];
|
||||
FireWithString(kEventUnitRemoved,
|
||||
FormatNamePlateToken(tokenBuf, sizeof tokenBuf, oneBased));
|
||||
g_orderedGUIDs.erase(it);
|
||||
Unit::TokenObserver::Unregister(kv.first, &NamePlateFieldCb);
|
||||
*it = 0; // free the slot (no shift of survivors)
|
||||
while (!g_slots.empty() && g_slots.back() == 0)
|
||||
g_slots.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,24 +280,35 @@ static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
// suppress every refire, and the freshly-built wrapper would lack
|
||||
// the addon's `.nameplate` field.
|
||||
//
|
||||
// `g_orderedGUIDs` is also cleared so the post-reload token indices
|
||||
// start at `nameplate1` again, matching the order plates re-fire in.
|
||||
// `g_slots` is also cleared so post-reload token slots start fresh.
|
||||
void PrepareForReload() {
|
||||
g_seenPlates.clear();
|
||||
g_lastTickPlates.clear();
|
||||
g_orderedGUIDs.clear();
|
||||
// Objects survive a /reload, so their observer nodes do too. The first
|
||||
// post-reload tick re-fires ADDED and re-registers, which would stack a
|
||||
// second observer per field (the registrar never dedups) → double events.
|
||||
// Tear ours down here so re-registration starts clean (skip free slots).
|
||||
for (uint64_t guid : g_slots)
|
||||
if (guid != 0)
|
||||
Unit::TokenObserver::Unregister(guid, &NamePlateFieldCb);
|
||||
g_slots.clear();
|
||||
}
|
||||
|
||||
// Exposed via `nameplate/Walk.h` so the `nameplateN` token resolver
|
||||
// in `unit/TokenExtensions.cpp` can map an index to a GUID without seeing
|
||||
// the internal vector.
|
||||
// the internal array. Returns 0 for an out-of-range OR currently-free slot.
|
||||
uint64_t GetGUIDByIndex(int oneBased) {
|
||||
if (oneBased <= 0)
|
||||
return 0;
|
||||
const size_t idx = static_cast<size_t>(oneBased - 1);
|
||||
if (idx >= g_orderedGUIDs.size())
|
||||
if (idx >= g_slots.size())
|
||||
return 0;
|
||||
return g_orderedGUIDs[idx];
|
||||
return g_slots[idx]; // 0 when the slot is free
|
||||
}
|
||||
|
||||
// Number of slots (1-based max index). Because the slot array is SPARSE,
|
||||
// callers that iterate all plates must scan `1..GetSlotCount()` and skip the
|
||||
// slots where `GetGUIDByIndex` returns 0 — they can't stop at the first gap.
|
||||
int GetSlotCount() { return static_cast<int>(g_slots.size()); }
|
||||
|
||||
} // namespace NamePlate::Events
|
||||
|
||||
+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
|
||||
|
||||
+10
-4
@@ -37,12 +37,18 @@ void PushNamePlateFrame(void *L, void *nameplate);
|
||||
namespace NamePlate::Events {
|
||||
|
||||
// Returns the GUID currently bound to `nameplateN` (1-based, matching
|
||||
// the modern Lua token form). `0` means no nameplate at that index.
|
||||
// Backed by the per-tick UNIT_ADDED / UNIT_REMOVED diff in
|
||||
// `Events.cpp` — order is creation-order (append on ADDED, erase on
|
||||
// REMOVED), stable within a frame.
|
||||
// the modern Lua token form). `0` means the slot is out of range OR
|
||||
// currently free. Backed by the retail-exact slot assignment in
|
||||
// `Events.cpp`: a plate keeps its slot for its lifetime and freed slots
|
||||
// are reused, so the slot array is SPARSE (gaps read back as 0).
|
||||
uint64_t GetGUIDByIndex(int oneBased);
|
||||
|
||||
// Highest slot index in use (1-based). Callers that iterate every plate
|
||||
// must loop `1..GetSlotCount()` and skip indices where `GetGUIDByIndex`
|
||||
// returns 0 — they must NOT stop at the first gap, since the array is
|
||||
// sparse.
|
||||
int GetSlotCount();
|
||||
|
||||
// Clears the per-tick diff state (seen pointers, last tick's
|
||||
// snapshot, ordered GUID list) so the post-`/reload` tick refires
|
||||
// CREATED + UNIT_ADDED for every currently-visible plate. Called
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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 "object/ClosestByEntry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Object::ClosestByEntry {
|
||||
|
||||
namespace {
|
||||
|
||||
// `ClntObjMgrEnumVisibleObjects(cb, ctx)` — walks the visible-object
|
||||
// table; the callback's dummy EDX slot absorbs the second __fastcall
|
||||
// register so the trailing `uint64_t guid` lands where the engine puts it.
|
||||
// Return non-zero to keep iterating.
|
||||
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx,
|
||||
uint64_t guid);
|
||||
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
|
||||
|
||||
// `ClntObjMgrObjectPtr(typeMask, dbg, guidLo, guidHi, dbgCode)` — resolves
|
||||
// a GUID to its live `CGObject_C *`, filtered by typeMask; NULL if none.
|
||||
using ObjectPtr_t = void *(__fastcall *)(uint32_t typeMask, const char *dbg,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int dbgCode);
|
||||
|
||||
struct ScanCtx {
|
||||
uint32_t typeMask;
|
||||
Guid::Type guidType;
|
||||
uint32_t wantEntry;
|
||||
float px, py, pz; // player position
|
||||
Result r;
|
||||
};
|
||||
|
||||
int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
// Type + entry checks are pure GUID bit-reads, so we skip the (costly)
|
||||
// object resolve for everything that can't match.
|
||||
if (Guid::Classify(guid) != ctx->guidType)
|
||||
return 1;
|
||||
const uint32_t entry = static_cast<uint32_t>((guid >> 24) & 0xFFFFFFu);
|
||||
if (entry != ctx->wantEntry)
|
||||
return 1;
|
||||
|
||||
auto ObjectPtr =
|
||||
reinterpret_cast<ObjectPtr_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(ctx->typeMask, nullptr, static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
|
||||
float pos[3];
|
||||
if (!Unit::Position::Read(obj, pos))
|
||||
return 1;
|
||||
|
||||
const float dx = pos[0] - ctx->px;
|
||||
const float dy = pos[1] - ctx->py;
|
||||
const float dz = pos[2] - ctx->pz;
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
if (!ctx->r.found || distSq < ctx->r.distSq) {
|
||||
ctx->r.found = true;
|
||||
ctx->r.x = pos[0];
|
||||
ctx->r.y = pos[1];
|
||||
ctx->r.distSq = distSq;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Find(uint32_t typeMask, Guid::Type guidType, uint32_t entry) {
|
||||
const Result none{false, 0.0f, 0.0f, 0.0f};
|
||||
if (entry == 0)
|
||||
return none;
|
||||
|
||||
// The enumerator derefs the object manager unconditionally; it's NULL
|
||||
// during glue / character-select. Bail rather than crash.
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) ==
|
||||
nullptr)
|
||||
return none;
|
||||
|
||||
float ppos[3];
|
||||
if (!Unit::Position::Read(Unit::Position::ResolveToken("player"), ppos))
|
||||
return none;
|
||||
|
||||
ScanCtx ctx{typeMask, guidType, entry, ppos[0], ppos[1], ppos[2], none};
|
||||
auto Enum = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
Enum(reinterpret_cast<EnumCallback_t>(&ScanCallback), &ctx);
|
||||
return ctx.r;
|
||||
}
|
||||
|
||||
} // namespace Object::ClosestByEntry
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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
|
||||
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared "nearest visible object of a given template entry" scan, backing
|
||||
// both `ClosestUnitPosition` (creatures) and `ClosestGameObjectPosition`
|
||||
// (game objects). Vanilla packs the template entry into GUID bits 24-47
|
||||
// for the world-object types, so the only per-caller differences are the
|
||||
// GUID type prefix and the object-manager type mask.
|
||||
namespace Object::ClosestByEntry {
|
||||
|
||||
struct Result {
|
||||
bool found;
|
||||
float x, y; // world position of the nearest match
|
||||
float distSq; // squared center-to-center distance from the player
|
||||
};
|
||||
|
||||
// Nearest CURRENTLY-VISIBLE object whose GUID is of `guidType` (Creature /
|
||||
// GameObject / …) and encodes template `entry` (GUID bits 24-47),
|
||||
// relative to the local player. `typeMask` is the `ClntObjMgrObjectPtr`
|
||||
// filter used to resolve the GUID (`TYPEMASK_UNIT` / `TYPEMASK_GAMEOBJECT`).
|
||||
// Returns `{found=false}` for entry 0, pre-world, on a player-position
|
||||
// miss, or when nothing matches.
|
||||
Result Find(uint32_t typeMask, Guid::Type guidType, uint32_t entry);
|
||||
|
||||
} // namespace Object::ClosestByEntry
|
||||
@@ -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
|
||||
+12
-10
@@ -63,6 +63,7 @@
|
||||
#include "item/Location.h"
|
||||
#include "item/StatAccum.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/HealingAura.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -139,18 +140,19 @@ long ItemFlatHealing(const uint8_t *cgItem) {
|
||||
|
||||
// --- Buff / talent healing auras (flat + Spirit/Armor conversions) ---
|
||||
// One decoder for every healing aura a buff or passive talent can carry,
|
||||
// mirroring the server's SpellBaseHealingBonusDone:
|
||||
// mirroring the server's SpellBaseHealingBonusDone. Stock vanilla auras are
|
||||
// handled inline:
|
||||
// 135 MOD_HEALING_DONE → flat += amount
|
||||
// 175 MOD_SPELL_HEALING_OF_STAT_PERCENT → += Spirit × amount / 100 (Spiritual Guidance)
|
||||
// 199 MOD_SPELL_HEALING_OF_ARMOR_PERCENT → += Armor × amount / 100 (Turtle Ironclad)
|
||||
// Amounts/percents are read straight from Spell.dbc — generic (any talent/buff
|
||||
// using these auras, incl. Turtle customs) and robust (no client-vs-server
|
||||
// value delta). `requirePassive` gates the bitmap walk to always-on talents,
|
||||
// so a *known but not-currently-cast* buff (Divine Spirit) isn't counted as
|
||||
// on; active buffs pass `requirePassive == false`.
|
||||
// Any other aura index is deferred to `Spell::HealingAura::Resolve`, the
|
||||
// extension point where server-custom healing auras live (e.g. Turtle's
|
||||
// Ironclad, aura 199 = × Armor — see src/turtle/HealingAura.cpp). Amounts/
|
||||
// percents are read straight from Spell.dbc — generic and robust (no
|
||||
// client-vs-server value delta). `requirePassive` gates the bitmap walk to
|
||||
// always-on talents, so a *known but not-currently-cast* buff (Divine
|
||||
// Spirit) isn't counted as on; active buffs pass `requirePassive == false`.
|
||||
constexpr int kAuraModHealingDone = 135;
|
||||
constexpr int kAuraModHealingOfStatPercent = 175; // × Spirit
|
||||
constexpr int kAuraModHealingOfArmorPercent = 199; // × Armor (Turtle Ironclad)
|
||||
|
||||
void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
|
||||
int32_t armor, bool requirePassive) {
|
||||
@@ -177,8 +179,8 @@ void AddSpellHealingAuras(long &total, int spellID, int32_t spirit,
|
||||
total += amount;
|
||||
else if (aura[i] == kAuraModHealingOfStatPercent)
|
||||
total += static_cast<long>(spirit) * amount / 100;
|
||||
else if (aura[i] == kAuraModHealingOfArmorPercent)
|
||||
total += static_cast<long>(armor) * amount / 100;
|
||||
else
|
||||
total += Spell::HealingAura::Resolve(aura[i], amount, spirit, armor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-4
@@ -91,6 +91,7 @@ using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, in
|
||||
constexpr int OFF_NAME = 0x1E0; // localized name[9]
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // -> SpellIcon.dbc
|
||||
constexpr int OFF_ATTRIBUTES = 0x18; // u32 Attributes flags
|
||||
constexpr int OFF_ATTRIBUTES_EX2 = 0x24; // u32 AttributesEx2 flags
|
||||
constexpr int OFF_SUBRECORD_VALUE = 0x04; // SpellIcon path field
|
||||
|
||||
// SPELL_ATTR_TRADESPELL — set on profession recipe casts (enchant,
|
||||
@@ -99,6 +100,12 @@ constexpr int OFF_SUBRECORD_VALUE = 0x04; // SpellIcon path field
|
||||
// there, +0x18 here — the bit value is the same across builds).
|
||||
constexpr uint32_t SPELL_ATTR_TRADESPELL = 0x20;
|
||||
|
||||
// SPELL_ATTR_RANGED — every ranged ability carries it; the server adds a
|
||||
// flat +500ms "aim time" to these (SpellEntry::GetCastTime). Auto-repeat
|
||||
// shots (Auto Shot / Shoot, AttributesEx2 bit 0x20) are the one exclusion.
|
||||
constexpr uint32_t SPELL_ATTR_RANGED = 0x2;
|
||||
constexpr uint32_t SPELL_ATTR_EX2_AUTOREPEAT = 0x20;
|
||||
|
||||
struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
int startMs;
|
||||
@@ -163,8 +170,30 @@ void *Resolve(const char *token) {
|
||||
// matches the engine's cast bar exactly — including talents like a Mage's
|
||||
// faster casts. Unit arg 0 = local player; flag 0 clamps negatives to 0.
|
||||
int CastTimeMs(int spellID) {
|
||||
return static_cast<int>(
|
||||
int ms = static_cast<int>(
|
||||
reinterpret_cast<GetCastTime_t>(Offsets::FUN_GET_CAST_TIME)(spellID, 0, 0));
|
||||
// FUN_006e3340 (also the spell tooltip's cast-time source) does NOT add the
|
||||
// flat +500ms "aim time" the server applies to ranged, non-autorepeat
|
||||
// spells — SpellEntry::GetCastTime does `castTime += 500` for
|
||||
// SPELL_ATTR_RANGED. So our zero-latency prediction runs ~500ms short of
|
||||
// the server for Volley / Aimed Shot / Multi-Shot; the confirming
|
||||
// SMSG_SPELL_START then snaps the bar out ~1 RTT later. Mirror the server
|
||||
// here so the prediction is right from frame 1 (only for spells that
|
||||
// already have a cast time — instant ranged abilities that the server
|
||||
// gives a 500ms aim time still surface through the packet path). The
|
||||
// cast-time reconcile in HandleSpellStart remains the backstop.
|
||||
if (ms > 0) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec != nullptr) {
|
||||
const uint32_t attr =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES);
|
||||
const uint32_t attrEx2 =
|
||||
*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX2);
|
||||
if ((attr & SPELL_ATTR_RANGED) && !(attrEx2 & SPELL_ATTR_EX2_AUTOREPEAT))
|
||||
ms += 500;
|
||||
}
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
// Effective channel duration (ms) for the local player, 0 if none. Uses
|
||||
@@ -407,13 +436,26 @@ void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
// Spell_C_CastSpell at `spellID == VAR_CURRENT_CAST_SPELL`, so the
|
||||
// client path (FUN_CAST_START_SET hook) never fires for them — this
|
||||
// server packet is the only signal. Dedup against a fresh client stamp
|
||||
// (same spell, started within ~latency) so a normal cast's confirming
|
||||
// packet doesn't restart its bar; otherwise it's a chained recast the
|
||||
// (same spell, started within ~latency): a normal cast's confirming
|
||||
// packet mustn't RESTART its bar; otherwise it's a chained recast the
|
||||
// client bailed on — take it, and flag it so the VAR==0 poll (which
|
||||
// never saw it) won't clear it.
|
||||
if (g_cast.spellID == spellID && now < g_cast.endMs &&
|
||||
now - g_cast.startMs < kCastStartDedupMs)
|
||||
now - g_cast.startMs < kCastStartDedupMs) {
|
||||
// Confirming packet. Keep startMs (no visual restart), but SNAP the
|
||||
// end time to the server's authoritative castTime. The client stamp
|
||||
// came from FUN_006e3340, which folds in the caster's cast-speed /
|
||||
// ranged-haste multiplier (descriptor +0x22c) — right for spells the
|
||||
// server also hastes (Aimed/Multi-Shot), WRONG for ones it doesn't.
|
||||
// Volley is the case that surfaced this: not channeled, not sped by
|
||||
// Quick Shots server-side, so the hasted prediction ended the bar
|
||||
// early and the player was "still casting after the bar finished."
|
||||
// The server's castTime is reality; reconcile to it (preserving any
|
||||
// pushback already accumulated). A no-op when client and server agree.
|
||||
g_cast.endMs =
|
||||
g_cast.startMs + static_cast<int>(castTime) + g_cast.delayMs;
|
||||
return;
|
||||
}
|
||||
g_cast = TrackedSpell{spellID, now, now + static_cast<int>(castTime), 0};
|
||||
g_castFromServer = true;
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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 "spell/HealingAura.h"
|
||||
|
||||
namespace Spell::HealingAura {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init by each `AutoResolver` constructor, before
|
||||
// `DllMain`. Zero-initialized ahead of any dynamic init.
|
||||
AutoResolver *g_resolvers = nullptr;
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoResolver::AutoResolver(Resolver f) : fn(f), next(g_resolvers) {
|
||||
g_resolvers = this;
|
||||
}
|
||||
|
||||
long Resolve(int auraIndex, long amount, int32_t spirit, int32_t armor) {
|
||||
for (AutoResolver *r = g_resolvers; r != nullptr; r = r->next)
|
||||
if (const long v = r->fn(auraIndex, amount, spirit, armor))
|
||||
return v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace Spell::HealingAura
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Extension point for non-stock healing-aura semantics feeding
|
||||
// `GetSpellBonusHealing`. The stock vanilla healing auras — 135
|
||||
// (MOD_HEALING_DONE, flat) and 175 (MOD_SPELL_HEALING_OF_STAT_PERCENT,
|
||||
// ×Spirit) — are decoded inline in `spell/BonusDamage.cpp`. Server-custom
|
||||
// aura effects (e.g. Turtle's Ironclad, aura 199 = ×Armor) register a
|
||||
// resolver here so their meaning lives outside the stock decoder.
|
||||
namespace Spell::HealingAura {
|
||||
|
||||
// Flat healing contribution for a single Spell.dbc aura effect: the aura
|
||||
// index plus its computed amount (base + dice), and the player's Spirit /
|
||||
// Armor for stat-scaled auras. Returns 0 for aura indices no resolver
|
||||
// recognizes. Consulted by the stock decoder for any effect that isn't a
|
||||
// stock healing aura.
|
||||
long Resolve(int auraIndex, long amount, int32_t spirit, int32_t armor);
|
||||
|
||||
// A resolver maps one aura effect to its flat healing contribution, or
|
||||
// returns 0 if it doesn't recognize the aura index. Declare a file-scope
|
||||
// `static const Spell::HealingAura::AutoResolver` in a platform module to
|
||||
// register one at static-init time.
|
||||
using Resolver = long (*)(int auraIndex, long amount, int32_t spirit,
|
||||
int32_t armor);
|
||||
struct AutoResolver {
|
||||
explicit AutoResolver(Resolver fn);
|
||||
Resolver fn;
|
||||
AutoResolver *next;
|
||||
};
|
||||
|
||||
} // namespace Spell::HealingAura
|
||||
@@ -55,6 +55,71 @@ static int __fastcall Script_GameTooltipSetSpellByID(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `GameTooltip:AddSpellByID(spellID)` — APPENDS a spell's tooltip to the
|
||||
// current one instead of clearing+rebuilding (the `SetSpellByID` behavior).
|
||||
// The natural pair to `SetSpellByID`; lets callers compose tooltips like
|
||||
// `AddLine(talentName) + AddSpellByID(rankSpellID)`.
|
||||
//
|
||||
// `BuildSpellTooltip`'s last arg (`param_7`) gates the clear: `0` calls the
|
||||
// per-tooltip Clear (`FUN_00530050`) first — that's `SetSpellByID`; non-zero
|
||||
// SKIPS the clear and appends. The append path is the engine's talent
|
||||
// "next rank" preview, so it (a) emits a `"\nNext rank:"` header line
|
||||
// instead of the spell name, and (b) does NOT stash the displayed spellID at
|
||||
// `+0x39C` (so `GetSpell` keeps reflecting the base tooltip — correct for an
|
||||
// append). We fix (a) by overwriting that header line — which lands at index
|
||||
// `numLines`-before-the-build (0-based) — with the real spell name via the
|
||||
// line pool's left-text FontString. Verified against `FUN_0052E610`.
|
||||
void AppendByID(void *L, int spellID) {
|
||||
if (spellID <= 0)
|
||||
return;
|
||||
void *tooltipObj = Game::Lua::ResolveObject(L, 1);
|
||||
if (tooltipObj == nullptr)
|
||||
return;
|
||||
auto *tt = static_cast<uint8_t *>(tooltipObj);
|
||||
|
||||
const int before = *reinterpret_cast<const int *>(
|
||||
tt + Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
|
||||
auto BuildSpellTooltip =
|
||||
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
|
||||
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, 0, 0, /*param_7 (append)=*/1);
|
||||
|
||||
const int after = *reinterpret_cast<const int *>(
|
||||
tt + Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
if (after <= before)
|
||||
return; // nothing appended (bad spellID / missing Spell.dbc record)
|
||||
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return;
|
||||
const int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + OFF_SPELL_NAME + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return;
|
||||
|
||||
// Left-text FontString array: descriptor at +0x324, data ptr at +0x8.
|
||||
auto **textLeft = *reinterpret_cast<void ***>(
|
||||
tt + Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 8);
|
||||
if (textLeft == nullptr)
|
||||
return;
|
||||
void *fs = textLeft[before];
|
||||
if (fs == nullptr)
|
||||
return;
|
||||
using SetText_t = void(__thiscall *)(void *fs, const char *text, int flag);
|
||||
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(fs, name, 0);
|
||||
}
|
||||
|
||||
static int __fastcall Script_GameTooltipAddSpellByID(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE ||
|
||||
!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:AddSpellByID(spellID)");
|
||||
return 0;
|
||||
}
|
||||
AppendByID(L, static_cast<int>(Game::Lua::ToNumber(L, 2)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `GameTooltip:GetSpell()` → (name, rank, spellID) for whichever spell
|
||||
// the tooltip is currently displaying, or nothing if it isn't showing
|
||||
// a spell. BuildSpellTooltip writes the spellID to `tooltip+0x39C`;
|
||||
@@ -126,6 +191,7 @@ static int __fastcall Script_GameTooltipHasSpell(void *L) {
|
||||
|
||||
static const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetSpellByID", &Script_GameTooltipSetSpellByID},
|
||||
{"AddSpellByID", &Script_GameTooltipAddSpellByID},
|
||||
{"GetSpell", &Script_GameTooltipGetSpell},
|
||||
{"HasSpell", &Script_GameTooltipHasSpell},
|
||||
};
|
||||
|
||||
@@ -26,4 +26,13 @@ namespace Spell::Tooltip {
|
||||
// so we render the rank-1 spell tooltip instead).
|
||||
void ShowByID(void *L, int spellID);
|
||||
|
||||
// Like `ShowByID` but APPENDS the spell's tooltip to the current one
|
||||
// (no clear), and overwrites the engine's talent "next rank" header with
|
||||
// the real spell name. Backs `GameTooltip:AddSpellByID`. Silent no-op if
|
||||
// `spellID <= 0`, stack[1] isn't a frame object, or the build added no
|
||||
// lines. Does not stash the displayed spellID (so `GetSpell` still
|
||||
// reflects the base tooltip). Useful for cross-class talent tooltips
|
||||
// (`AddLine(talentName) + AppendByID(rankSpellID)`).
|
||||
void AppendByID(void *L, int spellID);
|
||||
|
||||
} // namespace Spell::Tooltip
|
||||
|
||||
@@ -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
|
||||
@@ -366,6 +366,80 @@ int __fastcall Script_GetAllTaxiNodes(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Direct-edge test — mirrors FUN_004dbad0: a 256x256 [from][to] matrix of
|
||||
// TaxiPath IDs (0 = no direct flight path). IDs outside 1..256 are never direct.
|
||||
bool DirectEdge(int from, int to) {
|
||||
if (from <= 0 || from > Offsets::TAXI_ADJACENCY_DIM ||
|
||||
to <= 0 || to > Offsets::TAXI_ADJACENCY_DIM)
|
||||
return false;
|
||||
const int *adj = reinterpret_cast<const int *>(Offsets::VAR_TAXI_ADJACENCY);
|
||||
return adj[from * Offsets::TAXI_ADJACENCY_DIM + to] != 0;
|
||||
}
|
||||
|
||||
// `C_TaxiMap.GetTaxiRoute(slotIndex)` — the TaxiNodes.dbc ID chain the client
|
||||
// would fly to the taxi-map slot `slotIndex` (1..NumTaxiNodes), in flight
|
||||
// order { currentNodeID, ..., destNodeID }. This mirrors TakeTaxiNode's own
|
||||
// decision (FUN_004dc8d0): a direct hop when a TaxiPath edge exists, otherwise
|
||||
// the client's precomputed multi-hop route array. It is the exact node list
|
||||
// TakeTaxiNode would hand the server, so an addon can identify every hop
|
||||
// without matching flight-map coordinates back to nodes. Returns nothing when
|
||||
// no taxi map is open, the slot is out of range, it is the current node, or no
|
||||
// route exists.
|
||||
int __fastcall Script_GetTaxiRoute(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
const int slot1 = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int count =
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_TAXI_NODE_COUNT);
|
||||
if (count <= 0 || slot1 < 1 || slot1 > count)
|
||||
return 0;
|
||||
|
||||
const auto *arrayBase =
|
||||
reinterpret_cast<const uint8_t *>(Offsets::VAR_TAXI_NODE_ARRAY);
|
||||
const uint8_t *slotRec =
|
||||
arrayBase + (slot1 - 1) * Offsets::TAXI_NODE_STRIDE;
|
||||
|
||||
const uint8_t *curRec = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_TAXI_CURRENT_REC);
|
||||
const uint8_t *dstRec = *reinterpret_cast<const uint8_t *const *>(
|
||||
slotRec + Offsets::OFF_TAXI_SLOT_RECORD);
|
||||
if (curRec == nullptr || dstRec == nullptr)
|
||||
return 0;
|
||||
const int currentID = IntField(curRec, 0);
|
||||
const int destID = IntField(dstRec, 0);
|
||||
if (currentID == 0 || destID == 0 || currentID == destID)
|
||||
return 0;
|
||||
|
||||
// Collect the chain exactly as FUN_004dc8d0 (TakeTaxiNode) would send it.
|
||||
int ids[64];
|
||||
int n = 0;
|
||||
if (DirectEdge(currentID, destID)) {
|
||||
ids[n++] = currentID;
|
||||
ids[n++] = destID;
|
||||
} else if (*(slotRec + Offsets::OFF_TAXI_SLOT_MULTIHOP) != 0) {
|
||||
const int *route = *reinterpret_cast<const int *const *>(
|
||||
slotRec + Offsets::OFF_TAXI_SLOT_ROUTE);
|
||||
const int rc = *reinterpret_cast<const int *>(
|
||||
slotRec + Offsets::OFF_TAXI_SLOT_ROUTE_COUNT);
|
||||
if (route == nullptr || rc < 2)
|
||||
return 0;
|
||||
const int cap = rc < 64 ? rc : 64;
|
||||
for (int i = 0; i < cap; ++i)
|
||||
ids[n++] = route[i];
|
||||
} else {
|
||||
return 0; // reachable node with no computed route (shouldn't happen)
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(ids[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
static const Game::Lua::EnumIntegerEntry kFaction[] = {
|
||||
{"Neutral", FACTION_NEUTRAL},
|
||||
@@ -385,6 +459,8 @@ void RegisterLuaFunctions() {
|
||||
&Script_GetTaxiNodesForMap);
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetAllTaxiNodes",
|
||||
&Script_GetAllTaxiNodes);
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetTaxiRoute",
|
||||
&Script_GetTaxiRoute);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
// 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_TaxiMap.GetTaxiPaths()` and `C_TaxiMap.GetTaxiPathWaypoints(pathID)` —
|
||||
// direct readers for `TaxiPath.dbc` (the directed flight-path edge list with
|
||||
// costs) and `TaxiPathNode.dbc` (each path's world-coordinate waypoint
|
||||
// polyline). Vanilla exposes neither; addons can't read DBCs at all, so this
|
||||
// hands them the raw taxi graph + geometry.
|
||||
//
|
||||
// Together they let an addon build the taxi routing graph (nodes + weighted
|
||||
// directed edges) and measure real flight distances (sum the 3D chords of a
|
||||
// path's waypoints). Uses for a flight-path database, a map overlay, or a
|
||||
// travel-time estimator — see the sibling `C_TaxiMap.GetTaxiNodesForMap` for
|
||||
// the node table these edges connect.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Taxi::Paths {
|
||||
|
||||
namespace {
|
||||
|
||||
int IntField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const int *>(rec + off);
|
||||
}
|
||||
float FloatField(const uint8_t *rec, int off) {
|
||||
return *reinterpret_cast<const float *>(rec + off);
|
||||
}
|
||||
|
||||
// `C_TaxiMap.GetTaxiPaths()` — every row of TaxiPath.dbc as
|
||||
// { pathID, fromNodeID, toNodeID, cost }. Directed: A->B and B->A are two
|
||||
// separate entries.
|
||||
int __fastcall Script_GetTaxiPaths(void *L) {
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
const int count = *reinterpret_cast<const int *>(Offsets::VAR_TAXIPATH_COUNT);
|
||||
int outIdx = 0;
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXIPATH_RECORDS,
|
||||
Offsets::VAR_TAXIPATH_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
|
||||
outIdx += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "pathID", id);
|
||||
Game::Lua::SetFieldNumber(L, "fromNodeID", IntField(rec, Offsets::OFF_TAXIPATH_FROM));
|
||||
Game::Lua::SetFieldNumber(L, "toNodeID", IntField(rec, Offsets::OFF_TAXIPATH_TO));
|
||||
Game::Lua::SetFieldNumber(L, "cost", IntField(rec, Offsets::OFF_TAXIPATH_COST));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_TaxiMap.GetTaxiPathWaypoints(pathID)` — the path's waypoints as
|
||||
// { x, y, z, mapID }, ordered by node index (flight order). Returns nothing
|
||||
// for a missing / unknown pathID or an empty path.
|
||||
int __fastcall Script_GetTaxiPathWaypoints(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1))
|
||||
return 0;
|
||||
const int wantPath = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (wantPath <= 0)
|
||||
return 0;
|
||||
|
||||
struct Waypoint {
|
||||
int index;
|
||||
float x, y, z;
|
||||
int mapID;
|
||||
};
|
||||
std::vector<Waypoint> pts;
|
||||
const int count = *reinterpret_cast<const int *>(Offsets::VAR_TAXIPATHNODE_COUNT);
|
||||
for (int id = 1; id <= count; ++id) {
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_TAXIPATHNODE_RECORDS,
|
||||
Offsets::VAR_TAXIPATHNODE_COUNT,
|
||||
static_cast<uint32_t>(id));
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
if (IntField(rec, Offsets::OFF_TAXIPATHNODE_PATH_ID) != wantPath)
|
||||
continue;
|
||||
pts.push_back({IntField(rec, Offsets::OFF_TAXIPATHNODE_INDEX),
|
||||
FloatField(rec, Offsets::OFF_TAXIPATHNODE_X),
|
||||
FloatField(rec, Offsets::OFF_TAXIPATHNODE_Y),
|
||||
FloatField(rec, Offsets::OFF_TAXIPATHNODE_Z),
|
||||
IntField(rec, Offsets::OFF_TAXIPATHNODE_MAP)});
|
||||
}
|
||||
if (pts.empty())
|
||||
return 0;
|
||||
|
||||
// DBC rows for a path are stored in index order, but sort to guarantee a
|
||||
// dense 1-based (ipairs-safe) array in flight order regardless.
|
||||
std::sort(pts.begin(), pts.end(),
|
||||
[](const Waypoint &a, const Waypoint &b) { return a.index < b.index; });
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
int outIdx = 0;
|
||||
for (const Waypoint &p : pts) {
|
||||
outIdx += 1;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(outIdx));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "x", p.x);
|
||||
Game::Lua::SetFieldNumber(L, "y", p.y);
|
||||
Game::Lua::SetFieldNumber(L, "z", p.z);
|
||||
Game::Lua::SetFieldNumber(L, "mapID", p.mapID);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetTaxiPaths",
|
||||
&Script_GetTaxiPaths);
|
||||
Game::Lua::RegisterTableFunction("C_TaxiMap", "GetTaxiPathWaypoints",
|
||||
&Script_GetTaxiPathWaypoints);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Taxi::Paths
|
||||
@@ -0,0 +1,657 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetTotemInfo(slot)` — the four-element totem bar, backported. Vanilla
|
||||
// 1.12 tracks nothing about the shaman's totems client-side (the totem
|
||||
// bar, PLAYER_TOTEM_UPDATE, and GetTotemInfo are all TBC additions), so
|
||||
// this is a self-tracking subsystem — but a fully DATA-DRIVEN one:
|
||||
//
|
||||
// * SLOT is read straight from the totem summon spell's `Spell.dbc`
|
||||
// effect: 87/88/89/90 = SUMMON_TOTEM_SLOT1..4 = Fire/Earth/Water/Air,
|
||||
// so `slot = effect - 86`. No hardcoded spell table; Turtle custom
|
||||
// totems self-classify.
|
||||
// * CREATURE ENTRY is the effect's `EffectMiscValue` (the summoned
|
||||
// totem's creature-template ID) — used to detect the totem's death.
|
||||
// * DURATION is the summon spell's `SpellDuration.dbc` value (via the
|
||||
// engine's own duration helper, so player duration mods apply).
|
||||
//
|
||||
// Detection: the player's totem cast fires SMSG_SPELL_GO, which
|
||||
// `Aura::Source` already co-hooks; it calls `OnPlayerSpellGo` here (before
|
||||
// its aura gate, since a summon applies no aura). We stamp the slot with
|
||||
// start time + duration.
|
||||
//
|
||||
// Death / removal: on WorldTick we scan the object manager for a
|
||||
// player-owned creature of the slot's entry. A "seen once, then gone"
|
||||
// latch clears the slot early (totem killed, Totemic Recall, or manually
|
||||
// destroyed) — the owner check (CreatedBy == player) means another
|
||||
// shaman's same-entry totem can't keep ours alive, and the seen-latch
|
||||
// tolerates the ~1-tick spawn lag before the totem appears. Duration is
|
||||
// the backstop for the normal case. A recast into the same slot simply
|
||||
// re-stamps it.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "event/SignalHook.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "item/Count.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "ui/ColorData.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Totem::Tracker {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc record field offsets (see spell/Info.cpp for the shared set).
|
||||
constexpr int OFF_EFFECT = 0xF4; // int32[3] — SPELL_EFFECT_*
|
||||
constexpr int OFF_TOTEM_TOOL = 0xA0; // int32[2] — Totem (required tool item IDs)
|
||||
constexpr int OFF_MISC_VALUE = 0x1A8; // int32[3] — EffectMiscValue
|
||||
constexpr int OFF_ICON_ID = 0x1D4; // int32 — SpellIconID
|
||||
constexpr int OFF_NAME = 0x1E0; // char*[9] locale array
|
||||
|
||||
// SUMMON_TOTEM_SLOT1..4 spell effects (verified against Spell.dbc: Searing
|
||||
// 87/Fire, Stoneskin 88/Earth, Healing Stream 89/Water, Windfury 90/Air).
|
||||
constexpr int kEffectSummonTotemSlot1 = 87;
|
||||
constexpr int kEffectSummonTotemSlot4 = 90;
|
||||
constexpr int kSlots = 4;
|
||||
|
||||
|
||||
// Re-scan the object manager for totem existence at most this often.
|
||||
constexpr uint32_t kScanIntervalMs = 250;
|
||||
|
||||
struct Slot {
|
||||
bool active;
|
||||
bool seen; // the totem creature has been observed in the world
|
||||
uint32_t spellID;
|
||||
uint32_t entry; // summoned creature-template ID (for death detection)
|
||||
uint32_t startMs; // FUN_OS_TICKCOUNT_MS (shares GetTime's epoch × 1000)
|
||||
uint32_t durationMs;
|
||||
};
|
||||
|
||||
Slot g_slots[kSlots] = {};
|
||||
uint32_t g_lastScanMs = 0;
|
||||
|
||||
// Per-slot totem tool item ID (Fire/Earth/Water/Air), read from the totem
|
||||
// spell's `Totem[0]` field — the required TOOL the spell lists (Earth
|
||||
// Totem, Fire Totem, …), NOT a consumed reagent, and NOT hardcoded. Filled
|
||||
// from the player's spellbook on SPELLS_CHANGED and refreshed at cast; 0 =
|
||||
// not yet resolved. `g_spellsChangedId` caches the event id.
|
||||
uint32_t g_toolItem[kSlots] = {};
|
||||
int g_spellsChangedId = -1;
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// The totem tool item a spell requires (`Totem[0]`), or 0.
|
||||
uint32_t TotemToolItem(const uint8_t *rec) {
|
||||
const int32_t t = *reinterpret_cast<const int32_t *>(rec + OFF_TOTEM_TOOL);
|
||||
return t > 0 ? static_cast<uint32_t>(t) : 0;
|
||||
}
|
||||
|
||||
// Fill any still-unknown slot tool items by scanning the player's spellbook
|
||||
// for totem summons and reading each one's required tool. Tools are fixed
|
||||
// per element, so once a slot is resolved it never changes and the scan
|
||||
// early-outs. Walks the flat learned-spell array `VAR_PLAYER_SPELLBOOK`
|
||||
// (the same list `GetSpellName` reads) — the `IsPlayerSpell` bitmap
|
||||
// doesn't reliably carry totems. Driven by SPELLS_CHANGED (when the
|
||||
// spellbook is populated), so no polling.
|
||||
void ScanTools() {
|
||||
bool anyUnknown = false;
|
||||
for (int s = 0; s < kSlots; ++s)
|
||||
if (g_toolItem[s] == 0) {
|
||||
anyUnknown = true;
|
||||
break;
|
||||
}
|
||||
if (!anyUnknown)
|
||||
return;
|
||||
|
||||
auto *book = reinterpret_cast<const int32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELLBOOK));
|
||||
for (int slot = 0; slot < Offsets::SPELLBOOK_MAX_SLOTS; ++slot) {
|
||||
const int spellID = book[slot];
|
||||
if (spellID <= 0)
|
||||
continue;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
continue;
|
||||
const auto *eff = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int e = eff[i];
|
||||
if (e < kEffectSummonTotemSlot1 || e > kEffectSummonTotemSlot4)
|
||||
continue;
|
||||
const int s = e - kEffectSummonTotemSlot1;
|
||||
if (g_toolItem[s] == 0)
|
||||
g_toolItem[s] = TotemToolItem(rec);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-resolve tool items whenever the player's spells (re)load.
|
||||
// SPELLS_CHANGED is a no-arg event through the same dispatcher
|
||||
// Event::SignalHook owns, so we observe it there (never suppress — return
|
||||
// false). By the time it fires the spellbook is populated, so the scan
|
||||
// sees the totems.
|
||||
bool OnSignal(int eventID) {
|
||||
if (g_spellsChangedId < 0)
|
||||
g_spellsChangedId = Event::Custom::LookupByName("SPELLS_CHANGED");
|
||||
if (eventID == g_spellsChangedId)
|
||||
ScanTools();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _spellsChanged{&OnSignal};
|
||||
|
||||
uint32_t SpellDurationMs(const uint8_t *rec) {
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *rec, int unit,
|
||||
char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(rec, 0, 0);
|
||||
return ms > 0 ? static_cast<uint32_t>(ms) : 0;
|
||||
}
|
||||
|
||||
// ---- object-manager existence scan (player-owned creature by entry) ------
|
||||
|
||||
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);
|
||||
|
||||
struct ScanCtx {
|
||||
uint64_t playerGuid;
|
||||
uint32_t entries[kSlots]; // 0 = slot not being checked
|
||||
bool present[kSlots];
|
||||
};
|
||||
|
||||
int __fastcall ScanCallback(ScanCtx *ctx, void * /*unusedEdx*/, uint64_t guid) {
|
||||
if (!Guid::IsCreature(guid))
|
||||
return 1;
|
||||
const uint32_t entry = static_cast<uint32_t>((guid >> 24) & 0xFFFFFFu);
|
||||
if (entry == 0)
|
||||
return 1;
|
||||
int match = -1;
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
if (ctx->entries[s] == entry) {
|
||||
match = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match < 0)
|
||||
return 1;
|
||||
|
||||
// Confirm it's OUR totem: resolve + check CreatedBy == player.
|
||||
auto ObjectPtr =
|
||||
reinterpret_cast<ObjectPtr_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(obj) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 1;
|
||||
const uint64_t createdBy = *reinterpret_cast<const uint64_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_CREATEDBY);
|
||||
if (createdBy == ctx->playerGuid)
|
||||
ctx->present[match] = true;
|
||||
return 1; // keep scanning — other slots may match other creatures
|
||||
}
|
||||
|
||||
// On-demand lookup of the live GUID of the player's totem creature of a
|
||||
// given entry (for TargetTotem). Returns 0 when none is visible.
|
||||
struct FindGuidCtx {
|
||||
uint64_t playerGuid;
|
||||
uint32_t entry;
|
||||
uint64_t found;
|
||||
};
|
||||
|
||||
int __fastcall FindGuidCallback(FindGuidCtx *ctx, void * /*unusedEdx*/,
|
||||
uint64_t guid) {
|
||||
if (!Guid::IsCreature(guid))
|
||||
return 1;
|
||||
if (static_cast<uint32_t>((guid >> 24) & 0xFFFFFFu) != ctx->entry)
|
||||
return 1;
|
||||
auto ObjectPtr =
|
||||
reinterpret_cast<ObjectPtr_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *obj = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (obj == nullptr)
|
||||
return 1;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(obj) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 1;
|
||||
if (*reinterpret_cast<const uint64_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_CREATEDBY) == ctx->playerGuid) {
|
||||
ctx->found = guid;
|
||||
return 0; // stop
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint64_t FindTotemGuid(uint32_t entry) {
|
||||
if (entry == 0)
|
||||
return 0;
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) ==
|
||||
nullptr)
|
||||
return 0;
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
if (player == 0)
|
||||
return 0;
|
||||
FindGuidCtx ctx{player, entry, 0};
|
||||
auto Enum = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
Enum(reinterpret_cast<EnumCallback_t>(&FindGuidCallback), &ctx);
|
||||
return ctx.found;
|
||||
}
|
||||
|
||||
// PLAYER_TOTEM_UPDATE(totemSlot 1-4) — a TBC event absent from vanilla's
|
||||
// table, reserved as a custom event and fired when a slot's state changes
|
||||
// (totem dropped, expired, killed, or recalled). Changes are recorded in a
|
||||
// dirty mask by the cast path and the clear paths, then flushed from OnTick
|
||||
// (WorldTick context — safe to run Lua handlers, unlike mid-packet).
|
||||
constexpr const char *kPlayerTotemUpdate = "PLAYER_TOTEM_UPDATE";
|
||||
const Event::Custom::AutoReserve _totemUpdate{kPlayerTotemUpdate};
|
||||
int g_totemUpdateId = -1;
|
||||
uint8_t g_dirtyMask = 0;
|
||||
|
||||
void MarkDirty(int slotIdx) {
|
||||
g_dirtyMask |= static_cast<uint8_t>(1u << slotIdx);
|
||||
}
|
||||
|
||||
void FireDirty() {
|
||||
if (g_dirtyMask == 0)
|
||||
return;
|
||||
if (g_totemUpdateId < 0)
|
||||
g_totemUpdateId = Event::Custom::LookupByName(kPlayerTotemUpdate);
|
||||
if (g_totemUpdateId < 0)
|
||||
return; // slot not claimed yet — keep dirty, retry next tick
|
||||
for (int s = 0; s < kSlots; ++s)
|
||||
if (g_dirtyMask & (1u << s))
|
||||
Event::Custom::Fire(g_totemUpdateId, "%d", s + 1);
|
||||
g_dirtyMask = 0;
|
||||
}
|
||||
|
||||
void OnTick() {
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
// Duration backstop — always cheap, runs every tick.
|
||||
bool anyActive = false;
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
Slot &t = g_slots[s];
|
||||
if (!t.active)
|
||||
continue;
|
||||
if (t.durationMs != 0 && now - t.startMs >= t.durationMs) {
|
||||
t.active = false;
|
||||
MarkDirty(s);
|
||||
continue;
|
||||
}
|
||||
anyActive = true;
|
||||
}
|
||||
|
||||
// Death / removal scan — throttled, and only while a slot is up. The
|
||||
// object-manager enumerator derefs the manager unconditionally, so it's
|
||||
// gated on the local player existing.
|
||||
if (anyActive && now - g_lastScanMs >= kScanIntervalMs &&
|
||||
*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) !=
|
||||
nullptr) {
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
if (player != 0) {
|
||||
g_lastScanMs = now;
|
||||
ScanCtx ctx{};
|
||||
ctx.playerGuid = player;
|
||||
for (int s = 0; s < kSlots; ++s)
|
||||
ctx.entries[s] = g_slots[s].active ? g_slots[s].entry : 0;
|
||||
|
||||
auto Enum = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
Enum(reinterpret_cast<EnumCallback_t>(&ScanCallback), &ctx);
|
||||
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
Slot &t = g_slots[s];
|
||||
if (!t.active)
|
||||
continue;
|
||||
if (ctx.present[s])
|
||||
t.seen = true;
|
||||
else if (t.seen) {
|
||||
t.active = false; // was up, now gone → died / recalled
|
||||
MarkDirty(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FireDirty();
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
// SpellIcon.dbc path for an icon ID (records indexed by ID; path at +4).
|
||||
const char *IconPath(int iconID) {
|
||||
if (iconID <= 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_ICON_COUNT));
|
||||
if (iconID > count)
|
||||
return nullptr;
|
||||
auto *const *records = *reinterpret_cast<const uint8_t *const *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_ICON_RECORDS));
|
||||
if (records == nullptr || records[iconID] == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(records[iconID] + 4);
|
||||
}
|
||||
|
||||
// Pushes the empty tail (no active totem) after `haveTotem` has been
|
||||
// pushed by the caller. Returns the total count including that first value.
|
||||
int PushEmptyTail(void *L) {
|
||||
Game::Lua::PushString(L, ""); // totemName
|
||||
Game::Lua::PushNumber(L, 0.0); // startTime
|
||||
Game::Lua::PushNumber(L, 0.0); // duration
|
||||
Game::Lua::PushNil(L); // icon
|
||||
Game::Lua::PushNumber(L, 1.0); // modRate
|
||||
Game::Lua::PushNumber(L, 0.0); // spellID
|
||||
return 7;
|
||||
}
|
||||
|
||||
// `GetTotemInfo(slot)` → `haveTotem, totemName, startTime, duration, icon,
|
||||
// modRate, spellID`. Slots: 1 Fire, 2 Earth, 3 Water, 4 Air. `haveTotem`
|
||||
// is TOOL presence (the documented meaning) — whether the player carries
|
||||
// the slot's totem tool item (Earth/Fire/Water/Air Totem) — NOT whether a
|
||||
// totem is summoned; the active totem is signaled by a non-empty
|
||||
// `totemName` / non-zero `startTime`.
|
||||
int __fastcall Script_GetTotemInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetTotemInfo(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (slot < 1 || slot > kSlots) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return PushEmptyTail(L);
|
||||
}
|
||||
|
||||
// Tool presence — independent of the summoned state. The tool item
|
||||
// (resolved from the totem spell's Totem[0] on SPELLS_CHANGED, not
|
||||
// hardcoded) is checked against the bag. The bag walk reads the Lua
|
||||
// stack, so it runs before we push any return values (`slot` captured).
|
||||
const uint32_t tool = g_toolItem[slot - 1];
|
||||
const bool haveTotem =
|
||||
tool != 0 && Item::Count::InInventory(L, static_cast<int>(tool)) > 0;
|
||||
|
||||
const Slot &t = g_slots[slot - 1];
|
||||
if (!t.active) {
|
||||
Game::Lua::PushBool(L, haveTotem);
|
||||
return PushEmptyTail(L);
|
||||
}
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(t.spellID));
|
||||
const char *name = "";
|
||||
const char *icon = nullptr;
|
||||
if (rec != nullptr) {
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_LOCALE_INDEX));
|
||||
const char *n = *reinterpret_cast<const char *const *>(
|
||||
rec + OFF_NAME + locale * 4);
|
||||
if (n != nullptr)
|
||||
name = n;
|
||||
icon = IconPath(*reinterpret_cast<const int *>(rec + OFF_ICON_ID));
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, haveTotem);
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(t.startMs) * 0.001);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(t.durationMs) / 1000.0);
|
||||
if (icon != nullptr)
|
||||
Game::Lua::PushString(L, icon);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, 1.0); // modRate — no per-totem haste in 1.12
|
||||
Game::Lua::PushNumber(L, static_cast<double>(t.spellID));
|
||||
return 7;
|
||||
}
|
||||
|
||||
// `GetTotemTimeLeft(slot)` → seconds until the slot's totem is auto-
|
||||
// destroyed, or `0` when no totem is active in that slot. Slots: 1 Fire,
|
||||
// 2 Earth, 3 Water, 4 Air.
|
||||
int __fastcall Script_GetTotemTimeLeft(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetTotemTimeLeft(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
double seconds = 0.0;
|
||||
if (slot >= 1 && slot <= kSlots) {
|
||||
const Slot &t = g_slots[slot - 1];
|
||||
if (t.active && t.durationMs != 0) {
|
||||
const uint32_t now = NowMs();
|
||||
const uint32_t end = t.startMs + t.durationMs;
|
||||
if (now < end)
|
||||
seconds = static_cast<double>(end - now) / 1000.0;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushNumber(L, seconds);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `GetTotemDuration(slot)` → the total duration (seconds) of the slot's
|
||||
// active totem, or `0` when none. This is the totem's full lifetime from
|
||||
// cast (the 4th value of GetTotemInfo); see GetTotemTimeLeft for the
|
||||
// remaining time.
|
||||
int __fastcall Script_GetTotemDuration(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetTotemDuration(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
double seconds = 0.0;
|
||||
if (slot >= 1 && slot <= kSlots) {
|
||||
const Slot &t = g_slots[slot - 1];
|
||||
if (t.active)
|
||||
seconds = static_cast<double>(t.durationMs) / 1000.0;
|
||||
}
|
||||
Game::Lua::PushNumber(L, seconds);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `TargetTotem(slot)` — target the player's totem in the given slot
|
||||
// (1 Fire, 2 Earth, 3 Water, 4 Air). No-op when the slot has no active
|
||||
// totem or the totem creature isn't currently visible. Finds the totem
|
||||
// creature's live GUID and sets the target via the same engine path
|
||||
// `TargetUnit` uses.
|
||||
int __fastcall Script_TargetTotem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: TargetTotem(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (slot < 1 || slot > kSlots)
|
||||
return 0;
|
||||
const Slot &t = g_slots[slot - 1];
|
||||
if (!t.active || t.entry == 0)
|
||||
return 0;
|
||||
uint64_t guid = FindTotemGuid(t.entry);
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
using TargetByGuid_t = void(__fastcall *)(const uint64_t *guid);
|
||||
reinterpret_cast<TargetByGuid_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_TARGET_BY_GUID))(&guid);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- GameTooltip:SetTotem(slot) -------------------------------------------
|
||||
//
|
||||
// Built with the engine's own tooltip primitives — the per-tooltip clear
|
||||
// and the raw add-line — exactly as retail's `Script_GameTooltip_SetTotem`
|
||||
// does (no Lua method dispatch). Colors are the packed `{b,g,r,a}` values
|
||||
// `FUN_GAMETOOLTIP_ADD_LINE` wants, pulled straight from `UI::ColorData`.
|
||||
|
||||
using AddLine_t = void(__thiscall *)(void *self, const char *left, const char *right,
|
||||
const void *leftColor, const void *rightColor, int wrap);
|
||||
using ClearTooltip_t = void(__fastcall *)(void *self);
|
||||
using ShowScript_t = int(__fastcall *)(void *L); // engine's Script_Show
|
||||
|
||||
// Retail's two line colors, read verbatim from the 3.3.5 binary's SetTotem
|
||||
// color data: name = NORMAL_FONT_COLOR (0xFFFFD200), time = white. Both are
|
||||
// already in the 0xAARRGGBB packing the line-color setter consumes.
|
||||
constexpr uint32_t kNameColor = UI::ColorData::ByTag("NORMAL_FONT_COLOR");
|
||||
constexpr uint32_t kTimeColor = UI::ColorData::ByTag("HIGHLIGHT_FONT_COLOR");
|
||||
|
||||
// Left-aligned colored line via the engine's raw add-line. `color` must
|
||||
// outlive the call (its address is passed through).
|
||||
void AddColoredLine(void *self, const char *text, const uint32_t &color) {
|
||||
reinterpret_cast<AddLine_t>(Offsets::FUN_GAMETOOLTIP_ADD_LINE)(
|
||||
self, text, nullptr, &color, nullptr, 0);
|
||||
}
|
||||
|
||||
// `GameTooltip:SetTotem(slot)` — a TBC (2.4.0) tooltip method backported to
|
||||
// 1.12. Fills the tooltip with the slot's active totem, mirroring retail's
|
||||
// two-line shape (3.3.5 `Script_GameTooltip_SetTotem` at 0x006205c0):
|
||||
//
|
||||
// <Totem Name> NORMAL_FONT_COLOR (yellow, 0xFFFFD200)
|
||||
// <Time Left> white — SecondsToTimeAbbrev(SPELL_TIME_REMAINING)
|
||||
//
|
||||
// The time line mirrors retail's `SecondsToTimeAbbrev` with its round-up
|
||||
// flag: raw seconds under a minute, minutes rounded up above. The unit
|
||||
// wording is localized through the project's GlobalString helper (retail's
|
||||
// `SPELL_TIME_REMAINING_SEC`/`_MIN` keys, English fallback). No-op (tooltip
|
||||
// left untouched) when the slot has no active totem — same as retail's
|
||||
// `+8/+0xc` timing guard. Shows itself at the end (via the engine's
|
||||
// Script_Show) — retail's TotemFrame.xml OnEnter calls SetTotem with no
|
||||
// trailing :Show(), so the method is expected to show.
|
||||
int __fastcall Script_GameTooltipSetTotem(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE ||
|
||||
!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:SetTotem(slot)");
|
||||
return 0;
|
||||
}
|
||||
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (slot < 1 || slot > kSlots)
|
||||
return 0;
|
||||
const Slot &t = g_slots[slot - 1];
|
||||
if (!t.active || t.spellID == 0)
|
||||
return 0;
|
||||
|
||||
void *self = Game::Lua::ResolveObject(L, 1);
|
||||
if (self == nullptr)
|
||||
return 0;
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(t.spellID));
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
const char *name =
|
||||
*reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return 0;
|
||||
|
||||
// Milliseconds remaining (0 = unknown / expired duration).
|
||||
uint32_t msLeft = 0;
|
||||
if (t.durationMs != 0) {
|
||||
const uint32_t now = NowMs();
|
||||
const uint32_t end = t.startMs + t.durationMs;
|
||||
if (now < end)
|
||||
msLeft = end - now;
|
||||
}
|
||||
|
||||
reinterpret_cast<ClearTooltip_t>(Offsets::FUN_GAMETOOLTIP_CLEAR)(self);
|
||||
AddColoredLine(self, name, kNameColor);
|
||||
|
||||
if (msLeft > 0) {
|
||||
const int savedTop = Game::Lua::GetTop(L);
|
||||
if (msLeft < 60000) {
|
||||
Game::Lua::PushLocalizedFormatInt(L, "SPELL_TIME_REMAINING_SEC",
|
||||
"%d Sec",
|
||||
static_cast<int>(msLeft / 1000));
|
||||
} else {
|
||||
const int minutes = static_cast<int>((msLeft + 59999) / 60000); // ceil
|
||||
Game::Lua::PushLocalizedFormatInt(L, "SPELL_TIME_REMAINING_MIN",
|
||||
"%d Min", minutes);
|
||||
}
|
||||
const char *timeText = Game::Lua::ToString(L, -1);
|
||||
if (timeText != nullptr)
|
||||
AddColoredLine(self, timeText, kTimeColor);
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
// Show ourselves — self is still at Lua stack index 1, which is what
|
||||
// Script_Show reads.
|
||||
reinterpret_cast<ShowScript_t>(Offsets::FUN_SCRIPT_FRAME_SHOW)(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetTotemInfo", &Script_GetTotemInfo);
|
||||
Game::Lua::RegisterGlobalFunction("GetTotemTimeLeft",
|
||||
&Script_GetTotemTimeLeft);
|
||||
Game::Lua::RegisterGlobalFunction("GetTotemDuration",
|
||||
&Script_GetTotemDuration);
|
||||
Game::Lua::RegisterGlobalFunction("TargetTotem", &Script_TargetTotem);
|
||||
|
||||
static const Game::Lua::FrameMethodEntry kTooltipMethods[] = {
|
||||
{"SetTotem", &Script_GameTooltipSetTotem},
|
||||
};
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_GAMETOOLTIP_METHOD_REGISTRY),
|
||||
kTooltipMethods,
|
||||
static_cast<int>(sizeof(kTooltipMethods) / sizeof(kTooltipMethods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnPlayerSpellGo(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
|
||||
const auto *effects = reinterpret_cast<const int32_t *>(rec + OFF_EFFECT);
|
||||
const auto *misc = reinterpret_cast<const int32_t *>(rec + OFF_MISC_VALUE);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
const int eff = effects[i];
|
||||
if (eff < kEffectSummonTotemSlot1 || eff > kEffectSummonTotemSlot4)
|
||||
continue;
|
||||
const int idx = eff - kEffectSummonTotemSlot1; // 0..3 (Fire..Air)
|
||||
Slot &t = g_slots[idx];
|
||||
t.active = true;
|
||||
t.seen = false;
|
||||
t.spellID = spellID;
|
||||
t.entry = static_cast<uint32_t>(misc[i]);
|
||||
t.startMs = NowMs();
|
||||
t.durationMs = SpellDurationMs(rec);
|
||||
MarkDirty(idx); // totem dropped → PLAYER_TOTEM_UPDATE (fired next tick)
|
||||
// The cast spell IS a totem of this element, so it names the slot's
|
||||
// tool item directly — capture it (covers the slot even before the
|
||||
// spellbook scan runs).
|
||||
if (g_toolItem[idx] == 0)
|
||||
g_toolItem[idx] = TotemToolItem(rec);
|
||||
return; // one totem-slot effect per spell
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Totem::Tracker
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Client-side totem-slot tracker backing `GetTotemInfo(slot)`. Vanilla
|
||||
// 1.12 has no totem-bar / PLAYER_TOTEM_UPDATE machinery (all TBC
|
||||
// additions), so we track the four element slots ourselves. See
|
||||
// Tracker.cpp for the design.
|
||||
namespace Totem::Tracker {
|
||||
|
||||
// Notify of a spell the LOCAL PLAYER just cast (from Aura::Source's
|
||||
// SpellGo hook — the totem summon doesn't apply an aura, so it must be
|
||||
// fed before that hook's aura gate). If `spellID` is a totem summon
|
||||
// (`Spell.dbc` effect 87-90 = SUMMON_TOTEM_SLOT1..4), it's recorded in the
|
||||
// matching element slot; otherwise ignored.
|
||||
void OnPlayerSpellGo(uint32_t spellID);
|
||||
|
||||
} // namespace Totem::Tracker
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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/>.
|
||||
|
||||
// Turtle WoW custom bag families. Turtle added four cooking/gathering bag
|
||||
// families as `ItemBagFamily.dbc` rows 10–13 (Meat, Fish, Leather,
|
||||
// Mining) — beyond retail vanilla, which stops at Keys (9). Turtle's own
|
||||
// bags normally carry `m_bagFamily` directly, so this mapping is a safety
|
||||
// net for any custom container shipped with an empty field; it plugs into
|
||||
// the stock `Item::BagFamily` resolver chain rather than polluting the
|
||||
// stock table with non-vanilla data.
|
||||
//
|
||||
// Subclass → raw family ID cross-referenced by name between
|
||||
// ItemSubClass.dbc (class 1, Container) and ItemBagFamily.dbc. NOTE: the
|
||||
// resulting bitmasks (`1 << (id-1)`) land on `0x200`–`0x1000`, which
|
||||
// collide with retail's Gem/Mining bag bits — see docs/API.md's
|
||||
// GetItemFamily family table for the caveat. The values stay
|
||||
// self-consistent within Turtle (a Meat Bag and the meat it holds both
|
||||
// report `0x200`).
|
||||
//
|
||||
// This file is the home for Turtle-only custom content that layers onto a
|
||||
// stock engine surface; add further custom-data modules alongside it.
|
||||
|
||||
#include "item/BagFamily.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Turtle::BagFamily {
|
||||
|
||||
namespace {
|
||||
|
||||
// Turtle custom Container-class subclasses → `ItemBagFamily.dbc` raw IDs.
|
||||
// Stock subclasses (Soul/Herb/Enchanting/Engineering) are handled by
|
||||
// `Item::BagFamily`'s own table and never reach here. Gated on Turtle
|
||||
// detection so the custom mapping can't misfire on a stock client (where
|
||||
// these subclasses don't exist as containers anyway).
|
||||
uint32_t Resolve(uint32_t itemClass, uint32_t subclass) {
|
||||
if (!Turtle::Detected() || itemClass != Item::BagFamily::kItemClassContainer)
|
||||
return 0;
|
||||
switch (subclass) {
|
||||
case 6: return 13; // Mining Bag → Mining Supplies
|
||||
case 7: return 12; // Leatherworking Bag → Leather
|
||||
case 8: return 10; // Meat Bag → Meat
|
||||
case 9: return 11; // Fish Bag → Fish
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const Item::BagFamily::AutoResolver _register{&Resolve};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Turtle::BagFamily
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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/>.
|
||||
|
||||
// Turtle WoW reworked Rip — known combo-finisher durations for a spell
|
||||
// whose client DBC row is missing. All six Rip ranks point at
|
||||
// SpellDuration row 87, which Turtle added on the SERVER only
|
||||
// ({8000, 0, 18000} — live data shows 10/12/14/16/18s at 1..5 CP, i.e.
|
||||
// 8s + 2s per combo point); the client patch never shipped the row (the
|
||||
// client table jumps 86 → 105), so the index dangles. Verified the only
|
||||
// dangling duration index in the entire client Spell.dbc.
|
||||
//
|
||||
// This supplies the values through `Aura::ComboDuration`'s dangling-row
|
||||
// resolver so the Turtle-specific data lives here, not in the stock
|
||||
// module. Unlike the other `src/turtle/` modules it does NOT gate on
|
||||
// `Turtle::Detected()`: the activation gate is the dangling-row condition
|
||||
// in `Aura::ComboDuration` (a client-data bug unique to this Rip), which
|
||||
// is the deliberate design — gate on the bug, not the realm. On a stock
|
||||
// client Rip resolves to a valid row and this resolver is never consulted;
|
||||
// if Turtle ever ships the missing client row, the DBC takes over and this
|
||||
// goes dormant automatically.
|
||||
|
||||
#include "aura/ComboDuration.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Turtle::ComboDuration {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Known {
|
||||
uint32_t spellId;
|
||||
int32_t baseMs;
|
||||
int32_t maxMs;
|
||||
};
|
||||
|
||||
constexpr Known kRip[] = {
|
||||
{1079, 8000, 18000}, {9492, 8000, 18000}, {9493, 8000, 18000},
|
||||
{9752, 8000, 18000}, {9894, 8000, 18000}, {9896, 8000, 18000},
|
||||
};
|
||||
|
||||
bool Resolve(uint32_t spellId, int32_t *baseMs, int32_t *maxMs) {
|
||||
for (const auto &k : kRip) {
|
||||
if (k.spellId == spellId) {
|
||||
*baseMs = k.baseMs;
|
||||
*maxMs = k.maxMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const Aura::ComboDuration::AutoDanglingResolver _register{&Resolve};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Turtle::ComboDuration
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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 "turtle/Detect.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Turtle {
|
||||
|
||||
namespace {
|
||||
|
||||
// Latched once we've read the global as a string — Turtle never un-sets
|
||||
// it, so a true result is permanent. A false read is NOT cached: FrameXML
|
||||
// may not have run yet, so we re-probe on each call until it lands.
|
||||
bool g_detected = false;
|
||||
char g_version[32] = {0};
|
||||
|
||||
void Probe() {
|
||||
if (g_detected)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
|
||||
// Read `_G["TURTLE_WOW_VERSION"]` without disturbing the caller's
|
||||
// stack. RawGet avoids any (nonexistent, but free-to-skip) metatable
|
||||
// side effects on the globals table.
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, "TURTLE_WOW_VERSION");
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
if (Game::Lua::IsString(L, -1)) {
|
||||
const char *v = Game::Lua::ToString(L, -1);
|
||||
if (v != nullptr) {
|
||||
const size_t n = std::strlen(v);
|
||||
const size_t copy =
|
||||
(n < sizeof(g_version) - 1) ? n : sizeof(g_version) - 1;
|
||||
std::memcpy(g_version, v, copy);
|
||||
g_version[copy] = '\0';
|
||||
}
|
||||
g_detected = true;
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Detected() {
|
||||
Probe();
|
||||
return g_detected;
|
||||
}
|
||||
|
||||
const char *Version() {
|
||||
Probe();
|
||||
return g_detected ? g_version : nullptr;
|
||||
}
|
||||
|
||||
} // namespace Turtle
|
||||
@@ -0,0 +1,37 @@
|
||||
// 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
|
||||
|
||||
// Turtle WoW client detection — the gate for every module under
|
||||
// `src/turtle/`. Turtle's FrameXML sets a `TURTLE_WOW_VERSION` global (see
|
||||
// `FrameXML/Globals.lua`, e.g. `"1.18.1"`); its presence is the
|
||||
// authoritative "this is the Turtle client" signal — no realm-list
|
||||
// sniffing (which conflates "connected to Turtle" with "running the
|
||||
// Turtle client" and was rejected for the Rip combo-duration work), no
|
||||
// binary fingerprinting.
|
||||
namespace Turtle {
|
||||
|
||||
// True iff running on a Turtle WoW client. Reads the `TURTLE_WOW_VERSION`
|
||||
// global and LATCHES true once seen. Safe to call any time: it returns
|
||||
// false until the in-game Lua state exists AND FrameXML's Globals.lua has
|
||||
// run (so it can read false very early in boot / on the glue screen), then
|
||||
// flips true and stays true. Never caches a false result, so an early
|
||||
// probe can't wrongly pin a Turtle client as non-Turtle.
|
||||
bool Detected();
|
||||
|
||||
// The Turtle client version string (e.g. `"1.18.1"`), or nullptr when not
|
||||
// Turtle / not yet detected. Points at an internally-cached copy.
|
||||
const char *Version();
|
||||
|
||||
} // namespace Turtle
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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/>.
|
||||
|
||||
// Turtle WoW custom healing auras. Aura 199
|
||||
// (MOD_SPELL_HEALING_OF_ARMOR_PERCENT, "Ironclad") adds
|
||||
// `Armor × amount / 100` to the player's spell healing bonus — not a stock
|
||||
// vanilla aura index. Plugs into `Spell::HealingAura`'s resolver chain so
|
||||
// the Turtle-specific aura semantic lives here, not in the stock
|
||||
// GetSpellBonusHealing decoder. Gated on Turtle detection (the aura index
|
||||
// is a generic slot number that could mean something else on another
|
||||
// server).
|
||||
|
||||
#include "spell/HealingAura.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Turtle::HealingAura {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kAuraModHealingOfArmorPercent = 199; // × Armor (Ironclad)
|
||||
|
||||
long Resolve(int auraIndex, long amount, int32_t /*spirit*/, int32_t armor) {
|
||||
if (!Turtle::Detected() || auraIndex != kAuraModHealingOfArmorPercent)
|
||||
return 0;
|
||||
return static_cast<long>(armor) * amount / 100;
|
||||
}
|
||||
|
||||
const Spell::HealingAura::AutoResolver _register{&Resolve};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Turtle::HealingAura
|
||||
@@ -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
|
||||
@@ -0,0 +1,64 @@
|
||||
// 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/>.
|
||||
|
||||
// `ClosestUnitPosition(creatureID)` → `xPos, yPos, distance` — the world
|
||||
// position of the nearest creature with the given NPC (creature-template)
|
||||
// ID, and its distance from the player in yards. Returns nothing when no
|
||||
// matching creature is found (the modern `MayReturnNothing` contract).
|
||||
//
|
||||
// Semantic note vs. retail: modern WoW's `ClosestUnitPosition` reads a
|
||||
// static client-side spawn database and only works for starting-zone mobs
|
||||
// (a tutorial/help helper). Vanilla 1.12 has no such database, so we
|
||||
// implement the useful, 1.12-native reading: the closest creature of that
|
||||
// entry that is CURRENTLY VISIBLE (in the client's object manager /
|
||||
// broadcast window). The shared scan lives in Object::ClosestByEntry.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "object/ClosestByEntry.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::ClosestPosition {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_ClosestUnitPosition(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: ClosestUnitPosition(creatureID)");
|
||||
return 0;
|
||||
}
|
||||
const auto entry = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
|
||||
const Object::ClosestByEntry::Result r = Object::ClosestByEntry::Find(
|
||||
Offsets::TYPEMASK_UNIT, Guid::Type::Creature, entry);
|
||||
if (!r.found)
|
||||
return 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.x));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.y));
|
||||
Game::Lua::PushNumber(L, std::sqrt(static_cast<double>(r.distSq)));
|
||||
return 3;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("ClosestUnitPosition",
|
||||
&Script_ClosestUnitPosition);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::ClosestPosition
|
||||
+114
-9
@@ -33,6 +33,17 @@
|
||||
// every world tick — when it returns null, we `Set(0)` which fires
|
||||
// the event. Cost is one hash-table lookup per tick while focus is
|
||||
// set; zero when no focus is active.
|
||||
//
|
||||
// Unit events for `focus` / `focustarget`: `Unit::TokenObserver` watches
|
||||
// the focus unit's descriptor fields so `UNIT_HEALTH` / `UNIT_AURA` / …
|
||||
// fire with `arg1 == "focus"` (see FocusFieldCb). `focustarget` gets the
|
||||
// same treatment via a second observer — but its GUID is the focus unit's
|
||||
// *current target*, which changes with no event to signal it (vanilla's
|
||||
// `UNIT_FIELD_TARGET`, field 10, has no event-table slot). So the WorldTick
|
||||
// watcher re-reads the focus's target each tick and re-points the
|
||||
// focustarget observer only when that identity changes; the focustarget
|
||||
// *events* still fire event-driven through the observer — only the
|
||||
// re-pointing is tick-gated (≤1 tick lag after the focus swaps targets).
|
||||
|
||||
#include "Focus.h"
|
||||
|
||||
@@ -40,6 +51,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/TokenObserver.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -52,6 +64,43 @@ const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
|
||||
uint64_t g_focusGUID = 0;
|
||||
|
||||
// Descriptor-field observer callback — fires the changed field's unit event
|
||||
// with the `"focus"` token, making `arg1 == "focus"` work for `UNIT_HEALTH`,
|
||||
// `UNIT_MANA`, `UNIT_AURA`, … exactly like a native token. Registered per
|
||||
// focus unit via `Unit::TokenObserver` (mirrors the engine's own watch of
|
||||
// target/party/raid units). The event id is the field index (`fieldOffset >>
|
||||
// 2`); the engine fires every unit event with format "%s" + the token, so we
|
||||
// match that. Guarded on the live focus GUID so an in-flight field change that
|
||||
// races an unregister can't fire a stale token.
|
||||
int __fastcall FocusFieldCb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const uint32_t * /*oldValue*/, void * /*userArg*/) {
|
||||
if (guidLo != static_cast<uint32_t>(g_focusGUID) ||
|
||||
guidHi != static_cast<uint32_t>(g_focusGUID >> 32))
|
||||
return 1;
|
||||
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
|
||||
reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT))(
|
||||
static_cast<int>(fieldOffset >> 2), "%s", "focus");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// GUID currently watched as `focustarget` (the focus unit's target), or 0.
|
||||
// Re-pointed by `OnWorldTick`; distinct from `g_focusGUID`.
|
||||
uint64_t g_focusTargetGUID = 0;
|
||||
|
||||
// Same as `FocusFieldCb` but fires the `"focustarget"` token.
|
||||
int __fastcall FocusTargetFieldCb(uint32_t fieldOffset, uint32_t /*size*/,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const uint32_t * /*oldValue*/, void * /*userArg*/) {
|
||||
if (guidLo != static_cast<uint32_t>(g_focusTargetGUID) ||
|
||||
guidHi != static_cast<uint32_t>(g_focusTargetGUID >> 32))
|
||||
return 1;
|
||||
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
|
||||
reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT))(
|
||||
static_cast<int>(fieldOffset >> 2), "%s", "focustarget");
|
||||
return 1;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -71,15 +120,39 @@ uint64_t ResolveTokenGUID(const char *token) {
|
||||
// "leaves render distance → focus drops" behavior. Won't refocus
|
||||
// when the unit comes back, also matching modern.
|
||||
void OnWorldTick() {
|
||||
if (g_focusGUID == 0)
|
||||
return;
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
if (resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
|
||||
static_cast<uint32_t>(g_focusGUID),
|
||||
static_cast<uint32_t>(g_focusGUID >> 32),
|
||||
0x172) == nullptr)
|
||||
Set(0);
|
||||
// Resolve the focus object (this is also the despawn check) and, if it's
|
||||
// alive, read its current target — that GUID is `focustarget`. Vanilla
|
||||
// fires no event when a unit re-targets, so this once-per-tick identity
|
||||
// read is the only way to notice the focus switching targets; the
|
||||
// focustarget *events* still fire event-driven via the observer below.
|
||||
uint64_t desiredFocusTarget = 0;
|
||||
if (g_focusGUID != 0) {
|
||||
auto resolve = reinterpret_cast<ResolveByGUID_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OBJECT_RESOLVE_BY_GUID));
|
||||
auto *focusObj = static_cast<const uint8_t *>(
|
||||
resolve(Offsets::OBJ_TYPE_UNIT, "Focus",
|
||||
static_cast<uint32_t>(g_focusGUID),
|
||||
static_cast<uint32_t>(g_focusGUID >> 32), 0x172));
|
||||
if (focusObj == nullptr) {
|
||||
Set(0); // focus left the object table → drop focus
|
||||
} else {
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
focusObj + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (fields != nullptr)
|
||||
desiredFocusTarget = *reinterpret_cast<const uint64_t *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_TARGET);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-point the focustarget observer only when the target identity
|
||||
// changes (focus swapped targets, focus cleared, or focus has no target).
|
||||
if (desiredFocusTarget != g_focusTargetGUID) {
|
||||
if (g_focusTargetGUID != 0)
|
||||
TokenObserver::Unregister(g_focusTargetGUID, &FocusTargetFieldCb);
|
||||
g_focusTargetGUID = desiredFocusTarget;
|
||||
if (g_focusTargetGUID != 0)
|
||||
TokenObserver::Register(g_focusTargetGUID, &FocusTargetFieldCb);
|
||||
}
|
||||
}
|
||||
|
||||
// `FocusUnit(unit)` — sets focus to whatever GUID `unit` currently
|
||||
@@ -111,6 +184,24 @@ void RegisterLuaFunctions() {
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// Drop focus when the player logs out to the glue screen. The DLL isn't
|
||||
// reloaded on a logout→character-select→login, so g_focusGUID would
|
||||
// otherwise carry a dead GUID from character A into character B. The
|
||||
// WorldTick despawn probe self-heals it within a tick (ObjectByGUID miss
|
||||
// → Set(0)), but that fires a spurious PLAYER_FOCUS_CHANGED on B's first
|
||||
// tick and can't cover the (astronomically rare) GUID-collision case, so
|
||||
// clearing here is both cleaner and exact retail parity. We use the glue
|
||||
// hook, NOT FrameScript_Initialize, because it fires on the world→glue
|
||||
// return a logout triggers but NOT on /reload — so focus correctly
|
||||
// survives /reload (retail behavior) and drops only on logout. No event /
|
||||
// observer work: char A's focus observers already died with the object
|
||||
// during world teardown, and B starts with focus nil (no event expected).
|
||||
void ClearOnLogout() {
|
||||
g_focusGUID = 0;
|
||||
g_focusTargetGUID = 0;
|
||||
}
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint64_t Get() { return g_focusGUID; }
|
||||
@@ -118,7 +209,21 @@ uint64_t Get() { return g_focusGUID; }
|
||||
void Set(uint64_t guid) {
|
||||
if (guid == g_focusGUID)
|
||||
return; // no-op: same target → no event fire (matches 3.3.5)
|
||||
// Stop watching the old focus, start watching the new one, so unit events
|
||||
// (UNIT_HEALTH, …) fire with "focus" for whatever is focused — even a unit
|
||||
// the engine isn't otherwise watching. Unregister is a safe no-op if the
|
||||
// old unit already despawned (nodes died with it).
|
||||
if (g_focusGUID != 0)
|
||||
TokenObserver::Unregister(g_focusGUID, &FocusFieldCb);
|
||||
// Focus changed → the old focustarget is stale; drop its observer now.
|
||||
// WorldTick re-resolves and re-points to the new focus's target next tick.
|
||||
if (g_focusTargetGUID != 0) {
|
||||
TokenObserver::Unregister(g_focusTargetGUID, &FocusTargetFieldCb);
|
||||
g_focusTargetGUID = 0;
|
||||
}
|
||||
g_focusGUID = guid;
|
||||
if (guid != 0)
|
||||
TokenObserver::Register(guid, &FocusFieldCb);
|
||||
const int slot = Event::Custom::Lookup(kEventName);
|
||||
if (slot >= 0)
|
||||
Event::Custom::Fire(slot, "");
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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/>.
|
||||
|
||||
// `UnitHealthMissing(unit)` — the health deficit, i.e. the equivalent of
|
||||
// `UnitHealthMax(unit) - UnitHealth(unit)`. A convenience for healing addons
|
||||
// (overheal checks, "missing health" bars) that otherwise call both engine
|
||||
// functions and subtract in Lua every frame. Reads the two health fields off
|
||||
// the unit's descriptor directly (same path `Unit::Power` uses for POWER),
|
||||
// so it's a single resolve + two field reads.
|
||||
//
|
||||
// Health carries no display divisor — unlike rage/happiness, HEALTH and
|
||||
// MAXHEALTH are stored at face value — so the deficit is a plain subtraction,
|
||||
// clamped at 0 (a transient current > max should never surface as negative).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::Health {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
}
|
||||
|
||||
// `UnitHealthMissing("unit")` — returns `max - current` health (never
|
||||
// negative). Returns 0 for a valid-but-absent unit (e.g. `"target"` with no
|
||||
// target), matching `UnitHealth`'s 0-for-missing convention.
|
||||
static int __fastcall Script_UnitHealthMissing(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitHealthMissing(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *desc = Descriptor(ResolveUnit(Game::Lua::ToString(L, 1)));
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t cur = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_HEALTH);
|
||||
const uint32_t max = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXHEALTH);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(max > cur ? max - cur : 0u));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitHealthMissing", &Script_UnitHealthMissing);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::Health
|
||||
+8
-10
@@ -289,16 +289,14 @@ const char *TokenFromGUID(uint64_t target, char *buf, size_t bufSize) {
|
||||
if (GuidForToken(buf) == target) return buf;
|
||||
}
|
||||
|
||||
// Visible nameplates. The ordered list (see
|
||||
// `NamePlate::Events::g_orderedGUIDs`) is dense — `GetGUIDByIndex`
|
||||
// returns 0 once we step past the populated count, terminating
|
||||
// the loop. Skips the token-resolver hook + SStrCmpI cost per
|
||||
// index by comparing GUIDs directly.
|
||||
for (int i = 1; ; ++i) {
|
||||
const uint64_t plateGuid = NamePlate::Events::GetGUIDByIndex(i);
|
||||
if (plateGuid == 0)
|
||||
break;
|
||||
if (plateGuid == target) {
|
||||
// Visible nameplates. The slot array is SPARSE (retail-exact slot
|
||||
// assignment — freed middle slots stay as gaps until reused), so we
|
||||
// scan `1..GetSlotCount()` and skip empties rather than stopping at
|
||||
// the first 0. Compares GUIDs directly, skipping the token-resolver
|
||||
// hook + SStrCmpI cost per index.
|
||||
const int plateCount = NamePlate::Events::GetSlotCount();
|
||||
for (int i = 1; i <= plateCount; ++i) {
|
||||
if (NamePlate::Events::GetGUIDByIndex(i) == target) {
|
||||
std::snprintf(buf, bufSize, "nameplate%d", i);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -163,6 +163,27 @@ int __fastcall Script_UnitIsOtherPlayersPet(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitOwnerGUID(unit)` — the GUID string of the unit's owner (the
|
||||
// summoner of a pet/guardian/totem, or the charmer of a charmed unit),
|
||||
// formatted `"0x…"`. Reuses the same owner field the pet predicates use
|
||||
// (CharmedBy, else CreatedBy). Returns `nil` for an unresolved unit and
|
||||
// for anything with no owner (players, world creatures).
|
||||
int __fastcall Script_UnitOwnerGUID(void *L) {
|
||||
const uint8_t *unit = ResolveUnit(L);
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t owner = OwnerGuid(unit);
|
||||
if (owner == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushString(L, Guid::FormatAsString(owner, buf, sizeof buf));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -170,6 +191,7 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsPet", &Script_UnitIsPet);
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsOtherPlayersPet",
|
||||
&Script_UnitIsOtherPlayersPet);
|
||||
Game::Lua::RegisterGlobalFunction("UnitOwnerGUID", &Script_UnitOwnerGUID);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+46
-7
@@ -8,9 +8,10 @@
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// Modern power-API polyfill: `UnitPower(unit [, type])`,
|
||||
// `UnitPowerMax(unit [, type])`, and an extended `UnitPowerType(unit)`
|
||||
// that returns the modern 2-tuple `(powerType, powerToken)`.
|
||||
// Modern power-API polyfill: `UnitPower(unit [, type [, unmodified]])`,
|
||||
// `UnitPowerMax(unit [, type [, unmodified]])`, `UnitPowerMissing(unit
|
||||
// [, type [, unmodified]])`, and an extended `UnitPowerType(unit)` that
|
||||
// returns the modern 2-tuple `(powerType, powerToken)`.
|
||||
//
|
||||
// Shape modeled on 3.3.5's `Script_UnitPower` (`0x0060ED40` in the
|
||||
// Frostmourne client) and `Script_UnitPowerMax` (`0x0060EF40`). The
|
||||
@@ -146,7 +147,7 @@ bool ResolveArgs(void *L, const uint8_t **outDesc, int *outType) {
|
||||
// vanilla).
|
||||
static int __fastcall Script_UnitPower(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitPower(\"unit\" [, type])");
|
||||
Game::Lua::Error(L, "Usage: UnitPower(\"unit\" [, type [, unmodified]])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *desc;
|
||||
@@ -157,7 +158,9 @@ static int __fastcall Script_UnitPower(void *L) {
|
||||
}
|
||||
const uint32_t raw = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
const uint32_t divisor = PowerDivisor(type);
|
||||
// `unmodified` (arg 3, truthy) returns the raw internal value; otherwise
|
||||
// apply the per-type display divisor. Matches retail's third UnitPower arg.
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
@@ -167,7 +170,7 @@ static int __fastcall Script_UnitPower(void *L) {
|
||||
// `UnitPower`.
|
||||
static int __fastcall Script_UnitPowerMax(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitPowerMax(\"unit\" [, type])");
|
||||
Game::Lua::Error(L, "Usage: UnitPowerMax(\"unit\" [, type [, unmodified]])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *desc;
|
||||
@@ -178,11 +181,46 @@ static int __fastcall Script_UnitPowerMax(void *L) {
|
||||
}
|
||||
const uint32_t raw = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = PowerDivisor(type);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(raw / divisor));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitPowerMissing("unit" [, powerType [, unmodified]])` — the power
|
||||
// deficit, i.e. `UnitPowerMax - UnitPower` for the given type. Same arg
|
||||
// shape and fallbacks as `UnitPower`; `unmodified` (arg 3, truthy) skips
|
||||
// the display divisor and returns the raw internal deficit, mirroring
|
||||
// retail's third `UnitPower` arg.
|
||||
//
|
||||
// Computed as the difference of the two DISPLAY values (each integer-divided
|
||||
// by the per-type divisor), NOT by dividing the raw difference — so it equals
|
||||
// `UnitPowerMax(...) - UnitPower(...)` exactly. For rage those diverge:
|
||||
// raw 1000/55 displays as 100/5, giving a deficit of 95, whereas
|
||||
// `(1000-55)/10` truncates to 94.
|
||||
static int __fastcall Script_UnitPowerMissing(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: UnitPowerMissing(\"unit\" [, powerType [, unmodified]])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *desc;
|
||||
int type;
|
||||
if (!ResolveArgs(L, &desc, &type)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t cur = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_POWER1 + type * 4);
|
||||
const uint32_t max = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_MAXPOWER1 + type * 4);
|
||||
const uint32_t divisor = Game::Lua::ToBoolean(L, 3) ? 1u : PowerDivisor(type);
|
||||
const uint32_t curDisp = cur / divisor;
|
||||
const uint32_t maxDisp = max / divisor;
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(maxDisp > curDisp ? maxDisp - curDisp : 0u));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitPowerType(unit)` — extended to return `(powerType, powerToken)`.
|
||||
// Vanilla 1.12's implementation pushes only the integer; we chain
|
||||
// to the engine's original (`0x00517940`) to preserve its full
|
||||
@@ -210,6 +248,7 @@ static int __fastcall Script_UnitPowerType(void *L) {
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitPower", &Script_UnitPower);
|
||||
Game::Lua::RegisterGlobalFunction("UnitPowerMax", &Script_UnitPowerMax);
|
||||
Game::Lua::RegisterGlobalFunction("UnitPowerMissing", &Script_UnitPowerMissing);
|
||||
Game::Lua::RegisterGlobalFunction("UnitPowerType", &Script_UnitPowerType);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "PowerType",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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/>.
|
||||
|
||||
// `UnitSpellHaste(unit)` — backport of the TBC+ spell-haste getter to 1.12,
|
||||
// which has no native haste API. It reads UNIT_MOD_CAST_SPEED (the cast-time
|
||||
// multiplier at descriptor +0x22c that the server folds into
|
||||
// `SpellEntry::GetCastTime` — `castTime *= GetFloatValue(UNIT_MOD_CAST_SPEED)`)
|
||||
// and converts it to the modern haste PERCENTAGE:
|
||||
//
|
||||
// haste% = (1 / modCastSpeed - 1) * 100
|
||||
//
|
||||
// so `1.0` (no modifier) -> `0`, `0.5` (half cast time) -> `100`, and a
|
||||
// slowing effect like Curse of Tongues (`modCastSpeed > 1.0`) -> a negative
|
||||
// value. This is the same field nampower surfaces raw as `modCastSpeed`;
|
||||
// `UnitSpellHaste` is the Blizzard-shaped percentage over it, so addons can
|
||||
// drop the `GetUnitField("player", "modCastSpeed")` nampower dependency.
|
||||
// (Consumers that need the raw multiplier can recover it as
|
||||
// `1 / (1 + UnitSpellHaste(unit) / 100)`.)
|
||||
//
|
||||
// Vanilla has very few caster-haste sources, so this reads `0` for most
|
||||
// casters most of the time — the value is meaningful for Curse of Tongues,
|
||||
// Nature's Grace, and the handful of other cast-speed effects (and, as the
|
||||
// cast-time work showed, whatever bleeds into the field for hunter shots).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::SpellHaste {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
}
|
||||
|
||||
// `UnitSpellHaste("unit")` — spell haste percentage. `0` for an unhasted or
|
||||
// invalid unit; negative for a slowed one (Curse of Tongues).
|
||||
static int __fastcall Script_UnitSpellHaste(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitSpellHaste(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *desc = Descriptor(ResolveUnit(Game::Lua::ToString(L, 1)));
|
||||
if (desc == nullptr) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
const float mult = *reinterpret_cast<const float *>(
|
||||
desc + Offsets::OFF_UNIT_MOD_CAST_SPEED);
|
||||
// `mult` is the cast-time multiplier; haste% is its inverse deviation from
|
||||
// 1.0. Guard a non-positive / uninitialized field against div-by-zero
|
||||
// (treat it as "no haste").
|
||||
const double haste = (mult > 0.0f)
|
||||
? (1.0 / static_cast<double>(mult) - 1.0) * 100.0
|
||||
: 0.0;
|
||||
Game::Lua::PushNumber(L, haste);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitSpellHaste", &Script_UnitSpellHaste);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Unit::SpellHaste
|
||||
@@ -0,0 +1,128 @@
|
||||
// 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/>.
|
||||
|
||||
// Replicates the engine's per-unit "watch descriptor fields → fire per-token
|
||||
// unit events" registration (`FUN_0051bbb0` / `FUN_0051bdb0`) so synthetic
|
||||
// tokens can be made first-class. The engine reaches this from
|
||||
// `FUN_0060c520`, which recomputes a unit's token-category mask
|
||||
// (target/party/raid/pet/mouseover) at `unit+0xc90` and, on a 0→nonzero
|
||||
// transition, registers a bank-UNIT observer on every named event field with
|
||||
// callback `FUN_0051bd50` → `FUN_00515e50(guid, eventId)` → the GUID→token
|
||||
// reverse map `FUN_00515c50` → fire the event once per token.
|
||||
//
|
||||
// We register the same field set with a caller-supplied callback instead, so
|
||||
// the caller fires its own token. The field set / sizes are read from the
|
||||
// static event-name table exactly as the engine does; see the
|
||||
// FUN_DESC_OBSERVER_REGISTER block in Offsets.h.
|
||||
//
|
||||
// Cross-checked against nampower's offsets.hpp: `SendUnitSignal` (the
|
||||
// broadcast) = `0x00515e50`, `StringParamFormat` ("%s") = `0x0082E280`,
|
||||
// `SignalEventParam` = `0x00703F50`, `UNIT_HEALTH` = 16 — all consistent with
|
||||
// the field-index-is-event-id derivation here. (nampower left its
|
||||
// `GetNamesFromGUID` reverse-map offset as an unfilled TODO; it is
|
||||
// `FUN_00515c50`.)
|
||||
|
||||
#include "unit/TokenObserver.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::TokenObserver {
|
||||
|
||||
namespace {
|
||||
|
||||
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset,
|
||||
uint32_t guidLo, uint32_t guidHi, int size,
|
||||
const void *callback, void *userArg1,
|
||||
void *userArg2);
|
||||
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const void *callback, void *userArg1);
|
||||
|
||||
const char *const *EventNames() {
|
||||
return reinterpret_cast<const char *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_NAME_TABLE_STATIC));
|
||||
}
|
||||
|
||||
// Both register and unregister funnel through `FUN_00464890`, which
|
||||
// dereferences the client/player-rooted observer registry at
|
||||
// `[VAR_LOCAL_PLAYER_PTR]` (reads its `+0x24` mask). That root is NULL out of
|
||||
// world — login screen, loading screens, logout teardown — where the engine
|
||||
// helper null-derefs (crash: `MOV EAX,[EDX+0x24]`, EDX=0). Bail when it isn't
|
||||
// up: there are no live objects or observer nodes to touch anyway (nodes die
|
||||
// with their objects during teardown), and in-world callers re-register/clean
|
||||
// up on the next tick. Unlike `Player::Equipment` (which only ever registers
|
||||
// from the enter-world hook), our consumers register from Lua / WorldTick,
|
||||
// which can run mid-transition — hence the guard here.
|
||||
bool RegistryReady() {
|
||||
return *reinterpret_cast<void *const volatile *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR)) != nullptr;
|
||||
}
|
||||
|
||||
// Bytes the engine watches for the observer on event index `i` — verbatim
|
||||
// from `FUN_0051bbb0`'s size switch. The 8-byte entries are the 64-bit GUID
|
||||
// fields; index 0x29 (`UNIT_FIELD_AURA`) spans the whole 0xD8 aura block.
|
||||
int SizeForIndex(int i) {
|
||||
switch (i) {
|
||||
case 0:
|
||||
case 2:
|
||||
case 4:
|
||||
case 10:
|
||||
return 8;
|
||||
case 0x29:
|
||||
return 0xD8;
|
||||
case 0x6B:
|
||||
return 0x30;
|
||||
case 0xA7:
|
||||
case 0xAE:
|
||||
return 0x1C;
|
||||
default:
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Register(uint64_t guid, FieldCallback cb) {
|
||||
if (guid == 0 || cb == nullptr || !RegistryReady())
|
||||
return;
|
||||
auto reg = reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER);
|
||||
const char *const *names = EventNames();
|
||||
const uint32_t lo = static_cast<uint32_t>(guid);
|
||||
const uint32_t hi = static_cast<uint32_t>(guid >> 32);
|
||||
for (int i = 0; i < Offsets::EVENT_NAME_UNIT_MAX; ++i) {
|
||||
if (names[i] == nullptr)
|
||||
continue; // unnamed slot → not a real unit event
|
||||
reg(Offsets::DESC_OBSERVER_BANK_UNIT, static_cast<uint32_t>(i * 4), lo, hi,
|
||||
SizeForIndex(i), reinterpret_cast<const void *>(cb), nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Unregister(uint64_t guid, FieldCallback cb) {
|
||||
if (guid == 0 || cb == nullptr || !RegistryReady())
|
||||
return;
|
||||
auto unreg =
|
||||
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER);
|
||||
const char *const *names = EventNames();
|
||||
const uint32_t lo = static_cast<uint32_t>(guid);
|
||||
const uint32_t hi = static_cast<uint32_t>(guid >> 32);
|
||||
for (int i = 0; i < Offsets::EVENT_NAME_UNIT_MAX; ++i) {
|
||||
if (names[i] == nullptr)
|
||||
continue;
|
||||
unreg(Offsets::DESC_OBSERVER_BANK_UNIT, static_cast<uint32_t>(i * 4), lo, hi,
|
||||
reinterpret_cast<const void *>(cb), nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Unit::TokenObserver
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Unit::TokenObserver {
|
||||
|
||||
// Descriptor-field observer callback — the engine's __AUCMirrorHandler__ node
|
||||
// ABI (see the FUN_DESC_OBSERVER_REGISTER block in Offsets.h): return 1,
|
||||
// callee cleans 0x10. `fieldOffset` is the bank-relative offset as registered;
|
||||
// the corresponding unit event id is `fieldOffset >> 2` (the descriptor field
|
||||
// index equals the unit event id — health field +0x40 → event 16 =
|
||||
// `UNIT_HEALTH`).
|
||||
using FieldCallback = int(__fastcall *)(uint32_t fieldOffset, uint32_t size,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
const uint32_t *oldValue, void *userArg);
|
||||
|
||||
// Register / unregister the engine's *complete* unit-event descriptor-field
|
||||
// observer set for `guid`, dispatching to `cb` on each watched field change.
|
||||
// Mirrors the engine's own `FUN_0051bbb0` / `FUN_0051bdb0` (identical field
|
||||
// set + per-field watched sizes), so `cb` fires for exactly the fields the
|
||||
// engine treats as token-scoped unit events (`UNIT_HEALTH`, `UNIT_MANA`,
|
||||
// `UNIT_AURA`, `UNIT_LEVEL`, …).
|
||||
//
|
||||
// This makes a synthetic token (focus, nameplateN) a first-class unit-event
|
||||
// source: the engine only registers these observers for its own tokens
|
||||
// (target/party/raid/pet/mouseover via the `unit+0xc90` watch mask), so a
|
||||
// focused mob that isn't otherwise one of them fires no unit events until we
|
||||
// watch it ourselves. The registrar appends a fresh node per (guid, field)
|
||||
// with no dedup, so our observer coexists with the engine's — a unit that is
|
||||
// simultaneously the target keeps firing `"target"` while ours fires its own
|
||||
// token. Unregister is a safe no-op once the object is gone (the engine frees
|
||||
// observer nodes with the object).
|
||||
//
|
||||
// `cb` runs inside SMSG_UPDATE_OBJECT dispatch; firing engine events from it
|
||||
// is sanctioned (the engine's own callback `FUN_0051bd50` does exactly that).
|
||||
void Register(uint64_t guid, FieldCallback cb);
|
||||
void Unregister(uint64_t guid, FieldCallback cb);
|
||||
|
||||
} // namespace Unit::TokenObserver
|
||||
Reference in New Issue
Block a user