diff --git a/.luacheckrc b/.luacheckrc index d283814..36a3e70 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -33,7 +33,7 @@ globals = { "sendInventoryItemCommand", "LE_ITEM_CLASS_QUESTITEM", "ITEMS", "LOADING", "QUEST_LOG", "INSPECT", "SPELLBOOK", "MB_TAB_TITLE_DEFAULT", "ensureHiddenTooltip", "IsInGuild", "GetGuildInfo", "GetGuildRosterShowOffline", "SetGuildRosterShowOffline", "PLAYER","Ambiguate", "ChatFrame_AddMessageEventFilter", "ChatTypeInfo", "x", "y", "CLASS_ICON_TCOORDS", "GetLootSlotLink", "GetLootSlotInfo", "GetLootMethod", "GetMasterLootCandidate", "LOCALIZED_CLASS_NAMES_MALE", "LOCALIZED_CLASS_NAMES_FEMALE", "date", "LootSlotIsCoin", "LootSlotIsItem", "classColor", "GetNumLootItems", "GetItemQualityColor", "GiveMasterLoot", "GetLootThreshold", - "QuestFrameRewardPanel", "QuestFrame", "GetFactionInfoByID", "PanelTemplates_SetTab", "PanelTemplates_SetNumTabs", "ABANDON_QUEST", "GetCoinTextureString", "InviteUnit" + "QuestFrameRewardPanel", "QuestFrame", "GetFactionInfoByID", "PanelTemplates_SetTab", "PanelTemplates_SetNumTabs", "ABANDON_QUEST", "GetCoinTextureString", "InviteUnit", "TradeFrame" } diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index 3913043..06c7299 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -18,7 +18,10 @@ local INVENTORY_CAPABILITY = "INVENTORY_V1" 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 GROUP_ROLL_TIMEOUT_SECONDS = 5.0 +local ENCHANT_TRADE_TIMEOUT_SECONDS = 5.0 +local ENCHANT_TRADE_MAX_ACTIVE = 8 local GROUP_ROLL_MAX_ITEM_LINK_LENGTH = 160 local STATE_TIMEOUT_SECONDS = 5.0 local STATES_TIMEOUT_SECONDS = 15.0 @@ -239,6 +242,11 @@ local function ensureBridgeState() state.inventoryBulkSellCapable = state.inventoryBulkSellCapable or false state.inventoryOpenCapable = state.inventoryOpenCapable or false state.groupRollCapable = state.groupRollCapable or false + state.enchantTradeCapable = state.enchantTradeCapable or false + state.enchantTradeSeq = state.enchantTradeSeq or 0 + state.enchantTradeActive = state.enchantTradeActive or nil + state.enchantTradeCommands = state.enchantTradeCommands or {} + state.enchantTradeLists = state.enchantTradeLists or {} state.groupRollSeq = state.groupRollSeq or 0 state.groupRollCommands = state.groupRollCommands or {} state.strategyMutationSeq = state.strategyMutationSeq or 0 @@ -1710,6 +1718,143 @@ function Comm.RequestProfessionRecipes(name, skillId) return true end +function Comm.IsEnchantTradeCapable() + local state = ensureBridgeState() + return state.connected == true and state.enchantTradeCapable == true +end + +function Comm.IsBotEnchanter(name) + local state = ensureBridgeState() + name = string.lower(trim(name)) + if name == "" then + return false + end + + local entry = state.professions and state.professions[name] or nil + return type(entry) == "table" + and type(entry.professions) == "table" + and entry.professions.enchanting ~= nil +end + +local function scheduleEnchantTradeListTimeout(name, token, delaySeconds) + safeDelay(delaySeconds or ENCHANT_TRADE_TIMEOUT_SECONDS, function() + local bridge = ensureBridgeState() + local active = bridge.enchantTradeActive + if type(active) ~= "table" or active.token ~= token then + return + end + + local now = safeNow() + local lastProgressAt = tonumber(active.lastProgressAt) or tonumber(active.startedAt) or 0 + if now > 0 and lastProgressAt > 0 then + local idleSeconds = now - lastProgressAt + if idleSeconds < ENCHANT_TRADE_TIMEOUT_SECONDS + and MultiBot + and type(MultiBot.TimerAfter) == "function" then + scheduleEnchantTradeListTimeout( + name, + token, + math.max(0.05, ENCHANT_TRADE_TIMEOUT_SECONDS - idleSeconds) + ) + return + end + end + + bridge.enchantTradeActive = nil + if MultiBot.OnBridgeEnchantTradeList then + MultiBot.OnBridgeEnchantTradeList(active.botName or name, {}, { + token = token, + status = "ERR", + reason = "TIMEOUT", + skillValue = 0, + maxSkill = 0, + }) + end + end) +end + +local function markEnchantTradeListProgress(active) + if type(active) == "table" then + active.lastProgressAt = safeNow() + end +end + +function Comm.RequestEnchantTrade(name) + local state = ensureBridgeState() + name = trim(name) + if name == "" or not state.connected or state.enchantTradeCapable ~= true then + return false + end + + state.enchantTradeSeq = (tonumber(state.enchantTradeSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-ench-list-" .. tostring(state.enchantTradeSeq) + local now = safeNow() + state.enchantTradeActive = { + botName = name, + botNameKey = string.lower(name), + token = token, + startedAt = now, + lastProgressAt = now, + began = false, + status = "PENDING", + reason = "", + skillValue = 0, + maxSkill = 0, + items = {}, + } + + if not Comm.Send("GET", "ENCHANT_TRADE~" .. name .. "~" .. token) then + state.enchantTradeActive = nil + return false + end + + scheduleEnchantTradeListTimeout(name, token, ENCHANT_TRADE_TIMEOUT_SECONDS) + return token +end + +function Comm.RunEnchantTrade(name, spellId) + local state = ensureBridgeState() + name = trim(name) + spellId = tonumber(spellId or 0) or 0 + if name == "" or spellId <= 0 or not state.connected or state.enchantTradeCapable ~= true then + return false + end + + if countTableEntries(state.enchantTradeCommands) >= ENCHANT_TRADE_MAX_ACTIVE then + return false + end + + state.enchantTradeSeq = (tonumber(state.enchantTradeSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-ench-run-" .. tostring(state.enchantTradeSeq) + state.enchantTradeCommands[token] = { + botName = name, + botNameKey = string.lower(name), + spellId = spellId, + token = token, + startedAt = safeNow(), + } + + if not Comm.Send("RUN", "ENCHANT_TRADE~" .. name .. "~" .. token .. "~" .. tostring(spellId)) then + state.enchantTradeCommands[token] = nil + return false + end + + safeDelay(ENCHANT_TRADE_TIMEOUT_SECONDS, function() + local bridge = ensureBridgeState() + local command = bridge.enchantTradeCommands[token] + if not command then + return + end + + bridge.enchantTradeCommands[token] = nil + if MultiBot.OnBridgeEnchantTradeResult then + MultiBot.OnBridgeEnchantTradeResult(command.botName, command.spellId, "ERR", "TIMEOUT", command) + end + end) + + return token +end + function Comm.RunProfessionRecipeCraft(name, skillId, spellId, itemId) local state = ensureBridgeState() name = trim(name) @@ -1893,6 +2038,24 @@ function Comm.MarkDisconnected(reason) state.botEmblemActive = nil state.professionRecipeActive = nil state.professionRecipeCrafts = {} + + if type(state.enchantTradeActive) == "table" and MultiBot.OnBridgeEnchantTradeList then + MultiBot.OnBridgeEnchantTradeList(state.enchantTradeActive.botName or "", {}, { + token = state.enchantTradeActive.token or "", + status = "ERR", + reason = "DISCONNECTED", + skillValue = 0, + maxSkill = 0, + }) + end + for _, command in pairs(state.enchantTradeCommands or {}) do + if MultiBot.OnBridgeEnchantTradeResult then + MultiBot.OnBridgeEnchantTradeResult(command.botName or "", command.spellId or 0, "ERR", "DISCONNECTED", command) + end + end + state.enchantTradeActive = nil + state.enchantTradeCommands = {} + state.enchantTradeLists = {} state.outfitActive = nil state.outfitCommands = {} state.trainerActive = nil @@ -1905,6 +2068,7 @@ function Comm.MarkDisconnected(reason) state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false state.groupRollCapable = false + state.enchantTradeCapable = false state.stateFramingCapable = false state.capabilityFallbackDeadline = 0 state.capabilityFallbackGeneration = 0 @@ -1916,6 +2080,10 @@ function Comm.MarkDisconnected(reason) state.pendingStateRefreshAll = false state.pendingStateRefreshByBot = {} + if MultiBot.RefreshEnchantingEveryButtons then + MultiBot.RefreshEnchantingEveryButtons() + end + local pendingTokens = {} for token in pairs(state.strategyMutationCommands or {}) do pendingTokens[#pendingTokens + 1] = token @@ -3516,6 +3684,24 @@ local function getActiveProfessionRecipeRequest(botName, token, skillId) return active end +local function getActiveEnchantTradeRequest(botName, token) + local state = ensureBridgeState() + local active = state.enchantTradeActive + if type(active) ~= "table" then + return nil + end + + if botName and botName ~= "" and string.lower(trim(botName)) ~= trim(active.botNameKey or "") then + return nil + end + + if token and token ~= "" and tostring(token) ~= tostring(active.token or "") then + return nil + end + + return active +end + local function parseRecipeMaterials(raw) local materials = {} for token in string.gmatch(raw or "", "([^;]+)") do @@ -3596,6 +3782,7 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false state.groupRollCapable = false + state.enchantTradeCapable = false for capability in string.gmatch(payload or "", "([^,]+)") do capability = trim(capability) if capability == STATE_FRAMING_CAPABILITY then @@ -3612,6 +3799,8 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) state.inventoryOpenCapable = true elseif capability == GROUP_ROLL_CAPABILITY then state.groupRollCapable = true + elseif capability == ENCHANT_TRADE_CAPABILITY then + state.enchantTradeCapable = true end end state.capabilityFallbackDeadline = 0 @@ -3619,6 +3808,9 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) state.capabilitiesResolved = true debugPrint("ADDON:RX", "CAPS", payload or "") flushPendingStateRefreshes() + if MultiBot.RefreshEnchantingEveryButtons then + MultiBot.RefreshEnchantingEveryButtons() + end return true end @@ -4470,6 +4662,274 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) return true end + if opcode == "ENCHANT_TRADE_BEGIN" then + local fields = splitFields(payload or "") + if #fields ~= 6 then + state.lastError = "ENCHANT_TRADE_BEGIN_BAD_FIELD_COUNT" + return true + end + + local botName = trim(urlDecodeField(fields[1])) + local token = trim(fields[2]) + local status = string.upper(trim(fields[3])) + local reason = string.upper(trim(urlDecodeField(fields[4]))) + local skillValue = tonumber(fields[5] or "0") or 0 + local maxSkill = tonumber(fields[6] or "0") or 0 + state.connected = true + state.lastError = nil + + local active = getActiveEnchantTradeRequest(botName, token) + if active then + if active.began then + active.integrityError = active.integrityError or "DUPLICATE_BEGIN" + state.lastError = "ENCHANT_TRADE_DUPLICATE_BEGIN" + else + active.began = true + active.status = status + active.reason = reason + active.skillValue = skillValue + active.maxSkill = maxSkill + active.items = {} + active.itemBySpellId = {} + markEnchantTradeListProgress(active) + end + end + + return true + end + + if opcode == "ENCHANT_TRADE_ITEM" then + local fields = splitFields(payload or "") + if #fields ~= 7 then + state.lastError = "ENCHANT_TRADE_ITEM_BAD_FIELD_COUNT" + return true + end + + local botName = trim(urlDecodeField(fields[1])) + local token = trim(fields[2]) + local spellId = tonumber(fields[3] or "0") or 0 + local difficulty = trim(urlDecodeField(fields[4])) + local available = tonumber(fields[5] or "0") or 0 + local hasTools = tonumber(fields[6] or "0") or 0 + local materialCount = tonumber(fields[7] or "") + state.connected = true + state.lastError = nil + + local active = getActiveEnchantTradeRequest(botName, token) + if active then + if not active.began then + active.integrityError = active.integrityError or "MISSING_BEGIN" + state.lastError = "ENCHANT_TRADE_ITEM_BEFORE_BEGIN" + elseif spellId <= 0 or materialCount == nil or materialCount < 0 or materialCount > 256 then + active.integrityError = active.integrityError or "BAD_ITEM" + state.lastError = "ENCHANT_TRADE_ITEM_INVALID" + else + active.itemBySpellId = active.itemBySpellId or {} + if active.itemBySpellId[spellId] then + active.integrityError = active.integrityError or "DUPLICATE_SPELL_ID" + state.lastError = "ENCHANT_TRADE_DUPLICATE_SPELL_ID" + else + local entry = { + spellId = spellId, + difficulty = difficulty, + available = available ~= 0 and 1 or 0, + materials = {}, + hasTools = hasTools ~= 0 and 1 or 0, + expectedMaterialCount = materialCount, + receivedMaterialCount = 0, + materialIndexes = {}, + } + table.insert(active.items, entry) + active.itemBySpellId[spellId] = entry + markEnchantTradeListProgress(active) + end + end + end + + return true + end + + if opcode == "ENCHANT_TRADE_MATERIAL" then + local fields = splitFields(payload or "") + if #fields ~= 7 then + state.lastError = "ENCHANT_TRADE_MATERIAL_BAD_FIELD_COUNT" + return true + end + + local botName = trim(urlDecodeField(fields[1])) + local token = trim(fields[2]) + local spellId = tonumber(fields[3] or "0") or 0 + local materialIndex = tonumber(fields[4] or "0") or 0 + local itemId = tonumber(fields[5] or "0") or 0 + local required = tonumber(fields[6] or "0") or 0 + local available = tonumber(fields[7] or "0") or 0 + state.connected = true + state.lastError = nil + + local active = getActiveEnchantTradeRequest(botName, token) + if active then + if not active.began then + active.integrityError = active.integrityError or "MISSING_BEGIN" + state.lastError = "ENCHANT_TRADE_MATERIAL_BEFORE_BEGIN" + else + local entry = active.itemBySpellId and active.itemBySpellId[spellId] or nil + if not entry then + active.integrityError = active.integrityError or "MATERIAL_WITHOUT_ITEM" + state.lastError = "ENCHANT_TRADE_MATERIAL_WITHOUT_ITEM" + else + local expectedMaterialCount = tonumber(entry.expectedMaterialCount or 0) or 0 + entry.materialIndexes = entry.materialIndexes or {} + if materialIndex <= 0 or materialIndex > expectedMaterialCount then + active.integrityError = active.integrityError or "MATERIAL_INDEX_OUT_OF_RANGE" + state.lastError = "ENCHANT_TRADE_MATERIAL_INDEX_OUT_OF_RANGE" + elseif entry.materialIndexes[materialIndex] then + active.integrityError = active.integrityError or "DUPLICATE_MATERIAL_INDEX" + state.lastError = "ENCHANT_TRADE_DUPLICATE_MATERIAL_INDEX" + elseif itemId <= 0 or required <= 0 then + active.integrityError = active.integrityError or "BAD_MATERIAL" + state.lastError = "ENCHANT_TRADE_MATERIAL_INVALID" + else + entry.materials[materialIndex] = { + itemId = itemId, + required = required, + available = available, + } + entry.materialIndexes[materialIndex] = true + entry.receivedMaterialCount = (tonumber(entry.receivedMaterialCount or 0) or 0) + 1 + markEnchantTradeListProgress(active) + end + end + end + end + + return true + end + + if opcode == "ENCHANT_TRADE_END" then + local fields = splitFields(payload or "") + if #fields ~= 5 then + state.lastError = "ENCHANT_TRADE_END_BAD_FIELD_COUNT" + return true + end + + local botName = trim(urlDecodeField(fields[1])) + local token = trim(fields[2]) + local status = string.upper(trim(fields[3])) + local reason = string.upper(trim(urlDecodeField(fields[4]))) + local count = tonumber(fields[5] or "0") or 0 + state.connected = true + state.lastError = nil + + local active = getActiveEnchantTradeRequest(botName, token) + if active then + active.status = status + active.reason = reason + active.count = count + + local items = active.items or {} + local deliveredItems = items + local integrityError = active.integrityError + + if not active.began then + integrityError = integrityError or "MISSING_BEGIN" + end + + if status == "OK" and not integrityError and #items ~= count then + integrityError = "COUNT_MISMATCH" + end + + if status == "OK" and not integrityError then + for _, entry in ipairs(items) do + local expectedMaterialCount = tonumber(entry.expectedMaterialCount or 0) or 0 + local receivedMaterialCount = tonumber(entry.receivedMaterialCount or 0) or 0 + if receivedMaterialCount ~= expectedMaterialCount then + integrityError = "MATERIAL_COUNT_MISMATCH" + break + end + + for materialIndex = 1, expectedMaterialCount do + if not entry.materialIndexes or not entry.materialIndexes[materialIndex] then + integrityError = "MATERIAL_INDEX_GAP" + break + end + end + + if integrityError then + break + end + end + end + + if integrityError then + status = "ERR" + reason = "TRY_AGAIN" + active.status = status + active.reason = reason + state.lastError = "ENCHANT_TRADE_" .. integrityError + deliveredItems = {} + elseif status == "OK" then + local key = string.lower(active.botName or botName) + state.enchantTradeLists[key] = items + end + + state.enchantTradeActive = nil + + if MultiBot.OnBridgeEnchantTradeList then + MultiBot.OnBridgeEnchantTradeList(active.botName or botName, deliveredItems, { + token = active.token or token, + status = status, + reason = reason, + skillValue = active.skillValue or 0, + maxSkill = active.maxSkill or 0, + count = count, + }) + end + end + + return true + end + + if opcode == "ENCHANT_TRADE_RESULT" then + local fields = splitFields(payload or "") + if #fields ~= 6 then + state.lastError = "ENCHANT_TRADE_RESULT_BAD_FIELD_COUNT" + return true + end + + local botName = trim(urlDecodeField(fields[1])) + local token = trim(fields[2]) + local spellId = tonumber(fields[3] or "0") or 0 + local status = string.upper(trim(fields[4])) + local reason = string.upper(trim(urlDecodeField(fields[5]))) + local accepted = tonumber(fields[6] or "0") or 0 + state.connected = true + state.lastError = nil + + local command = state.enchantTradeCommands and state.enchantTradeCommands[token] or nil + if command then + if botName == "" or string.lower(botName) ~= tostring(command.botNameKey or "") then + state.lastError = "ENCHANT_TRADE_RESULT_BOT_MISMATCH" + return true + end + + if spellId <= 0 or spellId ~= tonumber(command.spellId or 0) then + state.lastError = "ENCHANT_TRADE_RESULT_SPELL_MISMATCH" + return true + end + + state.enchantTradeCommands[token] = nil + command.accepted = accepted ~= 0 + command.status = status + command.reason = reason + + if MultiBot.OnBridgeEnchantTradeResult then + MultiBot.OnBridgeEnchantTradeResult(command.botName, command.spellId, status, reason, command) + end + end + + return true + end + if opcode == "PROFESSION_RECIPES_BEGIN" then local botName, rest = splitOnce(payload or "", "~") local token, skillId = splitOnce(rest or "", "~") @@ -4888,6 +5348,7 @@ function Comm.OnPlayerEnteringWorld() state.inventoryBulkSellCapable = false state.inventoryOpenCapable = false state.groupRollCapable = false + state.enchantTradeCapable = false state.strategyMutationCommands = {} state.details = {} state.stats = {} diff --git a/Core/MultiBotEvery.lua b/Core/MultiBotEvery.lua index 60fe5d8..335e59f 100644 --- a/Core/MultiBotEvery.lua +++ b/Core/MultiBotEvery.lua @@ -251,6 +251,19 @@ MultiBot.addEvery = function(pFrame, pCombat, pNormal) MultiBot.ShowHideSwitch(combatFrame) end + local enchantButton = pFrame.addButton("Enchant", 484, 0, "trade_engraving", MultiBot.L("lootmaster.profession.enchanting", "Enchanting")) + enchantButton.setDisable() + enchantButton.doHide() + enchantButton.doLeft = function(pButton) + if MultiBot.OpenBotEnchanting then + MultiBot.OpenBotEnchanting(pButton.getName(), pButton) + end + end + if MultiBot.IsBotEnchantingServiceAvailable and MultiBot.IsBotEnchantingServiceAvailable(botName) then + enchantButton.setEnable() + enchantButton.doShow() + end + addBotCombatButton(combatFrame, "CombatFocus", -28, 84, "Ability_Hunter_MasterMarksman", MultiBot.L("tips.every.combatfocus"), "co +focus", "co -focus") addBotCombatButton(combatFrame, "CombatAoe", 0, 84, "Spell_Fire_SelfDestruct", MultiBot.L("tips.every.combataoe"), "co +aoe", "co -aoe") addBotCombatButton(combatFrame, "CombatDpsAssist", -28, 56, "Ability_Hunter_Assassinate2", MultiBot.L("tips.every.combatdpsassist"), "co +dps assist", "co -dps assist") diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 90dbebc..bb0ebfd 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -1108,6 +1108,40 @@ local deDEValues = { ["info.outfits.feedback_reset"] = "Outfit „%s“ zurückgesetzt.", ["info.outfits.bridge_unavailable"] = "Outfit-Bridge nicht verfügbar. Es wurde kein Chat-Fallback gesendet.", ["info.outfits.send_failed"] = "Outfit-Anfrage konnte nicht an die Bridge gesendet werden.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Suche", + ["enchant.trade.apply"] = "Verzaubern", + ["enchant.trade.selected"] = "Ausgewählt", + ["enchant.trade.materials"] = "Reagenzien", + ["enchant.trade.no_reagents"] = "Keine Reagenzien", + ["enchant.trade.spell_fallback"] = "Zauber %d", + ["enchant.trade.item_fallback"] = "Gegenstand %d", + ["enchant.trade.count"] = "Verzauberungen: %d - Fertigkeit: %d/%d", + ["enchant.trade.status.service_unavailable"] = "Der Verzauberungsdienst ist nicht verfügbar.", + ["enchant.trade.status.trade_requested"] = "Handel angefordert. Lege deinen Gegenstand in den nicht gehandelten Slot und klicke dann erneut auf Verzaubern.", + ["enchant.trade.status.send_failed"] = "Die Verzauberungsanfrage konnte nicht gesendet werden.", + ["enchant.trade.status.requested"] = "Verzauberung angefordert...", + ["enchant.trade.status.started"] = "Verzauberung gestartet. Schließe den Handel wie gewohnt ab.", + ["enchant.trade.reason.NO_TRADE"] = "Öffne einen Handel mit diesem Bot.", + ["enchant.trade.reason.WRONG_TRADER"] = "Der geöffnete Handel ist nicht mit diesem Bot.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Lege deinen Gegenstand in den nicht gehandelten Slot.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "Dieser Bot ist kein Verzauberer.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "Dieser Bot kennt diese Verzauberung nicht.", + ["enchant.trade.reason.BAD_ENCHANT"] = "Dieser Zauber ist kein gültiger Verzauberungsdienst.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "In diesem Handel wartet bereits eine Verzauberung.", + ["enchant.trade.reason.BAD_TARGET"] = "Dieser Gegenstand kann diese Verzauberung nicht erhalten.", + ["enchant.trade.reason.IN_COMBAT"] = "Der Bot kann im Kampf nicht verzaubern.", + ["enchant.trade.reason.FORBIDDEN"] = "Du darfst diesen Bot nicht steuern.", + ["enchant.trade.reason.NO_BOT"] = "Der Bot ist nicht verfügbar.", + ["enchant.trade.reason.RATE_LIMIT"] = "Zu viele Verzauberungsanfragen. Versuche es gleich erneut.", + ["enchant.trade.reason.TIMEOUT"] = "Die Verzauberungsanfrage ist abgelaufen.", + ["enchant.trade.reason.DISCONNECTED"] = "Die Bridge wurde getrennt.", + ["enchant.trade.reason.NO_SESSION"] = "Die Sitzung des Bots ist nicht verfügbar.", + ["enchant.trade.reason.LOST_CONTROL"] = "Der Bot kann gerade nicht handeln.", + ["enchant.trade.reason.IN_FLIGHT"] = "Der Bot kann während des Flugs nicht verzaubern.", + ["enchant.trade.reason.CHANNELING"] = "Der Bot kanalisiert bereits einen anderen Zauber.", + ["enchant.trade.reason.TRY_AGAIN"] = "Die Verzauberung konnte nicht gestartet werden. Versuche es erneut.", + ["enchant.trade.reason.UNKNOWN"] = "Verzauberung fehlgeschlagen (%s).", } register("deDE", deDEValues) diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index d752754..40748f0 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -1111,6 +1111,40 @@ local enGBValues = { ["info.outfits.feedback_reset"] = "Outfit \"%s\" reset.", ["info.outfits.bridge_unavailable"] = "Outfit bridge unavailable. No chat fallback was sent.", ["info.outfits.send_failed"] = "Outfit request could not be sent to the bridge.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Search", + ["enchant.trade.apply"] = "Enchant", + ["enchant.trade.selected"] = "Selected", + ["enchant.trade.materials"] = "Reagents", + ["enchant.trade.no_reagents"] = "No reagents", + ["enchant.trade.spell_fallback"] = "Spell %d", + ["enchant.trade.item_fallback"] = "Item %d", + ["enchant.trade.count"] = "Enchantments: %d - Skill: %d/%d", + ["enchant.trade.status.service_unavailable"] = "Enchanting service is not available.", + ["enchant.trade.status.trade_requested"] = "Trade requested. Put your item in the Will not be traded slot, then click Enchant again.", + ["enchant.trade.status.send_failed"] = "Enchanting request could not be sent.", + ["enchant.trade.status.requested"] = "Enchanting requested...", + ["enchant.trade.status.started"] = "Enchanting started. Complete the trade normally.", + ["enchant.trade.reason.NO_TRADE"] = "Open a trade with this bot.", + ["enchant.trade.reason.WRONG_TRADER"] = "The open trade is not with this bot.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Place your item in the Will not be traded slot.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "This bot is not an enchanter.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "This bot does not know that enchantment.", + ["enchant.trade.reason.BAD_ENCHANT"] = "This spell is not a valid enchanting service.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "An enchantment is already pending in this trade.", + ["enchant.trade.reason.BAD_TARGET"] = "That item cannot receive this enchantment.", + ["enchant.trade.reason.IN_COMBAT"] = "The bot cannot enchant while in combat.", + ["enchant.trade.reason.FORBIDDEN"] = "You are not allowed to control this bot.", + ["enchant.trade.reason.NO_BOT"] = "The bot is not available.", + ["enchant.trade.reason.RATE_LIMIT"] = "Too many enchanting requests. Try again shortly.", + ["enchant.trade.reason.TIMEOUT"] = "The enchanting request timed out.", + ["enchant.trade.reason.DISCONNECTED"] = "The bridge disconnected.", + ["enchant.trade.reason.NO_SESSION"] = "The bot session is not available.", + ["enchant.trade.reason.LOST_CONTROL"] = "The bot cannot act right now.", + ["enchant.trade.reason.IN_FLIGHT"] = "The bot cannot enchant while in flight.", + ["enchant.trade.reason.CHANNELING"] = "The bot is already channeling another spell.", + ["enchant.trade.reason.TRY_AGAIN"] = "The enchantment could not start. Try again.", + ["enchant.trade.reason.UNKNOWN"] = "Enchanting failed (%s).", } register("enGB", enGBValues) diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 23a885a..387c5f1 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -1111,6 +1111,40 @@ local enUSValues = { ["info.outfits.feedback_reset"] = "Outfit \"%s\" reset.", ["info.outfits.bridge_unavailable"] = "Outfit bridge unavailable. No chat fallback was sent.", ["info.outfits.send_failed"] = "Outfit request could not be sent to the bridge.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Search", + ["enchant.trade.apply"] = "Enchant", + ["enchant.trade.selected"] = "Selected", + ["enchant.trade.materials"] = "Reagents", + ["enchant.trade.no_reagents"] = "No reagents", + ["enchant.trade.spell_fallback"] = "Spell %d", + ["enchant.trade.item_fallback"] = "Item %d", + ["enchant.trade.count"] = "Enchantments: %d - Skill: %d/%d", + ["enchant.trade.status.service_unavailable"] = "Enchanting service is not available.", + ["enchant.trade.status.trade_requested"] = "Trade requested. Put your item in the Will not be traded slot, then click Enchant again.", + ["enchant.trade.status.send_failed"] = "Enchanting request could not be sent.", + ["enchant.trade.status.requested"] = "Enchanting requested...", + ["enchant.trade.status.started"] = "Enchanting started. Complete the trade normally.", + ["enchant.trade.reason.NO_TRADE"] = "Open a trade with this bot.", + ["enchant.trade.reason.WRONG_TRADER"] = "The open trade is not with this bot.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Place your item in the Will not be traded slot.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "This bot is not an enchanter.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "This bot does not know that enchantment.", + ["enchant.trade.reason.BAD_ENCHANT"] = "This spell is not a valid enchanting service.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "An enchantment is already pending in this trade.", + ["enchant.trade.reason.BAD_TARGET"] = "That item cannot receive this enchantment.", + ["enchant.trade.reason.IN_COMBAT"] = "The bot cannot enchant while in combat.", + ["enchant.trade.reason.FORBIDDEN"] = "You are not allowed to control this bot.", + ["enchant.trade.reason.NO_BOT"] = "The bot is not available.", + ["enchant.trade.reason.RATE_LIMIT"] = "Too many enchanting requests. Try again shortly.", + ["enchant.trade.reason.TIMEOUT"] = "The enchanting request timed out.", + ["enchant.trade.reason.DISCONNECTED"] = "The bridge disconnected.", + ["enchant.trade.reason.NO_SESSION"] = "The bot session is not available.", + ["enchant.trade.reason.LOST_CONTROL"] = "The bot cannot act right now.", + ["enchant.trade.reason.IN_FLIGHT"] = "The bot cannot enchant while in flight.", + ["enchant.trade.reason.CHANNELING"] = "The bot is already channeling another spell.", + ["enchant.trade.reason.TRY_AGAIN"] = "The enchantment could not start. Try again.", + ["enchant.trade.reason.UNKNOWN"] = "Enchanting failed (%s).", } register("enUS", enUSValues, true) diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index 8f431b9..3c72d6d 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -1109,6 +1109,40 @@ local esESValues = { ["info.outfits.feedback_reset"] = "Conjunto «%s» restablecido.", ["info.outfits.bridge_unavailable"] = "Bridge de conjuntos no disponible. No se envió ningún fallback por chat.", ["info.outfits.send_failed"] = "No se pudo enviar la solicitud de conjunto al bridge.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Buscar", + ["enchant.trade.apply"] = "Encantar", + ["enchant.trade.selected"] = "Seleccionado", + ["enchant.trade.materials"] = "Componentes", + ["enchant.trade.no_reagents"] = "Sin componentes", + ["enchant.trade.spell_fallback"] = "Hechizo %d", + ["enchant.trade.item_fallback"] = "Objeto %d", + ["enchant.trade.count"] = "Encantamientos: %d - Habilidad: %d/%d", + ["enchant.trade.status.service_unavailable"] = "El servicio de encantamiento no está disponible.", + ["enchant.trade.status.trade_requested"] = "Intercambio solicitado. Coloca tu objeto en la casilla que no se intercambia y vuelve a hacer clic en Encantar.", + ["enchant.trade.status.send_failed"] = "No se pudo enviar la solicitud de encantamiento.", + ["enchant.trade.status.requested"] = "Encantamiento solicitado...", + ["enchant.trade.status.started"] = "Encantamiento iniciado. Completa el intercambio normalmente.", + ["enchant.trade.reason.NO_TRADE"] = "Abre un intercambio con este bot.", + ["enchant.trade.reason.WRONG_TRADER"] = "El intercambio abierto no es con este bot.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Coloca tu objeto en la casilla que no se intercambia.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "Este bot no es encantador.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "Este bot no conoce ese encantamiento.", + ["enchant.trade.reason.BAD_ENCHANT"] = "Este hechizo no es un servicio de encantamiento válido.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "Ya hay un encantamiento pendiente en este intercambio.", + ["enchant.trade.reason.BAD_TARGET"] = "Este objeto no puede recibir este encantamiento.", + ["enchant.trade.reason.IN_COMBAT"] = "El bot no puede encantar durante el combate.", + ["enchant.trade.reason.FORBIDDEN"] = "No tienes permiso para controlar este bot.", + ["enchant.trade.reason.NO_BOT"] = "El bot no está disponible.", + ["enchant.trade.reason.RATE_LIMIT"] = "Demasiadas solicitudes de encantamiento. Inténtalo de nuevo en un momento.", + ["enchant.trade.reason.TIMEOUT"] = "La solicitud de encantamiento ha caducado.", + ["enchant.trade.reason.DISCONNECTED"] = "El bridge se ha desconectado.", + ["enchant.trade.reason.NO_SESSION"] = "La sesión del bot no está disponible.", + ["enchant.trade.reason.LOST_CONTROL"] = "El bot no puede actuar en este momento.", + ["enchant.trade.reason.IN_FLIGHT"] = "El bot no puede encantar mientras está volando.", + ["enchant.trade.reason.CHANNELING"] = "El bot ya está canalizando otro hechizo.", + ["enchant.trade.reason.TRY_AGAIN"] = "No se pudo iniciar el encantamiento. Inténtalo de nuevo.", + ["enchant.trade.reason.UNKNOWN"] = "El encantamiento ha fallado (%s).", } register("esES", esESValues) diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 5f03c2b..33862bd 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -1108,6 +1108,40 @@ local frFRValues = { ["info.outfits.feedback_reset"] = "Tenue « %s » réinitialisée.", ["info.outfits.bridge_unavailable"] = "Bridge Tenues indisponible. Aucun fallback chat n'a été envoyé.", ["info.outfits.send_failed"] = "La requête de tenue n'a pas pu être envoyée au bridge.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Recherche", + ["enchant.trade.apply"] = "Enchanter", + ["enchant.trade.selected"] = "Sélection", + ["enchant.trade.materials"] = "Composants", + ["enchant.trade.no_reagents"] = "Aucun composant", + ["enchant.trade.spell_fallback"] = "Sort %d", + ["enchant.trade.item_fallback"] = "Objet %d", + ["enchant.trade.count"] = "Enchantements : %d - Compétence : %d/%d", + ["enchant.trade.status.service_unavailable"] = "Service d'enchantement indisponible.", + ["enchant.trade.status.trade_requested"] = "Échange demandé. Placez votre objet dans la case « Ne sera pas échangé », puis cliquez à nouveau sur Enchanter.", + ["enchant.trade.status.send_failed"] = "La demande d'enchantement n'a pas pu être envoyée.", + ["enchant.trade.status.requested"] = "Enchantement demandé...", + ["enchant.trade.status.started"] = "Enchantement lancé. Terminez l'échange normalement.", + ["enchant.trade.reason.NO_TRADE"] = "Ouvrez un échange avec ce bot.", + ["enchant.trade.reason.WRONG_TRADER"] = "L'échange ouvert n'est pas avec ce bot.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Placez votre objet dans la case « Ne sera pas échangé ».", + ["enchant.trade.reason.NOT_ENCHANTER"] = "Ce bot n'est pas enchanteur.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "Ce bot ne connaît pas cet enchantement.", + ["enchant.trade.reason.BAD_ENCHANT"] = "Ce sort n'est pas un service d'enchantement valide.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "Un enchantement est déjà en attente dans cet échange.", + ["enchant.trade.reason.BAD_TARGET"] = "Cet objet ne peut pas recevoir cet enchantement.", + ["enchant.trade.reason.IN_COMBAT"] = "Le bot ne peut pas enchanter pendant le combat.", + ["enchant.trade.reason.FORBIDDEN"] = "Vous n'êtes pas autorisé à contrôler ce bot.", + ["enchant.trade.reason.NO_BOT"] = "Le bot n'est pas disponible.", + ["enchant.trade.reason.RATE_LIMIT"] = "Trop de demandes d'enchantement. Réessayez dans un instant.", + ["enchant.trade.reason.TIMEOUT"] = "La demande d'enchantement a expiré.", + ["enchant.trade.reason.DISCONNECTED"] = "Le bridge est déconnecté.", + ["enchant.trade.reason.NO_SESSION"] = "La session du bot n'est pas disponible.", + ["enchant.trade.reason.LOST_CONTROL"] = "Le bot ne peut pas agir pour le moment.", + ["enchant.trade.reason.IN_FLIGHT"] = "Le bot ne peut pas enchanter en vol.", + ["enchant.trade.reason.CHANNELING"] = "Le bot canalise déjà un autre sort.", + ["enchant.trade.reason.TRY_AGAIN"] = "L'enchantement n'a pas pu démarrer. Réessayez.", + ["enchant.trade.reason.UNKNOWN"] = "Échec de l'enchantement (%s).", } register("frFR", frFRValues) diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 540f147..f69cbb4 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -1100,6 +1100,40 @@ local koKRValues = { ["info.outfits.feedback_reset"] = "장비 세트 \"%s\": 초기화 완료.", ["info.outfits.bridge_unavailable"] = "장비 세트 브리지를 사용할 수 없습니다. 채팅 대체 전송을 사용하지 않았습니다.", ["info.outfits.send_failed"] = "장비 세트 요청을 브리지로 보내지 못했습니다.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "검색", + ["enchant.trade.apply"] = "마법부여", + ["enchant.trade.selected"] = "선택", + ["enchant.trade.materials"] = "재료", + ["enchant.trade.no_reagents"] = "필요한 재료 없음", + ["enchant.trade.spell_fallback"] = "주문 %d", + ["enchant.trade.item_fallback"] = "아이템 %d", + ["enchant.trade.count"] = "마법부여: %d - 숙련도: %d/%d", + ["enchant.trade.status.service_unavailable"] = "마법부여 서비스를 사용할 수 없습니다.", + ["enchant.trade.status.trade_requested"] = "거래를 요청했습니다. 아이템을 거래하지 않는 슬롯에 넣은 뒤 마법부여를 다시 클릭하세요.", + ["enchant.trade.status.send_failed"] = "마법부여 요청을 보내지 못했습니다.", + ["enchant.trade.status.requested"] = "마법부여 요청 중...", + ["enchant.trade.status.started"] = "마법부여를 시작했습니다. 평소처럼 거래를 완료하세요.", + ["enchant.trade.reason.NO_TRADE"] = "이 봇과 거래를 시작하세요.", + ["enchant.trade.reason.WRONG_TRADER"] = "현재 거래 상대가 이 봇이 아닙니다.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "아이템을 거래하지 않는 슬롯에 넣으세요.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "이 봇은 마법부여사가 아닙니다.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "이 봇은 해당 마법부여를 배우지 않았습니다.", + ["enchant.trade.reason.BAD_ENCHANT"] = "이 주문은 유효한 마법부여 서비스가 아닙니다.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "이 거래에는 이미 대기 중인 마법부여가 있습니다.", + ["enchant.trade.reason.BAD_TARGET"] = "이 아이템에는 해당 마법부여를 적용할 수 없습니다.", + ["enchant.trade.reason.IN_COMBAT"] = "봇은 전투 중에 마법부여를 할 수 없습니다.", + ["enchant.trade.reason.FORBIDDEN"] = "이 봇을 제어할 권한이 없습니다.", + ["enchant.trade.reason.NO_BOT"] = "봇을 사용할 수 없습니다.", + ["enchant.trade.reason.RATE_LIMIT"] = "마법부여 요청이 너무 많습니다. 잠시 후 다시 시도하세요.", + ["enchant.trade.reason.TIMEOUT"] = "마법부여 요청 시간이 초과되었습니다.", + ["enchant.trade.reason.DISCONNECTED"] = "Bridge 연결이 끊어졌습니다.", + ["enchant.trade.reason.NO_SESSION"] = "봇 세션을 사용할 수 없습니다.", + ["enchant.trade.reason.LOST_CONTROL"] = "봇이 지금 행동할 수 없습니다.", + ["enchant.trade.reason.IN_FLIGHT"] = "봇은 비행 중에 마법부여를 할 수 없습니다.", + ["enchant.trade.reason.CHANNELING"] = "봇이 이미 다른 주문을 정신 집중 중입니다.", + ["enchant.trade.reason.TRY_AGAIN"] = "마법부여를 시작하지 못했습니다. 다시 시도하세요.", + ["enchant.trade.reason.UNKNOWN"] = "마법부여에 실패했습니다(%s).", } register("koKR", koKRValues) diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index f486292..1f53820 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -1109,6 +1109,40 @@ local ruRUValues = { ["info.outfits.feedback_reset"] = "Комплект «%s» сброшен.", ["info.outfits.bridge_unavailable"] = "Мост комплектов недоступен. Резервная отправка через чат не выполнялась.", ["info.outfits.send_failed"] = "Не удалось отправить запрос комплекта через мост.", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "Поиск", + ["enchant.trade.apply"] = "Наложить чары", + ["enchant.trade.selected"] = "Выбрано", + ["enchant.trade.materials"] = "Реагенты", + ["enchant.trade.no_reagents"] = "Реагенты не требуются", + ["enchant.trade.spell_fallback"] = "Заклинание %d", + ["enchant.trade.item_fallback"] = "Предмет %d", + ["enchant.trade.count"] = "Чары: %d - Навык: %d/%d", + ["enchant.trade.status.service_unavailable"] = "Сервис наложения чар недоступен.", + ["enchant.trade.status.trade_requested"] = "Запрошен обмен. Поместите предмет в ячейку, которая не передаётся, затем снова нажмите «Наложить чары».", + ["enchant.trade.status.send_failed"] = "Не удалось отправить запрос на наложение чар.", + ["enchant.trade.status.requested"] = "Запрошено наложение чар...", + ["enchant.trade.status.started"] = "Наложение чар начато. Завершите обмен обычным способом.", + ["enchant.trade.reason.NO_TRADE"] = "Откройте обмен с этим ботом.", + ["enchant.trade.reason.WRONG_TRADER"] = "Текущий обмен открыт не с этим ботом.", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "Поместите предмет в ячейку, которая не передаётся.", + ["enchant.trade.reason.NOT_ENCHANTER"] = "Этот бот не владеет наложением чар.", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "Этот бот не знает эти чары.", + ["enchant.trade.reason.BAD_ENCHANT"] = "Это заклинание не является допустимой услугой наложения чар.", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "В этом обмене уже ожидает другое наложение чар.", + ["enchant.trade.reason.BAD_TARGET"] = "На этот предмет нельзя наложить эти чары.", + ["enchant.trade.reason.IN_COMBAT"] = "Бот не может накладывать чары в бою.", + ["enchant.trade.reason.FORBIDDEN"] = "У вас нет права управлять этим ботом.", + ["enchant.trade.reason.NO_BOT"] = "Бот недоступен.", + ["enchant.trade.reason.RATE_LIMIT"] = "Слишком много запросов на наложение чар. Повторите попытку чуть позже.", + ["enchant.trade.reason.TIMEOUT"] = "Время ожидания запроса на наложение чар истекло.", + ["enchant.trade.reason.DISCONNECTED"] = "Соединение с bridge разорвано.", + ["enchant.trade.reason.NO_SESSION"] = "Сессия бота недоступна.", + ["enchant.trade.reason.LOST_CONTROL"] = "Бот сейчас не может действовать.", + ["enchant.trade.reason.IN_FLIGHT"] = "Бот не может накладывать чары во время полёта.", + ["enchant.trade.reason.CHANNELING"] = "Бот уже поддерживает другое заклинание.", + ["enchant.trade.reason.TRY_AGAIN"] = "Не удалось начать наложение чар. Повторите попытку.", + ["enchant.trade.reason.UNKNOWN"] = "Ошибка наложения чар (%s).", } register("ruRU", ruRUValues) diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index d1da2fc..ea0b3d6 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -1109,6 +1109,40 @@ local zhCNValues = { ["info.outfits.feedback_reset"] = "配装“%s”已重置。", ["info.outfits.bridge_unavailable"] = "配装桥接不可用。未通过聊天备用通道发送。", ["info.outfits.send_failed"] = "无法将配装请求发送到桥接。", + -- Chatless enchanting service UI + ["enchant.trade.search"] = "搜索", + ["enchant.trade.apply"] = "附魔", + ["enchant.trade.selected"] = "已选择", + ["enchant.trade.materials"] = "材料", + ["enchant.trade.no_reagents"] = "无需材料", + ["enchant.trade.spell_fallback"] = "法术 %d", + ["enchant.trade.item_fallback"] = "物品 %d", + ["enchant.trade.count"] = "附魔:%d - 技能:%d/%d", + ["enchant.trade.status.service_unavailable"] = "附魔服务不可用。", + ["enchant.trade.status.trade_requested"] = "已发起交易。请将物品放入“不交易”栏位,然后再次点击“附魔”。", + ["enchant.trade.status.send_failed"] = "无法发送附魔请求。", + ["enchant.trade.status.requested"] = "已请求附魔...", + ["enchant.trade.status.started"] = "附魔已开始。请正常完成交易。", + ["enchant.trade.reason.NO_TRADE"] = "请先与该机器人进行交易。", + ["enchant.trade.reason.WRONG_TRADER"] = "当前交易对象不是该机器人。", + ["enchant.trade.reason.NO_TRADE_ITEM"] = "请将物品放入“不交易”栏位。", + ["enchant.trade.reason.NOT_ENCHANTER"] = "该机器人不是附魔师。", + ["enchant.trade.reason.UNKNOWN_ENCHANT"] = "该机器人不会此附魔。", + ["enchant.trade.reason.BAD_ENCHANT"] = "该法术不是有效的附魔服务。", + ["enchant.trade.reason.ALREADY_ENCHANTED"] = "该交易中已有一个待处理的附魔。", + ["enchant.trade.reason.BAD_TARGET"] = "该物品无法接受此附魔。", + ["enchant.trade.reason.IN_COMBAT"] = "机器人无法在战斗中附魔。", + ["enchant.trade.reason.FORBIDDEN"] = "你无权控制该机器人。", + ["enchant.trade.reason.NO_BOT"] = "该机器人当前不可用。", + ["enchant.trade.reason.RATE_LIMIT"] = "附魔请求过多,请稍后再试。", + ["enchant.trade.reason.TIMEOUT"] = "附魔请求已超时。", + ["enchant.trade.reason.DISCONNECTED"] = "Bridge 已断开连接。", + ["enchant.trade.reason.NO_SESSION"] = "机器人会话不可用。", + ["enchant.trade.reason.LOST_CONTROL"] = "机器人当前无法行动。", + ["enchant.trade.reason.IN_FLIGHT"] = "机器人无法在飞行中附魔。", + ["enchant.trade.reason.CHANNELING"] = "机器人正在引导另一个法术。", + ["enchant.trade.reason.TRY_AGAIN"] = "无法开始附魔,请重试。", + ["enchant.trade.reason.UNKNOWN"] = "附魔失败(%s)。", } register("zhCN", zhCNValues) diff --git a/MultiBot.toc b/MultiBot.toc index 9f733c1..48badf8 100644 --- a/MultiBot.toc +++ b/MultiBot.toc @@ -65,6 +65,7 @@ UI\MultiBotStats.lua UI\MultiBotSpell.lua UI\MultiBotSpellBookFrame.lua UI\MultiBotCharacterInfoFrame.lua +UI\MultiBotEnchantingUI.lua UI\MultiBotRewardFrame.lua UI\MultiBotOutfitUI.lua UI\MultiBotTrainerUI.lua diff --git a/README.md b/README.md index 0aa8863..ebf30fa 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,7 @@ GET~BOT_SKILLS GET~BOT_REPUTATIONS GET~BOT_EMBLEMS GET~PROFESSION_RECIPES +GET~ENCHANT_TRADE GET~GLYPHS GET~OUTFITS GET~QUESTS @@ -111,6 +112,8 @@ RUN~POSITION RUN~LOOT RUN~STRATEGY RUN~FORMATION +RUN~GROUP_ROLL +RUN~ENCHANT_TRADE ``` The Formation family uses the following complete party/raid-wide contracts: @@ -129,17 +132,25 @@ FORMATIONS_END~~ ## Current state and strategy capabilities -The addon and bridge now negotiate two dedicated capabilities for authoritative bot-state synchronization and strategy mutations: +The addon and bridge negotiate dedicated capabilities so newer write/read paths are used only when both sides support them: ```text STATE_FRAMING_V1 STRATEGY_MUTATION_V1 +OUTFIT_V1 +INVENTORY_V1 +INVENTORY_BULK_SELL_V1 +INVENTORY_OPEN_V1 +GROUP_ROLL_V1 +ENCHANT_TRADE_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 native inventory read/refresh. `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. + 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. Manual playerbot commands are still intentionally preserved for diagnostics and gameplay actions. @@ -168,6 +179,8 @@ For targeted runtime diagnostics, the addon exposes: If the bot name is omitted, the current target is used. The command sends a single `GET~WEAPON_ENCHANT` request and displays the structured `WEAPON_ENCHANT` response with main-hand/off-hand item entries, temporary enchant IDs and remaining durations. This path is diagnostic only: it is on-demand, server-authorized and rate-limited, and is not used for polling or normal selector state synchronization. +The endpoint and safe Firestone/Spellstone switching code are present, but the project-level final revalidation of the real `TEMP_ENCHANTMENT_SLOT` behavior is intentionally deferred until the end of the normal roadmap. + --- # Features @@ -215,7 +228,19 @@ If the bot name is omitted, the current target is used. The command sends a sing Inventory - Bridge-first with icons and item tooltips + Bridge-first native read/refresh through INVENTORY_V1, with icons and item tooltips + + + 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 + + + Open items + Bridge-first and validatedINVENTORY_OPEN_V1 / OPEN_ITEMS with structured result handling and no silent chat fallback in normal bridge-first use + + + Group Roll + Bridge-first and runtime validatedGROUP_ROLL_V1 supports normal 0–100 rolls and Shift+click item rolls with tokenized pending state, duplicate-send protection and structured ACK handling Spellbook @@ -233,6 +258,10 @@ If the bot name is omitted, the current target is used. The command sends a sing Profession recipe frame Bridge-first recipe listing and recipe crafting opened from Character Info profession and secondary skill rows + + 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 + Glyphs Bridge-first with glyph icons and tooltips @@ -525,11 +554,15 @@ Implemented bridge-first / chatless areas: - Stats refresh. - PvP stats refresh. - Talent spec list refresh. -- Inventory refresh with icons and item tooltips. +- Inventory read/refresh through `INVENTORY_V1`, with icons and item tooltips. +- 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. +- `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`. - 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. - Glyph refresh with icons and glyph tooltips. - Outfits refresh and actions through the bridge. - Outfit equip/replace without detailed `Equipping [item] ...` chat spam. @@ -554,12 +587,22 @@ Validated development milestones on the current line: - PR #49 — bridge synchronization, strategy controls, persistent/offline favorites and STATE stabilization. - PR #50 — explicit strategy-command rejection diagnostics. - PR #51 — mechanical deduplication of shared roster workflow helpers. +- PR #53 — prevent silent chat fallback for strategy mutations. +- PR #54 — outfit actions migrated to the negotiated bridge capability. +- PR #55 — strict bridge routing for inventory read/refresh. +- 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`. - Final static STATE/strategy audit on 2026-08-07: 57 checks, 0 failures; final manual runtime matrix remains pending. -- Warlock selector batch validated on 2026-08-08: Stones, Soulstones, Pets and Curses migrated to bridge strategy mutations; authoritative bridge state handling validated; Firestone/Spellstone temporary-enchant switching validated bidirectionally with the companion bridge. +- 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. Known migration remaining: -- Remaining direct `SendChatMessage` occurrences outside the validated Warlock selector batch still need to be classified as manual command, diagnostic fallback, information message, UI mechanism to migrate, or dead code. +- 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. - 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: @@ -573,16 +616,22 @@ Kept intentionally: # Remaining Work -The Outfits, RTI, Pull Control, Combat Strategy, Disperse, Loot Rules, Quest, Game Object, Character Info, Profession Recipe, Reputations, Currencies and advanced inventory bank/vendor migrations are implemented. The Loot Master UI is also implemented as an optional client-side master-loot helper. The next step is final stabilization and cleanup. +The current line includes bridge-first inventory refresh, 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. + +Next normal roadmap work: + +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. + +Explicitly deferred until the normal roadmap is complete: -Planned follow-up work: +- 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`. +- Other small items that were explicitly deferred during previous validated batches. -- Regression test login, `/reload`, large raid groups, Units, EveryBars, Stats, PvP Stats, Inventory, Bot Bank, Guild Bank, Vendor Buy, Spellbook, Character Info, Reputations, Currencies, Profession Recipes, Talents, Glyphs, Outfits, Quests, Game Objects, RTI, Pull Control, Combat Strategies, Disperse, Loot Rules and Loot Master. -- Verify that `MultiBot.allowLegacyChatFallback = false` prevents automatic legacy refresh spam on all migrated UI paths. -- Keep manual diagnostic commands documented and functional. -- Remove obsolete debug prints. -- Remove dead legacy parser paths once bridge-first behavior is fully stable. -- Update screenshots and user documentation after wider testing. +Ongoing finalization work remains unchanged: regression testing, classification of residual `SendChatMessage` paths, removal of dead legacy parsers only after proof of non-regression, and documentation/screenshot cleanup after wider testing. --- diff --git a/UI/MultiBotCharacterInfoFrame.lua b/UI/MultiBotCharacterInfoFrame.lua index 723cc5e..e37189d 100644 --- a/UI/MultiBotCharacterInfoFrame.lua +++ b/UI/MultiBotCharacterInfoFrame.lua @@ -55,6 +55,7 @@ local RECIPE_CRAFT_BUTTON_WIDTH = 62 local RECIPE_TEXT_WIDTH = 176 local RECIPE_REFRESH_DELAY = 3.0 local COOKING_SKILL_ID = 185 +local ENCHANTING_SKILL_ID = 333 local REPUTATION_BAR_COLORS = { [0] = { 0.80, 0.12, 0.12 }, -- Hated @@ -850,6 +851,15 @@ local function ensureCharacterFrame() if not self.skill then return end if self.skill.category ~= "profession" and self.skill.category ~= "secondary" then return end + + if tonumber(self.skill.skillId or 0) == ENCHANTING_SKILL_ID + and MultiBot.IsBotEnchantingServiceAvailable + and MultiBot.IsBotEnchantingServiceAvailable(frame.botName) + and MultiBot.OpenBotEnchanting then + MultiBot.OpenBotEnchanting(frame.botName, nil) + return + end + if MultiBot.Comm and MultiBot.Comm.RequestProfessionRecipes then ensureRecipeFrame() setWindowTitle(MultiBot.professionRecipeFrame, getSkillDisplayName(self.skill) .. " - " .. (frame.botName or "")) diff --git a/UI/MultiBotEnchantingUI.lua b/UI/MultiBotEnchantingUI.lua new file mode 100644 index 0000000..9df5f28 --- /dev/null +++ b/UI/MultiBotEnchantingUI.lua @@ -0,0 +1,689 @@ +if not MultiBot then + return +end + +local AceGUI = LibStub and LibStub("AceGUI-3.0", true) + +local ENCHANT_WINDOW_WIDTH = 440 +local ENCHANT_WINDOW_HEIGHT = 480 +local ENCHANT_PAGE_SIZE = 9 +local ENCHANT_ROW_HEIGHT = 34 +local ENCHANT_REFRESH_DELAY = 0.60 + +local EnchantUI = MultiBot.EnchantingUI or {} +MultiBot.EnchantingUI = EnchantUI + +local function L(key, fallback) + if MultiBot and type(MultiBot.L) == "function" then + return MultiBot.L(key, fallback) + end + return fallback or key +end + +local function safeDelay(delaySeconds, callback) + if type(callback) ~= "function" then + return + end + if MultiBot and type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(delaySeconds or 0, callback) + return + end + callback() +end + +local function sameBotName(left, right) + return string.lower(tostring(left or "")) == string.lower(tostring(right or "")) +end + +local function setButtonEnabled(button, enabled) + if not button then + return + end + if enabled then + if button.Enable then button:Enable() end + if button.SetAlpha then button:SetAlpha(1) end + else + if button.Disable then button:Disable() end + if button.SetAlpha then button:SetAlpha(0.45) end + end +end + +local function addSimpleBackdrop(frame, bgAlpha) + if not frame or not frame.SetBackdrop then + return + end + frame:SetBackdrop({ + bgFile = "Interface\\Buttons\\WHITE8x8", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, + tileSize = 16, + edgeSize = 14, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + if frame.SetBackdropColor then + frame:SetBackdropColor(0.06, 0.06, 0.08, bgAlpha or 0.94) + end + if frame.SetBackdropBorderColor then + frame:SetBackdropBorderColor(0.35, 0.35, 0.35, 0.95) + end +end + +local function getWindowTitle(botName) + local title = L("lootmaster.profession.enchanting", "Enchanting") + if type(botName) == "string" and botName ~= "" then + return title .. " - " .. botName + end + return title +end + +local function getSpellData(spellId) + local resolvedSpellId = tonumber(spellId or 0) or 0 + local name, _, icon = GetSpellInfo(resolvedSpellId) + return name or string.format(L("enchant.trade.spell_fallback", "Spell %d"), resolvedSpellId), icon +end + +local function materialLabel(material) + local itemId = tonumber(material and material.itemId or 0) or 0 + local name = nil + if itemId > 0 and GetItemInfo then + name = GetItemInfo(itemId) + end + if not name then + name = string.format(L("enchant.trade.item_fallback", "Item %d"), itemId) + end + return name +end + +local function materialSummary(materials) + local values = {} + for _, material in ipairs(materials or {}) do + values[#values + 1] = tostring(tonumber(material.available or 0) or 0) .. "/" .. tostring(tonumber(material.required or 0) or 0) + if #values >= 3 then + break + end + end + if #values == 0 then + return L("enchant.trade.no_reagents", "No reagents") + end + return table.concat(values, " ") +end + +function EnchantUI:HasMaterialItem(itemId) + itemId = tonumber(itemId or 0) or 0 + if itemId <= 0 then + return false + end + + for _, entry in ipairs(self.entries or {}) do + for _, material in ipairs(entry.materials or {}) do + if tonumber(material.itemId or 0) == itemId then + return true + end + end + end + + return false +end + +local function getReasonText(reason) + reason = string.upper(tostring(reason or "")) + if reason == "" or reason == "OK" then + return "" + elseif reason == "NO_TRADE" then + return L("enchant.trade.reason.NO_TRADE", "Open a trade with this bot.") + elseif reason == "WRONG_TRADER" then + return L("enchant.trade.reason.WRONG_TRADER", "The open trade is not with this bot.") + elseif reason == "NO_TRADE_ITEM" then + return L("enchant.trade.reason.NO_TRADE_ITEM", "Place your item in the Will not be traded slot.") + elseif reason == "NOT_ENCHANTER" then + return L("enchant.trade.reason.NOT_ENCHANTER", "This bot is not an enchanter.") + elseif reason == "UNKNOWN_ENCHANT" then + return L("enchant.trade.reason.UNKNOWN_ENCHANT", "This bot does not know that enchantment.") + elseif reason == "BAD_ENCHANT" then + return L("enchant.trade.reason.BAD_ENCHANT", "This spell is not a valid enchanting service.") + elseif reason == "ALREADY_ENCHANTED" then + return L("enchant.trade.reason.ALREADY_ENCHANTED", "An enchantment is already pending in this trade.") + elseif reason == "BAD_TARGET" or reason == "NOT_TRADEABLE" then + return L("enchant.trade.reason.BAD_TARGET", "That item cannot receive this enchantment.") + elseif reason == "IN_COMBAT" then + return L("enchant.trade.reason.IN_COMBAT", "The bot cannot enchant while in combat.") + elseif reason == "FORBIDDEN" then + return L("enchant.trade.reason.FORBIDDEN", "You are not allowed to control this bot.") + elseif reason == "NO_BOT" then + return L("enchant.trade.reason.NO_BOT", "The bot is not available.") + elseif reason == "RATE_LIMIT" then + return L("enchant.trade.reason.RATE_LIMIT", "Too many enchanting requests. Try again shortly.") + elseif reason == "TIMEOUT" then + return L("enchant.trade.reason.TIMEOUT", "The enchanting request timed out.") + elseif reason == "DISCONNECTED" then + return L("enchant.trade.reason.DISCONNECTED", "The bridge disconnected.") + elseif reason == "NO_SESSION" then + return L("enchant.trade.reason.NO_SESSION", "The bot session is not available.") + elseif reason == "LOST_CONTROL" then + return L("enchant.trade.reason.LOST_CONTROL", "The bot cannot act right now.") + elseif reason == "IN_FLIGHT" then + return L("enchant.trade.reason.IN_FLIGHT", "The bot cannot enchant while in flight.") + elseif reason == "CHANNELING" then + return L("enchant.trade.reason.CHANNELING", "The bot is already channeling another spell.") + elseif reason == "TRY_AGAIN" then + return L("enchant.trade.reason.TRY_AGAIN", "The enchantment could not start. Try again.") + elseif reason == "NO_MATERIALS" then + return L("profession.recipes.craft.reason.NO_MATERIALS", "Missing reagents.") + elseif reason == "MISSING_TOOLS" then + return L("profession.recipes.craft.reason.MISSING_TOOLS", "A required enchanting tool is missing.") + elseif reason == "MOVING" then + return L("profession.recipes.craft.reason.MOVING", "The bot is moving.") + elseif reason == "NOT_STANDING" then + return L("profession.recipes.craft.reason.NOT_STANDING", "The bot must be standing.") + elseif reason == "NOT_READY" then + return L("profession.recipes.craft.reason.NOT_READY", "The spell is not ready.") + elseif reason == "OUT_OF_RANGE" then + return L("profession.recipes.craft.reason.OUT_OF_RANGE", "The target is out of range.") + end + return string.format(L("enchant.trade.reason.UNKNOWN", "Enchanting failed (%s)."), reason) +end +local function showEnchantTooltip(owner, entry) + if not owner or not entry or not GameTooltip then + return + end + EnchantUI.tooltipOwner = owner + EnchantUI.tooltipEntry = entry + GameTooltip:SetOwner(owner, "ANCHOR_RIGHT") + GameTooltip:SetHyperlink("spell:" .. tostring(entry.spellId or 0)) + if entry.materials and #entry.materials > 0 then + GameTooltip:AddLine(" ") + GameTooltip:AddLine(L("enchant.trade.materials", "Reagents"), 1, 0.82, 0) + for _, material in ipairs(entry.materials) do + local required = tonumber(material.required or 0) or 0 + local available = tonumber(material.available or 0) or 0 + local enough = available >= required + GameTooltip:AddDoubleLine( + materialLabel(material), + tostring(available) .. "/" .. tostring(required), + 1, 1, 1, + enough and 0.2 or 1, enough and 1 or 0.25, enough and 0.2 or 0.25 + ) + end + end + if tonumber(entry.hasTools or 1) == 0 then + GameTooltip:AddLine(L("profession.recipes.craft.reason.MISSING_TOOLS", "Required enchanting tool missing."), 1, 0.25, 0.25, true) + end + GameTooltip:Show() +end + +local function hideEnchantTooltip() + EnchantUI.tooltipOwner = nil + EnchantUI.tooltipEntry = nil + if GameTooltip then + GameTooltip:Hide() + end +end + +function EnchantUI:GetFilteredEntries() + local filtered = {} + local search = string.lower(tostring(self.searchText or "")) + for _, entry in ipairs(self.entries or {}) do + local name = getSpellData(entry.spellId) + if search == "" or string.find(string.lower(name), search, 1, true) then + filtered[#filtered + 1] = entry + end + end + return filtered +end + +function EnchantUI:GetMaxPage() + local count = #self:GetFilteredEntries() + return math.max(1, math.ceil(count / ENCHANT_PAGE_SIZE)) +end + +function EnchantUI:UpdateApplyButton() + local frame = self.frame + if not frame then + return + end + local selected = self.selectedEntry + local enabled = selected ~= nil and tonumber(selected.available or 0) ~= 0 and self.pendingToken == nil + setButtonEnabled(frame.apply, enabled) +end + +function EnchantUI:SelectEntry(entry) + self.selectedEntry = entry + self.selectedSpellId = entry and tonumber(entry.spellId or 0) or nil + self:Render() +end + +function EnchantUI:Render() + local frame = self:EnsureWindow() + local entries = self:GetFilteredEntries() + local maxPage = math.max(1, math.ceil(#entries / ENCHANT_PAGE_SIZE)) + self.page = math.max(1, math.min(tonumber(self.page or 1) or 1, maxPage)) + + local startIndex = ((self.page - 1) * ENCHANT_PAGE_SIZE) + 1 + for rowIndex = 1, ENCHANT_PAGE_SIZE do + local row = frame.rows[rowIndex] + local entry = entries[startIndex + rowIndex - 1] + row.entry = entry + row.icon.entry = entry + row.hit.entry = entry + if entry then + local name, icon = getSpellData(entry.spellId) + row.icon.texture:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark") + row.name:SetText(name) + row.materials:SetText(materialSummary(entry.materials)) + if self.selectedSpellId ~= nil and tonumber(entry.spellId or 0) == tonumber(self.selectedSpellId or -1) then + row.selection:Show() + else + row.selection:Hide() + end + if tonumber(entry.available or 0) ~= 0 then + row.name:SetTextColor(1, 1, 1) + row.materials:SetTextColor(0.8, 0.8, 0.8) + else + row.name:SetTextColor(0.55, 0.55, 0.55) + row.materials:SetTextColor(1, 0.35, 0.35) + end + row:Show() + else + row:Hide() + end + end + + frame.pageText:SetText(tostring(self.page) .. "/" .. tostring(maxPage)) + setButtonEnabled(frame.prev, self.page > 1) + setButtonEnabled(frame.next, self.page < maxPage) + + if self.selectedEntry then + local selectedName = getSpellData(self.selectedEntry.spellId) + frame.selected:SetText(L("enchant.trade.selected", "Selected") .. ": " .. selectedName) + else + frame.selected:SetText(L("enchant.trade.selected", "Selected") .. ": -") + end + self:UpdateApplyButton() +end + +function EnchantUI:EnsureWindow() + if self.frame then + return self.frame + end + + local frame + local content + if AceGUI then + local window = AceGUI:Create("Window") + window:SetTitle(getWindowTitle(self.botName)) + window:SetWidth(ENCHANT_WINDOW_WIDTH) + window:SetHeight(ENCHANT_WINDOW_HEIGHT) + window:EnableResize(false) + window:SetLayout("Fill") + frame = window.frame + content = window.content + frame._mbAceWindow = window + local strataLevel = MultiBot.GetGlobalStrataLevel and MultiBot.GetGlobalStrataLevel() + if strataLevel then frame:SetFrameStrata(strataLevel) end + if MultiBot.SetAceWindowCloseToHide then MultiBot.SetAceWindowCloseToHide(window) end + if MultiBot.RegisterAceWindowEscapeClose then MultiBot.RegisterAceWindowEscapeClose(window, "BotEnchanting") end + if MultiBot.BindAceWindowPosition then MultiBot.BindAceWindowPosition(window, "bot_enchanting_popup") end + else + frame = CreateFrame("Frame", "MultiBotEnchantingFrame", UIParent) + frame:SetSize(ENCHANT_WINDOW_WIDTH, ENCHANT_WINDOW_HEIGHT) + frame:SetPoint("CENTER", UIParent, "CENTER", -110, 20) + frame:SetFrameStrata("DIALOG") + frame:EnableMouse(true) + frame:SetMovable(true) + frame:RegisterForDrag("LeftButton") + frame:SetScript("OnDragStart", frame.StartMoving) + frame:SetScript("OnDragStop", frame.StopMovingOrSizing) + if UISpecialFrames then + table.insert(UISpecialFrames, "MultiBotEnchantingFrame") + end + addSimpleBackdrop(frame, 0.96) + frame.close = CreateFrame("Button", nil, frame, "UIPanelCloseButton") + frame.close:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -4, -4) + frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal") + frame.title:SetPoint("TOP", 0, -7) + content = CreateFrame("Frame", nil, frame) + content:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -34) + content:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 12) + end + + frame:Hide() + frame.content = content or frame + addSimpleBackdrop(frame.content, 0.90) + + frame.itemInfoEventFrame = CreateFrame("Frame") + pcall(frame.itemInfoEventFrame.RegisterEvent, frame.itemInfoEventFrame, "GET_ITEM_INFO_RECEIVED") + frame.itemInfoEventFrame:SetScript("OnEvent", function(_, _, itemId) + local receivedItemId = tonumber(itemId or arg1 or 0) or 0 + if frame:IsShown() and EnchantUI:HasMaterialItem(receivedItemId) then + EnchantUI:Render() + if EnchantUI.tooltipOwner and EnchantUI.tooltipEntry then + showEnchantTooltip(EnchantUI.tooltipOwner, EnchantUI.tooltipEntry) + end + end + end) + + frame.status = frame.content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + frame.status:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 10, -10) + frame.status:SetPoint("TOPRIGHT", frame.content, "TOPRIGHT", -10, -10) + frame.status:SetJustifyH("LEFT") + frame.status:SetText("") + + frame.searchPanel = CreateFrame("Frame", nil, frame.content) + frame.searchPanel:SetHeight(26) + frame.searchPanel:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 10, -31) + frame.searchPanel:SetPoint("TOPRIGHT", frame.content, "TOPRIGHT", -10, -31) + addSimpleBackdrop(frame.searchPanel, 0.78) + + frame.searchLabel = frame.searchPanel:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + frame.searchLabel:SetPoint("LEFT", frame.searchPanel, "LEFT", 8, 0) + frame.searchLabel:SetText(L("enchant.trade.search", "Search")) + + frame.search = CreateFrame("EditBox", nil, frame.searchPanel) + frame.search:SetHeight(20) + frame.search:SetPoint("LEFT", frame.searchLabel, "RIGHT", 8, 0) + frame.search:SetPoint("RIGHT", frame.searchPanel, "RIGHT", -6, 0) + frame.search:SetAutoFocus(false) + frame.search:SetFontObject(GameFontHighlightSmall) + frame.search:SetTextInsets(4, 4, 0, 0) + frame.search:SetScript("OnEscapePressed", function(editBox) + editBox:ClearFocus() + end) + frame.search:SetScript("OnTextChanged", function(editBox) + EnchantUI.searchText = editBox:GetText() or "" + EnchantUI.page = 1 + EnchantUI:Render() + end) + + frame.rows = {} + for index = 1, ENCHANT_PAGE_SIZE do + local row = CreateFrame("Frame", nil, frame.content) + row:SetPoint("TOPLEFT", frame.content, "TOPLEFT", 10, -64 - ((index - 1) * ENCHANT_ROW_HEIGHT)) + row:SetPoint("TOPRIGHT", frame.content, "TOPRIGHT", -10, -64 - ((index - 1) * ENCHANT_ROW_HEIGHT)) + row:SetHeight(ENCHANT_ROW_HEIGHT) + + row.selection = row:CreateTexture(nil, "BACKGROUND") + row.selection:SetAllPoints(row) + row.selection:SetTexture("Interface\\Buttons\\WHITE8x8") + row.selection:SetVertexColor(0.18, 0.34, 0.55, 0.32) + row.selection:Hide() + + row.icon = CreateFrame("Button", nil, row) + row.icon:SetSize(26, 26) + row.icon:SetPoint("LEFT", row, "LEFT", 2, 0) + row.icon.texture = row.icon:CreateTexture(nil, "ARTWORK") + row.icon.texture:SetAllPoints(row.icon) + + row.name = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight") + row.name:SetPoint("LEFT", row.icon, "RIGHT", 8, 7) + row.name:SetWidth(260) + row.name:SetJustifyH("LEFT") + + row.materials = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + row.materials:SetPoint("LEFT", row.icon, "RIGHT", 8, -8) + row.materials:SetWidth(260) + row.materials:SetJustifyH("LEFT") + + row.hit = CreateFrame("Button", nil, row) + row.hit:SetAllPoints(row) + row.hit:RegisterForClicks("LeftButtonUp") + row.hit:SetScript("OnClick", function(button) + if button.entry then EnchantUI:SelectEntry(button.entry) end + end) + row.hit:SetScript("OnEnter", function(button) + if button.entry then showEnchantTooltip(button, button.entry) end + end) + row.hit:SetScript("OnLeave", function() + hideEnchantTooltip() + end) + row.icon:SetScript("OnEnter", function(button) + if button.entry then showEnchantTooltip(button, button.entry) end + end) + row.icon:SetScript("OnLeave", function() + hideEnchantTooltip() + end) + row.icon:SetScript("OnClick", function(button) + if button.entry then EnchantUI:SelectEntry(button.entry) end + end) + + frame.rows[index] = row + end + + frame.prev = CreateFrame("Button", nil, frame.content, "UIPanelButtonTemplate") + frame.prev:SetSize(32, 22) + frame.prev:SetPoint("BOTTOMLEFT", frame.content, "BOTTOMLEFT", 10, 46) + frame.prev:SetText("<") + frame.prev:SetScript("OnClick", function() + EnchantUI.page = math.max(1, (EnchantUI.page or 1) - 1) + EnchantUI:Render() + end) + + frame.pageText = frame.content:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + frame.pageText:SetPoint("LEFT", frame.prev, "RIGHT", 8, 0) + frame.pageText:SetWidth(55) + frame.pageText:SetJustifyH("CENTER") + + frame.next = CreateFrame("Button", nil, frame.content, "UIPanelButtonTemplate") + frame.next:SetSize(32, 22) + frame.next:SetPoint("LEFT", frame.pageText, "RIGHT", 8, 0) + frame.next:SetText(">") + frame.next:SetScript("OnClick", function() + EnchantUI.page = math.min(EnchantUI:GetMaxPage(), (EnchantUI.page or 1) + 1) + EnchantUI:Render() + end) + + frame.refresh = CreateFrame("Button", nil, frame.content, "UIPanelButtonTemplate") + frame.refresh:SetSize(86, 22) + frame.refresh:SetPoint("LEFT", frame.next, "RIGHT", 12, 0) + frame.refresh:SetText(L("lootmaster.refresh", "Refresh")) + frame.refresh:SetScript("OnClick", function() + EnchantUI:RequestList() + end) + + frame.apply = CreateFrame("Button", nil, frame.content, "UIPanelButtonTemplate") + frame.apply:SetSize(130, 24) + frame.apply:SetPoint("BOTTOMRIGHT", frame.content, "BOTTOMRIGHT", -10, 12) + frame.apply:SetText(L("enchant.trade.apply", "Enchant")) + frame.apply:SetScript("OnClick", function() + EnchantUI:ApplySelected() + end) + + frame.selected = frame.content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall") + frame.selected:SetPoint("BOTTOMLEFT", frame.content, "BOTTOMLEFT", 10, 18) + frame.selected:SetPoint("RIGHT", frame.apply, "LEFT", -10, 0) + frame.selected:SetJustifyH("LEFT") + + self.frame = frame + return frame +end + +function EnchantUI:RequestList() + if not self.botName or self.botName == "" or not MultiBot.Comm or not MultiBot.Comm.RequestEnchantTrade then + return false + end + if self.listToken then + return false + end + + local frame = self:EnsureWindow() + frame.status:SetText(L("profession.recipes.loading", "Loading...")) + local token = MultiBot.Comm.RequestEnchantTrade(self.botName) + if not token then + setButtonEnabled(frame.refresh, true) + frame.status:SetText(L("enchant.trade.status.service_unavailable", "Enchanting service is not available.")) + return false + end + + self.listToken = token + setButtonEnabled(frame.refresh, false) + return true +end + +function EnchantUI:ApplySelected() + local frame = self:EnsureWindow() + local entry = self.selectedEntry + if not entry or tonumber(entry.spellId or 0) <= 0 then + return false + end + if tonumber(entry.available or 0) == 0 then + frame.status:SetText(getReasonText(tonumber(entry.hasTools or 1) == 0 and "MISSING_TOOLS" or "NO_MATERIALS")) + return false + end + if not MultiBot.Comm or not MultiBot.Comm.RunEnchantTrade then + return false + end + + if TradeFrame and TradeFrame.IsShown and not TradeFrame:IsShown() and InitiateTrade then + InitiateTrade(self.botName) + frame.status:SetText(L("enchant.trade.status.trade_requested", "Trade requested. Put your item in the Will not be traded slot, then click Enchant again.")) + return false + end + + local token = MultiBot.Comm.RunEnchantTrade(self.botName, entry.spellId) + if not token then + frame.status:SetText(L("enchant.trade.status.send_failed", "Enchanting request could not be sent.")) + return false + end + self.pendingToken = token + frame.status:SetText(L("enchant.trade.status.requested", "Enchanting requested...")) + self:UpdateApplyButton() + return true +end + +function EnchantUI:Open(botName) + botName = tostring(botName or "") + if botName == "" then + return false + end + if not MultiBot.IsBotEnchantingServiceAvailable or not MultiBot.IsBotEnchantingServiceAvailable(botName) then + return false + end + + self.botName = botName + self.entries = {} + self.selectedEntry = nil + self.selectedSpellId = nil + self.pendingToken = nil + self.listToken = nil + self.tooltipOwner = nil + self.tooltipEntry = nil + self.page = 1 + self.searchText = "" + + local frame = self:EnsureWindow() + if frame._mbAceWindow and frame._mbAceWindow.SetTitle then + frame._mbAceWindow:SetTitle(getWindowTitle(botName)) + elseif frame.title then + frame.title:SetText(getWindowTitle(botName)) + end + if frame.search then frame.search:SetText("") end + frame:Show() + self:Render() + setButtonEnabled(frame.refresh, true) + + if TradeFrame and TradeFrame.IsShown and not TradeFrame:IsShown() and InitiateTrade then + InitiateTrade(botName) + end + self:RequestList() + return true +end + +function MultiBot.IsBotEnchantingServiceAvailable(botName) + return MultiBot.Comm + and MultiBot.Comm.IsEnchantTradeCapable + and MultiBot.Comm.IsEnchantTradeCapable() + and MultiBot.Comm.IsBotEnchanter + and MultiBot.Comm.IsBotEnchanter(botName) + or false +end + +function MultiBot.RefreshEnchantingEveryButton(botName) + local main = MultiBot.frames and MultiBot.frames["MultiBar"] or nil + local units = main and main.frames and main.frames["Units"] or nil + if not units or not units.frames then + return + end + + for name, unitFrame in pairs(units.frames) do + if type(name) == "string" + and name ~= "" + and unitFrame + and (not botName or sameBotName(name, botName)) then + local button = unitFrame.getButton and unitFrame.getButton("Enchant") or nil + if button then + if MultiBot.IsBotEnchantingServiceAvailable(name) then + button.setEnable() + button.doShow() + else + button.doHide() + button.setDisable() + end + end + end + end +end + +function MultiBot.RefreshEnchantingEveryButtons() + MultiBot.RefreshEnchantingEveryButton(nil) +end + +function MultiBot.ApplyBridgeBotProfession(botName, _professions) + MultiBot.RefreshEnchantingEveryButton(botName) +end + +function MultiBot.OpenBotEnchanting(botName, _sourceButton) + return EnchantUI:Open(botName) +end + +function MultiBot.OnBridgeEnchantTradeList(botName, entries, meta) + local token = meta and tostring(meta.token or "") or "" + if not EnchantUI.botName or not sameBotName(EnchantUI.botName, botName) then + return + end + if not EnchantUI.listToken or token == "" or tostring(EnchantUI.listToken) ~= token then + return + end + + EnchantUI.listToken = nil + + local frame = EnchantUI:EnsureWindow() + setButtonEnabled(frame.refresh, true) + local status = meta and tostring(meta.status or "") or "" + local reason = meta and tostring(meta.reason or "") or "" + if status ~= "OK" then + frame.status:SetText(getReasonText(reason ~= "" and reason or status)) + else + EnchantUI.entries = type(entries) == "table" and entries or {} + EnchantUI.page = 1 + EnchantUI.selectedEntry = nil + EnchantUI.selectedSpellId = nil + + local skillValue = tonumber(meta and meta.skillValue or 0) or 0 + local maxSkill = tonumber(meta and meta.maxSkill or 0) or 0 + frame.status:SetText(string.format(L("enchant.trade.count", "Enchantments: %d - Skill: %d/%d"), #EnchantUI.entries, skillValue, maxSkill)) + end + EnchantUI:Render() +end + +function MultiBot.OnBridgeEnchantTradeResult(botName, _spellId, status, reason, command) + local commandToken = command and tostring(command.token or "") or "" + if not EnchantUI.pendingToken or commandToken == "" or tostring(EnchantUI.pendingToken) ~= commandToken then + return + end + if not EnchantUI.botName or not sameBotName(EnchantUI.botName, botName) then + return + end + + EnchantUI.pendingToken = nil + local frame = EnchantUI:EnsureWindow() + EnchantUI:UpdateApplyButton() + if tostring(status or "") == "OK" then + frame.status:SetText(L("enchant.trade.status.started", "Enchanting started. Complete the trade normally.")) + safeDelay(ENCHANT_REFRESH_DELAY, function() + if EnchantUI.botName and sameBotName(EnchantUI.botName, botName) then + EnchantUI:RequestList() + end + end) + else + frame.status:SetText(getReasonText(reason)) + end +end diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 71bdba8..a437750 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,19 +1,29 @@ # Multibot Chatless + Bridge — Roadmap de reprise -Statut : roadmap active issue de l'audit initial v1c du 1er août 2026. -Dernière mise à jour : 08/08/2026 — validation runtime du lot Warlock chatless, diagnostic TEMP_ENCHANT et bascule Firestone/Spellstone bridge-only. +Statut : roadmap active issue de l'audit initial v1c du 1er août 2026, resynchronisée avec l'état post-merge du 14 août 2026. +Dernière mise à jour : 14/08/2026 — service d'enchantement d'objet `ENCHANT_TRADE_V1` implémenté et validé en jeu, UI 440 px/i18n validées ; prochain chantier normal fixé à 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. ## Baseline auditée +Audit de synchronisation : `audit-multibot-roadmap-next-item-v1-2026-08-14-143927`. + - Addon : `L:\ChromieCraft_3.3.5a\Interface\AddOns\MultiBot` + - branche `main` ; + - HEAD et `origin/main` : `106074c3c93f80812f73af27e746860c7c8a4dcf` ; + - merge PR #61 : **Add chatless group Roll UI** ; + - worktree propre au début et à la fin de l'audit. - Bridge : `L:\AC_PB\azerothcore-wotlk\modules\mod-multibot-bridge` -- Playerbots : `L:\AC_PB\azerothcore-wotlk\modules\mod-playerbots` — lecture seule stricte -- Addon : baseline post-PR #51 auditée, dépôt Git propre, branche `main`, commit `270911305acf3e806d389712a34a9433131db981` -- AzerothCore : dépôt Git propre pour les modules bridge/Playerbots audités, branche `Playerbot`, commit `092e9ba6ff8dc6d861dddd1f31baa9d404381a85` -- Bridge : 7 fichiers, logique principale concentrée dans `src/MultiBotBridge.cpp` -- Communication actuelle : bridge-first pour les principaux rafraîchissements UI ; l'audit final du 07/08/2026 relève encore 159 lignes `SendChatMessage` à classifier, dont des reliquats `co/nc` directs dans des contrôles spécialisés. -- Fallback automatique legacy désactivé par défaut : `MultiBot.allowLegacyChatFallback = false`. + - branche `main` ; + - HEAD et `origin/main` : `210bd1f4f6597fe4f0691ec729ec4904ebe2d463` ; + - merge PR #26 : **Add chatless group Roll support** ; + - worktree propre au début et à la fin de l'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. +- 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. ## Règles de progression @@ -25,7 +35,7 @@ Audit → Analyse → Proposition → Validation utilisateur → Patch minimal - Rollback et hashes obligatoires. - Ne jamais ajouter d'exécuteur bridge générique acceptant une commande Playerbots arbitraire. -## Contribution externe Jellypowered — AUDIT AUTORISÉ, INTÉGRATION NON COMMENCÉE +## Contribution externe Jellypowered — RÉFÉRENCE CONSERVÉE, ATTRIBUTION OBLIGATOIRE Source reçue le 04/08/2026 : @@ -44,7 +54,7 @@ Décision validée : - ne jamais modifier `mod-playerbots` ; - ne marquer aucune fonction comme intégrée avant vérification, compilation, tests en jeu et validation explicite de l'utilisateur. -Fonctions candidates, toutes encore au statut `À AUDITER` : +Fonctions candidates identifiées dans la contribution lors de l'audit initial ; leur statut projet actuel doit être lu dans les phases et jalons ci-dessous, pas déduit de cette liste : 1. helpers de parsing numérique strict et réponses structurées ; 2. inventaire détaillé `INV_BAG`, `INV_ITEM_LOC`, `INV_EQUIP_LOC` ; @@ -74,7 +84,7 @@ Politique de tests : - les tests exhaustifs transversaux de toutes les fonctions pourront être exécutés vers la fin du projet ; - ce report des tests exhaustifs ne permet pas de déclarer une fonction validée avant ses propres tests ciblés. -Statut de reprise : contribution conservée pour un audit/intégration ultérieurs. La prochaine étape immédiate du projet est la migration des reliquats UI `co/nc` encore directs, puis la clôture de la matrice runtime finale STATE/stratégies. L'audit Jellypowered reprendra ensuite selon l'ordre validé. +Statut de reprise : contribution conservée comme source de référence. Les fonctions du projet déjà mergées sont suivies par leurs audits, patches et PR propres ; ce document ne doit pas déduire leur provenance sans preuve. Toute reprise future issue de cette contribution doit conserver l'attribution prévue ci-dessus et rester soumise au protocole Audit → Validation → Patch → Tests. ## Phase 0 — Assainissement documentaire — TERMINÉE @@ -170,41 +180,58 @@ Preuve d'audit final statique : Reste à terminer avant de fermer définitivement ce bloc : -- le lot Warlock Stones/Soulstones/Pets/Curses est validé au 08/08/2026 et ne fait plus partie des reliquats `co/nc` prioritaires ; +- le lot Warlock Stones/Soulstones/Pets/Curses est validé pour sa migration chatless et ne fait plus partie des reliquats `co/nc` prioritaires ; +- le comportement sans fallback silencieux des mutations stratégies a été durci et mergé après cette baseline intermédiaire ; - exécuter/consolider la matrice runtime finale : zéro/un/plusieurs bots, listes longues, fragment manquant/dupliqué/désordonné, réponse tardive, timeouts, déconnexion en cours de transaction, mutations valides/invalides, bot absent, plusieurs bots, smoke test toutes classes, zéro erreur Lua, contrôle chat et logs ; - classifier puis migrer les autres familles legacy réellement automatiques avant de déclarer le projet entièrement chatless. -## Validation livrée — Sélecteurs Warlock chatless + Stones — VALIDÉE LE 08/08/2026 +## Validation livrée — Sélecteurs Warlock chatless + Stones — MIGRATION CHATLESS VALIDÉE, RELIQUATS SUSPENDUS -Périmètre addon validé : +Périmètre validé et mergé : -- les sélecteurs Warlock Stones, Soulstones, Pets et Curses ne contiennent plus de `SendChatMessage` direct pour leurs mutations `co/nc` ; ils passent par `MultiBot.ActionToTarget()` puis `STRATEGY_MUTATION_V1` / `RUN~STRATEGY` lorsque le bridge est disponible ; -- `MultiBot.ActionToTarget()` distingue désormais le transport `bridge` du fallback `chat` ; avec le bridge, les sélecteurs n'appliquent plus d'état local optimiste et attendent l'état serveur autoritatif ; le fallback chat conserve son comportement immédiat de compatibilité ; -- les contrôles Warlock invalides `dps` et `dps debuff` ont été retirés, le placeholder Buff désactivé a été supprimé et le layout des contrôles a été compacté ; -- les quatre avertissements LuaLint ciblés sur les variables `action` ont été corrigés sans modifier le comportement. +- les sélecteurs Warlock Stones, Soulstones, Pets et Curses utilisent le transport structuré `STRATEGY_MUTATION_V1` / `RUN~STRATEGY` lorsque le bridge est disponible ; +- le chemin bridge attend l'état serveur autoritatif au lieu de valider localement une mutation avant l'ACK ; +- les contrôles Warlock invalides `dps` et `dps debuff` ainsi que le placeholder Buff désactivé ont été retirés et le layout a été compacté ; +- le bridge contient le mécanisme de bascule Firestone/Spellstone et l'endpoint diagnostique à la demande `GET~WEAPON_ENCHANT` / `WEAPON_ENCHANT` ; +- aucun fichier de `mod-playerbots` n'est modifié. -Diagnostic TEMP_ENCHANT validé : +Décision de roadmap au 14/08/2026 : -- `/mbdebug enchant [bot]` envoie à la demande `GET~WEAPON_ENCHANT` et affiche la réponse structurée `WEAPON_ENCHANT` ; -- le bridge lit l'item, l'ID de `TEMP_ENCHANTMENT_SLOT` et sa durée sur main-hand/off-hand ; -- l'endpoint est limité au bot visible et contrôlable, conserve `CheckLevelFor(...)`, et applique un rate-limit de 500 ms par requester ; -- aucun polling automatique n'est introduit et `mod-playerbots` n'est pas modifié. +- la **vérification réelle finale du `TEMP_ENCHANTMENT_SLOT` Firestone/Spellstone** reste un chantier suspendu à reprendre seulement à la fin de la roadmap normale ; +- les **quatre warnings LuaLint restants dans `Strategies/MultiBotWarlock.lua`** restent également suspendus ; +- ces reliquats ne doivent pas interrompre le chantier suivant de la Phase 5. -Cause et correction Firestone/Spellstone : +## Synchronisation post-merge — État livré au 14/08/2026 -- l'audit Playerbots en lecture seule a confirmé que `ItemForSpellValue` et `UseItemAction::UseItem()` refusent de cibler une arme dont `TEMP_ENCHANTMENT_SLOT` est déjà occupé ; la stratégie peut donc changer sans remplacer la pierre déjà appliquée ; -- le correctif reste dans `mod-multibot-bridge` : uniquement pour un Warlock, en `BOT_STATE_NON_COMBAT`, lors d'un vrai switch exclusif `firestone` ↔ `spellstone` ; -- le bridge découvre dynamiquement les enchant IDs des Firestone/Spellstone portées par le bot, refuse d'effacer un enchantement temporaire non reconnu, retire proprement l'ancien enchantement reconnu, puis réutilise l'action Playerbots existante avec `DoSpecificAction()` ; -- aucun ID Firestone/Spellstone n'est hardcodé dans le correctif et aucun fichier de `mod-playerbots` n'est modifié. +Les jalons suivants, postérieurs à la mise à jour du 08/08, sont présents dans les branches `main` auditées : -Preuves runtime : +- mutations stratégies : suppression du fallback chat silencieux lorsque le chemin structuré est requis ; +- 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 ; +- `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é. -- compilation Visual Studio `RelWithDebInfo x64` : 3 projets réussis, 0 échec ; worldserver démarré sans erreur bridge ; -- Apha, Spellstone → Firestone : `TEMP_ENCHANTMENT_SLOT` `3620` → `3614`, durée finale `3600000 ms`, utilisation réelle de Grand Firestone observée ; -- Apha, Firestone → Spellstone : `TEMP_ENCHANTMENT_SLOT` `3614` → `3620`, durée finale `3600000 ms`, utilisation réelle de Grand Spellstone observée ; -- audit final : `audit-multibot-warlock-stone-force-switch-final-v1-2026-08-08-160400-2026-08-08-160706.zip`, SHA-256 `C0025FCAC7817711B0D5493EA3349B5F57A3AA620C260E76F59E1CAA92F7EA1A` ; -- archivage patch : `patch-multibot-warlock-stone-force-switch-v1b-2026-08-08-154300-results-2026-08-08-162451.zip`, SHA-256 `8FABF24B50EA459EF6C7EE4A0D0BE21CFB251D1C7DEF6505C7C483BF43141C5B` ; -- `mod-playerbots` reste strictement en lecture seule. +Jalons de merge principaux : + +- Addon PR #58 — **Migrate inventory Sell Vendor to the bridge** ; +- Bridge PR #24 — **Add safe bridge-first SELL_VENDOR inventory action** ; +- Addon PR #60 — **Add bridge-first OPEN_ITEMS inventory action** ; +- Bridge PR #25 — **Add residual auto-safe OPEN_ITEMS handling** ; +- Addon PR #61 — **Add chatless group Roll UI** — merge `106074c3c93f80812f73af27e746860c7c8a4dcf` ; +- Bridge PR #26 — **Add chatless group Roll support** — merge `210bd1f4f6597fe4f0691ec729ec4904ebe2d463`. + +Validation `GROUP ROLL` : + +- roll normal 0–100 : OK ; +- roll avec objet par Shift+clic : OK ; +- seuls les bots éligibles au contexte Playerbots invoqué participent au roll item ; +- aucun whisper/chat parasite sur le workflow ; +- protection contre double envoi et refus d'un item vide/invalide ; +- pending nettoyé sur déconnexion/changement de monde ; +- UI finale validée : `240x245`, fond opaque style inventaire, padding horizontal `10 px`, padding vertical haut `10 px` ; +- compilation Bridge déjà validée sans erreur. ## Phase 1 — Baseline de compilation et tests de non-régression @@ -291,23 +318,45 @@ Avant chaque migration, classer l'occurrence `SendChatMessage` comme : - mécanisme UI à migrer ; - code mort à supprimer. -Ordre recommandé : - -1. **Formations — application par clic gauche : VALIDÉE** via `RUN~FORMATION~GROUP` par `patch-multibot-formation-chatless-v1c-2026-08-01-181300`. -2. **Consultation de la formation actuelle par clic droit : VALIDÉE** via `GET~FORMATIONS~GROUP`, `FORMATIONS_BEGIN/ITEM/END` et un tooltip local traduit. -3. **Infrastructure mutations stratégies `co/nc` : VALIDÉE STATIQUEMENT** via `STRATEGY_MUTATION_V1`, `RUN~STRATEGY`, `STRATEGY_ACK`, timeouts, limites et diagnostics explicites. -4. **Sélecteurs Warlock Stones/Soulstones/Pets/Curses : VALIDÉS** — mutations via `STRATEGY_MUTATION_V1` / `RUN~STRATEGY`, état UI autoritatif côté bridge et bascule réelle Firestone/Spellstone validée sans modification de Playerbots. -5. `s *` — vente générale bridge-first. -6. `s vendor` — vente vendeur bridge-first, sans whisper item par item. -7. `open items` — ouverture de conteneurs bridge-first. -8. `roll` et `roll [item]`. -9. Enchantement d'objet, après validation du flux trade/cast disponible sans modification de Playerbots. -10. Ajout/retrait d'items précis dans les règles de loot. -11. Décision sur `Quest`/`Skill` versus `Disenchant`, sans inventer de stratégie absente de Playerbots. -12. Ordres collectifs `follow`, `attack`, `stay` seulement après validation manuelle exacte des sélecteurs Playerbots ; ne pas réintroduire `RUN~ORDER` générique. +Ordre recommandé et état réel : + +1. **Formations — application par clic gauche : TERMINÉ / VALIDÉ** via `RUN~FORMATION~GROUP`. +2. **Consultation de la formation actuelle par clic droit : TERMINÉ / VALIDÉ** via `GET~FORMATIONS~GROUP`, `FORMATIONS_BEGIN/ITEM/END` et tooltip local traduit. +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. +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 — PR EN COURS (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. **PROCHAIN CHANTIER NORMAL — Ajout/retrait d'items précis dans les règles de loot.** +11. **À FAIRE — Décision sur `Quest`/`Skill` versus `Disenchant`**, sans inventer de stratégie absente de Playerbots. +12. **À 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. +### Validation Enchanting Trade Service — 14/08/2026 + +- audit Trade/Cast et interfaces Playerbots réalisé en lecture seule ; +- capacité négociée `ENCHANT_TRADE_V1` ; +- `GET~ENCHANT_TRADE` liste uniquement les sorts d'Enchanting connus et valides du bot avec disponibilité des composants/outils ; +- `RUN~ENCHANT_TRADE` accepte uniquement un bot contrôlable, un token et un ID de sort numérique ; aucun GUID d'objet arbitraire, texte de commande ou exécuteur Playerbots générique n'est exposé ; +- cible réelle via le Trade WoW natif et `TRADE_SLOT_NONTRADED`, avec revalidation Core au cast puis à l'acceptation finale du Trade ; +- rate-limit bridge : 4 requêtes par fenêtre de 2 secondes ; +- UI dédiée visible uniquement pour les bots enchanteurs, accessible depuis l'EveryBar et Character Info ; +- fenêtre réduite à 440 px, champ de recherche corrigé et textes Enchant Trade localisés dans les 8 locales runtime ; +- test en jeu : ouverture, liste, recherche, tooltips, Trade et enchantement réel **OK** ; +- spam chat automatique lié à ce service : **aucun**. + +### Chantiers suspendus — à reprendre seulement après la roadmap normale + +- `SELL_GREY` / sell-grey core API / bridge-first ; +- vérification réelle finale Firestone/Spellstone `TEMP_ENCHANTMENT_SLOT` ; +- quatre warnings LuaLint restants dans `Strategies/MultiBotWarlock.lua` ; +- autres petits reliquats explicitement reportés lors des étapes précédentes. + +Ces sujets restent enregistrés mais **ne doivent pas modifier l'ordre du prochain chantier**. + Critère de sortie : chaque famille migrée fonctionne bridge-first et ne génère plus de réponse chat automatique. ## Phase 6 — Backlog UI et fonctions secondaires