mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-01 02:11:51 -04:00
feat(spells): name the missing crafting tool in totem cast failures
Crafting without the required tool (blacksmith hammer, mining pick, ...) surfaced the raw protocol labels 'Totems' / 'Totem category'. The server appends up to two ids naming the tool — TotemCategory.dbc ids for totem-category failures, item ids for totem failures. Read them via a shared readCastResultArgs helper across all three expansion parsers and report 'Requires Blacksmith Hammer in your inventory.', with an informative fallback when the ids can't be resolved.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<uint32_t, std::string> spellFocusNames_;
|
||||
bool spellFocusDbcLoaded_ = false;
|
||||
|
||||
// TotemCategory.dbc names, loaded lazily
|
||||
std::unordered_map<uint32_t, std::string> totemCategoryNames_;
|
||||
bool totemCategoryDbcLoaded_ = false;
|
||||
|
||||
// Spell queue (400ms window)
|
||||
uint32_t queuedSpellId_ = 0;
|
||||
uint64_t queuedSpellTarget_ = 0;
|
||||
|
||||
@@ -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; }
|
||||
};
|
||||
|
||||
@@ -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 {};
|
||||
|
||||
@@ -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<int>(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<int>(vanillaResult));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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<int>(tbcResult),
|
||||
" mappedResult=", static_cast<int>(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<int>(tbcResult),
|
||||
" mappedResult=", static_cast<int>(data.result));
|
||||
|
||||
@@ -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<int>(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)) {
|
||||
|
||||
@@ -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<int>(data.result));
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user