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: