diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 69c1ab51..fe631337 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -394,6 +394,9 @@ public: // Ready check void initiateReadyCheck(); void respondToReadyCheck(bool ready); + bool hasPendingReadyCheck() const { return pendingReadyCheck_; } + void dismissReadyCheck() { pendingReadyCheck_ = false; } + const std::string& getReadyCheckInitiator() const { return readyCheckInitiator_; } // Duel void forfeitDuel(); @@ -898,6 +901,7 @@ public: int32_t standing = 0; }; const std::vector& getInitialFactions() const { return initialFactions_; } + const std::unordered_map& getFactionStandings() const { return factionStandings_; } uint32_t getLastContactListMask() const { return lastContactListMask_; } uint32_t getLastContactListCount() const { return lastContactListCount_; } bool isServerMovementAllowed() const { return serverMovementAllowed_; } @@ -1700,6 +1704,18 @@ private: int32_t lfgAvgWaitSec_ = -1; // estimated wait, -1=unknown uint32_t lfgTimeInQueueMs_= 0; // ms already in queue + // Ready check state + bool pendingReadyCheck_ = false; + std::string readyCheckInitiator_; + + // Faction standings (factionId → absolute standing value) + std::unordered_map factionStandings_; + // Faction name cache (factionId → name), populated lazily from Faction.dbc + std::unordered_map factionNameCache_; + bool factionNameCacheLoaded_ = false; + void loadFactionNameCache(); + std::string getFactionName(uint32_t factionId) const; + // ---- Phase 4: Group ---- GroupListData partyData; bool pendingGroupInvite = false; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index bf81cd4e..117ca82e 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -228,6 +228,7 @@ private: void renderMinimapMarkers(game::GameHandler& gameHandler); void renderGuildRoster(game::GameHandler& gameHandler); void renderGuildInvitePopup(game::GameHandler& gameHandler); + void renderReadyCheckPopup(game::GameHandler& gameHandler); void renderChatBubbles(game::GameHandler& gameHandler); void renderMailWindow(game::GameHandler& gameHandler); void renderMailComposeWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a2252fd5..02bca1b1 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2399,12 +2399,32 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_PARTY_MEMBER_STATS_FULL: handlePartyMemberStats(packet, true); break; - case Opcode::MSG_RAID_READY_CHECK: - // Server ready-check prompt (minimal handling for now). - packet.setReadPos(packet.getSize()); + case Opcode::MSG_RAID_READY_CHECK: { + // Server is broadcasting a ready check (someone in the raid initiated it). + // Payload: empty body, or optional uint64 initiator GUID in some builds. + pendingReadyCheck_ = true; + readyCheckInitiator_.clear(); + if (packet.getSize() - packet.getReadPos() >= 8) { + uint64_t initiatorGuid = packet.readUInt64(); + auto entity = entityManager.getEntity(initiatorGuid); + if (auto* unit = dynamic_cast(entity.get())) { + readyCheckInitiator_ = unit->getName(); + } + } + if (readyCheckInitiator_.empty() && partyData.leaderGuid != 0) { + // Identify initiator from party leader + for (const auto& member : partyData.members) { + if (member.guid == partyData.leaderGuid) { readyCheckInitiator_ = member.name; break; } + } + } + addSystemChatMessage(readyCheckInitiator_.empty() + ? "Ready check initiated!" + : readyCheckInitiator_ + " initiated a ready check!"); + LOG_INFO("MSG_RAID_READY_CHECK: initiator=", readyCheckInitiator_); break; + } case Opcode::MSG_RAID_READY_CHECK_CONFIRM: - // Ready-check responses from members. + // Another member responded to the ready check — consume. packet.setReadPos(packet.getSize()); break; case Opcode::SMSG_RAID_INSTANCE_INFO: @@ -2686,6 +2706,40 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } + case Opcode::SMSG_SET_FACTION_STANDING: { + // uint8 showVisualEffect + uint32 count + count × (uint32 factionId + int32 standing) + if (packet.getSize() - packet.getReadPos() < 5) break; + /*uint8_t showVisual =*/ packet.readUInt8(); + uint32_t count = packet.readUInt32(); + count = std::min(count, 128u); + loadFactionNameCache(); + for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 8; ++i) { + uint32_t factionId = packet.readUInt32(); + int32_t standing = static_cast(packet.readUInt32()); + int32_t oldStanding = 0; + auto it = factionStandings_.find(factionId); + if (it != factionStandings_.end()) oldStanding = it->second; + factionStandings_[factionId] = standing; + int32_t delta = standing - oldStanding; + if (delta != 0) { + std::string name = getFactionName(factionId); + char buf[256]; + std::snprintf(buf, sizeof(buf), "Reputation with %s %s by %d.", + name.c_str(), + delta > 0 ? "increased" : "decreased", + std::abs(delta)); + addSystemChatMessage(buf); + } + LOG_DEBUG("SMSG_SET_FACTION_STANDING: faction=", factionId, " standing=", standing); + } + break; + } + case Opcode::SMSG_SET_FACTION_ATWAR: + case Opcode::SMSG_SET_FACTION_VISIBLE: + // uint32 factionId [+ uint8 flags for ATWAR] — consume; hostility is tracked via update fields + packet.setReadPos(packet.getSize()); + break; + case Opcode::SMSG_FEATURE_SYSTEM_STATUS: case Opcode::SMSG_SET_FLAT_SPELL_MODIFIER: case Opcode::SMSG_SET_PCT_SPELL_MODIFIER: @@ -15912,5 +15966,55 @@ void GameHandler::handleAchievementEarned(network::Packet& packet) { " achievementId=", achievementId, " self=", isSelf); } +// --------------------------------------------------------------------------- +// Faction name cache (lazily loaded from Faction.dbc) +// --------------------------------------------------------------------------- + +void GameHandler::loadFactionNameCache() { + if (factionNameCacheLoaded_) return; + factionNameCacheLoaded_ = true; + + auto* am = core::Application::getInstance().getAssetManager(); + if (!am || !am->isInitialized()) return; + + auto dbc = am->loadDBC("Faction.dbc"); + if (!dbc || !dbc->isLoaded()) return; + + // Faction.dbc WotLK 3.3.5a field layout: + // 0: ID + // 1-4: ReputationRaceMask[4] + // 5-8: ReputationClassMask[4] + // 9-12: ReputationBase[4] + // 13-16: ReputationFlags[4] + // 17: ParentFactionID + // 18-19: Spillover rates (floats) + // 20-21: MaxRank + // 22: Name (English locale, string ref) + constexpr uint32_t ID_FIELD = 0; + constexpr uint32_t NAME_FIELD = 22; // enUS name string + + if (dbc->getFieldCount() <= NAME_FIELD) { + LOG_WARNING("Faction.dbc: unexpected field count ", dbc->getFieldCount()); + return; + } + + uint32_t count = dbc->getRecordCount(); + for (uint32_t i = 0; i < count; ++i) { + uint32_t factionId = dbc->getUInt32(i, ID_FIELD); + if (factionId == 0) continue; + std::string name = dbc->getString(i, NAME_FIELD); + if (!name.empty()) { + factionNameCache_[factionId] = std::move(name); + } + } + LOG_INFO("Faction.dbc: loaded ", factionNameCache_.size(), " faction names"); +} + +std::string GameHandler::getFactionName(uint32_t factionId) const { + auto it = factionNameCache_.find(factionId); + if (it != factionNameCache_.end()) return it->second; + return "faction #" + std::to_string(factionId); +} + } // namespace game } // namespace wowee diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 141fcf40..9d232cb0 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -404,6 +404,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderSharedQuestPopup(gameHandler); renderItemTextWindow(gameHandler); renderGuildInvitePopup(gameHandler); + renderReadyCheckPopup(gameHandler); renderGuildRoster(gameHandler); renderBuffBar(gameHandler); renderLootWindow(gameHandler); @@ -4650,6 +4651,38 @@ void GameScreen::renderGuildInvitePopup(game::GameHandler& gameHandler) { ImGui::End(); } +void GameScreen::renderReadyCheckPopup(game::GameHandler& gameHandler) { + if (!gameHandler.hasPendingReadyCheck()) 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 - 175, screenH / 2 - 60), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always); + + if (ImGui::Begin("Ready Check", nullptr, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize)) { + const std::string& initiator = gameHandler.getReadyCheckInitiator(); + if (initiator.empty()) { + ImGui::Text("A ready check has been initiated!"); + } else { + ImGui::TextWrapped("%s has initiated a ready check!", initiator.c_str()); + } + ImGui::Spacing(); + + if (ImGui::Button("Ready", ImVec2(155, 30))) { + gameHandler.respondToReadyCheck(true); + gameHandler.dismissReadyCheck(); + } + ImGui::SameLine(); + if (ImGui::Button("Not Ready", ImVec2(155, 30))) { + gameHandler.respondToReadyCheck(false); + gameHandler.dismissReadyCheck(); + } + } + ImGui::End(); +} + void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) { // O key toggle (WoW default Social/Guild keybind) if (!ImGui::GetIO().WantCaptureKeyboard && ImGui::IsKeyPressed(ImGuiKey_O)) {