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)
|
||||||
8
MailboxCourier/MailboxCourier.toc
Normal file
8
MailboxCourier/MailboxCourier.toc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
## Interface: 30300
|
||||||
|
## Title: MailboxCourier
|
||||||
|
## Notes: Open a mailbox, then batch mail bag items to a saved recipient.
|
||||||
|
## Author: Codex
|
||||||
|
## Version: 0.1.0
|
||||||
|
## SavedVariablesPerCharacter: MailboxCourierDB
|
||||||
|
|
||||||
|
MailboxCourier.lua
|
||||||
42
MailboxCourier/README.md
Normal file
42
MailboxCourier/README.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# MailboxCourier
|
||||||
|
|
||||||
|
Wrath 3.3.5 addon for testing mailbox batch sending.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Copy the `MailboxCourier` folder into:
|
||||||
|
|
||||||
|
```text
|
||||||
|
World of Warcraft/Interface/AddOns/
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reload UI or restart the game.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
1. Open a mailbox.
|
||||||
|
2. Set recipient:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mc to CharacterName
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Send eligible bag items:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mc send
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful commands:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mc
|
||||||
|
/mc stop
|
||||||
|
/mc subject Auto mail
|
||||||
|
/mc dryrun on
|
||||||
|
/mc dryrun off
|
||||||
|
/mc black add 6948
|
||||||
|
/mc black del 6948
|
||||||
|
```
|
||||||
|
|
||||||
|
The addon skips soulbound items, quest items, containers, locked items, and blacklisted item IDs. Hearthstone item ID `6948` and Skinning Knife item ID `7005` are blacklisted by default.
|
||||||
@@ -256,6 +256,7 @@ class AutoBotMove:
|
|||||||
turn_error_key=None,
|
turn_error_key=None,
|
||||||
turn_error_hold_sec=None,
|
turn_error_hold_sec=None,
|
||||||
distance_interact_pause_sec=None,
|
distance_interact_pause_sec=None,
|
||||||
|
mailbox_route_path=None,
|
||||||
):
|
):
|
||||||
self.last_tab_time = 0
|
self.last_tab_time = 0
|
||||||
self.last_interaction_time = 0 # 记录上一次按互动键的时间
|
self.last_interaction_time = 0 # 记录上一次按互动键的时间
|
||||||
@@ -324,6 +325,17 @@ class AutoBotMove:
|
|||||||
self.logistics_manager = LogisticsManager(vendor_file)
|
self.logistics_manager = LogisticsManager(vendor_file)
|
||||||
self.logistics_manager.bag_full_hearthstone = bool(layout.get("bag_full_hearthstone", False))
|
self.logistics_manager.bag_full_hearthstone = bool(layout.get("bag_full_hearthstone", False))
|
||||||
self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b")
|
self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b")
|
||||||
|
self.logistics_manager.enable_bag_full_mail = bool(layout.get("enable_bag_full_mail", False))
|
||||||
|
self.logistics_manager.mailbox_route_file = str(
|
||||||
|
mailbox_route_path
|
||||||
|
or layout.get("mailbox_route_json", os.path.join("recorder", "mailbox.json"))
|
||||||
|
or os.path.join("recorder", "mailbox.json")
|
||||||
|
)
|
||||||
|
self.logistics_manager.mailbox_interact_key = str(layout.get("mailbox_interact_key", "8") or "8")
|
||||||
|
self.logistics_manager.mail_recipient_key = str(layout.get("mail_recipient_key", "") or "")
|
||||||
|
self.logistics_manager.mail_send_key = str(layout.get("mail_send_key", "f8") or "f8")
|
||||||
|
self.logistics_manager.mailbox_open_wait_sec = float(layout.get("mailbox_open_wait_sec", 2.0))
|
||||||
|
self.logistics_manager.mail_send_wait_sec = float(layout.get("mail_send_wait_sec", 60.0))
|
||||||
|
|
||||||
def _has_prepare_route(self) -> bool:
|
def _has_prepare_route(self) -> bool:
|
||||||
return bool(self.prepare_route_waypoints)
|
return bool(self.prepare_route_waypoints)
|
||||||
@@ -721,8 +733,19 @@ class AutoBotMove:
|
|||||||
self.patrol_controller.stop_all()
|
self.patrol_controller.stop_all()
|
||||||
self.is_moving = False
|
self.is_moving = False
|
||||||
self.patrol_controller.reset_stuck()
|
self.patrol_controller.reset_stuck()
|
||||||
# 勾选"包满炉石回城":按炉石后触发停止回调
|
bag_full_now = int(state.get('free_slots', 0) or 0) < 2
|
||||||
if self.logistics_manager.bag_full_hearthstone:
|
if bag_full_now and self.logistics_manager.enable_bag_full_mail:
|
||||||
|
get_state_fn = (lambda: None if self._should_stop() else parse_game_state())
|
||||||
|
self.logistics_manager.run_bag_full_mail_flow(
|
||||||
|
get_state_fn,
|
||||||
|
self.patrol_controller,
|
||||||
|
stop_check=self._should_stop,
|
||||||
|
)
|
||||||
|
if callable(getattr(self, '_on_hearthstone_stop', None)):
|
||||||
|
self._on_hearthstone_stop()
|
||||||
|
return
|
||||||
|
# 勾选"包满炉石回城":只有真正包满时才炉石;耐久低仍走修理路线
|
||||||
|
if bag_full_now and self.logistics_manager.bag_full_hearthstone:
|
||||||
get_state_fn = (lambda: None if self._should_stop() else parse_game_state())
|
get_state_fn = (lambda: None if self._should_stop() else parse_game_state())
|
||||||
self.logistics_manager.use_hearthstone_and_stop(get_state=get_state_fn)
|
self.logistics_manager.use_hearthstone_and_stop(get_state=get_state_fn)
|
||||||
if callable(getattr(self, '_on_hearthstone_stop', None)):
|
if callable(getattr(self, '_on_hearthstone_stop', None)):
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ _DEFAULTS = {
|
|||||||
# 炉石回城
|
# 炉石回城
|
||||||
"hearthstone_key": "b",
|
"hearthstone_key": "b",
|
||||||
"bag_full_hearthstone": False,
|
"bag_full_hearthstone": False,
|
||||||
|
# 包满炉石后跑邮箱,并触发 MailboxCourier 插件宏
|
||||||
|
"enable_bag_full_mail": False,
|
||||||
|
"mailbox_route_json": "recorder/mailbox.json",
|
||||||
|
"mailbox_interact_key": "8",
|
||||||
|
"mail_recipient_key": "",
|
||||||
|
"mail_send_key": "f8",
|
||||||
|
"mailbox_open_wait_sec": 2.0,
|
||||||
|
"mail_send_wait_sec": 60.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
SCREENSHOT_DIR = 'screenshot'
|
SCREENSHOT_DIR = 'screenshot'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
from hardware_control import hw_ctrl
|
from hardware_control import hw_ctrl
|
||||||
|
|
||||||
@@ -20,6 +21,32 @@ class LogisticsManager:
|
|||||||
self.bag_full_hearthstone = False # 包满时用炉石回城而非走路修理
|
self.bag_full_hearthstone = False # 包满时用炉石回城而非走路修理
|
||||||
self.hearthstone_key = "b" # 炉石按键
|
self.hearthstone_key = "b" # 炉石按键
|
||||||
self.hearthstone_cast_sec = 10.0 # 炉石施法等待秒数
|
self.hearthstone_cast_sec = 10.0 # 炉石施法等待秒数
|
||||||
|
self.enable_bag_full_mail = False
|
||||||
|
self.mailbox_route_file = os.path.join("recorder", "mailbox.json")
|
||||||
|
self.mailbox_interact_key = "8"
|
||||||
|
self.mail_recipient_key = ""
|
||||||
|
self.mail_send_key = "f8"
|
||||||
|
self.mailbox_open_wait_sec = 2.0
|
||||||
|
self.mail_send_wait_sec = 60.0
|
||||||
|
|
||||||
|
def _resolve_path(self, path):
|
||||||
|
if not path:
|
||||||
|
return ""
|
||||||
|
path = str(path)
|
||||||
|
if os.path.isabs(path):
|
||||||
|
return path
|
||||||
|
cwd_path = os.path.abspath(path)
|
||||||
|
if os.path.exists(cwd_path):
|
||||||
|
return cwd_path
|
||||||
|
return os.path.join(os.path.dirname(os.path.abspath(__file__)), path)
|
||||||
|
|
||||||
|
def _sleep_with_stop(self, seconds, stop_check=None):
|
||||||
|
end_at = time.time() + max(0.0, float(seconds))
|
||||||
|
while time.time() < end_at:
|
||||||
|
if callable(stop_check) and stop_check():
|
||||||
|
return False
|
||||||
|
time.sleep(min(0.1, end_at - time.time()))
|
||||||
|
return True
|
||||||
|
|
||||||
def check_logistics(self, state):
|
def check_logistics(self, state):
|
||||||
"""
|
"""
|
||||||
@@ -109,6 +136,83 @@ class LogisticsManager:
|
|||||||
hw_ctrl.press("4")
|
hw_ctrl.press("4")
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
|
|
||||||
|
def run_bag_full_mail_flow(self, get_state, patrol, stop_check=None):
|
||||||
|
"""
|
||||||
|
包满邮寄第一版流程:
|
||||||
|
炉石 -> 跑到邮箱路线终点 -> 交互打开邮箱 -> 按 MailboxCourier 宏键 -> 等待后停止。
|
||||||
|
Python 不判断邮件是否发完,发送细节交给游戏内插件。
|
||||||
|
"""
|
||||||
|
route_file = self._resolve_path(self.mailbox_route_file)
|
||||||
|
if not route_file or not os.path.exists(route_file):
|
||||||
|
print(f">>> [后勤-邮箱] 邮箱路线不存在,已停止: {route_file}")
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
if callable(stop_check) and stop_check():
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
ok = self.use_hearthstone_and_stop(get_state=get_state)
|
||||||
|
if not ok:
|
||||||
|
print(">>> [后勤-邮箱] 炉石失败,未继续跑邮箱。")
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(route_file, "r", encoding="utf-8") as f:
|
||||||
|
path = json.load(f)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f">>> [后勤-邮箱] 邮箱路线读取失败: {exc}")
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not path:
|
||||||
|
print(f">>> [后勤-邮箱] 邮箱路线为空,已停止: {route_file}")
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f">>> [后勤-邮箱] 开始跑邮箱路线: {route_file}")
|
||||||
|
old_enable_mount = getattr(patrol, "enable_mount", None)
|
||||||
|
if old_enable_mount is not None:
|
||||||
|
patrol.enable_mount = False
|
||||||
|
try:
|
||||||
|
ok = patrol.navigate_path(get_state, path, forward=True, arrival_threshold=VENDOR_ARRIVAL_THRESHOLD)
|
||||||
|
finally:
|
||||||
|
if old_enable_mount is not None:
|
||||||
|
patrol.enable_mount = old_enable_mount
|
||||||
|
if not ok:
|
||||||
|
print(">>> [后勤-邮箱] 邮箱路线未完成,已停止。")
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
if callable(stop_check) and stop_check():
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
patrol.stop_all()
|
||||||
|
print(f">>> [后勤-邮箱] 到达邮箱附近,按交互键: {self.mailbox_interact_key}")
|
||||||
|
hw_ctrl.press(self.mailbox_interact_key)
|
||||||
|
if not self._sleep_with_stop(self.mailbox_open_wait_sec, stop_check=stop_check):
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.mail_recipient_key:
|
||||||
|
print(f">>> [后勤-邮箱] 触发 MailboxCourier 收件人宏: {self.mail_recipient_key}")
|
||||||
|
hw_ctrl.press(self.mail_recipient_key)
|
||||||
|
if not self._sleep_with_stop(0.5, stop_check=stop_check):
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(f">>> [后勤-邮箱] 触发 MailboxCourier 发送宏: {self.mail_send_key}")
|
||||||
|
hw_ctrl.press(self.mail_send_key)
|
||||||
|
if not self._sleep_with_stop(self.mail_send_wait_sec, stop_check=stop_check):
|
||||||
|
self.is_returning = False
|
||||||
|
return False
|
||||||
|
|
||||||
|
print(">>> [后勤-邮箱] 邮寄宏已触发,流程结束。")
|
||||||
|
self.is_returning = False
|
||||||
|
return True
|
||||||
|
|
||||||
def run_route1_round(self, get_state, patrol, route_file=None):
|
def run_route1_round(self, get_state, patrol, route_file=None):
|
||||||
"""
|
"""
|
||||||
读取 route1.json 路径,先正向走完,执行交互(8、4),再反向走完,然后结束。
|
读取 route1.json 路径,先正向走完,执行交互(8、4),再反向走完,然后结束。
|
||||||
|
|||||||
@@ -305,6 +305,7 @@ class GameLoopWorker(QThread):
|
|||||||
waypoints_path=None,
|
waypoints_path=None,
|
||||||
prepare_route_path=None,
|
prepare_route_path=None,
|
||||||
vendor_path=None,
|
vendor_path=None,
|
||||||
|
mailbox_route_path=None,
|
||||||
record_filename=None,
|
record_filename=None,
|
||||||
record_min_distance=None,
|
record_min_distance=None,
|
||||||
attack_loop_path=None,
|
attack_loop_path=None,
|
||||||
@@ -342,6 +343,7 @@ class GameLoopWorker(QThread):
|
|||||||
self.waypoints_path = waypoints_path
|
self.waypoints_path = waypoints_path
|
||||||
self.prepare_route_path = prepare_route_path
|
self.prepare_route_path = prepare_route_path
|
||||||
self.vendor_path = vendor_path
|
self.vendor_path = vendor_path
|
||||||
|
self.mailbox_route_path = mailbox_route_path
|
||||||
self.record_filename = record_filename or 'waypoints'
|
self.record_filename = record_filename or 'waypoints'
|
||||||
self.record_min_distance = record_min_distance
|
self.record_min_distance = record_min_distance
|
||||||
self.attack_loop_path = attack_loop_path or None
|
self.attack_loop_path = attack_loop_path or None
|
||||||
@@ -422,6 +424,7 @@ class GameLoopWorker(QThread):
|
|||||||
turn_error_key=self.turn_error_key,
|
turn_error_key=self.turn_error_key,
|
||||||
turn_error_hold_sec=self.turn_error_hold_sec,
|
turn_error_hold_sec=self.turn_error_hold_sec,
|
||||||
distance_interact_pause_sec=self.distance_interact_pause_sec,
|
distance_interact_pause_sec=self.distance_interact_pause_sec,
|
||||||
|
mailbox_route_path=self.mailbox_route_path,
|
||||||
)
|
)
|
||||||
self.bot_move._on_hearthstone_stop = self.stop_signal.emit
|
self.bot_move._on_hearthstone_stop = self.stop_signal.emit
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
@@ -723,6 +726,8 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
self.waypoints_combo.setMinimumWidth(200)
|
self.waypoints_combo.setMinimumWidth(200)
|
||||||
self.vendor_combo = QComboBox()
|
self.vendor_combo = QComboBox()
|
||||||
self.vendor_combo.setMinimumWidth(200)
|
self.vendor_combo.setMinimumWidth(200)
|
||||||
|
self.mailbox_route_combo = QComboBox()
|
||||||
|
self.mailbox_route_combo.setMinimumWidth(200)
|
||||||
self.patrol_attack_loop_combo = QComboBox()
|
self.patrol_attack_loop_combo = QComboBox()
|
||||||
self.patrol_attack_loop_combo.setMinimumWidth(200)
|
self.patrol_attack_loop_combo.setMinimumWidth(200)
|
||||||
self.prepare_route_combo = QComboBox()
|
self.prepare_route_combo = QComboBox()
|
||||||
@@ -733,6 +738,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
patrol_layout.addRow("准备路线 JSON:", self.prepare_route_combo)
|
patrol_layout.addRow("准备路线 JSON:", self.prepare_route_combo)
|
||||||
patrol_layout.addRow("巡逻点 JSON:", self.waypoints_combo)
|
patrol_layout.addRow("巡逻点 JSON:", self.waypoints_combo)
|
||||||
patrol_layout.addRow("修理商 JSON:", self.vendor_combo)
|
patrol_layout.addRow("修理商 JSON:", self.vendor_combo)
|
||||||
|
patrol_layout.addRow("邮箱路线 JSON:", self.mailbox_route_combo)
|
||||||
self.resurrection_route_a_combo = QComboBox()
|
self.resurrection_route_a_combo = QComboBox()
|
||||||
self.resurrection_route_a_combo.setMinimumWidth(200)
|
self.resurrection_route_a_combo.setMinimumWidth(200)
|
||||||
self.resurrection_route_b_combo = QComboBox()
|
self.resurrection_route_b_combo = QComboBox()
|
||||||
@@ -1110,6 +1116,30 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
self.gs_hearthstone_key.setText("b")
|
self.gs_hearthstone_key.setText("b")
|
||||||
self.gs_bag_full_hearthstone = QCheckBox("包满时用炉石回城并停止")
|
self.gs_bag_full_hearthstone = QCheckBox("包满时用炉石回城并停止")
|
||||||
self.gs_bag_full_hearthstone.setChecked(False)
|
self.gs_bag_full_hearthstone.setChecked(False)
|
||||||
|
self.gs_enable_bag_full_mail = QCheckBox("包满炉石后跑邮箱并邮寄")
|
||||||
|
self.gs_enable_bag_full_mail.setChecked(False)
|
||||||
|
self.gs_mailbox_interact_key = QLineEdit()
|
||||||
|
self.gs_mailbox_interact_key.setPlaceholderText("如 8")
|
||||||
|
self.gs_mailbox_interact_key.setMaxLength(16)
|
||||||
|
self.gs_mailbox_interact_key.setText("8")
|
||||||
|
self.gs_mail_recipient_key = QLineEdit()
|
||||||
|
self.gs_mail_recipient_key.setPlaceholderText("如 f7")
|
||||||
|
self.gs_mail_recipient_key.setMaxLength(16)
|
||||||
|
self.gs_mail_recipient_key.setText("")
|
||||||
|
self.gs_mail_send_key = QLineEdit()
|
||||||
|
self.gs_mail_send_key.setPlaceholderText("如 f8")
|
||||||
|
self.gs_mail_send_key.setMaxLength(16)
|
||||||
|
self.gs_mail_send_key.setText("f8")
|
||||||
|
self.gs_mailbox_open_wait = QDoubleSpinBox()
|
||||||
|
self.gs_mailbox_open_wait.setRange(0.1, 30.0)
|
||||||
|
self.gs_mailbox_open_wait.setSingleStep(0.1)
|
||||||
|
self.gs_mailbox_open_wait.setValue(2.0)
|
||||||
|
self.gs_mailbox_open_wait.setSuffix(" 秒")
|
||||||
|
self.gs_mail_send_wait = QDoubleSpinBox()
|
||||||
|
self.gs_mail_send_wait.setRange(0.0, 600.0)
|
||||||
|
self.gs_mail_send_wait.setSingleStep(1.0)
|
||||||
|
self.gs_mail_send_wait.setValue(60.0)
|
||||||
|
self.gs_mail_send_wait.setSuffix(" 秒")
|
||||||
self.gs_enable_mount = QCheckBox("启用上马")
|
self.gs_enable_mount = QCheckBox("启用上马")
|
||||||
self.gs_enable_mount.setChecked(True)
|
self.gs_enable_mount.setChecked(True)
|
||||||
self.gs_mount_key = QLineEdit()
|
self.gs_mount_key = QLineEdit()
|
||||||
@@ -1175,7 +1205,18 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
game_grid.addWidget(self.gs_use_hardware_input, 8, 0, 1, 2)
|
game_grid.addWidget(self.gs_use_hardware_input, 8, 0, 1, 2)
|
||||||
game_grid.addWidget(QLabel("复活按键:"), 6, 2)
|
game_grid.addWidget(QLabel("复活按键:"), 6, 2)
|
||||||
game_grid.addWidget(self.gs_resurrect_key, 6, 3)
|
game_grid.addWidget(self.gs_resurrect_key, 6, 3)
|
||||||
game_grid.addWidget(self.gs_bag_full_hearthstone, 9, 1)
|
game_grid.addWidget(self.gs_bag_full_hearthstone, 9, 0, 1, 2)
|
||||||
|
game_grid.addWidget(self.gs_enable_bag_full_mail, 9, 2, 1, 2)
|
||||||
|
game_grid.addWidget(QLabel("邮箱交互键:"), 10, 0)
|
||||||
|
game_grid.addWidget(self.gs_mailbox_interact_key, 10, 1)
|
||||||
|
game_grid.addWidget(QLabel("收信人按键:"), 10, 2)
|
||||||
|
game_grid.addWidget(self.gs_mail_recipient_key, 10, 3)
|
||||||
|
game_grid.addWidget(QLabel("邮寄宏按键:"), 11, 0)
|
||||||
|
game_grid.addWidget(self.gs_mail_send_key, 11, 1)
|
||||||
|
game_grid.addWidget(QLabel("邮箱打开等待:"), 11, 2)
|
||||||
|
game_grid.addWidget(self.gs_mailbox_open_wait, 11, 3)
|
||||||
|
game_grid.addWidget(QLabel("邮寄后等待:"), 12, 0)
|
||||||
|
game_grid.addWidget(self.gs_mail_send_wait, 12, 1)
|
||||||
|
|
||||||
params_layout.addWidget(game_group)
|
params_layout.addWidget(game_group)
|
||||||
|
|
||||||
@@ -1211,10 +1252,17 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
self.gs_scan_height.setValue(cfg.get('scan_region_height', 15))
|
self.gs_scan_height.setValue(cfg.get('scan_region_height', 15))
|
||||||
self.gs_offset_left.setValue(cfg.get('offset_left', 20))
|
self.gs_offset_left.setValue(cfg.get('offset_left', 20))
|
||||||
self.gs_offset_top.setValue(cfg.get('offset_top', 45))
|
self.gs_offset_top.setValue(cfg.get('offset_top', 45))
|
||||||
|
self.gs_enable_mount.setChecked(bool(cfg.get('enable_mount', True)))
|
||||||
self.gs_mount_key.setText(str(cfg.get('mount_key', 'x') or 'x'))
|
self.gs_mount_key.setText(str(cfg.get('mount_key', 'x') or 'x'))
|
||||||
self.gs_mount_hold.setValue(float(cfg.get('mount_hold_sec', 1.6)))
|
self.gs_mount_hold.setValue(float(cfg.get('mount_hold_sec', 1.6)))
|
||||||
self.gs_hearthstone_key.setText(str(cfg.get('hearthstone_key', 'b') or 'b'))
|
self.gs_hearthstone_key.setText(str(cfg.get('hearthstone_key', 'b') or 'b'))
|
||||||
self.gs_bag_full_hearthstone.setChecked(bool(cfg.get('bag_full_hearthstone', False)))
|
self.gs_bag_full_hearthstone.setChecked(bool(cfg.get('bag_full_hearthstone', False)))
|
||||||
|
self.gs_enable_bag_full_mail.setChecked(bool(cfg.get('enable_bag_full_mail', False)))
|
||||||
|
self.gs_mailbox_interact_key.setText(str(cfg.get('mailbox_interact_key', '8') or '8'))
|
||||||
|
self.gs_mail_recipient_key.setText(str(cfg.get('mail_recipient_key', '') or ''))
|
||||||
|
self.gs_mail_send_key.setText(str(cfg.get('mail_send_key', 'f8') or 'f8'))
|
||||||
|
self.gs_mailbox_open_wait.setValue(float(cfg.get('mailbox_open_wait_sec', 2.0)))
|
||||||
|
self.gs_mail_send_wait.setValue(float(cfg.get('mail_send_wait_sec', 60.0)))
|
||||||
self.gs_mount_retry.setValue(float(cfg.get('mount_retry_after_sec', 2.0)))
|
self.gs_mount_retry.setValue(float(cfg.get('mount_retry_after_sec', 2.0)))
|
||||||
self.gs_release_spirit_key.setText(str(cfg.get('release_spirit_key', '9') or '9'))
|
self.gs_release_spirit_key.setText(str(cfg.get('release_spirit_key', '9') or '9'))
|
||||||
self.gs_resurrect_key.setText(str(cfg.get('resurrect_key', '0') or '0'))
|
self.gs_resurrect_key.setText(str(cfg.get('resurrect_key', '0') or '0'))
|
||||||
@@ -1254,11 +1302,18 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
cfg['scan_region_height'] = self.gs_scan_height.value()
|
cfg['scan_region_height'] = self.gs_scan_height.value()
|
||||||
cfg['offset_left'] = self.gs_offset_left.value()
|
cfg['offset_left'] = self.gs_offset_left.value()
|
||||||
cfg['offset_top'] = self.gs_offset_top.value()
|
cfg['offset_top'] = self.gs_offset_top.value()
|
||||||
|
cfg['enable_mount'] = self.gs_enable_mount.isChecked()
|
||||||
cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x')
|
cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x')
|
||||||
cfg['mount_hold_sec'] = float(self.gs_mount_hold.value())
|
cfg['mount_hold_sec'] = float(self.gs_mount_hold.value())
|
||||||
cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value())
|
cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value())
|
||||||
cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b')
|
cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b')
|
||||||
cfg['bag_full_hearthstone'] = self.gs_bag_full_hearthstone.isChecked()
|
cfg['bag_full_hearthstone'] = self.gs_bag_full_hearthstone.isChecked()
|
||||||
|
cfg['enable_bag_full_mail'] = self.gs_enable_bag_full_mail.isChecked()
|
||||||
|
cfg['mailbox_interact_key'] = self.gs_mailbox_interact_key.text().strip() or '8'
|
||||||
|
cfg['mail_recipient_key'] = self.gs_mail_recipient_key.text().strip()
|
||||||
|
cfg['mail_send_key'] = self.gs_mail_send_key.text().strip() or 'f8'
|
||||||
|
cfg['mailbox_open_wait_sec'] = float(self.gs_mailbox_open_wait.value())
|
||||||
|
cfg['mail_send_wait_sec'] = float(self.gs_mail_send_wait.value())
|
||||||
cfg['release_spirit_key'] = (self.gs_release_spirit_key.text().strip() or '9')
|
cfg['release_spirit_key'] = (self.gs_release_spirit_key.text().strip() or '9')
|
||||||
cfg['resurrect_key'] = (self.gs_resurrect_key.text().strip() or '0')
|
cfg['resurrect_key'] = (self.gs_resurrect_key.text().strip() or '0')
|
||||||
path = save_layout_config(cfg)
|
path = save_layout_config(cfg)
|
||||||
@@ -1471,6 +1526,16 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
]
|
]
|
||||||
if hasattr(self, "repair_vendor_combo"):
|
if hasattr(self, "repair_vendor_combo"):
|
||||||
combos_with_default.append((self.repair_vendor_combo, 'vendor.json'))
|
combos_with_default.append((self.repair_vendor_combo, 'vendor.json'))
|
||||||
|
if hasattr(self, "mailbox_route_combo"):
|
||||||
|
self.mailbox_route_combo.blockSignals(True)
|
||||||
|
self.mailbox_route_combo.clear()
|
||||||
|
self.mailbox_route_combo.addItem("-- 置空(不跑邮箱路线) --", "")
|
||||||
|
for name, path in items:
|
||||||
|
self.mailbox_route_combo.addItem(name, path)
|
||||||
|
idx = self.mailbox_route_combo.findData(os.path.join(get_recorder_dir(), 'mailbox.json'))
|
||||||
|
if idx >= 0:
|
||||||
|
self.mailbox_route_combo.setCurrentIndex(idx)
|
||||||
|
self.mailbox_route_combo.blockSignals(False)
|
||||||
for combo, default_name in combos_with_default:
|
for combo, default_name in combos_with_default:
|
||||||
combo.blockSignals(True)
|
combo.blockSignals(True)
|
||||||
combo.clear()
|
combo.clear()
|
||||||
@@ -1635,6 +1700,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
waypoints_path = None
|
waypoints_path = None
|
||||||
prepare_route_path = None
|
prepare_route_path = None
|
||||||
vendor_path = None
|
vendor_path = None
|
||||||
|
mailbox_route_path = None
|
||||||
flight_json_path = None
|
flight_json_path = None
|
||||||
resurrection_route_a_path = None
|
resurrection_route_a_path = None
|
||||||
resurrection_route_b_path = None
|
resurrection_route_b_path = None
|
||||||
@@ -1642,6 +1708,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
prep = self.prepare_route_combo.currentData() or ""
|
prep = self.prepare_route_combo.currentData() or ""
|
||||||
wp = self.waypoints_combo.currentData() or ""
|
wp = self.waypoints_combo.currentData() or ""
|
||||||
vp = self.vendor_combo.currentData() or ""
|
vp = self.vendor_combo.currentData() or ""
|
||||||
|
mp = self.mailbox_route_combo.currentData() or ""
|
||||||
route_a = self.resurrection_route_a_combo.currentData() or ""
|
route_a = self.resurrection_route_a_combo.currentData() or ""
|
||||||
route_b = self.resurrection_route_b_combo.currentData() or ""
|
route_b = self.resurrection_route_b_combo.currentData() or ""
|
||||||
if not wp:
|
if not wp:
|
||||||
@@ -1659,6 +1726,13 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
if prep and not os.path.exists(prep):
|
if prep and not os.path.exists(prep):
|
||||||
QMessageBox.warning(self, "提示", f"准备路线文件不存在: {prep}")
|
QMessageBox.warning(self, "提示", f"准备路线文件不存在: {prep}")
|
||||||
return
|
return
|
||||||
|
if self.gs_enable_bag_full_mail.isChecked():
|
||||||
|
if not mp:
|
||||||
|
QMessageBox.warning(self, "提示", "已启用包满邮寄,请选择邮箱路线 JSON 文件")
|
||||||
|
return
|
||||||
|
if not os.path.exists(mp):
|
||||||
|
QMessageBox.warning(self, "提示", f"邮箱路线文件不存在: {mp}")
|
||||||
|
return
|
||||||
if route_a and not os.path.exists(route_a):
|
if route_a and not os.path.exists(route_a):
|
||||||
QMessageBox.warning(self, "提示", f"复活路线 A 文件不存在: {route_a}")
|
QMessageBox.warning(self, "提示", f"复活路线 A 文件不存在: {route_a}")
|
||||||
return
|
return
|
||||||
@@ -1668,6 +1742,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
waypoints_path = wp
|
waypoints_path = wp
|
||||||
prepare_route_path = prep or None
|
prepare_route_path = prep or None
|
||||||
vendor_path = vp
|
vendor_path = vp
|
||||||
|
mailbox_route_path = mp or None
|
||||||
resurrection_route_a_path = route_a or None
|
resurrection_route_a_path = route_a or None
|
||||||
resurrection_route_b_path = route_b or None
|
resurrection_route_b_path = route_b or None
|
||||||
attack_loop_path = None
|
attack_loop_path = None
|
||||||
@@ -1753,6 +1828,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
|||||||
|
|
||||||
self.game_worker = GameLoopWorker(
|
self.game_worker = GameLoopWorker(
|
||||||
mode, waypoints_path=waypoints_path, prepare_route_path=prepare_route_path, vendor_path=vendor_path,
|
mode, waypoints_path=waypoints_path, prepare_route_path=prepare_route_path, vendor_path=vendor_path,
|
||||||
|
mailbox_route_path=mailbox_route_path,
|
||||||
attack_loop_path=attack_loop_path,
|
attack_loop_path=attack_loop_path,
|
||||||
skinning_wait_sec=skinning_wait_sec,
|
skinning_wait_sec=skinning_wait_sec,
|
||||||
food_key=food_key,
|
food_key=food_key,
|
||||||
|
|||||||
Reference in New Issue
Block a user