193 lines
7.1 KiB
Lua
193 lines
7.1 KiB
Lua
-- Guda ClamOpener
|
|
-- Walks the player's bags and uses each clam in turn until none remain.
|
|
-- Triggered by /guda openclams. Stops on UI_ERROR_MESSAGE so a full inventory
|
|
-- (or any other error from UseContainerItem) cleanly aborts the run.
|
|
|
|
local addon = Guda
|
|
local L = Guda_L
|
|
|
|
local ClamOpener = {}
|
|
addon.Modules.ClamOpener = ClamOpener
|
|
|
|
-- Hardcoded vanilla clam item IDs
|
|
local CLAM_IDS = {
|
|
[5523] = true, -- Small Barnacled Clam
|
|
[5524] = true, -- Thick-shelled Clam
|
|
[7973] = true, -- Big-mouth Clam
|
|
[15874] = true, -- Soft-shelled Clam
|
|
}
|
|
|
|
local OPEN_DELAY = 0.5 -- seconds between opens; lets loot/bag updates settle
|
|
local QUIET_DELAY = 0.5 -- seconds of silence after a *_CLOSED event before auto-opening
|
|
local running = false
|
|
local silentRun = false
|
|
local pendingToken = 0 -- bumped on every auto-trigger and on LOOT_OPENED to cancel stale timers
|
|
local waitingForLoot = false
|
|
local OpenNext -- forward declaration; defined after OnClamLootClosed
|
|
|
|
-- Returns true if any blocking window (loot/mail/trade/merchant/bank/auction)
|
|
-- is currently open. We don't want to UseContainerItem while these are active --
|
|
-- it can race the open window or get queued and lost.
|
|
local function IsBlockingWindowOpen()
|
|
local frames = {
|
|
"LootFrame", "MailFrame", "TradeFrame", "MerchantFrame",
|
|
"BankFrame", "AuctionFrame",
|
|
-- Guda's own bank/mail frames too
|
|
"Guda_BankFrame", "Guda_MailboxFrame",
|
|
}
|
|
for _, name in ipairs(frames) do
|
|
local f = getglobal(name)
|
|
if f and f.IsShown and f:IsShown() then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- Find the next clam in player bags. Returns bagID, slotID, itemLink or nil.
|
|
local function FindNextClam()
|
|
for bagID = 0, 4 do
|
|
local numSlots = GetContainerNumSlots(bagID)
|
|
if numSlots and numSlots > 0 then
|
|
for slotID = 1, numSlots do
|
|
local link = GetContainerItemLink(bagID, slotID)
|
|
if link then
|
|
local _, _, idStr = string.find(link, "item:(%d+)")
|
|
local itemID = idStr and tonumber(idStr)
|
|
if itemID and CLAM_IDS[itemID] then
|
|
return bagID, slotID
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function StopRun(reason)
|
|
if not running then return end
|
|
running = false
|
|
addon.Modules.Events:UnregisterOwner("ClamOpener_Run")
|
|
addon.Modules.Events:UnregisterOwner("ClamOpener_LootWait")
|
|
waitingForLoot = false
|
|
if reason and not silentRun then
|
|
addon:Print(reason)
|
|
end
|
|
silentRun = false
|
|
end
|
|
|
|
-- waitingForLoot: set between UseContainerItem and the LOOT_CLOSED that confirms
|
|
-- the clam loot window has fully closed, preventing any concurrent UseContainerItem.
|
|
local function OnClamLootClosed()
|
|
-- Unregister this one-shot listener immediately.
|
|
addon.Modules.Events:UnregisterOwner("ClamOpener_LootWait")
|
|
waitingForLoot = false
|
|
if not running then return end
|
|
-- Small delay to let the client fully settle before the next open.
|
|
Guda_ScheduleTimer(OPEN_DELAY, OpenNext)
|
|
end
|
|
|
|
OpenNext = function()
|
|
if not running then return end
|
|
if waitingForLoot then return end
|
|
|
|
-- Never use a clam while something else is in-flight: cursor busy,
|
|
-- a blocking window open, or a server-side loot still active. Any of
|
|
-- these + UseContainerItem races the open window or get queued and lost.
|
|
if CursorHasItem()
|
|
or IsBlockingWindowOpen()
|
|
or (GetNumLootItems and GetNumLootItems() > 0) then
|
|
Guda_ScheduleTimer(OPEN_DELAY, OpenNext)
|
|
return
|
|
end
|
|
|
|
local bagID, slotID = FindNextClam()
|
|
if not bagID then
|
|
StopRun(L["No more clams to open."])
|
|
return
|
|
end
|
|
|
|
-- Arm the one-shot LOOT_CLOSED listener before calling UseContainerItem
|
|
-- so we cannot miss the event even if it fires in the same frame.
|
|
waitingForLoot = true
|
|
addon.Modules.Events:Register("LOOT_CLOSED", OnClamLootClosed, "ClamOpener_LootWait")
|
|
UseContainerItem(bagID, slotID)
|
|
end
|
|
|
|
-- Stop on UI_ERROR_MESSAGE (e.g. inventory full). UseContainerItem fires this
|
|
-- the same frame on failure, so any error after we start counts as an abort.
|
|
local function OnUIError()
|
|
if not running then return end
|
|
local msg = arg1
|
|
StopRun(string.format(L["Clam opener stopped: %s"], tostring(msg or "error")))
|
|
end
|
|
|
|
-- silent: true to suppress chat messages (used by auto-trigger).
|
|
function ClamOpener:Open(silent)
|
|
if running then
|
|
if not silent then
|
|
addon:Print(L["Clam opener is already running."])
|
|
end
|
|
return
|
|
end
|
|
|
|
-- Quick sanity check so we don't print "stopped" without ever starting.
|
|
if not FindNextClam() then
|
|
if not silent then
|
|
addon:Print(L["No clams found in your bags."])
|
|
end
|
|
return
|
|
end
|
|
|
|
running = true
|
|
silentRun = silent and true or false
|
|
addon.Modules.Events:Register("UI_ERROR_MESSAGE", OnUIError, "ClamOpener_Run")
|
|
if not silent then
|
|
addon:Print(L["Opening clams..."])
|
|
end
|
|
OpenNext()
|
|
end
|
|
|
|
-- Initialize auto-open. Triggers:
|
|
-- * LOOT_CLOSED — the common case: clam looted from a corpse.
|
|
-- * MAIL_CLOSED — clam received via mail.
|
|
-- * TRADE_CLOSED — clam received via trade.
|
|
-- * BANKFRAME_CLOSED — clam withdrawn from bank.
|
|
-- * LOOT_OPENED — cancel-only: a new loot window during the quiet period
|
|
-- invalidates any pending auto-open. The matching
|
|
-- LOOT_CLOSED will re-arm it.
|
|
-- BAG_UPDATE is intentionally NOT a trigger: it fires many times during
|
|
-- AutoLoot's per-slot loop, which would stack timers that race the client's
|
|
-- loot-close transition and soft-lock the loot window.
|
|
-- Every trigger bumps a monotonic token; each scheduled callback captures its
|
|
-- token at schedule time and only fires if no newer event has superseded it.
|
|
function ClamOpener:Initialize()
|
|
local function OnLootOpened()
|
|
-- New loot window just opened: cancel any pending auto-open. The
|
|
-- matching LOOT_CLOSED will re-arm it once that window is done.
|
|
pendingToken = pendingToken + 1
|
|
end
|
|
|
|
local function tryAutoOpen()
|
|
if running then return end
|
|
if not (Guda.Modules.DB and Guda.Modules.DB:GetSetting("autoOpenClams")) then
|
|
return
|
|
end
|
|
pendingToken = pendingToken + 1
|
|
local myToken = pendingToken
|
|
Guda_ScheduleTimer(QUIET_DELAY, function()
|
|
if myToken ~= pendingToken then return end
|
|
if running then return end
|
|
if IsBlockingWindowOpen() then return end
|
|
if GetNumLootItems and GetNumLootItems() > 0 then return end
|
|
ClamOpener:Open(true)
|
|
end)
|
|
end
|
|
|
|
addon.Modules.Events:Register("LOOT_OPENED", OnLootOpened, "ClamOpener")
|
|
addon.Modules.Events:Register("LOOT_CLOSED", tryAutoOpen, "ClamOpener")
|
|
addon.Modules.Events:Register("MAIL_CLOSED", tryAutoOpen, "ClamOpener")
|
|
addon.Modules.Events:Register("TRADE_CLOSED", tryAutoOpen, "ClamOpener")
|
|
addon.Modules.Events:Register("BANKFRAME_CLOSED", tryAutoOpen, "ClamOpener")
|
|
end
|