mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-04-17 17:43:52 +00:00
Add quest opcodes, fix gossip select packet, and NPC combat animations
Fix CMSG_GOSSIP_SELECT_OPTION missing menuId field (was causing ByteBufferException). Add 12 quest opcodes and clickable quest items in gossip dialog. NPC attack/death animation callbacks now work for both single-player and server-spawned creatures, and SMSG_ATTACKERSTATEUPDATE triggers NPC swing animations.
This commit is contained in:
parent
a20dc947e2
commit
8b98888dd2
7 changed files with 84 additions and 8 deletions
|
|
@ -321,6 +321,7 @@ public:
|
||||||
// NPC Gossip
|
// NPC Gossip
|
||||||
void interactWithNpc(uint64_t guid);
|
void interactWithNpc(uint64_t guid);
|
||||||
void selectGossipOption(uint32_t optionId);
|
void selectGossipOption(uint32_t optionId);
|
||||||
|
void selectGossipQuest(uint32_t questId);
|
||||||
void closeGossip();
|
void closeGossip();
|
||||||
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
||||||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,20 @@ enum class Opcode : uint16_t {
|
||||||
SMSG_GOSSIP_COMPLETE = 0x17E,
|
SMSG_GOSSIP_COMPLETE = 0x17E,
|
||||||
SMSG_NPC_TEXT_UPDATE = 0x180,
|
SMSG_NPC_TEXT_UPDATE = 0x180,
|
||||||
|
|
||||||
|
// ---- Phase 5: Quests ----
|
||||||
|
CMSG_QUESTGIVER_STATUS_QUERY = 0x182,
|
||||||
|
SMSG_QUESTGIVER_STATUS = 0x183,
|
||||||
|
CMSG_QUESTGIVER_HELLO = 0x184,
|
||||||
|
CMSG_QUESTGIVER_QUERY_QUEST = 0x186,
|
||||||
|
SMSG_QUESTGIVER_QUEST_DETAILS = 0x188,
|
||||||
|
CMSG_QUESTGIVER_ACCEPT_QUEST = 0x189,
|
||||||
|
CMSG_QUESTGIVER_COMPLETE_QUEST = 0x18A,
|
||||||
|
SMSG_QUESTGIVER_REQUEST_ITEMS = 0x18B,
|
||||||
|
CMSG_QUESTGIVER_REQUEST_REWARD = 0x18C,
|
||||||
|
SMSG_QUESTGIVER_OFFER_REWARD = 0x18D,
|
||||||
|
CMSG_QUESTGIVER_CHOOSE_REWARD = 0x18E,
|
||||||
|
SMSG_QUESTGIVER_QUEST_COMPLETE = 0x191,
|
||||||
|
|
||||||
// ---- Phase 5: Vendor ----
|
// ---- Phase 5: Vendor ----
|
||||||
CMSG_LIST_INVENTORY = 0x19E,
|
CMSG_LIST_INVENTORY = 0x19E,
|
||||||
SMSG_LIST_INVENTORY = 0x19F,
|
SMSG_LIST_INVENTORY = 0x19F,
|
||||||
|
|
|
||||||
|
|
@ -1161,7 +1161,7 @@ public:
|
||||||
/** CMSG_GOSSIP_SELECT_OPTION packet builder */
|
/** CMSG_GOSSIP_SELECT_OPTION packet builder */
|
||||||
class GossipSelectOptionPacket {
|
class GossipSelectOptionPacket {
|
||||||
public:
|
public:
|
||||||
static network::Packet build(uint64_t npcGuid, uint32_t optionId, const std::string& code = "");
|
static network::Packet build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code = "");
|
||||||
};
|
};
|
||||||
|
|
||||||
/** SMSG_GOSSIP_MESSAGE parser */
|
/** SMSG_GOSSIP_MESSAGE parser */
|
||||||
|
|
@ -1170,6 +1170,18 @@ public:
|
||||||
static bool parse(network::Packet& packet, GossipMessageData& data);
|
static bool parse(network::Packet& packet, GossipMessageData& data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** CMSG_QUESTGIVER_QUERY_QUEST packet builder */
|
||||||
|
class QuestgiverQueryQuestPacket {
|
||||||
|
public:
|
||||||
|
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** CMSG_QUESTGIVER_ACCEPT_QUEST packet builder */
|
||||||
|
class QuestgiverAcceptQuestPacket {
|
||||||
|
public:
|
||||||
|
static network::Packet build(uint64_t npcGuid, uint32_t questId);
|
||||||
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Phase 5: Vendor
|
// Phase 5: Vendor
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
|
||||||
|
|
@ -1125,18 +1125,27 @@ void Application::spawnNpcs() {
|
||||||
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
gameHandler->setPosition(canonical.x, canonical.y, canonical.z);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set NPC death callback for single-player combat
|
// Set NPC animation callbacks (works for both single-player and online creatures)
|
||||||
if (singlePlayerMode && gameHandler && npcManager) {
|
if (gameHandler && npcManager) {
|
||||||
auto* npcMgr = npcManager.get();
|
auto* npcMgr = npcManager.get();
|
||||||
auto* cr = renderer->getCharacterRenderer();
|
auto* cr = renderer->getCharacterRenderer();
|
||||||
gameHandler->setNpcDeathCallback([npcMgr, cr](uint64_t guid) {
|
auto* app = this;
|
||||||
|
gameHandler->setNpcDeathCallback([npcMgr, cr, app](uint64_t guid) {
|
||||||
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
||||||
|
if (instanceId == 0) {
|
||||||
|
auto it = app->creatureInstances_.find(guid);
|
||||||
|
if (it != app->creatureInstances_.end()) instanceId = it->second;
|
||||||
|
}
|
||||||
if (instanceId != 0 && cr) {
|
if (instanceId != 0 && cr) {
|
||||||
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
|
cr->playAnimation(instanceId, 1, false); // animation ID 1 = Death
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
gameHandler->setNpcSwingCallback([npcMgr, cr](uint64_t guid) {
|
gameHandler->setNpcSwingCallback([npcMgr, cr, app](uint64_t guid) {
|
||||||
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
uint32_t instanceId = npcMgr->findRenderInstanceId(guid);
|
||||||
|
if (instanceId == 0) {
|
||||||
|
auto it = app->creatureInstances_.find(guid);
|
||||||
|
if (it != app->creatureInstances_.end()) instanceId = it->second;
|
||||||
|
}
|
||||||
if (instanceId != 0 && cr) {
|
if (instanceId != 0 && cr) {
|
||||||
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
|
cr->playAnimation(instanceId, 16, false); // animation ID 16 = Attack1
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1090,6 +1090,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_INVENTORY_CHANGE_FAILURE:
|
case Opcode::SMSG_INVENTORY_CHANGE_FAILURE:
|
||||||
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
case Opcode::SMSG_GAMEOBJECT_QUERY_RESPONSE:
|
||||||
case Opcode::MSG_RAID_TARGET_UPDATE:
|
case Opcode::MSG_RAID_TARGET_UPDATE:
|
||||||
|
case Opcode::SMSG_QUESTGIVER_STATUS:
|
||||||
|
case Opcode::SMSG_QUESTGIVER_QUEST_DETAILS:
|
||||||
|
case Opcode::SMSG_QUESTGIVER_REQUEST_ITEMS:
|
||||||
|
case Opcode::SMSG_QUESTGIVER_OFFER_REWARD:
|
||||||
|
case Opcode::SMSG_QUESTGIVER_QUEST_COMPLETE:
|
||||||
case Opcode::SMSG_GROUP_SET_LEADER:
|
case Opcode::SMSG_GROUP_SET_LEADER:
|
||||||
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
LOG_DEBUG("Ignoring known opcode: 0x", std::hex, opcode, std::dec);
|
||||||
break;
|
break;
|
||||||
|
|
@ -3071,6 +3076,9 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||||
if (isPlayerAttacker && meleeSwingCallback_) {
|
if (isPlayerAttacker && meleeSwingCallback_) {
|
||||||
meleeSwingCallback_();
|
meleeSwingCallback_();
|
||||||
}
|
}
|
||||||
|
if (!isPlayerAttacker && npcSwingCallback_) {
|
||||||
|
npcSwingCallback_(data.attackerGuid);
|
||||||
|
}
|
||||||
|
|
||||||
if (data.isMiss()) {
|
if (data.isMiss()) {
|
||||||
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker);
|
||||||
|
|
@ -3519,10 +3527,17 @@ void GameHandler::interactWithNpc(uint64_t guid) {
|
||||||
|
|
||||||
void GameHandler::selectGossipOption(uint32_t optionId) {
|
void GameHandler::selectGossipOption(uint32_t optionId) {
|
||||||
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||||
auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, optionId);
|
auto packet = GossipSelectOptionPacket::build(currentGossip.npcGuid, currentGossip.menuId, optionId);
|
||||||
socket->send(packet);
|
socket->send(packet);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::selectGossipQuest(uint32_t questId) {
|
||||||
|
if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return;
|
||||||
|
auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId);
|
||||||
|
socket->send(packet);
|
||||||
|
gossipWindowOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::closeGossip() {
|
void GameHandler::closeGossip() {
|
||||||
gossipWindowOpen = false;
|
gossipWindowOpen = false;
|
||||||
currentGossip = GossipMessageData{};
|
currentGossip = GossipMessageData{};
|
||||||
|
|
|
||||||
|
|
@ -1808,9 +1808,10 @@ network::Packet GossipHelloPacket::build(uint64_t npcGuid) {
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optionId, const std::string& code) {
|
network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t menuId, uint32_t optionId, const std::string& code) {
|
||||||
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_GOSSIP_SELECT_OPTION));
|
||||||
packet.writeUInt64(npcGuid);
|
packet.writeUInt64(npcGuid);
|
||||||
|
packet.writeUInt32(menuId);
|
||||||
packet.writeUInt32(optionId);
|
packet.writeUInt32(optionId);
|
||||||
if (!code.empty()) {
|
if (!code.empty()) {
|
||||||
packet.writeString(code);
|
packet.writeString(code);
|
||||||
|
|
@ -1818,6 +1819,22 @@ network::Packet GossipSelectOptionPacket::build(uint64_t npcGuid, uint32_t optio
|
||||||
return packet;
|
return packet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
network::Packet QuestgiverQueryQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
||||||
|
packet.writeUInt64(npcGuid);
|
||||||
|
packet.writeUInt32(questId);
|
||||||
|
packet.writeUInt8(1); // isDialogContinued = 1 (from gossip)
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
|
network::Packet QuestgiverAcceptQuestPacket::build(uint64_t npcGuid, uint32_t questId) {
|
||||||
|
network::Packet packet(static_cast<uint16_t>(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||||||
|
packet.writeUInt64(npcGuid);
|
||||||
|
packet.writeUInt32(questId);
|
||||||
|
packet.writeUInt32(0); // unused
|
||||||
|
return packet;
|
||||||
|
}
|
||||||
|
|
||||||
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
bool GossipMessageParser::parse(network::Packet& packet, GossipMessageData& data) {
|
||||||
data.npcGuid = packet.readUInt64();
|
data.npcGuid = packet.readUInt64();
|
||||||
data.menuId = packet.readUInt32();
|
data.menuId = packet.readUInt32();
|
||||||
|
|
|
||||||
|
|
@ -1608,7 +1608,15 @@ void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:");
|
ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.3f, 1.0f), "Quests:");
|
||||||
for (const auto& quest : gossip.quests) {
|
for (const auto& quest : gossip.quests) {
|
||||||
ImGui::BulletText("[%d] %s", quest.questLevel, quest.title.c_str());
|
ImGui::PushID(static_cast<int>(quest.questId));
|
||||||
|
char qlabel[256];
|
||||||
|
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.3f, 1.0f));
|
||||||
|
if (ImGui::Selectable(qlabel)) {
|
||||||
|
gameHandler.selectGossipQuest(quest.questId);
|
||||||
|
}
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
ImGui::PopID();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue