Add bag full mailbox mailing flow
This commit is contained in:
543
MailboxCourier/MailboxCourier.lua
Normal file
543
MailboxCourier/MailboxCourier.lua
Normal file
@@ -0,0 +1,543 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user