From 5883654e1efc049ffd015ceb8360834407594d39 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 12 Mar 2026 18:21:50 -0700 Subject: [PATCH] feat: replace page-text chat-dump with proper book/scroll window handlePageTextQueryResponse() now collects pages into bookPages_ vector instead of dumping lines to system chat. Multi-page items (nextPageId != 0) are automatically chained by requesting subsequent pages. The book window opens automatically when pages arrive, shows formatted text in a parchment- styled ImGui window with Prev/Next page navigation and a Close button. SMSG_READ_ITEM_OK clears bookPages_ so each item read starts fresh; handleGameObjectPageText() does the same before querying the first page. Closes the long-standing issue where reading scrolls and tattered notes spammed many separate chat messages instead of showing a readable UI. --- include/game/game_handler.hpp | 8 ++++ include/ui/game_screen.hpp | 5 +++ src/game/game_handler.cpp | 35 +++++++++++----- src/ui/game_screen.cpp | 77 +++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 621a8586..9c8c36d5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1462,6 +1462,13 @@ public: uint32_t getTempEnchantRemainingMs(uint32_t slot) const; static constexpr const char* kTempEnchantSlotNames[] = { "Main Hand", "Off Hand", "Ranged" }; + // ---- Readable text (books / scrolls / notes) ---- + // Populated by handlePageTextQueryResponse(); multi-page items chain via nextPageId. + struct BookPage { uint32_t pageId = 0; std::string text; }; + const std::vector& getBookPages() const { return bookPages_; } + bool hasBookOpen() const { return !bookPages_.empty(); } + void clearBook() { bookPages_.clear(); } + // Other player level-up callback — fires when another player gains a level using OtherPlayerLevelUpCallback = std::function; void setOtherPlayerLevelUpCallback(OtherPlayerLevelUpCallback cb) { otherPlayerLevelUpCallback_ = std::move(cb); } @@ -2820,6 +2827,7 @@ private: LevelUpCallback levelUpCallback_; LevelUpDeltas lastLevelUpDeltas_; std::vector tempEnchantTimers_; + std::vector bookPages_; // pages collected for the current readable item OtherPlayerLevelUpCallback otherPlayerLevelUpCallback_; AchievementEarnedCallback achievementEarnedCallback_; AreaDiscoveryCallback areaDiscoveryCallback_; diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 09f80551..65a0f7ce 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -437,6 +437,11 @@ private: bool showInspectWindow_ = false; void renderInspectWindow(game::GameHandler& gameHandler); + // Readable text window (books / scrolls / notes) + bool showBookWindow_ = false; + int bookCurrentPage_ = 0; + void renderBookWindow(game::GameHandler& gameHandler); + // Threat window bool showThreatWindow_ = false; void renderThreatWindow(game::GameHandler& gameHandler); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 052c0cbc..f46a12d4 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6098,7 +6098,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Read item results ---- case Opcode::SMSG_READ_ITEM_OK: - addSystemChatMessage("You read the item."); + bookPages_.clear(); // fresh book for this item read packet.setReadPos(packet.getSize()); break; case Opcode::SMSG_READ_ITEM_FAILED: @@ -11327,6 +11327,7 @@ void GameHandler::handleGameObjectPageText(network::Packet& packet) { else if (info.type == 10) pageId = info.data[7]; if (pageId != 0 && socket && state == WorldState::IN_WORLD) { + bookPages_.clear(); // start a fresh book for this interaction auto req = PageTextQueryPacket::build(pageId, guid); socket->send(req); return; @@ -11341,19 +11342,31 @@ void GameHandler::handlePageTextQueryResponse(network::Packet& packet) { PageTextQueryResponseData data; if (!PageTextQueryResponseParser::parse(packet, data)) return; - if (!data.text.empty()) { - std::istringstream iss(data.text); - std::string line; - bool wrote = false; - while (std::getline(iss, line)) { - if (line.empty()) continue; - addSystemChatMessage(line); - wrote = true; + if (!data.isValid()) return; + + // Append page if not already collected + bool alreadyHave = false; + for (const auto& bp : bookPages_) { + if (bp.pageId == data.pageId) { alreadyHave = true; break; } + } + if (!alreadyHave) { + bookPages_.push_back({data.pageId, data.text}); + } + + // Follow the chain: if there's a next page we haven't fetched yet, request it + if (data.nextPageId != 0) { + bool nextHave = false; + for (const auto& bp : bookPages_) { + if (bp.pageId == data.nextPageId) { nextHave = true; break; } } - if (!wrote) { - addSystemChatMessage(data.text); + if (!nextHave && socket && state == WorldState::IN_WORLD) { + auto req = PageTextQueryPacket::build(data.nextPageId, playerGuid); + socket->send(req); } } + LOG_DEBUG("handlePageTextQueryResponse: pageId=", data.pageId, + " nextPage=", data.nextPageId, + " totalPages=", bookPages_.size()); } // ============================================================ diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 0ee760e8..f4ea21d6 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -711,6 +711,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderAchievementWindow(gameHandler); renderGmTicketWindow(gameHandler); renderInspectWindow(gameHandler); + renderBookWindow(gameHandler); renderThreatWindow(gameHandler); renderBgScoreboard(gameHandler); // renderQuestMarkers(gameHandler); // Disabled - using 3D billboard markers now @@ -20194,6 +20195,82 @@ void GameScreen::renderObjectiveTracker(game::GameHandler&) { // full-featured draggable tracker with context menus and item icons. } +// ─── Book / Scroll / Note Window ────────────────────────────────────────────── +void GameScreen::renderBookWindow(game::GameHandler& gameHandler) { + // Auto-open when new pages arrive + if (gameHandler.hasBookOpen() && !showBookWindow_) { + showBookWindow_ = true; + bookCurrentPage_ = 0; + } + if (!showBookWindow_) return; + + const auto& pages = gameHandler.getBookPages(); + if (pages.empty()) { showBookWindow_ = false; return; } + + // Clamp page index + if (bookCurrentPage_ < 0) bookCurrentPage_ = 0; + if (bookCurrentPage_ >= static_cast(pages.size())) + bookCurrentPage_ = static_cast(pages.size()) - 1; + + ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_Appearing); + ImGui::SetNextWindowPos(ImVec2(400, 180), ImGuiCond_Appearing); + + bool open = showBookWindow_; + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.09f, 0.06f, 0.98f)); + ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.25f, 0.18f, 0.08f, 1.0f)); + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.37f, 0.18f, 1.0f)); + + char title[64]; + if (pages.size() > 1) + snprintf(title, sizeof(title), "Page %d / %d###BookWin", + bookCurrentPage_ + 1, static_cast(pages.size())); + else + snprintf(title, sizeof(title), "###BookWin"); + + if (ImGui::Begin(title, &open, ImGuiWindowFlags_NoCollapse)) { + // Parchment text colour + ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.78f, 0.62f, 1.0f)); + + const std::string& text = pages[bookCurrentPage_].text; + // Use a child region with word-wrap + ImGui::SetNextWindowContentSize(ImVec2(ImGui::GetContentRegionAvail().x, 0)); + if (ImGui::BeginChild("##BookText", + ImVec2(0, ImGui::GetContentRegionAvail().y - 34), + false, ImGuiWindowFlags_HorizontalScrollbar)) { + ImGui::SetNextItemWidth(-1); + ImGui::TextWrapped("%s", text.c_str()); + } + ImGui::EndChild(); + ImGui::PopStyleColor(); + + // Navigation row + ImGui::Separator(); + bool canPrev = (bookCurrentPage_ > 0); + bool canNext = (bookCurrentPage_ < static_cast(pages.size()) - 1); + + if (!canPrev) ImGui::BeginDisabled(); + if (ImGui::Button("< Prev", ImVec2(80, 0))) bookCurrentPage_--; + if (!canPrev) ImGui::EndDisabled(); + + ImGui::SameLine(); + if (!canNext) ImGui::BeginDisabled(); + if (ImGui::Button("Next >", ImVec2(80, 0))) bookCurrentPage_++; + if (!canNext) ImGui::EndDisabled(); + + ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60); + if (ImGui::Button("Close", ImVec2(60, 0))) { + open = false; + } + } + ImGui::End(); + ImGui::PopStyleColor(3); + + if (!open) { + showBookWindow_ = false; + gameHandler.clearBook(); + } +} + // ─── Inspect Window ─────────────────────────────────────────────────────────── void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) { if (!showInspectWindow_) return;