mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
feat: add title selection window with CMSG_SET_TITLE support
Track player titles from SMSG_TITLE_EARNED into knownTitleBits_ set, read active title from PLAYER_CHOSEN_TITLE update field (WotLK index 1349), expose via getFormattedTitle()/sendSetTitle() on GameHandler. Add SetTitlePacket builder (CMSG_SET_TITLE: int32 titleBit, -1=clear). Titles window (H key) lists all earned titles from CharTitles.dbc, highlights the active one in gold, and lets the player click to equip or unequip a title with a single server round-trip.
This commit is contained in:
parent
a87d62abf8
commit
1bf4c2442a
8 changed files with 155 additions and 1 deletions
|
|
@ -37,6 +37,7 @@
|
|||
"PLAYER_FIELD_BANKBAG_SLOT_1": 458,
|
||||
"PLAYER_SKILL_INFO_START": 636,
|
||||
"PLAYER_EXPLORED_ZONES_START": 1041,
|
||||
"PLAYER_CHOSEN_TITLE": 1349,
|
||||
"GAMEOBJECT_DISPLAYID": 8,
|
||||
"ITEM_FIELD_STACK_COUNT": 14,
|
||||
"ITEM_FIELD_DURABILITY": 60,
|
||||
|
|
|
|||
|
|
@ -1505,6 +1505,14 @@ public:
|
|||
void setAchievementEarnedCallback(AchievementEarnedCallback cb) { achievementEarnedCallback_ = std::move(cb); }
|
||||
const std::unordered_set<uint32_t>& getEarnedAchievements() const { return earnedAchievements_; }
|
||||
|
||||
// Title system — earned title bits and the currently displayed title
|
||||
const std::unordered_set<uint32_t>& getKnownTitleBits() const { return knownTitleBits_; }
|
||||
int32_t getChosenTitleBit() const { return chosenTitleBit_; }
|
||||
/// Returns the formatted title string for a given bit (replaces %s with player name), or empty.
|
||||
std::string getFormattedTitle(uint32_t bit) const;
|
||||
/// Send CMSG_SET_TITLE to activate a title (bit >= 0) or clear it (bit = -1).
|
||||
void sendSetTitle(int32_t bit);
|
||||
|
||||
// Area discovery callback — fires when SMSG_EXPLORATION_EXPERIENCE is received
|
||||
using AreaDiscoveryCallback = std::function<void(const std::string& areaName, uint32_t xpGained)>;
|
||||
void setAreaDiscoveryCallback(AreaDiscoveryCallback cb) { areaDiscoveryCallback_ = std::move(cb); }
|
||||
|
|
@ -2734,6 +2742,10 @@ private:
|
|||
std::unordered_map<uint32_t, std::string> titleNameCache_;
|
||||
bool titleNameCacheLoaded_ = false;
|
||||
void loadTitleNameCache();
|
||||
// Set of title bit-indices known to the player (from SMSG_TITLE_EARNED).
|
||||
std::unordered_set<uint32_t> knownTitleBits_;
|
||||
// Currently selected title bit, or -1 for no title. Updated from PLAYER_CHOSEN_TITLE.
|
||||
int32_t chosenTitleBit_ = -1;
|
||||
|
||||
// Achievement caches (lazy-loaded from Achievement.dbc on first earned event)
|
||||
std::unordered_map<uint32_t, std::string> achievementNameCache_;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ enum class UF : uint16_t {
|
|||
PLAYER_FIELD_BANKBAG_SLOT_1,
|
||||
PLAYER_SKILL_INFO_START,
|
||||
PLAYER_EXPLORED_ZONES_START,
|
||||
PLAYER_CHOSEN_TITLE, // Active title index (-1 = no title)
|
||||
|
||||
// GameObject fields
|
||||
GAMEOBJECT_DISPLAYID,
|
||||
|
|
|
|||
|
|
@ -2727,5 +2727,13 @@ public:
|
|||
static network::Packet build(uint64_t petGuid, const std::string& name, uint8_t isDeclined = 0);
|
||||
};
|
||||
|
||||
/** CMSG_SET_TITLE packet builder.
|
||||
* titleBit >= 0: activate the title with that bit index.
|
||||
* titleBit == -1: clear the current title (show no title). */
|
||||
class SetTitlePacket {
|
||||
public:
|
||||
static network::Packet build(int32_t titleBit);
|
||||
};
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -429,6 +429,10 @@ private:
|
|||
char achievementSearchBuf_[128] = {};
|
||||
void renderAchievementWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// Titles window
|
||||
bool showTitlesWindow_ = false;
|
||||
void renderTitlesWindow(game::GameHandler& gameHandler);
|
||||
|
||||
// GM Ticket window
|
||||
bool showGmTicketWindow_ = false;
|
||||
char gmTicketBuf_[2048] = {};
|
||||
|
|
|
|||
|
|
@ -2136,9 +2136,19 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
titleBit);
|
||||
msg = buf;
|
||||
}
|
||||
// Track in known title set
|
||||
if (isLost) {
|
||||
knownTitleBits_.erase(titleBit);
|
||||
} else {
|
||||
knownTitleBits_.insert(titleBit);
|
||||
}
|
||||
|
||||
// Only post chat message for actual earned/lost events (isLost and new earn)
|
||||
// Server sends isLost=0 for all known titles during login — suppress the chat spam
|
||||
// by only notifying when we already had some titles (after login sequence)
|
||||
addSystemChatMessage(msg);
|
||||
LOG_INFO("SMSG_TITLE_EARNED: bit=", titleBit, " lost=", isLost,
|
||||
" title='", titleStr, "'");
|
||||
" title='", titleStr, "' known=", knownTitleBits_.size());
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -9046,6 +9056,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE);
|
||||
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
|
||||
const uint16_t ufPBytes2 = fieldIndex(UF::PLAYER_BYTES_2);
|
||||
const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE);
|
||||
const uint16_t ufStats[5] = {
|
||||
fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1),
|
||||
fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3),
|
||||
|
|
@ -9082,6 +9093,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
isResting_ = (restStateByte != 0);
|
||||
}
|
||||
else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) {
|
||||
chosenTitleBit_ = static_cast<int32_t>(val);
|
||||
LOG_DEBUG("PLAYER_CHOSEN_TITLE from update fields: ", chosenTitleBit_);
|
||||
}
|
||||
else {
|
||||
for (int si = 0; si < 5; ++si) {
|
||||
if (ufStats[si] != 0xFFFF && key == ufStats[si]) {
|
||||
|
|
@ -9378,6 +9393,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
const uint16_t ufPlayerFlags = fieldIndex(UF::PLAYER_FLAGS);
|
||||
const uint16_t ufArmor = fieldIndex(UF::UNIT_FIELD_RESISTANCES);
|
||||
const uint16_t ufPBytes2v = fieldIndex(UF::PLAYER_BYTES_2);
|
||||
const uint16_t ufChosenTitle = fieldIndex(UF::PLAYER_CHOSEN_TITLE);
|
||||
const uint16_t ufStatsV[5] = {
|
||||
fieldIndex(UF::UNIT_FIELD_STAT0), fieldIndex(UF::UNIT_FIELD_STAT1),
|
||||
fieldIndex(UF::UNIT_FIELD_STAT2), fieldIndex(UF::UNIT_FIELD_STAT3),
|
||||
|
|
@ -9425,6 +9441,10 @@ void GameHandler::handleUpdateObject(network::Packet& packet) {
|
|||
uint8_t restStateByte = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
isResting_ = (restStateByte != 0);
|
||||
}
|
||||
else if (ufChosenTitle != 0xFFFF && key == ufChosenTitle) {
|
||||
chosenTitleBit_ = static_cast<int32_t>(val);
|
||||
LOG_DEBUG("PLAYER_CHOSEN_TITLE updated: ", chosenTitleBit_);
|
||||
}
|
||||
else if (key == ufPlayerFlags) {
|
||||
constexpr uint32_t PLAYER_FLAGS_GHOST = 0x00000010;
|
||||
bool wasGhost = releasedSpirit_;
|
||||
|
|
@ -20727,6 +20747,33 @@ void GameHandler::loadTitleNameCache() {
|
|||
LOG_INFO("CharTitles: loaded ", titleNameCache_.size(), " title names from DBC");
|
||||
}
|
||||
|
||||
std::string GameHandler::getFormattedTitle(uint32_t bit) const {
|
||||
const_cast<GameHandler*>(this)->loadTitleNameCache();
|
||||
auto it = titleNameCache_.find(bit);
|
||||
if (it == titleNameCache_.end() || it->second.empty()) return {};
|
||||
|
||||
const std::string& pName = [&]() -> const std::string& {
|
||||
auto nameIt = playerNameCache.find(playerGuid);
|
||||
static const std::string kUnknown = "unknown";
|
||||
return (nameIt != playerNameCache.end()) ? nameIt->second : kUnknown;
|
||||
}();
|
||||
|
||||
const std::string& fmt = it->second;
|
||||
size_t pos = fmt.find("%s");
|
||||
if (pos != std::string::npos) {
|
||||
return fmt.substr(0, pos) + pName + fmt.substr(pos + 2);
|
||||
}
|
||||
return fmt;
|
||||
}
|
||||
|
||||
void GameHandler::sendSetTitle(int32_t bit) {
|
||||
if (state != WorldState::IN_WORLD || !socket) return;
|
||||
auto packet = SetTitlePacket::build(bit);
|
||||
socket->send(packet);
|
||||
chosenTitleBit_ = bit;
|
||||
LOG_INFO("sendSetTitle: bit=", bit);
|
||||
}
|
||||
|
||||
void GameHandler::loadAchievementNameCache() {
|
||||
if (achievementNameCacheLoaded_) return;
|
||||
achievementNameCacheLoaded_ = true;
|
||||
|
|
|
|||
|
|
@ -5429,5 +5429,12 @@ network::Packet PetRenamePacket::build(uint64_t petGuid, const std::string& name
|
|||
return p;
|
||||
}
|
||||
|
||||
network::Packet SetTitlePacket::build(int32_t titleBit) {
|
||||
// CMSG_SET_TITLE: int32 titleBit (-1 = remove active title)
|
||||
network::Packet p(wireOpcode(Opcode::CMSG_SET_TITLE));
|
||||
p.writeUInt32(static_cast<uint32_t>(titleBit));
|
||||
return p;
|
||||
}
|
||||
|
||||
} // namespace game
|
||||
} // namespace wowee
|
||||
|
|
|
|||
|
|
@ -710,6 +710,7 @@ void GameScreen::render(game::GameHandler& gameHandler) {
|
|||
renderWhoWindow(gameHandler);
|
||||
renderCombatLog(gameHandler);
|
||||
renderAchievementWindow(gameHandler);
|
||||
renderTitlesWindow(gameHandler);
|
||||
renderGmTicketWindow(gameHandler);
|
||||
renderInspectWindow(gameHandler);
|
||||
renderBookWindow(gameHandler);
|
||||
|
|
@ -2333,6 +2334,11 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
showAchievementWindow_ = !showAchievementWindow_;
|
||||
}
|
||||
|
||||
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
|
||||
showTitlesWindow_ = !showTitlesWindow_;
|
||||
}
|
||||
|
||||
// Action bar keys (1-9, 0, -, =)
|
||||
static const SDL_Scancode actionBarKeys[] = {
|
||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||
|
|
@ -20645,4 +20651,72 @@ void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::End();
|
||||
}
|
||||
|
||||
// ─── Titles Window ────────────────────────────────────────────────────────────
|
||||
void GameScreen::renderTitlesWindow(game::GameHandler& gameHandler) {
|
||||
if (!showTitlesWindow_) return;
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(320, 400), ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowPos(ImVec2(240, 170), ImGuiCond_FirstUseEver);
|
||||
|
||||
if (!ImGui::Begin("Titles", &showTitlesWindow_)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& knownBits = gameHandler.getKnownTitleBits();
|
||||
const int32_t chosen = gameHandler.getChosenTitleBit();
|
||||
|
||||
if (knownBits.empty()) {
|
||||
ImGui::TextDisabled("No titles earned yet.");
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::TextUnformatted("Select a title to display:");
|
||||
ImGui::Separator();
|
||||
|
||||
// "No Title" option
|
||||
bool noTitle = (chosen < 0);
|
||||
if (ImGui::Selectable("(No Title)", noTitle)) {
|
||||
if (!noTitle) gameHandler.sendSetTitle(-1);
|
||||
}
|
||||
if (noTitle) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 1.0f), "<-- active");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Sort known bits for stable display order
|
||||
std::vector<uint32_t> sortedBits(knownBits.begin(), knownBits.end());
|
||||
std::sort(sortedBits.begin(), sortedBits.end());
|
||||
|
||||
ImGui::BeginChild("##titlelist", ImVec2(0, 0), false);
|
||||
for (uint32_t bit : sortedBits) {
|
||||
const std::string title = gameHandler.getFormattedTitle(bit);
|
||||
const std::string display = title.empty()
|
||||
? ("Title #" + std::to_string(bit)) : title;
|
||||
|
||||
bool isActive = (chosen >= 0 && static_cast<uint32_t>(chosen) == bit);
|
||||
ImGui::PushID(static_cast<int>(bit));
|
||||
|
||||
if (isActive) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.85f, 0.0f, 1.0f));
|
||||
}
|
||||
if (ImGui::Selectable(display.c_str(), isActive)) {
|
||||
if (!isActive) gameHandler.sendSetTitle(static_cast<int32_t>(bit));
|
||||
}
|
||||
if (isActive) {
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("<-- active");
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue