Files
2026-04-09 00:22:26 +04:00

371 lines
13 KiB
Lua

-- Guda Mailbox Scanner
-- Scans and stores mailbox contents
local addon = Guda
local MailboxScanner = {}
addon.Modules.MailboxScanner = MailboxScanner
local mailboxOpen = false
local savePending = false
local SAVE_DEBOUNCE = 0.5
-- Debounced save: coalesces bursts of MAIL_INBOX_UPDATE into a single rescan.
-- Scanning 100+ mails on every event was causing client crashes.
local function ScheduleSave()
if savePending then return end
if not mailboxOpen then return end
savePending = true
Guda_ScheduleTimer(SAVE_DEBOUNCE, function()
savePending = false
if mailboxOpen then
MailboxScanner:SaveToDatabase()
end
end)
end
-- Scan all mailbox items and return data
function MailboxScanner:ScanMailbox()
if not mailboxOpen then
addon:Debug("Cannot scan mailbox - not open")
return {}
end
local mailboxData = {}
local numItems = GetInboxNumItems()
for i = 1, numItems do
local mailRows = self:ScanMailItemRows(i)
for _, row in ipairs(mailRows) do
table.insert(mailboxData, row)
end
end
return mailboxData
end
-- Scan a single mail into one or more rows (flattened)
function MailboxScanner:ScanMailItemRows(index)
-- GetInboxHeaderInfo(index) returns: packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM
local packageIcon, stationeryIcon, sender, subject, money, CODAmount, daysLeft, hasItem, wasRead, wasReturned, textCreated, canReply, isGM = GetInboxHeaderInfo(index)
local rows = {}
if hasItem then
-- Turtle WoW supports up to 12 attachments per mail.
-- We use GetInboxNumAttachments if available to avoid over-scanning on servers
-- that might ignore the second argument of GetInboxItem.
local numAttachments = 0
if GetInboxNumAttachments then
numAttachments = GetInboxNumAttachments(index) or 0
end
-- Fallback: if we don't have the count but header says there's an item, assume at least 1.
if numAttachments == 0 and hasItem then
numAttachments = 1
end
for itemIndex = 1, numAttachments do
-- GetInboxItem(index, itemIndex) returns: name, texture, count, quality, canUse
local name, texture, count, quality, canUse = GetInboxItem(index, itemIndex)
if not name then break end
local itemLink = addon.Modules.Utils:GetInboxItemLink(index, itemIndex)
local itemID = itemLink and addon.Modules.Utils:ExtractItemID(itemLink)
-- Fallback 1: If link/itemID is missing, try GetItemInfo(name) which might be cached now
if not itemID or not itemLink then
local _, link = GetItemInfo(name)
if link then
itemLink = link
itemID = addon.Modules.Utils:ExtractItemID(link)
addon:Debug("Recovered link from GetItemInfo for %s", name)
end
end
-- (Removed) FindItemByName cross-character fallback: with cross-account
-- data this scales as mails * attachments * characters * slots and could
-- crash the client on large mailboxes. GetItemInfo above handles the
-- common case; missing links resolve on the next rescan once cached.
local itemData = {
link = itemLink,
texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
count = count or 1,
quality = quality or 0,
name = name,
itemID = itemID,
}
-- Ensure we have a link if we have an itemID
if itemData.itemID and not itemData.link then
itemData.link = "item:" .. itemData.itemID .. ":0:0:0"
end
-- If we have a link, try to get more detailed info from cache
if itemData.link then
local itemName, link, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(itemData.link)
if itemName then
itemData.name = itemName
itemData.quality = itemQuality or itemData.quality
itemData.iLevel = iLevel
itemData.type = itemType
itemData.class = itemCategory
itemData.subclass = itemSubType
itemData.equipSlot = itemEquipLoc
if itemTexture then itemData.texture = itemTexture end
end
end
table.insert(rows, {
sender = sender,
subject = subject,
money = (itemIndex == 1) and money or 0, -- Attach money only to the first row of this mail
CODAmount = (itemIndex == 1) and CODAmount or 0,
daysLeft = daysLeft,
hasItem = true,
item = itemData,
mailIndex = index,
itemIndex = itemIndex,
wasRead = wasRead,
packageIcon = packageIcon,
})
end
end
-- If no items found but there is money or it's just a letter
if table.getn(rows) == 0 then
table.insert(rows, {
sender = sender,
subject = subject,
money = money,
CODAmount = CODAmount,
daysLeft = daysLeft,
hasItem = false,
item = nil,
mailIndex = index,
itemIndex = 1,
wasRead = wasRead,
packageIcon = packageIcon,
})
end
return rows
end
-- Save current mailbox to database
function MailboxScanner:SaveToDatabase()
if not mailboxOpen then
return
end
local mailboxData = self:ScanMailbox()
addon.Modules.DB:SaveMailbox(mailboxData)
addon:Debug("Mailbox data saved")
end
-- Build item data from name/texture/count/quality
local function BuildSendMailItemData(name, texture, count, quality)
local _, link = GetItemInfo(name)
local itemData = {
name = name,
texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
count = count or 1,
quality = quality or 0,
link = link,
itemID = addon.Modules.Utils:ExtractItemID(link),
}
local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc = addon.Modules.Utils:GetItemInfo(name)
if itemName then
itemData.link = retLink or itemData.link
itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID
itemData.quality = itemQuality or itemData.quality
itemData.iLevel = iLevel
itemData.type = itemType
itemData.class = itemCategory
itemData.subclass = itemSubType
itemData.equipSlot = itemEquipLoc
if itemTexture then itemData.texture = itemTexture end
end
if not itemData.itemID and itemData.link then
itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link)
end
return itemData
end
-- Handle outgoing mail
function MailboxScanner:OnSendMail(recipient, subject, body)
if not recipient or recipient == "" then return end
local moneyAmount = GetSendMailMoney()
local subjectText = (subject and subject ~= "") and subject or "No Subject"
local senderName = UnitName("player")
local addedAny = false
-- Try multi-attachment (TurtleWoW): check if HasSendMailItem exists
if HasSendMailItem then
for itemIndex = 1, 12 do
if not HasSendMailItem(itemIndex) then break end
local name, texture, count, quality = GetSendMailItem(itemIndex)
if not name then break end
local mailRow = {
sender = senderName,
subject = subjectText,
money = (itemIndex == 1) and moneyAmount or 0,
CODAmount = 0,
daysLeft = 30,
hasItem = true,
item = BuildSendMailItemData(name, texture, count, quality),
wasRead = false,
}
addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow)
addedAny = true
end
end
-- Fallback: single attachment (vanilla API)
if not addedAny then
local name, texture, count, quality = GetSendMailItem()
if name then
local mailRow = {
sender = senderName,
subject = subjectText,
money = moneyAmount,
CODAmount = 0,
daysLeft = 30,
hasItem = true,
item = BuildSendMailItemData(name, texture, count, quality),
wasRead = false,
}
addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow)
addedAny = true
end
end
-- Money-only mail (no items)
if not addedAny and moneyAmount > 0 then
local mailRow = {
sender = senderName,
subject = subjectText,
money = moneyAmount,
CODAmount = 0,
daysLeft = 30,
hasItem = false,
item = nil,
wasRead = false,
}
addon.Modules.DB:AddMailToCharacter(recipient, nil, mailRow)
end
end
-- Handle auction house buyouts
function MailboxScanner:OnAuctionBid(type, index, bid)
local name, texture, count, quality, canUse, level, minBid, minIncrement, buyoutPrice, bidAmount, highBidder, owner = GetAuctionItemInfo(type, index)
if name and buyoutPrice > 0 and bid >= buyoutPrice then
local link = GetAuctionItemLink(type, index)
local itemData = {
name = name,
texture = texture or "Interface\\Icons\\INV_Misc_Bag_08",
count = count or 1,
quality = quality or 0,
link = link,
itemID = addon.Modules.Utils:ExtractItemID(link),
}
-- Try to get more info if it's in cache
local itemName, retLink, itemQuality, iLevel, itemCategory, itemType, itemStackCount, itemSubType, itemTexture, itemEquipLoc, itemSellPrice = addon.Modules.Utils:GetItemInfo(link or name)
if itemName then
itemData.link = retLink or itemData.link
itemData.itemID = addon.Modules.Utils:ExtractItemID(itemData.link) or itemData.itemID
itemData.quality = itemQuality or itemData.quality
itemData.iLevel = iLevel
itemData.type = itemType
itemData.class = itemCategory
itemData.subclass = itemSubType
itemData.equipSlot = itemEquipLoc
if itemTexture then itemData.texture = itemTexture end
end
local mailRow = {
sender = "Auction House",
subject = "Auction won: " .. name,
money = 0,
CODAmount = 0,
daysLeft = 30,
hasItem = true,
item = itemData,
wasRead = false,
}
addon.Modules.DB:AddMailToCharacter(UnitName("player"), nil, mailRow)
addon:Debug("Captured AH buyout: %s", name)
end
end
-- Initialize mailbox scanner
function MailboxScanner:Initialize()
-- Hook SendMail to capture outgoing mail to alts
local originalSendMail = SendMail
SendMail = function(recipient, subject, body)
MailboxScanner:OnSendMail(recipient, subject, body)
return originalSendMail(recipient, subject, body)
end
-- Hook PlaceAuctionBid to capture AH buyouts
local originalPlaceAuctionBid = PlaceAuctionBid
PlaceAuctionBid = function(type, index, bid)
MailboxScanner:OnAuctionBid(type, index, bid)
return originalPlaceAuctionBid(type, index, bid)
end
-- Hook taking items/money (original calls only, DB save handled by MAIL_INBOX_UPDATE)
local originalTakeInboxItem = TakeInboxItem
TakeInboxItem = function(index, attachmentIndex)
return originalTakeInboxItem(index, attachmentIndex)
end
local originalTakeInboxMoney = TakeInboxMoney
TakeInboxMoney = function(index)
return originalTakeInboxMoney(index)
end
local originalAutoLootMailItem = AutoLootMailItem
AutoLootMailItem = function(index)
return originalAutoLootMailItem(index)
end
-- Rescan mailbox when server confirms inbox changes (debounced)
addon.Modules.Events:Register("MAIL_INBOX_UPDATE", function()
ScheduleSave()
end, "MailboxScanner_InboxUpdate")
-- Mailbox opened
addon.Modules.Events:OnMailShow(function()
mailboxOpen = true
addon:Debug("Mailbox opened")
ScheduleSave()
end, "MailboxScanner")
-- Mailbox closed
addon.Modules.Events:OnMailClosed(function()
-- Final save on close (synchronous so it can't race with mailboxOpen=false)
if mailboxOpen then
MailboxScanner:SaveToDatabase()
end
mailboxOpen = false
savePending = false
addon:Debug("Mailbox closed")
end, "MailboxScanner")
end
-- Check if mailbox is currently open
function MailboxScanner:IsMailboxOpen()
return mailboxOpen
end