mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Fix quest turn-in flow and WoW quest text placeholders
This commit is contained in:
parent
a37004db03
commit
8efc21115f
6 changed files with 170 additions and 56 deletions
|
|
@ -1437,6 +1437,9 @@ private:
|
|||
// Quest turn-in
|
||||
bool questRequestItemsOpen_ = false;
|
||||
QuestRequestItemsData currentQuestRequestItems_;
|
||||
uint32_t pendingTurnInQuestId_ = 0;
|
||||
uint64_t pendingTurnInNpcGuid_ = 0;
|
||||
bool pendingTurnInRewardRequest_ = false;
|
||||
bool questOfferRewardOpen_ = false;
|
||||
QuestOfferRewardData currentQuestOfferReward_;
|
||||
|
||||
|
|
|
|||
|
|
@ -2006,6 +2006,12 @@ public:
|
|||
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||
};
|
||||
|
||||
/** CMSG_QUESTGIVER_REQUEST_REWARD packet builder */
|
||||
class QuestgiverRequestRewardPacket {
|
||||
public:
|
||||
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||
};
|
||||
|
||||
/** CMSG_QUESTGIVER_CHOOSE_REWARD packet builder */
|
||||
class QuestgiverChooseRewardPacket {
|
||||
public:
|
||||
|
|
|
|||
|
|
@ -1771,6 +1771,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
// Quest query failed - parse failure reason
|
||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t failReason = packet.readUInt32();
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
const char* reasonStr = "Unknown";
|
||||
switch (failReason) {
|
||||
case 0: reasonStr = "Don't have quest"; break;
|
||||
|
|
@ -1794,6 +1795,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||
uint32_t questId = packet.readUInt32();
|
||||
LOG_INFO("Quest completed: questId=", questId);
|
||||
if (pendingTurnInQuestId_ == questId) {
|
||||
pendingTurnInQuestId_ = 0;
|
||||
pendingTurnInNpcGuid_ = 0;
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
}
|
||||
for (auto it = questLog_.begin(); it != questLog_.end(); ++it) {
|
||||
if (it->questId == questId) {
|
||||
questLog_.erase(it);
|
||||
|
|
@ -8765,6 +8771,20 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) {
|
|||
LOG_WARNING("Failed to parse SMSG_QUESTGIVER_REQUEST_ITEMS");
|
||||
return;
|
||||
}
|
||||
|
||||
// Expansion-safe fallback: COMPLETE_QUEST is the default flow.
|
||||
// If a server echoes REQUEST_ITEMS again while still completable,
|
||||
// request the reward explicitly once.
|
||||
if (pendingTurnInRewardRequest_ &&
|
||||
data.questId == pendingTurnInQuestId_ &&
|
||||
data.npcGuid == pendingTurnInNpcGuid_ &&
|
||||
data.isCompletable() &&
|
||||
socket) {
|
||||
auto rewardReq = QuestgiverRequestRewardPacket::build(data.npcGuid, data.questId);
|
||||
socket->send(rewardReq);
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
}
|
||||
|
||||
currentQuestRequestItems_ = data;
|
||||
questRequestItemsOpen_ = true;
|
||||
gossipWindowOpen = false;
|
||||
|
|
@ -8814,6 +8834,11 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
|
|||
return;
|
||||
}
|
||||
LOG_INFO("Quest offer reward: questId=", data.questId, " title=\"", data.title, "\"");
|
||||
if (pendingTurnInQuestId_ == data.questId) {
|
||||
pendingTurnInQuestId_ = 0;
|
||||
pendingTurnInNpcGuid_ = 0;
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
}
|
||||
currentQuestOfferReward_ = data;
|
||||
questOfferRewardOpen_ = true;
|
||||
questRequestItemsOpen_ = false;
|
||||
|
|
@ -8829,6 +8854,11 @@ void GameHandler::handleQuestOfferReward(network::Packet& packet) {
|
|||
|
||||
void GameHandler::completeQuest() {
|
||||
if (!questRequestItemsOpen_ || state != WorldState::IN_WORLD || !socket) return;
|
||||
pendingTurnInQuestId_ = currentQuestRequestItems_.questId;
|
||||
pendingTurnInNpcGuid_ = currentQuestRequestItems_.npcGuid;
|
||||
pendingTurnInRewardRequest_ = currentQuestRequestItems_.isCompletable();
|
||||
|
||||
// Default quest turn-in flow used by all branches.
|
||||
auto packet = QuestgiverCompleteQuestPacket::build(
|
||||
currentQuestRequestItems_.npcGuid, currentQuestRequestItems_.questId);
|
||||
socket->send(packet);
|
||||
|
|
@ -8837,6 +8867,7 @@ void GameHandler::completeQuest() {
|
|||
}
|
||||
|
||||
void GameHandler::closeQuestRequestItems() {
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
questRequestItemsOpen_ = false;
|
||||
currentQuestRequestItems_ = QuestRequestItemsData{};
|
||||
}
|
||||
|
|
@ -8849,6 +8880,9 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
|
|||
auto packet = QuestgiverChooseRewardPacket::build(
|
||||
npcGuid, currentQuestOfferReward_.questId, rewardIndex);
|
||||
socket->send(packet);
|
||||
pendingTurnInQuestId_ = 0;
|
||||
pendingTurnInNpcGuid_ = 0;
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
questOfferRewardOpen_ = false;
|
||||
currentQuestOfferReward_ = QuestOfferRewardData{};
|
||||
|
||||
|
|
@ -8861,6 +8895,7 @@ void GameHandler::chooseQuestReward(uint32_t rewardIndex) {
|
|||
}
|
||||
|
||||
void GameHandler::closeQuestOfferReward() {
|
||||
pendingTurnInRewardRequest_ = false;
|
||||
questOfferRewardOpen_ = false;
|
||||
currentQuestOfferReward_ = QuestOfferRewardData{};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3110,30 +3110,75 @@ bool QuestRequestItemsParser::parse(network::Packet& packet, QuestRequestItemsDa
|
|||
return true;
|
||||
}
|
||||
|
||||
// emoteDelay (uint32) + emoteId (uint32) + autoFinish (uint8) = 9 bytes
|
||||
// (same format in vanilla 1.12 and WotLK 3.3.5a)
|
||||
/*uint32_t emoteDelay =*/ packet.readUInt32();
|
||||
/*uint32_t emoteId =*/ packet.readUInt32();
|
||||
/*uint8_t autoFinish =*/ packet.readUInt8();
|
||||
struct ParsedTail {
|
||||
uint32_t requiredMoney = 0;
|
||||
uint32_t completableFlags = 0;
|
||||
std::vector<QuestRewardItem> requiredItems;
|
||||
bool ok = false;
|
||||
int score = -1;
|
||||
};
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return true;
|
||||
data.requiredMoney = packet.readUInt32();
|
||||
auto parseTail = [&](size_t startPos, bool closeFlagIsU32) -> ParsedTail {
|
||||
ParsedTail out;
|
||||
packet.setReadPos(startPos);
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return true;
|
||||
uint32_t requiredItemCount = packet.readUInt32();
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) return out;
|
||||
/*uint32_t emoteDelay =*/ packet.readUInt32();
|
||||
/*uint32_t emoteId =*/ packet.readUInt32();
|
||||
|
||||
for (uint32_t i = 0; i < requiredItemCount && requiredItemCount < 20; ++i) {
|
||||
if (packet.getReadPos() + 12 > packet.getSize()) break;
|
||||
QuestRewardItem item;
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
if (item.itemId > 0)
|
||||
data.requiredItems.push_back(item);
|
||||
if (closeFlagIsU32) {
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
/*uint32_t closeOnCancel =*/ packet.readUInt32();
|
||||
} else {
|
||||
if (packet.getReadPos() + 1 > packet.getSize()) return out;
|
||||
/*uint8_t autoFinish =*/ packet.readUInt8();
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 8 > packet.getSize()) return out;
|
||||
out.requiredMoney = packet.readUInt32();
|
||||
uint32_t requiredItemCount = packet.readUInt32();
|
||||
if (requiredItemCount > 64) return out; // sanity guard against misalignment
|
||||
|
||||
out.requiredItems.reserve(requiredItemCount);
|
||||
for (uint32_t i = 0; i < requiredItemCount; ++i) {
|
||||
if (packet.getReadPos() + 12 > packet.getSize()) return out;
|
||||
QuestRewardItem item;
|
||||
item.itemId = packet.readUInt32();
|
||||
item.count = packet.readUInt32();
|
||||
item.displayInfoId = packet.readUInt32();
|
||||
if (item.itemId != 0) out.requiredItems.push_back(item);
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return out;
|
||||
out.completableFlags = packet.readUInt32();
|
||||
out.ok = true;
|
||||
|
||||
// Prefer layouts that produce plausible quest-requirement shapes.
|
||||
out.score = 0;
|
||||
if (requiredItemCount <= 6) out.score += 4;
|
||||
if (out.requiredItems.size() == requiredItemCount) out.score += 3;
|
||||
if ((out.completableFlags & ~0x3u) == 0) out.score += 2;
|
||||
if (closeFlagIsU32) out.score += 1; // classic cores often use 32-bit here
|
||||
return out;
|
||||
};
|
||||
|
||||
size_t tailStart = packet.getReadPos();
|
||||
ParsedTail parseU8 = parseTail(tailStart, false);
|
||||
ParsedTail parseU32 = parseTail(tailStart, true);
|
||||
const ParsedTail* chosen = nullptr;
|
||||
if (parseU8.ok && parseU32.ok) {
|
||||
chosen = (parseU32.score >= parseU8.score) ? &parseU32 : &parseU8;
|
||||
} else if (parseU32.ok) {
|
||||
chosen = &parseU32;
|
||||
} else if (parseU8.ok) {
|
||||
chosen = &parseU8;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet.getReadPos() + 4 > packet.getSize()) return true;
|
||||
data.completableFlags = packet.readUInt32();
|
||||
data.requiredMoney = chosen->requiredMoney;
|
||||
data.completableFlags = chosen->completableFlags;
|
||||
data.requiredItems = chosen->requiredItems;
|
||||
|
||||
LOG_INFO("Quest request items: id=", data.questId, " title='", data.title,
|
||||
"' items=", data.requiredItems.size(), " completable=", data.isCompletable());
|
||||
|
|
@ -3209,6 +3254,13 @@ network::Packet QuestgiverCompleteQuestPacket::build(uint64_t npcGuid, uint32_t
|
|||
return packet;
|
||||
}
|
||||
|
||||
network::Packet QuestgiverRequestRewardPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD));
|
||||
packet.writeUInt64(npcGuid);
|
||||
packet.writeUInt32(questId);
|
||||
return packet;
|
||||
}
|
||||
|
||||
network::Packet QuestgiverChooseRewardPacket::build(uint64_t npcGuid, uint32_t questId, uint32_t rewardIndex) {
|
||||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_CHOOSE_REWARD));
|
||||
packet.writeUInt64(npcGuid);
|
||||
|
|
|
|||
|
|
@ -6161,49 +6161,22 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game:
|
|||
|
||||
// Helper to trim whitespace
|
||||
auto trim = [](std::string& s) {
|
||||
s.erase(0, s.find_first_not_of(" \t\n\r"));
|
||||
s.erase(s.find_last_not_of(" \t\n\r") + 1);
|
||||
const char* ws = " \t\n\r";
|
||||
size_t start = s.find_first_not_of(ws);
|
||||
if (start == std::string::npos) { s.clear(); return; }
|
||||
size_t end = s.find_last_not_of(ws);
|
||||
s = s.substr(start, end - start + 1);
|
||||
};
|
||||
|
||||
// Replace simple placeholders first
|
||||
// $n = player name
|
||||
// $p = subject pronoun (he/she/they)
|
||||
// $o = object pronoun (him/her/them)
|
||||
// $s = possessive adjective (his/her/their)
|
||||
// $S = possessive pronoun (his/hers/theirs)
|
||||
// Replace $g/$G placeholders first.
|
||||
size_t pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
char marker = result[pos + 1];
|
||||
if (marker != 'g' && marker != 'G') { pos++; continue; }
|
||||
|
||||
char code = result[pos + 1];
|
||||
std::string replacement;
|
||||
|
||||
switch (code) {
|
||||
case 'n': case 'N': replacement = playerName; break;
|
||||
case 'p': replacement = pronouns.subject; break;
|
||||
case 'o': replacement = pronouns.object; break;
|
||||
case 's': replacement = pronouns.possessive; break;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'g':
|
||||
// Handle $g separately below
|
||||
pos++;
|
||||
continue;
|
||||
default:
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace the placeholder
|
||||
result.replace(pos, 2, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// Find and replace all $g placeholders (gender-specific text)
|
||||
// Format: $g<male>:<female>; or $g<male>:<female>:<nonbinary>;
|
||||
pos = 0;
|
||||
while ((pos = result.find("$g", pos)) != std::string::npos) {
|
||||
size_t endPos = result.find(';', pos);
|
||||
if (endPos == std::string::npos) break;
|
||||
if (endPos == std::string::npos) { pos += 2; continue; }
|
||||
|
||||
std::string placeholder = result.substr(pos + 2, endPos - pos - 2);
|
||||
|
||||
|
|
@ -6261,6 +6234,46 @@ std::string GameScreen::replaceGenderPlaceholders(const std::string& text, game:
|
|||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// Replace simple placeholders.
|
||||
// $n = player name
|
||||
// $p = subject pronoun (he/she/they)
|
||||
// $o = object pronoun (him/her/them)
|
||||
// $s = possessive adjective (his/her/their)
|
||||
// $S = possessive pronoun (his/hers/theirs)
|
||||
// $b/$B = line break
|
||||
pos = 0;
|
||||
while ((pos = result.find('$', pos)) != std::string::npos) {
|
||||
if (pos + 1 >= result.length()) break;
|
||||
|
||||
char code = result[pos + 1];
|
||||
std::string replacement;
|
||||
switch (code) {
|
||||
case 'n': case 'N': replacement = playerName; break;
|
||||
case 'p': replacement = pronouns.subject; break;
|
||||
case 'o': replacement = pronouns.object; break;
|
||||
case 's': replacement = pronouns.possessive; break;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'b': case 'B': replacement = "\n"; break;
|
||||
case 'g': case 'G': pos++; continue;
|
||||
default: pos++; continue;
|
||||
}
|
||||
|
||||
result.replace(pos, 2, replacement);
|
||||
pos += replacement.length();
|
||||
}
|
||||
|
||||
// WoW markup linebreak token.
|
||||
pos = 0;
|
||||
while ((pos = result.find("|n", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
pos = 0;
|
||||
while ((pos = result.find("|N", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler
|
|||
case 's': replacement = pronouns.possessive; break;
|
||||
case 'S': replacement = pronouns.possessiveP; break;
|
||||
case 'r': replacement = pronouns.object; break;
|
||||
case 'b': replacement = "\n"; break;
|
||||
case 'b': case 'B': replacement = "\n"; break;
|
||||
case 'g': case 'G': pos++; continue;
|
||||
default: pos++; continue;
|
||||
}
|
||||
|
|
@ -110,6 +110,11 @@ std::string replaceGenderPlaceholders(const std::string& text, game::GameHandler
|
|||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
pos = 0;
|
||||
while ((pos = result.find("|N", pos)) != std::string::npos) {
|
||||
result.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue