feat: send CMSG_SET_ACTION_BUTTON to server when action bar slot changes

Action bar changes (dragging spells/items) were only saved locally.
Now notifies the server via CMSG_SET_ACTION_BUTTON so the layout
persists across relogs. Supports Classic (5-byte) and TBC/WotLK
(packed uint32) wire formats.
This commit is contained in:
Kelsi 2026-03-13 04:25:05 -07:00
parent 8f08d75748
commit 4272491d56
3 changed files with 61 additions and 0 deletions

View file

@ -947,6 +947,21 @@ public:
static network::Packet build(uint8_t state);
};
// ============================================================
// Action Bar
// ============================================================
/** CMSG_SET_ACTION_BUTTON packet builder */
class SetActionButtonPacket {
public:
// button: 0-based slot index
// type: ActionBarSlot::Type (SPELL=0, ITEM=1, MACRO=2, EMPTY=0)
// id: spellId, itemId, or macroId (0 to clear)
// isClassic: true for Vanilla/Turtle format (5-byte payload),
// false for TBC/WotLK (5-byte packed uint32)
static network::Packet build(uint8_t button, uint8_t type, uint32_t id, bool isClassic);
};
// ============================================================
// Display Toggles
// ============================================================

View file

@ -16101,6 +16101,16 @@ void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t
queryItemInfo(id, 0);
}
saveCharacterConfig();
// Notify the server so the action bar persists across relogs.
if (state == WorldState::IN_WORLD && socket) {
const bool classic = isClassicLikeExpansion();
auto pkt = SetActionButtonPacket::build(
static_cast<uint8_t>(slot),
static_cast<uint8_t>(type),
id,
classic);
socket->send(pkt);
}
}
float GameHandler::getSpellCooldown(uint32_t spellId) const {

View file

@ -1905,6 +1905,42 @@ network::Packet StandStateChangePacket::build(uint8_t state) {
return packet;
}
// ============================================================
// Action Bar
// ============================================================
network::Packet SetActionButtonPacket::build(uint8_t button, uint8_t type, uint32_t id, bool isClassic) {
// Classic/Turtle (1.12): uint8 button + uint16 id + uint8 type + uint8 misc(0)
// type encoding: 0=spell, 1=item, 64=macro
// TBC/WotLK: uint8 button + uint32 packed (type<<24 | id)
// type encoding: 0x00=spell, 0x80=item, 0x40=macro
// packed=0 means clear the slot
network::Packet packet(wireOpcode(Opcode::CMSG_SET_ACTION_BUTTON));
packet.writeUInt8(button);
if (isClassic) {
// Classic: 16-bit id, 8-bit type code, 8-bit misc
// Map ActionBarSlot::Type (0=EMPTY,1=SPELL,2=ITEM,3=MACRO) → classic type byte
uint8_t classicType = 0; // 0 = spell
if (type == 2 /* ITEM */) classicType = 1;
if (type == 3 /* MACRO */) classicType = 64;
packet.writeUInt16(static_cast<uint16_t>(id));
packet.writeUInt8(classicType);
packet.writeUInt8(0); // misc
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (Classic): button=", (int)button,
" id=", id, " type=", (int)classicType);
} else {
// TBC/WotLK: type in bits 2431, id in bits 023; packed=0 clears slot
uint8_t packedType = 0x00; // spell
if (type == 2 /* ITEM */) packedType = 0x80;
if (type == 3 /* MACRO */) packedType = 0x40;
uint32_t packed = (id == 0) ? 0 : (static_cast<uint32_t>(packedType) << 24) | (id & 0x00FFFFFF);
packet.writeUInt32(packed);
LOG_DEBUG("Built CMSG_SET_ACTION_BUTTON (TBC/WotLK): button=", (int)button,
" packed=0x", std::hex, packed, std::dec);
}
return packet;
}
// ============================================================
// Display Toggles
// ============================================================