diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 3f7250b6..ee535eec 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2171,6 +2171,9 @@ public: // SpellFocusObject.dbc name ("Anvil", "Cooking Fire", ...) for // requires-spell-focus cast failures; empty if unknown. const std::string& getSpellFocusName(uint32_t focusId) const; + // TotemCategory.dbc name ("Blacksmith Hammer", "Mining Pick", ...) for + // totem-category cast failures; empty if unknown. + const std::string& getTotemCategoryName(uint32_t categoryId) const; const int32_t* getSpellEffectBasePoints(uint32_t spellId) const; float getSpellDuration(uint32_t spellId) const; std::string getEnchantName(uint32_t enchantId) const; diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index fd1b25fb..4ad7e982 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -136,19 +136,17 @@ public: * WotLK: castCount(u8) + spellId(u32) + result(u8) * TBC/Classic: spellId(u32) + result(u8) (no castCount prefix). * Classic/TBC result enums have no SUCCESS entry, so parsers shift +1. - * miscArg receives the trailing SpellFocusObject id for - * requires-spell-focus failures (0 otherwise). + * miscArg/miscArg2 receive the trailing ids of spell-focus and totem + * failures (0 otherwise) — see readCastResultArgs. */ virtual bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result, - uint32_t& miscArg) { + uint32_t& miscArg, uint32_t& miscArg2) { // WotLK default: skip castCount, read spellId + result if (packet.getSize() - packet.getReadPos() < 6) return false; packet.readUInt8(); // castCount spellId = packet.readUInt32(); result = packet.readUInt8(); - miscArg = 0; - if (result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - miscArg = packet.readUInt32(); + readCastResultArgs(packet, result, miscArg, miscArg2); return true; } @@ -359,7 +357,7 @@ public: bool parseGossipMessage(network::Packet& packet, GossipMessageData& data) override; // TBC 2.4.3 SMSG_CAST_RESULT: spellId(u32) + result(u8) + castCount(u8) bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result, - uint32_t& miscArg) override; + uint32_t& miscArg, uint32_t& miscArg2) override; // TBC 2.4.3 SMSG_CAST_FAILED: spellId(u32) + result(u8) + castCount(u8) bool parseCastFailed(network::Packet& packet, CastFailedData& data) override; // TBC 2.4.3 SMSG_INITIAL_SPELLS: uint16 spellId + uint16 unk per entry. @@ -444,7 +442,7 @@ public: uint64_t targetGuid = 0, uint64_t itemTargetGuid = 0) override; bool parseCastFailed(network::Packet& packet, CastFailedData& data) override; bool parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result, - uint32_t& miscArg) override; + uint32_t& miscArg, uint32_t& miscArg2) override; bool parseMessageChat(network::Packet& packet, MessageChatData& data) override; bool parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) override; // Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName string that TBC/WotLK include diff --git a/include/game/spell_handler.hpp b/include/game/spell_handler.hpp index bd65bece..1a4f9400 100644 --- a/include/game/spell_handler.hpp +++ b/include/game/spell_handler.hpp @@ -86,6 +86,10 @@ public: // requires-spell-focus cast failures; empty if unknown. const std::string& getSpellFocusName(uint32_t focusId); + // TotemCategory.dbc name ("Blacksmith Hammer", "Mining Pick", ...) for + // totem-category cast failures; empty if unknown. + const std::string& getTotemCategoryName(uint32_t categoryId); + // Spell queue (400ms window) uint32_t getQueuedSpellId() const { return queuedSpellId_; } void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; } @@ -368,6 +372,10 @@ private: std::unordered_map spellFocusNames_; bool spellFocusDbcLoaded_ = false; + // TotemCategory.dbc names, loaded lazily + std::unordered_map totemCategoryNames_; + bool totemCategoryDbcLoaded_ = false; + // Spell queue (400ms window) uint32_t queuedSpellId_ = 0; uint64_t queuedSpellTarget_ = 0; diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 60543fdd..8c15d3b8 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1859,16 +1859,41 @@ public: }; /** SMSG_CAST_FAILED data */ -// Normalized (WotLK-numbered) SpellCastResult whose SMSG_CAST_RESULT / -// SMSG_CAST_FAILED payload carries a trailing uint32 SpellFocusObject id -// (forge, anvil, cooking fire, ...) that the client can surface. +// Normalized (WotLK-numbered) SpellCastResults whose SMSG_CAST_RESULT / +// SMSG_CAST_FAILED payload carries trailing uint32 ids the client can surface: +// - requires-spell-focus: one SpellFocusObject id (forge, anvil, cooking fire) +// - totems / totem-category: up to two ids naming the missing crafting tool — +// item ids for totems, TotemCategory.dbc ids (Blacksmith Hammer, Mining +// Pick, ...) for totem-category. inline constexpr uint8_t kCastResultRequiresSpellFocus = 102; +inline constexpr uint8_t kCastResultTotemCategory = 130; +inline constexpr uint8_t kCastResultTotems = 131; + +/// Read the optional trailing ids of a cast failure payload (see above). +inline void readCastResultArgs(network::Packet& packet, uint8_t result, + uint32_t& arg1, uint32_t& arg2) { + arg1 = 0; + arg2 = 0; + switch (result) { + case kCastResultRequiresSpellFocus: + if (packet.hasRemaining(4)) arg1 = packet.readUInt32(); + break; + case kCastResultTotems: + case kCastResultTotemCategory: + if (packet.hasRemaining(4)) arg1 = packet.readUInt32(); + if (packet.hasRemaining(4)) arg2 = packet.readUInt32(); + break; + default: + break; + } +} struct CastFailedData { uint8_t castCount = 0; uint32_t spellId = 0; uint8_t result = 0; - uint32_t miscArg = 0; // SpellFocusObject id when result == kCastResultRequiresSpellFocus + uint32_t miscArg = 0; // first trailing id (see readCastResultArgs) + uint32_t miscArg2 = 0; // second trailing id (totem failures only) bool isValid() const { return spellId != 0; } }; diff --git a/src/game/game_handler_callbacks.cpp b/src/game/game_handler_callbacks.cpp index 38205efc..e5b7f668 100644 --- a/src/game/game_handler_callbacks.cpp +++ b/src/game/game_handler_callbacks.cpp @@ -2761,6 +2761,11 @@ const std::string& GameHandler::getSpellFocusName(uint32_t focusId) const { return EMPTY_STRING; } +const std::string& GameHandler::getTotemCategoryName(uint32_t categoryId) const { + if (spellHandler_) return spellHandler_->getTotemCategoryName(categoryId); + return EMPTY_STRING; +} + std::string GameHandler::getEnchantName(uint32_t enchantId) const { if (spellHandler_) return spellHandler_->getEnchantName(enchantId); return {}; diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index ca73d261..04b0e43c 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -999,9 +999,8 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa // WotLK enum starts at 0=SUCCESS, 1=AFFECTING_COMBAT. // Shift +1 to align with WotLK result strings. data.result = vanillaResult + 1; - // Requires-spell-focus failures append the SpellFocusObject id - if (data.result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - data.miscArg = packet.readUInt32(); + // Spell-focus and totem failures append ids naming the missing station/tool + readCastResultArgs(packet, data.result, data.miscArg, data.miscArg2); LOG_DEBUG("[Classic] Cast failed: spell=", data.spellId, " vanillaResult=", static_cast(vanillaResult)); return true; } @@ -1013,16 +1012,14 @@ bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedDa // align with WotLK's getSpellCastResultString table. // ============================================================================ bool ClassicPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result, - uint32_t& miscArg) { + uint32_t& miscArg, uint32_t& miscArg2) { if (!packet.hasRemaining(5)) return false; spellId = packet.readUInt32(); uint8_t vanillaResult = packet.readUInt8(); // Shift +1: Vanilla result 0=AFFECTING_COMBAT maps to WotLK result 1=AFFECTING_COMBAT result = vanillaResult + 1; - // Requires-spell-focus failures append the SpellFocusObject id - miscArg = 0; - if (result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - miscArg = packet.readUInt32(); + // Spell-focus and totem failures append ids naming the missing station/tool + readCastResultArgs(packet, result, miscArg, miscArg2); LOG_DEBUG("[Classic] Cast result: spell=", spellId, " vanillaResult=", static_cast(vanillaResult)); return true; } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 6b433a10..70cfbfdf 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1501,16 +1501,14 @@ static uint8_t translateTbcCastFailure(uint8_t tbcResult) { // TBC format: spellId(u32) + result(u8) + castCount(u8). // ============================================================================ bool TbcPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result, - uint32_t& miscArg) { + uint32_t& miscArg, uint32_t& miscArg2) { if (!packet.hasRemaining(5)) return false; spellId = packet.readUInt32(); uint8_t tbcResult = packet.readUInt8(); result = translateTbcCastFailure(tbcResult); if (packet.hasRemaining(1)) packet.readUInt8(); // castCount - // Requires-spell-focus failures append the SpellFocusObject id - miscArg = 0; - if (result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - miscArg = packet.readUInt32(); + // Spell-focus and totem failures append ids naming the missing station/tool + readCastResultArgs(packet, result, miscArg, miscArg2); LOG_DEBUG("[TBC] Cast result: spell=", spellId, " tbcResult=", static_cast(tbcResult), " mappedResult=", static_cast(result)); @@ -1529,9 +1527,8 @@ bool TbcPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& uint8_t tbcResult = packet.readUInt8(); data.result = translateTbcCastFailure(tbcResult); if (packet.hasRemaining(1)) data.castCount = packet.readUInt8(); - // Requires-spell-focus failures append the SpellFocusObject id - if (data.result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - data.miscArg = packet.readUInt32(); + // Spell-focus and totem failures append ids naming the missing station/tool + readCastResultArgs(packet, data.result, data.miscArg, data.miscArg2); LOG_DEBUG("[TBC] Cast failed: spell=", data.spellId, " tbcResult=", static_cast(tbcResult), " mappedResult=", static_cast(data.result)); diff --git a/src/game/spell_handler.cpp b/src/game/spell_handler.cpp index 8ec4d9ad..188fb31d 100644 --- a/src/game/spell_handler.cpp +++ b/src/game/spell_handler.cpp @@ -63,7 +63,8 @@ bool isBandageSpell(const GameHandler& owner, uint32_t spellId) { } std::string castFailureMessage(const GameHandler& owner, uint32_t spellId, - uint8_t result, int powerType, uint32_t miscArg = 0) { + uint8_t result, int powerType, uint32_t miscArg = 0, + uint32_t miscArg2 = 0) { // Bandages use a hidden target aura to enforce the Recently Bandaged // lockout. Exposing the protocol label ("Target aurastate") gives the // player no actionable information. @@ -85,6 +86,27 @@ std::string castFailureMessage(const GameHandler& owner, uint32_t spellId, return "You must be near the right crafting station (forge, anvil, cooking fire, ...)."; } + // "Totems" / "Totem category" mean a required crafting tool is missing + // (blacksmith hammer, mining pick, ...). Name it from the packet's ids: + // TotemCategory.dbc entries for totem-category, item ids for totems. + if (result == kCastResultTotemCategory || result == kCastResultTotems) { + std::string tools; + for (uint32_t id : {miscArg, miscArg2}) { + if (id == 0) continue; + std::string name; + if (result == kCastResultTotemCategory) { + name = owner.getTotemCategoryName(id); + } else if (const auto* info = owner.getItemInfo(id); info && info->valid) { + name = info->name; + } + if (name.empty()) continue; + if (!tools.empty()) tools += " and "; + tools += name; + } + if (!tools.empty()) return "Requires " + tools + " in your inventory."; + return "Requires a crafting tool you don't have (blacksmith hammer, mining pick, ...)."; + } + const char* reason = getSpellCastResultString(result, powerType); return reason ? reason : ("Spell cast failed (error " + std::to_string(result) + ")"); @@ -1181,8 +1203,14 @@ void SpellHandler::handleCastFailed(network::Packet& packet) { if (data.result == kSpellFailedNotReady) { seedCooldownFromSpellInfo(data.spellId); } + // Totem failures name tool item ids; request their info so a retry of the + // craft can show the tool's name even if it wasn't cached yet. + if (data.result == kCastResultTotems) { + if (data.miscArg != 0) owner_.ensureItemInfo(data.miscArg); + if (data.miscArg2 != 0) owner_.ensureItemInfo(data.miscArg2); + } std::string errMsg = castFailureMessage(owner_, data.spellId, data.result, powerType, - data.miscArg); + data.miscArg, data.miscArg2); if (gatherCast) { errMsg = gatherCastFailureMessage(data.result, errMsg); if (shouldDespawnGatherTarget(data.result)) { @@ -2389,6 +2417,31 @@ const std::string& SpellHandler::getSpellFocusName(uint32_t focusId) { return it != spellFocusNames_.end() ? it->second : kEmpty; } +const std::string& SpellHandler::getTotemCategoryName(uint32_t categoryId) { + static const std::string kEmpty; + if (!totemCategoryDbcLoaded_) { + totemCategoryDbcLoaded_ = true; + auto* am = owner_.services().assetManager; + if (am && am->isInitialized()) { + // TBC/WotLK only — absent in Vanilla, where totem failures carry + // item ids instead of category ids. + auto dbc = am->loadDBC("TotemCategory.dbc"); + // ID(0) + Name locstring whose English text sits at field 1. + if (dbc && dbc->isLoaded() && dbc->getFieldCount() >= 2) { + for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) { + uint32_t id = dbc->getUInt32(i, 0); + std::string name = dbc->getString(i, 1); + if (id != 0 && !name.empty()) + totemCategoryNames_[id] = std::move(name); + } + LOG_INFO("Loaded ", totemCategoryNames_.size(), " totem category names"); + } + } + } + auto it = totemCategoryNames_.find(categoryId); + return it != totemCategoryNames_.end() ? it->second : kEmpty; +} + uint32_t SpellHandler::tradeskillOpenerSkillLine(uint32_t spellId) { owner_.loadSpellNameCache(); owner_.loadSkillLineDbc(); @@ -2755,8 +2808,9 @@ void SpellHandler::handleCastResult(network::Packet& packet) { uint32_t castResultSpellId = 0; uint8_t castResult = 0; uint32_t castResultMiscArg = 0; + uint32_t castResultMiscArg2 = 0; if (owner_.getPacketParsers()->parseCastResult(packet, castResultSpellId, castResult, - castResultMiscArg)) { + castResultMiscArg, castResultMiscArg2)) { LOG_DEBUG("SMSG_CAST_RESULT: spellId=", castResultSpellId, " result=", static_cast(castResult)); if (castResult != 0) { const uint64_t gatherGoGuid = owner_.lastInteractedGoGuidRef(); @@ -2774,9 +2828,15 @@ void SpellHandler::handleCastResult(network::Packet& packet) { if (castResult == kSpellFailedNotReady) { seedCooldownFromSpellInfo(castResultSpellId); } + // Totem failures name tool item ids; request their info so a retry + // of the craft can show the tool's name even if it wasn't cached. + if (castResult == kCastResultTotems) { + if (castResultMiscArg != 0) owner_.ensureItemInfo(castResultMiscArg); + if (castResultMiscArg2 != 0) owner_.ensureItemInfo(castResultMiscArg2); + } std::string errMsg = castFailureMessage(owner_, castResultSpellId, castResult, playerPowerType, - castResultMiscArg); + castResultMiscArg, castResultMiscArg2); if (gatherCast) { errMsg = gatherCastFailureMessage(castResult, errMsg); if (shouldDespawnGatherTarget(castResult)) { diff --git a/src/game/world_packets_entity.cpp b/src/game/world_packets_entity.cpp index 03be7c61..e6cbdcef 100644 --- a/src/game/world_packets_entity.cpp +++ b/src/game/world_packets_entity.cpp @@ -1112,10 +1112,9 @@ bool CastFailedParser::parse(network::Packet& packet, CastFailedData& data) { data.castCount = packet.readUInt8(); data.spellId = packet.readUInt32(); data.result = packet.readUInt8(); - // Requires-spell-focus failures append the SpellFocusObject id (forge, - // anvil, cooking fire, ...) so the client can name the missing object. - if (data.result == kCastResultRequiresSpellFocus && packet.hasRemaining(4)) - data.miscArg = packet.readUInt32(); + // Spell-focus and totem failures append ids naming the missing + // station/tool so the client can surface them. + readCastResultArgs(packet, data.result, data.miscArg, data.miscArg2); LOG_INFO("Cast failed: spell=", data.spellId, " result=", static_cast(data.result)); return true; }