mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-03 08:03:50 +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;
|
std::unordered_map<uint32_t, std::pair<uint32_t, uint32_t>> killCounts;
|
||||||
// Quest item progress: itemId -> current count
|
// Quest item progress: itemId -> current count
|
||||||
std::unordered_map<uint32_t, uint32_t> itemCounts;
|
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_; }
|
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
|
||||||
void abandonQuest(uint32_t questId);
|
void abandonQuest(uint32_t questId);
|
||||||
bool requestQuestQuery(uint32_t questId, bool force = false);
|
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_; }
|
const std::unordered_map<uint32_t, uint32_t>& getWorldStates() const { return worldStates_; }
|
||||||
std::optional<uint32_t> getWorldState(uint32_t key) const {
|
std::optional<uint32_t> getWorldState(uint32_t key) const {
|
||||||
auto it = worldStates_.find(key);
|
auto it = worldStates_.find(key);
|
||||||
|
|
@ -1437,6 +1443,8 @@ private:
|
||||||
// Quest log
|
// Quest log
|
||||||
std::vector<QuestLogEntry> questLog_;
|
std::vector<QuestLogEntry> questLog_;
|
||||||
std::unordered_set<uint32_t> pendingQuestQueryIds_;
|
std::unordered_set<uint32_t> pendingQuestQueryIds_;
|
||||||
|
int questQueryTracePacketsLeft_ = 0;
|
||||||
|
uint32_t questQueryTraceQuestId_ = 0;
|
||||||
|
|
||||||
// Quest giver status per NPC
|
// Quest giver status per NPC
|
||||||
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
|
std::unordered_map<uint64_t, QuestGiverStatus> npcQuestStatus_;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "game/game_handler.hpp"
|
#include "game/game_handler.hpp"
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace wowee { namespace ui {
|
namespace wowee { namespace ui {
|
||||||
|
|
||||||
|
|
@ -18,6 +19,8 @@ private:
|
||||||
bool lKeyWasDown = false;
|
bool lKeyWasDown = false;
|
||||||
int selectedIndex = -1;
|
int selectedIndex = -1;
|
||||||
uint32_t lastDetailRequestQuestId_ = 0;
|
uint32_t lastDetailRequestQuestId_ = 0;
|
||||||
|
double lastDetailRequestAt_ = 0.0;
|
||||||
|
std::unordered_set<uint32_t> questDetailQueryNoResponse_;
|
||||||
};
|
};
|
||||||
|
|
||||||
}} // namespace wowee::ui
|
}} // namespace wowee::ui
|
||||||
|
|
|
||||||
|
|
@ -932,6 +932,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// Translate wire opcode to logical opcode via expansion table
|
// Translate wire opcode to logical opcode via expansion table
|
||||||
auto logicalOp = opcodeTable_.fromWire(opcode);
|
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) {
|
if (!logicalOp) {
|
||||||
static std::unordered_set<uint16_t> loggedUnknownWireOpcodes;
|
static std::unordered_set<uint16_t> loggedUnknownWireOpcodes;
|
||||||
if (loggedUnknownWireOpcodes.insert(opcode).second) {
|
if (loggedUnknownWireOpcodes.insert(opcode).second) {
|
||||||
|
|
@ -1861,6 +1880,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
bool updatedAny = false;
|
bool updatedAny = false;
|
||||||
for (auto& quest : questLog_) {
|
for (auto& quest : questLog_) {
|
||||||
if (quest.complete) continue;
|
if (quest.complete) continue;
|
||||||
|
const bool tracksItem =
|
||||||
|
quest.requiredItemCounts.count(itemId) > 0 ||
|
||||||
|
quest.itemCounts.count(itemId) > 0;
|
||||||
|
if (!tracksItem) continue;
|
||||||
quest.itemCounts[itemId] = count;
|
quest.itemCounts[itemId] = count;
|
||||||
updatedAny = true;
|
updatedAny = true;
|
||||||
}
|
}
|
||||||
|
|
@ -1940,6 +1963,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t questId = packet.readUInt32();
|
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
|
packet.readUInt32(); // questMethod
|
||||||
|
|
||||||
const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3;
|
const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3;
|
||||||
|
|
@ -1964,7 +1989,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
q.title = parsed.title;
|
q.title = parsed.title;
|
||||||
}
|
}
|
||||||
if (!parsed.objectives.empty() &&
|
if (!parsed.objectives.empty() &&
|
||||||
(q.objectives.empty() || parsed.objectives.size() > q.objectives.size())) {
|
(q.objectives.empty() || q.objectives.size() < 16)) {
|
||||||
q.objectives = parsed.objectives;
|
q.objectives = parsed.objectives;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -8657,28 +8682,12 @@ void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||||
void GameHandler::selectGossipQuest(uint32_t questId) {
|
void GameHandler::selectGossipQuest(uint32_t questId) {
|
||||||
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||||
|
|
||||||
// Check if quest is in our quest log and completable
|
// Always query quest from gossip and let the server drive next step:
|
||||||
bool isInLog = false;
|
// - details (new quest), or
|
||||||
bool isCompletable = false;
|
// - request items (turn-in check), or
|
||||||
for (const auto& quest : questLog_) {
|
// - offer reward.
|
||||||
if (quest.questId == questId) {
|
auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId);
|
||||||
isInLog = true;
|
socket->send(packet);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
gossipWindowOpen = false;
|
gossipWindowOpen = false;
|
||||||
}
|
}
|
||||||
|
|
@ -8689,6 +8698,11 @@ bool GameHandler::requestQuestQuery(uint32_t questId, bool force) {
|
||||||
|
|
||||||
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
|
network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY));
|
||||||
pkt.writeUInt32(questId);
|
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);
|
socket->send(pkt);
|
||||||
pendingQuestQueryIds_.insert(questId);
|
pendingQuestQueryIds_.insert(questId);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -8785,6 +8799,37 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) {
|
||||||
for (const auto& item : data.requiredItems) {
|
for (const auto& item : data.requiredItems) {
|
||||||
queryItemInfo(item.itemId, 0);
|
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) {
|
void GameHandler::handleQuestOfferReward(network::Packet& packet) {
|
||||||
|
|
|
||||||
|
|
@ -4532,6 +4532,23 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
||||||
|
|
||||||
bool open = true;
|
bool open = true;
|
||||||
const auto& quest = gameHandler.getQuestRequestItems();
|
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);
|
std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler);
|
||||||
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
||||||
if (!quest.completionText.empty()) {
|
if (!quest.completionText.empty()) {
|
||||||
|
|
@ -4545,11 +4562,17 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:");
|
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:");
|
||||||
for (const auto& item : quest.requiredItems) {
|
for (const auto& item : quest.requiredItems) {
|
||||||
|
uint32_t have = countItemInInventory(item.itemId);
|
||||||
|
bool enough = have >= item.count;
|
||||||
auto* info = gameHandler.getItemInfo(item.itemId);
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
||||||
if (info && info->valid)
|
const char* name = (info && info->valid) ? info->name.c_str() : nullptr;
|
||||||
ImGui::Text(" %s x%u", info->name.c_str(), item.count);
|
if (name && *name) {
|
||||||
else
|
ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f),
|
||||||
ImGui::Text(" Item %u x%u", item.itemId, item.count);
|
" %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::Separator();
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
||||||
if (quest.isCompletable()) {
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
||||||
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
gameHandler.completeQuest();
|
||||||
gameHandler.completeQuest();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ImGui::BeginDisabled();
|
|
||||||
ImGui::Button("Incomplete", ImVec2(buttonW, 0));
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
}
|
}
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
||||||
gameHandler.closeQuestRequestItems();
|
gameHandler.closeQuestRequestItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!quest.isCompletable()) {
|
||||||
|
ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -284,11 +284,14 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||||
if (clicked) {
|
if (clicked) {
|
||||||
selectedIndex = static_cast<int>(i);
|
selectedIndex = static_cast<int>(i);
|
||||||
if (q.objectives.empty()) {
|
if (q.objectives.empty()) {
|
||||||
if (gameHandler.requestQuestQuery(q.questId)) {
|
if (!questDetailQueryNoResponse_.count(q.questId) &&
|
||||||
|
gameHandler.requestQuestQuery(q.questId)) {
|
||||||
lastDetailRequestQuestId_ = q.questId;
|
lastDetailRequestQuestId_ = q.questId;
|
||||||
|
lastDetailRequestAt_ = ImGui::GetTime();
|
||||||
}
|
}
|
||||||
} else if (lastDetailRequestQuestId_ == q.questId) {
|
} else if (lastDetailRequestQuestId_ == q.questId) {
|
||||||
lastDetailRequestQuestId_ = 0;
|
lastDetailRequestQuestId_ = 0;
|
||||||
|
questDetailQueryNoResponse_.erase(q.questId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ImGui::PopID();
|
ImGui::PopID();
|
||||||
|
|
@ -310,23 +313,37 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
|
|
||||||
if (sel.objectives.empty()) {
|
if (sel.objectives.empty()) {
|
||||||
if (lastDetailRequestQuestId_ != sel.questId) {
|
bool noResponse = questDetailQueryNoResponse_.count(sel.questId) > 0;
|
||||||
if (gameHandler.requestQuestQuery(sel.questId)) {
|
bool pending = noResponse ? false : gameHandler.isQuestQueryPending(sel.questId);
|
||||||
lastDetailRequestQuestId_ = 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...");
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Loading quest details...");
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Quest summary not available yet.");
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Quest summary not available yet.");
|
||||||
}
|
}
|
||||||
if (ImGui::Button("Retry Details")) {
|
if (ImGui::Button("Retry Details")) {
|
||||||
|
questDetailQueryNoResponse_.erase(sel.questId);
|
||||||
|
gameHandler.clearQuestQueryPending(sel.questId);
|
||||||
if (gameHandler.requestQuestQuery(sel.questId, true)) {
|
if (gameHandler.requestQuestQuery(sel.questId, true)) {
|
||||||
lastDetailRequestQuestId_ = sel.questId;
|
lastDetailRequestQuestId_ = sel.questId;
|
||||||
|
lastDetailRequestAt_ = ImGui::GetTime();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
|
if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0;
|
||||||
|
questDetailQueryNoResponse_.erase(sel.questId);
|
||||||
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
|
ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary");
|
||||||
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler);
|
||||||
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
|
float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue