diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index 76812c5..7e5122f 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -18,6 +18,8 @@ local OUTFIT_CAPABILITY = "OUTFIT_V1" local INVENTORY_CAPABILITY = "INVENTORY_V1" local INVENTORY_EXACT_CAPABILITY = "INVENTORY_EXACT_V1" local INVENTORY_ITEM_MOVE_CAPABILITY = "ITEM_MOVE_V1" +local INVENTORY_ITEM_TRADE_CAPABILITY = "ITEM_TRADE_V1" +local INVENTORY_ITEM_DEPOSIT_EXACT_CAPABILITY = "ITEM_DEPOSIT_EXACT_V1" local INVENTORY_ITEM_EQUIP_CAPABILITY = "ITEM_EQUIP_V1" local INVENTORY_ITEM_UNEQUIP_CAPABILITY = "ITEM_UNEQUIP_V1" local INVENTORY_ITEM_DESTROY_CAPABILITY = "ITEM_DESTROY_V1" @@ -27,10 +29,56 @@ local INVENTORY_BULK_SELL_CAPABILITY = "INVENTORY_BULK_SELL_V1" local INVENTORY_OPEN_CAPABILITY = "INVENTORY_OPEN_V1" local GROUP_ROLL_CAPABILITY = "GROUP_ROLL_V1" local ENCHANT_TRADE_CAPABILITY = "ENCHANT_TRADE_V1" +local QUEST_ABANDON_CAPABILITY = "QUEST_ABANDON_V1" +local TALENT_APPLY_CAPABILITY = "TALENT_APPLY_V1" +local TALENT_SPEC_APPLY_CAPABILITY = "TALENT_SPEC_APPLY_V1" +local CRAFT_RECIPE_TARGET_CAPABILITY = "CRAFT_RECIPE_TARGET_V1" +local LOOT_RULE_ITEM_CAPABILITY = "LOOT_RULE_ITEM_V1" +-- MB_LUA51_UPVALUE_REFACTOR_V1_BEGIN +-- Keep capability-to-state mapping outside Comm.HandleAddonMessage so each +-- capability does not consume a separate Lua 5.1 upvalue in that dispatcher. +local CAPABILITY_STATE_FIELDS = { + [STATE_FRAMING_CAPABILITY] = "stateFramingCapable", + [STRATEGY_MUTATION_CAPABILITY] = "strategyMutationCapable", + ["SELF_STRATEGY_V1"] = "selfStrategyCapable", + [SELF_ACTION_CAPABILITY] = "selfActionCapable", + [OUTFIT_CAPABILITY] = "outfitCapable", + [INVENTORY_CAPABILITY] = "inventoryCapable", + [INVENTORY_EXACT_CAPABILITY] = "inventoryExactCapable", + [INVENTORY_ITEM_MOVE_CAPABILITY] = "inventoryItemMoveCapable", + [INVENTORY_ITEM_TRADE_CAPABILITY] = "inventoryItemTradeCapable", + [INVENTORY_ITEM_DEPOSIT_EXACT_CAPABILITY] = "inventoryItemDepositExactCapable", + [INVENTORY_ITEM_EQUIP_CAPABILITY] = "inventoryItemEquipCapable", + [INVENTORY_ITEM_UNEQUIP_CAPABILITY] = "inventoryItemUnequipCapable", + [INVENTORY_ITEM_DESTROY_CAPABILITY] = "inventoryItemDestroyCapable", + [INVENTORY_ITEM_USE_CAPABILITY] = "inventoryItemUseCapable", + [INVENTORY_ITEM_SELL_CAPABILITY] = "inventoryItemSellCapable", + ["VENDOR_BUYBACK_V1"] = "inventoryBuybackCapable", + [INVENTORY_BULK_SELL_CAPABILITY] = "inventoryBulkSellCapable", + [INVENTORY_OPEN_CAPABILITY] = "inventoryOpenCapable", + [GROUP_ROLL_CAPABILITY] = "groupRollCapable", + [ENCHANT_TRADE_CAPABILITY] = "enchantTradeCapable", + [QUEST_ABANDON_CAPABILITY] = "questAbandonCapable", + [TALENT_APPLY_CAPABILITY] = "talentApplyCapable", + [TALENT_SPEC_APPLY_CAPABILITY] = "talentSpecApplyCapable", + [CRAFT_RECIPE_TARGET_CAPABILITY] = "craftRecipeTargetCapable", + [LOOT_RULE_ITEM_CAPABILITY] = "lootRuleItemCapable", + ["SELF_BOT_V1"] = "selfBotCapable", +} +-- MB_LUA51_UPVALUE_REFACTOR_V1_END local SELF_BOT_TIMEOUT_SECONDS = 5.0 local GROUP_ROLL_TIMEOUT_SECONDS = 5.0 local ENCHANT_TRADE_TIMEOUT_SECONDS = 5.0 +local QUEST_ABANDON_TIMEOUT_SECONDS = 5.0 +local TALENT_APPLY_TIMEOUT_SECONDS = 5.0 +local TALENT_SPEC_APPLY_TIMEOUT_SECONDS = 5.0 +local CRAFT_RECIPE_TARGET_TIMEOUT_SECONDS = 5.0 +local CRAFT_RECIPE_TARGET_MAX_ACTIVE = 8 +local LOOT_RULE_ITEM_TIMEOUT_SECONDS = 5.0 +local LOOT_RULE_ITEM_MAX_ACTIVE = 32 local INVENTORY_ITEM_MOVE_TIMEOUT_SECONDS = 5.0 +local INVENTORY_ITEM_TRADE_TIMEOUT_SECONDS = 5.0 +local INVENTORY_ITEM_DEPOSIT_EXACT_TIMEOUT_SECONDS = 5.0 local INVENTORY_ITEM_EQUIP_TIMEOUT_SECONDS = 5.0 local INVENTORY_ITEM_UNEQUIP_TIMEOUT_SECONDS = 5.0 local INVENTORY_ITEM_DESTROY_TIMEOUT_SECONDS = 5.0 @@ -38,6 +86,8 @@ local INVENTORY_ITEM_USE_TIMEOUT_SECONDS = 5.0 local INVENTORY_ITEM_SELL_TIMEOUT_SECONDS = 5.0 local INVENTORY_BUYBACK_TIMEOUT_SECONDS = 5.0 local INVENTORY_ITEM_MOVE_MAX_COUNT = 1000 +local INVENTORY_ITEM_TRADE_MAX_COUNT = 1000 +local INVENTORY_ITEM_DEPOSIT_EXACT_MAX_COUNT = 1000 local INVENTORY_ITEM_EQUIP_MAX_COUNT = 1000 local INVENTORY_ITEM_DESTROY_MAX_COUNT = 1000 local INVENTORY_ITEM_USE_MAX_COUNT = 1000 @@ -281,6 +331,8 @@ local function ensureBridgeState() state.inventoryCapable = state.inventoryCapable or false state.inventoryExactCapable = state.inventoryExactCapable or false state.inventoryItemMoveCapable = state.inventoryItemMoveCapable or false + state.inventoryItemTradeCapable = state.inventoryItemTradeCapable or false + state.inventoryItemDepositExactCapable = state.inventoryItemDepositExactCapable or false state.inventoryItemEquipCapable = state.inventoryItemEquipCapable or false state.inventoryItemUnequipCapable = state.inventoryItemUnequipCapable or false state.inventoryItemDestroyCapable = state.inventoryItemDestroyCapable or false @@ -291,6 +343,11 @@ local function ensureBridgeState() state.inventoryOpenCapable = state.inventoryOpenCapable or false state.groupRollCapable = state.groupRollCapable or false state.enchantTradeCapable = state.enchantTradeCapable or false + state.questAbandonCapable = state.questAbandonCapable or false + state.talentApplyCapable = state.talentApplyCapable or false + state.talentSpecApplyCapable = state.talentSpecApplyCapable or false + state.craftRecipeTargetCapable = state.craftRecipeTargetCapable or false + state.lootRuleItemCapable = state.lootRuleItemCapable or false state.selfBotCapable = state.selfBotCapable or false state.selfBotStateSeq = state.selfBotStateSeq or 0 state.selfBotStateActive = state.selfBotStateActive or nil @@ -305,6 +362,12 @@ local function ensureBridgeState() state.enchantTradeLists = state.enchantTradeLists or {} state.groupRollSeq = state.groupRollSeq or 0 state.groupRollCommands = state.groupRollCommands or {} + state.questAbandonSeq = state.questAbandonSeq or 0 + state.questAbandonCommands = state.questAbandonCommands or {} + state.talentApplySeq = state.talentApplySeq or 0 + state.talentApplyCommands = state.talentApplyCommands or {} + state.talentSpecApplySeq = state.talentSpecApplySeq or 0 + state.talentSpecApplyCommands = state.talentSpecApplyCommands or {} state.strategyMutationSeq = state.strategyMutationSeq or 0 state.strategyMutationCommands = state.strategyMutationCommands or {} state.selfStrategySeq = state.selfStrategySeq or 0 @@ -334,6 +397,10 @@ local function ensureBridgeState() state.inventoryExactSnapshots = state.inventoryExactSnapshots or {} state.inventoryItemMoveSeq = state.inventoryItemMoveSeq or 0 state.inventoryItemMoves = state.inventoryItemMoves or {} + state.inventoryItemTradeSeq = state.inventoryItemTradeSeq or 0 + state.inventoryItemTrades = state.inventoryItemTrades or {} + state.inventoryItemDepositExactSeq = state.inventoryItemDepositExactSeq or 0 + state.inventoryItemDepositExacts = state.inventoryItemDepositExacts or {} state.inventoryItemEquipSeq = state.inventoryItemEquipSeq or 0 state.inventoryItemEquips = state.inventoryItemEquips or {} state.inventoryItemUnequipSeq = state.inventoryItemUnequipSeq or 0 @@ -373,6 +440,8 @@ local function ensureBridgeState() state.professionRecipeActive = state.professionRecipeActive or nil state.professionRecipeCraftSeq = state.professionRecipeCraftSeq or 0 state.professionRecipeCrafts = state.professionRecipeCrafts or {} + state.professionRecipeTargetSeq = state.professionRecipeTargetSeq or 0 + state.professionRecipeTargetCommands = state.professionRecipeTargetCommands or {} state.outfitSeq = state.outfitSeq or 0 state.outfitActive = state.outfitActive or nil state.outfitCommands = state.outfitCommands or {} @@ -386,6 +455,8 @@ local function ensureBridgeState() state.rtiSeq = state.rtiSeq or 0 state.combatSeq = state.combatSeq or 0 state.positionSeq = state.positionSeq or 0 + state.lootRuleItemSeq = state.lootRuleItemSeq or 0 + state.lootRuleItemCommands = state.lootRuleItemCommands or {} state.lootSeq = state.lootSeq or 0 state.formationSeq = state.formationSeq or 0 state.formationCommands = state.formationCommands or {} @@ -866,6 +937,8 @@ state.selfActionCapable = false state.inventoryCapable = false state.inventoryExactCapable = false state.inventoryItemMoveCapable = false + state.inventoryItemTradeCapable = false + state.inventoryItemDepositExactCapable = false state.inventoryItemEquipCapable = false state.inventoryItemUnequipCapable = false state.inventoryItemDestroyCapable = false @@ -874,8 +947,13 @@ state.selfActionCapable = false state.inventoryBuybackCapable = false state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false + state.lootRuleItemCapable = false state.groupRollCapable = false state.enchantTradeCapable = false + state.questAbandonCapable = false + state.talentApplyCapable = false + state.talentSpecApplyCapable = false + state.craftRecipeTargetCapable = false state.selfBotCapable = false end @@ -1323,6 +1401,108 @@ function Comm.RunLootCommand(scope, target, command) return Comm.Send("RUN", "LOOT~" .. scope .. "~" .. urlEncodeField(target) .. "~" .. token .. "~" .. urlEncodeField(command)) end +-- MB_LOOT_RULE_ITEM_V1_TX_BEGIN +function Comm.IsLootRuleItemCapable() + local state = ensureBridgeState() + return state.connected == true and state.lootRuleItemCapable == true +end + +function Comm.RunLootRuleItem(scope, target, action, itemId) + local state = ensureBridgeState() + if not state.connected or state.lootRuleItemCapable ~= true then + state.lastError = "LOOT_RULE_ITEM_CAPABILITY_UNAVAILABLE" + return false + end + + scope = string.upper(trim(scope or "ALL")) + target = trim(target or "") + action = string.upper(trim(action or "")) + itemId = parseBoundedInteger(tostring(itemId or ""), 1, 4294967295) + + local validScope = scope == "ALL" or scope == "RAID" or scope == "GROUP" + or scope == "PARTY" or scope == "BOT" + if not validScope then + state.lastError = "LOOT_RULE_ITEM_BAD_SCOPE" + return false + end + if string.len(target) > 64 + or (scope == "BOT" and target == "") + or ((scope == "ALL" or scope == "RAID") and target ~= "") then + state.lastError = "LOOT_RULE_ITEM_BAD_TARGET" + return false + end + if (scope == "GROUP" or scope == "PARTY") and target ~= "" then + local groupNumber = tonumber(target) + if not groupNumber or math.floor(groupNumber) ~= groupNumber or groupNumber < 1 or groupNumber > 8 then + state.lastError = "LOOT_RULE_ITEM_BAD_TARGET" + return false + end + end + if action ~= "ADD" and action ~= "REMOVE" then + state.lastError = "LOOT_RULE_ITEM_BAD_ACTION" + return false + end + if not itemId then + state.lastError = "LOOT_RULE_ITEM_BAD_ITEM" + return false + end + if countTableEntries(state.lootRuleItemCommands) >= LOOT_RULE_ITEM_MAX_ACTIVE then + state.lastError = "LOOT_RULE_ITEM_TOO_MANY_REQUESTS" + return false + end + + local targetKey = string.lower(target) + for _, pending in pairs(state.lootRuleItemCommands) do + if type(pending) == "table" + and pending.scope == scope + and pending.targetKey == targetKey + and pending.action == action + and pending.itemId == itemId then + state.lastError = "LOOT_RULE_ITEM_ALREADY_PENDING" + return false + end + end + + state.lootRuleItemSeq = (tonumber(state.lootRuleItemSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-loot-item-" .. tostring(state.lootRuleItemSeq) + state.lootRuleItemCommands[token] = { + scope = scope, + target = target, + targetKey = targetKey, + action = action, + itemId = itemId, + startedAt = safeNow(), + } + + local payload = table.concat({ + "LOOT_RULE_ITEM", scope, urlEncodeField(target), token, action, tostring(itemId), + }, "~") + if not Comm.Send("RUN", payload) then + state.lootRuleItemCommands[token] = nil + state.lastError = "LOOT_RULE_ITEM_SEND_FAILED" + return false + end + + safeDelay(LOOT_RULE_ITEM_TIMEOUT_SECONDS, function() + local bridge = ensureBridgeState() + local pending = bridge.lootRuleItemCommands[token] + if type(pending) ~= "table" then + return + end + + bridge.lootRuleItemCommands[token] = nil + bridge.lastError = "LOOT_RULE_ITEM_TIMEOUT" + if MultiBot.OnLootRuleItemResult then + MultiBot.OnLootRuleItemResult( + pending.scope, pending.target, pending.action, pending.itemId, + "ERR", "TIMEOUT", 0, 0, pending + ) + end + end) + + return token +end +-- MB_LOOT_RULE_ITEM_V1_TX_END function Comm.RunPositionCommand(scope, target, command) local state = ensureBridgeState() @@ -2270,6 +2450,167 @@ function Comm.RunInventoryItemMove(name, srcBag, srcSlot, srcItemId, srcCount, d return token end +function Comm.IsInventoryItemTradeCapable() + local state = ensureBridgeState() + return state.connected == true and state.inventoryExactCapable == true and state.inventoryItemTradeCapable == true +end + +function Comm.RunInventoryItemTrade(name, srcBag, srcSlot, srcItemId, srcCount) + local state = ensureBridgeState() + name = trim(name) + + srcBag = parseBoundedInteger(tostring(srcBag or ""), 0, 255) + srcSlot = parseBoundedInteger(tostring(srcSlot or ""), 0, 255) + srcItemId = parseBoundedInteger(tostring(srcItemId or ""), 1, 4294967295) + srcCount = parseBoundedInteger(tostring(srcCount or ""), 1, INVENTORY_ITEM_TRADE_MAX_COUNT) + + if name == "" or not state.connected or state.inventoryExactCapable ~= true or state.inventoryItemTradeCapable ~= true then + return false + end + if srcBag == nil or srcSlot == nil or not srcItemId or not srcCount then + return false + end + + local botNameKey = string.lower(name) + for _, pending in pairs(state.inventoryItemTrades or {}) do + if pending.botNameKey == botNameKey + and pending.srcBag == srcBag + and pending.srcSlot == srcSlot + and pending.srcItemId == srcItemId + and pending.srcCount == srcCount then + return false + end + end + + state.inventoryItemTradeSeq = (tonumber(state.inventoryItemTradeSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-trade-" .. tostring(state.inventoryItemTradeSeq) + local command = { + token = token, + botName = name, + botNameKey = botNameKey, + srcBag = srcBag, + srcSlot = srcSlot, + srcItemId = srcItemId, + srcCount = srcCount, + startedAt = safeNow(), + } + state.inventoryItemTrades[token] = command + + local payload = table.concat({ + "ITEM_TRADE", name, token, + tostring(srcBag), tostring(srcSlot), tostring(srcItemId), tostring(srcCount), + }, "~") + + if not Comm.Send("RUN", payload) then + state.inventoryItemTrades[token] = nil + return false + end + + safeDelay(INVENTORY_ITEM_TRADE_TIMEOUT_SECONDS, function() + local bridgeState = ensureBridgeState() + local pending = bridgeState.inventoryItemTrades and bridgeState.inventoryItemTrades[token] or nil + if not pending then + return + end + + bridgeState.inventoryItemTrades[token] = nil + bridgeState.lastError = "ITEM_TRADE_TIMEOUT" + if MultiBot.OnBridgeInventoryItemTradeResult then + MultiBot.OnBridgeInventoryItemTradeResult( + pending.botName, "ERR", "TIMEOUT", + pending.srcBag, pending.srcSlot, pending.srcItemId, pending.srcCount, 255, pending + ) + end + end) + + return token +end + +function Comm.IsInventoryItemDepositExactCapable() + local state = ensureBridgeState() + return state.connected == true + and state.inventoryExactCapable == true + and state.inventoryItemDepositExactCapable == true +end + +function Comm.RunInventoryItemDepositExact(name, action, srcBag, srcSlot, srcItemId, srcCount) + local state = ensureBridgeState() + name = trim(name) + action = string.upper(trim(action)) + + srcBag = parseBoundedInteger(tostring(srcBag or ""), 0, 255) + srcSlot = parseBoundedInteger(tostring(srcSlot or ""), 0, 255) + srcItemId = parseBoundedInteger(tostring(srcItemId or ""), 1, 4294967295) + srcCount = parseBoundedInteger(tostring(srcCount or ""), 1, INVENTORY_ITEM_DEPOSIT_EXACT_MAX_COUNT) + + if name == "" + or (action ~= "BANK_DEPOSIT" and action ~= "GBANK_DEPOSIT") + or not state.connected + or state.inventoryExactCapable ~= true + or state.inventoryItemDepositExactCapable ~= true then + return false + end + if srcBag == nil or srcSlot == nil or not srcItemId or not srcCount then + return false + end + + local botNameKey = string.lower(name) + for _, pending in pairs(state.inventoryItemDepositExacts or {}) do + if pending.botNameKey == botNameKey + and pending.action == action + and pending.srcBag == srcBag + and pending.srcSlot == srcSlot + and pending.srcItemId == srcItemId + and pending.srcCount == srcCount then + return false + end + end + + state.inventoryItemDepositExactSeq = (tonumber(state.inventoryItemDepositExactSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-deposit-" .. tostring(state.inventoryItemDepositExactSeq) + local command = { + token = token, + botName = name, + botNameKey = botNameKey, + action = action, + srcBag = srcBag, + srcSlot = srcSlot, + srcItemId = srcItemId, + srcCount = srcCount, + startedAt = safeNow(), + } + state.inventoryItemDepositExacts[token] = command + + local payload = table.concat({ + "ITEM_DEPOSIT_EXACT", name, token, action, + tostring(srcBag), tostring(srcSlot), tostring(srcItemId), tostring(srcCount), + }, "~") + + if not Comm.Send("RUN", payload) then + state.inventoryItemDepositExacts[token] = nil + return false + end + + safeDelay(INVENTORY_ITEM_DEPOSIT_EXACT_TIMEOUT_SECONDS, function() + local bridgeState = ensureBridgeState() + local pending = bridgeState.inventoryItemDepositExacts and bridgeState.inventoryItemDepositExacts[token] or nil + if not pending then + return + end + + bridgeState.inventoryItemDepositExacts[token] = nil + bridgeState.lastError = "ITEM_DEPOSIT_EXACT_TIMEOUT" + if MultiBot.OnBridgeInventoryItemActionResult then + MultiBot.OnBridgeInventoryItemActionResult( + pending.botName, pending.action, pending.srcItemId, + "ERR", "TIMEOUT", 0, pending + ) + end + end) + + return token +end + function Comm.IsInventoryItemEquipCapable() local state = ensureBridgeState() return state.connected == true and state.inventoryExactCapable == true and state.inventoryItemEquipCapable == true @@ -3082,9 +3423,565 @@ function Comm.RunProfessionRecipeCraft(name, skillId, spellId, itemId) return false end - return token + return token +end + +-- MB_CRAFT_RECIPE_TARGET_V1_COMM_BEGIN +function Comm.IsProfessionRecipeTargetCapable() + local state = ensureBridgeState() + return state.connected == true and state.craftRecipeTargetCapable == true +end + +function Comm.RunProfessionRecipeTarget(name, skillId, spellId, targetBag, targetSlot, targetItemId) + local state = ensureBridgeState() + name = trim(name) + skillId = parseBoundedInteger(tostring(skillId or ""), 1, 4294967295) + spellId = parseBoundedInteger(tostring(spellId or ""), 1, 4294967295) + targetBag = parseBoundedInteger(tostring(targetBag or ""), 0, 255) + targetSlot = parseBoundedInteger(tostring(targetSlot or ""), 0, 255) + targetItemId = parseBoundedInteger(tostring(targetItemId or ""), 1, 4294967295) + + if name == "" + or not skillId + or not spellId + or not targetBag + or not targetSlot + or not targetItemId + or not state.connected + or state.craftRecipeTargetCapable ~= true then + return false + end + + if countTableEntries(state.professionRecipeTargetCommands) >= CRAFT_RECIPE_TARGET_MAX_ACTIVE then + return false + end + + state.professionRecipeTargetSeq = (tonumber(state.professionRecipeTargetSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-craft-target-" .. tostring(state.professionRecipeTargetSeq) + state.professionRecipeTargetCommands[token] = { + botName = name, + botNameKey = string.lower(name), + skillId = skillId, + spellId = spellId, + targetBag = targetBag, + targetSlot = targetSlot, + targetItemId = targetItemId, + token = token, + startedAt = safeNow(), + } + + local payload = table.concat({ + "CRAFT_RECIPE_TARGET", + token, + urlEncodeField(name), + tostring(skillId), + tostring(spellId), + tostring(targetBag), + tostring(targetSlot), + tostring(targetItemId), + }, "~") + + if not Comm.Send("RUN", payload) then + state.professionRecipeTargetCommands[token] = nil + return false + end + + safeDelay(CRAFT_RECIPE_TARGET_TIMEOUT_SECONDS, function() + local bridge = ensureBridgeState() + local pending = bridge.professionRecipeTargetCommands[token] + if type(pending) ~= "table" then + return + end + + bridge.professionRecipeTargetCommands[token] = nil + bridge.lastError = "CRAFT_RECIPE_TARGET_TIMEOUT" + if MultiBot.OnBridgeProfessionRecipeTargetResult then + MultiBot.OnBridgeProfessionRecipeTargetResult( + pending.botName, "ERR", "TIMEOUT", + pending.skillId, pending.spellId, + pending.targetBag, pending.targetSlot, pending.targetItemId, + pending + ) + end + end) + + return token +end + +function Comm.ApplyProfessionRecipeTargetResultPayload(payload) + local token, rest = splitOnce(payload or "", "~") + local encodedBotName, rest2 = splitOnce(rest or "", "~") + local status, rest3 = splitOnce(rest2 or "", "~") + local encodedReason, rest4 = splitOnce(rest3 or "", "~") + local skillIdValue, rest5 = splitOnce(rest4 or "", "~") + local spellIdValue, rest6 = splitOnce(rest5 or "", "~") + local targetBagValue, rest7 = splitOnce(rest6 or "", "~") + local targetSlotValue, targetItemIdValue = splitOnce(rest7 or "", "~") + + token = trim(token) + local botName = trim(urlDecodeField(encodedBotName)) + status = trim(status) + local reason = trim(urlDecodeField(encodedReason)) + local skillId = parseBoundedInteger(skillIdValue or "", 1, 4294967295) + local spellId = parseBoundedInteger(spellIdValue or "", 1, 4294967295) + local targetBag = parseBoundedInteger(targetBagValue or "", 0, 255) + local targetSlot = parseBoundedInteger(targetSlotValue or "", 0, 255) + local targetItemId = parseBoundedInteger(targetItemIdValue or "", 1, 4294967295) + + local state = ensureBridgeState() + local pending = state.professionRecipeTargetCommands[token] + if type(pending) ~= "table" then + return false + end + + local valid = botName ~= "" + and (status == "OK" or status == "ERR") + and reason ~= "" + and skillId ~= nil + and spellId ~= nil + and targetBag ~= nil + and targetSlot ~= nil + and targetItemId ~= nil + and string.lower(botName) == pending.botNameKey + and skillId == pending.skillId + and spellId == pending.spellId + and targetBag == pending.targetBag + and targetSlot == pending.targetSlot + and targetItemId == pending.targetItemId + + state.professionRecipeTargetCommands[token] = nil + + if not valid then + state.lastError = "CRAFT_RECIPE_TARGET_BAD_RESPONSE" + if MultiBot.OnBridgeProfessionRecipeTargetResult then + MultiBot.OnBridgeProfessionRecipeTargetResult( + pending.botName, "ERR", "BAD_RESPONSE", + pending.skillId, pending.spellId, + pending.targetBag, pending.targetSlot, pending.targetItemId, + pending + ) + end + return true + end + + state.connected = true + state.lastError = status == "OK" and nil or ("CRAFT_RECIPE_TARGET_" .. reason) + if MultiBot.OnBridgeProfessionRecipeTargetResult then + MultiBot.OnBridgeProfessionRecipeTargetResult( + botName, status, reason, + skillId, spellId, targetBag, targetSlot, targetItemId, + pending + ) + end + + debugPrint("ADDON:RX", "CRAFT_RECIPE_TARGET_RESULT", botName, skillId, spellId, status, reason) + return true +end +-- MB_CRAFT_RECIPE_TARGET_V1_COMM_END + +-- MB_TALENT_APPLY_V1_BEGIN +local function finishTalentApplyCommand(token, result) + local state = ensureBridgeState() + local pending = state.talentApplyCommands[token] + if type(pending) ~= "table" then + return false + end + + state.talentApplyCommands[token] = nil + result = type(result) == "table" and result or {} + result.botName = result.botName or pending.botName + result.build = result.build or pending.build + + if type(pending.callback) == "function" then + pending.callback(result) + end + return true +end + +function Comm.IsTalentApplyCapable() + local state = ensureBridgeState() + return state.connected == true and state.talentApplyCapable == true +end + +function Comm.RunTalentApply(botName, build, callback) + local state = ensureBridgeState() + botName = trim(botName or "") + build = type(build) == "string" and build or "" + + if not state.connected or state.talentApplyCapable ~= true then + state.lastError = "TALENT_APPLY_CAPABILITY_UNAVAILABLE" + return false + end + if botName == "" or #botName > 64 then + state.lastError = "TALENT_APPLY_BAD_BOT" + return false + end + if #build == 0 or #build > 128 or not string.match(build, "^[0-5]+%-[0-5]+%-[0-5]+$") then + state.lastError = "TALENT_APPLY_BAD_BUILD" + return false + end + if countTableEntries(state.talentApplyCommands) >= 8 then + state.lastError = "TALENT_APPLY_TOO_MANY_REQUESTS" + return false + end + + state.talentApplySeq = (tonumber(state.talentApplySeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-talent-apply-" .. tostring(state.talentApplySeq) + state.talentApplyCommands[token] = { + botName = botName, + botNameKey = string.lower(botName), + build = build, + callback = type(callback) == "function" and callback or nil, + startedAt = safeNow(), + } + + local payload = "TALENT_APPLY~" .. token .. "~" .. urlEncodeField(botName) .. "~" .. build + if not Comm.Send("RUN", payload) then + state.talentApplyCommands[token] = nil + state.lastError = "TALENT_APPLY_SEND_FAILED" + return false + end + + safeDelay(TALENT_APPLY_TIMEOUT_SECONDS, function() + local bridgeState = ensureBridgeState() + local pending = bridgeState.talentApplyCommands[token] + if type(pending) ~= "table" then + return + end + + bridgeState.lastError = "TALENT_APPLY_TIMEOUT" + finishTalentApplyCommand(token, { + status = "error", + reason = "TIMEOUT", + botName = pending.botName, + build = pending.build, + treePoints = {0, 0, 0}, + }) + end) + + return token +end + +local function handleTalentApplyResponse(payload, state) + local fields = splitFields(payload or "") + if #fields ~= 7 then + state.lastError = "TALENT_APPLY_BAD_FIELD_COUNT" + return true + end + + local token = trim(fields[1]) + local botName = urlDecodeFieldStrict(fields[2], 64, false) + local status = string.upper(trim(fields[3])) + local reason = urlDecodeFieldStrict(fields[4], 64, false) + local tree0 = parseBoundedInteger(fields[5], 0, 255) + local tree1 = parseBoundedInteger(fields[6], 0, 255) + local tree2 = parseBoundedInteger(fields[7], 0, 255) + local pending = state.talentApplyCommands[token] + + local valid = isValidStateToken(token) + and botName ~= nil + and (status == "OK" or status == "ERR") + and reason ~= nil + and tree0 ~= nil and tree1 ~= nil and tree2 ~= nil + and type(pending) == "table" + and string.lower(botName) == pending.botNameKey + + if not valid then + state.lastError = "TALENT_APPLY_BAD_RESPONSE" + if type(pending) == "table" then + finishTalentApplyCommand(token, { + status = "error", + reason = "BAD_RESPONSE", + botName = pending.botName, + build = pending.build, + treePoints = {0, 0, 0}, + }) + end + return true + end + + state.connected = true + state.lastError = status == "OK" and nil or ("TALENT_APPLY_" .. reason) + finishTalentApplyCommand(token, { + status = status == "OK" and "ok" or "error", + reason = reason, + botName = botName, + build = pending.build, + treePoints = {tree0, tree1, tree2}, + }) + return true +end +-- MB_TALENT_APPLY_V1_END +-- MB_TALENT_SPEC_APPLY_V1_BEGIN +local function finishTalentSpecApplyCommand(token, result) + local state = ensureBridgeState() + local pending = state.talentSpecApplyCommands[token] + if type(pending) ~= "table" then + return false + end + + state.talentSpecApplyCommands[token] = nil + result = type(result) == "table" and result or {} + result.botName = result.botName or pending.botName + result.slot = result.slot or pending.slot + result.specIndex = result.specIndex or pending.specIndex + result.specName = result.specName or pending.specName + + if type(pending.callback) == "function" then + pending.callback(result) + end + return true +end + +function Comm.IsTalentSpecApplyCapable() + local state = ensureBridgeState() + return state.connected == true and state.talentSpecApplyCapable == true +end + +function Comm.RunTalentSpecApply(botName, slot, specIndex, specName, callback) + local state = ensureBridgeState() + botName = trim(botName or "") + slot = tonumber(slot or 0) or 0 + specIndex = tonumber(specIndex or -1) or -1 + specName = trim(specName or "") + + if not state.connected or state.talentSpecApplyCapable ~= true then + state.lastError = "TALENT_SPEC_APPLY_CAPABILITY_UNAVAILABLE" + return false + end + if botName == "" or #botName > 64 then + state.lastError = "TALENT_SPEC_APPLY_BAD_BOT" + return false + end + if slot ~= 1 and slot ~= 2 then + state.lastError = "TALENT_SPEC_APPLY_BAD_SLOT" + return false + end + if specIndex < 0 or specIndex > 30 or math.floor(specIndex) ~= specIndex then + state.lastError = "TALENT_SPEC_APPLY_BAD_SPEC" + return false + end + if countTableEntries(state.talentSpecApplyCommands) >= 8 then + state.lastError = "TALENT_SPEC_APPLY_TOO_MANY_REQUESTS" + return false + end + + state.talentSpecApplySeq = (tonumber(state.talentSpecApplySeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-talent-spec-apply-" .. tostring(state.talentSpecApplySeq) + state.talentSpecApplyCommands[token] = { + botName = botName, + botNameKey = string.lower(botName), + slot = slot, + specIndex = specIndex, + specName = specName, + callback = type(callback) == "function" and callback or nil, + startedAt = safeNow(), + } + + local payload = "TALENT_SPEC_APPLY~" .. token .. "~" .. urlEncodeField(botName) .. "~" .. slot .. "~" .. specIndex + if not Comm.Send("RUN", payload) then + state.talentSpecApplyCommands[token] = nil + state.lastError = "TALENT_SPEC_APPLY_SEND_FAILED" + return false + end + + safeDelay(TALENT_SPEC_APPLY_TIMEOUT_SECONDS, function() + local bridgeState = ensureBridgeState() + local pending = bridgeState.talentSpecApplyCommands[token] + if type(pending) ~= "table" then + return + end + + bridgeState.lastError = "TALENT_SPEC_APPLY_TIMEOUT" + finishTalentSpecApplyCommand(token, { + status = "error", + reason = "TIMEOUT", + botName = pending.botName, + slot = pending.slot, + specIndex = pending.specIndex, + specName = pending.specName, + treePoints = {0, 0, 0}, + }) + end) + + return token +end + +local function handleTalentSpecApplyResponse(payload, state) + local fields = splitFields(payload or "") + if #fields ~= 9 then + state.lastError = "TALENT_SPEC_APPLY_BAD_FIELD_COUNT" + return true + end + + local token = trim(fields[1]) + local botName = urlDecodeFieldStrict(fields[2], 64, false) + local status = string.upper(trim(fields[3])) + local reason = urlDecodeFieldStrict(fields[4], 64, false) + local slot = parseBoundedInteger(fields[5], 1, 2) + local specIndex = parseBoundedInteger(fields[6], 0, 30) + local tree0 = parseBoundedInteger(fields[7], 0, 255) + local tree1 = parseBoundedInteger(fields[8], 0, 255) + local tree2 = parseBoundedInteger(fields[9], 0, 255) + local pending = state.talentSpecApplyCommands[token] + + local valid = isValidStateToken(token) + and botName ~= nil + and (status == "OK" or status == "ERR") + and reason ~= nil + and slot ~= nil and specIndex ~= nil + and tree0 ~= nil and tree1 ~= nil and tree2 ~= nil + and type(pending) == "table" + and string.lower(botName) == pending.botNameKey + and slot == pending.slot + and specIndex == pending.specIndex + + if not valid then + state.lastError = "TALENT_SPEC_APPLY_BAD_RESPONSE" + if type(pending) == "table" then + finishTalentSpecApplyCommand(token, { + status = "error", + reason = "BAD_RESPONSE", + botName = pending.botName, + slot = pending.slot, + specIndex = pending.specIndex, + specName = pending.specName, + treePoints = {0, 0, 0}, + }) + end + return true + end + + state.connected = true + state.lastError = status == "OK" and nil or ("TALENT_SPEC_APPLY_" .. reason) + finishTalentSpecApplyCommand(token, { + status = status == "OK" and "ok" or "error", + reason = reason, + botName = botName, + slot = slot, + specIndex = specIndex, + specName = pending.specName, + treePoints = {tree0, tree1, tree2}, + }) + return true +end +-- MB_TALENT_SPEC_APPLY_V1_END-- MB_QUEST_ABANDON_V1_BEGIN + +local function finishQuestAbandonCommand(token, result) + local state = ensureBridgeState() + local pending = state.questAbandonCommands[token] + if type(pending) ~= "table" then + return false + end + + state.questAbandonCommands[token] = nil + result = type(result) == "table" and result or {} + result.token = token + result.questId = result.questId or pending.questId + + if type(pending.callback) == "function" then + pending.callback(result) + end + + if MultiBot.OnBridgeQuestAbandonResult then + MultiBot.OnBridgeQuestAbandonResult(result) + end + + return true +end + +function Comm.IsQuestAbandonCapable() + local state = ensureBridgeState() + return state.connected == true and state.questAbandonCapable == true +end + +function Comm.RunQuestAbandon(questId, callback) + local state = ensureBridgeState() + questId = tonumber(questId or 0) or 0 + + if not state.connected or state.questAbandonCapable ~= true then + return false + end + if questId <= 0 or questId > 4294967295 or math.floor(questId) ~= questId then + return false + end + if countTableEntries(state.questAbandonCommands) >= 8 then + state.lastError = "QUEST_ABANDON_TOO_MANY_REQUESTS" + return false + end + + state.questAbandonSeq = (tonumber(state.questAbandonSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-quest-abandon-" .. tostring(state.questAbandonSeq) + state.questAbandonCommands[token] = { + questId = questId, + callback = type(callback) == "function" and callback or nil, + startedAt = safeNow(), + } + + if not Comm.Send("RUN", "QUEST_ABANDON~" .. token .. "~" .. tostring(questId)) then + state.questAbandonCommands[token] = nil + return false + end + + safeDelay(QUEST_ABANDON_TIMEOUT_SECONDS, function() + local bridgeState = ensureBridgeState() + if not bridgeState.questAbandonCommands[token] then + return + end + + bridgeState.lastError = "QUEST_ABANDON_TIMEOUT" + finishQuestAbandonCommand(token, { + status = "error", + reason = "TIMEOUT", + matched = 0, + abandoned = 0, + questId = questId, + }) + end) + + return token +end + +local function handleQuestAbandonResponse(payload, state) + local fields = splitFields(payload or "") + if #fields ~= 6 then + state.lastError = "QUEST_ABANDON_BAD_FIELD_COUNT" + return true + end + + local token = trim(fields[1]) + local questId = parseBoundedInteger(fields[2], 1, 4294967295) + local status = string.upper(trim(fields[3])) + local reason = urlDecodeFieldStrict(fields[4], 64, false) + local matched = parseBoundedInteger(fields[5], 0, 128) + local abandoned = parseBoundedInteger(fields[6], 0, 128) + local pending = state.questAbandonCommands[token] + + if not isValidStateToken(token) + or questId == nil + or (status ~= "OK" and status ~= "ERR") + or reason == nil + or matched == nil + or abandoned == nil + or abandoned > matched + or type(pending) ~= "table" + or pending.questId ~= questId then + state.lastError = "QUEST_ABANDON_BAD_RESPONSE" + return true + end + + state.connected = true + state.lastError = status == "OK" and nil or ("QUEST_ABANDON_" .. reason) + finishQuestAbandonCommand(token, { + status = status == "OK" and "ok" or "error", + reason = reason, + matched = matched, + abandoned = abandoned, + questId = questId, + }) + return true end - +-- MB_QUEST_ABANDON_V1_END local function finishGroupRollCommand(token, result) local state = ensureBridgeState() local pending = state.groupRollCommands[token] @@ -3261,6 +4158,24 @@ function Comm.MarkDisconnected(reason) end end state.inventoryItemMoves = {} + for _, command in pairs(state.inventoryItemTrades or {}) do + if MultiBot.OnBridgeInventoryItemTradeResult then + MultiBot.OnBridgeInventoryItemTradeResult( + command.botName or "", "ERR", "DISCONNECTED", + command.srcBag or 0, command.srcSlot or 0, command.srcItemId or 0, command.srcCount or 0, 255, command + ) + end + end + state.inventoryItemTrades = {} + for _, command in pairs(state.inventoryItemDepositExacts or {}) do + if MultiBot.OnBridgeInventoryItemActionResult then + MultiBot.OnBridgeInventoryItemActionResult( + command.botName or "", command.action or "BANK_DEPOSIT", command.srcItemId or 0, + "ERR", "DISCONNECTED", 0, command + ) + end + end + state.inventoryItemDepositExacts = {} state.inventoryItemEquips = {} for _, command in pairs(state.inventoryItemUnequips or {}) do if MultiBot.OnBridgeInventoryItemUnequipResult then @@ -3320,6 +4235,56 @@ function Comm.MarkDisconnected(reason) state.guildBankActive = nil state.inventoryItemActions = {} + local pendingTalentApplyTokens = {} + for token in pairs(state.talentApplyCommands or {}) do + pendingTalentApplyTokens[#pendingTalentApplyTokens + 1] = token + end + for _, token in ipairs(pendingTalentApplyTokens) do + local pending = state.talentApplyCommands[token] + finishTalentApplyCommand(token, { + status = "error", + reason = "DISCONNECTED", + botName = pending and pending.botName or "", + build = pending and pending.build or "", + treePoints = {0, 0, 0}, + }) + end + state.talentApplyCommands = {} + + local pendingTalentSpecApplyTokens = {} + for token in pairs(state.talentSpecApplyCommands or {}) do + pendingTalentSpecApplyTokens[#pendingTalentSpecApplyTokens + 1] = token + end + for _, token in ipairs(pendingTalentSpecApplyTokens) do + local pending = state.talentSpecApplyCommands[token] + finishTalentSpecApplyCommand(token, { + status = "error", + reason = "DISCONNECTED", + botName = pending and pending.botName or "", + slot = pending and pending.slot or 1, + specIndex = pending and pending.specIndex or 0, + specName = pending and pending.specName or "", + treePoints = {0, 0, 0}, + }) + end + state.talentSpecApplyCommands = {} + + local pendingQuestAbandonTokens = {} + for token in pairs(state.questAbandonCommands or {}) do + pendingQuestAbandonTokens[#pendingQuestAbandonTokens + 1] = token + end + for _, token in ipairs(pendingQuestAbandonTokens) do + local pending = state.questAbandonCommands[token] + finishQuestAbandonCommand(token, { + status = "error", + reason = "DISCONNECTED", + matched = 0, + abandoned = 0, + questId = pending and pending.questId or 0, + }) + end + state.questAbandonCommands = {} + local pendingRollTokens = {} for token in pairs(state.groupRollCommands or {}) do pendingRollTokens[#pendingRollTokens + 1] = token @@ -3334,12 +4299,33 @@ function Comm.MarkDisconnected(reason) end state.groupRollCommands = {} + for _, pending in pairs(state.lootRuleItemCommands or {}) do + if MultiBot.OnLootRuleItemResult then + MultiBot.OnLootRuleItemResult( + pending.scope or "ALL", pending.target or "", pending.action or "ADD", pending.itemId or 0, + "ERR", "DISCONNECTED", 0, 0, pending + ) + end + end + state.lootRuleItemCommands = {} + state.spellbookActive = nil state.botSkillActive = nil state.botReputationActive = nil state.botEmblemActive = nil state.professionRecipeActive = nil state.professionRecipeCrafts = {} + for _, command in pairs(state.professionRecipeTargetCommands or {}) do + if MultiBot.OnBridgeProfessionRecipeTargetResult then + MultiBot.OnBridgeProfessionRecipeTargetResult( + command.botName or "", "ERR", "DISCONNECTED", + command.skillId or 0, command.spellId or 0, + command.targetBag or 0, command.targetSlot or 0, command.targetItemId or 0, + command + ) + end + end + state.professionRecipeTargetCommands = {} if type(state.enchantTradeActive) == "table" and MultiBot.OnBridgeEnchantTradeList then MultiBot.OnBridgeEnchantTradeList(state.enchantTradeActive.botName or "", {}, { @@ -3371,6 +4357,8 @@ state.selfActionCapable = false state.inventoryCapable = false state.inventoryExactCapable = false state.inventoryItemMoveCapable = false + state.inventoryItemTradeCapable = false + state.inventoryItemDepositExactCapable = false state.inventoryItemEquipCapable = false state.inventoryItemUnequipCapable = false state.inventoryItemDestroyCapable = false @@ -3379,8 +4367,13 @@ state.selfActionCapable = false state.inventoryBuybackCapable = false state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false + state.lootRuleItemCapable = false state.groupRollCapable = false state.enchantTradeCapable = false + state.questAbandonCapable = false + state.talentApplyCapable = false + state.talentSpecApplyCapable = false + state.craftRecipeTargetCapable = false state.selfBotCapable = false state.stateFramingCapable = false state.capabilityFallbackDeadline = 0 @@ -4313,7 +5306,38 @@ function Comm.ApplyTalentSpecBeginPayload(payload) return true end +local function handleTalentSpecCurrentResponse(payload, state) + local fields = splitFields(payload or "") + if #fields ~= 6 then + state.lastError = "TALENT_SPEC_CURRENT_BAD_FIELD_COUNT" + return true + end + + local botName = urlDecodeFieldStrict(fields[1], 64, false) + local token = trim(fields[2]) + local slot = parseBoundedInteger(fields[3], 1, 2) + local tree0 = parseBoundedInteger(fields[4], 0, 255) + local tree1 = parseBoundedInteger(fields[5], 0, 255) + local tree2 = parseBoundedInteger(fields[6], 0, 255) + + if not botName or not isValidStateToken(token) or not slot + or tree0 == nil or tree1 == nil or tree2 == nil + or not getActiveTalentSpecRequest(botName, token) then + state.lastError = "TALENT_SPEC_CURRENT_BAD_RESPONSE" + return true + end + + state.connected = true + state.lastError = nil + if MultiBot.ApplyBridgeTalentSpecCurrent then + MultiBot.ApplyBridgeTalentSpecCurrent(botName, token, slot, tree0, tree1, tree2) + end + + debugPrint("ADDON:RX", "TALENT_SPEC_CURRENT", botName, slot, tree0, tree1, tree2) + return true +end function Comm.ApplyTalentSpecItemPayload(payload) + local botName, rest = splitOnce(payload or "", "~") local token, rest2 = splitOnce(rest or "", "~") local index, rest3 = splitOnce(rest2 or "", "~") @@ -5542,6 +6566,319 @@ function Comm.IsExpectedBridgeSender(sender) return true end +local function resetCapabilityFlags(state) + for _, stateField in pairs(CAPABILITY_STATE_FIELDS) do + state[stateField] = false + end +end + +local function finishCapabilityResolution(state, debugOpcode, payload) + state.capabilityFallbackDeadline = 0 + state.capabilityFallbackGeneration = 0 + state.capabilitiesResolved = true + debugPrint("ADDON:RX", debugOpcode, payload or "") + flushPendingStateRefreshes() + if state.selfBotCapable == true and type(Comm.RequestSelfBotState) == "function" then + Comm.RequestSelfBotState() + end + if MultiBot.RefreshEnchantingEveryButtons then + MultiBot.RefreshEnchantingEveryButtons() + end +end + +local function handleCapabilityMessage(opcode, payload, state) + if opcode == "CAPS_BEGIN" then + resetCapabilityFlags(state) + state.capabilityBatchActive = true + state.capabilitiesResolved = false + debugPrint("ADDON:RX", "CAPS_BEGIN") + return true + end + + if opcode == "CAPS" then + if not state.capabilityBatchActive then + resetCapabilityFlags(state) + end + + for capability in string.gmatch(payload or "", "([^,]+)") do + capability = trim(capability) + local stateField = CAPABILITY_STATE_FIELDS[capability] + if stateField then + state[stateField] = true + end + end + + if state.capabilityBatchActive then + debugPrint("ADDON:RX", "CAPS_PART", payload or "") + return true + end + + finishCapabilityResolution(state, "CAPS", payload) + return true + end + + if opcode == "CAPS_END" then + if not state.capabilityBatchActive then + return true + end + + state.capabilityBatchActive = false + finishCapabilityResolution(state, "CAPS_END") + return true + end + + return false +end + +local function handleInventoryItemDepositExactResponse(payload, state) + local fields = splitFields(payload) + if #fields ~= 10 then + state.lastError = "ITEM_DEPOSIT_EXACT_BAD_FIELD_COUNT" + return true + end + + local botName = urlDecodeFieldStrict(fields[1], 64, false) + local token = trim(fields[2]) + local status = string.upper(trim(fields[3])) + local reason = urlDecodeFieldStrict(fields[4], 64, false) + local action = string.upper(trim(fields[5])) + local srcBag = parseBoundedInteger(fields[6], 0, 255) + local srcSlot = parseBoundedInteger(fields[7], 0, 255) + local srcItemId = parseBoundedInteger(fields[8], 1, 4294967295) + local srcCount = parseBoundedInteger(fields[9], 1, INVENTORY_ITEM_DEPOSIT_EXACT_MAX_COUNT) + local movedCount = parseBoundedInteger(fields[10], 0, INVENTORY_ITEM_DEPOSIT_EXACT_MAX_COUNT) + + state.connected = true + local command = state.inventoryItemDepositExacts and state.inventoryItemDepositExacts[token] or nil + if not botName + or not isValidStateToken(token) + or (status ~= "OK" and status ~= "ERR") + or not reason + or (action ~= "BANK_DEPOSIT" and action ~= "GBANK_DEPOSIT") + or srcBag == nil + or srcSlot == nil + or srcItemId == nil + or srcCount == nil + or movedCount == nil then + state.lastError = "ITEM_DEPOSIT_EXACT_BAD_RESPONSE" + if command then + state.inventoryItemDepositExacts[token] = nil + if MultiBot.OnBridgeInventoryItemActionResult then + MultiBot.OnBridgeInventoryItemActionResult( + command.botName, command.action, command.srcItemId, + "ERR", "BAD_RESPONSE", 0, command + ) + end + end + return true + end + + if not command then + return true + end + + local responseMatches = string.lower(botName) == command.botNameKey + and action == command.action + and srcBag == command.srcBag + and srcSlot == command.srcSlot + and srcItemId == command.srcItemId + and srcCount == command.srcCount + and ((status == "OK" and movedCount == command.srcCount) + or (status == "ERR" and movedCount == 0)) + + state.inventoryItemDepositExacts[token] = nil + if not responseMatches then + status = "ERR" + reason = "RESPONSE_MISMATCH" + movedCount = 0 + state.lastError = "ITEM_DEPOSIT_EXACT_RESPONSE_MISMATCH" + elseif status == "OK" then + state.lastError = nil + else + state.lastError = "ITEM_DEPOSIT_EXACT_" .. reason + end + + if MultiBot.OnBridgeInventoryItemActionResult then + MultiBot.OnBridgeInventoryItemActionResult( + command.botName, command.action, command.srcItemId, + status, reason, movedCount, command + ) + end + + debugPrint( + "ADDON:RX", "ITEM_DEPOSIT_EXACT", + botName, token, status, reason, action, + srcBag, srcSlot, srcItemId, srcCount, movedCount + ) + return true +end + +-- MB_LOOT_RULE_ITEM_V1_RX_BEGIN +local function handleLootRuleItemResponse(payload, state) + local fields = splitFields(payload or "") + if #fields ~= 9 then + state.lastError = "LOOT_RULE_ITEM_BAD_FIELD_COUNT" + return true + end + + local scope = string.upper(trim(fields[1])) + local target = urlDecodeFieldStrict(fields[2], 64, true) + local token = trim(fields[3]) + local action = string.upper(trim(fields[4])) + local itemId = parseBoundedInteger(fields[5], 1, 4294967295) + local status = string.upper(trim(fields[6])) + local reason = urlDecodeFieldStrict(fields[7], 64, false) + local matched = parseBoundedInteger(fields[8], 0, 128) + local changed = parseBoundedInteger(fields[9], 0, 128) + local pending = state.lootRuleItemCommands[token] + local validScope = scope == "ALL" or scope == "RAID" or scope == "GROUP" + or scope == "PARTY" or scope == "BOT" + + state.connected = true + if not validScope + or target == nil + or not isValidStateToken(token) + or (action ~= "ADD" and action ~= "REMOVE") + or itemId == nil + or (status ~= "OK" and status ~= "ERR") + or reason == nil + or matched == nil + or changed == nil + or changed > matched + or (status == "OK" and matched == 0) + or (status == "ERR" and changed ~= 0) then + state.lastError = "LOOT_RULE_ITEM_BAD_RESPONSE" + if type(pending) == "table" then + state.lootRuleItemCommands[token] = nil + if MultiBot.OnLootRuleItemResult then + MultiBot.OnLootRuleItemResult( + pending.scope, pending.target, pending.action, pending.itemId, + "ERR", "BAD_RESPONSE", 0, 0, pending + ) + end + end + return true + end + + if type(pending) ~= "table" then + return true + end + + local responseMatches = scope == pending.scope + and string.lower(target) == pending.targetKey + and action == pending.action + and itemId == pending.itemId + state.lootRuleItemCommands[token] = nil + + if not responseMatches then + status = "ERR" + reason = "RESPONSE_MISMATCH" + matched = 0 + changed = 0 + state.lastError = "LOOT_RULE_ITEM_RESPONSE_MISMATCH" + elseif status == "OK" then + state.lastError = nil + else + state.lastError = "LOOT_RULE_ITEM_" .. reason + end + + if MultiBot.OnLootRuleItemResult then + MultiBot.OnLootRuleItemResult( + pending.scope, pending.target, pending.action, pending.itemId, + status, reason, matched, changed, pending + ) + end + + debugPrint( + "ADDON:RX", "LOOT_RULE_ITEM_RESULT", + scope, target, token, action, itemId, status, reason, matched, changed + ) + return true +end +-- MB_LOOT_RULE_ITEM_V1_RX_END +local function handleInventoryItemTradeResponse(payload, state) + local fields = splitFields(payload) + if #fields ~= 9 then + state.lastError = "ITEM_TRADE_BAD_FIELD_COUNT" + return true + end + + local botName = urlDecodeFieldStrict(fields[1], 64, false) + local token = trim(fields[2]) + local status = string.upper(trim(fields[3])) + local reason = urlDecodeFieldStrict(fields[4], 64, false) + local srcBag = parseBoundedInteger(fields[5], 0, 255) + local srcSlot = parseBoundedInteger(fields[6], 0, 255) + local srcItemId = parseBoundedInteger(fields[7], 1, 4294967295) + local srcCount = parseBoundedInteger(fields[8], 1, INVENTORY_ITEM_TRADE_MAX_COUNT) + local tradeSlot = parseBoundedInteger(fields[9], 0, 255) + + state.connected = true + local command = state.inventoryItemTrades and state.inventoryItemTrades[token] or nil + if not botName or not isValidStateToken(token) or (status ~= "OK" and status ~= "ERR") or not reason or + srcBag == nil or srcSlot == nil or srcItemId == nil or srcCount == nil or tradeSlot == nil then + state.lastError = "ITEM_TRADE_BAD_RESPONSE" + if command then + state.inventoryItemTrades[token] = nil + if MultiBot.OnBridgeInventoryItemTradeResult then + MultiBot.OnBridgeInventoryItemTradeResult( + command.botName, "ERR", "BAD_RESPONSE", + command.srcBag, command.srcSlot, command.srcItemId, command.srcCount, 255, command + ) + end + end + return true + end + + if not command then + return true + end + + local responseMatches = string.lower(botName) == command.botNameKey and + srcBag == command.srcBag and srcSlot == command.srcSlot and + srcItemId == command.srcItemId and srcCount == command.srcCount and + ((status == "OK" and tradeSlot >= 0 and tradeSlot <= 5) or status == "ERR") + + state.inventoryItemTrades[token] = nil + if not responseMatches then + status = "ERR" + reason = "RESPONSE_MISMATCH" + tradeSlot = 255 + state.lastError = "ITEM_TRADE_RESPONSE_MISMATCH" + elseif status == "OK" then + state.lastError = nil + else + state.lastError = "ITEM_TRADE_" .. reason + end + + if MultiBot.OnBridgeInventoryItemTradeResult then + MultiBot.OnBridgeInventoryItemTradeResult( + command.botName, status, reason, + command.srcBag, command.srcSlot, command.srcItemId, command.srcCount, tradeSlot, command + ) + end + + debugPrint("ADDON:RX", "INVENTORY_ITEM_TRADE", botName, token, status, reason, srcBag, srcSlot, srcItemId, srcCount, tradeSlot) + return true +end + +local function handleProfessionRecipeTargetResponse(payload) + return Comm.ApplyProfessionRecipeTargetResultPayload(payload) +end + +-- New structured response handlers should be registered here instead of adding +-- another large branch directly inside Comm.HandleAddonMessage. +local STRUCTURED_OPCODE_HANDLERS = { + ITEM_DEPOSIT_EXACT = handleInventoryItemDepositExactResponse, + LOOT_RULE_ITEM_RESULT = handleLootRuleItemResponse, + INVENTORY_ITEM_TRADE = handleInventoryItemTradeResponse, + QUEST_ABANDON_RESULT = handleQuestAbandonResponse, + TALENT_APPLY_RESULT = handleTalentApplyResponse, + TALENT_SPEC_CURRENT = handleTalentSpecCurrentResponse, + TALENT_SPEC_APPLY_RESULT = handleTalentSpecApplyResponse, + CRAFT_RECIPE_TARGET_RESULT = handleProfessionRecipeTargetResponse, +} + function Comm.HandleAddonMessage(prefix, message, distribution, sender) if prefix ~= Comm.prefix then return false @@ -5604,136 +6941,10 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) return true end - if opcode == "CAPS_BEGIN" then - state.stateFramingCapable = false - state.strategyMutationCapable = false -state.selfStrategyCapable = false -state.selfActionCapable = false - state.outfitCapable = false - state.inventoryCapable = false - state.inventoryExactCapable = false - state.inventoryItemMoveCapable = false - state.inventoryItemEquipCapable = false - state.inventoryItemUnequipCapable = false - state.inventoryItemDestroyCapable = false - state.inventoryItemUseCapable = false - state.inventoryItemSellCapable = false - state.inventoryBuybackCapable = false - state.inventoryBulkSellCapable = false - state.inventoryOpenCapable = false - state.groupRollCapable = false - state.enchantTradeCapable = false - state.selfBotCapable = false - state.capabilityBatchActive = true - state.capabilitiesResolved = false - debugPrint("ADDON:RX", "CAPS_BEGIN") - return true - end - - if opcode == "CAPS" then - if not state.capabilityBatchActive then - state.stateFramingCapable = false - state.strategyMutationCapable = false -state.selfStrategyCapable = false -state.selfActionCapable = false - state.outfitCapable = false - state.inventoryCapable = false - state.inventoryExactCapable = false - state.inventoryItemMoveCapable = false - state.inventoryItemEquipCapable = false - state.inventoryItemUnequipCapable = false - state.inventoryItemDestroyCapable = false - state.inventoryItemUseCapable = false - state.inventoryItemSellCapable = false - state.inventoryBuybackCapable = false - state.inventoryBulkSellCapable = false - state.inventoryOpenCapable = false - state.groupRollCapable = false - state.enchantTradeCapable = false - state.selfBotCapable = false - end - - for capability in string.gmatch(payload or "", "([^,]+)") do - capability = trim(capability) - if capability == STATE_FRAMING_CAPABILITY then - state.stateFramingCapable = true - elseif capability == STRATEGY_MUTATION_CAPABILITY then - state.strategyMutationCapable = true - elseif capability == "SELF_STRATEGY_V1" then - state.selfStrategyCapable = true - elseif capability == SELF_ACTION_CAPABILITY then - state.selfActionCapable = true - elseif capability == OUTFIT_CAPABILITY then - state.outfitCapable = true - elseif capability == INVENTORY_CAPABILITY then - state.inventoryCapable = true - elseif capability == INVENTORY_EXACT_CAPABILITY then - state.inventoryExactCapable = true - elseif capability == INVENTORY_ITEM_MOVE_CAPABILITY then - state.inventoryItemMoveCapable = true - elseif capability == INVENTORY_ITEM_EQUIP_CAPABILITY then - state.inventoryItemEquipCapable = true - elseif capability == INVENTORY_ITEM_UNEQUIP_CAPABILITY then - state.inventoryItemUnequipCapable = true - elseif capability == INVENTORY_ITEM_DESTROY_CAPABILITY then - state.inventoryItemDestroyCapable = true - elseif capability == INVENTORY_ITEM_USE_CAPABILITY then - state.inventoryItemUseCapable = true - elseif capability == INVENTORY_ITEM_SELL_CAPABILITY then - state.inventoryItemSellCapable = true - elseif capability == "VENDOR_BUYBACK_V1" then - state.inventoryBuybackCapable = true - elseif capability == INVENTORY_BULK_SELL_CAPABILITY then - state.inventoryBulkSellCapable = true - elseif capability == INVENTORY_OPEN_CAPABILITY then - state.inventoryOpenCapable = true - elseif capability == GROUP_ROLL_CAPABILITY then - state.groupRollCapable = true - elseif capability == ENCHANT_TRADE_CAPABILITY then - state.enchantTradeCapable = true - elseif capability == "SELF_BOT_V1" then - state.selfBotCapable = true - end - end - - if state.capabilityBatchActive then - debugPrint("ADDON:RX", "CAPS_PART", payload or "") - return true - end - - state.capabilityFallbackDeadline = 0 - state.capabilityFallbackGeneration = 0 - state.capabilitiesResolved = true - debugPrint("ADDON:RX", "CAPS", payload or "") - flushPendingStateRefreshes() - if state.selfBotCapable == true and type(Comm.RequestSelfBotState) == "function" then - Comm.RequestSelfBotState() - end - if MultiBot.RefreshEnchantingEveryButtons then - MultiBot.RefreshEnchantingEveryButtons() - end + if handleCapabilityMessage(opcode, payload, state) then return true end - if opcode == "CAPS_END" then - if not state.capabilityBatchActive then - return true - end - - state.capabilityBatchActive = false - state.capabilityFallbackDeadline = 0 - state.capabilityFallbackGeneration = 0 - state.capabilitiesResolved = true - debugPrint("ADDON:RX", "CAPS_END") - flushPendingStateRefreshes() - if state.selfBotCapable == true and type(Comm.RequestSelfBotState) == "function" then - Comm.RequestSelfBotState() - end - if MultiBot.RefreshEnchantingEveryButtons then - MultiBot.RefreshEnchantingEveryButtons() - end - return true - end if opcode == "WEAPON_ENCHANT" then state.connected = true @@ -6549,6 +7760,11 @@ state.selfActionCapable = false return true end + local structuredHandler = STRUCTURED_OPCODE_HANDLERS[opcode] + if structuredHandler then + return structuredHandler(payload, state) + end + if opcode == "INVENTORY_ITEM_EQUIP" then local fields = splitFields(payload) if #fields ~= 7 then @@ -7793,6 +9009,16 @@ state.selfActionCapable = false return true elseif Comm.HandleSelfStrategyProtocolError(requestType, token, reason, state) then return true + elseif requestType == "LOOT_RULE_ITEM" and state.lootRuleItemCommands[token] then + local pending = state.lootRuleItemCommands[token] + state.lootRuleItemCommands[token] = nil + state.lastError = "LOOT_RULE_ITEM_" .. reason + if MultiBot.OnLootRuleItemResult then + MultiBot.OnLootRuleItemResult( + pending.scope, pending.target, pending.action, pending.itemId, + "ERR", reason, 0, 0, pending + ) + end elseif requestType == "GROUP_ROLL" and state.groupRollCommands[token] then finishGroupRollCommand(token, { status = "error", @@ -7861,6 +9087,8 @@ state.selfActionCapable = false state.inventoryCapable = false state.inventoryExactCapable = false state.inventoryItemMoveCapable = false + state.inventoryItemTradeCapable = false + state.inventoryItemDepositExactCapable = false state.inventoryItemEquipCapable = false state.inventoryItemUnequipCapable = false state.inventoryItemDestroyCapable = false @@ -7869,10 +9097,16 @@ state.selfActionCapable = false state.inventoryBuybackCapable = false state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false + state.lootRuleItemCapable = false state.groupRollCapable = false state.enchantTradeCapable = false + state.questAbandonCapable = false + state.talentApplyCapable = false + state.talentSpecApplyCapable = false + state.craftRecipeTargetCapable = false state.selfBotCapable = false state.strategyMutationCommands = {} + state.lootRuleItemCommands = {} state.details = {} state.stats = {} state.pvpStats = {} @@ -7898,6 +9132,7 @@ state.selfActionCapable = false state.professionRecipes = {} state.professionRecipeActive = nil state.professionRecipeCrafts = {} + state.professionRecipeTargetCommands = {} state.outfitActive = nil state.outfitCommands = {} state.trainerActive = nil diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 28bf5b0..fe63060 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local deDEValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Immer plündern: Gegenstand hinzufügen", + ["loot.item.remove"] = "Immer plündern: Gegenstand entfernen", + ["loot.item.bridge.required"] = "Exakte Beuteregeln pro Gegenstand benötigen LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "Das Eingabefenster für Gegenstände ist nicht verfügbar.", + ["loot.item.add.prompt"] = "Gegenstands-ID oder Link zu Immer plündern hinzufügen", + ["loot.item.remove.prompt"] = "Gegenstands-ID oder Link aus Immer plündern entfernen", + ["loot.item.invalid"] = "Ungültige Gegenstands-ID oder ungültiger Gegenstandslink.", + ["loot.item.send.failed"] = "Die Anfrage für die Gegenstands-Beuteregel wurde nicht gesendet.", + ["loot.item.result.added"] = "%s - bei %d Bot(s) hinzugefügt.", + ["loot.item.result.removed"] = "%s - bei %d Bot(s) entfernt.", + ["loot.item.result.already_present"] = "%s - bei %d Bot(s) bereits vorhanden.", + ["loot.item.result.already_absent"] = "%s - bei %d Bot(s) bereits nicht vorhanden.", + ["loot.item.result.partial"] = "%s - teilweise aktualisiert (%d/%d Bot(s)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d Bot(s)).", + ["loot.item.result.failed"] = "%s - Beuteregel fehlgeschlagen: %s.", + ["loot.item.reason.RATE_LIMIT"] = "zu viele Anfragen; bitte kurz warten", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "Spielersitzung nicht verfügbar", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "Spieler ist nicht in der Welt", + ["loot.item.reason.DUPLICATE"] = "doppelte Anfrage", + ["loot.item.reason.BAD_ACTION"] = "ungültige Aktion", + ["loot.item.reason.INVALID_ITEM"] = "ungültiger Gegenstand", + ["loot.item.reason.NO_BOTS"] = "keine geeigneten Bots", + ["loot.item.reason.TOO_MANY_BOTS"] = "zu viele ausgewählte Bots", + ["loot.item.reason.FORBIDDEN"] = "Bot-Steuerung nicht erlaubt", + ["loot.item.reason.NO_BOT_SESSION"] = "Bot-Sitzung nicht verfügbar", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "Bot ist nicht in der Welt", + ["loot.item.reason.BOT_DEAD"] = "Bot ist tot", + ["loot.item.reason.NO_BOT_CONTEXT"] = "Bot-KI-Kontext nicht verfügbar", + ["loot.item.reason.PERSISTENCE_BUSY"] = "Persistenz ist ausgelastet; bitte kurz warten", + ["loot.item.reason.FAILED"] = "Vorgang fehlgeschlagen", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Talentspezialisierung %s wurde erfolgreich auf %s angewendet.", + ["talent.spec.apply.failed"] = "Talentspezialisierung %s konnte nicht auf %s angewendet werden (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Talente wurden erfolgreich auf %s angewendet.", + ["talent.apply.failed"] = "Talente konnten nicht auf %s angewendet werden (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "Die Bridge ist nicht verfügbar: Die SelfBot-Aktion wurde nicht gesendet.", ["selfaction.failed"] = "SelfBot-Aktion fehlgeschlagen: %s", @@ -84,6 +124,30 @@ local deDEValues = { ["profession.recipes.craft.reason.CHANNELING"] = "Der Bot kanalisiert bereits einen Zauber.", ["profession.recipes.craft.reason.cast_code"] = "Der Server hat den Zauber abgelehnt (Code %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "Der Server hat einen unbekannten Herstellungsfehler gemeldet.", + ["profession.recipes.target.apply"] = "Anwenden", + ["profession.recipes.target.unavailable"] = "Die exakte Gegenstandsauswahl über die Bridge ist nicht verfügbar.", + ["profession.recipes.target.select"] = "Wähle den exakten Gegenstand im Inventar oder in der Ausrüstung des Bots aus.", + ["profession.recipes.target.send_failed"] = "Die gezielte Herstellungsanfrage konnte nicht gesendet werden.", + ["profession.recipes.target.pending"] = "Rezept wird auf den ausgewählten Gegenstand angewendet...", + ["profession.recipes.target.wrong_bot"] = "Wähle einen Gegenstand des Bots aus, dessen Rezept geöffnet ist.", + ["profession.recipes.target.exact_required"] = "Dieses Ziel muss aus der exakten Inventaransicht stammen.", + ["profession.recipes.target.invalid_scope"] = "Wähle einen ausgerüsteten Gegenstand oder einen Gegenstand aus Rucksack oder ausgerüsteten Taschen.", + ["profession.recipes.target.ok"] = "Das Rezept wurde auf den ausgewählten Gegenstand angewendet.", + ["profession.recipes.target.err"] = "Gezielte Herstellung fehlgeschlagen: %s", + ["profession.recipes.target.failed"] = "Die gezielte Herstellungsanfrage ist fehlgeschlagen.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "Diese Inventar- oder Ausrüstungsposition ist nicht erlaubt.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "Der ausgewählte Gegenstand ist nicht mehr vorhanden.", + ["profession.recipes.target.reason.TARGET_STALE"] = "Der ausgewählte Platz enthält jetzt einen anderen Gegenstand.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "Dieses Rezept akzeptiert keinen Gegenstand als Ziel.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "Der ausgewählte Gegenstand ist kein gültiges Ziel für dieses Rezept.", + ["profession.recipes.target.reason.REPLAY"] = "Diese gezielte Anfrage wurde bereits verarbeitet.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Zu viele gezielte Herstellungsanfragen. Versuche es gleich erneut.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "Der Bot ist momentan nicht verfügbar.", + ["profession.recipes.target.reason.BOT_DEAD"] = "Der Bot muss leben, um dieses Rezept zu verwenden.", + ["profession.recipes.target.reason.FORBIDDEN"] = "Du darfst diesen Bot nicht steuern.", + ["profession.recipes.target.reason.TIMEOUT"] = "Zeitüberschreitung bei der gezielten Herstellungsanfrage.", + ["profession.recipes.target.reason.DISCONNECTED"] = "Die Bridge wurde getrennt, bevor die gezielte Herstellung abgeschlossen war.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "Die Bridge hat eine ungültige Antwort für die gezielte Herstellung geliefert.", ["character.tab.skills"] = "Fertigkeiten", ["character.tab.reputations"] = "Ruf", ["character.tab.emblems"] = "Abzeichen", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index 0df4e4d..fdd59f4 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local enGBValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Always loot: add item", + ["loot.item.remove"] = "Always loot: remove item", + ["loot.item.bridge.required"] = "Exact loot-item rules require LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "Item prompt is unavailable.", + ["loot.item.add.prompt"] = "Add always-loot item ID or link", + ["loot.item.remove.prompt"] = "Remove always-loot item ID or link", + ["loot.item.invalid"] = "Invalid item ID or item link.", + ["loot.item.send.failed"] = "Loot item rule request was not sent.", + ["loot.item.result.added"] = "%s - added to %d bot(s).", + ["loot.item.result.removed"] = "%s - removed from %d bot(s).", + ["loot.item.result.already_present"] = "%s - already present on %d bot(s).", + ["loot.item.result.already_absent"] = "%s - already absent on %d bot(s).", + ["loot.item.result.partial"] = "%s - partially updated (%d/%d bot(s)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d bot(s)).", + ["loot.item.result.failed"] = "%s - loot rule failed: %s.", + ["loot.item.reason.RATE_LIMIT"] = "too many requests; try again shortly", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "requester session unavailable", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "requester is not in world", + ["loot.item.reason.DUPLICATE"] = "duplicate request", + ["loot.item.reason.BAD_ACTION"] = "invalid action", + ["loot.item.reason.INVALID_ITEM"] = "invalid item", + ["loot.item.reason.NO_BOTS"] = "no eligible bots", + ["loot.item.reason.TOO_MANY_BOTS"] = "too many selected bots", + ["loot.item.reason.FORBIDDEN"] = "bot control forbidden", + ["loot.item.reason.NO_BOT_SESSION"] = "bot session unavailable", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "bot is not in world", + ["loot.item.reason.BOT_DEAD"] = "bot is dead", + ["loot.item.reason.NO_BOT_CONTEXT"] = "bot AI context unavailable", + ["loot.item.reason.PERSISTENCE_BUSY"] = "persistence is busy; try again shortly", + ["loot.item.reason.FAILED"] = "operation failed", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Talent specialization %s applied successfully to %s.", + ["talent.spec.apply.failed"] = "Failed to apply talent specialization %s to %s (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Talents applied successfully to %s.", + ["talent.apply.failed"] = "Failed to apply talents to %s (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "The bridge is unavailable: the SelfBot action was not sent.", ["selfaction.failed"] = "SelfBot action failed: %s", @@ -84,6 +124,30 @@ local enGBValues = { ["profession.recipes.craft.reason.CHANNELING"] = "The bot is already channeling a spell.", ["profession.recipes.craft.reason.cast_code"] = "The server refused the cast (code %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "The server returned an unknown crafting error.", + ["profession.recipes.target.apply"] = "Apply", + ["profession.recipes.target.unavailable"] = "Exact item targeting via the bridge is unavailable.", + ["profession.recipes.target.select"] = "Select the exact item to modify in the bot's inventory or equipment.", + ["profession.recipes.target.send_failed"] = "The targeted crafting request could not be sent.", + ["profession.recipes.target.pending"] = "Applying the recipe to the selected item...", + ["profession.recipes.target.wrong_bot"] = "Select an item belonging to the bot whose recipe is open.", + ["profession.recipes.target.exact_required"] = "This target must come from the exact inventory view.", + ["profession.recipes.target.invalid_scope"] = "Choose an equipped item or an item from the backpack or equipped bags.", + ["profession.recipes.target.ok"] = "The recipe was applied to the selected item.", + ["profession.recipes.target.err"] = "Targeted craft failed: %s", + ["profession.recipes.target.failed"] = "The targeted crafting request failed.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "That inventory or equipment position is not allowed.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "The selected item is no longer present.", + ["profession.recipes.target.reason.TARGET_STALE"] = "The selected slot now contains a different item.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "This recipe does not accept an item target.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "The selected item is not a valid target for this recipe.", + ["profession.recipes.target.reason.REPLAY"] = "This targeted request was already processed.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Too many targeted crafting requests. Try again shortly.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "The bot is not available right now.", + ["profession.recipes.target.reason.BOT_DEAD"] = "The bot must be alive to use this recipe.", + ["profession.recipes.target.reason.FORBIDDEN"] = "You are not allowed to control this bot.", + ["profession.recipes.target.reason.TIMEOUT"] = "The targeted crafting request timed out.", + ["profession.recipes.target.reason.DISCONNECTED"] = "The bridge disconnected before the targeted craft completed.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "The bridge returned an invalid targeted crafting response.", ["character.tab.skills"] = "Skills", ["character.tab.reputations"] = "Reputations", ["character.tab.emblems"] = "Emblems", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index d855cef..4aa5afd 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local enUSValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Always loot: add item", + ["loot.item.remove"] = "Always loot: remove item", + ["loot.item.bridge.required"] = "Exact loot-item rules require LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "Item prompt is unavailable.", + ["loot.item.add.prompt"] = "Add always-loot item ID or link", + ["loot.item.remove.prompt"] = "Remove always-loot item ID or link", + ["loot.item.invalid"] = "Invalid item ID or item link.", + ["loot.item.send.failed"] = "Loot item rule request was not sent.", + ["loot.item.result.added"] = "%s - added to %d bot(s).", + ["loot.item.result.removed"] = "%s - removed from %d bot(s).", + ["loot.item.result.already_present"] = "%s - already present on %d bot(s).", + ["loot.item.result.already_absent"] = "%s - already absent on %d bot(s).", + ["loot.item.result.partial"] = "%s - partially updated (%d/%d bot(s)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d bot(s)).", + ["loot.item.result.failed"] = "%s - loot rule failed: %s.", + ["loot.item.reason.RATE_LIMIT"] = "too many requests; try again shortly", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "requester session unavailable", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "requester is not in world", + ["loot.item.reason.DUPLICATE"] = "duplicate request", + ["loot.item.reason.BAD_ACTION"] = "invalid action", + ["loot.item.reason.INVALID_ITEM"] = "invalid item", + ["loot.item.reason.NO_BOTS"] = "no eligible bots", + ["loot.item.reason.TOO_MANY_BOTS"] = "too many selected bots", + ["loot.item.reason.FORBIDDEN"] = "bot control forbidden", + ["loot.item.reason.NO_BOT_SESSION"] = "bot session unavailable", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "bot is not in world", + ["loot.item.reason.BOT_DEAD"] = "bot is dead", + ["loot.item.reason.NO_BOT_CONTEXT"] = "bot AI context unavailable", + ["loot.item.reason.PERSISTENCE_BUSY"] = "persistence is busy; try again shortly", + ["loot.item.reason.FAILED"] = "operation failed", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Talent specialization %s applied successfully to %s.", + ["talent.spec.apply.failed"] = "Failed to apply talent specialization %s to %s (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Talents applied successfully to %s.", + ["talent.apply.failed"] = "Failed to apply talents to %s (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "The bridge is unavailable: the SelfBot action was not sent.", ["selfaction.failed"] = "SelfBot action failed: %s", @@ -84,6 +124,30 @@ local enUSValues = { ["profession.recipes.craft.reason.CHANNELING"] = "The bot is already channeling a spell.", ["profession.recipes.craft.reason.cast_code"] = "The server refused the cast (code %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "The server returned an unknown crafting error.", + ["profession.recipes.target.apply"] = "Apply", + ["profession.recipes.target.unavailable"] = "Exact item targeting via the bridge is unavailable.", + ["profession.recipes.target.select"] = "Select the exact item to modify in the bot's inventory or equipment.", + ["profession.recipes.target.send_failed"] = "The targeted crafting request could not be sent.", + ["profession.recipes.target.pending"] = "Applying the recipe to the selected item...", + ["profession.recipes.target.wrong_bot"] = "Select an item belonging to the bot whose recipe is open.", + ["profession.recipes.target.exact_required"] = "This target must come from the exact inventory view.", + ["profession.recipes.target.invalid_scope"] = "Choose an equipped item or an item from the backpack or equipped bags.", + ["profession.recipes.target.ok"] = "The recipe was applied to the selected item.", + ["profession.recipes.target.err"] = "Targeted craft failed: %s", + ["profession.recipes.target.failed"] = "The targeted crafting request failed.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "That inventory or equipment position is not allowed.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "The selected item is no longer present.", + ["profession.recipes.target.reason.TARGET_STALE"] = "The selected slot now contains a different item.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "This recipe does not accept an item target.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "The selected item is not a valid target for this recipe.", + ["profession.recipes.target.reason.REPLAY"] = "This targeted request was already processed.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Too many targeted crafting requests. Try again shortly.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "The bot is not available right now.", + ["profession.recipes.target.reason.BOT_DEAD"] = "The bot must be alive to use this recipe.", + ["profession.recipes.target.reason.FORBIDDEN"] = "You are not allowed to control this bot.", + ["profession.recipes.target.reason.TIMEOUT"] = "The targeted crafting request timed out.", + ["profession.recipes.target.reason.DISCONNECTED"] = "The bridge disconnected before the targeted craft completed.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "The bridge returned an invalid targeted crafting response.", ["character.tab.skills"] = "Skills", ["character.tab.reputations"] = "Reputations", ["character.tab.emblems"] = "Emblems", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index 8b1a937..c7aa10d 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local esESValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Despojar siempre: añadir objeto", + ["loot.item.remove"] = "Despojar siempre: retirar objeto", + ["loot.item.bridge.required"] = "Las reglas exactas de botín por objeto requieren LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "La ventana de entrada del objeto no está disponible.", + ["loot.item.add.prompt"] = "ID o enlace del objeto que se debe despojar siempre", + ["loot.item.remove.prompt"] = "ID o enlace del objeto que se debe retirar de Despojar siempre", + ["loot.item.invalid"] = "ID o enlace de objeto no válido.", + ["loot.item.send.failed"] = "No se envió la solicitud de regla de botín por objeto.", + ["loot.item.result.added"] = "%s - añadido en %d bot(s).", + ["loot.item.result.removed"] = "%s - retirado en %d bot(s).", + ["loot.item.result.already_present"] = "%s - ya presente en %d bot(s).", + ["loot.item.result.already_absent"] = "%s - ya ausente en %d bot(s).", + ["loot.item.result.partial"] = "%s - actualizado parcialmente (%d/%d bot(s)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d bot(s)).", + ["loot.item.result.failed"] = "%s - error en la regla de botín: %s.", + ["loot.item.reason.RATE_LIMIT"] = "demasiadas solicitudes; inténtalo de nuevo en unos segundos", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "sesión del jugador no disponible", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "el jugador no está en el mundo", + ["loot.item.reason.DUPLICATE"] = "solicitud duplicada", + ["loot.item.reason.BAD_ACTION"] = "acción no válida", + ["loot.item.reason.INVALID_ITEM"] = "objeto no válido", + ["loot.item.reason.NO_BOTS"] = "no hay bots elegibles", + ["loot.item.reason.TOO_MANY_BOTS"] = "demasiados bots seleccionados", + ["loot.item.reason.FORBIDDEN"] = "control del bot no permitido", + ["loot.item.reason.NO_BOT_SESSION"] = "sesión del bot no disponible", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "el bot no está en el mundo", + ["loot.item.reason.BOT_DEAD"] = "el bot está muerto", + ["loot.item.reason.NO_BOT_CONTEXT"] = "contexto de IA del bot no disponible", + ["loot.item.reason.PERSISTENCE_BUSY"] = "la persistencia está ocupada; inténtalo de nuevo en unos segundos", + ["loot.item.reason.FAILED"] = "operación fallida", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Especialización %s aplicada correctamente a %s.", + ["talent.spec.apply.failed"] = "No se pudo aplicar la especialización %s a %s (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Talentos aplicados correctamente a %s.", + ["talent.apply.failed"] = "No se pudieron aplicar los talentos a %s (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "El bridge no está disponible: la acción de SelfBot no se ha enviado.", ["selfaction.failed"] = "La acción de SelfBot ha fallado: %s", @@ -84,6 +124,30 @@ local esESValues = { ["profession.recipes.craft.reason.CHANNELING"] = "El bot ya está canalizando un hechizo.", ["profession.recipes.craft.reason.cast_code"] = "El servidor rechazó el lanzamiento (código %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "El servidor devolvió un error de fabricación desconocido.", + ["profession.recipes.target.apply"] = "Aplicar", + ["profession.recipes.target.unavailable"] = "El apuntado exacto de objetos mediante el bridge no está disponible.", + ["profession.recipes.target.select"] = "Selecciona el objeto exacto que quieres modificar en el inventario o equipo del bot.", + ["profession.recipes.target.send_failed"] = "No se pudo enviar la solicitud de fabricación dirigida.", + ["profession.recipes.target.pending"] = "Aplicando la receta al objeto seleccionado...", + ["profession.recipes.target.wrong_bot"] = "Selecciona un objeto que pertenezca al bot cuya receta está abierta.", + ["profession.recipes.target.exact_required"] = "Este objetivo debe proceder de la vista exacta del inventario.", + ["profession.recipes.target.invalid_scope"] = "Elige un objeto equipado o un objeto de la mochila o de las bolsas equipadas.", + ["profession.recipes.target.ok"] = "La receta se aplicó al objeto seleccionado.", + ["profession.recipes.target.err"] = "Falló la fabricación dirigida: %s", + ["profession.recipes.target.failed"] = "La solicitud de fabricación dirigida falló.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "Esa posición de inventario o equipo no está permitida.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "El objeto seleccionado ya no está presente.", + ["profession.recipes.target.reason.TARGET_STALE"] = "La ranura seleccionada ahora contiene otro objeto.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "Esta receta no admite un objeto como objetivo.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "El objeto seleccionado no es un objetivo válido para esta receta.", + ["profession.recipes.target.reason.REPLAY"] = "Esta solicitud dirigida ya fue procesada.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Demasiadas solicitudes de fabricación dirigida. Inténtalo de nuevo en breve.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "El bot no está disponible ahora.", + ["profession.recipes.target.reason.BOT_DEAD"] = "El bot debe estar vivo para usar esta receta.", + ["profession.recipes.target.reason.FORBIDDEN"] = "No tienes permiso para controlar este bot.", + ["profession.recipes.target.reason.TIMEOUT"] = "La solicitud de fabricación dirigida agotó el tiempo de espera.", + ["profession.recipes.target.reason.DISCONNECTED"] = "El bridge se desconectó antes de completar la fabricación dirigida.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "El bridge devolvió una respuesta de fabricación dirigida no válida.", ["character.tab.skills"] = "Habilidades", ["character.tab.reputations"] = "Reputaciones", ["character.tab.emblems"] = "Emblemas", diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index ec5b49a..334b305 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local frFRValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Toujours ramasser : ajouter un objet", + ["loot.item.remove"] = "Toujours ramasser : retirer un objet", + ["loot.item.bridge.required"] = "Les règles exactes de butin par objet nécessitent LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "La fenêtre de saisie de l'objet est indisponible.", + ["loot.item.add.prompt"] = "ID ou lien de l'objet à toujours ramasser", + ["loot.item.remove.prompt"] = "ID ou lien de l'objet à retirer de Toujours ramasser", + ["loot.item.invalid"] = "ID ou lien d'objet invalide.", + ["loot.item.send.failed"] = "La demande de règle de butin par objet n'a pas été envoyée.", + ["loot.item.result.added"] = "%s - ajouté à la liste de %d bot(s).", + ["loot.item.result.removed"] = "%s - retiré de la liste de %d bot(s).", + ["loot.item.result.already_present"] = "%s - déjà présent sur %d bot(s).", + ["loot.item.result.already_absent"] = "%s - déjà absent sur %d bot(s).", + ["loot.item.result.partial"] = "%s - mise à jour partielle (%d/%d bot(s)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d bot(s)).", + ["loot.item.result.failed"] = "%s - échec de la règle de butin : %s.", + ["loot.item.reason.RATE_LIMIT"] = "trop de requêtes ; réessayez dans quelques secondes", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "session du joueur indisponible", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "le joueur n'est pas dans le monde", + ["loot.item.reason.DUPLICATE"] = "requête en double", + ["loot.item.reason.BAD_ACTION"] = "action invalide", + ["loot.item.reason.INVALID_ITEM"] = "objet invalide", + ["loot.item.reason.NO_BOTS"] = "aucun bot éligible", + ["loot.item.reason.TOO_MANY_BOTS"] = "trop de bots sélectionnés", + ["loot.item.reason.FORBIDDEN"] = "contrôle du bot interdit", + ["loot.item.reason.NO_BOT_SESSION"] = "session du bot indisponible", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "le bot n'est pas dans le monde", + ["loot.item.reason.BOT_DEAD"] = "le bot est mort", + ["loot.item.reason.NO_BOT_CONTEXT"] = "contexte IA du bot indisponible", + ["loot.item.reason.PERSISTENCE_BUSY"] = "persistance occupée ; réessayez dans quelques secondes", + ["loot.item.reason.FAILED"] = "opération échouée", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Spécialisation %s appliquée avec succès à %s.", + ["talent.spec.apply.failed"] = "Échec de l'application de la spécialisation %s à %s (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Talents appliqués avec succès à %s.", + ["talent.apply.failed"] = "Échec de l'application des talents à %s (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "Le bridge est indisponible : l'action SelfBot n'a pas été envoyée.", ["selfaction.failed"] = "Échec de l'action SelfBot : %s", @@ -84,6 +124,30 @@ local frFRValues = { ["profession.recipes.craft.reason.CHANNELING"] = "Le bot canalise déjà un sort.", ["profession.recipes.craft.reason.cast_code"] = "Le serveur a refusé le cast (code %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "Le serveur a renvoyé une erreur de fabrication inconnue.", + ["profession.recipes.target.apply"] = "Appliquer", + ["profession.recipes.target.unavailable"] = "Le ciblage exact d'objet via le bridge est indisponible.", + ["profession.recipes.target.select"] = "Sélectionnez l'objet exact à modifier dans l'inventaire ou l'équipement du bot.", + ["profession.recipes.target.send_failed"] = "La requête de craft ciblé n'a pas pu être envoyée.", + ["profession.recipes.target.pending"] = "Application de la recette sur l'objet sélectionné...", + ["profession.recipes.target.wrong_bot"] = "Sélectionnez un objet appartenant au bot dont la recette est ouverte.", + ["profession.recipes.target.exact_required"] = "Cette cible doit provenir de la vue d'inventaire exacte.", + ["profession.recipes.target.invalid_scope"] = "Choisissez un objet équipé ou un objet du sac à dos ou des sacs équipés.", + ["profession.recipes.target.ok"] = "La recette a été appliquée à l'objet sélectionné.", + ["profession.recipes.target.err"] = "Échec du craft ciblé : %s", + ["profession.recipes.target.failed"] = "La requête de craft ciblé a échoué.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "Cette position d'inventaire ou d'équipement n'est pas autorisée.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "L'objet sélectionné n'est plus présent.", + ["profession.recipes.target.reason.TARGET_STALE"] = "L'emplacement sélectionné contient maintenant un autre objet.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "Cette recette n'accepte pas de cible objet.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "L'objet sélectionné n'est pas une cible valide pour cette recette.", + ["profession.recipes.target.reason.REPLAY"] = "Cette requête ciblée a déjà été traitée.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Trop de requêtes de craft ciblé. Réessayez dans un instant.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "Le bot n'est pas disponible actuellement.", + ["profession.recipes.target.reason.BOT_DEAD"] = "Le bot doit être vivant pour utiliser cette recette.", + ["profession.recipes.target.reason.FORBIDDEN"] = "Vous n'êtes pas autorisé à contrôler ce bot.", + ["profession.recipes.target.reason.TIMEOUT"] = "La requête de craft ciblé a expiré.", + ["profession.recipes.target.reason.DISCONNECTED"] = "Le bridge s'est déconnecté avant la fin du craft ciblé.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "Le bridge a renvoyé une réponse de craft ciblé invalide.", ["character.tab.skills"] = "Compétences", ["character.tab.reputations"] = "Réputations", ["character.tab.emblems"] = "Monaies", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index d48dfcc..c4ef7e8 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local koKRValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "항상 획득: 아이템 추가", + ["loot.item.remove"] = "항상 획득: 아이템 제거", + ["loot.item.bridge.required"] = "아이템별 정확한 전리품 규칙에는 LOOT_RULE_ITEM_V1이 필요합니다.", + ["loot.item.prompt.required"] = "아이템 입력 창을 사용할 수 없습니다.", + ["loot.item.add.prompt"] = "항상 획득에 추가할 아이템 ID 또는 링크", + ["loot.item.remove.prompt"] = "항상 획득에서 제거할 아이템 ID 또는 링크", + ["loot.item.invalid"] = "아이템 ID 또는 아이템 링크가 올바르지 않습니다.", + ["loot.item.send.failed"] = "아이템 전리품 규칙 요청을 보내지 못했습니다.", + ["loot.item.result.added"] = "%s - %d개 봇에 추가했습니다.", + ["loot.item.result.removed"] = "%s - %d개 봇에서 제거했습니다.", + ["loot.item.result.already_present"] = "%s - %d개 봇에 이미 있습니다.", + ["loot.item.result.already_absent"] = "%s - %d개 봇에 이미 없습니다.", + ["loot.item.result.partial"] = "%s - 일부만 업데이트했습니다 (%d/%d개 봇).", + ["loot.item.result.ok"] = "%s - %s (%d/%d개 봇).", + ["loot.item.result.failed"] = "%s - 전리품 규칙 실패: %s.", + ["loot.item.reason.RATE_LIMIT"] = "요청이 너무 많습니다. 잠시 후 다시 시도하세요", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "플레이어 세션을 사용할 수 없습니다", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "플레이어가 월드에 없습니다", + ["loot.item.reason.DUPLICATE"] = "중복 요청", + ["loot.item.reason.BAD_ACTION"] = "잘못된 동작", + ["loot.item.reason.INVALID_ITEM"] = "잘못된 아이템", + ["loot.item.reason.NO_BOTS"] = "대상 봇이 없습니다", + ["loot.item.reason.TOO_MANY_BOTS"] = "선택된 봇이 너무 많습니다", + ["loot.item.reason.FORBIDDEN"] = "봇 제어 권한이 없습니다", + ["loot.item.reason.NO_BOT_SESSION"] = "봇 세션을 사용할 수 없습니다", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "봇이 월드에 없습니다", + ["loot.item.reason.BOT_DEAD"] = "봇이 죽어 있습니다", + ["loot.item.reason.NO_BOT_CONTEXT"] = "봇 AI 컨텍스트를 사용할 수 없습니다", + ["loot.item.reason.PERSISTENCE_BUSY"] = "저장 처리가 바쁩니다. 잠시 후 다시 시도하세요", + ["loot.item.reason.FAILED"] = "작업 실패", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "%s 특성이 %s에게 성공적으로 적용되었습니다.", + ["talent.spec.apply.failed"] = "%s 특성을 %s에게 적용하지 못했습니다. (%s)", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "%s에게 특성이 성공적으로 적용되었습니다.", + ["talent.apply.failed"] = "%s에게 특성을 적용하지 못했습니다. (%s)", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "브리지를 사용할 수 없어 SelfBot 동작을 보내지 못했습니다.", ["selfaction.failed"] = "SelfBot 동작 실패: %s", @@ -84,6 +124,30 @@ local koKRValues = { ["profession.recipes.craft.reason.CHANNELING"] = "봇이 이미 주문을 시전 중입니다.", ["profession.recipes.craft.reason.cast_code"] = "서버가 시전을 거부했습니다(코드 %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "서버가 알 수 없는 제작 오류를 반환했습니다.", + ["profession.recipes.target.apply"] = "적용", + ["profession.recipes.target.unavailable"] = "Bridge를 통한 정확한 아이템 대상 지정을 사용할 수 없습니다.", + ["profession.recipes.target.select"] = "봇의 가방이나 장비에서 수정할 정확한 아이템을 선택하세요.", + ["profession.recipes.target.send_failed"] = "대상 제작 요청을 전송하지 못했습니다.", + ["profession.recipes.target.pending"] = "선택한 아이템에 제작법을 적용하는 중...", + ["profession.recipes.target.wrong_bot"] = "현재 제작법을 연 봇의 아이템을 선택하세요.", + ["profession.recipes.target.exact_required"] = "대상은 정확한 인벤토리 보기에서 선택해야 합니다.", + ["profession.recipes.target.invalid_scope"] = "장착 아이템, 배낭 아이템 또는 장착한 가방의 아이템을 선택하세요.", + ["profession.recipes.target.ok"] = "선택한 아이템에 제작법을 적용했습니다.", + ["profession.recipes.target.err"] = "대상 제작 실패: %s", + ["profession.recipes.target.failed"] = "대상 제작 요청에 실패했습니다.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "해당 인벤토리 또는 장비 위치는 허용되지 않습니다.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "선택한 아이템이 더 이상 존재하지 않습니다.", + ["profession.recipes.target.reason.TARGET_STALE"] = "선택한 슬롯에 이제 다른 아이템이 있습니다.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "이 제작법은 아이템을 대상으로 사용할 수 없습니다.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "선택한 아이템은 이 제작법의 유효한 대상이 아닙니다.", + ["profession.recipes.target.reason.REPLAY"] = "이 대상 요청은 이미 처리되었습니다.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "대상 제작 요청이 너무 많습니다. 잠시 후 다시 시도하세요.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "현재 봇을 사용할 수 없습니다.", + ["profession.recipes.target.reason.BOT_DEAD"] = "이 제작법을 사용하려면 봇이 살아 있어야 합니다.", + ["profession.recipes.target.reason.FORBIDDEN"] = "이 봇을 제어할 권한이 없습니다.", + ["profession.recipes.target.reason.TIMEOUT"] = "대상 제작 요청 시간이 초과되었습니다.", + ["profession.recipes.target.reason.DISCONNECTED"] = "대상 제작이 완료되기 전에 Bridge 연결이 끊어졌습니다.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "Bridge가 잘못된 대상 제작 응답을 반환했습니다.", ["character.tab.skills"] = "기술", ["character.tab.reputations"] = "평판", ["character.tab.emblems"] = "엠블렘", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index edebc4b..0dda507 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local ruRUValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "Всегда собирать: добавить предмет", + ["loot.item.remove"] = "Всегда собирать: убрать предмет", + ["loot.item.bridge.required"] = "Точные правила добычи для предметов требуют LOOT_RULE_ITEM_V1.", + ["loot.item.prompt.required"] = "Окно ввода предмета недоступно.", + ["loot.item.add.prompt"] = "ID или ссылка предмета для списка «Всегда собирать»", + ["loot.item.remove.prompt"] = "ID или ссылка предмета для удаления из списка «Всегда собирать»", + ["loot.item.invalid"] = "Недопустимый ID или ссылка предмета.", + ["loot.item.send.failed"] = "Запрос правила добычи для предмета не был отправлен.", + ["loot.item.result.added"] = "%s - добавлено для %d бот(ов).", + ["loot.item.result.removed"] = "%s - удалено для %d бот(ов).", + ["loot.item.result.already_present"] = "%s - уже есть у %d бот(ов).", + ["loot.item.result.already_absent"] = "%s - уже отсутствует у %d бот(ов).", + ["loot.item.result.partial"] = "%s - обновлено частично (%d/%d бот(ов)).", + ["loot.item.result.ok"] = "%s - %s (%d/%d бот(ов)).", + ["loot.item.result.failed"] = "%s - ошибка правила добычи: %s.", + ["loot.item.reason.RATE_LIMIT"] = "слишком много запросов; повторите через несколько секунд", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "сессия игрока недоступна", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "игрок не находится в мире", + ["loot.item.reason.DUPLICATE"] = "повторный запрос", + ["loot.item.reason.BAD_ACTION"] = "недопустимое действие", + ["loot.item.reason.INVALID_ITEM"] = "недопустимый предмет", + ["loot.item.reason.NO_BOTS"] = "нет подходящих ботов", + ["loot.item.reason.TOO_MANY_BOTS"] = "выбрано слишком много ботов", + ["loot.item.reason.FORBIDDEN"] = "управление ботом запрещено", + ["loot.item.reason.NO_BOT_SESSION"] = "сессия бота недоступна", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "бот не находится в мире", + ["loot.item.reason.BOT_DEAD"] = "бот мёртв", + ["loot.item.reason.NO_BOT_CONTEXT"] = "контекст ИИ бота недоступен", + ["loot.item.reason.PERSISTENCE_BUSY"] = "сохранение занято; повторите через несколько секунд", + ["loot.item.reason.FAILED"] = "операция завершилась ошибкой", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "Специализация %s успешно применена к %s.", + ["talent.spec.apply.failed"] = "Не удалось применить специализацию %s к %s (%s).", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "Таланты успешно применены к %s.", + ["talent.apply.failed"] = "Не удалось применить таланты к %s (%s).", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "Мост недоступен: действие SelfBot не было отправлено.", ["selfaction.failed"] = "Ошибка действия SelfBot: %s", @@ -84,6 +124,30 @@ local ruRUValues = { ["profession.recipes.craft.reason.CHANNELING"] = "Бот уже поддерживает заклинание.", ["profession.recipes.craft.reason.cast_code"] = "Сервер отклонил применение (код %s).", ["profession.recipes.craft.reason.UNKNOWN"] = "Сервер вернул неизвестную ошибку создания предмета.", + ["profession.recipes.target.apply"] = "Применить", + ["profession.recipes.target.unavailable"] = "Точный выбор предмета через bridge недоступен.", + ["profession.recipes.target.select"] = "Выберите точный предмет в инвентаре или экипировке бота.", + ["profession.recipes.target.send_failed"] = "Не удалось отправить запрос на применение рецепта к предмету.", + ["profession.recipes.target.pending"] = "Рецепт применяется к выбранному предмету...", + ["profession.recipes.target.wrong_bot"] = "Выберите предмет того бота, для которого открыт рецепт.", + ["profession.recipes.target.exact_required"] = "Цель должна быть выбрана в точном представлении инвентаря.", + ["profession.recipes.target.invalid_scope"] = "Выберите экипированный предмет или предмет из рюкзака либо надетых сумок.", + ["profession.recipes.target.ok"] = "Рецепт применён к выбранному предмету.", + ["profession.recipes.target.err"] = "Не удалось применить рецепт к предмету: %s", + ["profession.recipes.target.failed"] = "Запрос на применение рецепта к предмету завершился ошибкой.", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "Эта позиция инвентаря или экипировки недопустима.", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "Выбранного предмета больше нет.", + ["profession.recipes.target.reason.TARGET_STALE"] = "В выбранной ячейке теперь находится другой предмет.", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "Этот рецепт не принимает предмет в качестве цели.", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "Выбранный предмет не подходит для этого рецепта.", + ["profession.recipes.target.reason.REPLAY"] = "Этот запрос уже был обработан.", + ["profession.recipes.target.reason.RATE_LIMIT"] = "Слишком много запросов. Повторите попытку позже.", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "Бот сейчас недоступен.", + ["profession.recipes.target.reason.BOT_DEAD"] = "Бот должен быть жив, чтобы использовать этот рецепт.", + ["profession.recipes.target.reason.FORBIDDEN"] = "У вас нет права управлять этим ботом.", + ["profession.recipes.target.reason.TIMEOUT"] = "Время ожидания запроса истекло.", + ["profession.recipes.target.reason.DISCONNECTED"] = "Bridge отключился до завершения применения рецепта.", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "Bridge вернул некорректный ответ на запрос.", ["character.tab.skills"] = "Навыки", ["character.tab.reputations"] = "Репутации", ["character.tab.emblems"] = "Знаки", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index d64c50c..26de0d8 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -4,6 +4,46 @@ if type(register) ~= "function" then end local zhCNValues = { + -- MB_LOOT_RULE_ITEM_I18N_V1_BEGIN + ["loot.item.add"] = "始终拾取:添加物品", + ["loot.item.remove"] = "始终拾取:移除物品", + ["loot.item.bridge.required"] = "精确的按物品拾取规则需要 LOOT_RULE_ITEM_V1。", + ["loot.item.prompt.required"] = "物品输入窗口不可用。", + ["loot.item.add.prompt"] = "要加入始终拾取的物品 ID 或链接", + ["loot.item.remove.prompt"] = "要从始终拾取移除的物品 ID 或链接", + ["loot.item.invalid"] = "无效的物品 ID 或物品链接。", + ["loot.item.send.failed"] = "物品拾取规则请求未发送。", + ["loot.item.result.added"] = "%s - 已添加到 %d 个机器人。", + ["loot.item.result.removed"] = "%s - 已从 %d 个机器人移除。", + ["loot.item.result.already_present"] = "%s - 已存在于 %d 个机器人。", + ["loot.item.result.already_absent"] = "%s - 已在 %d 个机器人中不存在。", + ["loot.item.result.partial"] = "%s - 部分更新(%d/%d 个机器人)。", + ["loot.item.result.ok"] = "%s - %s(%d/%d 个机器人)。", + ["loot.item.result.failed"] = "%s - 拾取规则失败:%s。", + ["loot.item.reason.RATE_LIMIT"] = "请求过多;请稍后重试", + ["loot.item.reason.NO_REQUESTER_SESSION"] = "玩家会话不可用", + ["loot.item.reason.REQUESTER_NOT_IN_WORLD"] = "玩家不在世界中", + ["loot.item.reason.DUPLICATE"] = "重复请求", + ["loot.item.reason.BAD_ACTION"] = "无效操作", + ["loot.item.reason.INVALID_ITEM"] = "无效物品", + ["loot.item.reason.NO_BOTS"] = "没有符合条件的机器人", + ["loot.item.reason.TOO_MANY_BOTS"] = "选择的机器人过多", + ["loot.item.reason.FORBIDDEN"] = "无权控制机器人", + ["loot.item.reason.NO_BOT_SESSION"] = "机器人会话不可用", + ["loot.item.reason.BOT_NOT_IN_WORLD"] = "机器人不在世界中", + ["loot.item.reason.BOT_DEAD"] = "机器人已死亡", + ["loot.item.reason.NO_BOT_CONTEXT"] = "机器人 AI 上下文不可用", + ["loot.item.reason.PERSISTENCE_BUSY"] = "持久化繁忙;请稍后重试", + ["loot.item.reason.FAILED"] = "操作失败", + -- MB_LOOT_RULE_ITEM_I18N_V1_END + -- MB_TALENT_SPEC_APPLY_I18N_V1_BEGIN + ["talent.spec.apply.success"] = "已成功将天赋专精 %s 应用于 %s。", + ["talent.spec.apply.failed"] = "无法将天赋专精 %s 应用于 %s(%s)。", + -- MB_TALENT_SPEC_APPLY_I18N_V1_END + -- MB_TALENT_APPLY_I18N_V1_BEGIN + ["talent.apply.success"] = "已成功为 %s 应用天赋。", + ["talent.apply.failed"] = "为 %s 应用天赋失败(%s)。", + -- MB_TALENT_APPLY_I18N_V1_END -- MB_SELFACTION_I18N_V1_BEGIN ["selfaction.bridge_unavailable"] = "Bridge 不可用:SelfBot 操作未发送。", ["selfaction.failed"] = "SelfBot 操作失败:%s", @@ -84,6 +124,30 @@ local zhCNValues = { ["profession.recipes.craft.reason.CHANNELING"] = "机器人已经在引导法术。", ["profession.recipes.craft.reason.cast_code"] = "服务器拒绝施法(代码 %s)。", ["profession.recipes.craft.reason.UNKNOWN"] = "服务器返回了未知的制造错误。", + ["profession.recipes.target.apply"] = "应用", + ["profession.recipes.target.unavailable"] = "Bridge 当前不支持精确物品目标。", + ["profession.recipes.target.select"] = "请在机器人的背包或装备中选择要修改的准确物品。", + ["profession.recipes.target.send_failed"] = "无法发送定向制作请求。", + ["profession.recipes.target.pending"] = "正在把配方应用到所选物品...", + ["profession.recipes.target.wrong_bot"] = "请选择当前打开配方所属机器人的物品。", + ["profession.recipes.target.exact_required"] = "目标必须来自精确背包视图。", + ["profession.recipes.target.invalid_scope"] = "请选择已装备物品、背包物品或已装备容器中的物品。", + ["profession.recipes.target.ok"] = "配方已应用到所选物品。", + ["profession.recipes.target.err"] = "定向制作失败:%s", + ["profession.recipes.target.failed"] = "定向制作请求失败。", + ["profession.recipes.target.reason.BAD_TARGET_POSITION"] = "该背包或装备位置不允许作为目标。", + ["profession.recipes.target.reason.MISSING_TARGET_ITEM"] = "所选物品已不存在。", + ["profession.recipes.target.reason.TARGET_STALE"] = "所选槽位现在包含其他物品。", + ["profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE"] = "此配方不能以物品为目标。", + ["profession.recipes.target.reason.INVALID_TARGET_ITEM"] = "所选物品不是此配方的有效目标。", + ["profession.recipes.target.reason.REPLAY"] = "该定向请求已经处理。", + ["profession.recipes.target.reason.RATE_LIMIT"] = "定向制作请求过多,请稍后重试。", + ["profession.recipes.target.reason.BOT_UNAVAILABLE"] = "机器人当前不可用。", + ["profession.recipes.target.reason.BOT_DEAD"] = "机器人必须存活才能使用此配方。", + ["profession.recipes.target.reason.FORBIDDEN"] = "你无权控制此机器人。", + ["profession.recipes.target.reason.TIMEOUT"] = "定向制作请求超时。", + ["profession.recipes.target.reason.DISCONNECTED"] = "Bridge 在定向制作完成前断开连接。", + ["profession.recipes.target.reason.BAD_RESPONSE"] = "Bridge 返回了无效的定向制作响应。", ["character.tab.skills"] = "技能", ["character.tab.reputations"] = "声望", ["character.tab.emblems"] = "徽章", diff --git a/README.md b/README.md index 0c2360e..ac05b79 100644 --- a/README.md +++ b/README.md @@ -104,9 +104,12 @@ GET~QUESTS GET~GAMEOBJECTS GET~FORMATIONS RUN~CRAFT_RECIPE +RUN~CRAFT_RECIPE_TARGET RUN~ITEM_ACTION RUN~ITEM_EQUIP RUN~ITEM_UNEQUIP +RUN~ITEM_TRADE +RUN~QUEST_ABANDON RUN~OUTFIT RUN~RTI RUN~COMBAT @@ -145,6 +148,7 @@ INVENTORY_EXACT_V1 ITEM_MOVE_V1 ITEM_EQUIP_V1 ITEM_UNEQUIP_V1 +ITEM_TRADE_V1 ITEM_USE_V1 ITEM_SELL_SINGLE_V1 VENDOR_BUYBACK_V1 @@ -152,13 +156,22 @@ INVENTORY_BULK_SELL_V1 INVENTORY_OPEN_V1 GROUP_ROLL_V1 ENCHANT_TRADE_V1 +CRAFT_RECIPE_TARGET_V1 +QUEST_ABANDON_V1 +SELF_BOT_V1 +SELF_STRATEGY_V1 +SELF_ACTION_V1 ``` `STATE_FRAMING_V1` uses tokenized `STATE` / `STATES` transactions with framed responses, bounded payloads, cleanup on terminal errors/timeouts, and stale-response protection. Per-bot requests use a 5-second timeout; global state requests use a 15-second timeout. `STRATEGY_MUTATION_V1` provides structured `co/nc` mutations through `RUN~STRATEGY` and completion through `STRATEGY_ACK`. The bridge reports matched, succeeded and failed bot counts, while the addon applies explicit timeout and rejection diagnostics. -`INVENTORY_V1` provides the established native inventory read/refresh path. `INVENTORY_EXACT_V1` complements it with exact physical topology for Backpack, Bag 1..4 and Keyring, including empty slots and per-container filtering in the inventory UI. `ITEM_MOVE_V1` adds server-authoritative whole-stack drag/drop between allowed physical slots. The addon keeps only synthetic drag state: it does not call `PickupContainerItem`, `PickupInventoryItem`, `GetCursorInfo` or `ClearCursor`, and it does not mutate the displayed inventory optimistically; an exact snapshot refresh follows the server result. Stack splitting remains outside this capability. `ITEM_EQUIP_V1` equips an exact item from Backpack or Bag 1..4 through a structured bridge request and waits for the authoritative result before refreshing. `ITEM_UNEQUIP_V1` routes Inspect right-click through the exact equipment slot plus item ID, converts client Inspect slots 1..19 to Core slots 0..18, waits for the structured result and then refreshes. The historical `ue` whisper fallback is used only when `MultiBot.allowLegacyChatFallback == true`; normal bridge-first configuration keeps that fallback disabled. `ITEM_USE_V1` uses the exact physical source, waits for the structured `INVENTORY_ITEM_USE` result and delegates execution to the native use-item path. `ITEM_DESTROY` is a specialized exact-item destruction path with server-side source revalidation and an authoritative result. `ITEM_SELL_SINGLE_V1` validates the exact source and nearby vendor before native single-item sale and returns `INVENTORY_ITEM_SELL`. `VENDOR_BUYBACK_V1` exposes a structured Buyback list/result flow and uses the native Buyback handler before authoritative inventory/list refreshes. `INVENTORY_BULK_SELL_V1` and `INVENTORY_OPEN_V1` gate the current bulk-sell and `OPEN_ITEMS` bridge paths. `GROUP_ROLL_V1` gates the group Roll workflow; normal rolls and item-linked rolls are tokenized and completed through a structured `GROUP_ROLL_ACK`. `ENCHANT_TRADE_V1` gates the Enchanting Trade Service: the addon lists only known Enchanting spells exposed by the bot, uses the native WoW Trade window and the non-traded item slot, then requests one validated numeric spell ID through the bridge. +`INVENTORY_V1` provides the established native inventory read/refresh path. `INVENTORY_EXACT_V1` complements it with exact physical topology for Backpack, Bag 1..4 and Keyring, including empty slots and per-container filtering in the inventory UI. `ITEM_MOVE_V1` adds server-authoritative whole-stack drag/drop between allowed physical slots. The addon keeps only synthetic drag state: it does not call `PickupContainerItem`, `PickupInventoryItem`, `GetCursorInfo` or `ClearCursor`, and it does not mutate the displayed inventory optimistically; an exact snapshot refresh follows the server result. Stack splitting remains outside this capability. `ITEM_EQUIP_V1` equips an exact item from Backpack or Bag 1..4 through a structured bridge request and waits for the authoritative result before refreshing. `ITEM_UNEQUIP_V1` routes Inspect right-click through the exact equipment slot plus item ID, converts client Inspect slots 1..19 to Core slots 0..18, waits for the structured result and then refreshes. `ITEM_TRADE_V1` routes Inventory -> Trade through an exact source identity, preserves the native WoW Trade UI, waits for the structured `INVENTORY_ITEM_TRADE` result and keeps the historical give path behind the explicit compatibility fallback flag. The historical `ue` whisper fallback is used only when `MultiBot.allowLegacyChatFallback == true`; normal bridge-first configuration keeps that fallback disabled. `ITEM_USE_V1` uses the exact physical source, waits for the structured `INVENTORY_ITEM_USE` result and delegates execution to the native use-item path. `ITEM_DESTROY` is a specialized exact-item destruction path with server-side source revalidation and an authoritative result. `ITEM_SELL_SINGLE_V1` validates the exact source and nearby vendor before native single-item sale and returns `INVENTORY_ITEM_SELL`. `VENDOR_BUYBACK_V1` exposes a structured Buyback list/result flow and uses the native Buyback handler before authoritative inventory/list refreshes. `INVENTORY_BULK_SELL_V1` gates the current bulk-sell path. Normal `SELL_VENDOR` operation is bridge-first; the historical `s vendor` whisper is reachable only when `MultiBot.allowLegacyChatFallback == true`. The validated Bridge path accepts `ITEM_USAGE_VENDOR` only for `SELL_VENDOR` and excludes `ITEM_USAGE_AH`. `INVENTORY_OPEN_V1` gates the current `OPEN_ITEMS` bridge path. `GROUP_ROLL_V1` gates the group Roll workflow; normal rolls and item-linked rolls are tokenized and completed through a structured `GROUP_ROLL_ACK`. `ENCHANT_TRADE_V1` gates the Enchanting Trade Service: the addon lists only known Enchanting spells exposed by the bot, uses the native WoW Trade window and the non-traded item slot, then requests one validated numeric spell ID through the bridge. `QUEST_ABANDON_V1` routes bot quest abandon through a tokenized structured request/result; the player still abandons locally with the native WoW quest API, while the legacy `drop` group-chat fallback is available only when `MultiBot.allowLegacyChatFallback == true`. Quest sharing remains intentionally native through `QuestLogPushQuest()` and does not require a `QUEST_SHARE_V1` bridge capability. + +`CRAFT_RECIPE_TARGET_V1` handles profession recipes that require an exact bot-owned item target. Normal `RUN~CRAFT_RECIPE` remains unchanged for ordinary crafting and returns `TARGET_REQUIRED` for exact-item recipes. The addon then reuses the selected bot's Inventory and Inspect views, sends the exact `bag` / `slot` / `itemId` identity, keeps at most 8 pending target requests with a 5-second timeout and consumes `CRAFT_RECIPE_TARGET_RESULT` through the structured opcode dispatcher. Target selection is limited to equipment, Backpack and equipped Bag 1..4; Bank, Keyring and player Trade items are outside this capability. Recipes with `craftable > 0` are highlighted with a bright-green name, while non-craftable recipes keep their existing difficulty color. + +`SELF_BOT_V1` controls the player's own SelfBot mode with explicit ENABLE/DISABLE requests and authoritative state/result replies. `SELF_STRATEGY_V1` reads and mutates only the active SelfBot's whitelisted combat/non-combat strategies through framed state plus `SELF_STRATEGY_ACK`; it is not a generic strategy executor. `SELF_ACTION_V1` exposes only the audited SelfBot actions `AUTOGEAR`, `MAINTENANCE` and `WAIT_ATTACK_TIME`, with server-side SelfBot/security/rate-limit checks. These SelfBot paths are a separate completed workstream inherited by the Jellypowered v2 branch; normal-bot Maintenance/Autogear paths that still use legacy chat are not implicitly migrated by these capabilities. The migration is intentionally incremental. The Warlock stone, soulstone, pet and curse selectors are now migrated to structured `RUN~STRATEGY` mutations. When those selectors use the bridge, the addon waits for authoritative server `STATE` data before committing the selected UI state instead of applying an optimistic local state. Other specialized legacy UI paths still issue Playerbots chat commands directly and must be migrated before the addon can be described as fully chatless. @@ -234,8 +247,12 @@ The endpoint and safe Firestone/Spellstone switching code are present, but the p
talents spec list chat spamGET~TALENT_SPEC_LIST lists server-side premade templates; TALENT_SPEC_CURRENT returns the authoritative active slot/tree totals, and TALENT_SPEC_APPLY_V1 applies a server-revalidated template index to slot 1 or 2 with dual-spec handling, glyph initialization and structured verification without normal chat commandsTALENT_APPLY_V1 validates the complete custom talent build against the bot class/DBC/available points, applies it through the audited Playerbots factory path and confirms success only after authoritative tree-point verificationINVENTORY_BULK_SELL_V1 routes SELL_VENDOR and the existing SELL_GREY action through the bridge; legacy per-item fallback remains a compatibility path, and further SELL_GREY work is deferredINVENTORY_BULK_SELL_V1 routes SELL_VENDOR and the existing SELL_GREY action through the bridge. For SELL_VENDOR, the legacy s vendor path is available only when MultiBot.allowLegacyChatFallback == true; the Bridge accepts ITEM_USAGE_VENDOR only and excludes ITEM_USAGE_AH. Runtime regression tests preserved Symbol of Kings and Gold Ore. Further SELL_GREY work remains deferred.ITEM_DEPOSIT_EXACT_V1 when available, carrying exact bag/slot/itemId/count source identity and whole-stack semantics. Bank and guild-bank withdrawals remain on the existing non-exact-stack path; exact withdrawals are deferred.RUN~CRAFT_RECIPE; item-target recipes return TARGET_REQUIRED and continue through CRAFT_RECIPE_TARGET_V1 with exact Inventory/Inspect selection by bag/slot/itemId. Recipes currently craftable are highlighted with a bright-green name.ENCHANT_TRADE_V1 exposes known Enchanting services, reagent/tool availability and native Trade-slot execution without a generic cast/chat executor; the same dedicated window is available from the enchanter EveryBar and Character Info, with UI text localized in all eight runtime localesENCHANT_TRADE_V1 exposes known Enchanting services, reagent/tool availability and native Trade-slot execution without a generic cast/chat executor; the same dedicated window is available from the enchanter EveryBar and Character Info, with UI text localized in all eight runtime locales. Enchantments for which all reagents and required tools are present are highlighted with a bright-green name.RUN~LOOTRUN~LOOT; exact always-loot item add/remove uses negotiated LOOT_RULE_ITEM_V1 with structured results and no normal chat/whisper path.RUN~LOOTRUN~LOOT; exact always-loot item add/remove uses negotiated LOOT_RULE_ITEM_V1 with structured results and no normal chat/whisper path.