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 Bridge-first - Talent spec lists - Bridge-first template listing without automatic talents spec list chat spam + Talent spec templates + Bridge-first and runtime validatedGET~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 commands + + + Custom talent apply + Bridge-first and runtime validatedTALENT_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 verification Inventory @@ -271,7 +288,7 @@ The endpoint and safe Firestone/Spellstone switching code are present, but the p Inventory bulk sell - Bridge-first when supportedINVENTORY_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 deferred + Bridge-first and runtime revalidatedINVENTORY_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. Open items @@ -291,15 +308,15 @@ The endpoint and safe Firestone/Spellstone switching code are present, but the p Bot bank / guild bank / vendor buy - Bridge-first bank snapshots, guild bank snapshots, bank deposit/withdraw, guild bank deposit/withdraw and vendor buy actions + Bridge-first bank and guild-bank snapshots plus vendor buy actions. `BANK_DEPOSIT` and `GBANK_DEPOSIT` use negotiated 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. Profession recipe frame - Bridge-first recipe listing and recipe crafting opened from Character Info profession and secondary skill rows + Bridge-first and runtime validated — recipe listing and normal crafting remain on 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. Enchanting Trade Service - Bridge-first and runtime validatedENCHANT_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 + Bridge-first and runtime validatedENCHANT_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. Trade inventory chat suppression @@ -310,7 +327,7 @@ The endpoint and safe Firestone/Spellstone switching code are present, but the p Bridge-first with glyph icons and tooltips Loot rules - Bridge-first loot enable/disable and loot list profiles through RUN~LOOT + Bridge-first and runtime validated — loot enable/disable and loot-list profiles remain on RUN~LOOT; exact always-loot item add/remove uses negotiated LOOT_RULE_ITEM_V1 with structured results and no normal chat/whisper path. Loot Master frame @@ -362,7 +379,7 @@ The endpoint and safe Firestone/Spellstone switching code are present, but the p Loot rules - Bridge-first loot enable/disable and loot list profiles through RUN~LOOT + Bridge-first and runtime validated — loot enable/disable and loot-list profiles remain on RUN~LOOT; exact always-loot item add/remove uses negotiated LOOT_RULE_ITEM_V1 with structured results and no normal chat/whisper path. Loot Master frame @@ -601,22 +618,28 @@ Implemented bridge-first / chatless areas: - Whole-stack inventory drag/drop through `ITEM_MOVE_V1`, with synthetic addon drag state, no native player cursor APIs, no optimistic inventory mutation and an exact snapshot refresh after the structured server result. Stack splitting remains out of scope. - Exact inventory equip through `ITEM_EQUIP_V1`, with source identity revalidation, authoritative result handling and no optimistic UI mutation. - Exact Inspect unequip through `ITEM_UNEQUIP_V1`, with exact equipment slot/item identity and legacy `ue` fallback disabled unless explicitly enabled. +- Generic exact-item Trade through `ITEM_TRADE_V1`, with exact source identity, native WoW Trade UI preservation, native AzerothCore trade handling, structured `INVENTORY_ITEM_TRADE` completion and legacy give fallback only when compatibility fallback is explicitly enabled. Runtime validation passed in both trade directions. - Exact item use through `ITEM_USE_V1`, with native use-item execution, source revalidation, structured result handling and localized failure reasons. - Exact item destruction through the specialized `ITEM_DESTROY` path with server-side source revalidation. - Exact single-item vendor sale through `ITEM_SELL_SINGLE_V1`, with nearby-vendor validation, protected-item guards, replay/rate limiting and structured result handling. - Vendor Buyback through `VENDOR_BUYBACK_V1`, with structured list/result messages, native Buyback execution and authoritative inventory/list refreshes. -- Bulk inventory sell through `INVENTORY_BULK_SELL_V1` when supported; `SELL_VENDOR` is bridge-first in normal current operation, while legacy compatibility fallback remains available and SELL_GREY follow-up is deferred. +- Bulk inventory sell through `INVENTORY_BULK_SELL_V1` when supported; `SELL_VENDOR` is bridge-first in normal operation, its historical `s vendor` fallback is reachable only when `MultiBot.allowLegacyChatFallback == true`, and the validated Bridge action accepts `ITEM_USAGE_VENDOR` only while excluding `ITEM_USAGE_AH`. Runtime regression testing preserved Symbol of Kings and Gold Ore; SELL_GREY follow-up remains deferred. - `OPEN_ITEMS` through `INVENTORY_OPEN_V1`, with structured result handling and no silent chat fallback in the normal bridge-first path. - Group Roll through `GROUP_ROLL_V1`: normal 0–100 roll and Shift+click item roll, tokenized pending state, duplicate-send protection, timeout/cleanup handling and structured `GROUP_ROLL_ACK`. +- SelfBot enable/disable through `SELF_BOT_V1`, with explicit desired state, authoritative server verification and legacy `.playerbot bot self` fallback only when compatibility fallback is explicitly enabled. +- SelfBot strategy state/mutation through `SELF_STRATEGY_V1`, restricted to the player's active SelfBot and server-side class/state allowlists. +- SelfBot EveryBar actions through `SELF_ACTION_V1` for `AUTOGEAR`, `MAINTENANCE` and `WAIT_ATTACK_TIME`; this does not migrate equivalent normal-bot legacy chat paths. - Spellbook refresh, with profession/crafting spells separated from the combat spellbook path. - Character Info frame through the bridge with Blizzard-style tabs for class, profession, secondary, weapon and armor skills, reputations and currencies/emblems. -- Bot bank and guild bank snapshots through the bridge, plus bank deposit/withdraw, guild bank deposit/withdraw and vendor buy item actions. -- Profession recipe frame through the bridge, opened from profession and secondary skill rows. -- Enchanting Trade Service through `ENCHANT_TRADE_V1`: dedicated enchanter-only UI from EveryBar/Character Info, known-spell listing, reagent/tool availability, native `TRADE_SLOT_NONTRADED` targeting and validated numeric spell execution without generic Playerbots command/chat dispatch. +- Bot bank and guild bank snapshots through the bridge, plus vendor buy and bank/guild-bank item actions. Exact deposits use `ITEM_DEPOSIT_EXACT_V1`: the addon sends the selected inventory `bag/slot/itemId/count`, waits for the authoritative structured result and refreshes after success. Runtime validation confirmed only the clicked physical stack moves for both BANK and GBANK; stale source identity is rejected with `SOURCE_STALE`. Exact BANK/GBANK withdrawals are deferred because their current snapshots do not expose a selectable physical source stack end to end. +- Profession recipe frame through the bridge, opened from profession and secondary skill rows. Normal recipes use `RUN~CRAFT_RECIPE`; exact-item recipes continue through the runtime-validated `CRAFT_RECIPE_TARGET_V1` flow after `TARGET_REQUIRED`, using exact Inventory/Inspect `bag/slot/itemId` selection. Recipes with `craftable > 0` use a bright-green name. +- Enchanting Trade Service through `ENCHANT_TRADE_V1`: dedicated enchanter-only UI from EveryBar/Character Info, known-spell listing, reagent/tool availability, native `TRADE_SLOT_NONTRADED` targeting and validated numeric spell execution without generic Playerbots command/chat dispatch. Entries with all required reagents and tools available use a bright-green name. - Glyph refresh with icons and glyph tooltips. - Outfits refresh and actions through the bridge. - Outfit equip/replace without detailed `Equipping [item] ...` chat spam. - Quest list refresh through the bridge. +- Quest abandon through `QUEST_ABANDON_V1`: right-click keeps the player's native `SetAbandonQuest()` / `AbandonQuest()` path, sends a structured bot-abandon request through the bridge and emits no legacy `drop` chat in normal bridge-first configuration. Runtime validation with one controlled bot passed with zero chat spam and zero Lua errors; the mixed multi-bot scenario is explicitly deferred until suitable bots are available. +- Quest sharing remains intentionally native/chatless through `QuestLogPushQuest()`; no `QUEST_SHARE_V1` endpoint is required for the current behavior. - Game object search results and copy frame through the bridge. - RTI controls through the bridge. - Pull Control frame through the bridge. @@ -624,7 +647,7 @@ Implemented bridge-first / chatless areas: - Disperse controls through the bridge with `disperse set ` and `disperse disable`. - Party/raid-wide formation application through `RUN~FORMATION`, with per-bot effective formation inspection through `GET~FORMATIONS` and no PARTY/RAID chat output. - Localized formation status tooltip for all eight addon locale files. -- Loot rules through the bridge with `nc +loot`, `nc -loot` and `ll all|normal|gray|quest|skill`. +- Loot rules through the bridge with `nc +loot` and `nc -loot`. For the audited Playerbots build, the verified loot-list modes are `all`, `normal`, `gray` and `disenchant`; the older Quest/Skill wording is not treated as a validated capability and remains a separate roadmap decision. Exact always-loot item add/remove is handled by `LOOT_RULE_ITEM_V1`. - Loot Master UI for master-loot distribution with item tooltips, candidate scoring, profession/spec hints, saved preferences and recent loot history. - Bridge-visible bot discovery for AddClass bots, altbots and grouped randombots. - Custom glyph socket mapping and apply order. @@ -643,16 +666,39 @@ Validated development milestones on the current line: - PR #58 — single-bot inventory Sell Vendor migrated to the bridge. - PR #60 — bridge-first `OPEN_ITEMS`. - PR #61 — chatless Group Roll UI, merged as `106074c3c93f80812f73af27e746860c7c8a4dcf`. +- PR #67 — Jellypowered chatless/inventory integration merged into `main`, merge commit `70e72ba6cb9a7170497b201e0dbe469bb29e6be9`. +- PR #72 — `Complete SelfBot chatless integration`, merged into `main`; current audited Addon baseline is merge commit `833d541063f207354c4131cf6a614c7df176348d`. +- The current `jellypowered-chatless-integration-v2` branch was created from that `main` baseline on 2026-08-20 and initially matched `main`/`origin/main`/its remote tracking branch at 0/0 ahead-behind. - Final static STATE/strategy audit on 2026-08-07: 57 checks, 0 failures; final manual runtime matrix remains pending. - Warlock selector batch is migrated to bridge strategy mutations; final project-level real TEMP_ENCHANT revalidation and the four remaining LuaLint warnings are explicitly deferred. - Group Roll runtime validation on 2026-08-14: normal roll, item roll, eligibility, no chat spam, duplicate protection, invalid/empty item rejection and pending cleanup all validated. - Enchanting Trade Service runtime validation on 2026-08-14: enchanter-only button, list/search/tooltips, localized 440 px frame, normal WoW Trade flow and real item enchant application all validated with no automatic chat executor. + +## Talent migration progress — 2026-08-21 + +- `TALENT_APPLY_V1` is implemented, compiled and runtime validated for the editable **Custom Talents** flow. The Bridge validates the complete build, reuses the audited Playerbots parse/apply path, verifies the resulting three talent-tree totals with `BuildTalentTabPoints`, resets strategies only after successful verification, and the Addon displays localized success only after the structured server `OK`. +- `TALENT_SPEC_APPLY_V1` is implemented, compiled and runtime validated for the EveryBar premade specialization selector. `TALENT_SPEC_CURRENT` replaces the normal current-spec whisper, left-click applies slot 1, right-click applies slot 2 including dual-spec creation/validation where eligible, the server reproduces cast interruption, revalidates the premade spec index, applies talents, resets `custom_glyphs`, runs `InitGlyphs(false)`, verifies final tree totals and returns one structured result. +- Normal Bridge operation no longer needs `talents`, `talents spec list`, `stopcasting`, `talents switch ` or `talents spec ` for these selector flows. The historical chat path remains available only when `MultiBot.allowLegacyChatFallback == true`. +- Both talent success/failure messages are localized in the 8 currently loaded locales; successful operations are shown in white only after authoritative server confirmation. +- `mod-playerbots` remains strictly read-only and no generic Playerbots command executor was introduced. + +`CRAFT_RECIPE_TARGET_V1` is now implemented, compiled and runtime validated. + +`SELL_VENDOR` safety hardening was completed and runtime revalidated on 2026-08-23. The Addon remains bridge-first and exposes the historical `s vendor` whisper only when `MultiBot.allowLegacyChatFallback == true`; the Bridge accepts `ITEM_USAGE_VENDOR` only for this action and excludes `ITEM_USAGE_AH`. Symbol of Kings and Gold Ore were both preserved in the runtime regression test while `SELL_VENDOR` remained functional. Validated commits: Addon `fe2c807785219b82ca885f1a95d7c1dc27f0eed0`, Bridge `3ccf5047f7994218b742312fe1437f4b303f7159`. + +The bank / guild-bank comparison produced the completed `ITEM_DEPOSIT_EXACT_V1` P3A path. Exact BANK withdrawal (P3B) and exact GBANK withdrawal (P3C) remain intentionally deferred because their current read models do not expose a physical source stack end to end. `LOOT_RULE_ITEM_V1` is now also complete, runtime validated, persistent and localized. The next normal-roadmap decision is Quest/Skill versus Disenchant based on the Playerbots capabilities actually present in the audited build. +`LOOT_RULE_ITEM_V1` adds or removes one exact `itemId` from the audited Playerbots `always loot list` value. The protocol supports `ALL`, `RAID`, `GROUP`, `PARTY` and `BOT` scopes; the current Loot menu intentionally sends `ALL`. The Bridge revalidates requester/session/world state, strict scope/target syntax, visible controllable bots, Playerbots security, bot session/world/alive/context and the item template before mutation, with a maximum of 128 matched bots. Requests are bounded to 8 per requester per 2 seconds with 10-second replay protection, 32 recent tokens and 512 requester states. Only bots whose list actually changes are persisted, and a global budget of 128 bot saves per 10 seconds rejects the whole operation with `PERSISTENCE_BUSY` before mutation when insufficient capacity remains. Runtime validation covered ADD, idempotent ADD, REMOVE, idempotent REMOVE, invalid input/item rejection, item-link input, disconnect/reconnect persistence, full worldserver-restart persistence, localized results in the eight existing locale files and the corrected clickable/editable prompt, with no chat/whisper spam observed. + Known migration remaining: + - Remaining direct `SendChatMessage` occurrences outside migrated paths still need to be classified as manual command, diagnostic fallback, information message, UI mechanism to migrate, compatibility fallback, or dead code. - Item enchanting is now **implemented and runtime validated** through the closed `ENCHANT_TRADE_V1` Trade Service; it does not expose a generic cast or arbitrary Playerbots command executor. -- The next normal roadmap item is **item-specific loot-rule add/remove**, followed by the Quest/Skill versus Disenchant decision and collective `follow` / `attack` / `stay` orders. +- `ITEM_MOVE_V1` already covers whole-stack movement between allowed Backpack / Bag 1..4 / Keyring physical slots, including inter-container moves. A future `BAG_MOVE` item must therefore mean moving/re-equipping the **bag objects themselves** in equipped bag slots, not moving ordinary inventory items. +- Generic exact-item Trade is now implemented and runtime validated through `ITEM_TRADE_V1`; it remains distinct from the specialized `ENCHANT_TRADE_V1` service and does not expose a generic command executor. +- Quest abandon is now implemented through `QUEST_ABANDON_V1` and runtime validated with one bot. Quest sharing remains native through `QuestLogPushQuest()`. The mixed multi-bot Quest Abandon runtime scenario is deferred until suitable bots are available. +- `TALENT_APPLY_V1`, `TALENT_SPEC_APPLY_V1`, `CRAFT_RECIPE_TARGET_V1`, P3A `ITEM_DEPOSIT_EXACT_V1` and `LOOT_RULE_ITEM_V1` are complete and runtime validated. `ITEM_DEPOSIT_EXACT_V1` moves only the selected physical inventory stack for BANK/GBANK deposits and rejects stale `bag/slot/itemId/count` identity before mutation. `LOOT_RULE_ITEM_V1` performs exact persistent always-loot add/remove with structured results and bounded persistence. P3B/P3C exact withdrawals are deferred. The next normal-roadmap item is the Quest/Skill versus Disenchant decision, followed by collective `follow` / `attack` / `stay` orders. - The project should be described as **bridge-first / mostly chatless**, not fully chatless, until these remaining paths are classified/migrated and the final runtime matrix is closed. Kept intentionally: @@ -666,16 +712,22 @@ Kept intentionally: # Remaining Work -The current line includes bridge-first inventory refresh, exact bag-aware inventory topology, native whole-stack item drag/drop, outfits, Sell Vendor, `OPEN_ITEMS`, Group Roll and the runtime-validated Enchanting Trade Service in addition to the previously migrated UI areas. The roadmap is intentionally continuing feature-family by feature-family rather than jumping directly to final cleanup. +The current `jellypowered-chatless-integration-v2` line starts from the merged Jellypowered inventory baseline plus the separately completed SelfBot work. Its validated v2 feature batch is now complete through `LOOT_RULE_ITEM_V1`. No new functional work should be stacked onto this branch before the final documentation commit and the Addon/Bridge PRs to `main`; after merge, normal-roadmap development should restart on new branches created from the updated `main` baselines. + +Immediate roadmap work after those merges: + +1. Decide the Quest/Skill versus Disenchant path from verified Playerbots capabilities; do not infer unsupported loot modes from historical documentation. +2. Audit collective `follow`, `attack` and `stay` selectors before any structured group-order migration. -Next normal roadmap work: +Completed on the current v2 line: `TALENT_APPLY_V1`, `TALENT_SPEC_APPLY_V1`, `CRAFT_RECIPE_TARGET_V1`, the 2026-08-23 `SELL_VENDOR` safety hardening, P3A `ITEM_DEPOSIT_EXACT_V1` and `LOOT_RULE_ITEM_V1`. P3A was runtime validated for BANK and GBANK with two identical physical stacks: only the selected stack moved, while a deliberately stale `count` returned `SOURCE_STALE` and left the real stack unchanged. `LOOT_RULE_ITEM_V1` was validated for exact ADD/REMOVE, idempotence, persistence across reconnect and worldserver restart, localized UI/results and no chat/whisper spam. -1. Audit and implement item-specific loot-rule add/remove using verified Playerbots interfaces only. -2. Decide the Quest/Skill versus Disenchant path from verified Playerbots capabilities. -3. Audit collective `follow`, `attack` and `stay` selectors before any structured group-order migration. +Low-priority residual: moving/re-equipping the **equipped bag objects themselves** (`BAG_MOVE`) only if the need remains; ordinary item moves between Backpack / Bag 1..4 / Keyring are already covered by `ITEM_MOVE_V1`. Explicitly deferred until the normal roadmap is complete: +- P3B — exact BANK withdrawal; current bank withdrawal snapshot is aggregated and needs a wider protocol/UI design. +- P3C — exact GBANK withdrawal; physical source selection is not exposed end to end to the addon. +- Dedicated localized UI text for `SOURCE_STALE`; the current generic error text is non-blocking. - SELL_GREY / sell-grey core API / bridge-first follow-up. - Final real Firestone/Spellstone `TEMP_ENCHANTMENT_SLOT` revalidation. - Four remaining LuaLint warnings in `Strategies/MultiBotWarlock.lua`. diff --git a/UI/MultiBotBankFrame.lua b/UI/MultiBotBankFrame.lua index 74724e8..7c74c09 100644 --- a/UI/MultiBotBankFrame.lua +++ b/UI/MultiBotBankFrame.lua @@ -451,7 +451,10 @@ function MultiBot.RefreshBotBank(botName, delay) end local function refresh() - if frame and frame.IsShown and frame:IsShown() and MultiBot.Comm and MultiBot.Comm.RequestBank then + if frame and frame.IsShown and frame:IsShown() + and frame.mode == "bank" + and string.lower(tostring(frame.botName or "")) == string.lower(botName) + and MultiBot.Comm and MultiBot.Comm.RequestBank then MultiBot.Comm.RequestBank(botName) end end @@ -473,7 +476,10 @@ function MultiBot.RefreshBotGuildBank(botName, delay) end local function refresh() - if frame and frame.IsShown and frame:IsShown() and MultiBot.Comm and MultiBot.Comm.RequestGuildBank then + if frame and frame.IsShown and frame:IsShown() + and frame.mode == "gbank" + and string.lower(tostring(frame.botName or "")) == string.lower(botName) + and MultiBot.Comm and MultiBot.Comm.RequestGuildBank then MultiBot.Comm.RequestGuildBank(botName) end end diff --git a/UI/MultiBotCharacterInfoFrame.lua b/UI/MultiBotCharacterInfoFrame.lua index e37189d..78c0081 100644 --- a/UI/MultiBotCharacterInfoFrame.lua +++ b/UI/MultiBotCharacterInfoFrame.lua @@ -487,6 +487,215 @@ local function getCraftReasonText(reason, skillId) return L("profession.recipes.craft.reason." .. reason, L("profession.recipes.craft.reason.UNKNOWN", "The server returned an unknown crafting error.")) end + +-- MB_CRAFT_RECIPE_TARGET_V1_UI_BEGIN +local function getRecipeTargetReasonText(reason, skillId) + reason = tostring(reason or "") + local specific = { + BAD_TARGET_POSITION = { "profession.recipes.target.reason.BAD_TARGET_POSITION", "That inventory or equipment position is not allowed." }, + MISSING_TARGET_ITEM = { "profession.recipes.target.reason.MISSING_TARGET_ITEM", "The selected item is no longer present." }, + TARGET_STALE = { "profession.recipes.target.reason.TARGET_STALE", "The selected slot now contains a different item." }, + NOT_ITEM_TARGET_RECIPE = { "profession.recipes.target.reason.NOT_ITEM_TARGET_RECIPE", "This recipe does not accept an item target." }, + INVALID_TARGET_ITEM = { "profession.recipes.target.reason.INVALID_TARGET_ITEM", "The selected item is not a valid target for this recipe." }, + REPLAY = { "profession.recipes.target.reason.REPLAY", "This targeted request was already processed." }, + RATE_LIMIT = { "profession.recipes.target.reason.RATE_LIMIT", "Too many targeted crafting requests. Try again shortly." }, + BOT_UNAVAILABLE = { "profession.recipes.target.reason.BOT_UNAVAILABLE", "The bot is not available right now." }, + BOT_DEAD = { "profession.recipes.target.reason.BOT_DEAD", "The bot must be alive to use this recipe." }, + FORBIDDEN = { "profession.recipes.target.reason.FORBIDDEN", "You are not allowed to control this bot." }, + TIMEOUT = { "profession.recipes.target.reason.TIMEOUT", "The targeted crafting request timed out." }, + DISCONNECTED = { "profession.recipes.target.reason.DISCONNECTED", "The bridge disconnected before the targeted craft completed." }, + BAD_RESPONSE = { "profession.recipes.target.reason.BAD_RESPONSE", "The bridge returned an invalid targeted crafting response." }, + } + + local entry = specific[reason] + if entry then + return L(entry[1], entry[2]) + end + return getCraftReasonText(reason, skillId) +end + +local function isAllowedRecipeTargetPosition(bag, slot) + bag = tonumber(bag) + slot = tonumber(slot) + if not bag or not slot then + return false + end + + if bag == 255 then + return (slot >= 0 and slot <= 18) or (slot >= 23 and slot <= 38) + end + + return bag >= 19 and bag <= 22 and slot >= 0 and slot <= 255 +end + +local function getRecipeTargetSelection() + local selection = MultiBot.professionRecipeTargetSelection + if type(selection) ~= "table" then + return nil + end + return selection +end + +local function setRecipeTargetStatus(selection, text) + local frame = selection and selection.frame or MultiBot.professionRecipeFrame + if frame and frame.status and frame:IsShown() then + frame.status:SetText(text or "") + end +end + +local function clearRecipeTargetSelection(frame) + local selection = getRecipeTargetSelection() + if selection and (not frame or selection.frame == frame) then + MultiBot.professionRecipeTargetSelection = nil + end +end + +local function beginRecipeTargetSelection(frame, botName, skillId, spellId) + if not frame + or not MultiBot.Comm + or not MultiBot.Comm.IsProfessionRecipeTargetCapable + or not MultiBot.Comm.IsProfessionRecipeTargetCapable() then + if frame and frame.status then + frame.status:SetText(L( + "profession.recipes.target.unavailable", + "Exact item targeting via the bridge is unavailable." + )) + end + return false + end + + local key = getRecipePendingKey(botName, skillId, spellId) + frame.targetRequiredRecipes[key] = true + MultiBot.professionRecipeTargetSelection = { + frame = frame, + botName = botName, + botNameKey = string.lower(tostring(botName or "")), + skillId = tonumber(skillId or 0) or 0, + spellId = tonumber(spellId or 0) or 0, + } + + frame.status:SetText(L( + "profession.recipes.target.select", + "Select the exact item to modify in the bot's inventory or equipment." + )) + frame:render() + + if MultiBot.RequestBotInventory then + MultiBot.RequestBotInventory(botName) + end + return true +end + +local function runRecipeTargetSelection(selection, targetBag, targetSlot, targetItemId) + if not selection or not selection.frame then + return false + end + + local frame = selection.frame + if not MultiBot.Comm + or not MultiBot.Comm.RunProfessionRecipeTarget + or not MultiBot.Comm.IsProfessionRecipeTargetCapable + or not MultiBot.Comm.IsProfessionRecipeTargetCapable() then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.unavailable", + "Exact item targeting via the bridge is unavailable." + )) + return false + end + + local token = MultiBot.Comm.RunProfessionRecipeTarget( + selection.botName, + selection.skillId, + selection.spellId, + targetBag, + targetSlot, + targetItemId + ) + + if not token then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.send_failed", + "The targeted crafting request could not be sent." + )) + return false + end + + frame.pendingCrafts[getRecipePendingKey(selection.botName, selection.skillId, selection.spellId)] = true + frame.status:SetText(L( + "profession.recipes.target.pending", + "Applying the recipe to the selected item..." + )) + clearRecipeTargetSelection(frame) + frame:render() + return true +end + +function MultiBot.TryProfessionRecipeTargetInventoryItem(botName, item) + local selection = getRecipeTargetSelection() + if not selection then + return false + end + + if string.lower(tostring(botName or "")) ~= selection.botNameKey then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.wrong_bot", + "Select an item belonging to the bot whose recipe is open." + )) + return true + end + + if type(item) ~= "table" or item.exactLocation ~= true then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.exact_required", + "This target must come from the exact inventory view." + )) + return true + end + + local bag = tonumber(item.bag) + local slot = tonumber(item.slot) + local itemId = tonumber(item.id or 0) or 0 + if itemId <= 0 or not isAllowedRecipeTargetPosition(bag, slot) then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.invalid_scope", + "Choose an equipped item or an item from the backpack or equipped bags." + )) + return true + end + + runRecipeTargetSelection(selection, bag, slot, itemId) + return true +end + +function MultiBot.TryProfessionRecipeTargetEquipmentItem(botName, serverSlot, itemId) + local selection = getRecipeTargetSelection() + if not selection then + return false + end + + if string.lower(tostring(botName or "")) ~= selection.botNameKey then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.wrong_bot", + "Select an item belonging to the bot whose recipe is open." + )) + return true + end + + serverSlot = tonumber(serverSlot) + itemId = tonumber(itemId or 0) or 0 + if itemId <= 0 or not isAllowedRecipeTargetPosition(255, serverSlot) or serverSlot > 18 then + setRecipeTargetStatus(selection, L( + "profession.recipes.target.invalid_scope", + "Choose an equipped item or an item from the backpack or equipped bags." + )) + return true + end + + runRecipeTargetSelection(selection, 255, serverSlot, itemId) + return true +end +-- MB_CRAFT_RECIPE_TARGET_V1_UI_END + local function scheduleRecipeRefresh(botName, skillId) if not MultiBot.Comm or not MultiBot.Comm.RequestProfessionRecipes then return @@ -533,6 +742,7 @@ local function ensureRecipeFrame() frame.pageSize = 14 frame.recipes = {} frame.pendingCrafts = {} + frame.targetRequiredRecipes = {} for i = 1, frame.pageSize do local row = CreateFrame("Button", nil, content) @@ -582,10 +792,16 @@ local function ensureRecipeFrame() return end + local pendingKey = getRecipePendingKey(frame.botName, skillId, spellId) + if frame.targetRequiredRecipes[pendingKey] then + beginRecipeTargetSelection(frame, frame.botName, skillId, spellId) + return + end + if MultiBot.Comm and MultiBot.Comm.RunProfessionRecipeCraft then local token = MultiBot.Comm.RunProfessionRecipeCraft(frame.botName, skillId, spellId, itemId) if token then - frame.pendingCrafts[getRecipePendingKey(frame.botName, skillId, spellId)] = true + frame.pendingCrafts[pendingKey] = true frame.status:SetText(L("profession.recipes.craft.pending", "Craft requested...")) frame:render() else @@ -659,12 +875,18 @@ local function ensureRecipeFrame() local name, icon = getSpellDisplay(recipe.spellId) local color = DIFFICULTY_COLORS[recipe.difficulty or ""] or "|cffffffff" local craftable = tonumber(recipe.craftable or 0) or 0 + if craftable > 0 then color = "|cff00ff00" end local pending = self.pendingCrafts[getRecipePendingKey(self.botName, recipe.skillId, recipe.spellId)] local missing = getFirstMissingMaterial(recipe) row.icon:SetTexture(MultiBot.SafeTexturePath(icon)) row.text:SetText(color .. name .. "|r |cff999999x" .. craftable .. "|r") if craftable > 0 then - row.craftButton:SetText(pending and "..." or L("profession.recipes.craft", "Craft")) + local targetRequired = self.targetRequiredRecipes[getRecipePendingKey(self.botName, recipe.skillId, recipe.spellId)] == true + row.craftButton:SetText( + pending and "..." + or (targetRequired and L("profession.recipes.target.apply", "Apply") + or L("profession.recipes.craft", "Craft")) + ) setButtonEnabled(row.craftButton, tonumber(recipe.spellId or 0) > 0 and not pending) elseif missing then row.craftButton:SetText(L("profession.recipes.buy_missing", "Buy")) @@ -693,6 +915,15 @@ local function ensureRecipeFrame() end) frame.setRecipes = function(self, botName, skill, recipes) + local previousBot = string.lower(tostring(self.botName or "")) + local previousSkillId = self.skill and tonumber(self.skill.skillId or 0) or 0 + local nextBot = string.lower(tostring(botName or "")) + local nextSkillId = skill and tonumber(skill.skillId or 0) or 0 + if previousBot ~= nextBot or previousSkillId ~= nextSkillId then + self.targetRequiredRecipes = {} + clearRecipeTargetSelection(self) + end + self.botName = botName self.skill = skill self.recipes = recipes or {} @@ -703,6 +934,12 @@ local function ensureRecipeFrame() self:Show() end + if frame.HookScript then + frame:HookScript("OnHide", function() + clearRecipeTargetSelection(frame) + end) + end + frame:Hide() MultiBot.professionRecipeFrame = frame return frame @@ -1318,8 +1555,9 @@ function MultiBot.OnBridgeProfessionRecipeCraftResult(botName, skillId, spellId, local frame = ensureRecipeFrame() local sameBot = string.lower(tostring(frame.botName or "")) == string.lower(tostring(botName or "")) local sameSkill = frame.skill and tonumber(frame.skill.skillId or 0) == tonumber(skillId or 0) + local pendingKey = getRecipePendingKey(botName, skillId, spellId) - frame.pendingCrafts[getRecipePendingKey(botName, skillId, spellId)] = nil + frame.pendingCrafts[pendingKey] = nil if result == "OK" then if sameBot and sameSkill then @@ -1330,8 +1568,16 @@ function MultiBot.OnBridgeProfessionRecipeCraftResult(botName, skillId, spellId, return end + if reason == "TARGET_REQUIRED" then + frame.targetRequiredRecipes[pendingKey] = true + if sameBot and sameSkill then + beginRecipeTargetSelection(frame, botName, skillId, spellId) + end + return + end + if sameBot and sameSkill then - local reasonText = getCraftReasonText(reason, skillId) + local reasonText = getRecipeTargetReasonText(reason, skillId) if reasonText ~= "" then frame.status:SetText(string.format(L("profession.recipes.craft.err", "Craft failed: %s"), reasonText)) else @@ -1341,6 +1587,44 @@ function MultiBot.OnBridgeProfessionRecipeCraftResult(botName, skillId, spellId, end end +function MultiBot.OnBridgeProfessionRecipeTargetResult(botName, result, reason, skillId, spellId, _targetBag, _targetSlot, _targetItemId) + local frame = ensureRecipeFrame() + local sameBot = string.lower(tostring(frame.botName or "")) == string.lower(tostring(botName or "")) + local sameSkill = frame.skill and tonumber(frame.skill.skillId or 0) == tonumber(skillId or 0) + local pendingKey = getRecipePendingKey(botName, skillId, spellId) + + frame.pendingCrafts[pendingKey] = nil + frame.targetRequiredRecipes[pendingKey] = true + + if result == "OK" then + if sameBot and sameSkill then + frame.status:SetText(L( + "profession.recipes.target.ok", + "The recipe was applied to the selected item." + )) + frame:render() + end + scheduleRecipeRefresh(botName, skillId) + return + end + + if sameBot and sameSkill then + local reasonText = getCraftReasonText(reason, skillId) + if reasonText ~= "" then + frame.status:SetText(string.format( + L("profession.recipes.target.err", "Targeted craft failed: %s"), + reasonText + )) + else + frame.status:SetText(L( + "profession.recipes.target.failed", + "The targeted crafting request failed." + )) + end + frame:render() + end +end + function MultiBot.InitializeCharacterInfoFrame() ensureCharacterFrame() ensureRecipeFrame() diff --git a/UI/MultiBotEnchantingUI.lua b/UI/MultiBotEnchantingUI.lua index 31068b7..d95d5ab 100644 --- a/UI/MultiBotEnchantingUI.lua +++ b/UI/MultiBotEnchantingUI.lua @@ -276,7 +276,7 @@ function EnchantUI:Render() row.selection:Hide() end if tonumber(entry.available or 0) ~= 0 then - row.name:SetTextColor(1, 1, 1) + row.name:SetTextColor(0, 1, 0) row.materials:SetTextColor(0.8, 0.8, 0.8) else row.name:SetTextColor(0.55, 0.55, 0.55) diff --git a/UI/MultiBotInspectUI.lua b/UI/MultiBotInspectUI.lua index 4d4812c..ff85928 100644 --- a/UI/MultiBotInspectUI.lua +++ b/UI/MultiBotInspectUI.lua @@ -169,11 +169,26 @@ local function showRightClickHint(self) end local function onInspectSlotClick(self, mouseButton) - if mouseButton ~= "RightButton" then + local botName = getInspectedBotName() + + -- MB_CRAFT_RECIPE_TARGET_V1_INSPECT_BEGIN + if mouseButton == "LeftButton" then + local itemLink = getSlotItemLink(self) + local inspectSlotId = getInspectSlotId(self) + local itemId = getItemIdFromLink(itemLink) + if inspectSlotId and itemId and MultiBot.TryProfessionRecipeTargetEquipmentItem then + local serverSlot = inspectSlotId - 1 + if MultiBot.TryProfessionRecipeTargetEquipmentItem(botName, serverSlot, itemId) then + return + end + end return end + -- MB_CRAFT_RECIPE_TARGET_V1_INSPECT_END - local botName = getInspectedBotName() + if mouseButton ~= "RightButton" then + return + end if not canUnequipInspectedBot(botName) then return end diff --git a/UI/MultiBotInventoryFrame.lua b/UI/MultiBotInventoryFrame.lua index f4acac0..97fbade 100644 --- a/UI/MultiBotInventoryFrame.lua +++ b/UI/MultiBotInventoryFrame.lua @@ -1615,6 +1615,10 @@ local function runInventoryInstantAction(botName, command, options) return false end + if cmd == "s vendor" and MultiBot.allowLegacyChatFallback ~= true then + return false + end + local inventory = MultiBot.inventory local itemsFrame = inventory and inventory.frames and inventory.frames.Items local itemButtons = itemsFrame and itemsFrame.buttons diff --git a/UI/MultiBotInventoryItem.lua b/UI/MultiBotInventoryItem.lua index a40826d..fe3d757 100644 --- a/UI/MultiBotInventoryItem.lua +++ b/UI/MultiBotInventoryItem.lua @@ -473,6 +473,37 @@ local function runBridgeInventoryItemAction(action, button, botName, options) return true end +local function runBridgeInventoryItemDepositExact(action, button, botName) + if not action or action == "" or not button or not button.item or not botName or botName == "" then + return false + end + + local item = button.item + if item.exactLocation ~= true then + return false + end + + local srcBag = tonumber(item.bag) + local srcSlot = tonumber(item.slot) + local srcItemId = tonumber(item.id or 0) or 0 + local srcCount = tonumber(item._serverCount or item.count or 1) or 1 + if srcBag == nil or srcSlot == nil or srcItemId <= 0 or srcCount < 1 then + return false + end + + if not MultiBot.Comm + or not MultiBot.Comm.IsInventoryItemDepositExactCapable + or not MultiBot.Comm.IsInventoryItemDepositExactCapable() + or not MultiBot.Comm.RunInventoryItemDepositExact then + return false + end + + local token = MultiBot.Comm.RunInventoryItemDepositExact( + botName, action, srcBag, srcSlot, srcItemId, srcCount + ) + return token and true or false +end + -- MB_ITEM_SELL_SINGLE_V1_HELPER_BEGIN local function runBridgeInventoryItemSell(button, botName) if not button or not button.item or not botName or botName == "" then @@ -503,6 +534,34 @@ local function runBridgeInventoryItemSell(button, botName) end -- MB_ITEM_SELL_SINGLE_V1_HELPER_END +local function runBridgeInventoryItemTrade(button, botName) + if not button or not button.item or not botName or botName == "" then + return false + end + + local item = button.item + if item.exactLocation ~= true then + return false + end + + local srcBag = tonumber(item.bag) + local srcSlot = tonumber(item.slot) + local itemId = tonumber(item.id or 0) or 0 + local count = tonumber(item._serverCount or item.count or 1) or 1 + if srcBag == nil or srcSlot == nil or itemId <= 0 or count < 1 then + return false + end + + if not MultiBot.Comm or not MultiBot.Comm.RunInventoryItemTrade then + return false + end + + local token = MultiBot.Comm.RunInventoryItemTrade( + botName, srcBag, srcSlot, itemId, count + ) + return token and true or false +end + local function runBridgeInventoryItemUse(button, botName) if not button or not button.item or not botName or botName == "" then return false @@ -587,10 +646,29 @@ MultiBot.OnBridgeInventoryItemDestroyResult = function(botName, _, reason) requestInventoryRefresh(0.15, botName) end +MultiBot.OnBridgeInventoryItemTradeResult = function(_, status, reason) + if reason == "DISCONNECTED" then + return + end + + if status ~= "OK" then + addInventorySystemMessage( + inventoryItemL("inventory.item_trade.failed", "The item could not be added to the trade.") + .. " [" .. tostring(reason or "FAILED") .. "]" + ) + end +end + local function handleInventoryItemClick(button) - local action, botName = getInventoryItemActionState() local item = button and button.item or nil + local inventoryBotName = MultiBot.inventory and MultiBot.inventory.name or nil + if item + and MultiBot.TryProfessionRecipeTargetInventoryItem + and MultiBot.TryProfessionRecipeTargetInventoryItem(inventoryBotName, item) then + return + end + local action, botName = getInventoryItemActionState() if action == "" then sendInventoryFeedback("action", "Choose an action first") return @@ -661,33 +739,80 @@ local function handleInventoryItemClick(button) end if action == "give" then - sendInventoryItemCommand(action, button, botName) + local bridgeCapable = MultiBot.Comm + and MultiBot.Comm.IsInventoryItemTradeCapable + and MultiBot.Comm.IsInventoryItemTradeCapable() + + if bridgeCapable then + if runBridgeInventoryItemTrade(button, botName) then + return + end + + addInventorySystemMessage(inventoryItemL( + "inventory.item_trade.send_failed", + "The item-trade request could not be sent." + )) + return + end + + if MultiBot.allowLegacyChatFallback == true then + sendInventoryItemCommand(action, button, botName) + else + addInventorySystemMessage(inventoryItemL( + "inventory.item_trade.unavailable", + "Item trading via the bridge is unavailable." + )) + end return end if action == "bank" then + local exactDepositCapable = item.exactLocation == true + and MultiBot.Comm + and MultiBot.Comm.IsInventoryItemDepositExactCapable + and MultiBot.Comm.IsInventoryItemDepositExactCapable() + + if exactDepositCapable then + runBridgeInventoryItemDepositExact("BANK_DEPOSIT", button, botName) + return + end + if runBridgeInventoryItemAction("BANK_DEPOSIT", button, botName) then return end - sendInventoryItemCommand("bank", button, botName, { - postActionRefresh = true, - refreshDelay = 0.45, - followupRefreshDelay = 1.20, - }) + if MultiBot.allowLegacyChatFallback == true then + sendInventoryItemCommand("bank", button, botName, { + postActionRefresh = true, + refreshDelay = 0.45, + followupRefreshDelay = 1.20, + }) + end return end if action == "gb" then + local exactDepositCapable = item.exactLocation == true + and MultiBot.Comm + and MultiBot.Comm.IsInventoryItemDepositExactCapable + and MultiBot.Comm.IsInventoryItemDepositExactCapable() + + if exactDepositCapable then + runBridgeInventoryItemDepositExact("GBANK_DEPOSIT", button, botName) + return + end + if runBridgeInventoryItemAction("GBANK_DEPOSIT", button, botName) then return end - sendInventoryItemCommand("gb", button, botName, { - postActionRefresh = true, - refreshDelay = 0.45, - followupRefreshDelay = 1.20, - }) + if MultiBot.allowLegacyChatFallback == true then + sendInventoryItemCommand("gb", button, botName, { + postActionRefresh = true, + refreshDelay = 0.45, + followupRefreshDelay = 1.20, + }) + end return end @@ -696,11 +821,13 @@ local function handleInventoryItemClick(button) return end - sendInventoryItemCommand("b", button, botName, { - postActionRefresh = true, - refreshDelay = 0.45, - followupRefreshDelay = 1.20, - }) + if MultiBot.allowLegacyChatFallback == true then + sendInventoryItemCommand("b", button, botName, { + postActionRefresh = true, + refreshDelay = 0.45, + followupRefreshDelay = 1.20, + }) + end return end diff --git a/UI/MultiBotLootUI.lua b/UI/MultiBotLootUI.lua index ccacbfb..d1c107a 100644 --- a/UI/MultiBotLootUI.lua +++ b/UI/MultiBotLootUI.lua @@ -14,6 +14,8 @@ local LOOT_COMMANDS = { { key = "gray", command = "ll gray", icon = "inv_misc_coin_01", tip = "tips.loot.gray", fallback = "Loot profile: Gray" }, { key = "quest", command = "ll quest", icon = "inv_misc_note_05", tip = "tips.loot.quest", fallback = "Loot profile: Quest" }, { key = "skill", command = "ll skill", icon = "inv_misc_book_07", tip = "tips.loot.skill", fallback = "Loot profile: Skill" }, + { key = "additem", action = "ADD", icon = "inv_misc_bag_10", tip = "loot.item.add", fallback = "Always loot: add item" }, + { key = "removeitem", action = "REMOVE", icon = "inv_misc_bag_07", tip = "loot.item.remove", fallback = "Always loot: remove item" }, } local LOOT_PROFILE_KEYS = { @@ -47,19 +49,130 @@ local function RunLootCommand(command) return ok end +-- MB_LOOT_RULE_ITEM_V1_UI_BEGIN +local function ShowLootItemMessage(message, ok) + message = tostring(message or "") + if message == "" then + return + end + + if UIErrorsFrame and UIErrorsFrame.AddMessage then + if ok then + UIErrorsFrame:AddMessage(message, 0.2, 1.0, 0.2, 1.0) + else + UIErrorsFrame:AddMessage(message, 1.0, 0.2, 0.2, 1.0) + end + elseif DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + DEFAULT_CHAT_FRAME:AddMessage(message) + end +end + +local function ParseLootRuleItemId(value) + value = tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "") + local itemId = tonumber(value) + if not itemId then + itemId = tonumber(value:match("|Hitem:(%d+):") or value:match("Hitem:(%d+):")) + end + + if not itemId or itemId < 1 or itemId > 4294967295 or math.floor(itemId) ~= itemId then + return nil + end + + return itemId +end + +local function LootRuleItemReasonText(reason) + local normalized = tostring(reason or "FAILED") + return L("loot.item.reason." .. normalized, normalized) +end + +local function FormatLootRuleItemResult(itemText, status, reason, matched, changed) + local ok = status == "OK" + local matchedCount = tonumber(matched) or 0 + local changedCount = tonumber(changed) or 0 + + if ok then + if reason == "ADDED" then + return string.format(L("loot.item.result.added", "%s - added to %d bot(s)."), itemText, changedCount), true + elseif reason == "REMOVED" then + return string.format(L("loot.item.result.removed", "%s - removed from %d bot(s)."), itemText, changedCount), true + elseif reason == "ALREADY_PRESENT" then + return string.format(L("loot.item.result.already_present", "%s - already present on %d bot(s)."), itemText, matchedCount), true + elseif reason == "ALREADY_ABSENT" then + return string.format(L("loot.item.result.already_absent", "%s - already absent on %d bot(s)."), itemText, matchedCount), true + elseif reason == "PARTIAL" then + return string.format(L("loot.item.result.partial", "%s - partially updated (%d/%d bot(s))."), itemText, changedCount, matchedCount), true + end + + return string.format( + L("loot.item.result.ok", "%s - %s (%d/%d bot(s))."), + itemText, + LootRuleItemReasonText(reason), + changedCount, + matchedCount + ), true + end + + return string.format( + L("loot.item.result.failed", "%s - loot rule failed: %s."), + itemText, + LootRuleItemReasonText(reason) + ), false +end + +local function ShowLootRuleItemPrompt(action) + if not MultiBot or not MultiBot.Comm + or not MultiBot.Comm.IsLootRuleItemCapable + or not MultiBot.Comm.RunLootRuleItem + or not MultiBot.Comm.IsLootRuleItemCapable() then + ShowLootItemMessage(L("loot.item.bridge.required", "Exact loot-item rules require LOOT_RULE_ITEM_V1."), false) + return + end + + if type(MultiBot.ShowPrompt) ~= "function" then + ShowLootItemMessage(L("loot.item.prompt.required", "Item prompt is unavailable."), false) + return + end + + local title = action == "REMOVE" + and L("loot.item.remove.prompt", "Remove always-loot item ID or link") + or L("loot.item.add.prompt", "Add always-loot item ID or link") + + MultiBot.ShowPrompt(title, function(value) + local itemId = ParseLootRuleItemId(value) + if not itemId then + ShowLootItemMessage(L("loot.item.invalid", "Invalid item ID or item link."), false) + return + end + + local token = MultiBot.Comm.RunLootRuleItem("ALL", "", action, itemId) + if not token then + ShowLootItemMessage(L("loot.item.send.failed", "Loot item rule request was not sent."), false) + end + end, "") +end + +MultiBot.OnLootRuleItemResult = function(scope, target, action, itemId, status, reason, matched, changed) + local itemName, itemLink = GetItemInfo(itemId) + local itemText = itemLink or itemName or ("item:" .. tostring(itemId or 0)) + local message, ok = FormatLootRuleItemResult(itemText, status, reason, matched, changed) + ShowLootItemMessage(message, ok) +end +-- MB_LOOT_RULE_ITEM_V1_UI_END + function MultiBot.BuildLootUI(tLeft) if not tLeft or MultiBot.frames.loot then return MultiBot.frames.loot end local button - local menu = tLeft.addFrame("LootMenu", -73, 34, 24, 24, 170).doHide() + local menu = tLeft.addFrame("LootMenu", -73, 34, 24, 24, 218).doHide() local menuOpen = false local menuButtons = {} local menuButtonsByKey = {} menu._mbDropdownManaged = true menu:SetWidth(24) - menu:SetHeight(170) + menu:SetHeight(218) local function updateClickBlocker() if MultiBot.RequestClickBlockerUpdate then @@ -148,7 +261,11 @@ function MultiBot.BuildLootUI(tLeft) for index, entry in ipairs(LOOT_COMMANDS) do local menuButton = menu.addButton("Loot" .. entry.key, 0, (index - 1) * 24, entry.icon, L(entry.tip, entry.fallback)) menuButton.doLeft = function() - RunLootCommand(entry.command) + if entry.command then + RunLootCommand(entry.command) + elseif entry.action then + ShowLootRuleItemPrompt(entry.action) + end end menuButtons[index] = menuButton diff --git a/UI/MultiBotPromptDialog.lua b/UI/MultiBotPromptDialog.lua index 498fd0a..30ec349 100644 --- a/UI/MultiBotPromptDialog.lua +++ b/UI/MultiBotPromptDialog.lua @@ -72,6 +72,18 @@ function ShowPrompt(title, onOk, defaultText, anchorFrame) end window:AddChild(edit) + -- The bundled AceGUI EditBox binds OnMouseDown to drag handling, which + -- clears focus even when no drag payload exists. Keep this override + -- local to the universal prompt so the field remains clickable/editable. + local nativeEditBox = edit.editbox + if nativeEditBox and nativeEditBox.SetScript then + nativeEditBox:SetScript("OnMouseDown", function(frame) + if frame and frame.SetFocus then + frame:SetFocus() + end + end) + end + local okButton = aceGUI:Create("Button") okButton:SetText(OKAY) okButton:SetWidth(PROMPT_OK_BUTTON_WIDTH) @@ -95,6 +107,9 @@ function ShowPrompt(title, onOk, defaultText, anchorFrame) local editBox = PROMPT.edit and PROMPT.edit.editbox if editBox and editBox.SetFocus then editBox:SetFocus() + if defaultText and defaultText ~= "" and editBox.HighlightText then + editBox:HighlightText() + end end PROMPT.okButton:SetCallback("OnClick", function() diff --git a/UI/MultiBotQuestLogFrame.lua b/UI/MultiBotQuestLogFrame.lua index ce06212..89d9afc 100644 --- a/UI/MultiBotQuestLogFrame.lua +++ b/UI/MultiBotQuestLogFrame.lua @@ -75,11 +75,22 @@ local function handleQuestClick(questID, button) SelectQuestLogEntry(questIndex) if button == "RightButton" then - if GetNumRaidMembers() > 0 then - SendChatMessage("drop " .. questLink, "RAID") - elseif GetNumPartyMembers() > 0 then - SendChatMessage("drop " .. questLink, "PARTY") + local bridgeHandled = false + if MultiBot.Comm + and MultiBot.Comm.IsQuestAbandonCapable + and MultiBot.Comm.IsQuestAbandonCapable() + and MultiBot.Comm.RunQuestAbandon then + bridgeHandled = MultiBot.Comm.RunQuestAbandon(questID) and true or false end + + if not bridgeHandled and MultiBot.allowLegacyChatFallback == true then + if GetNumRaidMembers() > 0 then + SendChatMessage("drop " .. questLink, "RAID") + elseif GetNumPartyMembers() > 0 then + SendChatMessage("drop " .. questLink, "PARTY") + end + end + SetAbandonQuest() AbandonQuest() else diff --git a/UI/MultiBotSpecUI.lua b/UI/MultiBotSpecUI.lua index f6ec4e6..228bbfc 100644 --- a/UI/MultiBotSpecUI.lua +++ b/UI/MultiBotSpecUI.lua @@ -438,29 +438,30 @@ function Spec:RequestList(bot, wrapper) wrapper = wrapper, specs = {}, builds = {}, + indices = {}, } self.activeWrapper = wrapper - -- On garde volontairement la réponse legacy de la spé courante : - -- "My current talent spec is: ...". - -- Les lignes d'aide inutiles renvoyées par cette commande sont masquées côté chat. - suppressNextTalentUsageLines(bot) - SendChatMessage("talents", "WHISPER", nil, bot) + local comm = MultiBot.Comm or nil + local bridgeApplyCapable = comm + and comm.IsTalentSpecApplyCapable + and comm.IsTalentSpecApplyCapable() - -- La liste complète des modèles disponibles est maintenant demandée en bridge-first - -- pour éviter le spam "talents spec list" dans le chat - MultiBot.TimerAfter(0.2, function() - if Spec.pending and Spec.pending.bot == bot then - local comm = MultiBot.Comm or nil - if comm and comm.RequestTalentSpecList and comm.RequestTalentSpecList(bot) then - return - end + -- The current active slot/build is returned by TALENT_SPEC_CURRENT on a + -- TALENT_SPEC_APPLY_V1 bridge. Keep the old "talents" whisper only as an + -- explicitly enabled legacy fallback. + if not bridgeApplyCapable and MultiBot.allowLegacyChatFallback == true then + suppressNextTalentUsageLines(bot) + SendChatMessage("talents", "WHISPER", nil, bot) + end - if MultiBot.allowLegacyChatFallback == true then - SendChatMessage("talents spec list", "WHISPER", nil, bot) - end - end - end) + if comm and comm.RequestTalentSpecList and comm.RequestTalentSpecList(bot) then + return + end + + if MultiBot.allowLegacyChatFallback == true then + SendChatMessage("talents spec list", "WHISPER", nil, bot) + end end @@ -690,6 +691,7 @@ function MultiBot.ApplyBridgeTalentSpecBegin(botName, token) pending.bridgeToken = token pending.specs = {} pending.builds = {} + pending.indices = {} return true end @@ -709,10 +711,35 @@ function MultiBot.ApplyBridgeTalentSpecItem(botName, token, entry) tinsert(pending.specs, strtrim(entry.name)) tinsert(pending.builds, strtrim(entry.build or "")) + tinsert(pending.indices, tonumber(entry.index) or -1) return true end +function MultiBot.ApplyBridgeTalentSpecCurrent(botName, token, slot, tree0, tree1, tree2) + local pending = Spec.pending + if not pending or short(botName) ~= short(pending.bot) then + return false + end + + if pending.bridgeToken and token and pending.bridgeToken ~= token then + return false + end + + slot = tonumber(slot) + tree0 = tonumber(tree0) + tree1 = tonumber(tree1) + tree2 = tonumber(tree2) + if (slot ~= 1 and slot ~= 2) or not tree0 or not tree1 or not tree2 then + return false + end + + Spec.currentBuild[short(botName):lower()] = + tostring(tree0) .. "-" .. tostring(tree1) .. "-" .. tostring(tree2) + pending.currentSlot = slot + return true +end function MultiBot.ApplyBridgeTalentSpecEnd(botName, token) + local pending = Spec.pending if not pending or short(botName) ~= short(pending.bot) then return false @@ -850,7 +877,52 @@ local function applyDropdownPosition(frame, anchor, isEmbedded) end end -local function bindSpecSelection(button, spec, build, tip, bot, className, currentBuild) +local function refreshSpecTalentInspection(bot, className) + local unit = MultiBot.toUnit(bot) + if not unit then + return + end + + if not MultiBot.talent:IsShown() then + MultiBot.talent.name = bot + MultiBot.talent.class = className + end + + MultiBot.TimerAfter(0.2, function() + MultiBot.auto.talent = true + InspectUnit(unit) + + if InspectFrame then + HideUIPanel(InspectFrame) + end + + MultiBot.TimerAfter(0.1, function() + if MultiBot.talent:IsShown() then + MultiBot.talent:Hide() + end + end) + end) +end + +local function runLegacySpecSelection(bot, slot, spec, className) + SendChatMessage("stopcasting", "WHISPER", nil, bot) + SendChatMessage("talents switch " .. slot, "WHISPER", nil, bot) + + MultiBot.TimerAfter(0.4, function() + SendChatMessage("talents spec " .. spec, "WHISPER", nil, bot) + end) + + Spec.pendingRefresh = bot + MultiBot.TimerAfter(1.3, function() + if Spec.pendingRefresh and Spec.pendingRefresh == bot then + refreshSpecTalentInspection(bot, className) + Spec.pendingRefresh = nil + Spec.busy = false + end + end) +end + +local function bindSpecSelection(button, spec, specIndex, build, tip, bot, className, currentBuild) if build == currentBuild then button:SetAlpha(0.4) local tex = button:GetNormalTexture() @@ -866,48 +938,66 @@ local function bindSpecSelection(button, spec, build, tip, bot, className, curre end Spec.busy = true - SendChatMessage("stopcasting", "WHISPER", nil, bot) - local slot = (btn == "RightButton") and 2 or 1 - SendChatMessage("talents switch " .. slot, "WHISPER", nil, bot) - - MultiBot.TimerAfter(0.4, function() - SendChatMessage("talents spec " .. spec, "WHISPER", nil, bot) - end) + local comm = MultiBot.Comm or nil + local request - Spec.pendingRefresh = bot - Spec:HideDropdown() + if comm and comm.RunTalentSpecApply then + request = comm.RunTalentSpecApply(bot, slot, specIndex, spec, function(result) + Spec.busy = false + if type(result) ~= "table" then + return + end - MultiBot.TimerAfter(1.3, function() - if Spec.pendingRefresh and Spec.pendingRefresh == bot then - local unit = MultiBot.toUnit(bot) + if result.status == "ok" then + local points = result.treePoints or {} + if points[1] ~= nil and points[2] ~= nil and points[3] ~= nil then + Spec.currentBuild[short(bot):lower()] = + tostring(points[1]) .. "-" .. tostring(points[2]) .. "-" .. tostring(points[3]) + end - if unit then - if not MultiBot.talent:IsShown() then - MultiBot.talent.name = bot - MultiBot.talent.class = className + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + DEFAULT_CHAT_FRAME:AddMessage( + string.format(MultiBot.L("talent.spec.apply.success"), result.specName or spec, bot), + 1, 1, 1 + ) end + refreshSpecTalentInspection(bot, className) + else + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + DEFAULT_CHAT_FRAME:AddMessage( + string.format( + MultiBot.L("talent.spec.apply.failed"), + result.specName or spec, + bot, + tostring(result.reason or "UNKNOWN") + ), + 1, 0.2, 0.2 + ) + end + end + end) + end - MultiBot.TimerAfter(0.6, function() - MultiBot.auto.talent = true - InspectUnit(unit) + Spec:HideDropdown() - if InspectFrame then - HideUIPanel(InspectFrame) - end + if request then + return + end - MultiBot.TimerAfter(0.1, function() - if MultiBot.talent:IsShown() then - MultiBot.talent:Hide() - end - end) - end) - end + if MultiBot.allowLegacyChatFallback == true then + runLegacySpecSelection(bot, slot, spec, className) + return + end - Spec.pendingRefresh = nil - Spec.busy = false - end - end) + Spec.busy = false + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + local reason = comm and MultiBot.bridge and MultiBot.bridge.lastError or "BRIDGE_UNAVAILABLE" + DEFAULT_CHAT_FRAME:AddMessage( + string.format(MultiBot.L("talent.spec.apply.failed"), spec, bot, tostring(reason or "UNKNOWN")), + 1, 0.2, 0.2 + ) + end end) end @@ -1026,6 +1116,7 @@ function Spec:BuildDropdown() for index, button in ipairs(self.buttons) do if index <= needed then local specName = pending.specs[index] + local specIndex = pending.indices[index] local build = pending.builds[index] button:ClearAllPoints() @@ -1049,9 +1140,9 @@ function Spec:BuildDropdown() if (not alreadyMarked) and build == currentBuild then alreadyMarked = true - bindSpecSelection(button, specName, build, tip, pending.bot, className, currentBuild) + bindSpecSelection(button, specName, specIndex, build, tip, pending.bot, className, currentBuild) else - bindSpecSelection(button, specName, build, tip, pending.bot, className, nil) + bindSpecSelection(button, specName, specIndex, build, tip, pending.bot, className, nil) end else button:Hide() diff --git a/UI/MultiBotTalentFrame.lua b/UI/MultiBotTalentFrame.lua index c2f2901..920d2f9 100644 --- a/UI/MultiBotTalentFrame.lua +++ b/UI/MultiBotTalentFrame.lua @@ -1039,8 +1039,50 @@ function MultiBot.InitializeTalentFrameModule() return tValues end + local function showTalentApplyResult(result) + result = type(result) == "table" and result or {} + local botName = tostring(result.botName or "") + if result.status == "ok" then + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + DEFAULT_CHAT_FRAME:AddMessage(string.format(MultiBot.L("talent.apply.success"), botName), 1, 1, 1) + end + return + end + + if DEFAULT_CHAT_FRAME and DEFAULT_CHAT_FRAME.AddMessage then + DEFAULT_CHAT_FRAME:AddMessage( + string.format(MultiBot.L("talent.apply.failed"), botName, tostring(result.reason or "UNKNOWN")), + 1, 0.2, 0.2 + ) + end + end + + function MultiBot.talent.requestTalentApply(botName) + botName = type(botName) == "string" and botName or "" + local build = MultiBot.talent.buildTalentApplyValues() + + if MultiBot.Comm and MultiBot.Comm.RunTalentApply then + local token = MultiBot.Comm.RunTalentApply(botName, build, showTalentApplyResult) + if token then + return true + end + end + + if MultiBot.allowLegacyChatFallback == true then + SendChatMessage("talents apply " .. build, "WHISPER", nil, botName) + return true + end + + showTalentApplyResult({ + status = "error", + botName = botName, + reason = "BRIDGE_UNAVAILABLE", + }) + return false + end + function MultiBot.talent.applyCustomTalents() - SendChatMessage("talents apply " .. MultiBot.talent.buildTalentApplyValues(), "WHISPER", nil, MultiBot.talent.name) + return MultiBot.talent.requestTalentApply(MultiBot.talent.name) end function MultiBot.talent.copyCustomTalentsToTarget() @@ -1053,7 +1095,7 @@ function MultiBot.InitializeTalentFrameModule() local tUnit = MultiBot.toUnit(MultiBot.talent.name) if(UnitLevel(tUnit) ~= UnitLevel("target")) then return SendChatMessage("The Levels do not match.", "SAY") end - SendChatMessage("talents apply " .. MultiBot.talent.buildTalentApplyValues(), "WHISPER", nil, tName) + return MultiBot.talent.requestTalentApply(tName) end -- Talent trees frame initialization in scoped blocks diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3c131f1..d8a5cd2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,27 +1,32 @@ # Multibot Chatless + Bridge — Roadmap de reprise -Statut : roadmap active issue de l'audit initial v1c du 1er août 2026, resynchronisée avec l'état de stabilisation pré-merge du 17 août 2026. -Dernière mise à jour : 17/08/2026 — le support multi-préfixes configurable, `INVENTORY_EXACT_V1`, l'UI inventaire bag-aware, `ITEM_MOVE_V1`, `ITEM_EQUIP_V1`, `ITEM_UNEQUIP_V1`, `ITEM_USE_V1`, `ITEM_DESTROY`, `ITEM_SELL_SINGLE_V1` et `VENDOR_BUYBACK_V1` sont validés sur la branche Jellypowered. La stabilisation pré-merge a clos les correctifs CAPS, postconditions ITEM_MOVE/ITEM_USE, autorisation INVENTORY_EXACT, fallback Equip, recyclage de la frame inventaire, sécurité cold-cache, localisation ITEM_USE/Inspect, garde nil Buyback et warning LuaLint `BUYBACK_ROWS`. Les lectures bulk Jellypowered restent différées et les vérifications globales LuaLint/CI restent à exécuter avant merge. Le prochain chantier de la roadmap normale reste l'ajout/retrait d'items précis dans les règles de loot. -Cette roadmap est la source de vérité active du projet. Les anciens trackers et le fichier `TODO.md` ont été consolidés ici. +Statut : roadmap active issue de l'audit initial v1c du 1er août 2026, resynchronisée le 25 août 2026 après clôture de P3A `ITEM_DEPOSIT_EXACT_V1` puis de `LOOT_RULE_ITEM_V1`. Le lot fonctionnel courant des branches `jellypowered-chatless-integration-v2` est terminé et prêt pour la synchronisation documentaire finale puis les PR Addon/Bridge vers `main`; les prochains travaux de roadmap repartiront sur de nouvelles branches créées depuis les `main` mis à jour. +Dernière mise à jour : 25/08/2026 — `LOOT_RULE_ITEM_V1` est terminé, validé en jeu, audité, archivé, commité et poussé après P3A `ITEM_DEPOSIT_EXACT_V1`. ADD/REMOVE modifie l'`always loot list` vérifiée dans Playerbots pour un `itemId` exact, avec résultats structurés, idempotence, persistance des seuls bots modifiés, budget global de 128 sauvegardes/10 s et rejet pré-mutation `PERSISTENCE_BUSY` lorsque le budget est insuffisant. La persistance a été validée après reconnexion et restart worldserver, le prompt et les résultats sont localisés dans les huit locales présentes, et aucun spam chat/whisper n'a été observé. P3B/P3C, `SOURCE_STALE` UI, SELL_GREY, Firestone/Spellstone et LuaLint restent différés. `TODO.md` reste séparé et inchangé. Le prochain chantier normal est la décision Quest/Skill versus Disenchant à partir des capacités Playerbots réellement présentes. +Cette roadmap est la source de vérité active du projet. `TODO.md` reste un fichier de notes local séparé et est volontairement exclu de cette synchronisation documentaire. ## Baseline auditée -Audit de synchronisation : `audit-multibot-trade-inventory-whisper-spam-v1b-2026-08-15-004941`, complété par les patches runtime validés de suppression du dump Trade. +Synchronisation documentaire fondée sur : + +- `audit-multibot-jellypowered-docs-baseline-v2-2026-08-20-172627.zip` ; +- `audit-multibot-jellypowered-git-state-supplement-v1-2026-08-20-173756.zip` ; +- `audit-multibot-jellypowered-bag-move-item-trade-v1-2026-08-20-174218.zip`. - Addon : `L:\ChromieCraft_3.3.5a\Interface\AddOns\MultiBot` - - branche `main` ; - - HEAD et `origin/main` avant le correctif Trade local : `2c827f0acf305030d9d97ed797f9c798a25daab3` ; - - merge PR #63 : **Add chatless Enchanting Trade Service UI** ; - - correctif local validé en jeu : suppression du dump inventaire automatique lors des ouvertures Trade Inventory, Enchanting et client WoW natif. + - branche `jellypowered-chatless-integration-v2`, créée depuis `main` le 20/08/2026 ; + - HEAD = `main` = `origin/main` = `origin/jellypowered-chatless-integration-v2` au baseline audité : `833d541063f207354c4131cf6a614c7df176348d` ; + - ahead/behind versus `main` : `0/0` avant cette synchronisation documentaire ; + - PR Jellypowered #67 déjà mergée, merge commit `70e72ba6cb9a7170497b201e0dbe469bb29e6be9` ; + - PR SelfBot #72 **Complete SelfBot chatless integration** déjà mergée ; le chantier SelfBot reste séparé du développement Jellypowered ; + - seul `TODO.md` est modifié localement et non stagé ; il est explicitement hors scope de cette synchronisation. - Bridge : `L:\AC_PB\azerothcore-wotlk\modules\mod-multibot-bridge` - - branche `main` ; - - HEAD et `origin/main` : `112428373dbd5741b55028e3efca299480a769bb` ; - - merge PR #27 : **Add ENCHANT_TRADE_V1 native enchanting trade service** ; - - aucun changement requis pour le correctif de spam Trade. + - branche `jellypowered-chatless-integration-v2`, créée depuis `main` le 20/08/2026 ; + - HEAD = `main` = `origin/main` = `origin/jellypowered-chatless-integration-v2` au baseline audité : `d42b23dd288b6ff0871c57bedd98856b705594da` ; + - ahead/behind versus `main` : `0/0` avant cette synchronisation documentaire ; + - PR Jellypowered #28 déjà mergée, merge commit `5e5ff7594ec8afedf40926605a60848dbc14991e` ; + - PR SelfBot #30 **Complete SelfBot chatless bridge support** déjà mergée ; le worktree Bridge est propre au baseline audité. - Playerbots : `L:\AC_PB\azerothcore-wotlk\modules\mod-playerbots` - - branche `master`, commit `a7b885d27134466dbc1c91d39b8241ea725a1bbb` ; - - **lecture seule stricte** ; invariant avant/après audit : `OK`. -- AzerothCore : branche `Playerbot`, commit `092e9ba6ff8dc6d861dddd1f31baa9d404381a85`, worktree propre pendant l'audit. + - **lecture seule stricte** ; fingerprints source identiques avant/après les audits ciblés du 20/08/2026. - Communication actuelle : bridge-first pour les principaux rafraîchissements UI et pour plusieurs actions d'écriture explicitement bornées ; des occurrences `SendChatMessage` subsistent et doivent être classées/migrées famille par famille. - Fallback automatique legacy désactivé par défaut : `MultiBot.allowLegacyChatFallback = false`. Certains chemins de compatibilité historiques restent toutefois explicitement documentés jusqu'à leur migration ou leur suppression validée. @@ -135,31 +140,50 @@ La branche `Extended` est divergente et ne doit pas être mergée en bloc. Chaqu - le fallback legacy `ue` n'est possible que lorsque `MultiBot.allowLegacyChatFallback == true`; avec la configuration bridge-first normale (`false`), aucun whisper automatique n'est émis ; - tests en jeu validés : slot simple, main hand, 2H/offhand, inventaire plein, double clic rapide, aucune perte/duplication, rafraîchissement cohérent et zéro chat parasite. -5. **Utilisation native d'un item précis — PROCHAIN CHANTIER JELLYPOWERED / À AUDITER-ADAPTER** - - endpoint spécialisé `ITEM_USE` ; - - item exact, usage explicitement supporté, cooldown/cast/mouvement/état revalidés ; - - ne pas considérer l'action réussie avant validation du résultat réel. - -6. **Déplacement/rééquipement de sacs — À ADAPTER, priorité faible** - - `BAG_MOVE` Extended déplace un sac entre emplacements de sacs équipés ; - - ne pas le présenter comme un déplacement arbitraire de tout item. - -7. **Échange d'un item précis — À ADAPTER** - - étudier `ITEM_TRADE` avec quantité et emplacement exact ; - - revalider partenaire, distance, Trade actif, propriété et contrôle du bot. - -8. **Abandon et partage de quête — À ADAPTER** - - endpoints `QUEST_ABANDON` et `QUEST_SHARE` ; - - valider présence de la quête, partageabilité, groupe, cible et état du bot. +5. **Utilisation native d'un item précis — TERMINÉ / VALIDÉ EN JEU** + - `ITEM_USE_V1` utilise une source physique exacte et un résultat structuré `INVENTORY_ITEM_USE` ; + - le Bridge revalide le bot, l'identité de la source et l'état runtime avant d'emprunter le chemin natif `HandleUseItemOpcode` ; + - aucune mutation optimiste Addon ; le snapshot est rafraîchi après le résultat autoritatif ; + - la postcondition des objets démarrant une quête a été corrigée ; le cas négatif « quête déjà acceptée » reste un test runtime différé, pas un chantier `ITEM_USE_V1` à réimplémenter. + +6. **Déplacement/rééquipement des sacs équipés eux-mêmes — À ADAPTER, priorité faible** + - `ITEM_MOVE_V1` couvre déjà les piles d'items dans Backpack / sacs 1..4 / Keyring, y compris les déplacements inter-conteneurs ; + - le résiduel `BAG_MOVE` concerne uniquement l'objet sac placé dans un emplacement de sac équipé ; + - les emplacements d'équipement des sacs ne font pas partie de la whitelist actuelle `ITEM_MOVE_V1` et ne doivent pas être ajoutés sans audit dédié ; + - ne jamais présenter ce point comme un déplacement générique des objets d'inventaire. + +7. **Échange générique d'un item précis — TERMINÉ / VALIDÉ EN JEU — `ITEM_TRADE_V1`** + - l'UI Inventory -> Trade utilise désormais un endpoint structuré bridge-first avec identité de source exacte et résultat `INVENTORY_ITEM_TRADE` ; + - le workflow natif WoW Trade est préservé et le Bridge emprunte le handler AzerothCore natif pour placer l'item exact dans le Trade ; + - le fallback historique de don/échange reste derrière `MultiBot.allowLegacyChatFallback == true` et n'est pas utilisé en configuration bridge-first normale ; + - `ENCHANT_TRADE_V1` reste un service spécialisé distinct et n'a pas été généralisé ; + - validation runtime du 20/08/2026 : Trade natif validé dans les deux sens, aucune régression de l'UI Trade et aucun exécuteur générique de commande Playerbots. + +8. **Abandon et partage de quête — TERMINÉ POUR LE BESOIN ACTUEL** + - `QUEST_ABANDON_V1` utilise `RUN~QUEST_ABANDON~token~questId` et un résultat structuré `QUEST_ABANDON_RESULT` ; + - le Bridge limite l'exécution aux bots visibles du requester appartenant au même groupe, revalide la sécurité Playerbots, la session/l'état monde et recherche la quête dans les slots bornés `0..MAX_QUEST_LOG_SIZE-1` ; + - l'abandon bot passe par `WorldPackets::Quest::QuestLogRemoveQuest`, `Read()` puis `WorldSession::HandleQuestLogRemoveQuest`, avec vérification de postcondition ; + - protections : **4 requêtes / 2 s / requester**, TTL anti-rejeu **10 s**, **32** tokens récents maximum et **512** états requester maximum ; + - côté Addon, le joueur conserve l'abandon WoW natif `SetAbandonQuest()` / `AbandonQuest()` ; le fallback historique `drop ` n'est disponible que si `MultiBot.allowLegacyChatFallback == true` ; + - test runtime 1 bot du 20/08/2026 : joueur et bot abandonnent correctement, zéro chat/whisper parasite, zéro erreur Lua ; + - **test différé roadmap** — `DEFERRED_RUNTIME_TEST_NO_BOTS_AVAILABLE` : scénario avec plusieurs bots dont au moins un sans la quête, faute de bots disponibles au moment de la validation ; + - `QUEST_SHARE` est déjà natif/chatless via `QuestLogPushQuest()` : aucun endpoint `QUEST_SHARE_V1` n'est créé ni nécessaire pour le comportement actuel. 9. **Application/reset de talents — À ADAPTER AVEC PRUDENCE** - endpoint `TALENT_APPLY` ; - valider build, niveau, points, coûts/reset, combat, double spécialisation et effets runtime ; - vérifier les API Playerbots/AzerothCore dans le dépôt local avant toute reprise. -10. **Artisanat ciblé — À ADAPTER** - - étudier `CRAFT_RECIPE_TARGET` ; - - revalider profession, recette, matériaux, outils, cible exacte, compatibilité et état Trade. +10. **Artisanat ciblé — TERMINÉ / COMPILÉ / VALIDÉ EN JEU — `CRAFT_RECIPE_TARGET_V1`** + - `RUN~CRAFT_RECIPE` reste inchangé pour le craft normal et renvoie `TARGET_REQUIRED` lorsqu'une recette exige une cible item exacte ; + - l'Addon réutilise Inventory + Inspect du bot pour sélectionner exactement `bag/slot/itemId`, avec au plus **8** requêtes ciblées en attente et un timeout de **5 s** ; + - scope cible autorisé : équipement, Backpack et sacs équipés 1..4 ; Bank, Keyring et objet Trade joueur sont exclus de `CRAFT_RECIPE_TARGET_V1` ; + - le Bridge revalide requester/session, contrôle du bot, profession/recette, matériaux/outils, position autorisée et identité courante de l'item avant exécution ; + - la cible est re-résolue par `GetItemByPos`, contrôlée par `Spell::InitExplicitTargets()` / `CheckCast(true)` puis exécutée sur l'`Item*` exact ; + - protections serveur validées pour ce service : **4 requêtes / 2 s / requester**, TTL anti-rejeu **10 s**, **32** tokens récents maximum et **512** états requester maximum ; + - résultat structuré `CRAFT_RECIPE_TARGET_RESULT`, sans fallback chat normal ; `ENCHANT_TRADE_V1` reste le service séparé pour l'objet joueur dans `TRADE_SLOT_NONTRADED` ; + - tests runtime : craft simple **OK**, enchantement d'un objet exact appartenant au bot **OK**, aucun spam chat observé ; + - amélioration UI validée : recette `craftable > 0` en vert vif, sinon couleur de difficulté conservée ; dans la fenêtre Enchantements, `entry.available ~= 0` (tous réactifs + outils requis présents) affiche également le nom en vert. 11. **Banque / banque de guilde / vendeur — COMPARER PUIS ADAPTER** - ces familles existent déjà dans notre Bridge ; @@ -326,7 +350,7 @@ Les jalons suivants, postérieurs à la mise à jour du 08/08, sont présents da - Outfits : transport bridge-first et négociation `OUTFIT_V1` ; - inventaire : lecture/rafraîchissement natifs via `INVENTORY_V1` ; - banque, banque de guilde et achat vendeur : durcissements serveur des actions `ITEM_ACTION` ; -- vente inventaire `SELL_VENDOR` : bridge-first lorsque `INVENTORY_BULK_SELL_V1` est négocié ; le fallback legacy de compatibilité demeure hors chemin normal ; +- vente inventaire `SELL_VENDOR` : bridge-first lorsque `INVENTORY_BULK_SELL_V1` est négocié ; le fallback legacy `s vendor` n'est accessible que si `MultiBot.allowLegacyChatFallback == true` ; côté Bridge, `SELL_VENDOR` accepte uniquement `ITEM_USAGE_VENDOR` et exclut `ITEM_USAGE_AH` ; validation runtime du 23/08/2026 : Symbol of Kings et Gold Ore conservés, vente vendeur toujours fonctionnelle ; - `OPEN_ITEMS` : bridge-first via `INVENTORY_OPEN_V1`, avec traitement résiduel borné côté serveur ; - `GROUP ROLL` : bridge-first via `GROUP_ROLL_V1`, avec mode normal et mode item, filtrage aux bots visibles/contrôlables du groupe, rate-limit serveur et ACK structuré. @@ -442,13 +466,13 @@ Ordre recommandé et état réel : 3. **Infrastructure mutations stratégies `co/nc` : TERMINÉE pour les chemins migrés** via `STRATEGY_MUTATION_V1`, `RUN~STRATEGY`, `STRATEGY_ACK`, timeouts, limites et diagnostics explicites. 4. **Sélecteurs Warlock Stones/Soulstones/Pets/Curses : TERMINÉS pour la migration chatless validée**. Les reliquats TEMP_ENCHANT réel et LuaLint sont suspendus et ne bloquent pas la roadmap normale. 5. **`s *` / `SELL_GREY` : SUSPENDU** — le chemin actuel existe, mais le chantier `SELL_GREY / sell-grey core API / bridge-first` est explicitement reporté à la fin de la roadmap. -6. **`s vendor` / `SELL_VENDOR` : TERMINÉ pour le chemin bridge-first inventaire** — `INVENTORY_BULK_SELL_V1`, validation serveur et résultat structuré ; fallback legacy de compatibilité conservé si la capacité n'est pas disponible. +6. **`s vendor` / `SELL_VENDOR` : TERMINÉ / VALIDÉ EN JEU / SYNCHRONISÉ 23/08/2026** — `INVENTORY_BULK_SELL_V1`, validation serveur et résultat structuré ; fallback legacy conservé uniquement sous `MultiBot.allowLegacyChatFallback == true` ; le Bridge accepte seulement `ITEM_USAGE_VENDOR` et exclut `ITEM_USAGE_AH` pour cette action. Tests runtime : Symbol of Kings conservé, Gold Ore conservé, `SELL_VENDOR` fonctionnel. Commits validés : Addon `fe2c807785219b82ca885f1a95d7c1dc27f0eed0`, Bridge `3ccf5047f7994218b742312fe1437f4b303f7159`. 7. **`open items` / `OPEN_ITEMS` : TERMINÉ / VALIDÉ / MERGÉ** — `INVENTORY_OPEN_V1`, Addon PR #60, Bridge PR #25. 8. **`roll` et `roll [item]` : TERMINÉ / VALIDÉ / MERGÉ** — `GROUP_ROLL_V1`, Addon PR #61, Bridge PR #26. 9. **Enchantement d'objet : TERMINÉ / VALIDÉ EN JEU / MERGÉ — Addon #63 / Bridge #27** — `ENCHANT_TRADE_V1`, UI dédiée aux enchanteurs, liste des enchantements réellement connus, composants/outils, Trade WoW natif via le slot « ne sera pas échangé », exécution par ID de sort numérique validé côté bridge, sans exécuteur générique de cast/chat ; layout 440 px et i18n des 8 locales validés. 10. **Spam inventaire automatique à l'ouverture Trade : TERMINÉ / VALIDÉ EN JEU — PR ADDON #64 EN COURS** — réutilisation puis généralisation du filtre addon existant : détection du header exact `=== Inventory ===` pour un bot connu, suppression du dump lors des chemins Inventory → Trade, Enchanting → Trade et du menu natif WoW « Échanger », sans modification de Playerbots ni du Bridge. -11. **PROCHAIN CHANTIER NORMAL — Ajout/retrait d'items précis dans les règles de loot.** -12. **À FAIRE — Décision sur `Quest`/`Skill` versus `Disenchant`**, sans inventer de stratégie absente de Playerbots. +11. **TERMINÉ / VALIDÉ EN JEU / COMMITÉ / PUSHÉ — `LOOT_RULE_ITEM_V1`** : ajout/retrait exact d'un `itemId` dans l'`always loot list`, résultats structurés, idempotence, persistance bornée et UI localisée. +12. **PROCHAIN CHANTIER NORMAL — Décision sur `Quest`/`Skill` versus `Disenchant`**, uniquement à partir des capacités réellement présentes dans Playerbots ; ne pas réintroduire des modes issus de documentation historique non vérifiée. 13. **À FAIRE — Ordres collectifs `follow`, `attack`, `stay`**, seulement après validation manuelle exacte des sélecteurs Playerbots ; ne pas réintroduire `RUN~ORDER` générique. Les commandes informatives `who`, `co ?`, `nc ?` et `ss ?` restent manuelles tant qu'aucune UI structurée ne les remplace. Les mutations UI automatiques `co/nc`, en revanche, doivent passer par le bridge dès qu'un contrat structuré validé existe. @@ -516,9 +540,9 @@ Ces fonctions doivent rester séparées des patches de correction et de sécurit Critère de sortie : version stabilisée, documentée et reproductible du projet Multibot Chatless + Bridge. - + -## État consolidé Jellypowered / inventaire — 17/08/2026 +## État consolidé Jellypowered / inventaire — 20/08/2026 > **Référence de progression actuelle.** Ce bloc supplante les anciens libellés « prochain chantier Jellypowered » conservés plus haut à titre historique. Il ne modifie pas l'ordre de la roadmap normale après clôture du lot Jellypowered. @@ -534,10 +558,23 @@ Critère de sortie : version stabilisée, documentée et reproductible du projet - **Destruction d'un item exact** : `ITEM_DESTROY` validé via endpoint spécialisé. - **Vente unitaire exacte** : `ITEM_SELL_SINGLE_V1` validé avec source exacte, vendeur proche revalidé, protections des objets non vendables/protégés, rate limit/replay et absence de mutation optimiste. - **Rachat vendeur** : `VENDOR_BUYBACK_V1` validé avec liste structurée, vendeur proche revalidé, exécution via le handler natif de Buyback et rafraîchissements autoritatifs de l'inventaire et de la liste de rachat. +- **Échange générique exact** : `ITEM_TRADE_V1` validé en jeu dans les deux sens, avec source exacte, Trade WoW natif préservé, résultat structuré et fallback historique explicitement conditionné. +- **Abandon de quête** : `QUEST_ABANDON_V1` compilé et validé avec un bot, via le paquet Quest AzerothCore typé et le handler natif ; zéro spam chat et zéro erreur Lua. Le test multi-bots mixte reste différé faute de bots disponibles. +- **Partage de quête** : déjà natif/chatless via `QuestLogPushQuest()` ; aucun endpoint Bridge additionnel n'est nécessaire. +- **Application de talents personnalisés** : `TALENT_APPLY_V1` compilé et validé en jeu ; validation classe/DBC/points côté Bridge, réutilisation du chemin Playerbots audité, vérification serveur des trois arbres via `BuildTalentTabPoints`, reset des stratégies uniquement après succès et confirmation visuelle localisée côté Addon après `OK`. +- **Sélecteur de spécialisations prémontées** : `TALENT_SPEC_APPLY_V1` compilé et validé en jeu en clic gauche/slot 1 et clic droit/slot 2. `TALENT_SPEC_CURRENT` fournit l'état courant sans whisper normal ; le Bridge revalide l'index du modèle, gère le dual spec, interrompt le cast, applique talents + glyphes, vérifie les totaux finaux et renvoie un résultat structuré. Les anciens `talents`, `talents spec list`, `stopcasting`, `talents switch` et `talents spec` ne restent que dans le fallback explicitement activé. Toutes ces intégrations conservent `mod-playerbots` en **lecture seule stricte** et n'introduisent aucun exécuteur générique de commande Playerbots. -### Stabilisation pré-merge — 17/08/2026 +### Merges validés et nouvelle baseline v2 — 20/08/2026 + +- Addon : le lot Jellypowered est déjà intégré à `main` via la PR #67, merge commit `70e72ba6cb9a7170497b201e0dbe469bb29e6be9`. +- Bridge : le lot Jellypowered est déjà intégré à `main` via la PR #28, merge commit `5e5ff7594ec8afedf40926605a60848dbc14991e`. +- Le chantier SelfBot, traité séparément, est lui aussi déjà intégré à `main` via Addon PR #72 et Bridge PR #30 ; les capacités héritées sont `SELF_BOT_V1`, `SELF_STRATEGY_V1` et `SELF_ACTION_V1`. +- Les nouvelles branches `jellypowered-chatless-integration-v2` ont été créées directement depuis ces `main` le 20/08/2026 et étaient à `0/0` ahead/behind au baseline audité. +- Les résiduels SelfBot Bridge déjà identifiés (`dps aoe` -> stratégie native `aoe`, robustesse du rollback différé Warlock) restent un chantier séparé et ne doivent pas être corrigés pendant Jellypowered. + +### Stabilisation validée avant merge — historique du 17/08/2026 - CAPS : fragmentation/budget wire bornés et corrigés. - `ITEM_MOVE_V1` : postcondition de déplacement de pile entière corrigée. @@ -548,37 +585,53 @@ Toutes ces intégrations conservent `mod-playerbots` en **lecture seule stricte* - `ITEM_USE_V1` : namespace locale et raisons d'échec localisées ; tooltip Inspect localisé dans les 8 locales auditées. - `VENDOR_BUYBACK_V1` : garde nil de création de frame ajoutée sans changement de protocole. - LuaLint : variable inutilisée `BUYBACK_ROWS` supprimée. -- Les contrôles globaux LuaLint/CI restent une porte de sortie pré-merge et doivent encore être exécutés après cette synchronisation documentaire. +- Ces contrôles constituaient la porte de sortie du lot avant son merge ; les futurs patches Jellypowered v2 devront à nouveau passer les contrôles applicables avant tout nouveau merge. ### Audité et différé / non intégré - `GET~INVENTORY_BULK` : audité ; la forme Extended reste rejetée/différée tant qu'elle duplique sans bénéfice démontré `INVENTORY_EXACT_V1`. - `GET~BOT_SKILLS_BULK` : audité ; différé jusqu'à l'apparition d'un consommateur multi-bot réel. -### Jellypowered restant à étudier +### Clôture banque / banque de guilde — P3A validé le 24/08/2026 + +- **P3A — TERMINÉ / VALIDÉ EN JEU / COMMITÉ / PUSHÉ — `ITEM_DEPOSIT_EXACT_V1`** : `BANK_DEPOSIT` et `GBANK_DEPOSIT` utilisent désormais l'identité physique exacte `bag/slot/itemId/count` issue de l'inventaire exact et déplacent uniquement la pile entière sélectionnée. +- Le Bridge revalide la position source, l'`itemId` et le `count` avant mutation. Le test négatif runtime avec un faux `count` a renvoyé `SOURCE_STALE` et la pile réelle est restée inchangée. +- Les tests runtime avec deux piles distinctes du même objet ont validé que BANK et GBANK déplacent uniquement la pile cliquée, sans déplacer l'autre pile identique, sans erreur Lua, spam chat ou crash observé. +- Protections du service exact-deposit : **8 requêtes / 2 s / requester**, TTL anti-rejeu **10 s**, **32** tokens récents maximum et **512** états requester maximum. +- Commits P3A validés : Addon `b13ff797ab96144f2ae51cc4adf6a0d4a1c4e464`, Bridge `15abaae7adc6b983f25e9c4b5dec13ca314d5727`. +- **P3B — DIFFÉRÉ : retrait exact BANK**. Le snapshot de retrait actuel est agrégé par `itemId`; exposer une pile source physique exacte nécessite une évolution protocole/UI plus large. +- **P3C — DIFFÉRÉ : retrait exact GBANK**. Le Bridge manipule déjà des coordonnées physiques lors de l'exécution, mais la sélection source exacte n'est pas exposée de bout en bout jusqu'à l'Addon ; la refonte est reportée. +- **Résiduel UI basse priorité, non bloquant :** `SOURCE_STALE` utilise encore le libellé générique d'erreur d'action d'objet. +- **Résiduel basse priorité, non bloquant :** `BAG_MOVE` concerne uniquement le déplacement/rééquipement des **sacs équipés eux-mêmes**. Les déplacements d'items entre Backpack / sacs 1..4 / Keyring sont déjà terminés via `ITEM_MOVE_V1`. -L'ordre exact sera redécidé après synchronisation/commit de la branche ; **aucun de ces points n'est marqué comme prochain chantier actif par ce document**. +Le lot banque/GBANK prioritaire est clos pour la roadmap courante. Le chantier suivant, `LOOT_RULE_ITEM_V1`, est lui aussi désormais clos et validé ; la roadmap normale passe à la décision Quest/Skill versus Disenchant. -- `BAG_MOVE` — déplacement/rééquipement de sacs ; priorité faible. -- `ITEM_TRADE` — item exact, quantité, partenaire, distance, Trade actif, propriété et contrôle du bot ; ne pas régresser `ENCHANT_TRADE_V1`. -- `QUEST_ABANDON` — endpoint spécialisé uniquement. -- `QUEST_SHARE` — endpoint spécialisé uniquement. -- `TALENT_APPLY` — audit strict des API Playerbots locales, points, niveau, reset/coût, combat et dual spec avant toute proposition. -- `CRAFT_RECIPE_TARGET` — profession/recette/matériaux/outils/cible/Trade à revalider. -- Banque / banque de guilde / vendeur — comparer avec l'existant et ne reprendre que des améliorations démontrables. -- Comptage d'inventaire / restauration de sélection — comparer puis adapter uniquement si un défaut actuel est démontré. +### Clôture règles de loot exactes — `LOOT_RULE_ITEM_V1` validé le 24/08/2026 -### Chantiers suspendus — inchangés +- **TERMINÉ / VALIDÉ EN JEU / AUDITÉ / ARCHIVÉ / COMMITÉ / PUSHÉ** : ajout ou retrait d'un `itemId` exact dans la valeur Playerbots auditée `always loot list`, sans modification de `mod-playerbots`. +- Contrat : `RUN~LOOT_RULE_ITEM~~~~~` et résultat structuré `LOOT_RULE_ITEM_RESULT`; scopes protocole `ALL`, `RAID`, `GROUP`, `PARTY`, `BOT`, l'UI actuelle envoyant volontairement `ALL`. +- Prévalidation avant mutation : requester/session/world, scope/target, bots visibles et contrôlables, sécurité Playerbots, session/world/alive/context du bot, `ItemTemplate` valide ; maximum **128 bots**. +- Protections : **8 requêtes / 2 s / requester**, anti-rejeu **10 s**, **32** tokens récents et **512** états requester. +- Persistance : seuls les bots réellement modifiés sont sauvegardés ; budget serveur global **128 sauvegardes de bots / 10 s**. Un budget insuffisant renvoie `PERSISTENCE_BUSY` avant toute mutation. +- Sémantique idempotente validée : `ADDED`, `REMOVED`, `ALREADY_PRESENT`, `ALREADY_ABSENT`, avec `PARTIAL` prévu pour les résultats mixtes et erreurs structurées côté Bridge. +- Runtime validé : ADD/REMOVE et répétitions, itemId numérique, Shift+clic lien d'objet, entrée locale invalide sans envoi Bridge, item serveur invalide, persistance déconnexion/reconnexion, persistance après restart worldserver, nettoyage final de l'item de test 6948. +- UI/i18n validée : prompt cliquable/éditable corrigé localement sans modifier AceGUI global, `/reload` sans erreur Lua, boutons/résultats localisés dans les huit locales présentes (`deDE`, `enGB`, `enUS`, `esES`, `frFR`, `koKR`, `ruRU`, `zhCN`), aucun spam chat/whisper observé. +- Divergence documentaire conservée comme décision séparée : l'audit du build Playerbots courant ne permet pas de traiter `Quest`/`Skill` comme capacités validées sur la seule foi du wiki ; la prochaine étape doit trancher Quest/Skill versus Disenchant à partir du code réellement présent. + +### Chantiers suspendus — à reprendre seulement après la roadmap normale À ne pas reprendre pendant la roadmap normale sauf demande explicite : +- **P3B** — retrait exact BANK, avec snapshot/protocole/UI de sélection physique à concevoir ; +- **P3C** — retrait exact GBANK, avec sélection physique de source à exposer de bout en bout ; +- libellé UI dédié pour `SOURCE_STALE`, actuellement générique et non bloquant ; - `SELL_GREY` / sell-grey core API / bridge-first ; - diagnostic réel final Firestone / Spellstone `TEMP_ENCHANTMENT_SLOT` ; - quatre warnings LuaLint restants dans `Strategies/MultiBotWarlock.lua` ; - autres petits reliquats déjà explicitement reportés. -### Après le lot Jellypowered +### Reprise de la roadmap normale -Le **prochain chantier normal** reste : **ajout/retrait d'items précis dans les règles de loot**. +Le **prochain chantier fonctionnel** est : **décision Quest/Skill versus Disenchant à partir des capacités Playerbots réellement présentes**. Aucun endpoint, mode ou stratégie ne doit être conçu avant l'audit ciblé de ce point. -La décision du prochain sous-chantier Jellypowered restant sera prise après la synchronisation manuelle de la branche, à partir de cet état consolidé. +Le lot fonctionnel courant de `jellypowered-chatless-integration-v2` est terminé avec `LOOT_RULE_ITEM_V1`. Cette synchronisation documentaire prépare la clôture de la branche : après commit/push des documents, les branches Addon et Bridge doivent être proposées par PR vers `main`. Aucun nouveau développement fonctionnel ne doit être empilé sur ces branches avant les PR. Après merge et mise à jour des `main`, la roadmap normale reprendra sur de nouvelles branches créées depuis ces baselines. P3B/P3C, le libellé `SOURCE_STALE`, `BAG_MOVE`, `SELL_GREY`, Firestone/Spellstone TEMP_ENCHANT, LuaLint et les autres reliquats explicitement différés restent hors du prochain chantier.