Repository navigation
Bridge Protocol
Developer reference for the MBOT addon-message protocol between CleanBot and the
server-side mod-multibot-bridge
module. The client side lives in Bridge.lua; the bot-command survey is in
playerbot-commands.md.
All packets travel as addon messages with prefix MBOT:
SendAddonMessage("MBOT", msg, channel)CB_SendBridge(msg) in Bridge.lua picks the channel:
| Situation | Channel |
|---|---|
In a raid (GetNumRaidMembers() > 0) |
"RAID" — "PARTY" does not reach raid members |
| In a party | "PARTY" |
Solo with self-bot active (NS.selfBotActive) |
"WHISPER" to the player's own name |
| Solo, no self-bot | no-op (nothing to talk to) |
The self-whisper works because the server bridge replies directly to the sender
(player->SendDirectMessage) and its chat hook fires on the whisper overload regardless
of recipient — so a solo player can complete the handshake and carry all traffic.
Server-side, RUN~ commands route through ExecuteSilentBotCommand(), which calls
botAI->HandleCommand(CHAT_MSG_WHISPER, command, requester) — identical to a whisper,
but with no whisper log spam.
NS.bridgeState is "unknown" until detection resolves, then "present" or "absent".
-
Trigger —
CB_StartBridgeDetection()runs atPLAYER_ENTERING_WORLD(with retries at +1/+3/+6 s for late-loading rosters), onPARTY_MEMBERS_CHANGED/RAID_ROSTER_UPDATEwhile state is stillunknown, and when self-bot is enabled. It needs either a group or an active self-bot, and is idempotent. -
Probe — client sends
HELLO~1. -
Resolve — a reply starting
HELLO_ACK~⇒bridgeState = "present", accompanied by capability broadcast packets (CAPS_BEGIN,CAPS~<list>,CAPS_END), followed by an immediate debounced roster sync (GET~ROSTER/GET~DETAILS/GET~STATESorGET~STATES~<token>) and a linked-accounts fetch. No ack within 3 s ⇒bridgeState = "absent", and discovery falls back to whisper probing (co ?to each group member; only bots reply with aStrategies:line).
RUN~<OPCODE>~BOT~<botName>~<token>~<command>
Sent by NS.CB_SendBotCommand only when the effective bridge state is present AND
the command matches an opcode allowlist (CB_GetBridgeOpcode); everything else whispers.
Allowlists mirror the server's IsAllowed*() checks — keep Bridge.lua in sync with
MultiBotBridge.cpp when the bridge updates.
| Opcode | Allowed commands | Notes |
|---|---|---|
COMBAT |
co +/- for: focus, dps assist, aoe, dps aoe, tank assist, avoid aoe, save mana, threat, behind, wait for attack — plus wait for attack time <N> (N = 0–60, no co prefix) |
Matched case-insensitively |
POSITION |
disperse disable, disperse set <N> (0 < N ≤ 100) |
Allowlisted plumbing — no CleanBot UI sends these yet |
LOOT |
nc +loot, nc -loot, ll all/normal/gray/quest/skill
|
Case-sensitive on the server |
RTI |
rti <icon>, rti cc <icon> (STAR/CIRCLE/DIAMOND/TRIANGLE/MOON/SQUARE/CROSS/SKULL), attack rti target, pull rti target
|
Allowlisted plumbing — no CleanBot UI sends these yet |
Not routed via CB_SendBotCommand; bridge when present with exact bag/slot coordinates, whisper only when absent:
| Packet | Capability | Reply | Notes |
|---|---|---|---|
RUN~ITEM_ACTION~<bot>~<token>~SELL_GREY~0~0 |
INVENTORY_BULK_SELL_V1 |
INVENTORY_ITEM_ACTION~<bot>~<token>~SELL_GREY~<itemId>~<OK/ERR>~<reason>~<moved> |
Bulk "Sell Trash" (CB_BridgeBulkSell / CB_BridgeGroupBulkSell); whisper s gray only when absent |
RUN~ITEM_ACTION~<bot>~<token>~BANK_WITHDRAW~<itemId>~<count> |
— (matched by itemId+count, no coordinates) | INVENTORY_ITEM_ACTION~<bot>~<token>~BANK_WITHDRAW~<itemId>~<OK/ERR>~<reason>~<moved> |
Bank withdraw (CB_BridgeWithdrawItem); whisper bank -<link> only when absent |
RUN~ITEM_SELL~<bot>~<token>~<bag>~<slot>~<itemId>~<count> |
ITEM_SELL_SINGLE_V1 |
INVENTORY_ITEM_SELL~<bot>~<token>~<OK/ERR>~<reason>~<bag>~<slot>~<itemId>~<sold> |
Single-item vendor sell (CB_BridgeSellItem, wired into CB_DoSell); whisper s <link> only when absent |
RUN~ITEM_EQUIP~<bot>~<token>~<bag>~<slot>~<itemId>~<count> |
ITEM_EQUIP_V1 |
INVENTORY_ITEM_EQUIP~ |
Equip (CB_BridgeEquipItem); whisper e <link> only when absent |
RUN~ITEM_USE~<bot>~<token>~<bag>~<slot>~<itemId>~<count> |
ITEM_USE_V1 |
INVENTORY_ITEM_USE~ |
Use (CB_BridgeUseItem); whisper u <link> only when absent |
RUN~ITEM_DESTROY~<bot>~<token>~<bag>~<slot>~<itemId>~<count> |
ITEM_DESTROY_V1 |
INVENTORY_ITEM_DESTROY~ |
Destroy (CB_BridgeDestroyItem); whisper destroy <link> only when absent |
RUN~ITEM_DEPOSIT_EXACT~<bot>~<token>~BANK_DEPOSIT|GBANK_DEPOSIT~<bag>~<slot>~<itemId>~<count> |
ITEM_DEPOSIT_EXACT_V1 |
ITEM_DEPOSIT_EXACT~ |
Deposit to personal / guild bank (CB_BridgeDepositItem); whisper bank <link> / guild bank <link> only when absent |
RUN~FORMATION~GROUP~~<token>~<formation> |
— | FORMATION_ACK~ |
Set group formation (8 tokens: arrow, queue, near, melee, line, circle, chaos, shield); far and absent fallback to PARTY/RAID
|
RUN~CRAFT_RECIPE~<bot>~<token>~<skillId>~<spellId>~<itemId> |
— | PROFESSION_RECIPE_CRAFT~ |
Craft recipe without item target (CB_BridgeCraftRecipe) |
RUN~CRAFT_RECIPE_TARGET~<token>~<bot>~<skillId>~<spellId>~<bag>~<slot>~<itemId> |
— | CRAFT_RECIPE_TARGET_RESULT~ |
Craft recipe targeting inventory or equipment slot (CB_BridgeCraftRecipeTarget; bag 255 for paperdoll) |
Queries (co ?, nc ?, ll ?, plus items / quests all / bank / stats / formation ? / talents spec list when bridge is absent) are never
allowlisted, so they whisper and their replies arrive via CHAT_MSG_WHISPER as usual. When bridge is present, stats routes cleanly via GET~STATS~<botName>, formations route via GET~FORMATIONS~GROUP~~<token>, and premade specs route via GET~TALENT_SPEC_LIST~<botName>~<token>.
| Packet | Purpose | Reply packets |
|---|---|---|
GET~ROSTER |
Bot names in the group | ROSTER~ |
GET~DETAILS |
Per-bot identity/class | DETAIL~ |
GET~STATES~<token> |
Framed strategy snapshot (STATE_FRAMING_V1 capable) |
STATES_BEGIN~ / STATE_BEGIN~ / STATE_ITEM~ / STATE_END~ / STATES_END~ / STATE_ABORT~
|
GET~STATES |
Per-bot strategy snapshot (legacy fallback) | STATE~ |
GET~INVENTORY~<botName>~inv |
Bot's bag contents + money |
INV_BEGIN~ / INV_SUMMARY~ / INV_ITEM~ / INV_END~
|
GET~INVENTORY_EXACT~<botName>~<token> |
Bot's bag contents with exact coordinates (INVENTORY_EXACT_V1) |
INV_EXACT_BEGIN~ / INV_BAG~ / INV_ITEM_LOC~ / INV_EXACT_END~
|
GET~BANK~<botName>~<token> |
Bot's bank contents |
BANK_BEGIN~ / BANK_ITEM~ / BANK_ERROR~ / BANK_END~
|
GET~SPELLBOOK~<botName>~<token> |
Bot's spellbook |
SB_BEGIN~ / SB_ITEM~ / SB_END~ (SPELLBOOK_* alias) |
GET~QUESTS~ALL~<botName>~quests |
Bot's quest log |
QUESTS_BEGIN~ / QUESTS_ITEM~ / QUESTS_END~
|
GET~STATS~<botName> |
Bot stats (level, money, bags, durability, XP, mana) | STATS~ |
GET~FORMATIONS~GROUP~~<token> |
Bot movement formations for all group bots |
FORMATIONS_BEGIN~ / FORMATIONS_ITEM~ / FORMATIONS_END~
|
GET~TALENT_SPEC_LIST~<botName>~<token> |
Premade talent specs for bot's class and level |
TALENT_SPEC_BEGIN~ / TALENT_SPEC_CURRENT~ / TALENT_SPEC_ITEM~ / TALENT_SPEC_END~
|
GET~PROFESSION~<botName> |
Bot's learned professions and current/max skill ranks | PROFESSION~ |
GET~PROFESSION_RECIPES~<botName>~<skillId>~<token> |
Recipes learned by bot for specified skillId |
PROFESSION_RECIPES_BEGIN~ / PROFESSION_RECIPES_ITEM~ / PROFESSION_RECIPES_END~
|
GET~ROSTER/DETAILS/STATES are debounced: CB_RequestSync (0.5 s, all three) and
CB_RequestStates (0.4 s, states only — silent strategy reconciliation after a toggle).
Parsed in Bridge.lua's event handler. Fields are ~-separated; NS.CB_SplitOnce walks
them. <token> fields are request-correlation echoes and are skipped on parse.
| Packet | Layout | Handling |
|---|---|---|
HELLO_ACK~… |
— | Drives the real state machine (see below) |
CAPS_BEGIN |
— | Resets capabilities table and initiates capability batch |
CAPS~<c1>,<c2>,… |
comma-separated capabilities | Populates NS.capabilities; detects STATE_FRAMING_V1 (NS.stateFramingCapable) |
CAPS_END |
— | Marks NS.capabilitiesResolved = true
|
ROSTER~<rec>;<rec>;… |
one record per bot: <name>,<classId>,<level>,<mapId>,<alive>,<hp%>,<mana%> (classId = numeric Player::getClass()) |
Seeds a minimal entry (name + class) for each unknown bot |
DETAIL~<name>~?~?~<class>~… |
name + class | Establishes identity/class; preserves strategy data already parsed |
STATES_BEGIN~<token>~<botCount> |
starts global state sync | Initializes transaction tracker for <token> with expected bot count |
STATE_BEGIN~<token>~<name>~<cCount>~<nCount> |
starts bot state stream | Opens bot strategy accumulation buffer |
STATE_ITEM~<token>~<name>~<scope>~<idx>~<strat> |
single strategy (scope: C=combat, N=normal) |
Stored at index in buffer |
STATE_END~<token>~<name>~<cCount>~<nCount> |
ends bot state stream | Assembles strategies, calls CB_StoreCombat / CB_StoreNonCombat and refreshes |
STATES_END~<token>~<sentCount> |
ends global state sync | Clears request token, refreshes tabs |
STATE_ABORT~<token>~<name>~<reason> |
abort notification | Clears active buffers and request token |
STATE~<name>~<combat>~<nonCombat> |
comma-separated strategy lists (legacy fallback) | Stored via CB_StoreCombat / CB_StoreNonCombat
|
INV_BEGIN~<name>~… |
— | Resets entry.inventory = { items = {} }
|
INV_SUMMARY~<name>~<token>~<gold>~<silver>~<copper>~<bagUsed>~<bagTotal> |
money + bag counts | Bag is used/total (the whisper-path stats reply is free/total — converted on parse) |
INV_ITEM~<name>~<token>~<encodedItem> |
one item per packet | Decoded by NS.CB_ParseItemLine
|
INV_END~<name> |
— | Clears the in-flight flag; renders if the inventory frame is open |
STATS~<name>~<level>~<gold>~<silver>~<copper>~<bagUsed>~<bagTotal>~<durPct>~<xpPct>~<manaPct> |
bot stats snapshot | Populates level, money, bag totals, durability, and XP; refreshes inventory and paperdoll XP bar |
FORMATIONS_BEGIN~<token>~<count> |
— | Marks start of formations batch |
FORMATIONS_ITEM~<token>~<encodedBotName>~<encodedFormation> |
per-bot formation snapshot | Updates entry.formation for bot; clears awaitingFormation unconditionally |
FORMATIONS_END~<token>~<sentCount> |
— | Clears formationsPending and refreshes UI controls |
FORMATION_ACK~<scope>~<target>~<token>~<succeeded>~<failed>~<formation> |
formation change acknowledgement | Updates group members' formation if succeeded > 0; triggers re-fetch on failure |
QUESTS_BEGIN~<name>~<token>~<mode> |
— | Resets entry.quests
|
QUESTS_ITEM~<name>~<token>~<mode>~<status>~<questID>~<questName> |
status C/I; name URL-encoded — but the current bridge fills it with the questID again (SendQuestPacketsForBot) |
Appended as { id, status, name }; name kept only when the field differs from the id (a real title), since quest Abandon must drop by title |
QUESTS_END~<name>~<token>~<mode> |
— | Renders if the quest frame is open |
INV_EXACT_BEGIN~<name>~<token> |
— | Resets exact inventory staging |
INV_BAG~<name>~<token>~<kind>~<bag>~<slotStart>~<slotCount>~<bagItemId> |
bag layout | Accumulates exact bag totals |
INV_ITEM_LOC~<name>~<token>~<bag>~<slot>~<itemId>~<count>~<soulbound> |
one item with coordinates | Staged with bag/slot/itemId |
INV_EXACT_END~<name>~<token> |
— | Finalizes exact inventory and renders |
BANK_BEGIN~<name>~<token> |
— | Resets bank staging |
BANK_ITEM~<name>~<token>~<item> |
one bank item | Staged into bank list |
BANK_ERROR~<name>~<token>~<reason> |
error reason | Surfaces banker / guild-bank popup |
BANK_END~<name>~<token> |
— | Finalizes bank list and renders |
SB_BEGIN~<name>~<token> (SPELLBOOK_BEGIN~ alias) |
— | Resets spellbook staging |
SB_ITEM~<name>~<token>~<spell> (SPELLBOOK_ITEM~ alias) |
one spell | Staged into spellbook list |
SB_END~<name>~<token> (SPELLBOOK_END~ alias) |
— | Sorts and renders spellbook |
INVENTORY_ITEM_ACTION~<bot>~<token>~SELL_GREY~<itemId>~<OK/ERR>~<reason>~<moved> |
bulk-sell result | Aggregates group-sell totals |
INVENTORY_ITEM_SELL~<bot>~<token>~<OK/ERR>~<reason>~<bag>~<slot>~<itemId>~<sold> |
single-sell result | Reconciles inventory |
INVENTORY_ITEM_EQUIP~<bot>~<token>~<OK/ERR>~<reason> |
equip result | Refreshes inventory and paperdoll |
INVENTORY_ITEM_USE~<bot>~<token>~<OK/ERR>~<reason> |
use result | Reconciles inventory |
INVENTORY_ITEM_DESTROY~<bot>~<token>~<OK/ERR>~<reason> |
destroy result | Reconciles inventory |
ITEM_DEPOSIT_EXACT~<bot>~<token>~<status>~<reason>~<action>~<bag>~<slot>~<itemId>~<count>~<moved> |
deposit result | Reconciles inventory and bank |
PROFESSION~<botName>~<profs> |
Semicolon-separated list: <profKey>:<cur>/<max>;...
|
Updates entry.professions, triggers CB_OnProfessionsUpdated
|
PROFESSION_RECIPES_BEGIN~<bot>~<token>~<skillId> |
— | Resets profession recipe staging buffer |
PROFESSION_RECIPES_ITEM~<bot>~<token>~<skillId>~<spellId>~<itemId>~<difficulty>~<craftable>~<materials> |
Recipe metadata + reagents (<matId>:<reqCount>:<availCount>;...). Name, icon, and subType are client-resolved via GetSpellInfo/GetItemInfo
|
Staged into recipe list |
PROFESSION_RECIPES_END~<bot>~<token>~<skillId> |
— | Finalizes recipe cache and renders |
PROFESSION_RECIPE_CRAFT~<bot>~<token>~<skillId>~<spellId>~<itemId>~<OK/ERR>~<reason> |
Craft dispatch result | Confirms craft start or reports error; starts craft watcher |
CRAFT_RECIPE_TARGET_RESULT~<token>~<bot>~<OK/ERR>~<reason>~<skillId>~<spellId>~<bag>~<slot>~<itemId> |
Target craft result | Confirms enchant start or reports error; starts craft watcher |
CB_EffectiveBridgeState() (return NS.debugBridgeOverride or NS.bridgeState) is the
single decision point that makes /cbdebug bridge on|off|reset work (see
debug-tools.md). When adding bridge traffic:
-
Bot commands → send via
NS.CB_SendBotCommand(already honors simulate mode and the override). Never callSendChatMessage/SendAddonMessagedirectly for these. -
Raw
GET~/protocol traffic → gate the present branch onCB_EffectiveBridgeState() == "present", not rawNS.bridgeState, and provide a whisper fallback where one exists (mirrorCB_FetchInventory/CB_FetchQuests). -
New inbound handlers → add them below the
elseif CB_EffectiveBridgeState() ~= "present" then returnguard so data packets are dropped when the override forces no-bridge. OnlyHELLO_ACK~runs above the guard, because it drives the real state machine sobridge resetcan restore the truth.
Lifecycle reads (detection guard/timeout, HELLO_ACK → present, roster-event trigger,
login/logout resets) intentionally stay on raw NS.bridgeState so the true state is
always tracked underneath the override.
User Guide
Reference
Other