mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39dcfc73de | ||
|
|
6e5f7c3e43 | ||
|
|
2e05e86362 | ||
|
|
bcc631c4ed | ||
|
|
f9fb32186d | ||
|
|
60adbba7c3 | ||
|
|
c35d883af0 | ||
|
|
c202c1000f | ||
|
|
98b4ae14e7 | ||
|
|
8d957eb424 | ||
|
|
78618ea2cc | ||
|
|
1a4583e16e | ||
|
|
a20e1206fb | ||
|
|
9f67cade87 | ||
|
|
8629e13e1f | ||
|
|
ca9bfbab5e | ||
|
|
8fdc767cc7 | ||
|
|
aa31c25be5 | ||
|
|
29aafe88f8 | ||
|
|
461f15a4a6 | ||
|
|
6245d72694 | ||
|
|
bd14228ad7 | ||
|
|
5b15172894 | ||
|
|
b5470e8d3d | ||
|
|
c601967fdb | ||
|
|
5c063ade73 |
@@ -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,23 +30,23 @@ 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) |
|
||||
| [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` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `select`, `string.gmatch`, `string.match`, `table.wipe` |
|
||||
| [Lua](docs/API.md#lua) | `coroutine.create`, `coroutine.resume`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `math.fmod`, `math.huge`, `math.modf`, `select`, `string.gmatch`, `string.match`, `string.reverse`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.wipe` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapOverlays`, `C_Map.GetMapWorldSize` |
|
||||
@@ -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` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [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`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `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
|
||||
|
||||
|
||||
+612
-37
@@ -95,6 +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)
|
||||
@@ -154,6 +157,7 @@ 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)
|
||||
@@ -178,6 +182,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)
|
||||
@@ -230,7 +236,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)
|
||||
@@ -282,7 +289,14 @@ build instructions.
|
||||
- [`select(index, ...)`](#selectindex-)
|
||||
- [`table.wipe(t)`](#tablewipet)
|
||||
- [`string.match` / `string.gmatch`](#stringmatch--stringgmatch)
|
||||
- [`strsplit(sep, str [, pieces])`](#strsplitsep-str--pieces)
|
||||
- [`strjoin(delimiter, ...)`](#strjoindelimiter-)
|
||||
- [`strtrim(str [, chars])`](#strtrimstr--chars)
|
||||
- [`strreplace(str, find, replace)`](#strreplacestr-find-replace)
|
||||
- [`string.reverse(s)` / `strrev(s)`](#stringreverses--strrevs)
|
||||
- [`math.fmod(x, y)`](#mathfmodx-y)
|
||||
- [`math.modf(x)`](#mathmodfx)
|
||||
- [`math.huge`](#mathhuge)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
@@ -321,6 +335,9 @@ build instructions.
|
||||
- [`C_MerchantFrame.IsMerchantItemRefundable(slot)`](#c_merchantframeismerchantitemrefundableslot)
|
||||
- [`C_MerchantFrame.IsSellAllJunkEnabled()`](#c_merchantframeissellalljunkenabled)
|
||||
|
||||
- [ChatBubbles](#chatbubbles)
|
||||
- [`C_ChatBubbles.GetAllChatBubbles([includeForbidden])`](#c_chatbubblesgetallchatbubblesincludeforbidden)
|
||||
|
||||
- [NamePlate](#nameplate)
|
||||
- [`C_NamePlate.GetNamePlates()`](#c_nameplategetnameplates)
|
||||
- [`C_NamePlate.GetNamePlateGUIDs()`](#c_nameplategetnameplateguids)
|
||||
@@ -432,6 +449,14 @@ build instructions.
|
||||
- [`GetTalentSpellID(tabIndex, talentIndex, [rank])`](#gettalentspellidtabindex-talentindex-rank)
|
||||
- [`GetTalentIDByIndex(tabIndex, talentIndex)`](#gettalentidbyindextabindex-talentindex)
|
||||
|
||||
- [Targeting](#targeting)
|
||||
- [`GetPlayerFacing()`](#getplayerfacing)
|
||||
- [`TargetDirectionEnemy(facing [, coneAngle])`](#targetdirectionenemyfacing--coneangle)
|
||||
- [`TargetDirectionFriend(facing [, coneAngle])`](#targetdirectionfriendfacing--coneangle)
|
||||
- [`TargetNearest([reverse])`](#targetnearestreverse)
|
||||
- [`TargetNearestEnemyPlayer([reverse])`](#targetnearestenemyplayerreverse)
|
||||
- [`TargetNearestFriendPlayer([reverse])`](#targetnearestfriendplayerreverse)
|
||||
|
||||
- [TaxiMap](#taximap)
|
||||
- [`C_TaxiMap.GetTaxiNodesForMap([mapID])`](#c_taximapgettaxinodesformapmapid)
|
||||
- [`C_TaxiMap.GetAllTaxiNodes([uiMapID])`](#c_taximapgetalltaxinodesuimapid)
|
||||
@@ -449,6 +474,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)
|
||||
@@ -474,10 +505,12 @@ 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)
|
||||
- [`ClosestUnitPosition(creatureID)`](#closestunitpositioncreatureid)
|
||||
- [`UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`](#unitpowerunit--powertype--unitpowermaxunit--powertype)
|
||||
- [`UnitPowerType(unit)`](#unitpowertypeunit)
|
||||
|
||||
@@ -1373,17 +1406,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,
|
||||
@@ -2213,6 +2247,86 @@ 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)
|
||||
`PLAYER_ENTERING_WORLD`:
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_ENTERING_WORLD")
|
||||
f:SetScript("OnEvent", function()
|
||||
local isInitialLogin, isReloadingUi = arg1, arg2
|
||||
if isInitialLogin then -- fresh character login (from char-select)
|
||||
elseif isReloadingUi then -- /reload
|
||||
else -- zone / instance transition
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
- `isInitialLogin` — `true` on a fresh login (including logging out to
|
||||
char-select and back in), `nil` otherwise.
|
||||
- `isReloadingUi` — `true` when the fire is caused by a `/reload`, `nil`
|
||||
otherwise.
|
||||
- Both `nil` on an instance/zone transition — matching retail.
|
||||
|
||||
Delivered as real event args (`arg1`/`arg2`), so retail-style handlers work
|
||||
unchanged. Values are `true`→`1` / false→`nil` (the engine dispatcher has no
|
||||
boolean), so `if isInitialLogin then …` reads naturally. Existing handlers
|
||||
that ignore the args are unaffected.
|
||||
|
||||
PEW is a no-arg event, so the engine broadcasts it through the
|
||||
`FUN_FIRE_EVENT_NO_ARGS` dispatcher; we intercept that (via the shared
|
||||
`Event::SignalHook`) and re-broadcast through the variadic dispatcher with
|
||||
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`
|
||||
@@ -3545,6 +3659,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)`
|
||||
@@ -4256,6 +4390,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` /
|
||||
@@ -4903,15 +5084,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, …`).
|
||||
@@ -4920,16 +5123,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
|
||||
@@ -5470,13 +5677,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
|
||||
@@ -6839,6 +7064,83 @@ for word in string.gmatch("a,bb,ccc", "[^,]+") do print(word) end -- a / bb / cc
|
||||
> a hot VM-core function. This is the long-standing vanilla 1.12 constraint —
|
||||
> always write `string.match(s, p)`, never `s:match(p)`.
|
||||
|
||||
### `strsplit(sep, str [, pieces])`
|
||||
|
||||
The WoW global (backported from 3.3.5), splits `str` on **any** character in
|
||||
`sep` and returns the pieces as multiple values.
|
||||
|
||||
- `sep` — a set of delimiter *characters* (not a pattern); each character is
|
||||
a delimiter. `","` splits on commas; `" -"` splits on spaces and dashes.
|
||||
- `pieces` (optional) — caps the number of results. After `pieces - 1`
|
||||
splits the rest of the string (delimiters and all) is returned as the final
|
||||
piece. `0` or omitted = unlimited. `1` returns the whole string unsplit.
|
||||
|
||||
Consecutive/trailing delimiters produce empty pieces, matching retail.
|
||||
|
||||
```lua
|
||||
strsplit(",", "a,b,c") -- "a", "b", "c"
|
||||
strsplit(",", "a,b,c,d", 2) -- "a", "b,c,d" (capped at 2)
|
||||
strsplit(" -", "a b-c") -- "a", "b", "c" (space OR dash)
|
||||
strsplit(",", "a,,b") -- "a", "", "b" (empty middle piece)
|
||||
local zone, x, y = strsplit(":", "Durotar:52:38")
|
||||
```
|
||||
|
||||
Errors `strsplit(): Stack overflow` if a string splits into more pieces than
|
||||
the Lua stack can grow to hold (`lua_checkstack` guards every push) — the
|
||||
same guard and message 3.3.5 uses.
|
||||
|
||||
### `strjoin(delimiter, ...)`
|
||||
|
||||
The inverse of `strsplit`: concatenate the vararg pieces with `delimiter`
|
||||
between each.
|
||||
|
||||
```lua
|
||||
strjoin(",", "a", "b", "c") -- "a,b,c"
|
||||
strjoin("", "a", "b", "c") -- "abc"
|
||||
strjoin(",") -- "" (no pieces)
|
||||
```
|
||||
|
||||
A `nil` / non-string-coercible piece is treated as an empty string (so the
|
||||
delimiters still line up) rather than erroring.
|
||||
|
||||
### `strtrim(str [, chars])`
|
||||
|
||||
Remove any of the characters in `chars` from both ends of `str`. `chars` is a
|
||||
literal **set of characters** (not a Lua pattern); it defaults to whitespace
|
||||
(space, tab, `\n`, `\v`, `\f`, `\r`).
|
||||
|
||||
```lua
|
||||
strtrim(" hello ") -- "hello"
|
||||
strtrim("xxhelloxx", "x") -- "hello"
|
||||
strtrim("[a]", "[]") -- "a"
|
||||
```
|
||||
|
||||
### `strreplace(str, find, replace)`
|
||||
|
||||
Replace every occurrence of the literal substring `find` with `replace`,
|
||||
returning `(result, count)`. This is a **plain-text** replace — no pattern
|
||||
magic-character escaping, unlike `string.gsub`. An empty `find` returns `str`
|
||||
unchanged with count `0`.
|
||||
|
||||
```lua
|
||||
strreplace("a.b.c", ".", "-") -- "a-b-c", 2 (no pattern escaping)
|
||||
strreplace("hello world", "o", "0") -- "hell0 w0rld", 2
|
||||
```
|
||||
|
||||
> Not a stock WoW global — a ClassicAPI convenience. For pattern-based
|
||||
> replacement use `string.gsub`.
|
||||
|
||||
### `string.reverse(s)` / `strrev(s)`
|
||||
|
||||
Returns `s` with its bytes reversed. Lua 5.1 added `string.reverse`; 5.0's
|
||||
string library lacks it. Byte-wise (not UTF-8 aware), matching stock Lua.
|
||||
Registered both on the `string` table and as the `strrev` global.
|
||||
|
||||
```lua
|
||||
string.reverse("abc") -- "cba"
|
||||
strrev("hello") -- "olleh"
|
||||
```
|
||||
|
||||
### `math.fmod(x, y)`
|
||||
|
||||
The floating-point remainder of `x / y` (the quotient truncated toward zero),
|
||||
@@ -6853,6 +7155,28 @@ math.fmod(-7, 3) -- -1 (result takes the sign of the dividend)
|
||||
math.fmod(5.5, 2) -- 1.5
|
||||
```
|
||||
|
||||
### `math.modf(x)`
|
||||
|
||||
Returns `(integral, fractional)` — the integral part truncated toward zero
|
||||
and the fractional part carrying `x`'s sign. Lua 5.1 function, genuinely
|
||||
missing from 1.12's math library (not just renamed like `fmod`).
|
||||
|
||||
```lua
|
||||
math.modf(3.7) -- 3, 0.7
|
||||
math.modf(-3.7) -- -3, -0.7
|
||||
math.modf(5) -- 5, 0
|
||||
```
|
||||
|
||||
### `math.huge`
|
||||
|
||||
Positive infinity, as the numeric constant `math.huge` (Lua 5.1). Handy as a
|
||||
"larger than anything" sentinel in min/max scans.
|
||||
|
||||
```lua
|
||||
local best = math.huge
|
||||
for _, v in ipairs(costs) do best = math.min(best, v) end
|
||||
```
|
||||
|
||||
### `coroutine.*`
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
@@ -7721,6 +8045,42 @@ No retail equivalent (retail ships only the explored getter; its inverse,
|
||||
`C_Map.GetMapOverlays`, returns *everything*). Same table shape and
|
||||
`areaID` semantics as the explored getter.
|
||||
|
||||
## ChatBubbles
|
||||
|
||||
### `C_ChatBubbles.GetAllChatBubbles([includeForbidden])`
|
||||
|
||||
Returns a 1-based table of the currently-active chat-bubble `Frame`
|
||||
objects (the speech bubbles above characters who `/say` or `/yell`).
|
||||
Modern WoW added this in 7.2.5 to replace the old "iterate over
|
||||
`WorldFrame` children and guess which are bubbles" idiom — we hand you
|
||||
the exact set instead.
|
||||
|
||||
```lua
|
||||
for _, bubble in ipairs(C_ChatBubbles.GetAllChatBubbles()) do
|
||||
-- The spoken text lives in the bubble's FontString region.
|
||||
for _, region in ipairs({bubble:GetRegions()}) do
|
||||
if region.GetText and region:GetText() then
|
||||
print(region:GetText())
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Vanilla 1.12 chat bubbles are `CGChatBubbleFrame`s — full frames the
|
||||
engine creates in C++ without ever calling `CreateFrame` (same as
|
||||
default nameplates). The returned frames are the engine's **canonical
|
||||
wrappers**: real, method-capable (`:GetRegions()`, `:IsShown()`,
|
||||
`:GetPoint()`, …), and identical to the object any other addon that
|
||||
touches the bubble sees, so decorations survive. The spoken-text
|
||||
FontString is parented to the bubble, so it shows up in
|
||||
`:GetRegions()`.
|
||||
|
||||
`includeForbidden` is accepted for signature parity and ignored —
|
||||
vanilla has no forbidden frames, so every active bubble is returned
|
||||
regardless. Returns an empty table when no bubbles are showing. A
|
||||
bubble whose owner just despawned can linger for one frame before the
|
||||
engine prunes it; filter on `bubble:IsShown()` if that matters.
|
||||
|
||||
## NamePlate
|
||||
|
||||
Modern `C_NamePlate.*` returns nameplate `Frame` objects keyed off
|
||||
@@ -10644,6 +11004,74 @@ a different field.
|
||||
Equivalent to the talentID return slot of `GetTalentInfo` in modern
|
||||
WoW (5.0+; not exposed at all in 1.12).
|
||||
|
||||
## Targeting
|
||||
|
||||
Backports of the post-vanilla `TargetScript` selection functions. Vanilla
|
||||
already ships `TargetNearestEnemy/Friend/PartyMember/RaidMember`,
|
||||
`TargetLastEnemy/Target`, `AssistUnit`, `TargetUnit`, `ClearTarget`; these
|
||||
fill in the ones added in later patches. They're built on the engine's own
|
||||
tab-targeting core — the same visible-unit enumeration, per-mode validity
|
||||
predicate (reaction/attackability/alive), and selection commit the native
|
||||
`TargetNearestEnemy` uses — so hostility semantics match the client exactly.
|
||||
|
||||
Not backported (no vanilla equivalent — soft-target / action-camera era):
|
||||
`TargetPriorityHighlightStart/End`, `IsTargetLoose`, `TargetToggle`.
|
||||
|
||||
### `GetPlayerFacing()`
|
||||
|
||||
Returns the player's facing as an angle in radians (`0` … `2*pi`,
|
||||
increasing counter-clockwise), or nothing off-world (glue / loading).
|
||||
Vanilla never shipped this (only character-create/select facing exist), but
|
||||
it's the companion the direction-target functions below need — pass it to
|
||||
aim "in front of me."
|
||||
|
||||
```lua
|
||||
local facing = GetPlayerFacing() -- e.g. 1.65
|
||||
```
|
||||
|
||||
Read from the player unit's movement-block orientation field
|
||||
(`player + 0x9C4`).
|
||||
|
||||
### `TargetDirectionEnemy(facing [, coneAngle])`
|
||||
|
||||
Targets the nearest **attackable** unit within a cone centered on `facing`
|
||||
(an absolute world angle in radians, as returned by `GetPlayerFacing()`).
|
||||
`coneAngle` is the full cone width in radians; omitted, it defaults to
|
||||
`pi/2` (90° — 45° to either side).
|
||||
|
||||
```lua
|
||||
-- Target whatever enemy you're looking at:
|
||||
TargetDirectionEnemy(GetPlayerFacing())
|
||||
|
||||
-- Wider 180° sweep in front:
|
||||
TargetDirectionEnemy(GetPlayerFacing(), math.pi)
|
||||
```
|
||||
|
||||
Does nothing if no enemy falls inside the cone. "Attackable" uses the
|
||||
engine's own reaction check, so it matches what `TargetNearestEnemy` would
|
||||
consider a valid enemy (hostile, alive, not a critter).
|
||||
|
||||
### `TargetDirectionFriend(facing [, coneAngle])`
|
||||
|
||||
Same as `TargetDirectionEnemy`, but selects the nearest **friendly**
|
||||
(assistable) unit in the cone.
|
||||
|
||||
### `TargetNearest([reverse])`
|
||||
|
||||
Cycles the nearest unit you can attack or assist. Repeated calls within ~1s
|
||||
step through candidates nearest→farthest (a frozen snapshot, so the order
|
||||
is stable mid-cycle); a longer gap or an externally-changed target rebuilds
|
||||
and re-targets the nearest. `reverse` (truthy) steps the other way.
|
||||
|
||||
### `TargetNearestEnemyPlayer([reverse])`
|
||||
|
||||
Like `TargetNearest`, but restricted to hostile **players** (PvP targeting).
|
||||
Same cycle behavior and `reverse` flag.
|
||||
|
||||
### `TargetNearestFriendPlayer([reverse])`
|
||||
|
||||
Like `TargetNearestEnemyPlayer`, but restricted to friendly **players**.
|
||||
|
||||
## TaxiMap
|
||||
|
||||
Backports of the retail taxi-map node enumerators, reading
|
||||
@@ -10937,6 +11365,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
|
||||
@@ -11419,6 +11938,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
|
||||
@@ -11467,13 +12000,25 @@ end
|
||||
|
||||
| Return | Meaning |
|
||||
|--------|---------|
|
||||
| `inRange` | `true` if the unit's world position is within 40 yards of the player's. `false` if out of range OR if `checkedRange` is `false`. |
|
||||
| `inRange` | `true` if the unit is within a 40-yard heal's reach of the player. `false` if out of range OR if `checkedRange` is `false`. |
|
||||
| `checkedRange` | `true` when a position-based range check was performed. `false` when the unit's position isn't available — either the token didn't resolve (empty `partyN` slot, no target, raid member in a different zone, etc.) or `unit == "player"` (see below). |
|
||||
|
||||
Reads the world position via the `CGObject::GetPosition` vtable
|
||||
virtual (slot 5, offset `+0x14`) — same path
|
||||
`CheckInteractDistance` uses. Squared-distance compare against
|
||||
`40 * 40 = 1600`.
|
||||
`CheckInteractDistance` uses.
|
||||
|
||||
**Reach-aware.** The threshold is not a flat 40 yards on the raw
|
||||
center-to-center distance — it's `40 + playerReach + targetReach`,
|
||||
mirroring the engine's own spell-range formula (`FUN_006e3480`), where
|
||||
each unit's reach is the bounding-radius float at `[m_objectFields +
|
||||
0x1F0]` (the same size factor the interact-distance and loot-range
|
||||
checks add). A 40-yard heal lands on a target whose *center* is up to
|
||||
`40 + reach` yards away, so a bare center-distance cap of 40 wrongly
|
||||
reports out-of-range for a target the client can actually heal (on a
|
||||
large target the reach gap is 2–3 yards). If a unit's descriptor isn't
|
||||
populated yet the reach term is 0 and the check degrades to a plain
|
||||
40-yard cap. Note this is the one difference from `UnitDistanceSquared`,
|
||||
which stays raw center-to-center (matching retail).
|
||||
|
||||
> **`UnitInRange("player")` returns `(false, false)`** by design,
|
||||
> matching modern WoW's behavior. The function is meant for healing-
|
||||
@@ -11617,6 +12162,36 @@ 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`).
|
||||
|
||||
### `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.
|
||||
|
||||
### `UnitPower(unit [, powerType])` / `UnitPowerMax(unit [, powerType])`
|
||||
|
||||
Modern multi-power-type getters. Vanilla 1.12 only ships
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ namespace Lua {
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN)
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
|
||||
@@ -100,6 +100,7 @@ using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -142,6 +143,7 @@ extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
|
||||
@@ -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
|
||||
@@ -1467,6 +1500,15 @@ enum Offsets {
|
||||
// `0x0048BACF` / `0x0048BADC`.
|
||||
OFF_CGOBJECT_VTBL_GET_POSITION = 0x14,
|
||||
|
||||
// CGUnit movement-block orientation (facing) in radians, 0..2*pi. Lives
|
||||
// right after the {x,y,z} position block (at +0x9B8), i.e. pos+0xC. The
|
||||
// `GetPosition` virtual copies only x/y/z into the caller's buffer, so
|
||||
// read facing off the object directly. Backs `GetPlayerFacing`. Empirically
|
||||
// located by probing the player object across a known 180-degree turn
|
||||
// (value flipped by ~pi, stayed in range); mirrored at +0x9F8 / +0xC98
|
||||
// (last-sent / render copies) — +0x9C4 is the authoritative one.
|
||||
OFF_UNIT_FACING = 0x9C4,
|
||||
|
||||
// CGObject `m_objectType` field — 0 = (base) object, 1 = item,
|
||||
// 2 = container, 3 = unit, 4 = player, 5 = gameobject, … Read as
|
||||
// `*(int *)(obj + 0x14)`. (Distinct from GET_POSITION above, which
|
||||
@@ -3796,6 +3838,14 @@ enum Offsets {
|
||||
// `rawset(nil)` clear leaves it stale and subsequent `table.insert`
|
||||
// appends past the wiped slots.
|
||||
LUAL_SETN = 0x6F4EA0,
|
||||
// `lua_checkstack(L, n)` — `int __fastcall(L /*ecx*/, n /*edx*/)`.
|
||||
// Ensures room for `n` more stack values, growing if needed; returns 0
|
||||
// (without growing) when `(top-base)/16 + n` would exceed LUA_MAXCSTACK
|
||||
// (0x800 = 2048), 1 otherwise. Verified by the `cmp reg, 0x800` overflow
|
||||
// test at 0x006F2F43. Needed by any C function that pushes an unbounded
|
||||
// number of results (e.g. `strsplit`) — pushing past the ~20-slot
|
||||
// C-call guarantee without growing corrupts the stack.
|
||||
LUA_CHECK_STACK = 0x006F2F30,
|
||||
// `str_find` — the Lua 5.0 string-library `string.find` C function
|
||||
// (`int __fastcall(void *L)`), entry in the strlib luaL_reg table at
|
||||
// `0x00822dd0`. Returns `1` (nil pushed, no match), `2` (start, end),
|
||||
|
||||
+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;
|
||||
|
||||
+40
-5
@@ -14,16 +14,21 @@
|
||||
// Lua 5.1 math-library additions that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `math.fmod(x, y)` — floating-point remainder of `x / y`.
|
||||
// - `math.modf(x)` — split into integral + fractional parts.
|
||||
// - `math.huge` — positive infinity constant.
|
||||
//
|
||||
// This is a pure rename: Lua 5.0 ships the exact same C function under the
|
||||
// name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register `math.fmod` as a direct alias of the engine's `math.mod` C
|
||||
// function (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too
|
||||
// (we don't touch it), so code written for either name works.
|
||||
// `math.fmod` is a pure rename: Lua 5.0 ships the exact same C function under
|
||||
// the name `math.mod`; 5.1 renamed the Lua-facing entry to `math.fmod`. We
|
||||
// register it as a direct alias of the engine's `math.mod` C function
|
||||
// (`FUN_LUA_MATH_FMOD`) — no wrapper. `math.mod` stays available too, so code
|
||||
// written for either name works. `modf` and `huge` are genuinely new in 5.1
|
||||
// (not in 1.12's mathlib table), so we implement/define them here.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace BaseLib::MathLib {
|
||||
|
||||
namespace {
|
||||
@@ -33,8 +38,38 @@ namespace {
|
||||
const auto Script_math_fmod =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_MATH_FMOD);
|
||||
|
||||
// `math.modf(x)` — returns `(integral, fractional)`, the integral part
|
||||
// truncated toward zero and the fractional part carrying `x`'s sign
|
||||
// (`modf(3.7) → 3, 0.7`; `modf(-3.7) → -3, -0.7`). Matches C `modf` / Lua 5.1.
|
||||
int __fastcall Script_math_modf(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: math.modf(x)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
double integral = 0.0;
|
||||
const double fractional = std::modf(Game::Lua::ToNumber(L, 1), &integral);
|
||||
Game::Lua::PushNumber(L, integral);
|
||||
Game::Lua::PushNumber(L, fractional);
|
||||
return 2;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("math", "fmod", Script_math_fmod);
|
||||
Game::Lua::RegisterTableFunction("math", "modf", &Script_math_modf);
|
||||
|
||||
// `math.huge` — a numeric constant, not a function. Fetch the (now
|
||||
// guaranteed-present) `math` table and set the field directly.
|
||||
void *L = Game::Lua::State();
|
||||
if (L != nullptr) {
|
||||
Game::Lua::PushString(L, "math");
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX); // [math]
|
||||
if (Game::Lua::Type(L, -1) == 5 /* LUA_TTABLE */) {
|
||||
Game::Lua::SetFieldNumber(L, "huge", HUGE_VAL); // math.huge = inf; [math]
|
||||
Game::Lua::SetTop(L, -2); // pop [math]
|
||||
} else {
|
||||
Game::Lua::SetTop(L, -2); // pop whatever GetTable returned
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing this; RegisterTableFunction
|
||||
|
||||
+188
-4
@@ -11,16 +11,22 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Lua 5.1 string-library additions that 1.12's Lua 5.0 is missing:
|
||||
// String helpers that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction.
|
||||
// - `string.gmatch(s, pattern)` — match iterator.
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1).
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
// - `strjoin(delimiter, ...)` — WoW global; join varargs.
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
//
|
||||
// Both are the same underlying pattern engine as functions 5.0 already ships:
|
||||
// The two pattern `string.*` ones reuse machinery 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
// start/end indices — so we call the engine's `str_find` and transform.
|
||||
// - `gmatch` is exactly 5.0's `string.gfind` (renamed in 5.1); we register
|
||||
// it as a direct alias of the engine's `gfind` C function.
|
||||
// `strsplit` is a hand-rolled port of 3.3.5's `strsplit` (see below).
|
||||
//
|
||||
// NOTE: only the `string.foo(s, ...)` call form works — NOT the
|
||||
// `("x"):foo(...)` method sugar. WoW's Lua VM strips type-metatables for
|
||||
@@ -33,6 +39,8 @@
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace BaseLib::StringLib {
|
||||
|
||||
@@ -83,9 +91,185 @@ int __fastcall Script_string_match(void *L) {
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`), split `str` on ANY character in `sep` and return the
|
||||
// pieces as multiple values. `pieces > 0` caps the result count, with the
|
||||
// unsplit remainder as the final piece; `0` / omitted = unlimited. Ported
|
||||
// from 3.3.5's `strsplit` (FUN_00816a60) with one deliberate change: we do
|
||||
// NOT `lua_settop(L, 0)` up front. Popping the args would leave the source
|
||||
// string unreferenced, so a GC step triggered by a `pushlstring` could free
|
||||
// the very buffer we're still scanning. Keeping the args on the stack keeps
|
||||
// the source GC-rooted; Lua returns the top N values (our pieces) regardless
|
||||
// of the args sitting below them. Each push is guarded by `lua_checkstack`
|
||||
// (this pushes an unbounded number of results), erroring like 3.3.5 on
|
||||
// genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
const char *segStart = str;
|
||||
int count = 0;
|
||||
// Only scan for separators while unlimited (pieces == 0) or the cap
|
||||
// hasn't been reached (pieces > 1). pieces == 1 (or <= 0 but non-zero)
|
||||
// yields the whole string as a single piece — matches 3.3.5.
|
||||
if (pieces == 0 || pieces > 1) {
|
||||
for (const char *p = str; *p != '\0'; ++p) {
|
||||
bool isSep = false;
|
||||
for (const char *s = sep; *s != '\0'; ++s) {
|
||||
if (*p == *s) {
|
||||
isSep = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isSep)
|
||||
continue;
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, segStart,
|
||||
static_cast<unsigned int>(p - segStart));
|
||||
segStart = p + 1;
|
||||
if (count == pieces - 1)
|
||||
break; // cap hit; remainder becomes the final piece
|
||||
}
|
||||
}
|
||||
if (Game::Lua::CheckStack(L, count + 1) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushString(L, segStart); // final piece (remainder to end)
|
||||
return count + 1;
|
||||
}
|
||||
|
||||
// `strjoin(delimiter, ...)` — the inverse of `strsplit`: concatenate the
|
||||
// vararg pieces with `delimiter` between each. Standard WoW global (FrameXML
|
||||
// defines it in Lua; there's no C original to mirror). A nil / non-coercible
|
||||
// piece is treated as an empty string rather than erroring, so the delimiter
|
||||
// placement stays predictable. `strjoin(",")` with no pieces returns "".
|
||||
int __fastcall Script_strjoin(void *L) {
|
||||
const char *delim = Game::Lua::ToString(L, 1);
|
||||
if (delim == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strjoin(delimiter, ...)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned delimLen = Game::Lua::StrLen(L, 1);
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
|
||||
std::string out; // no Error() past this point — a longjmp would leak it
|
||||
for (int i = 2; i <= top; ++i) {
|
||||
if (i > 2)
|
||||
out.append(delim, delimLen);
|
||||
const char *piece = Game::Lua::ToString(L, i);
|
||||
if (piece != nullptr)
|
||||
out.append(piece, Game::Lua::StrLen(L, i));
|
||||
}
|
||||
Game::Lua::PushLString(L, out.data(), static_cast<unsigned int>(out.size()));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strtrim(str [, chars])` — remove any of the characters in `chars` from
|
||||
// both ends of `str` (default: whitespace). `chars` is a literal set of
|
||||
// characters, not a pattern. Standard WoW global. Two-ended scan — no
|
||||
// allocation, embedded NULs preserved (bounds come from the Lua length).
|
||||
int __fastcall Script_strtrim(void *L) {
|
||||
const char *str = Game::Lua::ToString(L, 1);
|
||||
if (str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strtrim(str [, chars])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned len = Game::Lua::StrLen(L, 1);
|
||||
const char *chars = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2)
|
||||
: " \t\n\v\f\r";
|
||||
|
||||
bool trim[256] = {false};
|
||||
for (const char *c = chars; *c != '\0'; ++c)
|
||||
trim[static_cast<unsigned char>(*c)] = true;
|
||||
|
||||
unsigned start = 0, end = len;
|
||||
while (start < end && trim[static_cast<unsigned char>(str[start])])
|
||||
++start;
|
||||
while (end > start && trim[static_cast<unsigned char>(str[end - 1])])
|
||||
--end;
|
||||
Game::Lua::PushLString(L, str + start, end - start);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strreplace(str, find, replace)` — replace every occurrence of the literal
|
||||
// substring `find` with `replace`; returns `(result, count)`. NOT a stock WoW
|
||||
// global and NOT pattern-based (that's `gsub`) — a plain-text replace that
|
||||
// needs no magic-character escaping. Empty `find` returns `str` unchanged
|
||||
// (count 0) rather than looping forever.
|
||||
int __fastcall Script_strreplace(void *L) {
|
||||
const char *str = Game::Lua::ToString(L, 1);
|
||||
const char *find = Game::Lua::ToString(L, 2);
|
||||
const char *repl = Game::Lua::ToString(L, 3);
|
||||
if (str == nullptr || find == nullptr || repl == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strreplace(str, find, replace)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned strLen = Game::Lua::StrLen(L, 1);
|
||||
const unsigned findLen = Game::Lua::StrLen(L, 2);
|
||||
const unsigned replLen = Game::Lua::StrLen(L, 3);
|
||||
|
||||
if (findLen == 0) { // nothing to match — return input unchanged
|
||||
Game::Lua::PushLString(L, str, strLen);
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 2;
|
||||
}
|
||||
|
||||
std::string out; // no Error() past this point
|
||||
int count = 0;
|
||||
unsigned i = 0;
|
||||
while (i < strLen) {
|
||||
if (i + findLen <= strLen && std::memcmp(str + i, find, findLen) == 0) {
|
||||
out.append(repl, replLen);
|
||||
i += findLen;
|
||||
++count;
|
||||
} else {
|
||||
out.push_back(str[i]);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushLString(L, out.data(), static_cast<unsigned int>(out.size()));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(count));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// `string.reverse(s)` / `strrev(s)` — return `s` with its bytes reversed.
|
||||
// Lua 5.1 added `string.reverse`; 5.0's strlib lacks it. Byte-wise (not
|
||||
// UTF-8 aware), matching stock Lua; embedded NULs preserved (bounds from the
|
||||
// Lua length).
|
||||
int __fastcall Script_string_reverse(void *L) {
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
if (s == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: string.reverse(s)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const unsigned len = Game::Lua::StrLen(L, 1);
|
||||
std::string out(len, '\0'); // no Error() past this point
|
||||
for (unsigned i = 0; i < len; ++i)
|
||||
out[i] = s[len - 1 - i];
|
||||
Game::Lua::PushLString(L, out.data(), len);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
Game::Lua::RegisterTableFunction("string", "reverse", &Script_string_reverse);
|
||||
Game::Lua::RegisterGlobalFunction("strsplit", &Script_strsplit);
|
||||
Game::Lua::RegisterGlobalFunction("strjoin", &Script_strjoin);
|
||||
Game::Lua::RegisterGlobalFunction("strtrim", &Script_strtrim);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strrev", &Script_string_reverse);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -107,11 +107,19 @@ static int AuxResume(void *L, void *co, int nargs) {
|
||||
: "cannot resume non-suspended coroutine");
|
||||
return -1;
|
||||
}
|
||||
if (Game::Lua::CheckStack(co, nargs) == 0) {
|
||||
Game::Lua::PushString(L, "too many arguments to resume coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, nargs);
|
||||
const int s = Game::Lua::Resume(co, nargs);
|
||||
const bool yielded = LuaCI(co) > LuaBaseCI(co);
|
||||
if (s == LUA_OK || yielded) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
if (Game::Lua::CheckStack(L, nres) == 0) {
|
||||
Game::Lua::PushString(L, "too many results to resume coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(co, L, nres);
|
||||
return nres;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// 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/>.
|
||||
|
||||
// `PLAYER_ENTERING_WORLD(isInitialLogin, isReloadingUi)` — the 8.0.1 payload,
|
||||
// backported. Vanilla fires PEW with no args; backported addons expect the
|
||||
// two booleans to distinguish a fresh login from a `/reload` from an
|
||||
// instance/zone transition.
|
||||
//
|
||||
// Delivery: PEW is a no-arg event, so the engine broadcasts it through
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` (the `__fastcall(int)` dispatcher), NOT the
|
||||
// variadic `FUN_FIRE_EVENT`. We intercept that dispatcher (via the shared
|
||||
// `Event::SignalHook`) and, for the PEW event id, suppress the no-arg fire
|
||||
// and instead re-broadcast through the variadic `FUN_FIRE_EVENT` with a
|
||||
// two-boolean payload. Both fire paths read the same frame chain off the
|
||||
// event's table slot, so the frames registered for PEW receive the args
|
||||
// exactly once. Booleans use the `%d`/`%s`+nullptr trick (`1` / `nil`), so
|
||||
// `if isInitialLogin then …` reads naturally.
|
||||
//
|
||||
// Flags, from signals we already produce — no PEW-specific engine hook:
|
||||
// - isInitialLogin: our GlueModuleAutoRegister runs on every glue-screen
|
||||
// boot (initial launch + each logout return), so a set latch means "the
|
||||
// next enter-world is a fresh login". The first PEW consumes it.
|
||||
// - isReloadingUi: our ModuleAutoRegister runs whenever the in-game Lua
|
||||
// state is (re)built — login OR `/reload`, but NOT a zone transition. So
|
||||
// "script re-init happened and it wasn't the fresh login" == a reload.
|
||||
// - Instance/zone transition rebuilds neither, so both latches are clear →
|
||||
// both false, matching retail.
|
||||
|
||||
#include "Game.h"
|
||||
#include "event/Custom.h"
|
||||
#include "event/SignalHook.h"
|
||||
|
||||
namespace Event::EnteringWorld {
|
||||
|
||||
namespace {
|
||||
|
||||
// Set on glue-screen boot; a pending latch means the next enter-world is a
|
||||
// fresh character login (from the login/char-select screen).
|
||||
bool g_atGlue = false;
|
||||
// Set when the in-game Lua state is (re)built (login or /reload).
|
||||
bool g_scriptReloaded = false;
|
||||
// Cached PLAYER_ENTERING_WORLD event-table slot; -1 = unresolved. Reset per
|
||||
// in-game init (the table is rebuilt, though engine slots are stable) and
|
||||
// re-resolved lazily at fire time.
|
||||
int g_pewId = -1;
|
||||
|
||||
int PewId() {
|
||||
if (g_pewId < 0)
|
||||
g_pewId = Event::Custom::LookupByName("PLAYER_ENTERING_WORLD");
|
||||
return g_pewId;
|
||||
}
|
||||
|
||||
// Shared-hook interceptor. For PEW, re-broadcast with the payload and
|
||||
// suppress the engine's no-arg fire; everything else passes through.
|
||||
bool Intercept(int eventID) {
|
||||
const int pew = PewId();
|
||||
if (pew < 0 || eventID != pew)
|
||||
return false;
|
||||
|
||||
const bool isInitialLogin = g_atGlue;
|
||||
g_atGlue = false;
|
||||
const bool isReloadingUi = !isInitialLogin && g_scriptReloaded;
|
||||
g_scriptReloaded = false;
|
||||
|
||||
// Re-fire through the variadic dispatcher with the two booleans. true → 1
|
||||
// (%d), false → nil (%s + nullptr, which the dispatcher's pushstring
|
||||
// tail-jumps to pushnil).
|
||||
auto *const kNil = static_cast<const char *>(nullptr);
|
||||
if (isInitialLogin && isReloadingUi)
|
||||
Event::Custom::Fire(pew, "%d%d", 1, 1);
|
||||
else if (isInitialLogin)
|
||||
Event::Custom::Fire(pew, "%d%s", 1, kNil);
|
||||
else if (isReloadingUi)
|
||||
Event::Custom::Fire(pew, "%s%d", kNil, 1);
|
||||
else
|
||||
Event::Custom::Fire(pew, "%s%s", kNil, kNil);
|
||||
return true; // handled — suppress the original no-arg PEW fire
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _intercept{&Intercept};
|
||||
|
||||
// In-game Lua (re)init: login or /reload rebuilt the script state (never a
|
||||
// zone transition). Also re-arm the PEW slot lookup — the event table was
|
||||
// rebuilt.
|
||||
void OnInGameInit() {
|
||||
g_scriptReloaded = true;
|
||||
g_pewId = -1;
|
||||
}
|
||||
|
||||
// Glue-screen boot: we're at the login/char-select screen, so the next
|
||||
// enter-world will be a fresh login.
|
||||
void OnGlueInit() { g_atGlue = true; }
|
||||
|
||||
const Game::ModuleAutoRegister _inGame{&OnInGameInit};
|
||||
const Game::GlueModuleAutoRegister _glue{&OnGlueInit};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Event::EnteringWorld
|
||||
@@ -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
|
||||
@@ -0,0 +1,49 @@
|
||||
// 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 "SignalHook.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Event::SignalHook {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init time by each `AutoSubscribe` constructor, before
|
||||
// `DllMain`. Zero-initialized (namespace static) ahead of any dynamic init,
|
||||
// so the head is valid when subscribers link in.
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` is `__fastcall(int eventID)`; we mirror loot's
|
||||
// long-standing shape with a dummy EDX so the trampoline call matches the
|
||||
// register layout, and forward `0` for it (unused by the callee).
|
||||
using FireNoArgs_t = void(__fastcall *)(int eventID, int edx);
|
||||
FireNoArgs_t g_orig = nullptr;
|
||||
|
||||
void __fastcall Hook(int eventID, int /*edx*/) {
|
||||
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
if (s->fn(eventID))
|
||||
return; // a subscriber handled it — suppress the original fire
|
||||
g_orig(eventID, 0);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS, reinterpret_cast<void *>(&Hook),
|
||||
reinterpret_cast<void **>(&g_orig)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Interceptor f) : fn(f), next(g_head) { g_head = this; }
|
||||
|
||||
} // namespace Event::SignalHook
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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
|
||||
|
||||
// Shared interceptor for the engine's no-arg event dispatcher
|
||||
// `FUN_FIRE_EVENT_NO_ARGS` (`0x00703E50`) — the `__fastcall(int eventID)`
|
||||
// path every no-arg event broadcast goes through (PLAYER_ENTERING_WORLD,
|
||||
// LOOT_OPENED/CLOSED, zone changes, …), as opposed to the variadic
|
||||
// `FUN_FIRE_EVENT` used by events that carry a printf-style payload.
|
||||
//
|
||||
// MinHook allows only ONE hook per target address, and more than one feature
|
||||
// needs to intercept this dispatcher (loot silent-scan suppression, the
|
||||
// PLAYER_ENTERING_WORLD payload). So — exactly like `Tick::WorldTick` for the
|
||||
// per-frame tick — this module owns the single hook and fans out to
|
||||
// subscribers. Declare a file-scope `static const
|
||||
// Event::SignalHook::AutoSubscribe` in your module.
|
||||
//
|
||||
// An interceptor returns `true` when it has fully handled `eventID` and the
|
||||
// engine's original no-arg fire must be SUPPRESSED — either because it
|
||||
// swallowed the event (loot) or re-fired it a different way (the PEW payload
|
||||
// re-broadcasts through the variadic `FUN_FIRE_EVENT` with args). Returning
|
||||
// `false` lets the event fall through to the next subscriber and ultimately
|
||||
// the original. The FIRST subscriber to return `true` wins and the original
|
||||
// is skipped, so subscribers must claim disjoint event ids.
|
||||
|
||||
namespace Event::SignalHook {
|
||||
|
||||
using Interceptor = bool (*)(int eventID);
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Interceptor fn);
|
||||
Interceptor fn;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Event::SignalHook
|
||||
@@ -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};
|
||||
|
||||
+10
-14
@@ -27,6 +27,7 @@
|
||||
#include "../Game.h"
|
||||
#include "../Offsets.h"
|
||||
#include "../event/Custom.h"
|
||||
#include "../event/SignalHook.h"
|
||||
#include "../guid/Guid.h"
|
||||
#include "../tick/WorldTick.h"
|
||||
|
||||
@@ -115,9 +116,6 @@ using LootSlotLinkBuilder_t = const char *(__fastcall *)(uint32_t userFacingSlot
|
||||
using LootController_t = void(__fastcall *)(int lootStructPtr,
|
||||
int coin,
|
||||
int unk);
|
||||
using FireEventNoArgs_t = void(__fastcall *)(int eventID,
|
||||
int /*edx unused*/);
|
||||
|
||||
struct C3Vector {
|
||||
float x, y, z;
|
||||
};
|
||||
@@ -256,7 +254,6 @@ void StartLoot(uint64_t guid) {
|
||||
// === Hooks ===
|
||||
|
||||
LootController_t s_lootController_o = nullptr;
|
||||
FireEventNoArgs_t s_fireEventNoArgs_o = nullptr;
|
||||
|
||||
// `LOOT_OPENED` / `LOOT_CLOSED` get suppressed only while a scan is
|
||||
// actively interleaving open/close cycles, signaled by
|
||||
@@ -265,12 +262,14 @@ FireEventNoArgs_t s_fireEventNoArgs_o = nullptr;
|
||||
// `SendCloseLoot` that follows, then dropped before
|
||||
// `TryStartNext`-initiated CMSG_LOOTs go out. Every other engine
|
||||
// event fire — and normal play's loot events — pass through.
|
||||
void __fastcall FireEventNoArgs_h(int eventID, int /*unusedEdx*/) {
|
||||
if (s_scan.suppressEvents &&
|
||||
(eventID == EVENT_LOOT_OPENED || eventID == EVENT_LOOT_CLOSED)) {
|
||||
return;
|
||||
}
|
||||
s_fireEventNoArgs_o(eventID, 0);
|
||||
//
|
||||
// Runs as an interceptor on the shared no-arg event hook
|
||||
// (`Event::SignalHook`) rather than owning the hook directly, so it can
|
||||
// coexist with other features that intercept the same dispatcher (e.g. the
|
||||
// PLAYER_ENTERING_WORLD payload). Return true to swallow.
|
||||
bool SuppressLootEvents(int eventID) {
|
||||
return s_scan.suppressEvents &&
|
||||
(eventID == EVENT_LOOT_OPENED || eventID == EVENT_LOOT_CLOSED);
|
||||
}
|
||||
|
||||
// Intercepts the engine's loot-window state controller. Two paths:
|
||||
@@ -336,10 +335,7 @@ const Game::HookAutoRegister _hookController{
|
||||
reinterpret_cast<void *>(&LootController_h),
|
||||
reinterpret_cast<void **>(&s_lootController_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookFireEvent{
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<void *>(&FireEventNoArgs_h),
|
||||
reinterpret_cast<void **>(&s_fireEventNoArgs_o)};
|
||||
const Event::SignalHook::AutoSubscribe _lootIntercept{&SuppressLootEvents};
|
||||
|
||||
// === Tick (timeout only) ===
|
||||
|
||||
|
||||
+7
-66
@@ -56,6 +56,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "ui/FrameObject.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -66,9 +67,6 @@ namespace {
|
||||
using NamePlate::Walk::ForEachNamePlatedUnit;
|
||||
using NamePlate::Walk::kOffUnitNamePlate;
|
||||
|
||||
using LuaRawGetI_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
using ScriptRegister_t = void(__fastcall *)(void *this_, void *edx_unused,
|
||||
const char *name);
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
using ResolveByGUID_t = void *(__fastcall *)(int type, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
@@ -115,70 +113,13 @@ static int __fastcall Script_GetNamePlateGUIDs(void *L) {
|
||||
// frame-push path. Internal callers (Script_GetNamePlates etc.) call
|
||||
// it through the unqualified name (they live in the same namespace).
|
||||
//
|
||||
// Delegate wrapper construction to the engine's own
|
||||
// `FrameScript_Object::ScriptRegister`. First call for a never-seen
|
||||
// nameplate builds a `{[0] = lightuserdata}` table with the
|
||||
// framescript metatable, `luaL_ref`s it into the registry, and
|
||||
// writes the refkey to `nameplate + 0x08` (`OFF_COBJECT_LUA_REGISTRY_REF`).
|
||||
// Subsequent calls find a populated refkey and just rawgeti.
|
||||
//
|
||||
// Why delegate instead of building our own wrapper:
|
||||
//
|
||||
// - **Single source of truth.** Every code path that pushes this
|
||||
// frame — ours via this function, addons' `CreateFrame("Type",
|
||||
// "name", plate)` extracting the parent, the engine's own internal
|
||||
// pushers — funnels through `rawgeti(REGISTRY, plate+0x08)` and
|
||||
// gets the same Lua table. Earlier "build our own wrapper +
|
||||
// `luaL_ref` it" approach kept a side cache, but pfUI's
|
||||
// `CreateFrame("Button", "name", parent)` inside `NAME_PLATE_CREATED`
|
||||
// ends up causing the engine to register the parent itself —
|
||||
// the engine's wrapper diverges from ours, and later
|
||||
// `GetNamePlateForUnit` calls return the engine's bare wrapper
|
||||
// instead of our decorated one. Addon fields on the wrapper
|
||||
// (`plate.nameplate = styledButton`) disappear from the API's
|
||||
// perspective even though they still exist on the orphaned
|
||||
// table.
|
||||
//
|
||||
// - **Refcount-pinning is benign here.** `ScriptRegister`
|
||||
// unconditionally increments `nameplate + 0x04`, which normally
|
||||
// keeps the engine from GC-ing the CObject. Nameplates are
|
||||
// pool-managed and never destroyed during a session, so pinning
|
||||
// doesn't actually leak anything. We call `ScriptRegister` at
|
||||
// most once per nameplate pointer (gated by `refKey <= 0`), so
|
||||
// the refcount tops out at one increment per pool slot.
|
||||
//
|
||||
// Defensive fallback: if the registry slot exists but isn't a table
|
||||
// any more (some other code freed it), we re-register. This shouldn't
|
||||
// happen in practice — engines only unref on frame destruction —
|
||||
// but a stale type check keeps callers from ever seeing a non-table.
|
||||
// Thin wrapper over the shared `UI::FrameObject::Push` — nameplates and
|
||||
// chat bubbles are both engine-created-in-C++ `CSimpleFrame`s that need
|
||||
// the same lazy `ScriptRegister` → `rawgeti` push so addons get the
|
||||
// engine's canonical wrapper (see that helper for the pfUI-divergence
|
||||
// rationale that motivated delegating to the engine).
|
||||
void PushNamePlateFrame(void *L, void *nameplate) {
|
||||
auto rawgeti = reinterpret_cast<LuaRawGetI_t>(
|
||||
Offsets::FUN_FRAMESCRIPT_PUSH_OBJECT);
|
||||
auto scriptRegister = reinterpret_cast<ScriptRegister_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER));
|
||||
|
||||
int refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
if (refKey <= 0) {
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
}
|
||||
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
|
||||
return;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
|
||||
// Slot got freed somewhere — re-register to rebuild a fresh
|
||||
// wrapper and slot. Zero the refcount so `ScriptRegister`
|
||||
// re-enters its build branch.
|
||||
*reinterpret_cast<int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REFCOUNT) = 0;
|
||||
scriptRegister(nameplate, nullptr, nullptr);
|
||||
refKey = *reinterpret_cast<const int *>(
|
||||
static_cast<uint8_t *>(nameplate) + Offsets::OFF_COBJECT_LUA_REGISTRY_REF);
|
||||
rawgeti(L, Game::Lua::REGISTRY_INDEX, refKey);
|
||||
UI::FrameObject::Push(L, nameplate);
|
||||
}
|
||||
|
||||
// GUID → nameplate Frame pushed on stack. Pushes nil if the GUID
|
||||
|
||||
@@ -0,0 +1,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,328 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Backports the post-vanilla TargetScript selection functions that 1.12.1
|
||||
// doesn't ship, built on the engine's own tab-targeting internals:
|
||||
//
|
||||
// TargetNearest() — cycle nearest attackable/assistable unit
|
||||
// TargetNearestEnemyPlayer([rev]) — cycle nearest hostile player (PvP)
|
||||
// TargetNearestFriendPlayer([rev]) — cycle nearest friendly player
|
||||
// TargetDirectionEnemy(facing[,cone]) — nearest hostile in a facing cone
|
||||
// TargetDirectionFriend(facing[,cone]) — nearest friendly in a facing cone
|
||||
//
|
||||
// Vanilla already ships `TargetNearestEnemy/Friend/PartyMember/RaidMember`;
|
||||
// they run through `FUN_00493f60(reverse, mode)` — enumerate visible units,
|
||||
// keep those `FUN_00493e40(player, cand, mode)` accepts, sort by distance,
|
||||
// cycle, and commit via `FUN_00493540(guidLo, guidHi)`. We reuse the two
|
||||
// load-bearing pieces of that machinery directly:
|
||||
//
|
||||
// * `FUN_00493e40` (Offsets::FUN_TARGET_CANDIDATE_VALID) — the exact
|
||||
// per-mode validity predicate (mode 1 = enemy: attackable + alive +
|
||||
// not-critter; mode 2 = friend: assistable + alive). Calling it gives
|
||||
// us the engine's real reaction/hostility semantics with zero
|
||||
// reimplementation.
|
||||
// * `FUN_00493540` (Offsets::FUN_SET_SELECTION_BY_GUID) — the canonical
|
||||
// setter the native cycle commits through (CMSG_SET_SELECTION + client
|
||||
// target + event 0xC3), which also maintains the current-selection
|
||||
// globals we read back for cycle continuity.
|
||||
//
|
||||
// So the only genuinely new logic here is (a) an extra player-GUID filter
|
||||
// for the `*Player` variants and (b) the cone geometry for the direction
|
||||
// variants — everything reaction-related defers to the engine.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Target::Nearest {
|
||||
|
||||
namespace {
|
||||
|
||||
// Candidate class each function selects from.
|
||||
enum class Filter { Enemy, Friend, EnemyPlayer, FriendPlayer, Any };
|
||||
constexpr int kFilterCount = 5;
|
||||
|
||||
// Default cone (full width, radians) when `coneAngle` is omitted — 90°, so
|
||||
// 45° to either side of the facing. The engine's own default constant
|
||||
// wasn't cleanly recoverable from the 3.3.5 reference binary; this is a
|
||||
// deliberate, documented choice that matches the "roughly in front of me"
|
||||
// intent of a direction-target keybind.
|
||||
constexpr float kDefaultConeAngle = 1.5707963f; // pi/2
|
||||
|
||||
// Re-cycle window: repeated presses within this many ms advance through the
|
||||
// frozen candidate snapshot (no ping-pong as distances shift); a longer gap
|
||||
// rebuilds and re-targets the nearest. Mirrors the ~1s reset the native
|
||||
// cycle uses.
|
||||
constexpr uint32_t kCycleWindowMs = 1000;
|
||||
|
||||
constexpr int kMaxCandidates = 64;
|
||||
|
||||
// Engine entry points ─────────────────────────────────────────────────
|
||||
using Predicate_t = int(__fastcall *)(void *player, void *cand, int mode);
|
||||
using SetTargetByGuid_t = void(__fastcall *)(const uint64_t *guid);
|
||||
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
|
||||
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
|
||||
using ObjectPtr_t = void *(__fastcall *)(uint32_t typeMask, const char *dbg,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int dbgCode);
|
||||
using TickMs_t = uint32_t(__fastcall *)();
|
||||
|
||||
bool PassesFilter(void *player, void *cand, uint64_t guid, Filter f) {
|
||||
auto valid = reinterpret_cast<Predicate_t>(Offsets::FUN_TARGET_CANDIDATE_VALID);
|
||||
switch (f) {
|
||||
case Filter::Enemy: return valid(player, cand, 1) != 0;
|
||||
case Filter::Friend: return valid(player, cand, 2) != 0;
|
||||
case Filter::EnemyPlayer: return Guid::IsPlayer(guid) && valid(player, cand, 1) != 0;
|
||||
case Filter::FriendPlayer: return Guid::IsPlayer(guid) && valid(player, cand, 2) != 0;
|
||||
case Filter::Any: return valid(player, cand, 1) != 0 ||
|
||||
valid(player, cand, 2) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct Candidate {
|
||||
uint64_t guid;
|
||||
float x, y; // world XY (for the direction cone)
|
||||
float distSq; // 3D squared distance from the player (for nearest)
|
||||
};
|
||||
|
||||
struct CollectCtx {
|
||||
void *player;
|
||||
Filter filter;
|
||||
float px, py, pz;
|
||||
Candidate list[kMaxCandidates];
|
||||
int count;
|
||||
};
|
||||
|
||||
int __fastcall CollectCb(CollectCtx *ctx, void * /*edx*/, uint64_t guid) {
|
||||
if (ctx->count >= kMaxCandidates)
|
||||
return 1;
|
||||
// Players and creatures both resolve under TYPEMASK_UNIT (CGPlayer
|
||||
// derives from CGUnit); non-unit objects resolve to null and drop out.
|
||||
auto ObjectPtr =
|
||||
reinterpret_cast<ObjectPtr_t>(Offsets::FUN_CLNT_OBJ_MGR_OBJECT_PTR);
|
||||
void *cand = ObjectPtr(Offsets::TYPEMASK_UNIT, nullptr,
|
||||
static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), 0);
|
||||
if (cand == nullptr || cand == ctx->player)
|
||||
return 1;
|
||||
if (!PassesFilter(ctx->player, cand, guid, ctx->filter))
|
||||
return 1;
|
||||
|
||||
float pos[3];
|
||||
if (!Unit::Position::Read(cand, pos))
|
||||
return 1;
|
||||
const float dx = pos[0] - ctx->px;
|
||||
const float dy = pos[1] - ctx->py;
|
||||
const float dz = pos[2] - ctx->pz;
|
||||
Candidate &c = ctx->list[ctx->count++];
|
||||
c.guid = guid;
|
||||
c.x = pos[0];
|
||||
c.y = pos[1];
|
||||
c.distSq = dx * dx + dy * dy + dz * dz;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Enumerate visible units matching `f` into `out`. Returns false (empty)
|
||||
// pre-world or on a player-position miss.
|
||||
bool Collect(Filter f, CollectCtx *out) {
|
||||
out->count = 0;
|
||||
// The enumerator derefs the object manager unconditionally; null on the
|
||||
// glue / character-select screen.
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) ==
|
||||
nullptr)
|
||||
return false;
|
||||
void *player = Unit::Position::ResolveToken("player");
|
||||
float ppos[3];
|
||||
if (!Unit::Position::Read(player, ppos))
|
||||
return false;
|
||||
|
||||
out->player = player;
|
||||
out->filter = f;
|
||||
out->px = ppos[0];
|
||||
out->py = ppos[1];
|
||||
out->pz = ppos[2];
|
||||
auto Enum = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS);
|
||||
Enum(reinterpret_cast<EnumCallback_t>(&CollectCb), out);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Commit the selection through the engine's `Script_TargetUnit` helper
|
||||
// rather than the tab-cycle's raw `FUN_00493540`: this one first checks the
|
||||
// GUID resolves to a live unit and bails otherwise, so a stale GUID left in
|
||||
// a cycle snapshot (unit despawned / went out of range between presses)
|
||||
// gets rejected instead of committed. Committing a dead GUID would leave
|
||||
// `"target"` pointing at freed unit data and crash the next handler that
|
||||
// reads it. `FUN_TARGET_BY_GUID` still routes through `FUN_00493540`
|
||||
// internally, so the current-selection globals we read back are maintained.
|
||||
void SetTarget(uint64_t guid) {
|
||||
reinterpret_cast<SetTargetByGuid_t>(Offsets::FUN_TARGET_BY_GUID)(&guid);
|
||||
}
|
||||
|
||||
uint64_t CurrentSelection() {
|
||||
const uint32_t lo = *reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
const uint32_t hi = *reinterpret_cast<volatile uint32_t *>(
|
||||
Offsets::VAR_CURRENT_SELECTION_GUID_HI);
|
||||
return (static_cast<uint64_t>(hi) << 32) | lo;
|
||||
}
|
||||
|
||||
uint32_t NowMs() {
|
||||
return reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)();
|
||||
}
|
||||
|
||||
// Ascending insertion sort by 3D distance (candidate counts are tiny).
|
||||
void SortByDistance(Candidate *a, int n) {
|
||||
for (int i = 1; i < n; ++i) {
|
||||
Candidate key = a[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && a[j].distSq > key.distSq) {
|
||||
a[j + 1] = a[j];
|
||||
--j;
|
||||
}
|
||||
a[j + 1] = key;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-filter tab-cycle state. A frozen snapshot of GUIDs (sorted nearest→
|
||||
// farthest) plus a cursor; presses within `kCycleWindowMs` that haven't had
|
||||
// the target changed out from under them advance the cursor over the frozen
|
||||
// list, so the order stays stable through a cycle burst.
|
||||
struct CycleState {
|
||||
bool valid = false;
|
||||
uint64_t snapshot[kMaxCandidates];
|
||||
int count = 0;
|
||||
int index = 0;
|
||||
uint32_t lastPressMs = 0;
|
||||
uint64_t lastSetGuid = 0;
|
||||
};
|
||||
CycleState g_cycle[kFilterCount];
|
||||
|
||||
void DoCycle(void *L, Filter f) {
|
||||
const bool reverse = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
CycleState &st = g_cycle[static_cast<int>(f)];
|
||||
const uint32_t now = NowMs();
|
||||
|
||||
const bool cont = st.valid && st.count > 0 &&
|
||||
(now - st.lastPressMs) <= kCycleWindowMs &&
|
||||
CurrentSelection() == st.lastSetGuid;
|
||||
|
||||
if (cont) {
|
||||
st.index = reverse ? (st.index + st.count - 1) % st.count
|
||||
: (st.index + 1) % st.count;
|
||||
} else {
|
||||
CollectCtx c;
|
||||
if (!Collect(f, &c) || c.count == 0) {
|
||||
st.valid = false;
|
||||
return;
|
||||
}
|
||||
SortByDistance(c.list, c.count);
|
||||
st.count = c.count;
|
||||
for (int i = 0; i < c.count; ++i)
|
||||
st.snapshot[i] = c.list[i].guid;
|
||||
st.index = 0;
|
||||
st.valid = true;
|
||||
}
|
||||
|
||||
const uint64_t guid = st.snapshot[st.index];
|
||||
SetTarget(guid);
|
||||
st.lastSetGuid = guid;
|
||||
st.lastPressMs = now;
|
||||
}
|
||||
|
||||
void DoDirection(void *L, bool hostile, const char *usage) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, usage);
|
||||
return;
|
||||
}
|
||||
const float facing = static_cast<float>(Game::Lua::ToNumber(L, 1));
|
||||
float cone = Game::Lua::IsNumber(L, 2)
|
||||
? static_cast<float>(Game::Lua::ToNumber(L, 2))
|
||||
: kDefaultConeAngle;
|
||||
if (cone <= 0.0f)
|
||||
cone = kDefaultConeAngle;
|
||||
const float cosHalf = std::cos(cone * 0.5f);
|
||||
const float dirX = std::cos(facing);
|
||||
const float dirY = std::sin(facing);
|
||||
|
||||
CollectCtx c;
|
||||
if (!Collect(hostile ? Filter::Enemy : Filter::Friend, &c) || c.count == 0)
|
||||
return;
|
||||
|
||||
int best = -1;
|
||||
float bestDistSq = 0.0f;
|
||||
for (int i = 0; i < c.count; ++i) {
|
||||
const float vx = c.list[i].x - c.px;
|
||||
const float vy = c.list[i].y - c.py;
|
||||
const float horizSq = vx * vx + vy * vy;
|
||||
if (horizSq <= 0.0001f)
|
||||
continue;
|
||||
// cos(angle between facing and the horizontal vector to the unit).
|
||||
const float cosAngle = (dirX * vx + dirY * vy) / std::sqrt(horizSq);
|
||||
if (cosAngle < cosHalf)
|
||||
continue; // outside the cone
|
||||
if (best < 0 || c.list[i].distSq < bestDistSq) {
|
||||
best = i;
|
||||
bestDistSq = c.list[i].distSq;
|
||||
}
|
||||
}
|
||||
if (best >= 0)
|
||||
SetTarget(c.list[best].guid);
|
||||
}
|
||||
|
||||
// Lua entry points ─────────────────────────────────────────────────────
|
||||
int __fastcall Script_TargetNearest(void *L) {
|
||||
DoCycle(L, Filter::Any);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetNearestEnemyPlayer(void *L) {
|
||||
DoCycle(L, Filter::EnemyPlayer);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetNearestFriendPlayer(void *L) {
|
||||
DoCycle(L, Filter::FriendPlayer);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetDirectionEnemy(void *L) {
|
||||
DoDirection(L, /*hostile*/ true,
|
||||
"Usage: TargetDirectionEnemy(facing [, coneAngle])");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_TargetDirectionFriend(void *L) {
|
||||
DoDirection(L, /*hostile*/ false,
|
||||
"Usage: TargetDirectionFriend(facing [, coneAngle])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearest", &Script_TargetNearest);
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearestEnemyPlayer",
|
||||
&Script_TargetNearestEnemyPlayer);
|
||||
Game::Lua::RegisterGlobalFunction("TargetNearestFriendPlayer",
|
||||
&Script_TargetNearestFriendPlayer);
|
||||
Game::Lua::RegisterGlobalFunction("TargetDirectionEnemy",
|
||||
&Script_TargetDirectionEnemy);
|
||||
Game::Lua::RegisterGlobalFunction("TargetDirectionFriend",
|
||||
&Script_TargetDirectionFriend);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Target::Nearest
|
||||
@@ -0,0 +1,546 @@
|
||||
// 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 "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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -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};
|
||||
|
||||
+69
-16
@@ -13,20 +13,33 @@
|
||||
// `GetPosition` virtual (vtable slot 5) `CheckInteractDistance` uses:
|
||||
//
|
||||
// `UnitInRange(unit) → (inRange, checkedRange)` — 40-yard healing-
|
||||
// range check (fixed threshold on the squared distance).
|
||||
// range check. Reach-aware: matches the engine's own spell-range
|
||||
// formula (see below), so a 40-yard heal registers in-range against
|
||||
// a target whose *center* is slightly past 40 yards but whose body
|
||||
// is within reach.
|
||||
// `UnitDistanceSquared(unit) → (distanceSquared, checkedPosition)` —
|
||||
// the raw squared world distance player→unit. Squared because
|
||||
// nearly every consumer compares against a threshold (`distSq <=
|
||||
// range*range`) or ranks by nearest, neither of which needs the
|
||||
// sqrt; that's why retail exposes only the squared form (added
|
||||
// 5.0.4). Center-to-center — NOT reach-aware (that's UnitXP_SP3's
|
||||
// niche); equivalent to computing it from SuperWoW `UnitPosition`,
|
||||
// but self-contained (reads the position in C, no sibling-DLL
|
||||
// dependency, no clash with SuperWoW's own `UnitPosition`).
|
||||
// 5.0.4). Center-to-center — this one is NOT reach-adjusted (retail's
|
||||
// `UnitDistanceSquared` is also raw center distance); the reach
|
||||
// correction lives only in `UnitInRange`.
|
||||
//
|
||||
// Reach-awareness: the engine's spell-range helper `FUN_006e3480` compares
|
||||
// center distance against `baseRange + reachCaster + reachTarget`, where
|
||||
// each unit's reach is the bounding-radius float at `[m_objectFields +
|
||||
// 0x1F0]` (the same size factor the interact-distance and loot-range
|
||||
// checks add). So the effective in-range distance for a 40-yard heal is
|
||||
// `40 + playerReach + targetReach`. On big targets that's easily 2–3
|
||||
// yards past the raw 40, which is why a pure center-to-center 40-yard cap
|
||||
// reported out-of-range for a target the client could actually heal.
|
||||
|
||||
#include "Game.h"
|
||||
#include "unit/Position.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Unit::Range {
|
||||
@@ -34,18 +47,44 @@ namespace Unit::Range {
|
||||
namespace {
|
||||
|
||||
constexpr float kRangeYards = 40.0f;
|
||||
constexpr float kRangeYardsSq = kRangeYards * kRangeYards;
|
||||
|
||||
// Squared world distance from the player to `token`'s unit. Returns true
|
||||
// and writes *outSq on success; false when either position is unavailable
|
||||
// (unresolved / absent token, or an object with no known position yet —
|
||||
// e.g. a party member outside the client's sync range). This false is
|
||||
// exactly the `checkedPosition = false` case retail flags.
|
||||
// Reads a unit's bounding radius (the engine's per-unit range "size
|
||||
// factor") from `[m_objectFields + 0x1F0]`. Returns 0 for a null object
|
||||
// or a unit whose descriptor isn't populated yet — a safe fallback that
|
||||
// degrades to the plain base-range check. Same field the loot/interact
|
||||
// range checks (loot/Nearby.cpp) read.
|
||||
float BoundingRadius(void *obj) {
|
||||
if (obj == nullptr)
|
||||
return 0.0f;
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(obj) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (fields == nullptr)
|
||||
return 0.0f;
|
||||
return *reinterpret_cast<const float *>(
|
||||
fields + Offsets::OFF_UNIT_FIELD_BOUNDING_RADIUS);
|
||||
}
|
||||
|
||||
// Resolves `token` once and reads both its world position and bounding
|
||||
// radius. Returns false when the position is unavailable (unresolved /
|
||||
// absent token, or an object with no known position yet — e.g. a party
|
||||
// member outside the client's sync range). This false is exactly the
|
||||
// `checkedPosition = false` case retail flags.
|
||||
bool ReadPosAndReach(const char *token, float pos[3], float *reach) {
|
||||
void *obj = Unit::Position::ResolveToken(token);
|
||||
if (!Unit::Position::Read(obj, pos))
|
||||
return false;
|
||||
*reach = BoundingRadius(obj);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Squared world distance from the player to `token`'s unit (raw,
|
||||
// center-to-center; reach is applied by the caller when needed).
|
||||
bool PlayerToUnitDistSq(const char *token, float *outSq) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
if (!Unit::Position::ReadToken(token, unitPos) ||
|
||||
!Unit::Position::ReadToken("player", playerPos))
|
||||
float reach = 0.0f;
|
||||
if (!ReadPosAndReach(token, unitPos, &reach) ||
|
||||
!ReadPosAndReach("player", playerPos, &reach))
|
||||
return false;
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
@@ -72,14 +111,28 @@ int __fastcall Script_UnitInRange(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
float distSq = 0.0f;
|
||||
if (!PlayerToUnitDistSq(token, &distSq)) {
|
||||
float unitPos[3] = {};
|
||||
float playerPos[3] = {};
|
||||
float unitReach = 0.0f;
|
||||
float playerReach = 0.0f;
|
||||
if (!ReadPosAndReach(token, unitPos, &unitReach) ||
|
||||
!ReadPosAndReach("player", playerPos, &playerReach)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Game::Lua::PushBool(L, distSq <= kRangeYardsSq);
|
||||
const float dx = unitPos[0] - playerPos[0];
|
||||
const float dy = unitPos[1] - playerPos[1];
|
||||
const float dz = unitPos[2] - playerPos[2];
|
||||
const float distSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
// Effective range = base + both units' reach, mirroring the engine's
|
||||
// spell-range formula (FUN_006e3480). Without the reach terms a target
|
||||
// whose center sits just past 40 yards but whose body is within reach
|
||||
// reports out-of-range even though a 40-yard heal would land.
|
||||
const float effRange = kRangeYards + playerReach + unitReach;
|
||||
Game::Lua::PushBool(L, distSq <= effRange * effRange);
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 2;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user