Compare commits

...

36 commits

Author SHA1 Message Date
Kelsi
c1b66f73c5 fix(vulkan): defer resource frees until frame fence 2026-03-14 03:25:52 -07:00
Kelsi
6a7071fd64 fix(combatlog): validate classic spell damage and heal GUIDs 2026-03-14 02:10:14 -07:00
Kelsi
011a148105 fix(combatlog): validate packed damage shield GUIDs 2026-03-14 02:01:07 -07:00
Kelsi
f6d8c01779 fix(combatlog): validate packed spell miss GUIDs 2026-03-14 01:54:01 -07:00
Kelsi
b059bbcf89 fix(combatlog): parse classic spell damage shield GUIDs as packed 2026-03-14 01:47:06 -07:00
Kelsi
468880e2c8 fix(combatlog): validate packed resist log GUIDs 2026-03-14 01:39:53 -07:00
Kelsi
0968a11234 fix(combatlog): validate packed instakill GUIDs 2026-03-14 01:32:45 -07:00
Kelsi
a48f6d1044 fix(combatlog): parse classic immune log GUIDs as packed 2026-03-14 01:25:47 -07:00
Kelsi
0fc887a3d2 fix(combatlog): validate packed proc log GUIDs 2026-03-14 01:18:28 -07:00
Kelsi
dbdc45a8a9 fix(combatlog): validate packed dispel-family GUIDs 2026-03-14 01:10:43 -07:00
Kelsi
bd8c46fa49 fix(combatlog): parse classic dispel failed GUIDs as packed 2026-03-14 01:00:56 -07:00
Kelsi
1fa2cbc64e fix(combatlog): parse classic dispel and spellsteal GUIDs as packed 2026-03-14 00:53:42 -07:00
Kelsi
fd8ea4e69e fix(combatlog): parse classic proc log GUIDs as packed 2026-03-14 00:45:50 -07:00
Kelsi
8ba5ca5337 fix(combatlog): parse classic instakill log GUIDs as packed 2026-03-14 00:38:22 -07:00
Kelsi
9c3b5d17cf fix(combatlog): parse classic resist log GUIDs as packed 2026-03-14 00:31:35 -07:00
Kelsi
ed5134d601 fix(combatlog): parse classic spelllogexecute GUIDs as packed 2026-03-14 00:24:21 -07:00
Kelsi
a147347393 fix(combattext): honor health leech multipliers 2026-03-14 00:16:28 -07:00
Kelsi
209f8db382 fix(combattext): honor power drain multipliers 2026-03-14 00:06:05 -07:00
Kelsi
64483a31d5 fix(combattext): show power drain separately from damage 2026-03-13 23:56:44 -07:00
Kelsi
e9d2c43191 fix(combatlog): validate classic spelllogexecute packed GUIDs 2026-03-13 23:47:57 -07:00
Kelsi
842771cb10 fix(combatlog): validate tbc spelllogexecute effect GUIDs 2026-03-13 23:40:39 -07:00
Kelsi
57265bfa4f fix(combattext): render evade results explicitly 2026-03-13 23:32:57 -07:00
Kelsi
6095170167 fix(combattext): correct reflect miss floating text 2026-03-13 23:24:09 -07:00
Kelsi
3f1083e9b5 fix(combatlog): consume reflect payload in spell-go miss entries 2026-03-13 23:15:56 -07:00
Kelsi
77d53baa09 fix(combattext): render deflect and reflect miss events 2026-03-13 23:08:49 -07:00
Kelsi
dceaf8f1ac fix(combattext): show aura names for dispel and spellsteal 2026-03-13 23:00:49 -07:00
Kelsi
5392243575 fix(combatlog): stop treating spell break logs as damage shield hits 2026-03-13 22:53:04 -07:00
Kelsi
3e4708fe15 fix(combatlog): parse classic spell miss GUIDs as packed 2026-03-13 22:45:59 -07:00
Kelsi
46b297aacc fix(combatlog): consume reflect payload in spell miss logs 2026-03-13 22:38:35 -07:00
Kelsi
21762485ea fix(combatlog): guard truncated spell energize packets 2026-03-13 22:30:25 -07:00
Kelsi
3ef5b546fb fix(combatlog): render instakill events explicitly 2026-03-13 22:22:00 -07:00
Kelsi
5be55b1b14 fix(combatlog): validate full TBC spell-go header 2026-03-13 22:14:04 -07:00
Kelsi
cf68c156f1 fix(combatlog): accept short packed spell-go packets 2026-03-13 21:16:24 -07:00
Kelsi
16c8a2fd33 fix(combatlog): parse packed spell-go hit target GUIDs 2026-03-13 21:08:00 -07:00
Kelsi
d4d876a563 fix(combatlog): list all dispelled and stolen auras in system messages 2026-03-13 21:00:34 -07:00
Kelsi
c5e7dde931 fix(combatlog): parse proc log GUIDs in classic and tbc 2026-03-13 20:52:34 -07:00
11 changed files with 580 additions and 219 deletions

View file

@ -51,16 +51,16 @@ struct ActionBarSlot {
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,
DISPEL, STEAL, INTERRUPT
EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER,
DISPEL, STEAL, INTERRUPT, INSTAKILL
};
Type type;
int32_t amount = 0;
uint32_t spellId = 0;
float age = 0.0f; // Seconds since creation (for fadeout)
bool isPlayerSource = false; // True if player dealt this
uint8_t powerType = 0; // For ENERGIZE: 0=mana,1=rage,2=focus,3=energy,6=runicpower
uint8_t powerType = 0; // For ENERGIZE/POWER_DRAIN: 0=mana,1=rage,2=focus,3=energy,6=runicpower
static constexpr float LIFETIME = 2.5f;
bool isExpired() const { return age >= LIFETIME; }

View file

@ -1858,7 +1858,7 @@ public:
/** SMSG_SPELL_GO data (simplified) */
struct SpellGoMissEntry {
uint64_t targetGuid = 0;
uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST
uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST 11=REFLECT
};
struct SpellGoData {

View file

@ -57,6 +57,15 @@ public:
void pollUploadBatches(); // Check completed async uploads, free staging buffers
void waitAllUploads(); // Block until all in-flight uploads complete
// Defer resource destruction until it is safe with multiple frames in flight.
//
// This queues work to run after the fence for the *current frame slot* has
// signaled the next time we enter beginFrame() for that slot (i.e. after
// MAX_FRAMES_IN_FLIGHT submissions). Use this for resources that may still
// be referenced by command buffers submitted in the previous frame(s),
// such as descriptor sets and buffers freed during streaming/unload.
void deferAfterFrameFence(std::function<void()>&& fn);
// Accessors
VkInstance getInstance() const { return instance; }
VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; }
@ -173,6 +182,9 @@ private:
};
std::vector<InFlightBatch> inFlightBatches_;
void runDeferredCleanup(uint32_t frameIndex);
std::vector<std::function<void()>> deferredCleanup_[MAX_FRAMES_IN_FLIGHT];
// Depth buffer (shared across all framebuffers)
VkImage depthImage = VK_NULL_HANDLE;
VkImageView depthImageView = VK_NULL_HANDLE;

View file

@ -100,6 +100,22 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) {
raw[0] == 'n' || raw[0] == 'N');
}
bool hasFullPackedGuid(const network::Packet& packet) {
if (packet.getReadPos() >= packet.getSize()) {
return false;
}
const auto& rawData = packet.getData();
const uint8_t mask = rawData[packet.getReadPos()];
size_t guidBytes = 1;
for (int bit = 0; bit < 8; ++bit) {
if ((mask & (1u << bit)) != 0) {
++guidBytes;
}
}
return packet.getSize() - packet.getReadPos() >= guidBytes;
}
std::string formatCopperAmount(uint32_t amount) {
uint32_t gold = amount / 10000;
uint32_t silver = (amount / 100) % 100;
@ -123,6 +139,40 @@ std::string formatCopperAmount(uint32_t amount) {
return oss.str();
}
std::string displaySpellName(GameHandler& handler, uint32_t spellId) {
if (spellId == 0) return {};
const std::string& name = handler.getSpellName(spellId);
if (!name.empty()) return name;
return "spell " + std::to_string(spellId);
}
std::string formatSpellNameList(GameHandler& handler,
const std::vector<uint32_t>& spellIds,
size_t maxShown = 3) {
if (spellIds.empty()) return {};
const size_t shownCount = std::min(spellIds.size(), maxShown);
std::ostringstream oss;
for (size_t i = 0; i < shownCount; ++i) {
if (i > 0) {
if (shownCount == 2) {
oss << " and ";
} else if (i == shownCount - 1) {
oss << ", and ";
} else {
oss << ", ";
}
}
oss << displaySpellName(handler, spellIds[i]);
}
if (spellIds.size() > shownCount) {
oss << ", and " << (spellIds.size() - shownCount) << " more";
}
return oss.str();
}
bool readCStringAt(const std::vector<uint8_t>& data, size_t start, std::string& out, size_t& nextPos) {
out.clear();
if (start >= data.size()) return false;
@ -2052,17 +2102,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
// ---- Spell proc resist log ----
case Opcode::SMSG_PROCRESIST: {
// WotLK: packed_guid caster + packed_guid victim + uint32 spellId + ...
// TBC/Classic: uint64 caster + uint64 victim + uint32 spellId + ...
const bool prTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
// WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + ...
// TBC: uint64 caster + uint64 victim + uint32 spellId + ...
const bool prUsesFullGuid = isActiveExpansion("tbc");
auto readPrGuid = [&]() -> uint64_t {
if (prTbcLike)
if (prUsesFullGuid)
return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0;
return UpdateObjectParser::readPackedGuid(packet);
};
if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break;
if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)
|| (!prUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t caster = readPrGuid();
if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break;
if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)
|| (!prUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victim = readPrGuid();
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t spellId = packet.readUInt32();
@ -2646,33 +2702,40 @@ void GameHandler::handlePacket(network::Packet& packet) {
// ---- Spell log miss ----
case Opcode::SMSG_SPELLLOGMISS: {
// All expansions: uint32 spellId first.
// WotLK: spellId(4) + packed_guid caster + uint8 unk + uint32 count
// WotLK/Classic: spellId(4) + packed_guid caster + uint8 unk + uint32 count
// + count × (packed_guid victim + uint8 missInfo)
// [missInfo==11(REFLECT): + uint32 reflectSpellId + uint8 reflectResult]
// TBC/Classic: spellId(4) + uint64 caster + uint8 unk + uint32 count
// TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count
// + count × (uint64 victim + uint8 missInfo)
const bool spellMissTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
// All expansions append uint32 reflectSpellId + uint8 reflectResult when
// missInfo==11 (REFLECT).
const bool spellMissUsesFullGuid = isActiveExpansion("tbc");
auto readSpellMissGuid = [&]() -> uint64_t {
if (spellMissTbcLike)
if (spellMissUsesFullGuid)
return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0;
return UpdateObjectParser::readPackedGuid(packet);
};
// spellId prefix present in all expansions
if (packet.getSize() - packet.getReadPos() < 4) break;
uint32_t spellId = packet.readUInt32();
if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 8 : 1)) break;
if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 8u : 1u)
|| (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = readSpellMissGuid();
if (packet.getSize() - packet.getReadPos() < 5) break;
/*uint8_t unk =*/ packet.readUInt8();
uint32_t count = packet.readUInt32();
count = std::min(count, 32u);
for (uint32_t i = 0; i < count; ++i) {
if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 9u : 2u)) break;
if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u)
|| (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = readSpellMissGuid();
if (packet.getSize() - packet.getReadPos() < 1) break;
uint8_t missInfo = packet.readUInt8();
// REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult
if (missInfo == 11 && !spellMissTbcLike) {
if (missInfo == 11) {
if (packet.getSize() - packet.getReadPos() >= 5) {
/*uint32_t reflectSpellId =*/ packet.readUInt32();
/*uint8_t reflectResult =*/ packet.readUInt8();
@ -2686,13 +2749,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
CombatTextEntry::DODGE, // 1=DODGE
CombatTextEntry::PARRY, // 2=PARRY
CombatTextEntry::BLOCK, // 3=BLOCK
CombatTextEntry::MISS, // 4=EVADE
CombatTextEntry::EVADE, // 4=EVADE
CombatTextEntry::IMMUNE, // 5=IMMUNE
CombatTextEntry::MISS, // 6=DEFLECT
CombatTextEntry::DEFLECT, // 6=DEFLECT
CombatTextEntry::ABSORB, // 7=ABSORB
CombatTextEntry::RESIST, // 8=RESIST
};
CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] : CombatTextEntry::MISS;
CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo]
: (missInfo == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS);
if (casterGuid == playerGuid) {
// We cast a spell and it missed the target
addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid);
@ -3203,12 +3267,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_DISPEL_FAILED: {
// WotLK: uint32 dispelSpellId + packed_guid caster + packed_guid victim
// [+ count × uint32 failedSpellId]
// TBC/Classic: uint64 caster + uint64 victim + uint32 spellId
// Classic: uint32 dispelSpellId + packed_guid caster + packed_guid victim
// [+ count × uint32 failedSpellId]
const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
// TBC: uint64 caster + uint64 victim + uint32 spellId
// [+ count × uint32 failedSpellId]
const bool dispelUsesFullGuid = isActiveExpansion("tbc");
uint32_t dispelSpellId = 0;
uint64_t dispelCasterGuid = 0;
if (dispelTbcLike) {
if (dispelUsesFullGuid) {
if (packet.getSize() - packet.getReadPos() < 20) break;
dispelCasterGuid = packet.readUInt64();
/*uint64_t victim =*/ packet.readUInt64();
@ -3216,9 +3282,13 @@ void GameHandler::handlePacket(network::Packet& packet) {
} else {
if (packet.getSize() - packet.getReadPos() < 4) break;
dispelSpellId = packet.readUInt32();
if (packet.getSize() - packet.getReadPos() < 1) break;
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(packet.getSize()); break;
}
dispelCasterGuid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 1) break;
if (!hasFullPackedGuid(packet)) {
packet.setReadPos(packet.getSize()); break;
}
/*uint64_t victim =*/ UpdateObjectParser::readPackedGuid(packet);
}
// Only show failure to the player who attempted the dispel
@ -3957,13 +4027,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
} else if (auraType == 98) {
// PERIODIC_MANA_LEECH: miscValue(powerType) + amount + float multiplier
if (packet.getSize() - packet.getReadPos() < 12) break;
/*uint32_t powerType =*/ packet.readUInt32();
uint8_t powerType = static_cast<uint8_t>(packet.readUInt32());
uint32_t amount = packet.readUInt32();
/*float multiplier =*/ packet.readUInt32(); // read as raw uint32 (float bits)
// Show as periodic damage from victim's perspective (mana drained)
float multiplier = packet.readFloat();
if (isPlayerVictim && amount > 0)
addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast<int32_t>(amount),
spellId, false, 0, casterGuid, victimGuid);
addCombatText(CombatTextEntry::POWER_DRAIN, static_cast<int32_t>(amount),
spellId, false, powerType, casterGuid, victimGuid);
if (isPlayerCaster && amount > 0 && multiplier > 0.0f && std::isfinite(multiplier)) {
const uint32_t gainedAmount = static_cast<uint32_t>(
std::lround(static_cast<double>(amount) * static_cast<double>(multiplier)));
if (gainedAmount > 0) {
addCombatText(CombatTextEntry::ENERGIZE, static_cast<int32_t>(gainedAmount),
spellId, true, powerType, casterGuid, casterGuid);
}
}
} else {
// Unknown/untracked aura type — stop parsing this event safely
packet.setReadPos(packet.getSize());
@ -3978,14 +4055,22 @@ void GameHandler::handlePacket(network::Packet& packet) {
// TBC: full uint64 victim + uint64 caster + uint32 spellId + uint8 powerType + int32 amount
// Classic/Vanilla: packed_guid (same as WotLK)
const bool energizeTbc = isActiveExpansion("tbc");
size_t rem = packet.getSize() - packet.getReadPos();
if (rem < (energizeTbc ? 8u : 2u)) { packet.setReadPos(packet.getSize()); break; }
uint64_t victimGuid = energizeTbc
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
uint64_t casterGuid = energizeTbc
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
rem = packet.getSize() - packet.getReadPos();
if (rem < 6) { packet.setReadPos(packet.getSize()); break; }
auto readEnergizeGuid = [&]() -> uint64_t {
if (energizeTbc)
return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0;
return UpdateObjectParser::readPackedGuid(packet);
};
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = readEnergizeGuid();
if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = readEnergizeGuid();
if (packet.getSize() - packet.getReadPos() < 9) {
packet.setReadPos(packet.getSize()); break;
}
uint32_t spellId = packet.readUInt32();
uint8_t energizePowerType = packet.readUInt8();
int32_t amount = static_cast<int32_t>(packet.readUInt32());
@ -6090,26 +6175,35 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
// ---- Spell combat logs (consume) ----
case Opcode::SMSG_AURACASTLOG:
case Opcode::SMSG_SPELLBREAKLOG:
case Opcode::SMSG_SPELLDAMAGESHIELD: {
// Classic/TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4)
// Classic: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + schoolMask(4)
// TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4)
// WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4)
const bool shieldClassicLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
const size_t shieldMinSz = shieldClassicLike ? 24u : 2u;
const bool shieldTbc = isActiveExpansion("tbc");
const bool shieldWotlkLike = !isClassicLikeExpansion() && !shieldTbc;
const auto shieldRem = [&]() { return packet.getSize() - packet.getReadPos(); };
const size_t shieldMinSz = shieldTbc ? 24u : 2u;
if (packet.getSize() - packet.getReadPos() < shieldMinSz) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = shieldClassicLike
if (!shieldTbc && (!hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = shieldTbc
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
uint64_t casterGuid = shieldClassicLike
if (packet.getSize() - packet.getReadPos() < (shieldTbc ? 8u : 1u)
|| (!shieldTbc && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = shieldTbc
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 12) {
const size_t shieldTailSize = shieldWotlkLike ? 16u : 12u;
if (shieldRem() < shieldTailSize) {
packet.setReadPos(packet.getSize()); break;
}
uint32_t shieldSpellId = packet.readUInt32();
uint32_t damage = packet.readUInt32();
if (!shieldClassicLike && packet.getSize() - packet.getReadPos() >= 4)
if (shieldWotlkLike)
/*uint32_t absorbed =*/ packet.readUInt32();
/*uint32_t school =*/ packet.readUInt32();
// Show combat text: damage shield reflect
@ -6122,18 +6216,30 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
}
case Opcode::SMSG_AURACASTLOG:
case Opcode::SMSG_SPELLBREAKLOG:
// These packets are not damage-shield events. Consume them without
// synthesizing reflected damage entries or misattributing GUIDs.
packet.setReadPos(packet.getSize());
break;
case Opcode::SMSG_SPELLORDAMAGE_IMMUNE: {
// WotLK: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType
// TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8
const bool immuneTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
const size_t minSz = immuneTbcLike ? 21u : 2u;
// WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType
// TBC: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8
const bool immuneUsesFullGuid = isActiveExpansion("tbc");
const size_t minSz = immuneUsesFullGuid ? 21u : 2u;
if (packet.getSize() - packet.getReadPos() < minSz) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = immuneTbcLike
if (!immuneUsesFullGuid && !hasFullPackedGuid(packet)) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = immuneUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < (immuneTbcLike ? 8u : 2u)) break;
uint64_t victimGuid = immuneTbcLike
if (packet.getSize() - packet.getReadPos() < (immuneUsesFullGuid ? 8u : 2u)
|| (!immuneUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = immuneUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 5) break;
uint32_t immuneSpellId = packet.readUInt32();
@ -6147,17 +6253,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_SPELLDISPELLOG: {
// WotLK: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen
// TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + ...
// WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen
// TBC: full uint64 casterGuid + full uint64 victimGuid + ...
// + uint32 count + count × (uint32 dispelled_spellId + uint32 unk)
const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) {
const bool dispelUsesFullGuid = isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)
|| (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t casterGuid = dispelTbcLike
uint64_t casterGuid = dispelUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) break;
uint64_t victimGuid = dispelTbcLike
if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)
|| (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = dispelUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 9) break;
/*uint32_t dispelSpell =*/ packet.readUInt32();
@ -6165,12 +6275,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
uint32_t count = packet.readUInt32();
// Preserve every dispelled aura in the combat log instead of collapsing
// multi-aura packets down to the first entry only.
const size_t dispelEntrySize = dispelTbcLike ? 8u : 5u;
const size_t dispelEntrySize = dispelUsesFullGuid ? 8u : 5u;
std::vector<uint32_t> dispelledIds;
dispelledIds.reserve(count);
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= dispelEntrySize; ++i) {
uint32_t dispelledId = packet.readUInt32();
if (dispelTbcLike) {
if (dispelUsesFullGuid) {
/*uint32_t unk =*/ packet.readUInt32();
} else {
/*uint8_t isPositive =*/ packet.readUInt8();
@ -6192,29 +6302,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
loggedIds = dispelledIds;
}
const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0;
const std::string displaySpellName = displaySpellId != 0
? [&]() {
const std::string& nm = getSpellName(displaySpellId);
return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm;
}()
: std::string{};
if (!displaySpellName.empty()) {
const std::string displaySpellNames = formatSpellNameList(*this, loggedIds);
if (!displaySpellNames.empty()) {
char buf[256];
const char* passiveVerb = loggedIds.size() == 1 ? "was" : "were";
if (isStolen) {
if (victimGuid == playerGuid && casterGuid != playerGuid)
std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "%s %s stolen.",
displaySpellNames.c_str(), passiveVerb);
else if (casterGuid == playerGuid)
std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellNames.c_str());
else
std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "%s %s stolen.",
displaySpellNames.c_str(), passiveVerb);
} else {
if (victimGuid == playerGuid && casterGuid != playerGuid)
std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "%s %s dispelled.",
displaySpellNames.c_str(), passiveVerb);
else if (casterGuid == playerGuid)
std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellNames.c_str());
else
std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "%s %s dispelled.",
displaySpellNames.c_str(), passiveVerb);
}
addSystemChatMessage(buf);
}
@ -6234,19 +6343,21 @@ void GameHandler::handlePacket(network::Packet& packet) {
case Opcode::SMSG_SPELLSTEALLOG: {
// Sent to the CASTER (Mage) when Spellsteal succeeds.
// Wire format mirrors SPELLDISPELLOG:
// WotLK: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count
// WotLK/Classic/Turtle: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count
// + count × (uint32 stolenSpellId + uint8 isPositive)
// TBC/Classic: full uint64 victim + full uint64 caster + same tail
const bool stealTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) {
// TBC: full uint64 victim + full uint64 caster + same tail
const bool stealUsesFullGuid = isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)
|| (!stealUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t stealVictim = stealTbcLike
uint64_t stealVictim = stealUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) {
if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)
|| (!stealUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t stealCaster = stealTbcLike
uint64_t stealCaster = stealUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 9) {
packet.setReadPos(packet.getSize()); break;
@ -6255,12 +6366,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
/*uint8_t isStolen =*/ packet.readUInt8();
uint32_t stealCount = packet.readUInt32();
// Preserve every stolen aura in the combat log instead of only the first.
const size_t stealEntrySize = stealTbcLike ? 8u : 5u;
const size_t stealEntrySize = stealUsesFullGuid ? 8u : 5u;
std::vector<uint32_t> stolenIds;
stolenIds.reserve(stealCount);
for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= stealEntrySize; ++i) {
uint32_t stolenId = packet.readUInt32();
if (stealTbcLike) {
if (stealUsesFullGuid) {
/*uint32_t unk =*/ packet.readUInt32();
} else {
/*uint8_t isPos =*/ packet.readUInt8();
@ -6277,19 +6388,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
loggedIds.push_back(stolenId);
}
const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0;
const std::string displaySpellName = displaySpellId != 0
? [&]() {
const std::string& nm = getSpellName(displaySpellId);
return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm;
}()
: std::string{};
if (!displaySpellName.empty()) {
const std::string displaySpellNames = formatSpellNameList(*this, loggedIds);
if (!displaySpellNames.empty()) {
char buf[256];
if (stealCaster == playerGuid)
std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellNames.c_str());
else
std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str());
std::snprintf(buf, sizeof(buf), "%s %s stolen.", displaySpellNames.c_str(),
loggedIds.size() == 1 ? "was" : "were");
addSystemChatMessage(buf);
}
// Some servers emit both SPELLDISPELLOG(isStolen=1) and SPELLSTEALLOG
@ -6306,15 +6412,24 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: {
// Format (all expansions): PackedGuid target + PackedGuid caster + uint32 spellId + ...
if (packet.getSize() - packet.getReadPos() < 3) {
// WotLK/Classic/Turtle: packed_guid target + packed_guid caster + uint32 spellId + ...
// TBC: uint64 target + uint64 caster + uint32 spellId + ...
const bool procChanceUsesFullGuid = isActiveExpansion("tbc");
auto readProcChanceGuid = [&]() -> uint64_t {
if (procChanceUsesFullGuid)
return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0;
return UpdateObjectParser::readPackedGuid(packet);
};
if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)
|| (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t procTargetGuid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 2) {
uint64_t procTargetGuid = readProcChanceGuid();
if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)
|| (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t procCasterGuid = UpdateObjectParser::readPackedGuid(packet);
uint64_t procCasterGuid = readProcChanceGuid();
if (packet.getSize() - packet.getReadPos() < 4) {
packet.setReadPos(packet.getSize()); break;
}
@ -6328,24 +6443,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
case Opcode::SMSG_SPELLINSTAKILLLOG: {
// Sent when a unit is killed by a spell with SPELL_ATTR_EX2_INSTAKILL (e.g. Execute, Obliterate, etc.)
// WotLK: packed_guid caster + packed_guid victim + uint32 spellId
// TBC/Classic: full uint64 caster + full uint64 victim + uint32 spellId
const bool ikTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
// WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId
// TBC: full uint64 caster + full uint64 victim + uint32 spellId
const bool ikUsesFullGuid = isActiveExpansion("tbc");
auto ik_rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; }
uint64_t ikCaster = ikTbcLike
if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)
|| (!ikUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t ikCaster = ikUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; }
uint64_t ikVictim = ikTbcLike
if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)
|| (!ikUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t ikVictim = ikUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0;
// Show kill/death feedback for the local player
if (ikCaster == playerGuid) {
// We killed a target instantly — show a KILL combat text hit
addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, true, 0, ikCaster, ikVictim);
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim);
} else if (ikVictim == playerGuid) {
// We were instantly killed — show a large incoming hit
addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, false, 0, ikCaster, ikVictim);
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, false, 0, ikCaster, ikVictim);
addSystemChatMessage("You were killed by an instant-kill effect.");
}
LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster,
@ -6354,8 +6473,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
}
case Opcode::SMSG_SPELLLOGEXECUTE: {
// WotLK: packed_guid caster + uint32 spellId + uint32 effectCount
// TBC/Classic: uint64 caster + uint32 spellId + uint32 effectCount
// WotLK/Classic/Turtle: packed_guid caster + uint32 spellId + uint32 effectCount
// TBC: uint64 caster + uint32 spellId + uint32 effectCount
// Per-effect: uint8 effectType + uint32 effectLogCount + effect-specific data
// Effect 10 = POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier
// Effect 11 = HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier
@ -6363,11 +6482,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
// Effect 26 = INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id
// Effect 49 = FEED_PET: uint32 itemEntry
// Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM)
const bool exeTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u)) {
const bool exeUsesFullGuid = isActiveExpansion("tbc");
if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t exeCaster = exeTbcLike
if (!exeUsesFullGuid && !hasFullPackedGuid(packet)) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t exeCaster = exeUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 8) {
packet.setReadPos(packet.getSize()); break;
@ -6385,44 +6507,73 @@ void GameHandler::handlePacket(network::Packet& packet) {
if (effectType == 10) {
// SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (packet.getSize() - packet.getReadPos() < 1) break;
uint64_t drainTarget = exeTbcLike
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)
|| (!exeUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t drainTarget = exeUsesFullGuid
? packet.readUInt64()
: UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); break; }
uint32_t drainAmount = packet.readUInt32();
uint32_t drainPower = packet.readUInt32(); // 0=mana,1=rage,3=energy,6=runic
/*float drainMult =*/ packet.readFloat();
float drainMult = packet.readFloat();
if (drainAmount > 0) {
if (drainTarget == playerGuid)
addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast<int32_t>(drainAmount), exeSpellId, false, 0,
addCombatText(CombatTextEntry::POWER_DRAIN, static_cast<int32_t>(drainAmount), exeSpellId, false,
static_cast<uint8_t>(drainPower),
exeCaster, drainTarget);
else if (isPlayerCaster)
addCombatText(CombatTextEntry::ENERGIZE, static_cast<int32_t>(drainAmount), exeSpellId, true,
if (isPlayerCaster) {
if (drainTarget != playerGuid) {
addCombatText(CombatTextEntry::POWER_DRAIN, static_cast<int32_t>(drainAmount), exeSpellId, true,
static_cast<uint8_t>(drainPower), exeCaster, drainTarget);
}
if (drainMult > 0.0f && std::isfinite(drainMult)) {
const uint32_t gainedAmount = static_cast<uint32_t>(
std::lround(static_cast<double>(drainAmount) * static_cast<double>(drainMult)));
if (gainedAmount > 0) {
addCombatText(CombatTextEntry::ENERGIZE, static_cast<int32_t>(gainedAmount), exeSpellId, true,
static_cast<uint8_t>(drainPower), exeCaster, exeCaster);
}
}
}
}
LOG_DEBUG("SMSG_SPELLLOGEXECUTE POWER_DRAIN: spell=", exeSpellId,
" power=", drainPower, " amount=", drainAmount);
" power=", drainPower, " amount=", drainAmount,
" multiplier=", drainMult);
}
} else if (effectType == 11) {
// SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (packet.getSize() - packet.getReadPos() < 1) break;
uint64_t leechTarget = exeTbcLike
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)
|| (!exeUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t leechTarget = exeUsesFullGuid
? packet.readUInt64()
: UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); break; }
uint32_t leechAmount = packet.readUInt32();
/*float leechMult =*/ packet.readFloat();
float leechMult = packet.readFloat();
if (leechAmount > 0) {
if (leechTarget == playerGuid)
if (leechTarget == playerGuid) {
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(leechAmount), exeSpellId, false, 0,
exeCaster, leechTarget);
else if (isPlayerCaster)
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(leechAmount), exeSpellId, true, 0,
} else if (isPlayerCaster) {
addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast<int32_t>(leechAmount), exeSpellId, true, 0,
exeCaster, leechTarget);
}
if (isPlayerCaster && leechMult > 0.0f && std::isfinite(leechMult)) {
const uint32_t gainedAmount = static_cast<uint32_t>(
std::lround(static_cast<double>(leechAmount) * static_cast<double>(leechMult)));
if (gainedAmount > 0) {
addCombatText(CombatTextEntry::HEAL, static_cast<int32_t>(gainedAmount), exeSpellId, true, 0,
exeCaster, exeCaster);
}
LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId, " amount=", leechAmount);
}
}
LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId,
" amount=", leechAmount, " multiplier=", leechMult);
}
} else if (effectType == 24 || effectType == 114) {
// SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry
@ -6449,9 +6600,12 @@ void GameHandler::handlePacket(network::Packet& packet) {
} else if (effectType == 26) {
// SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id
for (uint32_t li = 0; li < effectLogCount; ++li) {
if (packet.getSize() - packet.getReadPos() < 1) break;
uint64_t icTarget = exeTbcLike
? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0)
if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)
|| (!exeUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t icTarget = exeUsesFullGuid
? packet.readUInt64()
: UpdateObjectParser::readPackedGuid(packet);
if (packet.getSize() - packet.getReadPos() < 4) { packet.setReadPos(packet.getSize()); break; }
uint32_t icSpellId = packet.readUInt32();
@ -6899,19 +7053,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
// ---- Resistance/combat log ----
case Opcode::SMSG_RESISTLOG: {
// WotLK: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId
// WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId
// + float resistFactor + uint32 targetRes + uint32 resistedValue + ...
// TBC/Classic: same but full uint64 GUIDs
// TBC: same layout but full uint64 GUIDs
// Show RESIST combat text when player resists an incoming spell.
const bool rlTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
const bool rlUsesFullGuid = isActiveExpansion("tbc");
auto rl_rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; }
/*uint32_t hitInfo =*/ packet.readUInt32();
if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; }
uint64_t attackerGuid = rlTbcLike
if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)
|| (!rlUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t attackerGuid = rlUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; }
uint64_t victimGuid = rlTbcLike
if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)
|| (!rlUsesFullGuid && !hasFullPackedGuid(packet))) {
packet.setReadPos(packet.getSize()); break;
}
uint64_t victimGuid = rlUsesFullGuid
? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet);
if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; }
uint32_t spellId = packet.readUInt32();
@ -16333,14 +16493,14 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
addCombatText(CombatTextEntry::MELEE_DAMAGE, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
addCombatText(CombatTextEntry::BLOCK, static_cast<int32_t>(data.blocked), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 5) {
// VICTIMSTATE_EVADE: NPC evaded (out of combat zone). Show as miss.
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
// VICTIMSTATE_EVADE: NPC evaded (out of combat zone).
addCombatText(CombatTextEntry::EVADE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 6) {
// VICTIMSTATE_IS_IMMUNE: Target is immune to this attack.
addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else if (data.victimState == 7) {
// VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect).
addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
} else {
auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE;
addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
@ -16950,9 +17110,9 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
CombatTextEntry::DODGE, // 1=DODGE
CombatTextEntry::PARRY, // 2=PARRY
CombatTextEntry::BLOCK, // 3=BLOCK
CombatTextEntry::MISS, // 4=EVADE
CombatTextEntry::EVADE, // 4=EVADE
CombatTextEntry::IMMUNE, // 5=IMMUNE
CombatTextEntry::MISS, // 6=DEFLECT
CombatTextEntry::DEFLECT, // 6=DEFLECT
CombatTextEntry::ABSORB, // 7=ABSORB
CombatTextEntry::RESIST, // 8=RESIST
};
@ -16963,7 +17123,8 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
if (!playerIsCaster && m.targetGuid != playerGuid) {
continue;
}
CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] : CombatTextEntry::MISS;
CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType]
: (m.missType == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS);
addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid);
}
}

View file

@ -4,6 +4,26 @@
namespace wowee {
namespace game {
namespace {
bool hasFullPackedGuid(const network::Packet& packet) {
if (packet.getReadPos() >= packet.getSize()) {
return false;
}
const auto& rawData = packet.getData();
const uint8_t mask = rawData[packet.getReadPos()];
size_t guidBytes = 1;
for (int bit = 0; bit < 8; ++bit) {
if ((mask & (1u << bit)) != 0) {
++guidBytes;
}
}
return packet.getSize() - packet.getReadPos() >= guidBytes;
}
} // namespace
// ============================================================================
// Classic 1.12.1 movement flag constants
// Key differences from TBC:
@ -421,6 +441,11 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
m.targetGuid = UpdateObjectParser::readPackedGuid(packet);
if (rem() < 1) break;
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (rem() < 5) break;
(void)packet.readUInt32();
(void)packet.readUInt8();
}
data.missTargets.push_back(m);
}
// Check if we read all expected misses
@ -492,10 +517,10 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att
// ============================================================================
bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 2) return false;
if (rem() < 2 || !hasFullPackedGuid(packet)) return false;
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
if (rem() < 1) return false;
if (rem() < 1 || !hasFullPackedGuid(packet)) return false;
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
// uint32(spellId) + uint32(damage) + uint8(schoolMask) + uint32(absorbed)
@ -527,10 +552,10 @@ bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDam
// ============================================================================
bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
if (rem() < 2) return false;
if (rem() < 2 || !hasFullPackedGuid(packet)) return false;
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
if (rem() < 1) return false;
if (rem() < 1 || !hasFullPackedGuid(packet)) return false;
data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
if (rem() < 13) return false; // uint32 + uint32 + uint32 + uint8 = 13 bytes

View file

@ -1261,7 +1261,9 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
// WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags.
// ============================================================================
bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
if (packet.getSize() - packet.getReadPos() < 19) return false;
// Fixed header before hit/miss lists:
// casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32)
if (packet.getSize() - packet.getReadPos() < 25) return false;
data.casterGuid = packet.readUInt64(); // full GUID in TBC
data.casterUnit = packet.readUInt64(); // full GUID in TBC
@ -1304,6 +1306,13 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
SpellGoMissEntry m;
m.targetGuid = packet.readUInt64(); // full GUID in TBC
m.missType = packet.readUInt8();
if (m.missType == 11) {
if (packet.getReadPos() + 5 > packet.getSize()) {
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
}
data.missTargets.push_back(m);
}
// Check if we read all expected misses

View file

@ -3632,8 +3632,9 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
}
bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
// Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1) + missCount(1) = 24 bytes minimum
if (packet.getSize() - packet.getReadPos() < 24) return false;
// Packed GUIDs are variable-length, so only require the smallest possible
// shape up front: 2 GUID masks + fixed fields through missCount.
if (packet.getSize() - packet.getReadPos() < 17) return false;
size_t startPos = packet.getReadPos();
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
@ -3660,12 +3661,13 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
data.hitTargets.reserve(data.hitCount);
for (uint8_t i = 0; i < data.hitCount; ++i) {
if (packet.getSize() - packet.getReadPos() < 8) {
// WotLK hit targets are packed GUIDs, like the caster and miss targets.
if (packet.getSize() - packet.getReadPos() < 1) {
LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount);
data.hitCount = i;
break;
}
data.hitTargets.push_back(packet.readUInt64());
data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet));
}
// Validate missCount field exists
@ -3682,7 +3684,8 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
data.missTargets.reserve(data.missCount);
for (uint8_t i = 0; i < data.missCount; ++i) {
// Each miss entry: packed GUID(1-8 bytes) + missType(1 byte), validate before reading
// Each miss entry: packed GUID(1-8 bytes) + missType(1 byte).
// REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult.
if (packet.getSize() - packet.getReadPos() < 2) {
LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount);
data.missCount = i;
@ -3691,6 +3694,15 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
SpellGoMissEntry m;
m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK
m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0;
if (m.missType == 11) {
if (packet.getSize() - packet.getReadPos() < 5) {
LOG_WARNING("Spell go: truncated reflect payload at miss index ", (int)i, "/", (int)data.missCount);
data.missCount = i;
break;
}
(void)packet.readUInt32();
(void)packet.readUInt8();
}
data.missTargets.push_back(m);
}

View file

@ -990,42 +990,64 @@ void TerrainRenderer::clear() {
}
chunks.clear();
renderedChunks = 0;
if (materialDescPool) {
vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0);
}
}
void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
if (chunk.vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = chunk.vertexBuffer; ab.allocation = chunk.vertexAlloc;
destroyBuffer(allocator, ab);
chunk.vertexBuffer = VK_NULL_HANDLE;
}
if (chunk.indexBuffer) {
AllocatedBuffer ab{}; ab.buffer = chunk.indexBuffer; ab.allocation = chunk.indexAlloc;
destroyBuffer(allocator, ab);
chunk.indexBuffer = VK_NULL_HANDLE;
}
if (chunk.paramsUBO) {
AllocatedBuffer ab{}; ab.buffer = chunk.paramsUBO; ab.allocation = chunk.paramsAlloc;
destroyBuffer(allocator, ab);
chunk.paramsUBO = VK_NULL_HANDLE;
}
// Return material descriptor set to the pool so it can be reused by new chunks
if (chunk.materialSet && materialDescPool) {
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &chunk.materialSet);
}
chunk.materialSet = VK_NULL_HANDLE;
// These resources may still be referenced by in-flight command buffers from
// previous frames. Defer actual destruction until this frame slot is safe.
::VkBuffer vertexBuffer = chunk.vertexBuffer;
VmaAllocation vertexAlloc = chunk.vertexAlloc;
::VkBuffer indexBuffer = chunk.indexBuffer;
VmaAllocation indexAlloc = chunk.indexAlloc;
::VkBuffer paramsUBO = chunk.paramsUBO;
VmaAllocation paramsAlloc = chunk.paramsAlloc;
VkDescriptorPool pool = materialDescPool;
VkDescriptorSet materialSet = chunk.materialSet;
// Destroy owned alpha textures (VkTexture::~VkTexture is a no-op, must call destroy() explicitly)
VkDevice device = vkCtx->getDevice();
std::vector<VkTexture*> alphaTextures;
alphaTextures.reserve(chunk.ownedAlphaTextures.size());
for (auto& tex : chunk.ownedAlphaTextures) {
if (tex) tex->destroy(device, allocator);
alphaTextures.push_back(tex.release());
}
chunk.vertexBuffer = VK_NULL_HANDLE;
chunk.vertexAlloc = VK_NULL_HANDLE;
chunk.indexBuffer = VK_NULL_HANDLE;
chunk.indexAlloc = VK_NULL_HANDLE;
chunk.paramsUBO = VK_NULL_HANDLE;
chunk.paramsAlloc = VK_NULL_HANDLE;
chunk.materialSet = VK_NULL_HANDLE;
chunk.ownedAlphaTextures.clear();
vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
paramsUBO, paramsAlloc, pool, materialSet, alphaTextures]() {
if (vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc;
destroyBuffer(allocator, ab);
}
if (indexBuffer) {
AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc;
destroyBuffer(allocator, ab);
}
if (paramsUBO) {
AllocatedBuffer ab{}; ab.buffer = paramsUBO; ab.allocation = paramsAlloc;
destroyBuffer(allocator, ab);
}
if (materialSet && pool) {
VkDescriptorSet set = materialSet;
vkFreeDescriptorSets(device, pool, 1, &set);
}
for (VkTexture* tex : alphaTextures) {
if (!tex) continue;
tex->destroy(device, allocator);
delete tex;
}
});
}
int TerrainRenderer::getTriangleCount() const {

View file

@ -55,6 +55,11 @@ void VkContext::shutdown() {
vkDeviceWaitIdle(device);
}
// With the device idle, it is safe to run any deferred per-frame cleanup.
for (uint32_t fi = 0; fi < MAX_FRAMES_IN_FLIGHT; fi++) {
runDeferredCleanup(fi);
}
LOG_WARNING("VkContext::shutdown - destroyImGuiResources...");
destroyImGuiResources();
@ -103,6 +108,19 @@ void VkContext::shutdown() {
LOG_WARNING("Vulkan context shutdown complete");
}
void VkContext::deferAfterFrameFence(std::function<void()>&& fn) {
deferredCleanup_[currentFrame].push_back(std::move(fn));
}
void VkContext::runDeferredCleanup(uint32_t frameIndex) {
auto& q = deferredCleanup_[frameIndex];
if (q.empty()) return;
for (auto& fn : q) {
if (fn) fn();
}
q.clear();
}
bool VkContext::createInstance(SDL_Window* window) {
// Get required SDL extensions
unsigned int sdlExtCount = 0;
@ -1349,6 +1367,9 @@ VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) {
return VK_NULL_HANDLE;
}
// Any work queued for this frame slot is now guaranteed to be unused by the GPU.
runDeferredCleanup(currentFrame);
// Acquire next swapchain image
VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX,
frame.imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex);

View file

@ -1029,10 +1029,6 @@ void WaterRenderer::clear() {
destroyWaterMesh(surface);
}
surfaces.clear();
if (vkCtx && materialDescPool) {
vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0);
}
}
// ==============================================================
@ -1358,27 +1354,45 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) {
void WaterRenderer::destroyWaterMesh(WaterSurface& surface) {
if (!vkCtx) return;
VkDevice device = vkCtx->getDevice();
VmaAllocator allocator = vkCtx->getAllocator();
if (surface.vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = surface.vertexBuffer; ab.allocation = surface.vertexAlloc;
destroyBuffer(allocator, ab);
::VkBuffer vertexBuffer = surface.vertexBuffer;
VmaAllocation vertexAlloc = surface.vertexAlloc;
::VkBuffer indexBuffer = surface.indexBuffer;
VmaAllocation indexAlloc = surface.indexAlloc;
::VkBuffer materialUBO = surface.materialUBO;
VmaAllocation materialAlloc = surface.materialAlloc;
VkDescriptorPool pool = materialDescPool;
VkDescriptorSet materialSet = surface.materialSet;
surface.vertexBuffer = VK_NULL_HANDLE;
}
if (surface.indexBuffer) {
AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc;
destroyBuffer(allocator, ab);
surface.vertexAlloc = VK_NULL_HANDLE;
surface.indexBuffer = VK_NULL_HANDLE;
}
if (surface.materialUBO) {
AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc;
destroyBuffer(allocator, ab);
surface.indexAlloc = VK_NULL_HANDLE;
surface.materialUBO = VK_NULL_HANDLE;
}
if (surface.materialSet && materialDescPool) {
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet);
}
surface.materialAlloc = VK_NULL_HANDLE;
surface.materialSet = VK_NULL_HANDLE;
vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc,
materialUBO, materialAlloc, pool, materialSet]() {
if (vertexBuffer) {
AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc;
destroyBuffer(allocator, ab);
}
if (indexBuffer) {
AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc;
destroyBuffer(allocator, ab);
}
if (materialUBO) {
AllocatedBuffer ab{}; ab.buffer = materialUBO; ab.allocation = materialAlloc;
destroyBuffer(allocator, ab);
}
if (materialSet && pool) {
VkDescriptorSet set = materialSet;
vkFreeDescriptorSets(device, pool, 1, &set);
}
});
}
// ==============================================================

View file

@ -8342,6 +8342,11 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
break;
case game::CombatTextEntry::EVADE:
snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade");
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
break;
case game::CombatTextEntry::PERIODIC_DAMAGE:
snprintf(text, sizeof(text), "-%d", entry.amount);
color = outgoing ?
@ -8366,6 +8371,16 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; // Mana (0): blue
}
break;
case game::CombatTextEntry::POWER_DRAIN:
snprintf(text, sizeof(text), "-%d", entry.amount);
switch (entry.powerType) {
case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break;
case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break;
case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break;
case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break;
default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break;
}
break;
case game::CombatTextEntry::XP_GAIN:
snprintf(text, sizeof(text), "+%d XP", entry.amount);
color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP
@ -8388,6 +8403,16 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
snprintf(text, sizeof(text), "Resisted");
color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist
break;
case game::CombatTextEntry::DEFLECT:
snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect");
color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha)
: ImVec4(0.5f, 0.9f, 1.0f, alpha);
break;
case game::CombatTextEntry::REFLECT:
snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect");
color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha)
: ImVec4(0.75f, 0.85f, 1.0f, alpha);
break;
case game::CombatTextEntry::PROC_TRIGGER: {
const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : "";
if (!procName.empty())
@ -8398,11 +8423,27 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
break;
}
case game::CombatTextEntry::DISPEL:
if (entry.spellId != 0) {
const std::string& dispelledName = gameHandler.getSpellName(entry.spellId);
if (!dispelledName.empty())
snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str());
else
snprintf(text, sizeof(text), "Dispel");
} else {
snprintf(text, sizeof(text), "Dispel");
}
color = ImVec4(0.6f, 0.9f, 1.0f, alpha);
break;
case game::CombatTextEntry::STEAL:
if (entry.spellId != 0) {
const std::string& stolenName = gameHandler.getSpellName(entry.spellId);
if (!stolenName.empty())
snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str());
else
snprintf(text, sizeof(text), "Spellsteal");
} else {
snprintf(text, sizeof(text), "Spellsteal");
}
color = ImVec4(0.8f, 0.7f, 1.0f, alpha);
break;
case game::CombatTextEntry::INTERRUPT: {
@ -8414,6 +8455,11 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
color = ImVec4(1.0f, 0.6f, 0.9f, alpha);
break;
}
case game::CombatTextEntry::INSTAKILL:
snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!");
color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha)
: ImVec4(1.0f, 0.1f, 0.1f, alpha);
break;
default:
snprintf(text, sizeof(text), "%d", entry.amount);
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
@ -20231,6 +20277,13 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount);
color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f);
break;
case T::EVADE:
if (spell)
snprintf(desc, sizeof(desc), "%s evades %s's %s", tgt, src, spell);
else
snprintf(desc, sizeof(desc), "%s evades %s's attack", tgt, src);
color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f);
break;
case T::IMMUNE:
if (spell)
snprintf(desc, sizeof(desc), "%s is immune to %s", tgt, spell);
@ -20260,6 +20313,20 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "Resisted");
color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f);
break;
case T::DEFLECT:
if (spell)
snprintf(desc, sizeof(desc), "%s deflects %s's %s", tgt, src, spell);
else
snprintf(desc, sizeof(desc), "%s deflects %s's attack", tgt, src);
color = ImVec4(0.65f, 0.8f, 0.95f, 1.0f);
break;
case T::REFLECT:
if (spell)
snprintf(desc, sizeof(desc), "%s reflects %s's %s", tgt, src, spell);
else
snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src);
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
break;
case T::ENVIRONMENTAL:
snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount);
color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f);
@ -20271,6 +20338,13 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "%s gains %d power", tgt, e.amount);
color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f);
break;
case T::POWER_DRAIN:
if (spell)
snprintf(desc, sizeof(desc), "%s loses %d power to %s's %s", tgt, e.amount, src, spell);
else
snprintf(desc, sizeof(desc), "%s loses %d power", tgt, e.amount);
color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f);
break;
case T::XP_GAIN:
snprintf(desc, sizeof(desc), "You gain %d experience", e.amount);
color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f);
@ -20315,6 +20389,17 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
snprintf(desc, sizeof(desc), "%s interrupted", tgt);
color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f);
break;
case T::INSTAKILL:
if (spell && e.isPlayerSource)
snprintf(desc, sizeof(desc), "You instantly kill %s with %s", tgt, spell);
else if (spell)
snprintf(desc, sizeof(desc), "%s instantly kills %s with %s", src, tgt, spell);
else if (e.isPlayerSource)
snprintf(desc, sizeof(desc), "You instantly kill %s", tgt);
else
snprintf(desc, sizeof(desc), "%s instantly kills %s", src, tgt);
color = ImVec4(1.0f, 0.2f, 0.2f, 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);