From 3d7fdbd058ba8901ce3f6090ab884805a682278b Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:19:51 +0200 Subject: [PATCH 1/3] Add bridge-first formation controls and localized status tooltip Moves formation selection and inspection out of PARTY/RAID chat, adds a localized per-bot formation tooltip, and keeps the scope raid-wide for all controllable bots. --- Core/MultiBotComm.lua | 267 +++++++++++++++++++++++++++++ Locales/MultiBotAceLocale-deDE.lua | 17 ++ Locales/MultiBotAceLocale-enGB.lua | 17 ++ Locales/MultiBotAceLocale-enUS.lua | 17 ++ Locales/MultiBotAceLocale-esES.lua | 17 ++ Locales/MultiBotAceLocale-frFR.lua | 17 ++ Locales/MultiBotAceLocale-koKR.lua | 17 ++ Locales/MultiBotAceLocale-ruRU.lua | 17 ++ Locales/MultiBotAceLocale-zhCN.lua | 17 ++ README.md | 38 ++++ UI/MultiBotFormationUI.lua | 197 +++++++++++++++++++-- docs/ROADMAP.md | 50 +++++- 12 files changed, 668 insertions(+), 20 deletions(-) diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index de73ada..6d4d4c3 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -152,6 +152,10 @@ local function ensureBridgeState() state.combatSeq = state.combatSeq or 0 state.positionSeq = state.positionSeq or 0 state.lootSeq = state.lootSeq or 0 + state.formationSeq = state.formationSeq or 0 + state.formationCommands = state.formationCommands or {} + state.formationQuerySeq = state.formationQuerySeq or 0 + state.formationQueryActive = state.formationQueryActive or nil return state end @@ -399,6 +403,197 @@ function Comm.RunPositionCommand(scope, target, command) return Comm.Send("RUN", "POSITION~" .. scope .. "~" .. urlEncodeField(target) .. "~" .. token .. "~" .. urlEncodeField(command)) end +function Comm.RunFormationCommand(scope, target, formation, callback) + local state = ensureBridgeState() + + if not state.connected then + return false + end + + scope = string.upper(trim(scope or "GROUP")) + target = trim(target or "") + formation = string.lower(trim(formation or "")) + + local allowed = { + arrow = true, + queue = true, + near = true, + melee = true, + line = true, + circle = true, + chaos = true, + shield = true, + } + + if scope ~= "GROUP" or target ~= "" or not allowed[formation] then + return false + end + + state.formationSeq = (tonumber(state.formationSeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-formation-" .. tostring(state.formationSeq) + state.formationCommands[token] = { + formation = formation, + callback = type(callback) == "function" and callback or nil, + startedAt = safeNow(), + } + + if not Comm.Send("RUN", "FORMATION~" .. scope .. "~~" .. token .. "~" .. urlEncodeField(formation)) then + state.formationCommands[token] = nil + return false + end + + return token +end + +function Comm.RequestFormations(callback) + local state = ensureBridgeState() + + if not state.connected then + return false + end + + state.formationQuerySeq = (tonumber(state.formationQuerySeq) or 0) + 1 + local token = tostring(math.floor(safeNow() * 1000)) .. "-formations-" .. tostring(state.formationQuerySeq) + + state.formationQueryActive = { + token = token, + callback = type(callback) == "function" and callback or nil, + startedAt = safeNow(), + expected = 0, + items = {}, + begun = false, + } + + if not Comm.Send("GET", "FORMATIONS~GROUP~~" .. token) then + state.formationQueryActive = nil + return false + end + + if MultiBot and type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(5.0, function() + local bridge = ensureBridgeState() + local active = bridge.formationQueryActive + if not active or active.token ~= token then + return + end + + bridge.formationQueryActive = nil + local result = { + token = token, + status = "timeout", + expected = tonumber(active.expected or 0) or 0, + sent = #(active.items or {}), + items = active.items or {}, + } + + if type(active.callback) == "function" then + active.callback(result) + end + + if MultiBot.OnFormationQueryCompleted then + MultiBot.OnFormationQueryCompleted(result) + end + end) + end + + return token +end + +local function getActiveFormationQuery(token) + local active = ensureBridgeState().formationQueryActive + token = trim(token) + + if type(active) ~= "table" or token == "" or active.token ~= token then + return nil + end + + return active +end + +function Comm.ApplyFormationsBeginPayload(payload) + local token, countText = splitOnce(payload or "", "~") + token = trim(token) + + local active = getActiveFormationQuery(token) + if not active then + return false + end + + active.expected = tonumber(countText or "0") or 0 + active.items = {} + active.begun = true + + debugPrint("ADDON:RX", "FORMATIONS_BEGIN", token, active.expected) + return true +end + +function Comm.ApplyFormationsItemPayload(payload) + local token, rest = splitOnce(payload or "", "~") + local encodedBotName, encodedFormation = splitOnce(rest or "", "~") + token = trim(token) + + local active = getActiveFormationQuery(token) + if not active or not active.begun then + return false + end + + local botName = trim(urlDecodeField(encodedBotName)) + local formation = string.lower(trim(urlDecodeField(encodedFormation))) + + if botName == "" then + return false + end + + if formation == "" then + formation = "?" + end + + active.items[#active.items + 1] = { + botName = botName, + formation = formation, + } + + debugPrint("ADDON:RX", "FORMATIONS_ITEM", token, botName, formation) + return true +end + +function Comm.ApplyFormationsEndPayload(payload) + local token, sentText = splitOnce(payload or "", "~") + token = trim(token) + + local state = ensureBridgeState() + local active = getActiveFormationQuery(token) + if not active or not active.begun then + return false + end + + table.sort(active.items, function(left, right) + return string.lower(left.botName or "") < string.lower(right.botName or "") + end) + + state.formationQueryActive = nil + + local result = { + token = token, + status = "ok", + expected = tonumber(active.expected or 0) or 0, + sent = tonumber(sentText or "0") or 0, + items = active.items or {}, + } + + debugPrint("ADDON:RX", "FORMATIONS_END", token, result.sent) + + if type(active.callback) == "function" then + active.callback(result) + end + + if MultiBot.OnFormationQueryCompleted then + MultiBot.OnFormationQueryCompleted(result) + end + + return true +end + function Comm.RequestOutfits(name) local state = ensureBridgeState() name = trim(name) @@ -892,6 +1087,7 @@ function Comm.MarkDisconnected(reason) state.outfitCommands = {} state.trainerActive = nil state.trainerCommands = {} + state.formationQueryActive = nil end local function parseBridgeDetailPayload(payload) @@ -3087,6 +3283,75 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) return Comm.ApplyProfessionRecipeCraftPayload(payload) end + if opcode == "FORMATIONS_BEGIN" then + state.connected = true + state.lastError = nil + return Comm.ApplyFormationsBeginPayload(payload) + end + + if opcode == "FORMATIONS_ITEM" then + state.connected = true + state.lastError = nil + return Comm.ApplyFormationsItemPayload(payload) + end + + if opcode == "FORMATIONS_END" then + state.connected = true + state.lastError = nil + return Comm.ApplyFormationsEndPayload(payload) + end + + if opcode == "FORMATION_ACK" then + local scope, rest = splitOnce(payload or "", "~") + local target, rest2 = splitOnce(rest or "", "~") + local token, rest3 = splitOnce(rest2 or "", "~") + local successText, rest4 = splitOnce(rest3 or "", "~") + local failureText, encodedFormation = splitOnce(rest4 or "", "~") + + scope = string.upper(trim(scope)) + target = trim(urlDecodeField(target)) + token = trim(token) + local success = tonumber(successText or "0") or 0 + local failure = tonumber(failureText or "0") or 0 + local formation = string.lower(trim(urlDecodeField(encodedFormation))) + + state.connected = true + state.lastError = nil + debugPrint("ADDON:RX", "FORMATION_ACK", payload or "") + + local pending = state.formationCommands[token] + state.formationCommands[token] = nil + + local result = { + scope = scope, + target = target, + token = token, + success = success, + failure = failure, + formation = formation, + } + + if pending and type(pending.callback) == "function" then + pending.callback(result) + end + + if MultiBot.OnFormationCommandApplied then + MultiBot.OnFormationCommandApplied(result) + end + + if success <= 0 then + systemMessage(L("formation.confirm.none", "Formation was not applied to any grouped bot.")) + elseif failure > 0 then + systemMessage(string.format( + L("formation.confirm.partial", "Formation applied to %d bot(s), failed for %d bot(s)."), + success, + failure + )) + end + + return true + end + if opcode == "RTI_ACK" then state.connected = true state.lastError = nil @@ -3208,6 +3473,8 @@ function Comm.OnPlayerEnteringWorld() state.questActive = {} state.gameObjects = {} state.gameObjectActive = {} + state.formationCommands = {} + state.formationQueryActive = nil state.talentSpecs = {} state.talentSpecActive = nil state.inventoryActive = nil diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index f82bb61..06c4527 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -1027,6 +1027,23 @@ local deDEValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "Dieser Zauber ist nicht mehr verfügbar.", ["info.trainer.reason.TOO_EXPENSIVE"] = "Der Bot hat nicht genug verfügbares Geld.", ["tips.outfits.equip"] = "Linksklick: Ausrüsten\nRechtsklick: Ersetzen", + -- Chatless formation query + ["formation.query.title"] = "Aktuelle Formationen", + ["formation.query.unavailable"] = "Bridge nicht verfügbar.", + ["formation.query.timeout"] = "Zeitüberschreitung bei der Formationsabfrage.", + ["formation.query.empty"] = "Kein steuerbarer Bot in der Gruppe oder im Schlachtzug gefunden.", + ["formation.query.mixed"] = "Formationsstatus: gemischt", + ["formation.query.common"] = "Aktuelle Formation: %s (%d Bot(s))", + ["formation.name.arrow"] = "Pfeil", + ["formation.name.queue"] = "Kolonne", + ["formation.name.near"] = "Nah", + ["formation.name.melee"] = "Nahkampf", + ["formation.name.line"] = "Linie", + ["formation.name.circle"] = "Kreis", + ["formation.name.chaos"] = "Chaos", + ["formation.name.shield"] = "Schild", + ["formation.name.far"] = "Entfernt", + ["formation.name.unknown"] = "Unbekannt", } register("deDE", deDEValues) diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index 68d2532..7696f20 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -1030,6 +1030,23 @@ local enGBValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "This spell is no longer available.", ["info.trainer.reason.TOO_EXPENSIVE"] = "The bot does not have enough available money.", ["tips.outfits.equip"] = "Left click: Equip\nRight click: Replace", + -- Chatless formation query + ["formation.query.title"] = "Current formations", + ["formation.query.unavailable"] = "Bridge unavailable.", + ["formation.query.timeout"] = "Formation query timed out.", + ["formation.query.empty"] = "No controllable bot found in the group or raid.", + ["formation.query.mixed"] = "Formation state: mixed", + ["formation.query.common"] = "Current formation: %s (%d bot(s))", + ["formation.name.arrow"] = "Arrow", + ["formation.name.queue"] = "Queue", + ["formation.name.near"] = "Near", + ["formation.name.melee"] = "Melee", + ["formation.name.line"] = "Line", + ["formation.name.circle"] = "Circle", + ["formation.name.chaos"] = "Chaos", + ["formation.name.shield"] = "Shield", + ["formation.name.far"] = "Far", + ["formation.name.unknown"] = "Unknown", } register("enGB", enGBValues) diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index 7b28c58..c45eeee 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -1030,6 +1030,23 @@ local enUSValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "This spell is no longer available.", ["info.trainer.reason.TOO_EXPENSIVE"] = "The bot does not have enough available money.", ["tips.outfits.equip"] = "Left click: Equip\nRight click: Replace", + -- Chatless formation query + ["formation.query.title"] = "Current formations", + ["formation.query.unavailable"] = "Bridge unavailable.", + ["formation.query.timeout"] = "Formation query timed out.", + ["formation.query.empty"] = "No controllable bot found in the group or raid.", + ["formation.query.mixed"] = "Formation state: mixed", + ["formation.query.common"] = "Current formation: %s (%d bot(s))", + ["formation.name.arrow"] = "Arrow", + ["formation.name.queue"] = "Queue", + ["formation.name.near"] = "Near", + ["formation.name.melee"] = "Melee", + ["formation.name.line"] = "Line", + ["formation.name.circle"] = "Circle", + ["formation.name.chaos"] = "Chaos", + ["formation.name.shield"] = "Shield", + ["formation.name.far"] = "Far", + ["formation.name.unknown"] = "Unknown", } register("enUS", enUSValues, true) diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index b2d2731..d63b2e6 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -1028,6 +1028,23 @@ local esESValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "Este hechizo ya no está disponible.", ["info.trainer.reason.TOO_EXPENSIVE"] = "El bot no tiene suficiente dinero disponible.", ["tips.outfits.equip"] = "Clic izquierdo: Equipar\nClic derecho: Reemplazar", + -- Chatless formation query + ["formation.query.title"] = "Formaciones actuales", + ["formation.query.unavailable"] = "Bridge no disponible.", + ["formation.query.timeout"] = "La consulta de formaciones agotó el tiempo de espera.", + ["formation.query.empty"] = "No se encontró ningún bot controlable en el grupo o la banda.", + ["formation.query.mixed"] = "Estado de las formaciones: mixto", + ["formation.query.common"] = "Formación actual: %s (%d bot(s))", + ["formation.name.arrow"] = "Flecha", + ["formation.name.queue"] = "Fila", + ["formation.name.near"] = "Cercana", + ["formation.name.melee"] = "Cuerpo a cuerpo", + ["formation.name.line"] = "Línea", + ["formation.name.circle"] = "Círculo", + ["formation.name.chaos"] = "Caos", + ["formation.name.shield"] = "Escudo", + ["formation.name.far"] = "Lejana", + ["formation.name.unknown"] = "Desconocida", } register("esES", esESValues) diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index dddaf49..4e8823a 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -1027,6 +1027,23 @@ local frFRValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "Ce sort n'est plus disponible.", ["info.trainer.reason.TOO_EXPENSIVE"] = "Le bot n'a pas assez d'argent disponible.", ["tips.outfits.equip"] = "Clic gauche : Équiper\nClic droit : Remplacer", + -- Chatless formation query + ["formation.query.title"] = "Formations actuelles", + ["formation.query.unavailable"] = "Bridge indisponible.", + ["formation.query.timeout"] = "La consultation des formations a expiré.", + ["formation.query.empty"] = "Aucun bot contrôlable trouvé dans le groupe ou le raid.", + ["formation.query.mixed"] = "État des formations : mixte", + ["formation.query.common"] = "Formation actuelle : %s (%d bot(s))", + ["formation.name.arrow"] = "Flèche", + ["formation.name.queue"] = "File", + ["formation.name.near"] = "Rapprochée", + ["formation.name.melee"] = "Mêlée", + ["formation.name.line"] = "Ligne", + ["formation.name.circle"] = "Cercle", + ["formation.name.chaos"] = "Chaos", + ["formation.name.shield"] = "Bouclier", + ["formation.name.far"] = "Éloignée", + ["formation.name.unknown"] = "Inconnue", } register("frFR", frFRValues) diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 1280d42..35a40e4 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -1019,6 +1019,23 @@ local koKRValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "이 주문은 더 이상 사용할 수 없습니다.", ["info.trainer.reason.TOO_EXPENSIVE"] = "봇의 사용 가능한 돈이 부족합니다.", ["tips.outfits.equip"] = "왼쪽 클릭: 장착\n오른쪽 클릭: 바꾸기", + -- Chatless formation query + ["formation.query.title"] = "현재 포메이션", + ["formation.query.unavailable"] = "Bridge를 사용할 수 없습니다.", + ["formation.query.timeout"] = "포메이션 조회 시간이 초과되었습니다.", + ["formation.query.empty"] = "파티 또는 공격대에서 제어 가능한 봇을 찾을 수 없습니다.", + ["formation.query.mixed"] = "포메이션 상태: 혼합", + ["formation.query.common"] = "현재 포메이션: %s (봇 %d명)", + ["formation.name.arrow"] = "화살표", + ["formation.name.queue"] = "대기열", + ["formation.name.near"] = "근거리", + ["formation.name.melee"] = "근접전", + ["formation.name.line"] = "선형", + ["formation.name.circle"] = "원형", + ["formation.name.chaos"] = "혼돈", + ["formation.name.shield"] = "방패", + ["formation.name.far"] = "원거리", + ["formation.name.unknown"] = "알 수 없음", } register("koKR", koKRValues) diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 729efb0..9bc9986 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -1028,6 +1028,23 @@ local ruRUValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "Это заклинание больше недоступно.", ["info.trainer.reason.TOO_EXPENSIVE"] = "У бота недостаточно доступных денег.", ["tips.outfits.equip"] = "ЛКМ: Надеть\nПКМ: Заменить", + -- Chatless formation query + ["formation.query.title"] = "Текущие построения", + ["formation.query.unavailable"] = "Bridge недоступен.", + ["formation.query.timeout"] = "Время ожидания запроса построений истекло.", + ["formation.query.empty"] = "В группе или рейде нет доступных для управления ботов.", + ["formation.query.mixed"] = "Состояние построений: смешанное", + ["formation.query.common"] = "Текущее построение: %s (%d бот(ов))", + ["formation.name.arrow"] = "Клин", + ["formation.name.queue"] = "Колонна", + ["formation.name.near"] = "Рядом", + ["formation.name.melee"] = "Ближний бой", + ["formation.name.line"] = "Шеренга", + ["formation.name.circle"] = "Круг", + ["formation.name.chaos"] = "Хаос", + ["formation.name.shield"] = "Щит", + ["formation.name.far"] = "Дальнее", + ["formation.name.unknown"] = "Неизвестно", } register("ruRU", ruRUValues) diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index d53d8c1..1f7af17 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -1028,6 +1028,23 @@ local zhCNValues = { ["info.trainer.reason.NO_MATCHING_SPELL"] = "该法术已不可用。", ["info.trainer.reason.TOO_EXPENSIVE"] = "机器人的可用金币不足。", ["tips.outfits.equip"] = "左键:装备\n右键:替换", + -- Chatless formation query + ["formation.query.title"] = "当前阵型", + ["formation.query.unavailable"] = "Bridge 不可用。", + ["formation.query.timeout"] = "阵型查询超时。", + ["formation.query.empty"] = "小队或团队中没有可控制的机器人。", + ["formation.query.mixed"] = "阵型状态:混合", + ["formation.query.common"] = "当前阵型:%s(%d 个机器人)", + ["formation.name.arrow"] = "箭头", + ["formation.name.queue"] = "队列", + ["formation.name.near"] = "近距离", + ["formation.name.melee"] = "近战", + ["formation.name.line"] = "直线", + ["formation.name.circle"] = "圆形", + ["formation.name.chaos"] = "混乱", + ["formation.name.shield"] = "盾牌", + ["formation.name.far"] = "远距离", + ["formation.name.unknown"] = "未知", } register("zhCN", zhCNValues) diff --git a/README.md b/README.md index d063a89..3782c90 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ GET~GLYPHS GET~OUTFITS GET~QUESTS GET~GAMEOBJECTS +GET~FORMATIONS RUN~CRAFT_RECIPE RUN~ITEM_ACTION RUN~OUTFIT @@ -107,6 +108,7 @@ RUN~RTI RUN~COMBAT RUN~POSITION RUN~LOOT +RUN~FORMATION ``` Manual playerbot commands are still intentionally preserved for diagnostics and gameplay actions. @@ -201,6 +203,10 @@ The goal is to remove automatic UI-refresh spam. Random bot visibility Improved bridge-visible grouped randombots alongside AddClass bots and altbots + + Party / raid formation controls + Bridge-first and chatless — left-click applies one formation to every controllable bot in the current party or raid through RUN~FORMATION; right-click reads each bot's effective formation through GET~FORMATIONS and displays a localized tooltip + Legacy automatic chat fallback Disabled by default @@ -267,6 +273,7 @@ The goal is to remove automatic UI-refresh spam. - German client - French client - Spanish client +- Localization files included for enUS, enGB, frFR, esES, deDE, ruRU, zhCN and koKR. ## Server @@ -482,6 +489,8 @@ Implemented bridge-first / chatless areas: - Pull Control frame through the bridge. - Combat strategy fine tuning through the bridge. - Disperse controls through the bridge with `disperse set ` and `disperse disable`. +- Party/raid-wide formation application through `RUN~FORMATION`, with per-bot effective formation inspection through `GET~FORMATIONS` and no PARTY/RAID chat output. +- Localized formation status tooltip for all eight addon locale files. - Loot rules through the bridge with `nc +loot`, `nc -loot` and `ll all|normal|gray|quest|skill`. - Loot Master UI for master-loot distribution with item tooltips, candidate scoring, profession/spec hints, saved preferences and recent loot history. - Bridge-visible bot discovery for AddClass bots, altbots and grouped randombots. @@ -563,6 +572,24 @@ MultiBot.allowLegacyChatFallback = false +
+The formation tooltip does not appear or is incomplete + +Check that the bridge is connected and that the bots are controllable members of the same party or raid as the player. + +A right-click on the Formation button should produce structured bridge traffic similar to: + +```text +GET~FORMATIONS~GROUP~~ +FORMATIONS_BEGIN~~ +FORMATIONS_ITEM~~~ +FORMATIONS_END~~ +``` + +The query is raid-wide: it includes all controllable bots in the current party or raid and does not target individual raid subgroups. + +
+
Inventory, spellbook, glyphs or outfits do not update @@ -617,6 +644,15 @@ The frame uses the client master-loot candidate API and enriches candidates with --- +# Project Documentation + +The active project documentation is intentionally limited to two files: + +- [Development roadmap](docs/ROADMAP.md) — current phases, priorities, risks and acceptance criteria. +- [Debug and observability runbook](docs/DEBUG_RUNBOOK.md) — in-game debug commands, performance counters and bug-report procedure. + +--- + # Repository Layout ```text @@ -631,6 +667,8 @@ MultiBot-Chatless/ ├── Textures/ ├── UI/ ├── docs/ +│ ├── DEBUG_RUNBOOK.md +│ └── ROADMAP.md └── MultiBot.toc ``` diff --git a/UI/MultiBotFormationUI.lua b/UI/MultiBotFormationUI.lua index 34b7028..9a39f28 100644 --- a/UI/MultiBotFormationUI.lua +++ b/UI/MultiBotFormationUI.lua @@ -8,17 +8,150 @@ local FORMATION_FRAME_Y = 34 local FORMATION_CELL_WIDTH = 40 local FORMATION_CELL_HEIGHT = 30 +local latestFormationToken = nil +local latestFormationQueryToken = nil +local latestFormationTooltipToken = nil + local FORMATION_BUTTONS = { - { name = "Arrow", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_arrow.blp", cmd = "formation arrow" }, - { name = "Queue", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_queue.blp", cmd = "formation queue" }, - { name = "Near", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_near.blp", cmd = "formation near" }, - { name = "Melee", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_melee.blp", cmd = "formation melee" }, - { name = "Line", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_line.blp", cmd = "formation line" }, - { name = "Circle", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_circle.blp", cmd = "formation circle" }, - { name = "Chaos", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_chaos.blp", cmd = "formation chaos" }, - { name = "Shield", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_shield.blp", cmd = "formation shield" }, + { name = "Arrow", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_arrow.blp", value = "arrow" }, + { name = "Queue", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_queue.blp", value = "queue" }, + { name = "Near", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_near.blp", value = "near" }, + { name = "Melee", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_melee.blp", value = "melee" }, + { name = "Line", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_line.blp", value = "line" }, + { name = "Circle", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_circle.blp", value = "circle" }, + { name = "Chaos", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_chaos.blp", value = "chaos" }, + { name = "Shield", icon = "Interface\\AddOns\\MultiBot\\Icons\\formation_shield.blp", value = "shield" }, } +local function formationText(key) + if MultiBot and type(MultiBot.L) == "function" then + local value = MultiBot.L(key) + if type(value) == "string" and value ~= "" then + return value + end + end + + return key +end + +local function formationLabel(value) + local normalized = string.lower(tostring(value or "?")) + local key = "formation.name." .. normalized + + if normalized == "?" then + key = "formation.name.unknown" + end + + local label = formationText(key) + if label == key then + return normalized + end + + return label +end + +local function hideFormationTooltip(token) + if token ~= latestFormationTooltipToken then + return + end + + latestFormationTooltipToken = nil + if GameTooltip then + GameTooltip:Hide() + end +end + +local function showFormationTooltip(button, result) + if not button or not GameTooltip then + return + end + + local token = result and result.token or tostring(GetTime and GetTime() or 0) + latestFormationTooltipToken = token + + GameTooltip:SetOwner(button, "ANCHOR_TOPRIGHT", 0 - (button.size or 32), 2) + GameTooltip:ClearLines() + GameTooltip:AddLine(formationText("formation.query.title"), 1, 0.82, 0) + + if not result or result.status == "unavailable" then + GameTooltip:AddLine(formationText("formation.query.unavailable"), 1, 0.25, 0.25, true) + elseif result.status == "timeout" then + GameTooltip:AddLine(formationText("formation.query.timeout"), 1, 0.25, 0.25, true) + else + local items = result.items or {} + local count = #items + + if count == 0 then + GameTooltip:AddLine(formationText("formation.query.empty"), 0.8, 0.8, 0.8, true) + else + local commonFormation = items[1] and items[1].formation or "?" + local mixed = false + + for index = 2, count do + if items[index].formation ~= commonFormation then + mixed = true + break + end + end + + if mixed then + GameTooltip:AddLine(formationText("formation.query.mixed"), 1, 0.65, 0.2) + else + GameTooltip:AddLine( + string.format( + formationText("formation.query.common"), + formationLabel(commonFormation), + count + ), + 0.35, + 1, + 0.35 + ) + end + + GameTooltip:AddLine(" ") + for _, item in ipairs(items) do + GameTooltip:AddDoubleLine( + tostring(item.botName or "?"), + formationLabel(item.formation), + 1, + 1, + 1, + 0.5, + 0.82, + 1 + ) + end + end + end + + GameTooltip:Show() + + if MultiBot and type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(8.0, function() + hideFormationTooltip(token) + end) + end +end + +local function applyFormationSelection(parent, texture) + if not parent or not parent.frames or not parent.buttons then + return + end + + local frame = parent.frames[FORMATION_FRAME_NAME] + local button = parent.buttons[FORMATION_BUTTON_NAME] + if not frame or not button then + return + end + + button.setTexture(texture) + frame:Hide() + if MultiBot.RequestClickBlockerUpdate then + MultiBot.RequestClickBlockerUpdate(frame) + end +end + local function addFormationButton(frame, definition, column, row) frame.addButton( definition.name, @@ -27,7 +160,29 @@ local function addFormationButton(frame, definition, column, row) definition.icon, MultiBot.L("tips.format." .. string.lower(definition.name)) ).doLeft = function(button) - MultiBot.SelectToGroup(button.parent.parent, FORMATION_FRAME_NAME, button.texture, definition.cmd) + if not MultiBot.Comm or not MultiBot.Comm.RunFormationCommand then + return + end + + local parent = button.parent and button.parent.parent + local token + token = MultiBot.Comm.RunFormationCommand("GROUP", "", definition.value, function(result) + if not result or result.token ~= latestFormationToken then + return + end + + latestFormationToken = nil + if result.success > 0 + and result.failure == 0 + and result.formation == definition.value + then + applyFormationSelection(parent, definition.icon) + end + end) + + if token then + latestFormationToken = token + end end end @@ -48,8 +203,28 @@ function MultiBot.BuildFormationUI(tLeft) MultiBot.ShowHideSwitch(button.parent.frames[FORMATION_FRAME_NAME]) end - formatButton.doRight = function() - MultiBot.ActionToGroup("formation") + formatButton.doRight = function(button) + if not MultiBot.Comm or not MultiBot.Comm.RequestFormations then + showFormationTooltip(button, { status = "unavailable" }) + return + end + + local token + token = MultiBot.Comm.RequestFormations(function(result) + if not result or result.token ~= latestFormationQueryToken then + return + end + + latestFormationQueryToken = nil + showFormationTooltip(button, result) + end) + + if token then + latestFormationQueryToken = token + else + latestFormationQueryToken = nil + showFormationTooltip(button, { status = "unavailable" }) + end end local formatFrame = tLeft.addFrame(FORMATION_FRAME_NAME, FORMATION_FRAME_X, FORMATION_FRAME_Y) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a4cc3f0..07e4e97 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,7 +1,7 @@ # Multibot Chatless + Bridge — Roadmap de reprise Statut : roadmap active issue de l'audit initial v1c du 1er août 2026. -Dernière mise à jour : 01/08/2026 par `patch-multibot-docs-cleanup-roadmap-v1-2026-08-01-162227`. +Dernière mise à jour : 03/08/2026 par `patch-multibot-roadmap-formation-validation-v1-2026-08-03-184900`. 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 @@ -40,6 +40,36 @@ Réalisé par `patch-multibot-docs-cleanup-roadmap-v1-2026-08-01-162227` : Critère de sortie : phase validée par `verify.ps1`, avec hashes post-patch conformes et aucun ancien document actif. +## Validation livrée — Formations chatless — VALIDÉE LE 03/08/2026 + +Patch fonctionnel validé : `patch-multibot-formation-chatless-v1c-2026-08-01-181300`. + +Périmètre validé : + +- les clics gauche `arrow`, `queue`, `near`, `melee`, `line`, `circle`, `chaos` et `shield` utilisent désormais `RUN~FORMATION~GROUP` ; +- le bridge applique directement `FormationValue::Load()` sans passer par `HandleCommand()` ni par l'action Playerbots `set formation` ; +- le fonctionnement est validé avec la stratégie `passive`, en groupe et pour l'ensemble des bots contrôlables d'un raid ; +- aucun fichier de `mod-playerbots` n'a été modifié ; +- l'icône de l'addon n'est mise à jour qu'après un `FORMATION_ACK` complet ; +- aucun message `formation ...` n'est envoyé dans PARTY ou RAID pour ces clics. + +Preuves de validation : + +- compilation `worldserver` validée par l'utilisateur ; +- audit runtime : `audit-multibot-runtime-tests-v1c-2026-08-03-184046.zip` ; +- SHA-256 de l'audit : `7E3FBD948C51FAE34351B97B416BDFA663061F4577932F6AC251C79ECE933F25` ; +- 11 requêtes `RUN~FORMATION`, 11 réponses `FORMATION_ACK`, 55 applications réussies et 0 échec ; +- les huit formations ont provoqué visuellement le déplacement attendu des bots ; +- l'icône a été mise à jour visuellement sans message chat visible ; +- aucune erreur Lua MultiBot ni ancien blocage `PassiveMultiplier` observé. + +Reste explicitement hors périmètre : + +- le clic droit de consultation de la formation actuelle utilise encore `MultiBot.ActionToGroup("formation")` et l'ancien chemin PARTY/RAID ; +- la formation Playerbots `far` existe dans le module de référence mais n'est pas exposée par l'interface actuelle. + +Prochaine reprise recommandée : auditer puis migrer la consultation de formation par clic droit vers une lecture bridge structurée, dans un patch séparé. + ## Phase 1 — Baseline de compilation et tests de non-régression Objectif : prouver le fonctionnement de l'état actuel avant toute correction source. @@ -127,14 +157,16 @@ Avant chaque migration, classer l'occurrence `SendChatMessage` comme : Ordre recommandé : -1. `s *` — vente générale bridge-first. -2. `s vendor` — vente vendeur bridge-first, sans whisper item par item. -3. `open items` — ouverture de conteneurs bridge-first. -4. `roll` et `roll [item]`. -5. Enchantement d'objet, après validation du flux trade/cast disponible sans modification de Playerbots. -6. Ajout/retrait d'items précis dans les règles de loot. -7. Décision sur `Quest`/`Skill` versus `Disenchant`, sans inventer de stratégie absente de Playerbots. -8. Ordres collectifs `follow`, `attack`, `stay` seulement après validation manuelle exacte des sélecteurs Playerbots ; ne pas réintroduire `RUN~ORDER` générique. +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 — encore fondée sur PARTY/RAID ; prochaine migration recommandée via une lecture bridge structurée. +3. `s *` — vente générale bridge-first. +4. `s vendor` — vente vendeur bridge-first, sans whisper item par item. +5. `open items` — ouverture de conteneurs bridge-first. +6. `roll` et `roll [item]`. +7. Enchantement d'objet, après validation du flux trade/cast disponible sans modification de Playerbots. +8. Ajout/retrait d'items précis dans les règles de loot. +9. Décision sur `Quest`/`Skill` versus `Disenchant`, sans inventer de stratégie absente de Playerbots. +10. 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. From 7eca152ee53990b708bf98bc02a64d78cc707346 Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:41:35 +0200 Subject: [PATCH 2/3] fixes --- UI/MultiBotFormationUI.lua | 15 +++++++++++---- docs/ROADMAP.md | 27 ++++++++++++++++++--------- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/UI/MultiBotFormationUI.lua b/UI/MultiBotFormationUI.lua index 9a39f28..1d09c02 100644 --- a/UI/MultiBotFormationUI.lua +++ b/UI/MultiBotFormationUI.lua @@ -50,15 +50,22 @@ local function formationLabel(value) return label end -local function hideFormationTooltip(token) +local function hideFormationTooltip(token, owner) if token ~= latestFormationTooltipToken then return end latestFormationTooltipToken = nil - if GameTooltip then - GameTooltip:Hide() + + if not GameTooltip or not GameTooltip.GetOwner or not GameTooltip.Hide then + return + end + + if GameTooltip:GetOwner() ~= owner then + return end + + GameTooltip:Hide() end local function showFormationTooltip(button, result) @@ -129,7 +136,7 @@ local function showFormationTooltip(button, result) if MultiBot and type(MultiBot.TimerAfter) == "function" then MultiBot.TimerAfter(8.0, function() - hideFormationTooltip(token) + hideFormationTooltip(token, button) end) end end diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 07e4e97..d98c7c5 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,7 +1,7 @@ # Multibot Chatless + Bridge — Roadmap de reprise Statut : roadmap active issue de l'audit initial v1c du 1er août 2026. -Dernière mise à jour : 03/08/2026 par `patch-multibot-roadmap-formation-validation-v1-2026-08-03-184900`. +Dernière mise à jour : 03/08/2026 par `hotfix-multibot-formation-tooltip-roadmap-review-v1-2026-08-03-213000`. 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 @@ -40,9 +40,13 @@ Réalisé par `patch-multibot-docs-cleanup-roadmap-v1-2026-08-01-162227` : Critère de sortie : phase validée par `verify.ps1`, avec hashes post-patch conformes et aucun ancien document actif. -## Validation livrée — Formations chatless — VALIDÉE LE 03/08/2026 +## Validation livrée — Formations chatless — APPLICATION ET CONSULTATION VALIDÉES LE 03/08/2026 -Patch fonctionnel validé : `patch-multibot-formation-chatless-v1c-2026-08-01-181300`. +Patches fonctionnels validés : + +- application : `patch-multibot-formation-chatless-v1c-2026-08-01-181300` ; +- consultation : `patch-multibot-formation-query-chatless-v1-2026-08-03-195340` ; +- localisation : `patch-multibot-formation-query-i18n-v1b-2026-08-03-210300`. Périmètre validé : @@ -51,7 +55,11 @@ Périmètre validé : - le fonctionnement est validé avec la stratégie `passive`, en groupe et pour l'ensemble des bots contrôlables d'un raid ; - aucun fichier de `mod-playerbots` n'a été modifié ; - l'icône de l'addon n'est mise à jour qu'après un `FORMATION_ACK` complet ; -- aucun message `formation ...` n'est envoyé dans PARTY ou RAID pour ces clics. +- aucun message `formation ...` n'est envoyé dans PARTY ou RAID pour ces clics ; +- le clic droit utilise `MultiBot.Comm.RequestFormations()` puis `GET~FORMATIONS~GROUP` ; +- le bridge lit la valeur effective de chaque bot avec `FormationValue::Save()` et renvoie `FORMATIONS_BEGIN/ITEM/END` ; +- le résultat est affiché localement, une ligne par bot, dans un tooltip traduit pour les huit locales supportées ; +- aucun message PARTY, RAID, WHISPER ou `TellMaster` n'est produit par la consultation. Preuves de validation : @@ -61,15 +69,16 @@ Preuves de validation : - 11 requêtes `RUN~FORMATION`, 11 réponses `FORMATION_ACK`, 55 applications réussies et 0 échec ; - les huit formations ont provoqué visuellement le déplacement attendu des bots ; - l'icône a été mise à jour visuellement sans message chat visible ; -- aucune erreur Lua MultiBot ni ancien blocage `PassiveMultiplier` observé. +- aucune erreur Lua MultiBot ni ancien blocage `PassiveMultiplier` observé ; +- audit de consultation : `audit-multibot-runtime-tests-v1c-2026-08-03-203219.zip` ; +- SHA-256 de cet audit : `44627A920618C747BD9EEB0384D118FFFA13157828677172E46A642436677CB5` ; +- 9 requêtes `GET~FORMATIONS`, 9 séquences `FORMATIONS_BEGIN/END` et 23 réponses individuelles `FORMATIONS_ITEM` ; +- tooltip local et traductions validés visuellement par l'utilisateur, sans sortie chat. Reste explicitement hors périmètre : -- le clic droit de consultation de la formation actuelle utilise encore `MultiBot.ActionToGroup("formation")` et l'ancien chemin PARTY/RAID ; - la formation Playerbots `far` existe dans le module de référence mais n'est pas exposée par l'interface actuelle. -Prochaine reprise recommandée : auditer puis migrer la consultation de formation par clic droit vers une lecture bridge structurée, dans un patch séparé. - ## Phase 1 — Baseline de compilation et tests de non-régression Objectif : prouver le fonctionnement de l'état actuel avant toute correction source. @@ -158,7 +167,7 @@ Avant chaque migration, classer l'occurrence `SendChatMessage` comme : 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 — encore fondée sur PARTY/RAID ; prochaine migration recommandée via une lecture bridge structurée. +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. `s *` — vente générale bridge-first. 4. `s vendor` — vente vendeur bridge-first, sans whisper item par item. 5. `open items` — ouverture de conteneurs bridge-first. From 78a5c92d09f965980516070fbbf9d746495babdf Mon Sep 17 00:00:00 2001 From: Alex Dcnh <140754794+Wishmaster117@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:03:13 +0200 Subject: [PATCH 3/3] Fix 2 --- Core/MultiBotComm.lua | 36 ++++++++++++++++++++++++++++-- Locales/MultiBotAceLocale-deDE.lua | 3 +++ Locales/MultiBotAceLocale-enGB.lua | 3 +++ Locales/MultiBotAceLocale-enUS.lua | 3 +++ Locales/MultiBotAceLocale-esES.lua | 3 +++ Locales/MultiBotAceLocale-frFR.lua | 3 +++ Locales/MultiBotAceLocale-koKR.lua | 5 ++++- Locales/MultiBotAceLocale-ruRU.lua | 5 ++++- Locales/MultiBotAceLocale-zhCN.lua | 3 +++ README.md | 16 ++++++++++++- 10 files changed, 75 insertions(+), 5 deletions(-) diff --git a/Core/MultiBotComm.lua b/Core/MultiBotComm.lua index 6d4d4c3..ad85096 100644 --- a/Core/MultiBotComm.lua +++ b/Core/MultiBotComm.lua @@ -442,6 +442,37 @@ function Comm.RunFormationCommand(scope, target, formation, callback) return false end + if MultiBot and type(MultiBot.TimerAfter) == "function" then + MultiBot.TimerAfter(5.0, function() + local bridge = ensureBridgeState() + local pending = bridge.formationCommands[token] + if not pending then + return + end + + bridge.formationCommands[token] = nil + local result = { + status = "timeout", + scope = scope, + target = target, + token = token, + success = 0, + failure = 0, + formation = formation, + } + + if type(pending.callback) == "function" then + pending.callback(result) + end + + if MultiBot.OnFormationCommandApplied then + MultiBot.OnFormationCommandApplied(result) + end + + systemMessage(L("formation.confirm.timeout")) + end) + end + return token end @@ -1087,6 +1118,7 @@ function Comm.MarkDisconnected(reason) state.outfitCommands = {} state.trainerActive = nil state.trainerCommands = {} + state.formationCommands = {} state.formationQueryActive = nil end @@ -3340,10 +3372,10 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender) end if success <= 0 then - systemMessage(L("formation.confirm.none", "Formation was not applied to any grouped bot.")) + systemMessage(L("formation.confirm.none")) elseif failure > 0 then systemMessage(string.format( - L("formation.confirm.partial", "Formation applied to %d bot(s), failed for %d bot(s)."), + L("formation.confirm.partial"), success, failure )) diff --git a/Locales/MultiBotAceLocale-deDE.lua b/Locales/MultiBotAceLocale-deDE.lua index 06c4527..ab41baa 100644 --- a/Locales/MultiBotAceLocale-deDE.lua +++ b/Locales/MultiBotAceLocale-deDE.lua @@ -1034,6 +1034,9 @@ local deDEValues = { ["formation.query.empty"] = "Kein steuerbarer Bot in der Gruppe oder im Schlachtzug gefunden.", ["formation.query.mixed"] = "Formationsstatus: gemischt", ["formation.query.common"] = "Aktuelle Formation: %s (%d Bot(s))", + ["formation.confirm.none"] = "Die Formation wurde auf keinen Bot in der Gruppe oder im Schlachtzug angewendet.", + ["formation.confirm.partial"] = "Formation auf %d Bot(s) angewendet; bei %d Bot(s) fehlgeschlagen.", + ["formation.confirm.timeout"] = "Zeitüberschreitung beim Formationsbefehl.", ["formation.name.arrow"] = "Pfeil", ["formation.name.queue"] = "Kolonne", ["formation.name.near"] = "Nah", diff --git a/Locales/MultiBotAceLocale-enGB.lua b/Locales/MultiBotAceLocale-enGB.lua index 7696f20..cddf0bb 100644 --- a/Locales/MultiBotAceLocale-enGB.lua +++ b/Locales/MultiBotAceLocale-enGB.lua @@ -1037,6 +1037,9 @@ local enGBValues = { ["formation.query.empty"] = "No controllable bot found in the group or raid.", ["formation.query.mixed"] = "Formation state: mixed", ["formation.query.common"] = "Current formation: %s (%d bot(s))", + ["formation.confirm.none"] = "Formation was not applied to any bot in the group or raid.", + ["formation.confirm.partial"] = "Formation applied to %d bot(s); failed for %d bot(s).", + ["formation.confirm.timeout"] = "Formation command timed out.", ["formation.name.arrow"] = "Arrow", ["formation.name.queue"] = "Queue", ["formation.name.near"] = "Near", diff --git a/Locales/MultiBotAceLocale-enUS.lua b/Locales/MultiBotAceLocale-enUS.lua index c45eeee..249cb65 100644 --- a/Locales/MultiBotAceLocale-enUS.lua +++ b/Locales/MultiBotAceLocale-enUS.lua @@ -1037,6 +1037,9 @@ local enUSValues = { ["formation.query.empty"] = "No controllable bot found in the group or raid.", ["formation.query.mixed"] = "Formation state: mixed", ["formation.query.common"] = "Current formation: %s (%d bot(s))", + ["formation.confirm.none"] = "Formation was not applied to any bot in the group or raid.", + ["formation.confirm.partial"] = "Formation applied to %d bot(s); failed for %d bot(s).", + ["formation.confirm.timeout"] = "Formation command timed out.", ["formation.name.arrow"] = "Arrow", ["formation.name.queue"] = "Queue", ["formation.name.near"] = "Near", diff --git a/Locales/MultiBotAceLocale-esES.lua b/Locales/MultiBotAceLocale-esES.lua index d63b2e6..70a9b79 100644 --- a/Locales/MultiBotAceLocale-esES.lua +++ b/Locales/MultiBotAceLocale-esES.lua @@ -1035,6 +1035,9 @@ local esESValues = { ["formation.query.empty"] = "No se encontró ningún bot controlable en el grupo o la banda.", ["formation.query.mixed"] = "Estado de las formaciones: mixto", ["formation.query.common"] = "Formación actual: %s (%d bot(s))", + ["formation.confirm.none"] = "La formación no se aplicó a ningún bot del grupo o la banda.", + ["formation.confirm.partial"] = "Formación aplicada a %d bot(s); falló para %d bot(s).", + ["formation.confirm.timeout"] = "La orden de formación agotó el tiempo de espera.", ["formation.name.arrow"] = "Flecha", ["formation.name.queue"] = "Fila", ["formation.name.near"] = "Cercana", diff --git a/Locales/MultiBotAceLocale-frFR.lua b/Locales/MultiBotAceLocale-frFR.lua index 4e8823a..8017bd5 100644 --- a/Locales/MultiBotAceLocale-frFR.lua +++ b/Locales/MultiBotAceLocale-frFR.lua @@ -1034,6 +1034,9 @@ local frFRValues = { ["formation.query.empty"] = "Aucun bot contrôlable trouvé dans le groupe ou le raid.", ["formation.query.mixed"] = "État des formations : mixte", ["formation.query.common"] = "Formation actuelle : %s (%d bot(s))", + ["formation.confirm.none"] = "La formation n'a été appliquée à aucun bot du groupe ou du raid.", + ["formation.confirm.partial"] = "Formation appliquée à %d bot(s) ; échec pour %d bot(s).", + ["formation.confirm.timeout"] = "La commande de formation a expiré.", ["formation.name.arrow"] = "Flèche", ["formation.name.queue"] = "File", ["formation.name.near"] = "Rapprochée", diff --git a/Locales/MultiBotAceLocale-koKR.lua b/Locales/MultiBotAceLocale-koKR.lua index 35a40e4..504f470 100644 --- a/Locales/MultiBotAceLocale-koKR.lua +++ b/Locales/MultiBotAceLocale-koKR.lua @@ -1021,11 +1021,14 @@ local koKRValues = { ["tips.outfits.equip"] = "왼쪽 클릭: 장착\n오른쪽 클릭: 바꾸기", -- Chatless formation query ["formation.query.title"] = "현재 포메이션", - ["formation.query.unavailable"] = "Bridge를 사용할 수 없습니다.", + ["formation.query.unavailable"] = "브릿지를 사용할 수 없습니다.", ["formation.query.timeout"] = "포메이션 조회 시간이 초과되었습니다.", ["formation.query.empty"] = "파티 또는 공격대에서 제어 가능한 봇을 찾을 수 없습니다.", ["formation.query.mixed"] = "포메이션 상태: 혼합", ["formation.query.common"] = "현재 포메이션: %s (봇 %d명)", + ["formation.confirm.none"] = "파티 또는 공격대의 어떤 봇에도 포메이션을 적용하지 못했습니다.", + ["formation.confirm.partial"] = "봇 %d명에게 포메이션을 적용했고, %d명에게는 적용하지 못했습니다.", + ["formation.confirm.timeout"] = "포메이션 명령 시간이 초과되었습니다.", ["formation.name.arrow"] = "화살표", ["formation.name.queue"] = "대기열", ["formation.name.near"] = "근거리", diff --git a/Locales/MultiBotAceLocale-ruRU.lua b/Locales/MultiBotAceLocale-ruRU.lua index 9bc9986..fce29e0 100644 --- a/Locales/MultiBotAceLocale-ruRU.lua +++ b/Locales/MultiBotAceLocale-ruRU.lua @@ -1035,9 +1035,12 @@ local ruRUValues = { ["formation.query.empty"] = "В группе или рейде нет доступных для управления ботов.", ["formation.query.mixed"] = "Состояние построений: смешанное", ["formation.query.common"] = "Текущее построение: %s (%d бот(ов))", + ["formation.confirm.none"] = "Построение не применено ни к одному боту в группе или рейде.", + ["formation.confirm.partial"] = "Построение применено к %d бот(ам); не удалось применить к %d бот(ам).", + ["formation.confirm.timeout"] = "Время ожидания команды построения истекло.", ["formation.name.arrow"] = "Клин", ["formation.name.queue"] = "Колонна", - ["formation.name.near"] = "Рядом", + ["formation.name.near"] = "Скученное построение", ["formation.name.melee"] = "Ближний бой", ["formation.name.line"] = "Шеренга", ["formation.name.circle"] = "Круг", diff --git a/Locales/MultiBotAceLocale-zhCN.lua b/Locales/MultiBotAceLocale-zhCN.lua index 1f7af17..27b451b 100644 --- a/Locales/MultiBotAceLocale-zhCN.lua +++ b/Locales/MultiBotAceLocale-zhCN.lua @@ -1035,6 +1035,9 @@ local zhCNValues = { ["formation.query.empty"] = "小队或团队中没有可控制的机器人。", ["formation.query.mixed"] = "阵型状态:混合", ["formation.query.common"] = "当前阵型:%s(%d 个机器人)", + ["formation.confirm.none"] = "阵型未应用到小队或团队中的任何机器人。", + ["formation.confirm.partial"] = "阵型已应用到 %d 个机器人,%d 个机器人应用失败。", + ["formation.confirm.timeout"] = "阵型命令超时。", ["formation.name.arrow"] = "箭头", ["formation.name.queue"] = "队列", ["formation.name.near"] = "近距离", diff --git a/README.md b/README.md index 3782c90..63a279a 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ This fork focuses on removing automatic bot chat spam from the main UI refresh p The addon now requests structured data from the server through `mod-multibot-bridge`. -Examples of bridge requests: +Examples of bridge request families (arguments omitted here for readability): ```text MBOT HELLO @@ -111,6 +111,20 @@ RUN~LOOT RUN~FORMATION ``` +The Formation family uses the following complete party/raid-wide contracts: + +```text +RUN~FORMATION~GROUP~~~ +FORMATION_ACK~GROUP~~~~~ + +GET~FORMATIONS~GROUP~~ +FORMATIONS_BEGIN~~ +FORMATIONS_ITEM~~~ +FORMATIONS_END~~ +``` + +`GROUP` covers every controllable bot in the player's current party or raid. It does not target individual raid subgroups. + Manual playerbot commands are still intentionally preserved for diagnostics and gameplay actions. Commands such as: