From 3be191009151a3144e990b5004967fb4bf7c3470 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 3 Sep 2026 21:34:06 +0200 Subject: [PATCH 1/4] Expose ClassicAPI AoE loot capabilities --- api.lua | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api.lua b/api.lua index 85979fb..ae5d374 100644 --- a/api.lua +++ b/api.lua @@ -50,6 +50,9 @@ API.containeritems = type(_G.C_Container) == "table" API.containeriteminfo = type(_G.C_Container) == "table" and type(_G.C_Container.GetContainerItemInfo) == "function" +API.containeropenable = type(_G.C_Container) == "table" + and type(_G.C_Container.IsContainerItemOpenable) == "function" + API.items = type(_G.C_Item) == "table" API.iteminfo = API.items and type(_G.C_Item.GetItemInfo) == "function" API.iteminfoinstant = API.items @@ -119,6 +122,11 @@ API.loothistoryevents = API.loothistory and API.eventutils and _G.C_EventUtils.IsEventValid("LOOT_HISTORY_FULL_UPDATE") API.lootrollitemid = type(_G.GetLootRollItemID) == "function" +API.aoeloot = type(_G.C_Loot) == "table" + and type(_G.C_Loot.GetNearbyLootableUnits) == "function" + and type(_G.C_Loot.IsScanInProgress) == "function" + and type(_G.C_Loot.LootAllCorpses) == "function" + API.overridebindings = type(_G.SetOverrideBindingClick) == "function" and type(_G.ClearOverrideBindings) == "function" @@ -380,6 +388,30 @@ API.GetContainerNumFreeSlots = function(bag) return 0, 0 end +API.IsContainerItemOpenable = function(bag, slot) + if API.containeropenable then + return _G.C_Container.IsContainerItemOpenable(bag, slot) + end +end + +API.GetNearbyLootableUnits = function() + if API.aoeloot then + return _G.C_Loot.GetNearbyLootableUnits() + end +end + +API.IsLootScanInProgress = function() + return API.aoeloot and _G.C_Loot.IsScanInProgress() or false +end + +API.LootAllCorpses = function(maxCount) + if API.aoeloot then + if maxCount then return _G.C_Loot.LootAllCorpses(maxCount) end + return _G.C_Loot.LootAllCorpses() + end + return false +end + -- ClassicAPI updates its own left/right modifier bitmap before firing -- MODIFIER_STATE_CHANGED. Prefer that state over vanilla's merged Win32 key -- state when the richer ClassicAPI functions are available. From 5c609a438fe9722cc2d95b64697a840d30d51581 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 3 Sep 2026 21:34:09 +0200 Subject: [PATCH 2/4] Remove obsolete AoE Loot ClassicAPI fallbacks --- mods/aoe-loot.lua | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/mods/aoe-loot.lua b/mods/aoe-loot.lua index e7ae300..e06f08f 100644 --- a/mods/aoe-loot.lua +++ b/mods/aoe-loot.lua @@ -1,6 +1,7 @@ -- Adapted from AoELoot by Sandrea / ChatGPT for ShaguTweaks-ClassicAPI. local T = ShaguTweaks.T +local API = ShaguTweaks.API local module = ShaguTweaks:register({ title = T["AoE Loot"], @@ -11,15 +12,18 @@ local module = ShaguTweaks:register({ }) module.enable = function(self) + -- AoE Loot relies entirely on ClassicAPI's native corpse walker and + -- container classification. Do not keep partial legacy behavior when those + -- capabilities are unavailable. + if not API or not API.aoeloot or not API.containeropenable then return end + local frame = CreateFrame("Frame", "ShaguTweaksAoELoot") local pending = false local releaseDeadline = 0 local containerLootDeadline = 0 local function TrackContainerUse(bag, slot) - if not C_Container or not C_Container.IsContainerItemOpenable then return end - - local isOpenable, canOpen = C_Container.IsContainerItemOpenable(bag, slot) + local isOpenable, canOpen = API.IsContainerItemOpenable(bag, slot) if isOpenable and canOpen then -- LOOT_OPENED is also fired by clams, lockboxes and similar bag items. -- Mark that session before UseContainerItem runs so it remains under the @@ -39,21 +43,12 @@ module.enable = function(self) local function CanStartAoELoot() if IsMasterLootActive() then return false end - if not C_Loot or not C_Loot.LootAllCorpses then return false end - - if C_Loot.IsScanInProgress and C_Loot.IsScanInProgress() then - return false - end - + if API.IsLootScanInProgress() then return false end return true end local function HasNearbyLootableCorpse() - -- Older ClassicAPI builds may not expose the query yet. Keep AoE Loot - -- functional there; the container hook above still protects bag items. - if not C_Loot or not C_Loot.GetNearbyLootableUnits then return true end - - local units = C_Loot.GetNearbyLootableUnits() + local units = API.GetNearbyLootableUnits() return type(units) == "table" and table.getn(units) > 0 end @@ -64,7 +59,7 @@ module.enable = function(self) releaseDeadline = 0 if CanStartAoELoot() then - C_Loot.LootAllCorpses() + API.LootAllCorpses() end end From 8c070ac1b215beb772de1b4bdb356939519822f9 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Thu, 3 Sep 2026 23:30:23 +0200 Subject: [PATCH 3/4] Let native autoloot settle before AoE loot --- mods/aoe-loot.lua | 91 +++++++++++++++++++---------------------------- 1 file changed, 36 insertions(+), 55 deletions(-) diff --git a/mods/aoe-loot.lua b/mods/aoe-loot.lua index e06f08f..584e9af 100644 --- a/mods/aoe-loot.lua +++ b/mods/aoe-loot.lua @@ -19,7 +19,6 @@ module.enable = function(self) local frame = CreateFrame("Frame", "ShaguTweaksAoELoot") local pending = false - local releaseDeadline = 0 local containerLootDeadline = 0 local function TrackContainerUse(bag, slot) @@ -53,78 +52,60 @@ module.enable = function(self) end local function StartAoELoot() - -- Defer for one frame so the normal loot session can close cleanly. frame:SetScript("OnUpdate", nil) pending = false - releaseDeadline = 0 if CanStartAoELoot() then API.LootAllCorpses() end end - local function QueueAoELoot() - releaseDeadline = 0 + local function ResolveLootSession() + -- Give the client's own autoloot path one frame to act before taking over. + -- This avoids closing the loot session underneath SuperWoW/SuperAPI or a + -- quickloot-patched client while remaining independent of either mod. + frame:SetScript("OnUpdate", nil) + if not pending then return end + + -- Manual loot still has items after that frame. Close only in that case; + -- when native autoloot already emptied/closed the session, leave it alone. + if GetNumLootItems and GetNumLootItems() > 0 then + CloseLoot() + end + + -- The original path already waits one frame after CloseLoot before handing + -- control to ClassicAPI. Keep that release frame for both manual and native + -- autoloot paths. frame:SetScript("OnUpdate", StartAoELoot) end - local function WaitForLootRelease() - -- Native Auto Loot can finish before this module receives LOOT_OPENED. - -- In that case LOOT_CLOSED has already passed, so use the empty session - -- as a fallback and queue the ClassicAPI walk on the following frame. - if pending and (not GetNumLootItems or GetNumLootItems() == 0) then - QueueAoELoot() - elseif pending and GetTime() >= releaseDeadline then - -- Never leave a per-frame waiter behind if the native loot session - -- cannot finish, for example because every inventory bag is full. - pending = false - releaseDeadline = 0 - frame:SetScript("OnUpdate", nil) - end - end - frame:RegisterEvent("LOOT_OPENED") - frame:RegisterEvent("LOOT_CLOSED") frame:SetScript("OnEvent", function() - if event == "LOOT_OPENED" then - -- Inventory containers use the same loot events as corpses. Let the - -- normal client finish them instead of closing their loot session. - if containerLootDeadline > 0 then - local isContainerLoot = GetTime() <= containerLootDeadline - containerLootDeadline = 0 - if isContainerLoot then return end - end + if event ~= "LOOT_OPENED" then return end - -- The master looter must keep the normal window to inspect and assign loot. - if IsMasterLootActive() then - pending = false - releaseDeadline = 0 - frame:SetScript("OnUpdate", nil) - return - end - - -- Chests, fishing nodes and other non-corpse sources also emit - -- LOOT_OPENED. AoE Loot only has work to do when ClassicAPI can actually - -- see at least one nearby lootable unit. - if not HasNearbyLootableCorpse() then return end - - if pending or not CanStartAoELoot() then return end - - pending = true - releaseDeadline = GetTime() + 3 - frame:SetScript("OnUpdate", WaitForLootRelease) - - -- Close the normal session so ClassicAPI can take over. The temporary - -- waiter also handles clients whose native Auto Loot already completed - -- and fired LOOT_CLOSED before this module received LOOT_OPENED. - CloseLoot() + -- Inventory containers use the same loot event as corpses. Let the normal + -- client finish them instead of treating them as an AoE-loot trigger. + if containerLootDeadline > 0 then + local isContainerLoot = GetTime() <= containerLootDeadline + containerLootDeadline = 0 + if isContainerLoot then return end + end + -- The master looter must keep the normal window to inspect and assign loot. + if IsMasterLootActive() then + pending = false + frame:SetScript("OnUpdate", nil) return end - if event == "LOOT_CLOSED" and pending then - QueueAoELoot() - end + -- Chests, fishing nodes and other non-corpse sources also emit LOOT_OPENED. + -- Only take over when ClassicAPI can see at least one nearby lootable unit. + if not HasNearbyLootableCorpse() then return end + + if pending or not CanStartAoELoot() then return end + + pending = true + frame:SetScript("OnUpdate", ResolveLootSession) end) end From 58463e555846633729d101f7694d22a6c683bf96 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Fri, 4 Sep 2026 18:11:24 +0200 Subject: [PATCH 4/4] Give AoE Loot priority over native autoloot --- mods/aoe-loot.lua | 68 +++++++++++++++++++++++++++-------------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/mods/aoe-loot.lua b/mods/aoe-loot.lua index 584e9af..5549622 100644 --- a/mods/aoe-loot.lua +++ b/mods/aoe-loot.lua @@ -12,14 +12,21 @@ local module = ShaguTweaks:register({ }) module.enable = function(self) - -- AoE Loot relies entirely on ClassicAPI's native corpse walker and - -- container classification. Do not keep partial legacy behavior when those - -- capabilities are unavailable. - if not API or not API.aoeloot or not API.containeropenable then return end + -- AoE Loot relies entirely on ClassicAPI's native corpse walker, + -- container classification and timer. Do not keep partial legacy behavior + -- when those capabilities are unavailable. + if not API or not API.aoeloot or not API.containeropenable or not API.timer then return end local frame = CreateFrame("Frame", "ShaguTweaksAoELoot") local pending = false local containerLootDeadline = 0 + local takeoverGeneration = 0 + local TAKEOVER_DELAY = 0.3 + + local function CancelTakeover() + takeoverGeneration = takeoverGeneration + 1 + pending = false + end local function TrackContainerUse(bag, slot) local isOpenable, canOpen = API.IsContainerItemOpenable(bag, slot) @@ -51,32 +58,31 @@ module.enable = function(self) return type(units) == "table" and table.getn(units) > 0 end - local function StartAoELoot() - frame:SetScript("OnUpdate", nil) - pending = false + local function StartAoELoot(generation) + if generation ~= takeoverGeneration or not pending then return end + pending = false if CanStartAoELoot() then API.LootAllCorpses() end end - local function ResolveLootSession() - -- Give the client's own autoloot path one frame to act before taking over. - -- This avoids closing the loot session underneath SuperWoW/SuperAPI or a - -- quickloot-patched client while remaining independent of either mod. - frame:SetScript("OnUpdate", nil) - if not pending then return end + local function TakeOverLootSession() + -- AoE Loot owns corpse sessions while enabled. Close the normal client + -- window immediately so Vanilla autoloot, SuperAPI SetAutoloot modes and + -- quickloot-patched clients cannot keep control of the session. + takeoverGeneration = takeoverGeneration + 1 + local generation = takeoverGeneration + pending = true + CloseLoot() - -- Manual loot still has items after that frame. Close only in that case; - -- when native autoloot already emptied/closed the session, leave it alone. - if GetNumLootItems and GetNumLootItems() > 0 then - CloseLoot() - end - - -- The original path already waits one frame after CloseLoot before handing - -- control to ClassicAPI. Keep that release frame for both manual and native - -- autoloot paths. - frame:SetScript("OnUpdate", StartAoELoot) + -- Native autoloot may already have emitted loot packets before LOOT_OPENED + -- reaches Lua. ClassicAPI's own loot test uses the same 0.3s settling + -- window between loot operations; after it expires the native corpse walker + -- becomes the sole owner of all remaining nearby corpse loot. + C_Timer.After(TAKEOVER_DELAY, function() + StartAoELoot(generation) + end) end frame:RegisterEvent("LOOT_OPENED") @@ -89,13 +95,15 @@ module.enable = function(self) if containerLootDeadline > 0 then local isContainerLoot = GetTime() <= containerLootDeadline containerLootDeadline = 0 - if isContainerLoot then return end + if isContainerLoot then + CancelTakeover() + return + end end -- The master looter must keep the normal window to inspect and assign loot. if IsMasterLootActive() then - pending = false - frame:SetScript("OnUpdate", nil) + CancelTakeover() return end @@ -103,9 +111,11 @@ module.enable = function(self) -- Only take over when ClassicAPI can see at least one nearby lootable unit. if not HasNearbyLootableCorpse() then return end - if pending or not CanStartAoELoot() then return end + -- A queued takeover already owns this interaction. ClassicAPI suppresses + -- its own LOOT_OPENED/LOOT_CLOSED events while walking corpses, so no + -- additional session needs to be stacked here. + if pending then return end - pending = true - frame:SetScript("OnUpdate", ResolveLootSession) + TakeOverLootSession() end) end