544 lines
15 KiB
Lua
544 lines
15 KiB
Lua
local ADDON_NAME = "MailboxCourier"
|
||
|
||
local DEFAULTS = {
|
||
recipient = "",
|
||
subject = "Auto mail",
|
||
interval = 1.2,
|
||
dryRun = false,
|
||
autoConfirmUnknownRecipient = true,
|
||
skipSoulbound = true,
|
||
skipQuest = true,
|
||
skipContainers = true,
|
||
blacklist = {
|
||
[6948] = true, -- Hearthstone
|
||
[7005] = true, -- Skinning Knife
|
||
},
|
||
}
|
||
|
||
local state = {
|
||
running = false,
|
||
mailboxOpen = false,
|
||
queue = {},
|
||
batch = {},
|
||
batchCount = 0,
|
||
sentItems = 0,
|
||
sentMails = 0,
|
||
nextAt = 0,
|
||
waitingForSuccess = false,
|
||
lastConfirmAt = 0,
|
||
lastError = "",
|
||
}
|
||
|
||
local frame = CreateFrame("Frame")
|
||
local tooltip = CreateFrame("GameTooltip", "MailboxCourierTooltip", UIParent, "GameTooltipTemplate")
|
||
tooltip:SetOwner(UIParent, "ANCHOR_NONE")
|
||
|
||
local function copyDefaults(src, dst)
|
||
dst = dst or {}
|
||
for key, value in pairs(src) do
|
||
if type(value) == "table" then
|
||
dst[key] = copyDefaults(value, dst[key])
|
||
elseif dst[key] == nil then
|
||
dst[key] = value
|
||
end
|
||
end
|
||
return dst
|
||
end
|
||
|
||
local function db()
|
||
MailboxCourierDB = copyDefaults(DEFAULTS, MailboxCourierDB or {})
|
||
return MailboxCourierDB
|
||
end
|
||
|
||
local function trim(text)
|
||
return tostring(text or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||
end
|
||
|
||
local function log(message)
|
||
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[" .. ADDON_NAME .. "]|r " .. tostring(message))
|
||
end
|
||
|
||
local function parseItemId(itemLink)
|
||
if not itemLink then
|
||
return nil
|
||
end
|
||
local itemId = itemLink:match("item:(%d+)")
|
||
return itemId and tonumber(itemId) or nil
|
||
end
|
||
|
||
local function clearSendMailAttachments()
|
||
ClearCursor()
|
||
if ClearSendMail then
|
||
ClearSendMail()
|
||
return
|
||
end
|
||
for index = 1, 12 do
|
||
if HasSendMailItem and HasSendMailItem(index) then
|
||
ClickSendMailItemButton(index)
|
||
ClearCursor()
|
||
end
|
||
end
|
||
end
|
||
|
||
local function tooltipHasText(bag, slot, needles)
|
||
tooltip:ClearLines()
|
||
tooltip:SetBagItem(bag, slot)
|
||
for i = 1, tooltip:NumLines() do
|
||
local line = _G["MailboxCourierTooltipTextLeft" .. i]
|
||
local text = line and line:GetText()
|
||
if text then
|
||
for _, needle in ipairs(needles) do
|
||
if needle and needle ~= "" and text:find(needle, 1, true) then
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function isSoulbound(bag, slot)
|
||
return tooltipHasText(bag, slot, {
|
||
ITEM_SOULBOUND,
|
||
"Soulbound",
|
||
"灵魂绑定",
|
||
"靈魂綁定",
|
||
})
|
||
end
|
||
|
||
local function isQuestItem(bag, slot)
|
||
return tooltipHasText(bag, slot, {
|
||
ITEM_BIND_QUEST,
|
||
"Quest Item",
|
||
"任务物品",
|
||
"任務物品",
|
||
})
|
||
end
|
||
|
||
local function isContainerItem(itemId, itemLink)
|
||
local name, link, quality, itemLevel, reqLevel, itemType, itemSubType, stackCount, equipLoc = GetItemInfo(itemId or itemLink or "")
|
||
if equipLoc == "INVTYPE_BAG" then
|
||
return true
|
||
end
|
||
if itemType == ITEM_CLASS_CONTAINER or itemType == "Container" or itemType == "容器" or itemType == "容器" then
|
||
return true
|
||
end
|
||
return false
|
||
end
|
||
|
||
local function getContainerItemInfoCompat(bag, slot)
|
||
local texture, count, locked, quality, readable, lootable, itemLink
|
||
if C_Container and C_Container.GetContainerItemInfo then
|
||
local info = C_Container.GetContainerItemInfo(bag, slot)
|
||
if not info then
|
||
return nil
|
||
end
|
||
texture = info.iconFileID
|
||
count = info.stackCount
|
||
locked = info.isLocked
|
||
quality = info.quality
|
||
readable = info.isReadable
|
||
lootable = info.hasLoot
|
||
itemLink = info.hyperlink
|
||
else
|
||
texture, count, locked, quality, readable, lootable, itemLink = GetContainerItemInfo(bag, slot)
|
||
if not itemLink and GetContainerItemLink then
|
||
itemLink = GetContainerItemLink(bag, slot)
|
||
end
|
||
end
|
||
if not itemLink then
|
||
return nil
|
||
end
|
||
return {
|
||
texture = texture,
|
||
count = count or 1,
|
||
locked = locked,
|
||
quality = quality,
|
||
readable = readable,
|
||
lootable = lootable,
|
||
itemLink = itemLink,
|
||
itemId = parseItemId(itemLink),
|
||
}
|
||
end
|
||
|
||
local function getContainerNumSlotsCompat(bag)
|
||
if C_Container and C_Container.GetContainerNumSlots then
|
||
return C_Container.GetContainerNumSlots(bag) or 0
|
||
end
|
||
return GetContainerNumSlots(bag) or 0
|
||
end
|
||
|
||
local function shouldSkipItem(bag, slot, info)
|
||
local cfg = db()
|
||
local itemId = info.itemId
|
||
|
||
if info.locked then
|
||
return true, "locked"
|
||
end
|
||
if itemId and cfg.blacklist[itemId] then
|
||
return true, "blacklisted"
|
||
end
|
||
if cfg.skipSoulbound and isSoulbound(bag, slot) then
|
||
return true, "soulbound"
|
||
end
|
||
if cfg.skipQuest and isQuestItem(bag, slot) then
|
||
return true, "quest"
|
||
end
|
||
if cfg.skipContainers and isContainerItem(itemId, info.itemLink) then
|
||
return true, "container"
|
||
end
|
||
return false, nil
|
||
end
|
||
|
||
local function scanBags()
|
||
local queue = {}
|
||
local skipped = {}
|
||
|
||
for bag = 0, 4 do
|
||
for slot = 1, getContainerNumSlotsCompat(bag) do
|
||
local info = getContainerItemInfoCompat(bag, slot)
|
||
if info then
|
||
local skip, reason = shouldSkipItem(bag, slot, info)
|
||
if skip then
|
||
skipped[reason] = (skipped[reason] or 0) + 1
|
||
else
|
||
table.insert(queue, {
|
||
bag = bag,
|
||
slot = slot,
|
||
itemId = info.itemId,
|
||
itemLink = info.itemLink,
|
||
count = info.count or 1,
|
||
})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return queue, skipped
|
||
end
|
||
|
||
local function stop(reason)
|
||
state.running = false
|
||
state.waitingForSuccess = false
|
||
state.queue = {}
|
||
state.batch = {}
|
||
state.batchCount = 0
|
||
state.nextAt = 0
|
||
clearSendMailAttachments()
|
||
if reason and reason ~= "" then
|
||
log(reason)
|
||
end
|
||
end
|
||
|
||
local function popNextUnlockedItem()
|
||
while #state.queue > 0 do
|
||
local item = table.remove(state.queue, 1)
|
||
local info = getContainerItemInfoCompat(item.bag, item.slot)
|
||
if info and not info.locked and parseItemId(info.itemLink) == item.itemId then
|
||
local skip = shouldSkipItem(item.bag, item.slot, info)
|
||
if not skip then
|
||
return item
|
||
end
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function attachNextBatch()
|
||
clearSendMailAttachments()
|
||
state.batch = {}
|
||
state.batchCount = 0
|
||
|
||
for attachment = 1, 12 do
|
||
local item = popNextUnlockedItem()
|
||
if not item then
|
||
break
|
||
end
|
||
|
||
ClearCursor()
|
||
PickupContainerItem(item.bag, item.slot)
|
||
if CursorHasItem and not CursorHasItem() then
|
||
log("无法拿起物品,跳过: " .. tostring(item.itemLink))
|
||
else
|
||
ClickSendMailItemButton(attachment)
|
||
ClearCursor()
|
||
table.insert(state.batch, item)
|
||
state.batchCount = state.batchCount + 1
|
||
end
|
||
end
|
||
|
||
return state.batchCount > 0
|
||
end
|
||
|
||
local function sendCurrentBatch()
|
||
local cfg = db()
|
||
local recipient = trim(cfg.recipient)
|
||
if recipient == "" then
|
||
stop("没有收件人。先输入 /mc to 角色名")
|
||
return
|
||
end
|
||
|
||
local subject = trim(cfg.subject)
|
||
if subject == "" then
|
||
subject = "Auto mail"
|
||
end
|
||
subject = subject .. " " .. (state.sentMails + 1)
|
||
|
||
if cfg.dryRun then
|
||
state.sentItems = state.sentItems + state.batchCount
|
||
state.sentMails = state.sentMails + 1
|
||
log("dry-run: 模拟发送第 " .. state.sentMails .. " 封,附件 " .. state.batchCount .. " 件")
|
||
clearSendMailAttachments()
|
||
state.nextAt = GetTime() + cfg.interval
|
||
return
|
||
end
|
||
|
||
state.waitingForSuccess = true
|
||
state.nextAt = GetTime() + 8
|
||
SendMail(recipient, subject, "")
|
||
log("已提交第 " .. (state.sentMails + 1) .. " 封,附件 " .. state.batchCount .. " 件")
|
||
end
|
||
|
||
local function maybeConfirmMailPopup()
|
||
local cfg = db()
|
||
if not cfg.autoConfirmUnknownRecipient then
|
||
return false
|
||
end
|
||
if not state.running or not state.waitingForSuccess then
|
||
return false
|
||
end
|
||
if GetTime() - state.lastConfirmAt < 0.5 then
|
||
return false
|
||
end
|
||
|
||
local recipient = trim(cfg.recipient)
|
||
for i = 1, 4 do
|
||
local popup = _G["StaticPopup" .. i]
|
||
if popup and popup:IsShown() then
|
||
local which = tostring(popup.which or "")
|
||
local textFrame = _G["StaticPopup" .. i .. "Text"]
|
||
local text = tostring(textFrame and textFrame:GetText() or "")
|
||
local lowerText = text:lower()
|
||
local looksLikeMailPopup =
|
||
which:find("MAIL", 1, true)
|
||
or lowerText:find("mail", 1, true)
|
||
or lowerText:find("recipient", 1, true)
|
||
or lowerText:find("send", 1, true)
|
||
or (recipient ~= "" and text:find(recipient, 1, true))
|
||
|
||
if looksLikeMailPopup then
|
||
local button = _G["StaticPopup" .. i .. "Button1"]
|
||
if button and (not button.IsEnabled or button:IsEnabled()) then
|
||
state.lastConfirmAt = GetTime()
|
||
button:Click()
|
||
log("Auto-confirmed mail warning popup.")
|
||
return true
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
return false
|
||
end
|
||
|
||
local function process()
|
||
if not state.running then
|
||
return
|
||
end
|
||
if state.waitingForSuccess then
|
||
maybeConfirmMailPopup()
|
||
end
|
||
if GetTime() < state.nextAt then
|
||
return
|
||
end
|
||
if state.waitingForSuccess then
|
||
stop("等待邮件发送成功超时,已停止。最后错误: " .. tostring(state.lastError))
|
||
return
|
||
end
|
||
if not state.mailboxOpen then
|
||
stop("邮箱没有打开,已停止。")
|
||
return
|
||
end
|
||
|
||
if attachNextBatch() then
|
||
sendCurrentBatch()
|
||
else
|
||
stop("邮寄完成,共发送 " .. state.sentMails .. " 封," .. state.sentItems .. " 件物品。")
|
||
end
|
||
end
|
||
|
||
local function beginSend()
|
||
local cfg = db()
|
||
local recipient = trim(cfg.recipient)
|
||
if recipient == "" then
|
||
log("没有收件人。用法: /mc to 角色名")
|
||
return
|
||
end
|
||
if not state.mailboxOpen then
|
||
log("邮箱还没打开。请先和邮箱交互。")
|
||
return
|
||
end
|
||
|
||
state.queue = {}
|
||
state.batch = {}
|
||
state.batchCount = 0
|
||
state.sentItems = 0
|
||
state.sentMails = 0
|
||
state.lastError = ""
|
||
state.waitingForSuccess = false
|
||
|
||
local skipped
|
||
state.queue, skipped = scanBags()
|
||
if #state.queue == 0 then
|
||
log("没有找到可邮寄物品。")
|
||
return
|
||
end
|
||
|
||
state.running = true
|
||
state.nextAt = GetTime() + 0.2
|
||
log("开始邮寄给 " .. recipient .. ",待发送 " .. #state.queue .. " 个背包格。")
|
||
|
||
local parts = {}
|
||
for reason, count in pairs(skipped or {}) do
|
||
table.insert(parts, reason .. "=" .. count)
|
||
end
|
||
if #parts > 0 then
|
||
log("已跳过: " .. table.concat(parts, ", "))
|
||
end
|
||
end
|
||
|
||
local function showStatus()
|
||
local cfg = db()
|
||
log("收件人: " .. (trim(cfg.recipient) ~= "" and cfg.recipient or "未设置"))
|
||
log("主题: " .. cfg.subject .. ";间隔: " .. tostring(cfg.interval) .. " 秒;dry-run: " .. tostring(cfg.dryRun))
|
||
log("命令: /mc to 角色名, /mc send, /mc stop, /mc dryrun on|off, /mc black add|del itemID")
|
||
end
|
||
|
||
local function setRecipient(name)
|
||
name = trim(name)
|
||
if name == "" then
|
||
log("用法: /mc to 角色名")
|
||
return
|
||
end
|
||
db().recipient = name
|
||
log("收件人已设置为: " .. name)
|
||
end
|
||
|
||
local function setSubject(subject)
|
||
subject = trim(subject)
|
||
if subject == "" then
|
||
log("用法: /mc subject 邮件主题")
|
||
return
|
||
end
|
||
db().subject = subject
|
||
log("邮件主题已设置为: " .. subject)
|
||
end
|
||
|
||
local function setDryRun(value)
|
||
value = trim(value):lower()
|
||
if value == "on" or value == "1" or value == "true" then
|
||
db().dryRun = true
|
||
elseif value == "off" or value == "0" or value == "false" then
|
||
db().dryRun = false
|
||
else
|
||
log("用法: /mc dryrun on|off")
|
||
return
|
||
end
|
||
log("dry-run: " .. tostring(db().dryRun))
|
||
end
|
||
|
||
local function setAutoConfirm(value)
|
||
value = trim(value):lower()
|
||
if value == "on" or value == "1" or value == "true" then
|
||
db().autoConfirmUnknownRecipient = true
|
||
elseif value == "off" or value == "0" or value == "false" then
|
||
db().autoConfirmUnknownRecipient = false
|
||
else
|
||
log("Usage: /mc autoconfirm on|off")
|
||
return
|
||
end
|
||
log("auto-confirm unknown recipient: " .. tostring(db().autoConfirmUnknownRecipient))
|
||
end
|
||
|
||
local function updateBlacklist(rest)
|
||
local action, idText = trim(rest):match("^(%S+)%s+(%S+)$")
|
||
local itemId = tonumber(idText or "")
|
||
if not action or not itemId then
|
||
log("用法: /mc black add itemID 或 /mc black del itemID")
|
||
return
|
||
end
|
||
action = action:lower()
|
||
if action == "add" then
|
||
db().blacklist[itemId] = true
|
||
log("已加入黑名单 itemID: " .. itemId)
|
||
elseif action == "del" or action == "remove" then
|
||
db().blacklist[itemId] = nil
|
||
log("已移出黑名单 itemID: " .. itemId)
|
||
else
|
||
log("用法: /mc black add itemID 或 /mc black del itemID")
|
||
end
|
||
end
|
||
|
||
SLASH_MAILBOXCOURIER1 = "/mc"
|
||
SLASH_MAILBOXCOURIER2 = "/mailboxcourier"
|
||
SlashCmdList["MAILBOXCOURIER"] = function(msg)
|
||
local command, rest = trim(msg):match("^(%S*)%s*(.-)$")
|
||
command = (command or ""):lower()
|
||
rest = rest or ""
|
||
|
||
if command == "to" then
|
||
setRecipient(rest)
|
||
elseif command == "subject" then
|
||
setSubject(rest)
|
||
elseif command == "send" then
|
||
beginSend()
|
||
elseif command == "stop" then
|
||
stop("已手动停止。")
|
||
elseif command == "dryrun" then
|
||
setDryRun(rest)
|
||
elseif command == "autoconfirm" then
|
||
setAutoConfirm(rest)
|
||
elseif command == "black" or command == "blacklist" then
|
||
updateBlacklist(rest)
|
||
else
|
||
showStatus()
|
||
end
|
||
end
|
||
|
||
frame:RegisterEvent("ADDON_LOADED")
|
||
frame:RegisterEvent("MAIL_SHOW")
|
||
frame:RegisterEvent("MAIL_CLOSED")
|
||
frame:RegisterEvent("MAIL_SEND_SUCCESS")
|
||
frame:RegisterEvent("UI_ERROR_MESSAGE")
|
||
|
||
frame:SetScript("OnEvent", function(_, event, arg1, arg2)
|
||
if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
|
||
db()
|
||
log("已加载。输入 /mc 查看命令。")
|
||
elseif event == "MAIL_SHOW" then
|
||
state.mailboxOpen = true
|
||
log("邮箱已打开。")
|
||
elseif event == "MAIL_CLOSED" then
|
||
state.mailboxOpen = false
|
||
if state.running then
|
||
stop("邮箱关闭,已停止。")
|
||
end
|
||
elseif event == "MAIL_SEND_SUCCESS" then
|
||
state.sentItems = state.sentItems + state.batchCount
|
||
state.sentMails = state.sentMails + 1
|
||
state.waitingForSuccess = false
|
||
state.batch = {}
|
||
state.batchCount = 0
|
||
clearSendMailAttachments()
|
||
state.nextAt = GetTime() + db().interval
|
||
log("第 " .. state.sentMails .. " 封发送成功,累计 " .. state.sentItems .. " 件。")
|
||
elseif event == "UI_ERROR_MESSAGE" then
|
||
state.lastError = tostring(arg2 or arg1 or "")
|
||
if state.running and state.lastError ~= "" then
|
||
log("错误: " .. state.lastError)
|
||
end
|
||
end
|
||
end)
|
||
|
||
frame:SetScript("OnUpdate", process)
|