mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
62 commits
e51b215f85
...
251ed7733b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
251ed7733b | ||
|
|
5a63d3799c | ||
|
|
5be80a9cc6 | ||
|
|
94e5855d53 | ||
|
|
f0c01bf352 | ||
|
|
a5b877de67 | ||
|
|
14e58eaa01 | ||
|
|
aed1f2ad21 | ||
|
|
a09a24e58e | ||
|
|
8391f93ca6 | ||
|
|
565c78d141 | ||
|
|
6cfb439fd6 | ||
|
|
c1b66f73c5 | ||
|
|
edd934a3e3 | ||
|
|
6a7071fd64 | ||
|
|
011a148105 | ||
|
|
f6d8c01779 | ||
|
|
b059bbcf89 | ||
|
|
468880e2c8 | ||
|
|
0968a11234 | ||
|
|
a48f6d1044 | ||
|
|
0fc887a3d2 | ||
|
|
dbdc45a8a9 | ||
|
|
bd8c46fa49 | ||
|
|
1fa2cbc64e | ||
|
|
fd8ea4e69e | ||
|
|
8ba5ca5337 | ||
|
|
9c3b5d17cf | ||
|
|
ed5134d601 | ||
|
|
a147347393 | ||
|
|
f5386060b6 | ||
|
|
209f8db382 | ||
|
|
64483a31d5 | ||
|
|
e9d2c43191 | ||
|
|
842771cb10 | ||
|
|
57265bfa4f | ||
|
|
6095170167 | ||
|
|
3f1083e9b5 | ||
|
|
77d53baa09 | ||
|
|
dceaf8f1ac | ||
|
|
5392243575 | ||
|
|
3e4708fe15 | ||
|
|
46b297aacc | ||
|
|
21762485ea | ||
|
|
3ef5b546fb | ||
|
|
5be55b1b14 | ||
|
|
cf68c156f1 | ||
|
|
16c8a2fd33 | ||
|
|
d4d876a563 | ||
|
|
c5e7dde931 | ||
|
|
f09913d6d2 | ||
|
|
d61bb036a7 | ||
|
|
1214369755 | ||
|
|
91dc45d19e | ||
|
|
3edf280e06 | ||
|
|
98c195fb8e | ||
|
|
c45951b368 | ||
|
|
a48eab43b8 | ||
|
|
3fa495d9ea | ||
|
|
23023dc140 | ||
|
|
db681ec4c6 | ||
|
|
6af9f90f45 |
15 changed files with 911 additions and 318 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -100,3 +100,10 @@ node_modules/
|
|||
# Python cache artifacts
|
||||
tools/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# artifacts
|
||||
.codex-loop/
|
||||
|
||||
# Local agent instructions
|
||||
AGENTS.md
|
||||
codex-loop.sh
|
||||
|
|
|
|||
|
|
@ -2369,6 +2369,7 @@ private:
|
|||
|
||||
void addCombatText(CombatTextEntry::Type type, int32_t amount, uint32_t spellId, bool isPlayerSource, uint8_t powerType = 0,
|
||||
uint64_t srcGuid = 0, uint64_t dstGuid = 0);
|
||||
bool shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId);
|
||||
void addSystemChatMessage(const std::string& message);
|
||||
|
||||
/**
|
||||
|
|
@ -2575,7 +2576,15 @@ private:
|
|||
std::unordered_set<uint64_t> hostileAttackers_;
|
||||
std::vector<CombatTextEntry> combatText;
|
||||
static constexpr size_t MAX_COMBAT_LOG = 500;
|
||||
struct RecentSpellstealLogEntry {
|
||||
uint64_t casterGuid = 0;
|
||||
uint64_t victimGuid = 0;
|
||||
uint32_t spellId = 0;
|
||||
std::chrono::steady_clock::time_point timestamp{};
|
||||
};
|
||||
static constexpr size_t MAX_RECENT_SPELLSTEAL_LOGS = 32;
|
||||
std::deque<CombatLogEntry> combatLog_;
|
||||
std::deque<RecentSpellstealLogEntry> recentSpellstealLogs_;
|
||||
std::deque<std::string> areaTriggerMsgs_;
|
||||
// unitGuid → sorted threat list (descending by threat value)
|
||||
std::unordered_map<uint64_t, std::vector<ThreatEntry>> threatLists_;
|
||||
|
|
|
|||
|
|
@ -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, 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; }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -781,6 +831,13 @@ void GameHandler::update(float deltaTime) {
|
|||
it->timer -= deltaTime;
|
||||
if (it->timer <= 0.0f) {
|
||||
if (state == WorldState::IN_WORLD && socket) {
|
||||
// Avoid sending CMSG_LOOT while a timed cast is active (e.g. gathering).
|
||||
// handleSpellGo will trigger loot after the cast completes.
|
||||
if (casting && currentCastSpellId != 0) {
|
||||
it->timer = 0.20f;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
lootTarget(it->guid);
|
||||
}
|
||||
it = pendingGameObjectLootOpens_.erase(it);
|
||||
|
|
@ -2052,22 +2109,31 @@ 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();
|
||||
if (victim == playerGuid)
|
||||
if (victim == playerGuid) {
|
||||
addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, caster, victim);
|
||||
} else if (caster == playerGuid) {
|
||||
addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, caster, victim);
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
}
|
||||
|
|
@ -2643,33 +2709,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
|
||||
// + count × (packed_guid victim + uint8 missInfo)
|
||||
// [missInfo==11(REFLECT): + uint32 reflectSpellId + uint8 reflectResult]
|
||||
// TBC/Classic: spellId(4) + uint64 caster + uint8 unk + uint32 count
|
||||
// + count × (uint64 victim + uint8 missInfo)
|
||||
const bool spellMissTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc");
|
||||
// WotLK/Classic: spellId(4) + packed_guid caster + uint8 unk + uint32 count
|
||||
// + count × (packed_guid victim + uint8 missInfo)
|
||||
// TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count
|
||||
// + count × (uint64 victim + uint8 missInfo)
|
||||
// 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;
|
||||
uint32_t spellId = packet.readUInt32();
|
||||
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();
|
||||
|
|
@ -2683,19 +2756,20 @@ 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, 0, true, 0, casterGuid, victimGuid);
|
||||
addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid);
|
||||
} else if (victimGuid == playerGuid) {
|
||||
// Enemy spell missed us (we dodged/parried/blocked/resisted/etc.)
|
||||
addCombatText(ct, 0, 0, false, 0, casterGuid, victimGuid);
|
||||
addCombatText(ct, 0, spellId, false, 0, casterGuid, victimGuid);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
|
@ -3200,12 +3274,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();
|
||||
|
|
@ -3213,9 +3289,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
|
||||
|
|
@ -3954,13 +4034,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());
|
||||
|
|
@ -3975,14 +4062,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());
|
||||
|
|
@ -6087,26 +6182,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)
|
||||
// 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;
|
||||
// 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 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
|
||||
|
|
@ -6119,18 +6223,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();
|
||||
|
|
@ -6144,53 +6260,88 @@ 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();
|
||||
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) {
|
||||
// Preserve every dispelled aura in the combat log instead of collapsing
|
||||
// multi-aura packets down to the first entry only.
|
||||
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();
|
||||
/*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;
|
||||
if (dispelUsesFullGuid) {
|
||||
/*uint32_t unk =*/ packet.readUInt32();
|
||||
} else {
|
||||
/*uint8_t isPositive =*/ packet.readUInt8();
|
||||
}
|
||||
if (dispelledId != 0) {
|
||||
dispelledIds.push_back(dispelledId);
|
||||
}
|
||||
}
|
||||
// Show system message if player was victim or caster
|
||||
if (victimGuid == playerGuid || casterGuid == playerGuid) {
|
||||
const char* verb = isStolen ? "stolen" : "dispelled";
|
||||
if (!firstSpellName.empty()) {
|
||||
std::vector<uint32_t> loggedIds;
|
||||
if (isStolen) {
|
||||
loggedIds.reserve(dispelledIds.size());
|
||||
for (uint32_t dispelledId : dispelledIds) {
|
||||
if (shouldLogSpellstealAura(casterGuid, victimGuid, dispelledId))
|
||||
loggedIds.push_back(dispelledId);
|
||||
}
|
||||
} else {
|
||||
loggedIds = dispelledIds;
|
||||
}
|
||||
|
||||
const std::string displaySpellNames = formatSpellNameList(*this, loggedIds);
|
||||
if (!displaySpellNames.empty()) {
|
||||
char buf[256];
|
||||
if (victimGuid == playerGuid && casterGuid != playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "%s was %s.", firstSpellName.c_str(), verb);
|
||||
else if (casterGuid == playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "You %s %s.", verb, firstSpellName.c_str());
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%s %s.", firstSpellName.c_str(), verb);
|
||||
const char* passiveVerb = loggedIds.size() == 1 ? "was" : "were";
|
||||
if (isStolen) {
|
||||
if (victimGuid == playerGuid && casterGuid != playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "%s %s stolen.",
|
||||
displaySpellNames.c_str(), passiveVerb);
|
||||
else if (casterGuid == playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellNames.c_str());
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%s %s stolen.",
|
||||
displaySpellNames.c_str(), passiveVerb);
|
||||
} else {
|
||||
if (victimGuid == playerGuid && casterGuid != playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "%s %s dispelled.",
|
||||
displaySpellNames.c_str(), passiveVerb);
|
||||
else if (casterGuid == playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellNames.c_str());
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%s %s dispelled.",
|
||||
displaySpellNames.c_str(), passiveVerb);
|
||||
}
|
||||
addSystemChatMessage(buf);
|
||||
}
|
||||
// Add dispel event to combat log
|
||||
if (firstDispelledId != 0) {
|
||||
// Preserve stolen auras as spellsteal events so the log wording stays accurate.
|
||||
if (!loggedIds.empty()) {
|
||||
bool isPlayerCaster = (casterGuid == playerGuid);
|
||||
addCombatText(CombatTextEntry::DISPEL, 0, firstDispelledId, isPlayerCaster, 0,
|
||||
casterGuid, victimGuid);
|
||||
for (uint32_t dispelledId : loggedIds) {
|
||||
addCombatText(isStolen ? CombatTextEntry::STEAL : CombatTextEntry::DISPEL,
|
||||
0, dispelledId, isPlayerCaster, 0,
|
||||
casterGuid, victimGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
|
|
@ -6199,19 +6350,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
|
||||
// + 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)) {
|
||||
// WotLK/Classic/Turtle: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count
|
||||
// + count × (uint32 stolenSpellId + uint8 isPositive)
|
||||
// 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;
|
||||
|
|
@ -6219,47 +6372,71 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
/*uint32_t stealSpellId =*/ packet.readUInt32();
|
||||
/*uint8_t isStolen =*/ packet.readUInt8();
|
||||
uint32_t stealCount = packet.readUInt32();
|
||||
// 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) {
|
||||
// Preserve every stolen aura in the combat log instead of only the first.
|
||||
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();
|
||||
/*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 (stealUsesFullGuid) {
|
||||
/*uint32_t unk =*/ packet.readUInt32();
|
||||
} else {
|
||||
/*uint8_t isPos =*/ packet.readUInt8();
|
||||
}
|
||||
if (stolenId != 0) {
|
||||
stolenIds.push_back(stolenId);
|
||||
}
|
||||
}
|
||||
if (stealCaster == playerGuid || stealVictim == playerGuid) {
|
||||
if (!stolenName.empty()) {
|
||||
std::vector<uint32_t> loggedIds;
|
||||
loggedIds.reserve(stolenIds.size());
|
||||
for (uint32_t stolenId : stolenIds) {
|
||||
if (shouldLogSpellstealAura(stealCaster, stealVictim, stolenId))
|
||||
loggedIds.push_back(stolenId);
|
||||
}
|
||||
|
||||
const std::string displaySpellNames = formatSpellNameList(*this, loggedIds);
|
||||
if (!displaySpellNames.empty()) {
|
||||
char buf[256];
|
||||
if (stealCaster == playerGuid)
|
||||
std::snprintf(buf, sizeof(buf), "You stole %s.", stolenName.c_str());
|
||||
std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellNames.c_str());
|
||||
else
|
||||
std::snprintf(buf, sizeof(buf), "%s was stolen.", stolenName.c_str());
|
||||
std::snprintf(buf, sizeof(buf), "%s %s stolen.", displaySpellNames.c_str(),
|
||||
loggedIds.size() == 1 ? "was" : "were");
|
||||
addSystemChatMessage(buf);
|
||||
}
|
||||
// Add dispel/steal to combat log using DISPEL type (isStolen=true for steals)
|
||||
if (firstStolenId != 0) {
|
||||
// Some servers emit both SPELLDISPELLOG(isStolen=1) and SPELLSTEALLOG
|
||||
// for the same aura. Keep the first event and suppress the duplicate.
|
||||
if (!loggedIds.empty()) {
|
||||
bool isPlayerCaster = (stealCaster == playerGuid);
|
||||
addCombatText(CombatTextEntry::DISPEL, 0, firstStolenId, isPlayerCaster, 0,
|
||||
stealCaster, stealVictim);
|
||||
for (uint32_t stolenId : loggedIds) {
|
||||
addCombatText(CombatTextEntry::STEAL, 0, stolenId, isPlayerCaster, 0,
|
||||
stealCaster, stealVictim);
|
||||
}
|
||||
}
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
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;
|
||||
}
|
||||
|
|
@ -6273,24 +6450,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,
|
||||
|
|
@ -6299,8 +6480,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
|
||||
|
|
@ -6308,11 +6489,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;
|
||||
|
|
@ -6330,44 +6514,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,
|
||||
static_cast<uint8_t>(drainPower), exeCaster, drainTarget);
|
||||
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
|
||||
|
|
@ -6394,9 +6607,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();
|
||||
|
|
@ -6844,27 +7060,33 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
|||
|
||||
// ---- Resistance/combat log ----
|
||||
case Opcode::SMSG_RESISTLOG: {
|
||||
// WotLK: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId
|
||||
// + float resistFactor + uint32 targetRes + uint32 resistedValue + ...
|
||||
// TBC/Classic: same but full uint64 GUIDs
|
||||
// WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId
|
||||
// + float resistFactor + uint32 targetRes + uint32 resistedValue + ...
|
||||
// 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();
|
||||
// Show RESIST when player is the victim; show as caster-side MISS when player is attacker
|
||||
// Show RESIST when the player is involved on either side.
|
||||
if (victimGuid == playerGuid) {
|
||||
addCombatText(CombatTextEntry::MISS, 0, spellId, false, 0, attackerGuid, victimGuid);
|
||||
addCombatText(CombatTextEntry::RESIST, 0, spellId, false, 0, attackerGuid, victimGuid);
|
||||
} else if (attackerGuid == playerGuid) {
|
||||
addCombatText(CombatTextEntry::MISS, 0, spellId, true, 0, attackerGuid, victimGuid);
|
||||
addCombatText(CombatTextEntry::RESIST, 0, spellId, true, 0, attackerGuid, victimGuid);
|
||||
}
|
||||
packet.setReadPos(packet.getSize());
|
||||
break;
|
||||
|
|
@ -14061,6 +14283,31 @@ void GameHandler::addCombatText(CombatTextEntry::Type type, int32_t amount, uint
|
|||
combatLog_.push_back(std::move(log));
|
||||
}
|
||||
|
||||
bool GameHandler::shouldLogSpellstealAura(uint64_t casterGuid, uint64_t victimGuid, uint32_t spellId) {
|
||||
if (spellId == 0) return false;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
constexpr auto kRecentWindow = std::chrono::seconds(1);
|
||||
while (!recentSpellstealLogs_.empty() &&
|
||||
now - recentSpellstealLogs_.front().timestamp > kRecentWindow) {
|
||||
recentSpellstealLogs_.pop_front();
|
||||
}
|
||||
|
||||
for (auto it = recentSpellstealLogs_.begin(); it != recentSpellstealLogs_.end(); ++it) {
|
||||
if (it->casterGuid == casterGuid &&
|
||||
it->victimGuid == victimGuid &&
|
||||
it->spellId == spellId) {
|
||||
recentSpellstealLogs_.erase(it);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (recentSpellstealLogs_.size() >= MAX_RECENT_SPELLSTEAL_LOGS)
|
||||
recentSpellstealLogs_.pop_front();
|
||||
recentSpellstealLogs_.push_back({casterGuid, victimGuid, spellId, now});
|
||||
return true;
|
||||
}
|
||||
|
||||
void GameHandler::updateCombatText(float deltaTime) {
|
||||
for (auto& entry : combatText) {
|
||||
entry.age += deltaTime;
|
||||
|
|
@ -16253,14 +16500,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);
|
||||
|
|
@ -16862,23 +17109,30 @@ void GameHandler::handleSpellGo(network::Packet& packet) {
|
|||
// Clear unit cast bar when the spell lands (for any tracked unit)
|
||||
unitCastStates_.erase(data.casterUnit);
|
||||
|
||||
// Show miss/dodge/parry/etc combat text when player's spells miss targets
|
||||
if (data.casterUnit == playerGuid && !data.missTargets.empty()) {
|
||||
// Preserve spellId and actual participants for spell-go miss results.
|
||||
// This keeps the persistent combat log aligned with the later GUID fixes.
|
||||
if (!data.missTargets.empty()) {
|
||||
static const CombatTextEntry::Type missTypes[] = {
|
||||
CombatTextEntry::MISS, // 0=MISS
|
||||
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
|
||||
};
|
||||
// Show text for each miss (usually just 1 target per spell go)
|
||||
const uint64_t spellCasterGuid = data.casterUnit != 0 ? data.casterUnit : data.casterGuid;
|
||||
const bool playerIsCaster = (spellCasterGuid == playerGuid);
|
||||
|
||||
for (const auto& m : data.missTargets) {
|
||||
CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] : CombatTextEntry::MISS;
|
||||
addCombatText(ct, 0, 0, true);
|
||||
if (!playerIsCaster && m.targetGuid != playerGuid) {
|
||||
continue;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18118,14 +18372,25 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) {
|
|||
lower.find("coffer") != std::string::npos ||
|
||||
lower.find("cache") != std::string::npos);
|
||||
}
|
||||
// For WotLK, CMSG_GAMEOBJ_REPORT_USE is required for chests (and is harmless for others).
|
||||
if (!isMailbox && isActiveExpansion("wotlk")) {
|
||||
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
|
||||
reportUse.writeUInt64(guid);
|
||||
socket->send(reportUse);
|
||||
// Some servers require CMSG_GAMEOBJ_REPORT_USE for lootable gameobjects.
|
||||
// Only send it when the active opcode table actually supports it.
|
||||
if (!isMailbox) {
|
||||
const auto* table = getActiveOpcodeTable();
|
||||
if (table && table->hasOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE)) {
|
||||
network::Packet reportUse(wireOpcode(Opcode::CMSG_GAMEOBJ_REPORT_USE));
|
||||
reportUse.writeUInt64(guid);
|
||||
socket->send(reportUse);
|
||||
}
|
||||
}
|
||||
if (shouldSendLoot) {
|
||||
lootTarget(guid);
|
||||
// Some servers/scripts only make certain quest/chest GOs lootable after a short delay
|
||||
// (use animation, state change). Queue one delayed loot attempt to catch that case.
|
||||
pendingGameObjectLootOpens_.erase(
|
||||
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
|
||||
[&](const PendingLootOpen& p) { return p.guid == guid; }),
|
||||
pendingGameObjectLootOpens_.end());
|
||||
pendingGameObjectLootOpens_.push_back(PendingLootOpen{guid, 0.75f});
|
||||
} else {
|
||||
// Non-lootable interaction (mailbox, door, button, etc.) — no CMSG_LOOT will be
|
||||
// sent, and no SMSG_LOOT_RESPONSE will arrive to clear it. Clear the gather-loot
|
||||
|
|
@ -19298,6 +19563,10 @@ void GameHandler::handleLootResponse(network::Packet& packet) {
|
|||
if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return;
|
||||
lootWindowOpen = true;
|
||||
lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo
|
||||
pendingGameObjectLootOpens_.erase(
|
||||
std::remove_if(pendingGameObjectLootOpens_.begin(), pendingGameObjectLootOpens_.end(),
|
||||
[&](const PendingLootOpen& p) { return p.guid == currentLoot.lootGuid; }),
|
||||
pendingGameObjectLootOpens_.end());
|
||||
localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false};
|
||||
|
||||
// Query item info so loot window can show names instead of IDs
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -461,7 +461,7 @@ void PerformanceHUD::render(const Renderer* renderer, const Camera* camera) {
|
|||
|
||||
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.5f, 1.0f), "Movement");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "WASD: Move/Strafe");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Q/E: Turn left/right");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Q/E: Strafe left/right");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "Space: Jump");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "X: Sit/Stand");
|
||||
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 1.0f), "~: Auto-run");
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
surface.vertexBuffer = VK_NULL_HANDLE;
|
||||
}
|
||||
if (surface.indexBuffer) {
|
||||
AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc;
|
||||
destroyBuffer(allocator, ab);
|
||||
surface.indexBuffer = VK_NULL_HANDLE;
|
||||
}
|
||||
if (surface.materialUBO) {
|
||||
AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc;
|
||||
destroyBuffer(allocator, ab);
|
||||
surface.materialUBO = VK_NULL_HANDLE;
|
||||
}
|
||||
if (surface.materialSet && materialDescPool) {
|
||||
vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet);
|
||||
}
|
||||
::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;
|
||||
surface.vertexAlloc = VK_NULL_HANDLE;
|
||||
surface.indexBuffer = VK_NULL_HANDLE;
|
||||
surface.indexAlloc = VK_NULL_HANDLE;
|
||||
surface.materialUBO = VK_NULL_HANDLE;
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================================
|
||||
|
|
|
|||
|
|
@ -2275,9 +2275,25 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
auto& io = ImGui::GetIO();
|
||||
auto& input = core::Input::getInstance();
|
||||
|
||||
// If the user is typing (or about to focus chat this frame), do not allow
|
||||
// A-Z or 1-0 shortcuts to fire.
|
||||
if (!io.WantTextInput && !chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
||||
refocusChatInput = true;
|
||||
chatInputBuffer[0] = '/';
|
||||
chatInputBuffer[1] = '\0';
|
||||
chatInputMoveCursorToEnd = true;
|
||||
}
|
||||
if (!io.WantTextInput && !chatInputActive &&
|
||||
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
||||
refocusChatInput = true;
|
||||
}
|
||||
|
||||
const bool textFocus = chatInputActive || refocusChatInput || io.WantTextInput || io.WantCaptureKeyboard;
|
||||
|
||||
// Tab targeting (when keyboard not captured by UI)
|
||||
if (!io.WantCaptureKeyboard) {
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
||||
// When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts.
|
||||
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
||||
const auto& movement = gameHandler.getMovementInfo();
|
||||
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
||||
}
|
||||
|
|
@ -2300,88 +2316,76 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
|
||||
// Toggle character screen (C) and inventory/bags (I)
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
||||
inventoryScreen.toggleCharacter();
|
||||
}
|
||||
if (!textFocus) {
|
||||
// Toggle character screen (C) and inventory/bags (I)
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
||||
inventoryScreen.toggleCharacter();
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_BAGS)) {
|
||||
if (inventoryScreen.isSeparateBags()) {
|
||||
inventoryScreen.openAllBags();
|
||||
} else {
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
||||
showNameplates_ = !showNameplates_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_BAGS)) {
|
||||
if (inventoryScreen.isSeparateBags()) {
|
||||
inventoryScreen.openAllBags();
|
||||
} else {
|
||||
inventoryScreen.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
||||
showWorldMap_ = !showWorldMap_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
||||
showNameplates_ = !showNameplates_;
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
||||
showMinimap_ = !showMinimap_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
||||
showWorldMap_ = !showWorldMap_;
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
||||
showRaidFrames_ = !showRaidFrames_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
||||
showMinimap_ = !showMinimap_;
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_QUEST_LOG)) {
|
||||
questLogScreen.toggle();
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
||||
showRaidFrames_ = !showRaidFrames_;
|
||||
}
|
||||
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
||||
showAchievementWindow_ = !showAchievementWindow_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_QUEST_LOG)) {
|
||||
questLogScreen.toggle();
|
||||
}
|
||||
|
||||
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
|
||||
showTitlesWindow_ = !showTitlesWindow_;
|
||||
}
|
||||
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
||||
showAchievementWindow_ = !showAchievementWindow_;
|
||||
}
|
||||
|
||||
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
||||
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
|
||||
showTitlesWindow_ = !showTitlesWindow_;
|
||||
}
|
||||
|
||||
// Action bar keys (1-9, 0, -, =)
|
||||
static const SDL_Scancode actionBarKeys[] = {
|
||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
||||
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
||||
};
|
||||
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||
const auto& bar = gameHandler.getActionBar();
|
||||
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
||||
if (input.isKeyJustPressed(actionBarKeys[i])) {
|
||||
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
|
||||
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
|
||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.castSpell(bar[slotIdx].id, target);
|
||||
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
|
||||
gameHandler.useItemById(bar[slotIdx].id);
|
||||
// Action bar keys (1-9, 0, -, =)
|
||||
static const SDL_Scancode actionBarKeys[] = {
|
||||
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
||||
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
||||
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
||||
};
|
||||
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
||||
const auto& bar = gameHandler.getActionBar();
|
||||
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
||||
if (input.isKeyJustPressed(actionBarKeys[i])) {
|
||||
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
|
||||
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
|
||||
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
||||
gameHandler.castSpell(bar[slotIdx].id, target);
|
||||
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
|
||||
gameHandler.useItemById(bar[slotIdx].id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Slash key: focus chat input — always works unless already typing in chat
|
||||
if (!chatInputActive && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
||||
refocusChatInput = true;
|
||||
chatInputBuffer[0] = '/';
|
||||
chatInputBuffer[1] = '\0';
|
||||
chatInputMoveCursorToEnd = true;
|
||||
}
|
||||
|
||||
// Enter key: focus chat input (empty) — always works unless already typing
|
||||
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
||||
refocusChatInput = true;
|
||||
}
|
||||
|
||||
// Cursor affordance: show hand cursor over interactable game objects.
|
||||
if (!io.WantCaptureMouse) {
|
||||
auto* renderer = core::Application::getInstance().getRenderer();
|
||||
|
|
@ -2534,6 +2538,25 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
||||
auto* window = core::Application::getInstance().getWindow();
|
||||
if (camera && window) {
|
||||
// If a quest objective gameobject is under the cursor, prefer it over
|
||||
// hostile units so quest pickups (e.g. "Bundle of Wood") are reliable.
|
||||
std::unordered_set<uint32_t> questObjectiveGoEntries;
|
||||
{
|
||||
const auto& ql = gameHandler.getQuestLog();
|
||||
questObjectiveGoEntries.reserve(32);
|
||||
for (const auto& q : ql) {
|
||||
if (q.complete) continue;
|
||||
for (const auto& obj : q.killObjectives) {
|
||||
if (obj.npcOrGoId >= 0 || obj.required == 0) continue;
|
||||
uint32_t entry = static_cast<uint32_t>(-obj.npcOrGoId);
|
||||
uint32_t cur = 0;
|
||||
auto it = q.killCounts.find(entry);
|
||||
if (it != q.killCounts.end()) cur = it->second.first;
|
||||
if (cur < obj.required) questObjectiveGoEntries.insert(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
glm::vec2 mousePos = input.getMousePosition();
|
||||
float screenW = static_cast<float>(window->getWidth());
|
||||
float screenH = static_cast<float>(window->getHeight());
|
||||
|
|
@ -2543,13 +2566,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
game::ObjectType closestType = game::ObjectType::OBJECT;
|
||||
float closestHostileUnitT = 1e30f;
|
||||
uint64_t closestHostileUnitGuid = 0;
|
||||
float closestQuestGoT = 1e30f;
|
||||
uint64_t closestQuestGoGuid = 0;
|
||||
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
||||
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
||||
auto t = entity->getType();
|
||||
if (t != game::ObjectType::UNIT &&
|
||||
t != game::ObjectType::PLAYER &&
|
||||
t != game::ObjectType::GAMEOBJECT) continue;
|
||||
t != game::ObjectType::GAMEOBJECT)
|
||||
continue;
|
||||
if (guid == myGuid) continue;
|
||||
|
||||
glm::vec3 hitCenter;
|
||||
float hitRadius = 0.0f;
|
||||
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
||||
|
|
@ -2564,10 +2591,15 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
} else if (t == game::ObjectType::GAMEOBJECT) {
|
||||
// For GOs with no renderer instance yet, use a tight fallback
|
||||
// sphere (not 2.5f) so invisible/unloaded GOs (chairs, doodads)
|
||||
// are not accidentally clicked during camera right-drag.
|
||||
// sphere so invisible/unloaded doodads aren't accidentally clicked.
|
||||
hitRadius = 1.2f;
|
||||
heightOffset = 1.0f;
|
||||
// Quest objective GOs should be easier to click.
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
hitRadius = 2.2f;
|
||||
heightOffset = 1.2f;
|
||||
}
|
||||
}
|
||||
hitCenter = core::coords::canonicalToRender(
|
||||
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
||||
|
|
@ -2575,6 +2607,7 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
} else {
|
||||
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
||||
}
|
||||
|
||||
float hitT;
|
||||
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
||||
if (t == game::ObjectType::UNIT) {
|
||||
|
|
@ -2585,6 +2618,15 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
closestHostileUnitGuid = guid;
|
||||
}
|
||||
}
|
||||
if (t == game::ObjectType::GAMEOBJECT && !questObjectiveGoEntries.empty()) {
|
||||
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
||||
if (questObjectiveGoEntries.count(go->getEntry())) {
|
||||
if (hitT < closestQuestGoT) {
|
||||
closestQuestGoT = hitT;
|
||||
closestQuestGoGuid = guid;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hitT < closestT) {
|
||||
closestT = hitT;
|
||||
closestGuid = guid;
|
||||
|
|
@ -2592,11 +2634,17 @@ void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
|
||||
if (closestHostileUnitGuid != 0) {
|
||||
|
||||
// Prefer quest objective GOs over hostile monsters when both are hittable.
|
||||
if (closestQuestGoGuid != 0) {
|
||||
closestGuid = closestQuestGoGuid;
|
||||
closestType = game::ObjectType::GAMEOBJECT;
|
||||
} else if (closestHostileUnitGuid != 0) {
|
||||
// Prefer hostile monsters over nearby gameobjects/others when right-click picking.
|
||||
closestGuid = closestHostileUnitGuid;
|
||||
closestType = game::ObjectType::UNIT;
|
||||
}
|
||||
|
||||
if (closestGuid != 0) {
|
||||
if (closestType == game::ObjectType::GAMEOBJECT) {
|
||||
gameHandler.setTarget(closestGuid);
|
||||
|
|
@ -8342,6 +8390,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 +8419,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 +8451,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())
|
||||
|
|
@ -8397,6 +8470,44 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|||
color = ImVec4(1.0f, 0.85f, 0.0f, alpha); // Gold for proc
|
||||
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: {
|
||||
const std::string& interruptedName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : "";
|
||||
if (!interruptedName.empty())
|
||||
snprintf(text, sizeof(text), "Interrupt %s", interruptedName.c_str());
|
||||
else
|
||||
snprintf(text, sizeof(text), "Interrupt");
|
||||
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);
|
||||
|
|
@ -11161,7 +11272,9 @@ void GameScreen::renderLfgProposalPopup(game::GameHandler& gameHandler) {
|
|||
|
||||
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
||||
// Guild Roster toggle (customizable keybind)
|
||||
if (!ImGui::GetIO().WantCaptureKeyboard && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
||||
if (!chatInputActive && !ImGui::GetIO().WantTextInput &&
|
||||
!ImGui::GetIO().WantCaptureKeyboard &&
|
||||
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
||||
showGuildRoster_ = !showGuildRoster_;
|
||||
if (showGuildRoster_) {
|
||||
// Open friends tab directly if not in guild
|
||||
|
|
@ -19480,7 +19593,8 @@ void GameScreen::renderZoneText() {
|
|||
// ---------------------------------------------------------------------------
|
||||
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
||||
// Toggle Dungeon Finder (customizable keybind)
|
||||
if (!chatInputActive && KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
||||
if (!chatInputActive && !ImGui::GetIO().WantTextInput &&
|
||||
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
||||
showDungeonFinder_ = !showDungeonFinder_;
|
||||
}
|
||||
|
||||
|
|
@ -20214,6 +20328,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);
|
||||
|
|
@ -20243,6 +20364,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);
|
||||
|
|
@ -20254,6 +20389,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);
|
||||
|
|
@ -20276,6 +20418,17 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
|
|||
snprintf(desc, sizeof(desc), "%s dispels from %s", src, tgt);
|
||||
color = ImVec4(0.6f, 0.9f, 1.0f, 1.0f);
|
||||
break;
|
||||
case T::STEAL:
|
||||
if (spell && e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You steal %s from %s", spell, tgt);
|
||||
else if (spell)
|
||||
snprintf(desc, sizeof(desc), "%s steals %s from %s", src, spell, tgt);
|
||||
else if (e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You steal from %s", tgt);
|
||||
else
|
||||
snprintf(desc, sizeof(desc), "%s steals from %s", src, tgt);
|
||||
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
|
||||
break;
|
||||
case T::INTERRUPT:
|
||||
if (spell && e.isPlayerSource)
|
||||
snprintf(desc, sizeof(desc), "You interrupt %s's %s", tgt, spell);
|
||||
|
|
@ -20287,6 +20440,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);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
|
||||
namespace wowee::ui {
|
||||
|
||||
static bool isReservedMovementKey(ImGuiKey key) {
|
||||
return key == ImGuiKey_W || key == ImGuiKey_A || key == ImGuiKey_S ||
|
||||
key == ImGuiKey_D || key == ImGuiKey_Q || key == ImGuiKey_E;
|
||||
}
|
||||
|
||||
KeybindingManager& KeybindingManager::getInstance() {
|
||||
static KeybindingManager instance;
|
||||
return instance;
|
||||
|
|
@ -37,7 +42,21 @@ void KeybindingManager::initializeDefaults() {
|
|||
bool KeybindingManager::isActionPressed(Action action, bool repeat) {
|
||||
auto it = bindings_.find(static_cast<int>(action));
|
||||
if (it == bindings_.end()) return false;
|
||||
return ImGui::IsKeyPressed(it->second, repeat);
|
||||
ImGuiKey key = it->second;
|
||||
if (key == ImGuiKey_None) return false;
|
||||
|
||||
// When typing in a text field (e.g. chat input), never treat A-Z or 0-9 as shortcuts.
|
||||
const ImGuiIO& io = ImGui::GetIO();
|
||||
// Note: WantTextInput may not be set until the text widget is processed later in the
|
||||
// frame, but WantCaptureKeyboard remains true while an ImGui widget is active.
|
||||
if (io.WantTextInput || io.WantCaptureKeyboard) {
|
||||
if ((key >= ImGuiKey_A && key <= ImGuiKey_Z) ||
|
||||
(key >= ImGuiKey_0 && key <= ImGuiKey_9)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return ImGui::IsKeyPressed(key, repeat);
|
||||
}
|
||||
|
||||
ImGuiKey KeybindingManager::getKeyForAction(Action action) const {
|
||||
|
|
@ -47,6 +66,11 @@ ImGuiKey KeybindingManager::getKeyForAction(Action action) const {
|
|||
}
|
||||
|
||||
void KeybindingManager::setKeyForAction(Action action, ImGuiKey key) {
|
||||
// Reserve movement keys so they cannot be used as UI shortcuts.
|
||||
(void)action;
|
||||
if (isReservedMovementKey(key)) {
|
||||
key = ImGuiKey_None;
|
||||
}
|
||||
bindings_[static_cast<int>(action)] = key;
|
||||
}
|
||||
|
||||
|
|
@ -175,9 +199,14 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
|||
}
|
||||
}
|
||||
|
||||
if (key != ImGuiKey_None) {
|
||||
bindings_[actionIdx] = key;
|
||||
if (key == ImGuiKey_None) continue;
|
||||
|
||||
// Reserve movement keys so they cannot be used as UI shortcuts.
|
||||
if (isReservedMovementKey(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bindings_[actionIdx] = key;
|
||||
}
|
||||
|
||||
file.close();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue