mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Stabilize quest log details loading and turn-in item sync
This commit is contained in:
parent
334d4d3df6
commit
4fcf869e34
5 changed files with 135 additions and 41 deletions
|
|
@ -704,10 +704,16 @@ public:
|
|||
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
|
||||
// Quest item progress: itemId -> current count
|
||||
std::unordered_map<uint32_t, uint32_t> itemCounts;
|
||||
// Server-authoritative quest item requirements from REQUEST_ITEMS
|
||||
std::unordered_map<uint32_t, uint32_t> requiredItemCounts;
|
||||
};
|
||||
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||
void abandonQuest(uint32_t questId);
|
||||
bool requestQuestQuery(uint32_t questId, bool force = false);
|
||||
bool isQuestQueryPending(uint32_t questId) const {
|
||||
return pendingQuestQueryIds_.count(questId) > 0;
|
||||
}
|
||||
void clearQuestQueryPending(uint32_t questId) { pendingQuestQueryIds_.erase(questId); }
|
||||
const std::unordered_map<uint32_t, uint32_t>& getWorldStates() const { return worldStates_; }
|
||||
std::optional<uint32_t> getWorldState(uint32_t key) const {
|
||||
auto it = worldStates_.find(key);
|
||||
|
|
@ -1437,6 +1443,8 @@ private:
|
|||
// Quest log
|
||||
std::vector<QuestLogEntry> questLog_;
|
||||
std::unordered_set<uint32_t> pendingQuestQueryIds_;
|
||||
int questQueryTracePacketsLeft_ = 0;
|
||||
uint32_t questQueryTraceQuestId_ = 0;
|
||||
|
||||
// Quest giver status per NPC
|
||||
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
#include "game/game_handler.hpp"
|
||||
#include <imgui.h>
|
||||
#include <cstdint>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace wowee { namespace ui {
|
||||
|
||||
|
|
@ -18,6 +19,8 @@ private:
|
|||
bool lKeyWasDown = false;
|
||||
int selectedIndex = -1;
|
||||
uint32_t lastDetailRequestQuestId_ = 0;
|
||||
double lastDetailRequestAt_ = 0.0;
|
||||
std::unordered_set<uint32_t> questDetailQueryNoResponse_;
|
||||
};
|
||||
|
||||
}} // namespace wowee::ui
|
||||
|
|
|
|||
|
|
@ -932,6 +932,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// Translate wire opcode to logical opcode via expansion table
|
||||
auto logicalOp = opcodeTable_.fromWire(opcode);
|
||||
|
||||
if (questQueryTracePacketsLeft_ > 0) {
|
||||
int logicalId = logicalOp ? static_cast<int>(*logicalOp) : -1;
|
||||
size_t limit = std::min<size_t>(24, packet.getSize());
|
||||
std::string hex;
|
||||
const auto& raw = packet.getData();
|
||||
for (size_t i = 0; i < limit && i < raw.size(); ++i) {
|
||||
char buf[4];
|
||||
snprintf(buf, sizeof(buf), "%02x ", raw[i]);
|
||||
hex += buf;
|
||||
}
|
||||
LOG_INFO("QTRACE RX: wire=0x", std::hex, opcode, std::dec,
|
||||
" logical=", logicalId,
|
||||
" size=", packet.getSize(),
|
||||
" qid=", questQueryTraceQuestId_,
|
||||
" head=[", hex, "]");
|
||||
--questQueryTracePacketsLeft_;
|
||||
}
|
||||
|
||||
if (!logicalOp) {
|
||||
static std::unordered_set<uint16_t> loggedUnknownWireOpcodes;
|
||||
if (loggedUnknownWireOpcodes.insert(opcode).second) {
|
||||
|
|
@ -1861,6 +1880,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
bool updatedAny = false;
|
||||
for (auto& quest : questLog_) {
|
||||
if (quest.complete) continue;
|
||||
const bool tracksItem =
|
||||
quest.requiredItemCounts.count(itemId) > 0 ||
|
||||
quest.itemCounts.count(itemId) > 0;
|
||||
if (!tracksItem) continue;
|
||||
quest.itemCounts[itemId] = count;
|
||||
updatedAny = true;
|
||||
}
|
||||
|
|
@ -1940,6 +1963,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
}
|
||||
|
||||
uint32_t questId = packet.readUInt32();
|
||||
LOG_INFO("Quest query RX: wire=0x", std::hex, opcode, std::dec,
|
||||
" questId=", questId, " payloadSize=", packet.getSize());
|
||||
packet.readUInt32(); // questMethod
|
||||
|
||||
const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3;
|
||||
|
|
@ -1964,7 +1989,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
q.title = parsed.title;
|
||||
}
|
||||
if (!parsed.objectives.empty() &&
|
||||
(q.objectives.empty() || parsed.objectives.size() > q.objectives.size())) {
|
||||
(q.objectives.empty() || q.objectives.size() < 16)) {
|
||||
q.objectives = parsed.objectives;
|
||||
}
|
||||
break;
|
||||
|
|
@ -8657,28 +8682,12 @@ void GameHandler::selectGossipOption(uint32_t optionId) {
|
|||
void GameHandler::selectGossipQuest(uint32_t questId) {
|
||||
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||
|
||||
// Check if quest is in our quest log and completable
|
||||
bool isInLog = false;
|
||||
bool isCompletable = false;
|
||||
for (const auto& quest : questLog_) {
|
||||
if (quest.questId == questId) {
|
||||
isInLog = true;
|
||||
isCompletable = quest.complete;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isInLog && isCompletable) {
|
||||
// Quest is ready to turn in - request reward
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
|
||||
packet.writeUInt64(currentGossip.npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
socket->send(packet);
|
||||
} else {
|
||||
// New quest or not completable - query details
|
||||
auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId);
|
||||
socket->send(packet);
|
||||
}
|
||||
// Always query quest from gossip and let the server drive next step:
|
||||
// - details (new quest), or
|
||||
// - request items (turn-in check), or
|
||||
// - offer reward.
|
||||
auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId);
|
||||
socket->send(packet);
|
||||
|
||||
gossipWindowOpen = false;
|
||||
}
|
||||
|
|
@ -8689,6 +8698,11 @@ bool GameHandler::requestQuestQuery(uint32_t questId, bool force) {
|
|||
|
||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
|
||||
pkt.writeUInt32(questId);
|
||||
questQueryTraceQuestId_ = questId;
|
||||
questQueryTracePacketsLeft_ = 60;
|
||||
LOG_INFO("Quest query TX: questId=", questId, " wireOpcode=0x",
|
||||
std::hex, wireOpcode(Opcode::CMSG_QUEST_QUERY), std::dec,
|
||||
" force=", force ? 1 : 0);
|
||||
socket->send(pkt);
|
||||
pendingQuestQueryIds_.insert(questId);
|
||||
return true;
|
||||
|
|
@ -8785,6 +8799,37 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) {
|
|||
for (const auto& item : data.requiredItems) {
|
||||
queryItemInfo(item.itemId, 0);
|
||||
}
|
||||
|
||||
// Server-authoritative turn-in requirements: sync quest-log summary so
|
||||
// UI doesn't show stale/inferred objective numbers.
|
||||
for (auto& q : questLog_) {
|
||||
if (q.questId != data.questId) continue;
|
||||
q.complete = data.isCompletable();
|
||||
q.requiredItemCounts.clear();
|
||||
|
||||
std::ostringstream oss;
|
||||
if (!data.completionText.empty()) {
|
||||
oss << data.completionText;
|
||||
if (!data.requiredItems.empty() || data.requiredMoney > 0) oss << "\n\n";
|
||||
}
|
||||
if (!data.requiredItems.empty()) {
|
||||
oss << "Required items:";
|
||||
for (const auto& item : data.requiredItems) {
|
||||
std::string itemLabel = "Item " + std::to_string(item.itemId);
|
||||
if (const auto* info = getItemInfo(item.itemId)) {
|
||||
if (!info->name.empty()) itemLabel = info->name;
|
||||
}
|
||||
q.requiredItemCounts[item.itemId] = item.count;
|
||||
oss << "\n- " << itemLabel << " x" << item.count;
|
||||
}
|
||||
}
|
||||
if (data.requiredMoney > 0) {
|
||||
if (!data.requiredItems.empty()) oss << "\n";
|
||||
oss << "\nRequired money: " << formatCopperAmount(data.requiredMoney);
|
||||
}
|
||||
q.objectives = oss.str();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GameHandler::handleQuestOfferReward(network::Packet& packet) {
|
||||
|
|
|
|||
|
|
@ -4532,6 +4532,23 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|||
|
||||
bool open = true;
|
||||
const auto& quest = gameHandler.getQuestRequestItems();
|
||||
auto countItemInInventory = [&](uint32_t itemId) -> uint32_t {
|
||||
const auto& inv = gameHandler.getInventory();
|
||||
uint32_t total = 0;
|
||||
for (int i = 0; i < inv.getBackpackSize(); ++i) {
|
||||
const auto& slot = inv.getBackpackSlot(i);
|
||||
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
|
||||
}
|
||||
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; ++bag) {
|
||||
int bagSize = inv.getBagSize(bag);
|
||||
for (int s = 0; s < bagSize; ++s) {
|
||||
const auto& slot = inv.getBagSlot(bag, s);
|
||||
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||
if (!quest.completionText.empty()) {
|
||||
|
|
@ -4545,11 +4562,17 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:");
|
||||
for (const auto& item : quest.requiredItems) {
|
||||
uint32_t have = countItemInInventory(item.itemId);
|
||||
bool enough = have >= item.count;
|
||||
auto* info = gameHandler.getItemInfo(item.itemId);
|
||||
if (info && info->valid)
|
||||
ImGui::Text(" %s x%u", info->name.c_str(), item.count);
|
||||
else
|
||||
ImGui::Text(" Item %u x%u", item.itemId, item.count);
|
||||
const char* name = (info && info->valid) ? info->name.c_str() : nullptr;
|
||||
if (name && *name) {
|
||||
ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f),
|
||||
" %s %u/%u", name, have, item.count);
|
||||
} else {
|
||||
ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f),
|
||||
" Item %u %u/%u", item.itemId, have, item.count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4566,19 +4589,17 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|||
ImGui::Separator();
|
||||
ImGui::Spacing();
|
||||
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||
if (quest.isCompletable()) {
|
||||
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
||||
gameHandler.completeQuest();
|
||||
}
|
||||
} else {
|
||||
ImGui::BeginDisabled();
|
||||
ImGui::Button("Incomplete", ImVec2(buttonW, 0));
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
||||
gameHandler.completeQuest();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
||||
gameHandler.closeQuestRequestItems();
|
||||
}
|
||||
|
||||
if (!quest.isCompletable()) {
|
||||
ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated.");
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
|
||||
|
|
|
|||
|
|
@ -284,11 +284,14 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
|||
if (clicked) {
|
||||
selectedIndex = static_cast<int>(i);
|
||||
if (q.objectives.empty()) {
|
||||
if (gameHandler.requestQuestQuery(q.questId)) {
|
||||
if (!questDetailQueryNoResponse_.count(q.questId) &&
|
||||
gameHandler.requestQuestQuery(q.questId)) {
|
||||
lastDetailRequestQuestId_ = q.questId;
|
||||
lastDetailRequestAt_ = ImGui::GetTime();
|
||||
}
|
||||
} else if (lastDetailRequestQuestId_ == q.questId) {
|
||||
lastDetailRequestQuestId_ = 0;
|
||||
questDetailQueryNoResponse_.erase(q.questId);
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
|
@ -310,23 +313,37 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
|||
ImGui::Separator();
|
||||
|
||||
if (sel.objectives.empty()) {
|
||||
if (lastDetailRequestQuestId_ != sel.questId) {
|
||||
if (gameHandler.requestQuestQuery(sel.questId)) {
|
||||
lastDetailRequestQuestId_ = sel.questId;
|
||||
}
|
||||
bool noResponse = questDetailQueryNoResponse_.count(sel.questId) > 0;
|
||||
bool pending = noResponse ? false : gameHandler.isQuestQueryPending(sel.questId);
|
||||
const bool requestTimedOut =
|
||||
(lastDetailRequestQuestId_ == sel.questId) &&
|
||||
((ImGui::GetTime() - lastDetailRequestAt_) > 5.0);
|
||||
if (lastDetailRequestQuestId_ == sel.questId && !pending) {
|
||||
lastDetailRequestQuestId_ = 0;
|
||||
questDetailQueryNoResponse_.erase(sel.questId);
|
||||
} else if (requestTimedOut) {
|
||||
lastDetailRequestQuestId_ = 0;
|
||||
pending = false;
|
||||
questDetailQueryNoResponse_.insert(sel.questId);
|
||||
noResponse = true;
|
||||
gameHandler.clearQuestQueryPending(sel.questId);
|
||||
}
|
||||
if (lastDetailRequestQuestId_ == sel.questId) {
|
||||
if (pending) {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Loading quest details...");
|
||||
} else {
|
||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Quest summary not available yet.");
|
||||
}
|
||||
if (ImGui::Button("Retry Details")) {
|
||||
questDetailQueryNoResponse_.erase(sel.questId);
|
||||
gameHandler.clearQuestQueryPending(sel.questId);
|
||||
if (gameHandler.requestQuestQuery(sel.questId, true)) {
|
||||
lastDetailRequestQuestId_ = sel.questId;
|
||||
lastDetailRequestAt_ = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
|
||||
questDetailQueryNoResponse_.erase(sel.questId);
|
||||
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
|
||||
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue