mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e9e30567c | ||
|
|
8be78ad946 | ||
|
|
0dea869ab1 | ||
|
|
b4dffb75c0 | ||
|
|
9445fd6333 | ||
|
|
c8effdd6ec | ||
|
|
b3ac1c07f7 | ||
|
|
cfbc4f4e9c | ||
|
|
bb9579c398 | ||
|
|
5642644833 | ||
|
|
58e7e1a2c6 | ||
|
|
5339c97b1e | ||
|
|
7406ac5db1 | ||
|
|
1c263152e3 | ||
|
|
ab211962e5 | ||
|
|
33a23c563c |
@@ -8,5 +8,33 @@ if C_AddOns.DoesAddOnExist('pfUI') then
|
||||
unknownColor.colorStr = unknownColor:GenerateHexColor()
|
||||
return unknownColor
|
||||
end})
|
||||
|
||||
-- Compatibility with OLDER forks of pfUI. Our DLL adds the modern
|
||||
-- HookScript widget method; older pfUI forks branch on
|
||||
-- `if button.HookScript` and take a modern code path they were never
|
||||
-- written for on their pfActionBar buttons, which breaks them. While
|
||||
-- the actionbar module builds those buttons, temporarily wrap
|
||||
-- CreateFrame to shadow HookScript with a falsy field so those forks
|
||||
-- fall back to their vanilla SetScript path. The maintained "brues"
|
||||
-- fork handles HookScript itself, so it's detected via the TOC
|
||||
-- X-Website tag and left alone. The wrapper is removed as soon as the
|
||||
-- actionbar module finishes loading so it affects nothing else.
|
||||
local website = GetAddOnMetadata("pfUI", "X-Website")
|
||||
if not website or not string.find(GetAddOnMetadata("pfUI", "X-Website") or '', 'brues') then
|
||||
local _createFrame = CreateFrame
|
||||
CreateFrame = function(frameType, name, parent, template)
|
||||
if frameType == "Button" and string.find(name or "", "pfActionBar") then
|
||||
local frame = _createFrame(frameType, name, parent, template)
|
||||
frame.HookScript = false
|
||||
return frame
|
||||
end
|
||||
return _createFrame(frameType, name, parent, template)
|
||||
end
|
||||
hooksecurefunc(pfUI, 'LoadModule', function(frame, moduleName)
|
||||
if moduleName == "actionbar" then
|
||||
CreateFrame = _createFrame
|
||||
end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -11,12 +11,75 @@
|
||||
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
|
||||
-- the reader can craft themselves.
|
||||
|
||||
-- Frame geometry mirrors the real TradeSkillFrame (Blizzard_TradeSkillUI):
|
||||
-- 768x512, TW-TradeSkill quadrant art, 19 rows of 16px, list at (37,-98).
|
||||
local FRAME_W, FRAME_H = 768, 512;
|
||||
local NUM_ROWS = 19;
|
||||
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
|
||||
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
|
||||
-- vanilla 384x512 single-column art, so on that client we fall back to the
|
||||
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
|
||||
-- than stretch the wide art. LO holds every layout value that differs.
|
||||
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
|
||||
local FRAME_W = TURTLE and 768 or 384;
|
||||
local FRAME_H = 512;
|
||||
local NUM_ROWS = TURTLE and 19 or 8;
|
||||
local ROW_HEIGHT = 16;
|
||||
|
||||
local LO = TURTLE and {
|
||||
hit = { 2, 82, 2, 73 },
|
||||
quads = {
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -77, -8 },
|
||||
bar = { 596, 15, 76, -44 },
|
||||
barBdr = { 604, 22 },
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
-- detail scroll frame: { x, y, width, height }. Tall enough that the
|
||||
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
|
||||
detail = { 371, -110, 340, 290 },
|
||||
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
|
||||
} or {
|
||||
hit = { 0, 34, 0, 75 },
|
||||
quads = {
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -29, -8 },
|
||||
bar = { 268, 15, 73, -37 },
|
||||
barBdr = { 278, 22 },
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
|
||||
-- recipes scroll (clipping partial rows), like native.
|
||||
detail = { 20, -234, 297, 176 },
|
||||
exit = { "CENTER", "TOPLEFT", 305, -422 },
|
||||
sep = { 15, -221, 331 },
|
||||
};
|
||||
|
||||
-- Reagents render in a 2-column grid inside the detail scroll child (see
|
||||
-- the detail pane below). The detail is a real ScrollFrame like the
|
||||
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
|
||||
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
|
||||
-- of one reagent row.
|
||||
local REAGENT_ROW_HEIGHT = 43;
|
||||
|
||||
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
|
||||
-- list/detail scrolls use the ClassTrainer templates (they add the
|
||||
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
|
||||
-- doesn't want that groove, so it keeps the plain templates. Both detail
|
||||
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
|
||||
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
|
||||
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
|
||||
or "ClassTrainerListScrollFrameTemplate";
|
||||
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
|
||||
or "ClassTrainerDetailScrollFrameTemplate";
|
||||
|
||||
local PROFESSION_ICONS = {
|
||||
[171] = "Trade_Alchemy",
|
||||
[164] = "Trade_BlackSmithing",
|
||||
@@ -32,7 +95,7 @@ local PROFESSION_ICONS = {
|
||||
[356] = "Trade_Fishing",
|
||||
};
|
||||
|
||||
if TURTLE_WOW_VERSION then
|
||||
if TURTLE then
|
||||
PROFESSION_ICONS[142] = "Trade_Survival"
|
||||
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
|
||||
end
|
||||
@@ -188,45 +251,57 @@ local function CreateFrame_TradeSkillLink()
|
||||
f:SetPoint("CENTER", 0, 0);
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(2, 82, 2, 73);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
f:RegisterForDrag("LeftButton");
|
||||
f:SetScript("OnDragStart", function() this:StartMoving(); end);
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
|
||||
f:SetFrameStrata("DIALOG");
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
f.portrait = portrait;
|
||||
|
||||
local function Quad(file, w, h, point)
|
||||
for i = 1, table.getn(LO.quads) do
|
||||
local q = LO.quads[i];
|
||||
local tx = f:CreateTexture(nil, "BORDER");
|
||||
tx:SetTexture("Interface\\TradeSkillFrame\\" .. file);
|
||||
tx:SetSize(w, h);
|
||||
tx:SetPoint(point);
|
||||
tx:SetTexture(q[1]);
|
||||
tx:SetSize(q[2], q[3]);
|
||||
tx:SetPoint(q[4]);
|
||||
end
|
||||
|
||||
-- Horizontal divider between the recipe list and the detail pane.
|
||||
-- Stock only (Turtle's HD frame art already has its own).
|
||||
if LO.sep then
|
||||
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
|
||||
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
|
||||
-- bottom half empty). So sample the top quarter, stretched to width —
|
||||
-- the same region the native TradeSkillFrame uses for this divider.
|
||||
local bar = f:CreateTexture(nil, "ARTWORK");
|
||||
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
|
||||
bar:SetTexCoord(0, 1, 0, 0.25);
|
||||
bar:SetSize(LO.sep[3], 16);
|
||||
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
|
||||
end
|
||||
Quad("TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT");
|
||||
Quad("TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT");
|
||||
Quad("TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT");
|
||||
Quad("TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT");
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
|
||||
close:SetPoint("TOPRIGHT", -77, -8);
|
||||
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
|
||||
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint("CENTER", f, "BOTTOMRIGHT", -128, 92);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT or "Exit");
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
bar:SetSize(596, 15);
|
||||
bar:SetPoint("TOPLEFT", 76, -44);
|
||||
bar:SetSize(LO.bar[1], LO.bar[2]);
|
||||
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
|
||||
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
|
||||
bar:SetMinMaxValues(0, 1);
|
||||
@@ -236,7 +311,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
|
||||
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
|
||||
local border = CreateFrame("Frame", nil, bar);
|
||||
border:SetSize(604, 22);
|
||||
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
|
||||
border:SetPoint("LEFT", -5, 1);
|
||||
border:SetBackdrop({
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
|
||||
@@ -253,19 +328,19 @@ local function CreateFrame_TradeSkillLink()
|
||||
f.barRank = barRank;
|
||||
|
||||
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
summary:SetPoint("TOPRIGHT", -110, -70);
|
||||
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
|
||||
f.summary = summary;
|
||||
|
||||
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
|
||||
"UIDropDownMenuTemplate");
|
||||
dropdown:SetPoint("TOPLEFT", 40, -60);
|
||||
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
|
||||
f.dropdown = dropdown;
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, "FauxScrollFrameTemplate");
|
||||
scroll:SetSize(296, 330);
|
||||
scroll:SetPoint("TOPLEFT", 37, -98);
|
||||
f, LIST_SCROLL_TEMPLATE);
|
||||
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
|
||||
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
|
||||
end)
|
||||
@@ -309,17 +384,54 @@ local function CreateFrame_TradeSkillLink()
|
||||
f.rows[i] = row;
|
||||
end
|
||||
|
||||
local NUM_REAGENTS = 8;
|
||||
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
|
||||
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
|
||||
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
|
||||
-- auto-hides when the content fits (scrollBarHideable).
|
||||
local detailScroll = CreateFrame("ScrollFrame",
|
||||
"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.
|
||||
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);
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, f);
|
||||
prodIcon:SetSize(37, 37); -- TradeSkillSkillIcon is 37x37
|
||||
prodIcon:SetPoint("TOPLEFT", 379, -113);
|
||||
local detailChild = CreateFrame("Frame",
|
||||
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
|
||||
detailChild:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetScrollChild(detailChild);
|
||||
f.detailChild = detailChild;
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, detailChild);
|
||||
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
|
||||
prodIcon:SetPoint("TOPLEFT", 8, -3);
|
||||
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local hdrLeft = prodIcon:CreateTexture(nil, "BACKGROUND");
|
||||
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
|
||||
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
|
||||
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
|
||||
local hdrRight = prodIcon:CreateTexture(nil, "BACKGROUND");
|
||||
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
|
||||
hdrRight:SetSize(64, 64);
|
||||
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
|
||||
@@ -328,44 +440,65 @@ local function CreateFrame_TradeSkillLink()
|
||||
prodIcon.count = prodCount;
|
||||
f.prodIcon = prodIcon;
|
||||
|
||||
local prodName = prodIcon:CreateFontString(nil, "ARTWORK", "GameFontNormal");
|
||||
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
|
||||
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
|
||||
prodName:SetWidth(240);
|
||||
prodName:SetWidth(LO.detail[3] - 60);
|
||||
prodName:SetJustifyH("LEFT");
|
||||
prodName:SetJustifyV("MIDDLE");
|
||||
f.prodName = prodName;
|
||||
|
||||
local prodDesc = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
|
||||
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
|
||||
prodDesc:SetWidth(280);
|
||||
prodDesc:SetWidth(LO.detail[3] - 20);
|
||||
prodDesc:SetJustifyH("LEFT");
|
||||
prodDesc:SetJustifyV("TOP");
|
||||
f.prodDesc = prodDesc;
|
||||
|
||||
local reagentLabel = f:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
|
||||
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
|
||||
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
|
||||
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
|
||||
f.reagentLabel = reagentLabel;
|
||||
|
||||
prodIcon:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
if this.itemLink then
|
||||
GameTooltip:SetHyperlink(this.itemLink);
|
||||
elseif this.spellID then
|
||||
GameTooltip:SetSpellByID(this.spellID);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
|
||||
|
||||
-- Reagent buttons live in the scroll child (2 per row), built on
|
||||
-- demand so any reagent count is handled and they scroll/clip with
|
||||
-- the child.
|
||||
f.reagents = {};
|
||||
for i = 1, NUM_REAGENTS do
|
||||
local function AcquireReagent(i)
|
||||
if f.reagents[i] then
|
||||
return f.reagents[i];
|
||||
end
|
||||
local rowN = math.floor((i - 1) / 2);
|
||||
local col = (i - 1) - rowN * 2;
|
||||
local btn = CreateFrame("Button", nil, f);
|
||||
local btn = CreateFrame("Button", nil, detailChild);
|
||||
btn:SetSize(39, 39);
|
||||
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
|
||||
col * 147 - 5, -3 - rowN * 43);
|
||||
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
|
||||
-- Extend the hit area rightward over the name plate so the tooltip
|
||||
-- fires anywhere on the element, not just the icon. Stop short of the
|
||||
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
|
||||
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
|
||||
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local plate = btn:CreateTexture(nil, "BACKGROUND");
|
||||
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
|
||||
plate:SetSize(128, 64);
|
||||
plate:SetSize(114, 64);
|
||||
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
|
||||
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
|
||||
btn.count = cnt;
|
||||
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
|
||||
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
|
||||
nm:SetSize(90, 36);
|
||||
nm:SetSize(80, 36);
|
||||
nm:SetJustifyH("LEFT");
|
||||
nm:SetJustifyV("MIDDLE");
|
||||
btn.nameText = nm;
|
||||
@@ -377,8 +510,22 @@ local function CreateFrame_TradeSkillLink()
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
btn:Hide();
|
||||
f.reagents[i] = btn;
|
||||
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);
|
||||
local base = "ClassicAPITradeSkillLinkDetailScroll";
|
||||
local sb = getglobal(base .. "ScrollBar");
|
||||
if sb then sb:SetShown(need) end
|
||||
local top = getglobal(base .. "Top");
|
||||
if top then top:SetShown(need) end
|
||||
local bottom = getglobal(base .. "Bottom");
|
||||
if bottom then bottom:SetShown(need) end
|
||||
end
|
||||
|
||||
function f:RenderDetail(entry)
|
||||
@@ -405,11 +552,9 @@ local function CreateFrame_TradeSkillLink()
|
||||
prodText = entry.name;
|
||||
prodTex = C_Spell.GetSpellTexture(entry.spellID);
|
||||
end
|
||||
if created > 0 then
|
||||
self.prodDesc:SetText("");
|
||||
else
|
||||
self.prodDesc:SetText(C_Spell.GetSpellDescription(entry.spellID) or "");
|
||||
end
|
||||
local descText = (created > 0) and ""
|
||||
or (C_Spell.GetSpellDescription(entry.spellID) or "");
|
||||
self.prodDesc:SetText(descText);
|
||||
self.prodIcon:SetNormalTexture(prodTex or
|
||||
"Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
self.prodName:SetText(prodText or "");
|
||||
@@ -420,50 +565,66 @@ local function CreateFrame_TradeSkillLink()
|
||||
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
|
||||
self.prodIcon:Show();
|
||||
|
||||
-- Fill every reagent slot; the scroll child clips/scrolls the
|
||||
-- overflow. Buttons past the count are hidden.
|
||||
local reagents = entry.reagents or {};
|
||||
for i = 1, NUM_REAGENTS do
|
||||
local btn = self.reagents[i];
|
||||
local total = table.getn(reagents);
|
||||
for i = 1, total do
|
||||
local btn = AcquireReagent(i);
|
||||
local r = reagents[i];
|
||||
if r then
|
||||
local name, tex, link = itemInfo(r.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;
|
||||
if have >= need then
|
||||
btn:GetNormalTexture():SetVertexColor(1, 1, 1);
|
||||
btn.nameText:SetTextColor(HIGHLIGHT_FONT_COLOR:GetRGB());
|
||||
else
|
||||
btn:GetNormalTexture():SetVertexColor(0.5, 0.5, 0.5);
|
||||
btn.nameText:SetTextColor(GRAY_FONT_COLOR:GetRGB());
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
btn:Show();
|
||||
local name, tex, link = itemInfo(r.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;
|
||||
if have >= need then
|
||||
btn:GetNormalTexture():SetVertexColor(1, 1, 1);
|
||||
btn.nameText:SetTextColor(HIGHLIGHT_FONT_COLOR:GetRGB());
|
||||
else
|
||||
btn.itemLink = nil;
|
||||
btn:Hide();
|
||||
btn:GetNormalTexture():SetVertexColor(0.5, 0.5, 0.5);
|
||||
btn.nameText:SetTextColor(GRAY_FONT_COLOR:GetRGB());
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
btn:Show();
|
||||
end
|
||||
for i = total + 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
end
|
||||
|
||||
prodIcon:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
if this.itemLink then
|
||||
GameTooltip:SetHyperlink(this.itemLink);
|
||||
elseif this.spellID then
|
||||
GameTooltip:SetSpellByID(this.spellID);
|
||||
-- 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 sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
self:UpdateDetailScrollbar();
|
||||
RunNextFrame(function()
|
||||
if self:IsShown() then
|
||||
self:UpdateDetailScrollbar();
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function f:SelectRecipe(entry)
|
||||
self.selectedSpellID = entry.spellID;
|
||||
self.selectedEntry = entry;
|
||||
self:RenderDetail(entry);
|
||||
self:RenderDetail(entry); -- also resets the detail scroll to top
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
@@ -473,9 +634,14 @@ local function CreateFrame_TradeSkillLink()
|
||||
self.prodIcon:Hide();
|
||||
self.prodName:SetText("");
|
||||
self.prodDesc:SetText("");
|
||||
for i = 1, NUM_REAGENTS do
|
||||
for i = 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
self.detailChild:SetHeight(LO.detail[4]);
|
||||
self.detailNeedScroll = false;
|
||||
self:UpdateDetailScrollbar();
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
end
|
||||
|
||||
function f:Refresh()
|
||||
@@ -620,3 +786,70 @@ function SetItemRef(link, text, button, chatFrame)
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
local btnName = parentName .. "LinkButton";
|
||||
if _G[btnName] or not parent then
|
||||
return;
|
||||
end
|
||||
local title = _G[parentName .. "TitleText"];
|
||||
local btn = CreateFrame("Button", btnName, parent);
|
||||
btn:SetSize(16, 16);
|
||||
|
||||
local function reposition()
|
||||
if not title then
|
||||
return;
|
||||
end
|
||||
btn:ClearAllPoints();
|
||||
if title:GetJustifyH() == "LEFT" then
|
||||
btn:SetPoint("LEFT", title, "LEFT", title:GetStringWidth() + 4, 0);
|
||||
else
|
||||
btn:SetPoint("LEFT", title, "CENTER", title:GetStringWidth() / 2 + 4, 0);
|
||||
end
|
||||
end
|
||||
reposition()
|
||||
btn:SetScript("OnShow", function() RunNextFrame(reposition) end)
|
||||
hooksecurefunc(title, "SetText", function() RunNextFrame(reposition) end)
|
||||
|
||||
local icon = "Interface\\GossipFrame\\PetitionGossipIcon";
|
||||
btn:SetNormalTexture(icon);
|
||||
btn:SetHighlightTexture(icon, "ADD");
|
||||
|
||||
btn:SetScript("OnClick", function()
|
||||
local link = getLink();
|
||||
if not link then
|
||||
return;
|
||||
end
|
||||
if not ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Show();
|
||||
end
|
||||
ChatFrameEditBox:Insert(link);
|
||||
end);
|
||||
btn:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT");
|
||||
GameTooltip:SetText(LINK_TRADESKILL_TOOLTIP, nil, nil, nil, nil, 1);
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
@@ -24,3 +24,4 @@ SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
@@ -18,7 +18,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [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` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
|
||||
@@ -33,7 +33,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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](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` |
|
||||
| [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` |
|
||||
@@ -43,6 +43,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [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.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.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `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` |
|
||||
| [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` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount` |
|
||||
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
@@ -121,6 +122,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` |
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
|
||||
|
||||
+115
-4
@@ -20,6 +20,7 @@ build instructions.
|
||||
- [`C_AddOns.IsAddOnLoaded(indexOrName)`](#c_addonsisaddonloadedindexorname)
|
||||
- [`C_AddOns.GetAddOnSecurity(indexOrName)`](#c_addonsgetaddonsecurityindexorname)
|
||||
- [`C_AddOns.DoesAddOnExist(indexOrName)`](#c_addonsdoesaddonexistindexorname)
|
||||
- [`C_AddOns.GetAddOnOptionalDependencies(indexOrName)`](#c_addonsgetaddonoptionaldependenciesindexorname)
|
||||
|
||||
- [CharacterList](#characterlist)
|
||||
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
|
||||
@@ -99,6 +100,7 @@ build instructions.
|
||||
- [`EQUIPMENT_SWAP_PENDING` event](#equipment_swap_pending-event)
|
||||
- [`EQUIPMENT_SWAP_FINISHED` event](#equipment_swap_finished-event)
|
||||
- [`FACTION_STANDING_CHANGED` event](#faction_standing_changed-event)
|
||||
- [`LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
|
||||
- [`LOOT_SCAN_COMPLETED` event](#loot_scan_completed-event)
|
||||
- [`LOSS_OF_CONTROL_ADDED` / `LOSS_OF_CONTROL_UPDATE` events](#loss_of_control_added--loss_of_control_update-events)
|
||||
- [`MODIFIER_STATE_CHANGED` event](#modifier_state_changed-event)
|
||||
@@ -137,6 +139,7 @@ build instructions.
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
|
||||
- [FriendList](#friendlist)
|
||||
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
|
||||
@@ -254,6 +257,11 @@ build instructions.
|
||||
- [`C_Loot.IsScanInProgress()`](#c_lootisscaninprogress)
|
||||
- [`C_Loot.GetLastScanResults()`](#c_lootgetlastscanresults)
|
||||
|
||||
- [LootHistory](#loothistory)
|
||||
- [`C_LootHistory.GetNumItems()`](#c_loothistorygetnumitems)
|
||||
- [`C_LootHistory.GetItem(itemIndex)`](#c_loothistorygetitemitemindex)
|
||||
- [`C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)`](#c_loothistorygetplayerinfoitemindex-playerindex)
|
||||
|
||||
- [LossOfControl](#lossofcontrol)
|
||||
- [`C_LossOfControl.GetActiveLossOfControlDataCount()`](#c_lossofcontrolgetactivelossofcontroldatacount)
|
||||
- [`C_LossOfControl.GetActiveLossOfControlData(index)`](#c_lossofcontrolgetactivelossofcontroldataindex)
|
||||
@@ -604,11 +612,12 @@ the comment in [src/action/Info.cpp](../src/action/Info.cpp).
|
||||
|
||||
## AddOns
|
||||
|
||||
Six modern `C_AddOns.*` getters that splat the legacy
|
||||
Modern `C_AddOns.*` getters that splat the legacy
|
||||
`GetAddOnInfo(arg)` 7-tuple `(name, title, notes, enabled,
|
||||
loadable, reason, security)` into single-field accessors. Most
|
||||
bypass `GetAddOnInfo` entirely and call the engine's per-field
|
||||
helpers directly.
|
||||
loadable, reason, security)` into single-field accessors, plus a
|
||||
getter for the addon's optional dependencies. Most bypass
|
||||
`GetAddOnInfo` entirely and call the engine's per-field helpers
|
||||
directly.
|
||||
|
||||
All accept either a 1-based index (`1..GetNumAddOns()`) or an
|
||||
addon directory name string.
|
||||
@@ -725,6 +734,25 @@ C_AddOns.DoesAddOnExist("DebugTools") -- true
|
||||
C_AddOns.DoesAddOnExist("garbage") -- false
|
||||
```
|
||||
|
||||
### `C_AddOns.GetAddOnOptionalDependencies(indexOrName)`
|
||||
|
||||
Returns the addon's `## OptionalDeps:` names as multiple return
|
||||
values, in declared order — the counterpart to the stock
|
||||
`GetAddOnDependencies` (required deps), which vanilla exposes but
|
||||
never gave an optional-deps equivalent. An addon with no
|
||||
`## OptionalDeps:` field returns nothing.
|
||||
|
||||
Arg and error handling match stock `GetAddOnDependencies`: a
|
||||
numeric index out of range (or a non-string/non-number argument)
|
||||
raises the usage error; an unknown *name* string returns nothing
|
||||
without erroring.
|
||||
|
||||
```lua
|
||||
-- ## OptionalDeps: Atlas, pfQuest
|
||||
C_AddOns.GetAddOnOptionalDependencies("MyAddon") -- "Atlas", "pfQuest"
|
||||
C_AddOns.GetAddOnOptionalDependencies("Atlas") -- (nothing — no OptionalDeps)
|
||||
```
|
||||
|
||||
## CharacterList
|
||||
|
||||
### `GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only
|
||||
@@ -2294,6 +2322,22 @@ forwarding, which [`C_Reputation.GetLastStandingChange`](#c_reputationgetlaststa
|
||||
exposes — so addons can read the structured payload from inside the
|
||||
chat event without re-parsing the localized string.
|
||||
|
||||
### `LOOT_HISTORY_ROLL_CHANGED` / `LOOT_HISTORY_ROLL_COMPLETE` / `LOOT_HISTORY_FULL_UPDATE` events
|
||||
|
||||
Fire as group-loot rolls progress, so a loot-history UI can update without
|
||||
polling. Register like any engine event
|
||||
(`frame:RegisterEvent("LOOT_HISTORY_ROLL_COMPLETE")`), then read the item via
|
||||
[`C_LootHistory.GetItem`](#c_loothistorygetitemitemindex) /
|
||||
[`C_LootHistory.GetPlayerInfo`](#c_loothistorygetplayerinfoitemindex-playerindex).
|
||||
|
||||
| Event | Args | When |
|
||||
|---|---|---|
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex`, `playerIndex` (numbers) | A player rolled or passed on an item. |
|
||||
| `LOOT_HISTORY_ROLL_COMPLETE` | `itemIndex` (number) | The item was decided — won or all-passed. |
|
||||
| `LOOT_HISTORY_FULL_UPDATE` | — | Reserved for API / `IsEventValid` completeness (the display frame fires it on open); the DLL doesn't dispatch it. |
|
||||
|
||||
See the [LootHistory](#loothistory) section for the reconstruction and read API.
|
||||
|
||||
### `LOOT_SCAN_COMPLETED` event
|
||||
|
||||
Fires (no payload) once a [`C_Loot.ScanNearbyLoot()`](#c_lootscannearbyloot)
|
||||
@@ -3143,6 +3187,22 @@ button:HookScript("OnEnter", function(self)
|
||||
end)
|
||||
```
|
||||
|
||||
### `frame:IsEventRegistered(event)`
|
||||
|
||||
Vanilla ships `RegisterEvent` / `UnregisterEvent` but not the query.
|
||||
Returns `true` if the frame is currently registered for `event`,
|
||||
`false` otherwise (including for an unknown event name). Reuses the
|
||||
engine's own subscriber-chain membership check — the same walk
|
||||
`RegisterEvent` performs before appending — so the answer is exactly
|
||||
what the event dispatcher sees. Works on any frame.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("PLAYER_LOGIN")
|
||||
f:IsEventRegistered("PLAYER_LOGIN") -- true
|
||||
f:IsEventRegistered("PLAYER_LOGOUT") -- false
|
||||
```
|
||||
|
||||
## FriendList
|
||||
|
||||
### `C_FriendList.SendWhoQueryByName(name)`
|
||||
@@ -6108,6 +6168,57 @@ tooltip display, extract the payload form via
|
||||
requires the literal `"item:"` prefix and rejects full `|cff...|Hitem...|h`
|
||||
input.
|
||||
|
||||
## LootHistory
|
||||
|
||||
Backport of the MoP `C_LootHistory` namespace — group-loot roll history.
|
||||
Vanilla 1.12 has no loot-history store, but it does receive the live
|
||||
group-loot roll traffic (`SMSG_LOOT_ROLL` per player, `SMSG_LOOT_ROLL_WON`,
|
||||
`SMSG_LOOT_ALL_PASSED`) — which the stock UI only turns into chat lines. This
|
||||
reconstructs the history client-side: packet co-hooks accumulate a ring of
|
||||
rolled items with per-player Need/Greed/Pass results and the winner, exposed
|
||||
through the namespace. Rollers' **name and class come from the engine
|
||||
NameCache** captured at roll time, so they resolve even for a player who has
|
||||
left the group by the time their roll arrives.
|
||||
|
||||
History is in-memory (last 128 rolled items) and resets on `/reload` or logout.
|
||||
The MoP master-loot management functions (`SetExpiration` / `GiveMasterLoot` /
|
||||
`CanMasterLoot`) are intentionally omitted — they drive server support 1.12
|
||||
doesn't have.
|
||||
|
||||
Progress is signalled by the `LOOT_HISTORY_ROLL_CHANGED` /
|
||||
`LOOT_HISTORY_ROLL_COMPLETE` events — see the
|
||||
[Events](#loot_history_roll_changed--loot_history_roll_complete--loot_history_full_update-events)
|
||||
section.
|
||||
|
||||
### `C_LootHistory.GetNumItems()`
|
||||
|
||||
Returns the number of rolled items currently in the history.
|
||||
|
||||
### `C_LootHistory.GetItem(itemIndex)`
|
||||
|
||||
`1`-based. Returns `itemLink, numPlayers, isDone, winnerName`:
|
||||
- `itemLink` — `item:<id>:0:<suffix>:<unique>` (valid for `GetItemInfo` /
|
||||
`GameTooltip:SetHyperlink`), carrying the roll's random suffix so
|
||||
"of the X" items keep their affix.
|
||||
- `numPlayers` — how many players have rolled/passed so far.
|
||||
- `isDone` — `true` once the item is decided (won or all-passed).
|
||||
- `winnerName` — the winner's name, or `nil` if undecided / all passed.
|
||||
|
||||
Returns nothing for an out-of-range index.
|
||||
|
||||
### `C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)`
|
||||
|
||||
Both `1`-based. Returns `name, class, rollType, roll, isWinner, isMe`:
|
||||
- `name` — the roller's name (from the NameCache).
|
||||
- `class` — the class token (`"WARRIOR"`, `"MAGE"`, …) for
|
||||
`RAID_CLASS_COLORS`, or `nil` if unknown.
|
||||
- `rollType` — `0` = pass, `1` = need, `2` = greed.
|
||||
- `roll` — the `1`–`100` roll, or `0` if they passed.
|
||||
- `isWinner` — `true` if this player won the item.
|
||||
- `isMe` — `true` if this roller is the local player.
|
||||
|
||||
Returns nothing for an out-of-range item or player index.
|
||||
|
||||
## LossOfControl
|
||||
|
||||
Backports `C_LossOfControl` — the active crowd-control / interrupt effects on
|
||||
|
||||
+90
-17
@@ -1218,6 +1218,42 @@ enum Offsets {
|
||||
// aura that's still inside its computed duration window.
|
||||
FUN_ON_AURA_REMOVED = 0x00612320,
|
||||
|
||||
// Group-loot roll packet handlers (the inner display functions in
|
||||
// LootRoll.cpp). Each is `__fastcall`; the deserialized 0x48-byte
|
||||
// PendingLootRoll message struct is fully populated at entry and freed
|
||||
// at exit, so read its fields BEFORE calling the original. The register
|
||||
// the message arrives in differs per handler (matched in
|
||||
// loot/History.cpp): ROLL passes it in ECX, WON / ALL_PASSED in EDX.
|
||||
// Backs the C_LootHistory backport (loot/History.cpp) — 1.12 has no
|
||||
// loot-history store, so we reconstruct one from these packets.
|
||||
FUN_LOOT_ROLL_HANDLER = 0x0061C0B0, // SMSG_LOOT_ROLL (msg=ECX)
|
||||
FUN_LOOT_ROLL_WON_HANDLER = 0x0061B9E0, // SMSG_LOOT_ROLL_WON (msg=EDX)
|
||||
FUN_LOOT_ALL_PASSED_HANDLER = 0x0061B640, // SMSG_LOOT_ALL_PASSED (msg=EDX)
|
||||
|
||||
// SMSG_LOOT_START_ROLL (opcode 0x2A1) handler — `__fastcall(ECX = packet)`,
|
||||
// RET 0. The result packets (ROLL/WON/PASSED) carry 0 for the item's
|
||||
// random suffix/seed; only START_ROLL does. It deserializes a fresh
|
||||
// PendingLootRoll node (same 0x20/0x24 field offsets as the message) and
|
||||
// publishes it as the store head at VAR_LOOTROLL_STORE_HEAD, so we read
|
||||
// the suffix straight off that node right after the original runs.
|
||||
FUN_LOOT_START_ROLL_HANDLER = 0x0061B310,
|
||||
VAR_LOOTROLL_STORE_HEAD = 0x00C4DC00, // PendingLootRoll* — newest node
|
||||
|
||||
// PendingLootRoll message field offsets, derived from the SMSG_LOOT_ROLL
|
||||
// deserializer FUN_0061BFA0 (reads GUID/u32/byte fields off the packet).
|
||||
OFF_LOOTROLL_SOURCE_GUID = 0x10, // u64 looted-object GUID (roll identity)
|
||||
OFF_LOOTROLL_SLOT = 0x18, // u32 loot slot
|
||||
OFF_LOOTROLL_ITEM_ID = 0x1C, // u32 itemID
|
||||
// Random-enchant fields for the item link `item:id:enchant:suffix:unique`.
|
||||
// +0x20 is the suffix/random-property ID the engine's item-name builder
|
||||
// FUN_005D8B00 uses (indexes the random-properties DBC for the "of the X"
|
||||
// name); +0x24 is the paired seed/unique value.
|
||||
OFF_LOOTROLL_ITEM_SUFFIX = 0x20, // i32 random suffix / property ID
|
||||
OFF_LOOTROLL_ITEM_SEED = 0x24, // u32 random seed (link unique field)
|
||||
OFF_LOOTROLL_ROLLER_GUID = 0x30, // u64 roller / winner GUID
|
||||
OFF_LOOTROLL_ROLL_NUM = 0x38, // u8 roll (1..100); high bit = didn't roll
|
||||
OFF_LOOTROLL_ROLL_TYPE = 0x40, // u32 rollType: 0 = need, 2 = greed
|
||||
|
||||
// CGPlayer-side sub-struct, allocated for any player-controlled
|
||||
// unit (local self, target, party, raid, inspect targets — all of
|
||||
// them). Holds player-specific data that's *not* in the broadcast
|
||||
@@ -2279,7 +2315,18 @@ enum Offsets {
|
||||
// Verified via the `IsAssistingRitual` development session by
|
||||
// observing `0xEE00 → 0xEE05` on `/sit` over a chair (the medium-
|
||||
// chair sit value 5).
|
||||
//
|
||||
// Byte 3 (the AnimTier / misc-flags byte, mask 0x02000000 on the
|
||||
// u32) is the **stealth / sneak flag**: set while the unit is in
|
||||
// the sneaking anim tier (Rogue Stealth, Druid Prowl, Shadowmeld),
|
||||
// which is what drives the client's semi-transparent render of a
|
||||
// stealthed unit. Verified via `_classicapi_DiffAll`: a Prowl
|
||||
// toggle from a Cat-Form baseline flips byte 3 `0x00 → 0x02` while
|
||||
// every other descriptor change is aura-array slot metadata — so
|
||||
// this is the only standalone broadcast bit that means "stealthed".
|
||||
// Read by `IsStealthed()`.
|
||||
OFF_UNIT_FIELD_BYTES_1 = 0x210,
|
||||
UNIT_BYTES_1_STEALTH_FLAG = 0x02000000,
|
||||
|
||||
// `__fastcall(void)` engine helper that refreshes the bonus
|
||||
// action bar from the local player's current shapeshift form
|
||||
@@ -3452,6 +3499,15 @@ enum Offsets {
|
||||
OFF_EVENT_ENTRY_NAME = 0x00,
|
||||
OFF_EVENT_ENTRY_HEAD = 0x0C,
|
||||
|
||||
// Subscriber-chain node layout — the linked list hanging off each
|
||||
// entry's +0x0C head, derived from Frame::RegisterEvent
|
||||
// (FUN_00702140): each node is `{ ?, next@+0x04, frame@+0x08 }`, where
|
||||
// `frame` is the CFrameScriptObject* subscriber. A node value with its
|
||||
// low bit set (or null) is the end/empty sentinel. Walked by
|
||||
// Frame:IsEventRegistered to test membership.
|
||||
OFF_EVENT_NODE_NEXT = 0x04,
|
||||
OFF_EVENT_NODE_FRAME = 0x08,
|
||||
|
||||
// `Frame::RegisterEvent` — the C++ helper called by the Lua
|
||||
// `frame:RegisterEvent(eventName)` method (`Script_RegisterEvent` at
|
||||
// `0x00774A40`). `__thiscall` with `(this=frame, eventName)`. Walks
|
||||
@@ -3814,6 +3870,30 @@ enum Offsets {
|
||||
FUN_ADDON_GET_TITLE_BY_NAME = 0x0051DF20, // (name) → title string or NULL
|
||||
FUN_ADDON_GET_NOTES_BY_NAME = 0x0051E050, // (name) → notes string or NULL
|
||||
|
||||
// `__fastcall(const char *nameOrEntry) → AddOnEntry*+0x48 or NULL`.
|
||||
// Hash-table lookup by name (accepts an entry pointer too — its
|
||||
// first 12 bytes are the inline name), returning a pointer to the
|
||||
// matched record's RequiredDeps descriptor (`record + 0x48`), or
|
||||
// NULL on a miss. Stock `Script_GetAddOnDependencies` (`0x0048E5E0`)
|
||||
// calls this then reads count@+4 / data@+8 off the return to walk
|
||||
// the required deps. We reuse it only to recover the record base for
|
||||
// string input: `record = resolve(name) - OFF_ADDON_REQDEPS_DESC`.
|
||||
FUN_ADDON_RESOLVE_REQ_DEPS = 0x0051E350,
|
||||
|
||||
// AddOnEntry dependency-array descriptors are `{cap, count, data,
|
||||
// quantum}` (each field 4 bytes) laid out 0x10 apart, parsed by the
|
||||
// TOC parser `FUN_0051c9b0`:
|
||||
// OptionalDeps desc@+0x38 (count@+0x3C, data@+0x40)
|
||||
// RequiredDeps desc@+0x48 (count@+0x4C, data@+0x50) ← stock walks
|
||||
// LoadWith desc@+0x58
|
||||
// Optional offsets verified against the parser's append site at
|
||||
// `0x0051CDBA` (`mov esi,[rec+0x3C]` count, `lea edi,[rec+0x38]`);
|
||||
// required desc@+0x48 confirmed by `FUN_ADDON_RESOLVE_REQ_DEPS`'s
|
||||
// `lea eax,[rec+0x48]` return.
|
||||
OFF_ADDON_REQDEPS_DESC = 0x48,
|
||||
OFF_ADDON_OPTIONALDEPS_COUNT = 0x3C,
|
||||
OFF_ADDON_OPTIONALDEPS_ARRAY = 0x40,
|
||||
|
||||
// Per-field accessors `Script_GetAddOnInfo` calls internally for
|
||||
// returns 5 (loadable), 6 (reason string), 7 (security category).
|
||||
// We dispatch to these directly so the wrappers don't need to
|
||||
@@ -3880,23 +3960,15 @@ enum Offsets {
|
||||
VAR_ADDON_ARRAY = 0x00BE1B94,
|
||||
VAR_ADDON_COUNT = 0x00BE1B90,
|
||||
|
||||
// Player visibility/state byte field, broadcast in UpdateFields.
|
||||
// Found empirically via _classicapi_DescDump (see src/debug/).
|
||||
// Treated as a u32 but the meaningful bits all live in byte 0:
|
||||
// 0x02 — set when the player has any visibility/form-modifying
|
||||
// state active (stealth, mount, possibly shapeshift —
|
||||
// set by stealth and mount in our test data).
|
||||
// 0x08, 0x10 — set together when mounted (alongside 0x02).
|
||||
//
|
||||
// Ambiguity: bit 0x02 alone doesn't distinguish stealth from
|
||||
// other visibility-modifying states. Stealth detection AND-gates
|
||||
// with `mountDisplayID == 0` to exclude mount; if shapeshift
|
||||
// also sets bit 0x02 we'd false-positive on druids in form
|
||||
// (untested). The robust path is to walk the player's aura
|
||||
// list looking for the actual stealth/prowl spell — left as
|
||||
// future work if false-positives turn up.
|
||||
OFF_PLAYER_FIELD_VIS_BYTES = 0x17C,
|
||||
PLAYER_VIS_BIT_STEALTH = 0x02,
|
||||
// NOTE: an earlier revision defined OFF_PLAYER_FIELD_VIS_BYTES /
|
||||
// PLAYER_VIS_BIT_STEALTH here at 0x17C, believing bit 0x02 was a
|
||||
// "stealth bit". That was wrong: 0x17C is UNIT_FIELD_AURALEVELS
|
||||
// (per-slot caster-level bytes; see OFF_UNIT_FIELD_AURALEVELS above),
|
||||
// and the old read only appeared to work by coincidence (a caster-
|
||||
// level byte that happened to have bit 0x02 set on the test char;
|
||||
// at level 60 = 0x3C it doesn't, so it read false). The real stealth
|
||||
// flag is UNIT_FIELD_BYTES_1 byte 3 (UNIT_BYTES_1_STEALTH_FLAG); see
|
||||
// that field above. IsStealthed reads it.
|
||||
|
||||
// Mount display ID — the creature display ID the engine renders
|
||||
// under the mounted player. Zero when not mounted, non-zero
|
||||
@@ -4650,6 +4722,7 @@ enum Offsets {
|
||||
// reads `spellRec[+0x14]` as the spell's mechanic when the per-
|
||||
// effect EffectMiscValue is 0. Read by C_Spell.GetSpellMechanicByID.
|
||||
OFF_SPELL_RECORD_MECHANIC = 0x14,
|
||||
SPELL_AURA_MOD_STEALTH = 16,
|
||||
SPELL_AURA_MOD_SHAPESHIFT = 36,
|
||||
SPELL_AURA_MOUNTED = 78,
|
||||
|
||||
|
||||
@@ -41,6 +41,11 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck,
|
||||
int *outStatus, int *outDepHandle,
|
||||
const char *accountName);
|
||||
using GetAccountName_t = const char *(*)();
|
||||
// Hash-lookup by name → the matched record's RequiredDeps descriptor
|
||||
// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its
|
||||
// first bytes are the inline name). We use it only to recover the
|
||||
// record base for string input; see `Script_GetAddOnOptionalDependencies`.
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
|
||||
// Resolves the user's arg to a `const uint8_t *` that the engine's
|
||||
// per-field accessors can consume. They all read the addon name as
|
||||
@@ -295,6 +300,63 @@ int __fastcall Script_DoesAddOnExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's
|
||||
// `## OptionalDeps:` names as multiple return values, in declared order.
|
||||
// The exact shape of the stock `GetAddOnDependencies`, but reading the
|
||||
// *optional* dep array instead of the required one. No optional deps →
|
||||
// returns nothing.
|
||||
//
|
||||
// Vanilla parses `## OptionalDeps:` in the TOC parser `FUN_0051c9b0`
|
||||
// into a `{cap, count, data, quantum}` name-array descriptor at
|
||||
// `record+0x38` (count@+0x3C, data@+0x40), but ships no getter for it.
|
||||
//
|
||||
// Arg/error handling mirrors stock `Script_GetAddOnDependencies`
|
||||
// (`0x0048E5E0`): numeric out-of-range or a non-string/non-number arg
|
||||
// raises the usage error; an unknown *name* string returns nothing
|
||||
// (no error), matching the engine's own not-found path.
|
||||
//
|
||||
// Resolution mirrors stock `Script_GetAddOnDependencies` exactly: the
|
||||
// arg is first turned into a name-usable pointer (`ResolveAddOnName` —
|
||||
// numeric via `FUN_ADDON_GET_BY_INDEX`, string is the raw string), then
|
||||
// routed through the by-name hash lookup `FUN_ADDON_RESOLVE_REQ_DEPS`
|
||||
// to reach the deps-bearing record. `FUN_ADDON_GET_BY_INDEX` returns a
|
||||
// *name pointer*, NOT the record — reading dep offsets off it lands in
|
||||
// string memory and crashes (a numeric-path caller like pfUI's addon
|
||||
// list hit exactly that). The resolver returns the RequiredDeps
|
||||
// descriptor at `record+0x48`; subtract to recover the base. An unknown
|
||||
// name yields NULL → return nothing (no error), like stock.
|
||||
int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
static constexpr const char *kUsage =
|
||||
"Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")";
|
||||
|
||||
// NULL ⇒ out-of-range numeric index or a non-string/non-number arg;
|
||||
// stock raises the usage error in both cases.
|
||||
const uint8_t *nameOrEntry = ResolveAddOnName(L);
|
||||
if (nameOrEntry == nullptr) {
|
||||
Game::Lua::Error(L, "%s", kUsage);
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(reinterpret_cast<const char *>(nameOrEntry));
|
||||
if (reqDesc == nullptr)
|
||||
return 0; // unknown addon name — no error, push nothing
|
||||
const uint8_t *record = reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_COUNT);
|
||||
if (count == 0)
|
||||
return 0;
|
||||
auto names = *reinterpret_cast<const char *const *const *>(
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_ARRAY);
|
||||
if (names == nullptr)
|
||||
return 0;
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
Game::Lua::PushString(L, names[i]); // NULL → pushnil, per engine
|
||||
return static_cast<int>(count);
|
||||
}
|
||||
|
||||
// `Enum.AddOnSecurityStatus` — what `C_AddOns.GetAddOnSecurity`
|
||||
// returns. Slots 0..2 match the engine's own ordering at
|
||||
// `VAR_ADDON_SECURITY_TABLE`; slot 3 is our addition for
|
||||
@@ -323,6 +385,8 @@ static void RegisterLuaFunctions() {
|
||||
&Script_DoesAddOnExist);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
|
||||
&Script_IsAddOnLoaded);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
|
||||
@@ -150,10 +150,40 @@ void Evict(uint64_t targetGuid, uint32_t spellId) {
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (auto &e : g_cache)
|
||||
e.used = false;
|
||||
}
|
||||
|
||||
// -1 = no map seen yet (never a valid Map.dbc id), so the first tick just
|
||||
// records the current map without flushing.
|
||||
int g_lastMapId = -1;
|
||||
|
||||
// Drop entries whose timed aura has elapsed so the table doesn't fill with
|
||||
// stale combat debuffs. Infinite-duration entries (expirationMs == 0) stay
|
||||
// until overwritten.
|
||||
//
|
||||
// Also flush the entire cache on a map-ID change. A battleground/instance/
|
||||
// continent transition (including the teleport out of a BG via /afk) bulk-
|
||||
// clears the player's aura descriptor WITHOUT firing per-aura OnAuraRemoved,
|
||||
// so entries for auras that are now gone would linger and the
|
||||
// GetAuraDataByIndex fallback would keep surfacing them — the "buffs/debuffs
|
||||
// stuck after leaving a battleground" report. Every other unit from the old
|
||||
// map has despawned too, and the local player's real auras re-sync into the
|
||||
// descriptor / PLAYER_BUFF tables on entering the new world, so a full flush
|
||||
// loses nothing legitimate. Crucially, the descriptor drops the fallback
|
||||
// must survive — rogue stealth and party-range fluctuation — do NOT change
|
||||
// the map ID, so they're unaffected.
|
||||
void OnWorldTick() {
|
||||
const int mapId = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
|
||||
if (mapId != g_lastMapId) {
|
||||
if (g_lastMapId != -1)
|
||||
FlushAll();
|
||||
g_lastMapId = mapId;
|
||||
}
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
|
||||
@@ -186,6 +186,65 @@ int __fastcall Script_HookScript(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---- IsEventRegistered (Frame) ---------------------------------------------
|
||||
|
||||
// `frame:IsEventRegistered("event")` — vanilla has RegisterEvent /
|
||||
// UnregisterEvent but not the query. There's no engine Script_* to
|
||||
// delegate to, so we replicate the membership check that lives inside
|
||||
// Frame::RegisterEvent (FUN_00702140): find the event by name in the live
|
||||
// event table, then walk that entry's subscriber chain for this frame.
|
||||
// The frame pointer we compare against is the same CFrameScriptObject*
|
||||
// the engine stores (Game::Lua::ResolveObject resolves self exactly as
|
||||
// Script_RegisterEvent does).
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
bool FrameHasEvent(void *frame, const char *eventName) {
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_COUNT));
|
||||
auto *base = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_TABLE_BASE_PTR));
|
||||
if (count == 0 || base == nullptr)
|
||||
return false;
|
||||
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
|
||||
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (sstrcmpi(name, eventName, 0x7FFFFFFF) != 0)
|
||||
continue;
|
||||
// Names are unique, so this is the one entry — walk its chain.
|
||||
uintptr_t node = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_HEAD);
|
||||
while ((node & 1) == 0 && node != 0) {
|
||||
if (*reinterpret_cast<void *const *>(
|
||||
node + Offsets::OFF_EVENT_NODE_FRAME) == frame)
|
||||
return true;
|
||||
node = *reinterpret_cast<const uintptr_t *>(
|
||||
node + Offsets::OFF_EVENT_NODE_NEXT);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsEventRegistered(void *L) {
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: frame:IsEventRegistered(\"event\")");
|
||||
return 0;
|
||||
}
|
||||
void *frame = Game::Lua::ResolveObject(L, 1);
|
||||
const char *eventName = Game::Lua::ToString(L, 2);
|
||||
const bool registered =
|
||||
frame != nullptr && eventName != nullptr && FrameHasEvent(frame, eventName);
|
||||
Game::Lua::PushBool(L, registered);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- SetPoint one-arg form (Script_SetPoint co-hook) -----------------------
|
||||
|
||||
// Vanilla's Script_SetPoint accepts every modern form except the bare
|
||||
@@ -222,6 +281,7 @@ const Game::Lua::FrameMethodEntry g_frameMethods[] = {
|
||||
Offsets::FUN_SCRIPT_FRAME_HIDE>},
|
||||
{"SetResizeBounds", &Script_SetResizeBounds},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"IsEventRegistered", &Script_IsEventRegistered},
|
||||
};
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_textureMethods[] = {
|
||||
|
||||
@@ -0,0 +1,466 @@
|
||||
// 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_LootHistory backport.
|
||||
//
|
||||
// Modern WoW (2.x+, incl. 5.4.8) exposes a `C_LootHistory` namespace backed
|
||||
// by an engine-maintained ring buffer that a dedicated loot-history server
|
||||
// packet fills. Vanilla 1.12 has NO such packet or store — but it DOES
|
||||
// receive the raw group-loot roll traffic (SMSG_LOOT_ROLL per player,
|
||||
// SMSG_LOOT_ROLL_WON, SMSG_LOOT_ALL_PASSED), which the default UI only turns
|
||||
// into chat lines. So we reconstruct the history client-side (à la
|
||||
// Aura::Source): co-hook the three roll-result handlers and accumulate a ring
|
||||
// of rolled items + per-player results, then expose C_LootHistory reading
|
||||
// from it.
|
||||
//
|
||||
// The handlers run on the deserialized PendingLootRoll message (0x48 bytes,
|
||||
// LootRoll.cpp) with the fields at the OFF_LOOTROLL_* offsets. The message is
|
||||
// populated at handler entry and freed at exit, so each co-hook reads its
|
||||
// fields BEFORE calling the original.
|
||||
//
|
||||
// === Handler ABI (verified from disassembly — RET conventions DIFFER) ===
|
||||
// ROLL FUN_0061C0B0: __fastcall(msg=ECX, ctx=EDX, int stackArg) -> RET 4
|
||||
// WON FUN_0061B9E0: __fastcall(ctx=ECX, msg=EDX, int stackArg) -> RET 4
|
||||
// PASSED FUN_0061B640: __fastcall(ctx=ECX, msg=EDX) -> RET 0
|
||||
// The trailing stack arg (and matching callee cleanup) is LOAD-BEARING: an
|
||||
// earlier revision declared these as 2-arg hooks (implicit RET 0), so every
|
||||
// ROLL/WON call left ESP unbalanced by 4 -> stack corruption -> crash on the
|
||||
// first loot roll. Each hook below matches its handler's exact shape.
|
||||
//
|
||||
// Name + class per roller come from the engine NameCache (read at hook time,
|
||||
// guaranteed loaded then), so they resolve even for someone who has left the
|
||||
// group by the time their roll packet arrives. Surface: C_LootHistory
|
||||
// GetNumItems / GetItem / GetPlayerInfo, plus LOOT_HISTORY_ROLL_CHANGED /
|
||||
// _ROLL_COMPLETE events. The MoP master-loot management functions
|
||||
// (SetExpiration / GiveMasterLoot / CanMasterLoot) are intentionally omitted
|
||||
// — they depend on server support 1.12 doesn't have.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Loot::History {
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename T>
|
||||
T Read(const void *base, int off) {
|
||||
return *reinterpret_cast<const T *>(static_cast<const uint8_t *>(base) + off);
|
||||
}
|
||||
|
||||
// Modern rollType constants (what C_LootHistory.GetPlayerInfo returns).
|
||||
constexpr int ROLL_PASS = 0;
|
||||
constexpr int ROLL_NEED = 1;
|
||||
constexpr int ROLL_GREED = 2;
|
||||
|
||||
// The SMSG_LOOT_ROLL packet encodes rollType as 0 = need, 2 = greed; map to
|
||||
// the modern convention. A "didn't actually roll" (pass) is flagged by the
|
||||
// high bit of the roll-number byte, handled by the caller.
|
||||
int MapRollType(int packetType) {
|
||||
return packetType == 2 ? ROLL_GREED : ROLL_NEED;
|
||||
}
|
||||
|
||||
// GUID -> engine NameCache entry-data block. This is the same primitive the
|
||||
// engine's own `%N` name substitution uses (FUN_005083A0): for a LOADED entry
|
||||
// the lookup returns the data block (name[48] at +0, class at +0x140, …),
|
||||
// regardless of the (null) callback and regardless of current group
|
||||
// membership — so it resolves a roller who has since left the group, which
|
||||
// is exactly what a loot roll needs (the roll packet arrives after they may
|
||||
// have gone). Guaranteed loaded at hook time: the roll handler's wrapper
|
||||
// gates on this same lookup succeeding. Returns nullptr if not cached.
|
||||
const uint8_t *LookupPlayerRecord(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
using Lookup_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo,
|
||||
uint32_t hi, void *cookie,
|
||||
void *cb, void *ud, char flag);
|
||||
auto fn = reinterpret_cast<Lookup_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_PLAYER_INFO_LOOKUP));
|
||||
uint32_t cookie[2] = {0, 0};
|
||||
return fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_NAME_CACHE)),
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
|
||||
cookie, nullptr, nullptr, 0);
|
||||
}
|
||||
|
||||
const char *NameFromRecord(const uint8_t *rec) {
|
||||
return rec != nullptr
|
||||
? reinterpret_cast<const char *>(rec + Offsets::OFF_PLAYER_INFO_NAME)
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
uint32_t ClassIdFromRecord(const uint8_t *rec) {
|
||||
return rec != nullptr
|
||||
? *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_PLAYER_INFO_CLASS)
|
||||
: 0;
|
||||
}
|
||||
|
||||
// ChrClasses.dbc record ID -> class token ("WARRIOR", "MAGE", …). The DBC's
|
||||
// filename field is the token; RAID_CLASS_COLORS et al. key on it. nullptr if
|
||||
// the ID is out of range or the record is missing.
|
||||
const char *ClassTokenForId(uint32_t classId) {
|
||||
if (classId == 0)
|
||||
return nullptr;
|
||||
const int count = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_COUNT));
|
||||
const uint8_t *const *records = *reinterpret_cast<const uint8_t *const *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_CHRCLASSES_RECORDS));
|
||||
if (records == nullptr || static_cast<int>(classId) > count)
|
||||
return nullptr;
|
||||
const uint8_t *rec = records[classId];
|
||||
if (rec == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const char *const *>(
|
||||
rec + Offsets::OFF_CHRCLASSES_FILENAME);
|
||||
}
|
||||
|
||||
constexpr int kNameLen = 48; // engine NameCache inline name[48]
|
||||
|
||||
void CopyName(char *dst, const char *src) {
|
||||
if (src == nullptr) {
|
||||
dst[0] = '\0';
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (; i < kNameLen - 1 && src[i] != '\0'; ++i)
|
||||
dst[i] = src[i];
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
struct PlayerRoll {
|
||||
uint64_t guid;
|
||||
char name[kNameLen];
|
||||
uint32_t classId; // ChrClasses.dbc ID (0 = unknown)
|
||||
int rollType; // ROLL_* (modern); ROLL_PASS when the player passed
|
||||
int roll; // 1..100, or 0 if they passed / didn't roll
|
||||
};
|
||||
|
||||
constexpr int kMaxPlayers = 40;
|
||||
|
||||
struct HistItem {
|
||||
uint64_t sourceGuid; // looted-object GUID — the roll's identity
|
||||
uint32_t slot;
|
||||
uint32_t itemId;
|
||||
int32_t itemSuffix; // random suffix/property ID (link suffix field)
|
||||
uint32_t itemSeed; // random seed (link unique field)
|
||||
uint64_t winnerGuid;
|
||||
char winnerName[kNameLen];
|
||||
bool hasWinner;
|
||||
bool isDone;
|
||||
PlayerRoll players[kMaxPlayers];
|
||||
int playerCount;
|
||||
bool used;
|
||||
};
|
||||
|
||||
// Ring of recent rolled items. g_write is the next slot to fill; g_count
|
||||
// saturates at kMaxItems. GetNumItems returns g_count; item index i (1-based,
|
||||
// oldest→newest) maps to ring[(g_write - g_count + i - 1) mod kMaxItems].
|
||||
// ~3 KB per item (players[40] dominates), so 128 is ~380 KB static — bump
|
||||
// freely if a longer history is wanted.
|
||||
constexpr int kMaxItems = 128;
|
||||
HistItem g_items[kMaxItems];
|
||||
int g_write = 0;
|
||||
int g_count = 0;
|
||||
|
||||
HistItem *Find(uint64_t sourceGuid, uint32_t slot) {
|
||||
for (auto &e : g_items) {
|
||||
if (e.used && e.sourceGuid == sourceGuid && e.slot == slot)
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
HistItem *FindOrCreate(uint64_t sourceGuid, uint32_t slot, uint32_t itemId) {
|
||||
HistItem *e = Find(sourceGuid, slot);
|
||||
if (e != nullptr) {
|
||||
if (itemId != 0)
|
||||
e->itemId = itemId;
|
||||
return e;
|
||||
}
|
||||
e = &g_items[g_write];
|
||||
g_write = (g_write + 1) % kMaxItems;
|
||||
if (g_count < kMaxItems)
|
||||
++g_count;
|
||||
*e = HistItem{};
|
||||
e->used = true;
|
||||
e->sourceGuid = sourceGuid;
|
||||
e->slot = slot;
|
||||
e->itemId = itemId;
|
||||
return e;
|
||||
}
|
||||
|
||||
// 1-based access in oldest→newest order (matching GetNumItems' count).
|
||||
HistItem *GetEntry(int index1Based) {
|
||||
if (index1Based < 1 || index1Based > g_count)
|
||||
return nullptr;
|
||||
const int start = (g_write - g_count + kMaxItems) % kMaxItems;
|
||||
return &g_items[(start + index1Based - 1) % kMaxItems];
|
||||
}
|
||||
|
||||
// The 1-based index of an entry in oldest→newest order (0 if not live).
|
||||
int IndexOf(const HistItem *e) {
|
||||
const int start = (g_write - g_count + kMaxItems) % kMaxItems;
|
||||
for (int i = 0; i < g_count; ++i) {
|
||||
if (&g_items[(start + i) % kMaxItems] == e)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Modern loot-history events. ROLL_CHANGED(itemIdx, playerIdx) fires on each
|
||||
// roll; ROLL_COMPLETE(itemIdx) when the item is decided (winner / all-passed).
|
||||
// FULL_UPDATE is reserved for API/IsEventValid completeness (the UI fires it
|
||||
// on open); we don't dispatch it from C.
|
||||
constexpr const char *kRollChanged = "LOOT_HISTORY_ROLL_CHANGED";
|
||||
constexpr const char *kRollComplete = "LOOT_HISTORY_ROLL_COMPLETE";
|
||||
constexpr const char *kFullUpdate = "LOOT_HISTORY_FULL_UPDATE";
|
||||
const Event::Custom::AutoReserve _evRollChanged{kRollChanged};
|
||||
const Event::Custom::AutoReserve _evRollComplete{kRollComplete};
|
||||
const Event::Custom::AutoReserve _evFullUpdate{kFullUpdate};
|
||||
|
||||
// Returns the 1-based item index and writes the 1-based player index to
|
||||
// `*outPlayerIdx` (0 if none), so the hook can fire ROLL_CHANGED.
|
||||
int RecordRoll(const void *msg, int *outPlayerIdx) {
|
||||
*outPlayerIdx = 0;
|
||||
const uint64_t src = Read<uint64_t>(msg, Offsets::OFF_LOOTROLL_SOURCE_GUID);
|
||||
const uint32_t slot = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_SLOT);
|
||||
const uint32_t itemId = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_ITEM_ID);
|
||||
const uint64_t roller = Read<uint64_t>(msg, Offsets::OFF_LOOTROLL_ROLLER_GUID);
|
||||
const uint8_t rollByte = Read<uint8_t>(msg, Offsets::OFF_LOOTROLL_ROLL_NUM);
|
||||
const int packetType = Read<int32_t>(msg, Offsets::OFF_LOOTROLL_ROLL_TYPE);
|
||||
|
||||
const bool didRoll = (rollByte & 0x80) == 0;
|
||||
const int rollNum = didRoll ? (rollByte & 0x7F) : 0;
|
||||
const int rollType = didRoll ? MapRollType(packetType) : ROLL_PASS;
|
||||
|
||||
HistItem *e = FindOrCreate(src, slot, itemId);
|
||||
if (roller == 0)
|
||||
return IndexOf(e);
|
||||
|
||||
int slotIdx = -1;
|
||||
for (int i = 0; i < e->playerCount; ++i) {
|
||||
if (e->players[i].guid == roller) {
|
||||
slotIdx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slotIdx < 0 && e->playerCount < kMaxPlayers) {
|
||||
slotIdx = e->playerCount++;
|
||||
const uint8_t *rec = LookupPlayerRecord(roller); // cache loaded now
|
||||
e->players[slotIdx].guid = roller;
|
||||
CopyName(e->players[slotIdx].name, NameFromRecord(rec));
|
||||
e->players[slotIdx].classId = ClassIdFromRecord(rec);
|
||||
}
|
||||
if (slotIdx >= 0) {
|
||||
e->players[slotIdx].rollType = rollType;
|
||||
e->players[slotIdx].roll = rollNum;
|
||||
*outPlayerIdx = slotIdx + 1;
|
||||
}
|
||||
return IndexOf(e);
|
||||
}
|
||||
|
||||
int RecordWinner(const void *msg) {
|
||||
const uint64_t src = Read<uint64_t>(msg, Offsets::OFF_LOOTROLL_SOURCE_GUID);
|
||||
const uint32_t slot = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_SLOT);
|
||||
const uint32_t itemId = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_ITEM_ID);
|
||||
const uint64_t winner = Read<uint64_t>(msg, Offsets::OFF_LOOTROLL_ROLLER_GUID);
|
||||
|
||||
HistItem *e = FindOrCreate(src, slot, itemId);
|
||||
e->winnerGuid = winner;
|
||||
CopyName(e->winnerName, NameFromRecord(LookupPlayerRecord(winner)));
|
||||
e->hasWinner = true;
|
||||
e->isDone = true;
|
||||
return IndexOf(e);
|
||||
}
|
||||
|
||||
int RecordAllPassed(const void *msg) {
|
||||
const uint64_t src = Read<uint64_t>(msg, Offsets::OFF_LOOTROLL_SOURCE_GUID);
|
||||
const uint32_t slot = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_SLOT);
|
||||
const uint32_t itemId = Read<uint32_t>(msg, Offsets::OFF_LOOTROLL_ITEM_ID);
|
||||
|
||||
HistItem *e = FindOrCreate(src, slot, itemId);
|
||||
e->isDone = true;
|
||||
return IndexOf(e);
|
||||
}
|
||||
|
||||
// SMSG_LOOT_START_ROLL carries the item's random suffix/seed; the result
|
||||
// packets don't. It's read from the freshly-built store-head node (rather
|
||||
// than the packet) right after the START_ROLL handler runs — the node has
|
||||
// the same field offsets as the message. This is the only place the random
|
||||
// fields are populated, so the result recorders leave them alone.
|
||||
void RecordStart() {
|
||||
const uint8_t *node = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOOTROLL_STORE_HEAD));
|
||||
if (node == nullptr)
|
||||
return;
|
||||
const uint64_t src = Read<uint64_t>(node, Offsets::OFF_LOOTROLL_SOURCE_GUID);
|
||||
const uint32_t slot = Read<uint32_t>(node, Offsets::OFF_LOOTROLL_SLOT);
|
||||
const uint32_t itemId = Read<uint32_t>(node, Offsets::OFF_LOOTROLL_ITEM_ID);
|
||||
HistItem *e = FindOrCreate(src, slot, itemId);
|
||||
e->itemSuffix = Read<int32_t>(node, Offsets::OFF_LOOTROLL_ITEM_SUFFIX);
|
||||
e->itemSeed = Read<uint32_t>(node, Offsets::OFF_LOOTROLL_ITEM_SEED);
|
||||
}
|
||||
|
||||
// ---- Packet co-hooks -------------------------------------------------------
|
||||
// Signatures match the verified per-handler ABI above (note the differing
|
||||
// message register, stack arg, and RET). Read the message fields before the
|
||||
// original runs — it frees the message.
|
||||
|
||||
using RollFn_t = void(__fastcall *)(void *msg, void *ctx, int a3); // RET 4
|
||||
using WonFn_t = void(__fastcall *)(void *ctx, void *msg, int a3); // RET 4
|
||||
using PassedFn_t = void(__fastcall *)(void *ctx, void *msg); // RET 0
|
||||
using StartFn_t = void(__fastcall *)(void *packet); // RET 0
|
||||
|
||||
RollFn_t g_origRoll = nullptr;
|
||||
WonFn_t g_origWon = nullptr;
|
||||
PassedFn_t g_origPassed = nullptr;
|
||||
StartFn_t g_origStart = nullptr;
|
||||
|
||||
// START_ROLL: let the original build the store node, then read the item's
|
||||
// random suffix off it. Adds the item to the history so a UI can show it as
|
||||
// soon as the roll opens (before any roll comes in).
|
||||
void __fastcall Start_h(void *packet) {
|
||||
g_origStart(packet);
|
||||
RecordStart();
|
||||
}
|
||||
|
||||
// Record fields (message is freed by the original), run the original, THEN
|
||||
// fire the event — firing after the original keeps the engine's own handler
|
||||
// off our re-entrancy (same order Item::Data uses from its packet hook).
|
||||
void __fastcall Roll_h(void *msg, void *ctx, int a3) {
|
||||
int itemIdx = 0, playerIdx = 0;
|
||||
if (msg != nullptr)
|
||||
itemIdx = RecordRoll(msg, &playerIdx);
|
||||
g_origRoll(msg, ctx, a3);
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollChanged), "%d%d", itemIdx,
|
||||
playerIdx);
|
||||
}
|
||||
|
||||
void __fastcall Won_h(void *ctx, void *msg, int a3) {
|
||||
int itemIdx = 0;
|
||||
if (msg != nullptr)
|
||||
itemIdx = RecordWinner(msg);
|
||||
g_origWon(ctx, msg, a3);
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
}
|
||||
|
||||
void __fastcall Passed_h(void *ctx, void *msg) {
|
||||
int itemIdx = 0;
|
||||
if (msg != nullptr)
|
||||
itemIdx = RecordAllPassed(msg);
|
||||
g_origPassed(ctx, msg);
|
||||
if (itemIdx > 0)
|
||||
Event::Custom::Fire(Event::Custom::Lookup(kRollComplete), "%d", itemIdx);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookRoll{
|
||||
Offsets::FUN_LOOT_ROLL_HANDLER, reinterpret_cast<void *>(&Roll_h),
|
||||
reinterpret_cast<void **>(&g_origRoll)};
|
||||
const Game::HookAutoRegister _hookWon{
|
||||
Offsets::FUN_LOOT_ROLL_WON_HANDLER, reinterpret_cast<void *>(&Won_h),
|
||||
reinterpret_cast<void **>(&g_origWon)};
|
||||
const Game::HookAutoRegister _hookPassed{
|
||||
Offsets::FUN_LOOT_ALL_PASSED_HANDLER, reinterpret_cast<void *>(&Passed_h),
|
||||
reinterpret_cast<void **>(&g_origPassed)};
|
||||
const Game::HookAutoRegister _hookStart{
|
||||
Offsets::FUN_LOOT_START_ROLL_HANDLER, reinterpret_cast<void *>(&Start_h),
|
||||
reinterpret_cast<void **>(&g_origStart)};
|
||||
|
||||
// ---- Lua surface -----------------------------------------------------------
|
||||
|
||||
// `C_LootHistory.GetNumItems()` -> number of rolled items in the history.
|
||||
int __fastcall Script_GetNumItems(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_count));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_LootHistory.GetItem(itemIndex)` -> itemLink, numPlayers, isDone, winner.
|
||||
// itemLink is the `item:id:enchant:suffix:unique` form (valid for GetItemInfo
|
||||
// / SetHyperlink), carrying the roll's random suffix so "of the X" items keep
|
||||
// their affix; `winner` is the winner's name, or nil if undecided.
|
||||
int __fastcall Script_GetItem(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetItem(itemIndex)");
|
||||
return 0;
|
||||
}
|
||||
HistItem *e = GetEntry(static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
if (e == nullptr)
|
||||
return 0;
|
||||
static char link[64];
|
||||
// Vanilla item link: item:id:enchant:suffix:unique. Carry the roll's
|
||||
// random suffix/seed so "of the Bear"-style items keep their affix.
|
||||
std::snprintf(link, sizeof(link), "item:%u:0:%d:%u", e->itemId,
|
||||
e->itemSuffix, e->itemSeed);
|
||||
Game::Lua::PushString(L, link); // 1 itemLink
|
||||
Game::Lua::PushNumber(L, static_cast<double>(e->playerCount)); // 2 numPlayers
|
||||
Game::Lua::PushBool(L, e->isDone); // 3 isDone
|
||||
if (e->hasWinner && e->winnerName[0] != '\0')
|
||||
Game::Lua::PushString(L, e->winnerName); // 4 winner
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 4;
|
||||
}
|
||||
|
||||
// `C_LootHistory.GetPlayerInfo(itemIndex, playerIndex)` ->
|
||||
// name, classToken, rollType, roll, isWinner, isMe. classToken is the
|
||||
// ChrClasses filename ("WARRIOR", …) for RAID_CLASS_COLORS-style coloring;
|
||||
// both name and class come from the engine NameCache captured at roll time.
|
||||
// isMe compares the roller GUID to the local player's.
|
||||
int __fastcall Script_GetPlayerInfo(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: GetPlayerInfo(itemIndex, playerIndex)");
|
||||
return 0;
|
||||
}
|
||||
HistItem *e = GetEntry(static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
if (e == nullptr)
|
||||
return 0;
|
||||
const int p = static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1;
|
||||
if (p < 0 || p >= e->playerCount)
|
||||
return 0;
|
||||
const PlayerRoll &r = e->players[p];
|
||||
if (r.name[0] != '\0')
|
||||
Game::Lua::PushString(L, r.name); // 1 name
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
const char *classToken = ClassTokenForId(r.classId); // 2 class token
|
||||
if (classToken != nullptr)
|
||||
Game::Lua::PushString(L, classToken);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.rollType)); // 3 rollType
|
||||
Game::Lua::PushNumber(L, static_cast<double>(r.roll)); // 4 roll
|
||||
Game::Lua::PushBool(L, e->hasWinner && e->winnerGuid == r.guid); // 5 isWinner
|
||||
Game::Lua::PushBool(L, r.guid == Unit::Identity::PlayerGuid()); // 6 isMe
|
||||
return 6;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "GetNumItems",
|
||||
&Script_GetNumItems);
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "GetItem",
|
||||
&Script_GetItem);
|
||||
Game::Lua::RegisterTableFunction("C_LootHistory", "GetPlayerInfo",
|
||||
&Script_GetPlayerInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Loot::History
|
||||
+76
-17
@@ -62,6 +62,19 @@
|
||||
// BLP1 and BLP2 keep width/height at header offsets +0x0C/+0x10.
|
||||
// Resolution runs once per overlay per session (cached).
|
||||
//
|
||||
// ## HD map patches (uniform tile upscale)
|
||||
//
|
||||
// A custom `patch-*.mpq` can replace the overlay art with higher-res tiles
|
||||
// (every 256px tile re-exported at 256*scale). Because we read the *file*
|
||||
// dimensions, that upscale must not be assumed to be 256: the resolver
|
||||
// detects the full-cell size `S` (the largest tile dimension = 256*scale)
|
||||
// and expresses both the partial-tile threshold and the texcoords relative
|
||||
// to it. Draw sizes/positions remain in logical map pixels (the patch changes
|
||||
// texture resolution, not the map canvas), so an interior tile reads texcoord
|
||||
// 1.0 at any scale and stock data (S == 256) is byte-identical to before.
|
||||
// The upscale factor need not be integral; only a uniform per-overlay scale
|
||||
// is assumed.
|
||||
//
|
||||
// ## Return shape
|
||||
//
|
||||
// Array of overlay tables:
|
||||
@@ -102,6 +115,17 @@ using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line,
|
||||
|
||||
constexpr int kMaxTiles = 64;
|
||||
|
||||
// Smallest power of two >= v (min 16), matching the engine's own
|
||||
// WorldMapFrame tile-file-size loop. A tile's content fills content/NextPot
|
||||
// of its (power-of-two) file — a fraction that's identical whether the file
|
||||
// is stock or HD-upscaled, so texcoords derived from it are scale-invariant.
|
||||
int NextPot(int v) {
|
||||
int p = 16;
|
||||
while (p < v)
|
||||
p <<= 1;
|
||||
return p;
|
||||
}
|
||||
|
||||
// Resolved tile grid for one overlay, cached for the session (DBC and
|
||||
// MPQ contents are immutable per process).
|
||||
struct ResolvedOverlay {
|
||||
@@ -109,6 +133,7 @@ struct ResolvedOverlay {
|
||||
uint8_t rows;
|
||||
uint8_t count; // tiles to draw (foreign tail already dropped)
|
||||
bool upscaled;
|
||||
uint16_t cell; // full-cell file size (256 on stock, 256*scale on an HD patch)
|
||||
uint16_t fileW[kMaxTiles]; // BLP file dimensions per tile
|
||||
uint16_t fileH[kMaxTiles];
|
||||
};
|
||||
@@ -161,13 +186,29 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
if (dbcRows < 1)
|
||||
dbcRows = 1;
|
||||
|
||||
// Full-cell file size. On stock data a full (interior) tile is 256px; an
|
||||
// HD map patch (patch-*.mpq) upscales every tile uniformly, so a full cell
|
||||
// is 256*scale. Full tiles are the largest; edges/slivers are smaller — so
|
||||
// the max tile dimension is the full-cell size. Partial-tile detection
|
||||
// (and the texcoords in PushTiles) must compare against this S, not a
|
||||
// hardcoded 256, or an HD edge tile (e.g. a 200px edge upscaled to 400px)
|
||||
// reads as "full" and shears the row-major grid into diagonal strips.
|
||||
int S = 256;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileW[i] > S)
|
||||
S = out->fileW[i];
|
||||
if (out->fileH[i] > S)
|
||||
S = out->fileH[i];
|
||||
}
|
||||
out->cell = static_cast<uint16_t>(S);
|
||||
|
||||
// Real column count: the first partial-width tile is a row's right
|
||||
// edge. Search one past dbcCols so a sliver column the DBC width
|
||||
// rounds off is still found.
|
||||
int cols = dbcCols;
|
||||
bool widthEdge = false;
|
||||
for (int i = 0; i < n && i <= dbcCols; ++i) {
|
||||
if (out->fileW[i] < 256) {
|
||||
if (out->fileW[i] < S) {
|
||||
cols = i + 1;
|
||||
widthEdge = true;
|
||||
break;
|
||||
@@ -179,7 +220,7 @@ void ResolveTiles(ResolvedOverlay *out, const char *basePath, int dbcW,
|
||||
int rows = dbcRows;
|
||||
bool heightEdge = false;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (out->fileH[i] < 256) {
|
||||
if (out->fileH[i] < S) {
|
||||
rows = i / cols + 1;
|
||||
heightEdge = true;
|
||||
break;
|
||||
@@ -304,27 +345,45 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
posX = offX + col * drawW;
|
||||
posY = offY + row * drawH;
|
||||
} else {
|
||||
// Canonical 256px grid. Edge tiles draw their content pixels:
|
||||
// from the DBC remainder when it's honest, else (sliver
|
||||
// column the DBC rounded away) the whole file.
|
||||
// Canonical 256px logical grid. Draw sizes/positions stay in
|
||||
// logical map pixels; the texcoord is the content fraction of the
|
||||
// (possibly HD-upscaled) file. `scale` = file px per logical px
|
||||
// (1 on stock, 2/4 on an HD patch), so a full interior tile reads
|
||||
// texCoord 1.0 regardless of file size, and an edge's content
|
||||
// (remainder * scale) is measured against its actual file width.
|
||||
const int fileW = r->fileW[i];
|
||||
const int fileH = r->fileH[i];
|
||||
int pixW = 256;
|
||||
int pixH = 256;
|
||||
const double scale = (r->cell > 0) ? (r->cell / 256.0) : 1.0;
|
||||
|
||||
double logicalW = 256.0, logicalH = 256.0;
|
||||
tcX = 1.0;
|
||||
tcY = 1.0;
|
||||
if (col == r->cols - 1) {
|
||||
pixW = dbcW - 256 * (r->cols - 1);
|
||||
if (pixW <= 0 || pixW > fileW)
|
||||
pixW = fileW;
|
||||
const int rem = dbcW - 256 * (r->cols - 1);
|
||||
if (rem <= 0) {
|
||||
// Sliver the DBC width rounded away: whole file is content
|
||||
// (its logical width is the file width de-scaled).
|
||||
logicalW = fileW / scale;
|
||||
} else {
|
||||
// Content fills rem/NextPot(rem) of the POT tile file.
|
||||
// Scale-invariant, so an HD-upscaled file (POT*scale)
|
||||
// reads the same fraction — using the raw file width here
|
||||
// would halve the texcoord on any tile the patch doubled.
|
||||
logicalW = rem;
|
||||
tcX = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
if (row == r->rows - 1) {
|
||||
pixH = dbcH - 256 * (r->rows - 1);
|
||||
if (pixH <= 0 || pixH > fileH)
|
||||
pixH = fileH;
|
||||
const int rem = dbcH - 256 * (r->rows - 1);
|
||||
if (rem <= 0) {
|
||||
logicalH = fileH / scale;
|
||||
} else {
|
||||
logicalH = rem;
|
||||
tcY = rem / static_cast<double>(NextPot(rem));
|
||||
}
|
||||
}
|
||||
drawW = pixW;
|
||||
drawH = pixH;
|
||||
tcX = static_cast<double>(pixW) / fileW;
|
||||
tcY = static_cast<double>(pixH) / fileH;
|
||||
drawW = logicalW;
|
||||
drawH = logicalH;
|
||||
posX = offX + col * 256;
|
||||
posY = offY + row * 256;
|
||||
}
|
||||
|
||||
+24
-4
@@ -133,6 +133,26 @@ void StampChannel(int spellID, int startMs, int endMs) {
|
||||
|
||||
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
|
||||
|
||||
// Pushes an engine-ms timestamp to Lua as an UNSIGNED 32-bit value.
|
||||
//
|
||||
// `FUN_OS_TICKCOUNT_MS` returns a 64-bit ms tick from one of two backends
|
||||
// (GetTickCount, or a scaled rdtsc — the latter survives warm reboots, so
|
||||
// the value can be well past 2^31 even right after a "restart"). We keep
|
||||
// ticks in a signed int internally, which is fine for the small deltas and
|
||||
// window compares (they cancel), but a direct int->double push surfaces a
|
||||
// NEGATIVE startTimeMs/endTimeMs once the tick's high bit is set.
|
||||
//
|
||||
// The contract is that these match `GetTime()*1000`. Script_GetTime
|
||||
// (0x00515EA0) does `(double)(tick & 0xFFFFFFFF) * 0.001` — i.e. it masks
|
||||
// the tick to the low 32 bits UNSIGNED, then scales. So reinterpreting our
|
||||
// stored tick as uint32 reproduces GetTime()'s exact value for the whole
|
||||
// range, including across the ~49.7-day 2^32 wrap (both wrap together).
|
||||
// Carrying 64-bit precision here would be WRONG: it would diverge from
|
||||
// GetTime() past 2^32 and break addons' (now-start)/(end-start) math.
|
||||
void PushMs(void *L, int ms) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(static_cast<uint32_t>(ms)));
|
||||
}
|
||||
|
||||
void *Resolve(const char *token) {
|
||||
return reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN)(token);
|
||||
}
|
||||
@@ -200,8 +220,8 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
Game::Lua::PushString(L, name); // 1 name
|
||||
Game::Lua::PushString(L, name); // 2 displayName
|
||||
Game::Lua::PushString(L, SpellIconPath(rec)); // 3 textureID (path)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.startMs)); // 4 startTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.endMs)); // 5 endTimeMs
|
||||
PushMs(L, c.startMs); // 4 startTimeMs
|
||||
PushMs(L, c.endMs); // 5 endTimeMs
|
||||
Game::Lua::PushBool(L, IsTradeskill(rec)); // 6 isTradeskill
|
||||
Game::Lua::PushNil(L); // 7 castID
|
||||
Game::Lua::PushBool(L, false); // 8 notInterruptible
|
||||
@@ -230,8 +250,8 @@ int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes
|
||||
Game::Lua::PushString(L, name); // 2 text / displayName
|
||||
Game::Lua::PushString(L, SpellIconPath(rec)); // 3 textureID (path)
|
||||
if (haveTimes) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(startMs)); // 4 startTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(endMs)); // 5 endTimeMs
|
||||
PushMs(L, startMs); // 4 startTimeMs
|
||||
PushMs(L, endMs); // 5 endTimeMs
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNil(L);
|
||||
|
||||
+48
-31
@@ -46,6 +46,34 @@ uint32_t MountDisplayID(const uint8_t *desc) {
|
||||
desc + Offsets::OFF_UNIT_FIELD_MOUNTDISPLAYID);
|
||||
}
|
||||
|
||||
// Scans the player's helpful-aura (buff) slots for the first spell whose
|
||||
// Spell.dbc record lists `auraEffect` (a `SPELL_AURA_*` code) in any of
|
||||
// its three effect slots, and returns that spellID (0 if none). Used by
|
||||
// `Dismount` to find the active mount aura to cancel — the engine's
|
||||
// `CMSG_CANCEL_AURA` needs the spellID, not a slot index.
|
||||
uint32_t FindPlayerBuffWithEffect(int auraEffect) {
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] == auraEffect)
|
||||
return spellID;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsMounted()` — true iff the player is currently mounted. Reads
|
||||
// `UNIT_FIELD_MOUNTDISPLAYID` from the descriptor; non-zero means
|
||||
// the engine is rendering a mount under the player.
|
||||
@@ -71,46 +99,35 @@ int __fastcall Script_Dismount(void *L) {
|
||||
if (MountDisplayID(desc) == 0)
|
||||
return 0;
|
||||
|
||||
auto *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_BUFF_COUNT; ++slot) {
|
||||
if (!Aura::Data::IsSlotPopulated(player, slot))
|
||||
continue;
|
||||
const uint32_t spellID = Aura::Data::ReadSpellID(player, slot);
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(static_cast<int>(spellID));
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
|
||||
auto *auraTypes = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int eff = 0; eff < Offsets::SPELL_RECORD_EFFECT_COUNT; ++eff) {
|
||||
if (auraTypes[eff] != Offsets::SPELL_AURA_MOUNTED)
|
||||
continue;
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
return 0;
|
||||
}
|
||||
const uint32_t spellID = FindPlayerBuffWithEffect(Offsets::SPELL_AURA_MOUNTED);
|
||||
if (spellID != 0) {
|
||||
auto fn = reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND));
|
||||
fn(static_cast<int>(spellID));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `IsStealthed()` — true iff the player is currently stealthed
|
||||
// (Rogue Stealth / Druid Prowl). Checks `PLAYER_FIELD_VIS_BYTES`
|
||||
// bit 0x02. Mount also sets that bit, so we AND-gate with
|
||||
// `mountDisplayID == 0` to disambiguate. See the comment on
|
||||
// `OFF_PLAYER_FIELD_VIS_BYTES` in Offsets.h for the full bit map
|
||||
// and known limitations (untested for shapeshift forms).
|
||||
// (Rogue Stealth / Druid Prowl / Night Elf Shadowmeld / etc.). Reads
|
||||
// the sneak flag in `UNIT_FIELD_BYTES_1` byte 3 (mask
|
||||
// `UNIT_BYTES_1_STEALTH_FLAG`) — the same broadcast bit the client's
|
||||
// render path uses to draw a stealthed unit semi-transparent.
|
||||
//
|
||||
// Derived via `_classicapi_DiffAll`: a Prowl toggle from a Cat-Form
|
||||
// baseline flips byte 3 `0x00 -> 0x02` and touches nothing else but
|
||||
// aura-array slot metadata, so this is the one standalone descriptor
|
||||
// bit that means "stealthed" (the earlier `0x17C & 0x02` read was
|
||||
// actually AURALEVELS and only worked by coincidence). Being a unit
|
||||
// anim-tier flag rather than a class field, it covers every stealth
|
||||
// source and rank without a spellID list or an aura scan.
|
||||
int __fastcall Script_IsStealthed(void *L) {
|
||||
auto *desc = Unit::Identity::PlayerDescriptor();
|
||||
bool stealthed = false;
|
||||
if (desc != nullptr) {
|
||||
const uint32_t visBytes = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_PLAYER_FIELD_VIS_BYTES);
|
||||
stealthed = (visBytes & Offsets::PLAYER_VIS_BIT_STEALTH) != 0
|
||||
&& MountDisplayID(desc) == 0;
|
||||
const uint32_t bytes1 = *reinterpret_cast<const uint32_t *>(
|
||||
desc + Offsets::OFF_UNIT_FIELD_BYTES_1);
|
||||
stealthed = (bytes1 & Offsets::UNIT_BYTES_1_STEALTH_FLAG) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, stealthed);
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user