Files
wow/MailboxCourier/MailboxCourier.lua
2026-05-07 08:33:10 +08:00

544 lines
15 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)