mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e9e30567c | ||
|
|
8be78ad946 | ||
|
|
0dea869ab1 | ||
|
|
b4dffb75c0 | ||
|
|
9445fd6333 | ||
|
|
c8effdd6ec | ||
|
|
b3ac1c07f7 | ||
|
|
cfbc4f4e9c | ||
|
|
bb9579c398 | ||
|
|
5642644833 |
@@ -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
|
||||
@@ -36,7 +36,9 @@ local LO = TURTLE and {
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
detail = { 379, -113 },
|
||||
-- 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 },
|
||||
@@ -52,10 +54,32 @@ local LO = TURTLE and {
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
detail = { 28, -237 },
|
||||
-- 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",
|
||||
@@ -233,6 +257,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
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);
|
||||
@@ -247,6 +272,20 @@ local function CreateFrame_TradeSkillLink()
|
||||
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
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
@@ -299,7 +338,7 @@ local function CreateFrame_TradeSkillLink()
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, "FauxScrollFrameTemplate");
|
||||
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()
|
||||
@@ -345,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", LO.detail[1], LO.detail[2]);
|
||||
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);
|
||||
@@ -364,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;
|
||||
@@ -413,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)
|
||||
@@ -441,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 "");
|
||||
@@ -456,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
|
||||
|
||||
@@ -509,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()
|
||||
@@ -663,9 +793,24 @@ local function CreateProfessionLinkButton(parentName, getLink)
|
||||
if _G[btnName] or not parent then
|
||||
return;
|
||||
end
|
||||
local title = _G[parentName .. "TitleText"];
|
||||
local btn = CreateFrame("Button", btnName, parent);
|
||||
btn:SetSize(16, 16);
|
||||
btn:SetPoint("LEFT", _G[parentName .. "TitleText"], "RIGHT", 4, 0);
|
||||
|
||||
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);
|
||||
@@ -691,8 +836,20 @@ end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
end);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink);
|
||||
end);
|
||||
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)
|
||||
|
||||
@@ -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` |
|
||||
@@ -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"`) |
|
||||
|
||||
+98
-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)
|
||||
@@ -255,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)
|
||||
@@ -605,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.
|
||||
@@ -726,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
|
||||
@@ -2295,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)
|
||||
@@ -6125,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
|
||||
|
||||
@@ -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
|
||||
@@ -3834,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user