feat: add DISPEL and INTERRUPT combat log entries for dispel/spellsteal/interrupt events
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run

This commit is contained in:
Kelsi 2026-03-13 12:03:07 -07:00
parent ffef3dda7e
commit e51b215f85
3 changed files with 70 additions and 24 deletions

View file

@ -52,7 +52,8 @@ struct CombatTextEntry {
enum Type : uint8_t {
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER
ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER,
DISPEL, INTERRUPT
};
Type type;
int32_t amount = 0;

View file

@ -6160,20 +6160,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint32_t dispelSpell =*/ packet.readUInt32();
uint8_t isStolen = packet.readUInt8();
uint32_t count = packet.readUInt32();
// Collect first dispelled spell id/name; process all entries for combat log
// Each entry: uint32 spellId + uint8 isPositive (5 bytes in WotLK/TBC/Classic)
uint32_t firstDispelledId = 0;
std::string firstSpellName;
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 5; ++i) {
uint32_t dispelledId = packet.readUInt32();
/*uint8_t isPositive =*/ packet.readUInt8();
if (i == 0) {
firstDispelledId = dispelledId;
const std::string& nm = getSpellName(dispelledId);
firstSpellName = nm.empty() ? ("spell " + std::to_string(dispelledId)) : nm;
}
}
// Show system message if player was victim or caster
if (victimGuid == playerGuid || casterGuid == playerGuid) {
const char* verb = isStolen ? "stolen" : "dispelled";
// Collect first dispelled spell name for the message
// Each entry: uint32 spellId + uint8 isPositive (5 bytes in WotLK/TBC/Classic)
std::string firstSpellName;
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 5; ++i) {
uint32_t dispelledId = packet.readUInt32();
/*uint8_t isPositive =*/ packet.readUInt8();
if (i == 0) {
const std::string& nm = getSpellName(dispelledId);
firstSpellName = nm.empty() ? ("spell " + std::to_string(dispelledId)) : nm;
}
}
if (!firstSpellName.empty()) {
char buf[256];
if (victimGuid == playerGuid && casterGuid != playerGuid)
@ -6184,6 +6186,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
std::snprintf(buf, sizeof(buf), "%s %s.", firstSpellName.c_str(), verb);
addSystemChatMessage(buf);
}
// Add dispel event to combat log
if (firstDispelledId != 0) {
bool isPlayerCaster = (casterGuid == playerGuid);
addCombatText(CombatTextEntry::DISPEL, 0, firstDispelledId, isPlayerCaster, 0,
casterGuid, victimGuid);
}
}
packet.setReadPos(packet.getSize());
break;
@ -6198,7 +6206,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) {
packet.setReadPos(packet.getSize()); break;
}
/*uint64_t stealVictim =*/ stealTbcLike
uint64_t stealVictim = stealTbcLike
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) {
packet.setReadPos(packet.getSize()); break;
@ -6211,22 +6219,33 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint32_t stealSpellId =*/ packet.readUInt32();
/*uint8_t isStolen =*/ packet.readUInt8();
uint32_t stealCount = packet.readUInt32();
// Show feedback only when we are the caster (we stole something)
if (stealCaster == playerGuid) {
std::string stolenName;
for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= 5; ++i) {
uint32_t stolenId = packet.readUInt32();
/*uint8_t isPos =*/ packet.readUInt8();
if (i == 0) {
const std::string& nm = getSpellName(stolenId);
stolenName = nm.empty() ? ("spell " + std::to_string(stolenId)) : nm;
}
// Collect stolen spell info; show feedback when we are caster or victim
uint32_t firstStolenId = 0;
std::string stolenName;
for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= 5; ++i) {
uint32_t stolenId = packet.readUInt32();
/*uint8_t isPos =*/ packet.readUInt8();
if (i == 0) {
firstStolenId = stolenId;
const std::string& nm = getSpellName(stolenId);
stolenName = nm.empty() ? ("spell " + std::to_string(stolenId)) : nm;
}
}
if (stealCaster == playerGuid || stealVictim == playerGuid) {
if (!stolenName.empty()) {
char buf[256];
std::snprintf(buf, sizeof(buf), "You stole %s.", stolenName.c_str());
if (stealCaster == playerGuid)
std::snprintf(buf, sizeof(buf), "You stole %s.", stolenName.c_str());
else
std::snprintf(buf, sizeof(buf), "%s was stolen.", stolenName.c_str());
addSystemChatMessage(buf);
}
// Add dispel/steal to combat log using DISPEL type (isStolen=true for steals)
if (firstStolenId != 0) {
bool isPlayerCaster = (stealCaster == playerGuid);
addCombatText(CombatTextEntry::DISPEL, 0, firstStolenId, isPlayerCaster, 0,
stealCaster, stealVictim);
}
}
packet.setReadPos(packet.getSize());
break;
@ -6383,6 +6402,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint32_t icSpellId = packet.readUInt32();
// Clear the interrupted unit's cast bar immediately
unitCastStates_.erase(icTarget);
// Record interrupt in combat log when player is involved
if (isPlayerCaster || icTarget == playerGuid)
addCombatText(CombatTextEntry::INTERRUPT, 0, icSpellId, isPlayerCaster, 0,
exeCaster, icTarget);
LOG_DEBUG("SMSG_SPELLLOGEXECUTE INTERRUPT_CAST: spell=", exeSpellId,
" interrupted=", icSpellId, " target=0x", std::hex, icTarget, std::dec);
}

View file

@ -20265,6 +20265,28 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "Proc triggered");
color = ImVec4(1.0f, 0.85f, 0.3f, 1.0f);
break;
case T::DISPEL:
if (spell && e.isPlayerSource)
snprintf(desc, sizeof(desc), "You dispel %s from %s", spell, tgt);
else if (spell)
snprintf(desc, sizeof(desc), "%s dispels %s from %s", src, spell, tgt);
else if (e.isPlayerSource)
snprintf(desc, sizeof(desc), "You dispel from %s", tgt);
else
snprintf(desc, sizeof(desc), "%s dispels from %s", src, tgt);
color = ImVec4(0.6f, 0.9f, 1.0f, 1.0f);
break;
case T::INTERRUPT:
if (spell && e.isPlayerSource)
snprintf(desc, sizeof(desc), "You interrupt %s's %s", tgt, spell);
else if (spell)
snprintf(desc, sizeof(desc), "%s interrupts %s's %s", src, tgt, spell);
else if (e.isPlayerSource)
snprintf(desc, sizeof(desc), "You interrupt %s", tgt);
else
snprintf(desc, sizeof(desc), "%s interrupted", tgt);
color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f);
break;
default:
snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", (int)e.type, e.amount);
color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f);