-
Notifications
You must be signed in to change notification settings - Fork 2
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", followed by an immediate roster sync (GET~ROSTER/GET~DETAILS/GET~STATES) 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). -
Reset — state returns to
unknownat the firstPLAYER_ENTERING_WORLDof a session;PLAYER_LOGOUTclears the session flag so the next login is treated as fresh.
On a fresh login (not /reload), loginPhaseActive blocks whisper probing until
detection resolves — bots may not be in-world yet. Once detection resolves absent, the probe
sweep runs and records each probed-but-unconfirmed member as a joinCandidate. A bot that
wasn't in-world for its probe announces itself once loaded by whispering the player; that
unsolicited whisper (any text — greetings vary by playerbots version) re-sends co ?, and the
Strategies: reply confirms it. This replaces the old exact-Hello! match.
RUN~<OPCODE>~BOT~<botName>~~<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 |
Queries (co ?, nc ?, items, quests all, stats, talents spec list) are never
allowlisted, so they always whisper and their replies arrive via CHAT_MSG_WHISPER as usual.
| Packet | Purpose | Reply packets |
|---|---|---|
GET~ROSTER |
Bot names in the group | ROSTER~ |
GET~DETAILS |
Per-bot identity/class | DETAIL~ |
GET~STATES |
Per-bot strategy snapshot | STATE~ |
GET~INVENTORY~<botName>~inv |
Bot's bag contents + money |
INV_BEGIN~ / INV_SUMMARY~ / INV_ITEM~ / INV_END~
|
GET~QUESTS~ALL~<botName>~quests |
Bot's quest log |
QUESTS_BEGIN~ / QUESTS_ITEM~ / QUESTS_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) |
ROSTER~<name>,… |
name up to first comma | Seeds a minimal bot entry if unknown |
DETAIL~<name>~?~?~<class>~… |
name + class | Establishes identity/class; preserves strategy data already parsed from STATE~
|
STATE~<name>~<combat>~<nonCombat> |
comma-separated strategy lists | Stored via CB_StoreCombat / CB_StoreNonCombat; creates a minimal entry if STATE~ beats ROSTER~
|
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 |
QUESTS_BEGIN~<name>~<token>~<mode> |
— | Resets entry.quests
|
QUESTS_ITEM~<name>~<token>~<mode>~<status>~<questID>~<questName> |
status C/I; name URL-encoded |
Appended as { id, status }
|
QUESTS_END~<name>~<token>~<mode> |
— | Renders if the quest frame is open |
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