From 284b98d93a5ad06273a7fc342872146ffe868980 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 19:15:52 -0700 Subject: [PATCH] feat: implement pet stable system (MSG_LIST_STABLED_PETS, CMSG_STABLE_PET, CMSG_UNSTABLE_PET) - Parse MSG_LIST_STABLED_PETS (SMSG): populate StabledPet list with petNumber, entry, level, name, displayId, and active status - Detect stable master via gossip option text/keyword matching and auto-send MSG_LIST_STABLED_PETS request to open the stable UI - Refresh list automatically after SMSG_STABLE_RESULT to reflect state - New packet builders: ListStabledPetsPacket, StablePetPacket, UnstablePetPacket - New public API: requestStabledPetList(), stablePet(slot), unstablePet(petNumber) - Stable window UI: shows active/stabled pets with store/retrieve buttons, slot count, refresh, and close; opens when server sends pet list - Clear stable state on world logout/disconnect --- include/game/game_handler.hpp | 25 +++++++ include/game/world_packets.hpp | 19 ++++++ include/ui/game_screen.hpp | 1 + src/game/game_handler.cpp | 98 +++++++++++++++++++++++++++ src/game/world_packets.cpp | 24 +++++++ src/ui/game_screen.cpp | 118 +++++++++++++++++++++++++++++++++ 6 files changed, 285 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 573b3f44..87f35809 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -634,6 +634,24 @@ public: void sendPetAction(uint32_t action, uint64_t targetGuid = 0); const std::unordered_set& getKnownSpells() const { return knownSpells; } + // ---- Pet Stable ---- + struct StabledPet { + uint32_t petNumber = 0; // server-side pet number (used for unstable/swap) + uint32_t entry = 0; // creature entry ID + uint32_t level = 0; + std::string name; + uint32_t displayId = 0; + bool isActive = false; // true = currently summoned/active slot + }; + bool isStableWindowOpen() const { return stableWindowOpen_; } + void closeStableWindow() { stableWindowOpen_ = false; } + uint64_t getStableMasterGuid() const { return stableMasterGuid_; } + uint8_t getStableSlots() const { return stableNumSlots_; } + const std::vector& getStabledPets() const { return stabledPets_; } + void requestStabledPetList(); // CMSG MSG_LIST_STABLED_PETS + void stablePet(uint8_t slot); // CMSG_STABLE_PET (store active pet in slot) + void unstablePet(uint32_t petNumber); // CMSG_UNSTABLE_PET (retrieve to active) + // Player proficiency bitmasks (from SMSG_SET_PROFICIENCY) // itemClass 2 = Weapon (subClassMask bits: 0=Axe1H,1=Axe2H,2=Bow,3=Gun,4=Mace1H,5=Mace2H,6=Polearm,7=Sword1H,8=Sword2H,10=Staff,13=Fist,14=Misc,15=Dagger,16=Thrown,17=Crossbow,18=Wand,19=Fishing) // itemClass 4 = Armor (subClassMask bits: 1=Cloth,2=Leather,3=Mail,4=Plate,6=Shield) @@ -2390,6 +2408,13 @@ private: std::vector petSpellList_; // known pet spells std::unordered_set petAutocastSpells_; // spells with autocast on + // ---- Pet Stable ---- + bool stableWindowOpen_ = false; + uint64_t stableMasterGuid_ = 0; + uint8_t stableNumSlots_ = 0; + std::vector stabledPets_; + void handleListStabledPets(network::Packet& packet); + // ---- Battleground queue state ---- std::array bgQueues_{}; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 24d795f7..2bb89907 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2699,5 +2699,24 @@ public: static bool parse(network::Packet& packet, AuctionCommandResult& data); }; +/** Pet Stable packet builders */ +class ListStabledPetsPacket { +public: + /** MSG_LIST_STABLED_PETS (CMSG): request list from stable master */ + static network::Packet build(uint64_t stableMasterGuid); +}; + +class StablePetPacket { +public: + /** CMSG_STABLE_PET: store active pet in the given stable slot (1-based) */ + static network::Packet build(uint64_t stableMasterGuid, uint8_t slot); +}; + +class UnstablePetPacket { +public: + /** CMSG_UNSTABLE_PET: retrieve a stabled pet by its server-side petNumber */ + static network::Packet build(uint64_t stableMasterGuid, uint32_t petNumber); +}; + } // namespace game } // namespace wowee diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 65a0f7ce..69e0ff44 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -344,6 +344,7 @@ private: void renderQuestOfferRewardWindow(game::GameHandler& gameHandler); void renderVendorWindow(game::GameHandler& gameHandler); void renderTrainerWindow(game::GameHandler& gameHandler); + void renderStableWindow(game::GameHandler& gameHandler); void renderTaxiWindow(game::GameHandler& gameHandler); void renderDeathScreen(game::GameHandler& gameHandler); void renderReclaimCorpseButton(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index aaf340f8..3f8183ad 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2070,6 +2070,11 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } + // ---- Pet stable list ---- + case Opcode::MSG_LIST_STABLED_PETS: + if (state == WorldState::IN_WORLD) handleListStabledPets(packet); + break; + // ---- Pet stable result ---- case Opcode::SMSG_STABLE_RESULT: { // uint8 result @@ -2086,6 +2091,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } if (msg) addSystemChatMessage(msg); LOG_INFO("SMSG_STABLE_RESULT: result=", static_cast(result)); + // Refresh the stable list after a result to reflect the new state + if (stableWindowOpen_ && stableMasterGuid_ != 0 && socket && result <= 0x08) { + auto refreshPkt = ListStabledPetsPacket::build(stableMasterGuid_); + socket->send(refreshPkt); + } break; } @@ -6916,6 +6926,10 @@ void GameHandler::selectCharacter(uint64_t characterGuid) { unitAurasCache_.clear(); unitCastStates_.clear(); petGuid_ = 0; + stableWindowOpen_ = false; + stableMasterGuid_ = 0; + stableNumSlots_ = 0; + stabledPets_.clear(); playerXp_ = 0; playerNextLevelXp_ = 0; serverPlayerLevel_ = 1; @@ -14622,6 +14636,78 @@ void GameHandler::dismissPet() { socket->send(packet); } +void GameHandler::requestStabledPetList() { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return; + auto pkt = ListStabledPetsPacket::build(stableMasterGuid_); + socket->send(pkt); + LOG_INFO("Sent MSG_LIST_STABLED_PETS to npc=0x", std::hex, stableMasterGuid_, std::dec); +} + +void GameHandler::stablePet(uint8_t slot) { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0) return; + if (petGuid_ == 0) { + addSystemChatMessage("You do not have an active pet to stable."); + return; + } + auto pkt = StablePetPacket::build(stableMasterGuid_, slot); + socket->send(pkt); + LOG_INFO("Sent CMSG_STABLE_PET: slot=", static_cast(slot)); +} + +void GameHandler::unstablePet(uint32_t petNumber) { + if (state != WorldState::IN_WORLD || !socket || stableMasterGuid_ == 0 || petNumber == 0) return; + auto pkt = UnstablePetPacket::build(stableMasterGuid_, petNumber); + socket->send(pkt); + LOG_INFO("Sent CMSG_UNSTABLE_PET: petNumber=", petNumber); +} + +void GameHandler::handleListStabledPets(network::Packet& packet) { + // SMSG MSG_LIST_STABLED_PETS: + // uint64 stableMasterGuid + // uint8 petCount + // uint8 numSlots + // per pet: + // uint32 petNumber + // uint32 entry + // uint32 level + // string name (null-terminated) + // uint32 displayId + // uint8 isActive (1 = active/summoned, 0 = stabled) + constexpr size_t kMinHeader = 8 + 1 + 1; + if (packet.getSize() - packet.getReadPos() < kMinHeader) { + LOG_WARNING("MSG_LIST_STABLED_PETS: packet too short (", packet.getSize(), ")"); + return; + } + stableMasterGuid_ = packet.readUInt64(); + uint8_t petCount = packet.readUInt8(); + stableNumSlots_ = packet.readUInt8(); + + stabledPets_.clear(); + stabledPets_.reserve(petCount); + + for (uint8_t i = 0; i < petCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 4 + 4 + 4) break; + StabledPet pet; + pet.petNumber = packet.readUInt32(); + pet.entry = packet.readUInt32(); + pet.level = packet.readUInt32(); + pet.name = packet.readString(); + if (packet.getSize() - packet.getReadPos() < 4 + 1) break; + pet.displayId = packet.readUInt32(); + pet.isActive = (packet.readUInt8() != 0); + stabledPets_.push_back(std::move(pet)); + } + + stableWindowOpen_ = true; + LOG_INFO("MSG_LIST_STABLED_PETS: stableMasterGuid=0x", std::hex, stableMasterGuid_, std::dec, + " petCount=", (int)petCount, " numSlots=", (int)stableNumSlots_); + for (const auto& p : stabledPets_) { + LOG_DEBUG(" Pet: number=", p.petNumber, " entry=", p.entry, + " level=", p.level, " name='", p.name, "' displayId=", p.displayId, + " active=", p.isActive); + } +} + void GameHandler::setActionBarSlot(int slot, ActionBarSlot::Type type, uint32_t id) { if (slot < 0 || slot >= ACTION_BAR_SLOTS) return; actionBar[slot].type = type; @@ -15958,6 +16044,18 @@ void GameHandler::selectGossipOption(uint32_t optionId) { socket->send(bindPkt); LOG_INFO("Sent CMSG_BINDER_ACTIVATE for npc=0x", std::hex, currentGossip.npcGuid, std::dec); } + + // Stable master detection: GOSSIP_OPTION_STABLE or text keywords + if (text == "GOSSIP_OPTION_STABLE" || + textLower.find("stable") != std::string::npos || + textLower.find("my pet") != std::string::npos) { + stableMasterGuid_ = currentGossip.npcGuid; + stableWindowOpen_ = false; // will open when list arrives + auto listPkt = ListStabledPetsPacket::build(currentGossip.npcGuid); + socket->send(listPkt); + LOG_INFO("Sent MSG_LIST_STABLED_PETS (gossip) to npc=0x", + std::hex, currentGossip.npcGuid, std::dec); + } break; } } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index dd7dc33f..f5bb0e44 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -5397,5 +5397,29 @@ bool AuctionCommandResultParser::parse(network::Packet& packet, AuctionCommandRe return true; } +// ============================================================ +// Pet Stable System +// ============================================================ + +network::Packet ListStabledPetsPacket::build(uint64_t stableMasterGuid) { + network::Packet p(wireOpcode(Opcode::MSG_LIST_STABLED_PETS)); + p.writeUInt64(stableMasterGuid); + return p; +} + +network::Packet StablePetPacket::build(uint64_t stableMasterGuid, uint8_t slot) { + network::Packet p(wireOpcode(Opcode::CMSG_STABLE_PET)); + p.writeUInt64(stableMasterGuid); + p.writeUInt8(slot); + return p; +} + +network::Packet UnstablePetPacket::build(uint64_t stableMasterGuid, uint32_t petNumber) { + network::Packet p(wireOpcode(Opcode::CMSG_UNSTABLE_PET)); + p.writeUInt64(stableMasterGuid); + p.writeUInt32(petNumber); + return p; +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f4ea21d6..54fa0b7f 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -698,6 +698,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderQuestOfferRewardWindow(gameHandler); renderVendorWindow(gameHandler); renderTrainerWindow(gameHandler); + renderStableWindow(gameHandler); renderTaxiWindow(gameHandler); renderMailWindow(gameHandler); renderMailComposeWindow(gameHandler); @@ -13593,6 +13594,123 @@ void GameScreen::renderEscapeMenu() { ImGui::End(); } +// ============================================================ +// Pet Stable Window +// ============================================================ + +void GameScreen::renderStableWindow(game::GameHandler& gameHandler) { + if (!gameHandler.isStableWindowOpen()) return; + + auto* window = core::Application::getInstance().getWindow(); + float screenW = window ? static_cast(window->getWidth()) : 1280.0f; + float screenH = window ? static_cast(window->getHeight()) : 720.0f; + + ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 240.0f, screenH / 2.0f - 180.0f), + ImGuiCond_Once); + ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once); + + bool open = true; + if (!ImGui::Begin("Pet Stable", &open, + ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + ImGui::End(); + if (!open) { + // User closed the window; clear stable state + gameHandler.closeStableWindow(); + } + return; + } + + const auto& pets = gameHandler.getStabledPets(); + uint8_t numSlots = gameHandler.getStableSlots(); + + ImGui::TextDisabled("Stable slots: %u", static_cast(numSlots)); + ImGui::Separator(); + + // Active pets section + bool hasActivePets = false; + for (const auto& p : pets) { + if (p.isActive) { hasActivePets = true; break; } + } + + if (hasActivePets) { + ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "Active / Summoned"); + for (const auto& p : pets) { + if (!p.isActive) continue; + ImGui::PushID(static_cast(p.petNumber) * -1 - 1); + + const std::string displayName = p.name.empty() + ? ("Pet #" + std::to_string(p.petNumber)) + : p.name; + ImGui::Text(" %s (Level %u)", displayName.c_str(), p.level); + ImGui::SameLine(); + ImGui::TextDisabled("[Active]"); + + // Offer to stable the active pet if there are free slots + uint8_t usedSlots = 0; + for (const auto& sp : pets) { if (!sp.isActive) ++usedSlots; } + if (usedSlots < numSlots) { + ImGui::SameLine(); + if (ImGui::SmallButton("Store in stable")) { + // Slot 1 is first stable slot; server handles free slot assignment. + gameHandler.stablePet(1); + } + } + ImGui::PopID(); + } + ImGui::Separator(); + } + + // Stabled pets section + ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f), "Stabled Pets"); + + bool hasStabledPets = false; + for (const auto& p : pets) { + if (!p.isActive) { hasStabledPets = true; break; } + } + + if (!hasStabledPets) { + ImGui::TextDisabled(" (No pets in stable)"); + } else { + for (const auto& p : pets) { + if (p.isActive) continue; + ImGui::PushID(static_cast(p.petNumber)); + + const std::string displayName = p.name.empty() + ? ("Pet #" + std::to_string(p.petNumber)) + : p.name; + ImGui::Text(" %s (Level %u, Entry %u)", + displayName.c_str(), p.level, p.entry); + ImGui::SameLine(); + if (ImGui::SmallButton("Retrieve")) { + gameHandler.unstablePet(p.petNumber); + } + ImGui::PopID(); + } + } + + // Empty slots + uint8_t usedStableSlots = 0; + for (const auto& p : pets) { if (!p.isActive) ++usedStableSlots; } + if (usedStableSlots < numSlots) { + ImGui::TextDisabled(" %u empty slot(s) available", + static_cast(numSlots - usedStableSlots)); + } + + ImGui::Separator(); + if (ImGui::Button("Refresh")) { + gameHandler.requestStabledPetList(); + } + ImGui::SameLine(); + if (ImGui::Button("Close")) { + gameHandler.closeStableWindow(); + } + + ImGui::End(); + if (!open) { + gameHandler.closeStableWindow(); + } +} + // ============================================================ // Taxi Window // ============================================================