mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Compare commits
64 commits
5513c4aad5
...
87cb293297
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87cb293297 | ||
|
|
6f936f258f | ||
|
|
dd64724dbb | ||
|
|
a4415eb207 | ||
|
|
b00025918c | ||
|
|
c870460dea | ||
|
|
32497552d1 | ||
|
|
a731223e47 | ||
|
|
c70740fcdf | ||
|
|
005b1fcb54 | ||
|
|
b29d76bbc8 | ||
|
|
49ba89dfc3 | ||
|
|
67c8101f67 | ||
|
|
5df5f4d423 | ||
|
|
113be66314 | ||
|
|
48cb7df4b4 | ||
|
|
d44d462686 | ||
|
|
072f256af6 | ||
|
|
e62ae8b03e | ||
|
|
63f4d10ab1 | ||
|
|
4ce6fdb5f3 | ||
|
|
d0df6eed2c | ||
|
|
614fcf6b98 | ||
|
|
1f20f55c62 | ||
|
|
7c932559e0 | ||
|
|
279b4de09a | ||
|
|
b8712f380d | ||
|
|
f9947300da | ||
|
|
4a439fb0d1 | ||
|
|
d60d296b77 | ||
|
|
488ec945b6 | ||
|
|
36fed15d43 | ||
|
|
d558e3a927 | ||
|
|
315adfbe93 | ||
|
|
06ad676be1 | ||
|
|
2d00f00261 | ||
|
|
cd39cd821f | ||
|
|
8411c39eaf | ||
|
|
5ad849666d | ||
|
|
0f2f9ff78d | ||
|
|
b22183b000 | ||
|
|
220f1b177c | ||
|
|
495dfb7aae | ||
|
|
fba6aba80d | ||
|
|
dcf9aeed92 | ||
|
|
caad20285b | ||
|
|
d1a392cd0e | ||
|
|
1e80e294f0 | ||
|
|
cb99dbaea4 | ||
|
|
7e6de75e8a | ||
|
|
dab03f2729 | ||
|
|
dee33db0aa | ||
|
|
973db16658 | ||
|
|
1f1925797f | ||
|
|
98dc2a0dc7 | ||
|
|
c15ef915bf | ||
|
|
6d83027226 | ||
|
|
4edc4017ed | ||
|
|
3b79f44b54 | ||
|
|
020ba134cd | ||
|
|
03397ec23c | ||
|
|
f04875514e | ||
|
|
8b57e6fa45 | ||
|
|
b6ea78dfab |
26 changed files with 2501 additions and 219 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||||
"DispelType": 4
|
"DispelType": 4
|
||||||
|
|
@ -95,5 +95,14 @@
|
||||||
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
"ID": 0, "MapID": 1, "AreaID": 2, "AreaName": 3,
|
||||||
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
"LocLeft": 4, "LocRight": 5, "LocTop": 6, "LocBottom": 7,
|
||||||
"DisplayMapID": 8, "ParentWorldMapID": 10
|
"DisplayMapID": 8, "ParentWorldMapID": 10
|
||||||
|
},
|
||||||
|
"SpellVisual": {
|
||||||
|
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||||
|
},
|
||||||
|
"SpellVisualKit": {
|
||||||
|
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||||
|
},
|
||||||
|
"SpellVisualEffectName": {
|
||||||
|
"ID": 0, "FilePath": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 124,
|
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 124,
|
||||||
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215,
|
"Name": 127, "Tooltip": 154, "Rank": 136, "SchoolMask": 215,
|
||||||
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40,
|
"CastingTimeIndex": 22, "PowerType": 35, "ManaCost": 36, "RangeIndex": 40,
|
||||||
"DispelType": 3
|
"DispelType": 3
|
||||||
|
|
@ -111,5 +111,14 @@
|
||||||
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
"Threshold0": 38, "Threshold1": 39, "Threshold2": 40, "Threshold3": 41,
|
||||||
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
"Threshold4": 42, "Threshold5": 43, "Threshold6": 44, "Threshold7": 45,
|
||||||
"Threshold8": 46, "Threshold9": 47
|
"Threshold8": 46, "Threshold9": 47
|
||||||
|
},
|
||||||
|
"SpellVisual": {
|
||||||
|
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||||
|
},
|
||||||
|
"SpellVisualKit": {
|
||||||
|
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||||
|
},
|
||||||
|
"SpellVisualEffectName": {
|
||||||
|
"ID": 0, "FilePath": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 5, "IconID": 117,
|
"ID": 0, "Attributes": 5, "AttributesEx": 6, "IconID": 117,
|
||||||
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
"Name": 120, "Tooltip": 147, "Rank": 129, "SchoolEnum": 1,
|
||||||
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
"CastingTimeIndex": 15, "PowerType": 28, "ManaCost": 29, "RangeIndex": 33,
|
||||||
"DispelType": 4
|
"DispelType": 4
|
||||||
|
|
@ -108,5 +108,14 @@
|
||||||
"Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33,
|
"Threshold0": 30, "Threshold1": 31, "Threshold2": 32, "Threshold3": 33,
|
||||||
"Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37,
|
"Threshold4": 34, "Threshold5": 35, "Threshold6": 36, "Threshold7": 37,
|
||||||
"Threshold8": 38, "Threshold9": 39
|
"Threshold8": 38, "Threshold9": 39
|
||||||
|
},
|
||||||
|
"SpellVisual": {
|
||||||
|
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||||
|
},
|
||||||
|
"SpellVisualKit": {
|
||||||
|
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||||
|
},
|
||||||
|
"SpellVisualEffectName": {
|
||||||
|
"ID": 0, "FilePath": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"Spell": {
|
"Spell": {
|
||||||
"ID": 0, "Attributes": 4, "IconID": 133,
|
"ID": 0, "Attributes": 4, "AttributesEx": 5, "IconID": 133,
|
||||||
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225,
|
"Name": 136, "Tooltip": 139, "Rank": 153, "SchoolMask": 225,
|
||||||
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49,
|
"PowerType": 14, "ManaCost": 39, "CastingTimeIndex": 47, "RangeIndex": 49,
|
||||||
"DispelType": 2
|
"DispelType": 2
|
||||||
|
|
@ -116,5 +116,14 @@
|
||||||
},
|
},
|
||||||
"LFGDungeons": {
|
"LFGDungeons": {
|
||||||
"ID": 0, "Name": 1
|
"ID": 0, "Name": 1
|
||||||
|
},
|
||||||
|
"SpellVisual": {
|
||||||
|
"ID": 0, "CastKit": 2, "ImpactKit": 3, "MissileModel": 8
|
||||||
|
},
|
||||||
|
"SpellVisualKit": {
|
||||||
|
"ID": 0, "BaseEffect": 5, "SpecialEffect0": 11, "SpecialEffect1": 12, "SpecialEffect2": 13
|
||||||
|
},
|
||||||
|
"SpellVisualEffectName": {
|
||||||
|
"ID": 0, "FilePath": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -497,6 +497,15 @@ public:
|
||||||
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
return bgScoreboard_.players.empty() ? nullptr : &bgScoreboard_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
|
||||||
|
struct BgPlayerPosition {
|
||||||
|
uint64_t guid = 0;
|
||||||
|
float wowX = 0.0f; // canonical WoW X (north)
|
||||||
|
float wowY = 0.0f; // canonical WoW Y (west)
|
||||||
|
int group = 0; // 0 = first list (usually ally flag carriers), 1 = second list
|
||||||
|
};
|
||||||
|
const std::vector<BgPlayerPosition>& getBgPlayerPositions() const { return bgPlayerPositions_; }
|
||||||
|
|
||||||
// Network latency (milliseconds, updated each PONG response)
|
// Network latency (milliseconds, updated each PONG response)
|
||||||
uint32_t getLatencyMs() const { return lastLatency; }
|
uint32_t getLatencyMs() const { return lastLatency; }
|
||||||
|
|
||||||
|
|
@ -709,6 +718,8 @@ public:
|
||||||
void dismissPet();
|
void dismissPet();
|
||||||
void renamePet(const std::string& newName);
|
void renamePet(const std::string& newName);
|
||||||
bool hasPet() const { return petGuid_ != 0; }
|
bool hasPet() const { return petGuid_ != 0; }
|
||||||
|
// Returns true once after SMSG_PET_RENAMEABLE; consuming the flag clears it.
|
||||||
|
bool consumePetRenameablePending() { bool v = petRenameablePending_; petRenameablePending_ = false; return v; }
|
||||||
uint64_t getPetGuid() const { return petGuid_; }
|
uint64_t getPetGuid() const { return petGuid_; }
|
||||||
|
|
||||||
// ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ----
|
// ---- Pet state (populated by SMSG_PET_SPELLS / SMSG_PET_MODE) ----
|
||||||
|
|
@ -798,6 +809,7 @@ public:
|
||||||
uint32_t spellId = 0;
|
uint32_t spellId = 0;
|
||||||
float timeRemaining = 0.0f;
|
float timeRemaining = 0.0f;
|
||||||
float timeTotal = 0.0f;
|
float timeTotal = 0.0f;
|
||||||
|
bool interruptible = true; ///< false when SPELL_ATTR_EX_NOT_INTERRUPTIBLE is set
|
||||||
};
|
};
|
||||||
// Returns cast state for any unit by GUID (empty/non-casting if not found)
|
// Returns cast state for any unit by GUID (empty/non-casting if not found)
|
||||||
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
const UnitCastState* getUnitCastState(uint64_t guid) const {
|
||||||
|
|
@ -819,6 +831,10 @@ public:
|
||||||
auto* s = getUnitCastState(targetGuid);
|
auto* s = getUnitCastState(targetGuid);
|
||||||
return s ? s->timeRemaining : 0.0f;
|
return s ? s->timeRemaining : 0.0f;
|
||||||
}
|
}
|
||||||
|
bool isTargetCastInterruptible() const {
|
||||||
|
auto* s = getUnitCastState(targetGuid);
|
||||||
|
return s ? s->interruptible : true;
|
||||||
|
}
|
||||||
|
|
||||||
// Talents
|
// Talents
|
||||||
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
uint8_t getActiveTalentSpec() const { return activeTalentSpec_; }
|
||||||
|
|
@ -1160,6 +1176,11 @@ public:
|
||||||
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
|
uint32_t getTalentWipeCost() const { return talentWipeCost_; }
|
||||||
void confirmTalentWipe();
|
void confirmTalentWipe();
|
||||||
void cancelTalentWipe() { talentWipePending_ = false; }
|
void cancelTalentWipe() { talentWipePending_ = false; }
|
||||||
|
// Pet talent respec confirm
|
||||||
|
bool showPetUnlearnDialog() const { return petUnlearnPending_; }
|
||||||
|
uint32_t getPetUnlearnCost() const { return petUnlearnCost_; }
|
||||||
|
void confirmPetUnlearn();
|
||||||
|
void cancelPetUnlearn() { petUnlearnPending_ = false; }
|
||||||
/** True when ghost is within 40 yards of corpse position (same map). */
|
/** True when ghost is within 40 yards of corpse position (same map). */
|
||||||
bool canReclaimCorpse() const;
|
bool canReclaimCorpse() const;
|
||||||
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
|
/** Distance (yards) from ghost to corpse, or -1 if no corpse data. */
|
||||||
|
|
@ -1389,6 +1410,10 @@ public:
|
||||||
const LootResponseData& getCurrentLoot() const { return currentLoot; }
|
const LootResponseData& getCurrentLoot() const { return currentLoot; }
|
||||||
void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
|
void setAutoLoot(bool enabled) { autoLoot_ = enabled; }
|
||||||
bool isAutoLoot() const { return autoLoot_; }
|
bool isAutoLoot() const { return autoLoot_; }
|
||||||
|
void setAutoSellGrey(bool enabled) { autoSellGrey_ = enabled; }
|
||||||
|
bool isAutoSellGrey() const { return autoSellGrey_; }
|
||||||
|
void setAutoRepair(bool enabled) { autoRepair_ = enabled; }
|
||||||
|
bool isAutoRepair() const { return autoRepair_; }
|
||||||
|
|
||||||
// Master loot candidates (from SMSG_LOOT_MASTER_LIST)
|
// Master loot candidates (from SMSG_LOOT_MASTER_LIST)
|
||||||
const std::vector<uint64_t>& getMasterLootCandidates() const { return masterLootCandidates_; }
|
const std::vector<uint64_t>& getMasterLootCandidates() const { return masterLootCandidates_; }
|
||||||
|
|
@ -1435,6 +1460,9 @@ public:
|
||||||
void acceptQuest();
|
void acceptQuest();
|
||||||
void declineQuest();
|
void declineQuest();
|
||||||
void closeGossip();
|
void closeGossip();
|
||||||
|
// Quest-starting items: right-click triggers quest offer dialog via questgiver protocol
|
||||||
|
void offerQuestFromItem(uint64_t itemGuid, uint32_t questId);
|
||||||
|
uint64_t getBagItemGuid(int bagIndex, int slotIndex) const;
|
||||||
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
bool isGossipWindowOpen() const { return gossipWindowOpen; }
|
||||||
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
const GossipMessageData& getCurrentGossip() const { return currentGossip; }
|
||||||
bool isQuestDetailsOpen() {
|
bool isQuestDetailsOpen() {
|
||||||
|
|
@ -1919,6 +1947,11 @@ public:
|
||||||
float x = 0, y = 0, z = 0;
|
float x = 0, y = 0, z = 0;
|
||||||
};
|
};
|
||||||
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
|
const std::unordered_map<uint32_t, TaxiNode>& getTaxiNodes() const { return taxiNodes_; }
|
||||||
|
bool isKnownTaxiNode(uint32_t nodeId) const {
|
||||||
|
if (nodeId == 0 || nodeId > 384) return false;
|
||||||
|
uint32_t idx = nodeId - 1;
|
||||||
|
return (knownTaxiMask_[idx / 32] & (1u << (idx % 32))) != 0;
|
||||||
|
}
|
||||||
uint32_t getTaxiCostTo(uint32_t destNodeId) const;
|
uint32_t getTaxiCostTo(uint32_t destNodeId) const;
|
||||||
bool taxiNpcHasRoutes(uint64_t guid) const {
|
bool taxiNpcHasRoutes(uint64_t guid) const {
|
||||||
auto it = taxiNpcHasRoutes_.find(guid);
|
auto it = taxiNpcHasRoutes_.find(guid);
|
||||||
|
|
@ -2051,6 +2084,13 @@ public:
|
||||||
const std::string& getSkillLineName(uint32_t spellId) const;
|
const std::string& getSkillLineName(uint32_t spellId) const;
|
||||||
/// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other)
|
/// Returns the DispelType for a spell (0=none,1=magic,2=curse,3=disease,4=poison,5+=other)
|
||||||
uint8_t getSpellDispelType(uint32_t spellId) const;
|
uint8_t getSpellDispelType(uint32_t spellId) const;
|
||||||
|
/// Returns true if the spell can be interrupted by abilities like Kick/Counterspell.
|
||||||
|
/// False for spells with SPELL_ATTR_EX_NOT_INTERRUPTIBLE (attrEx bit 4 = 0x10).
|
||||||
|
bool isSpellInterruptible(uint32_t spellId) const;
|
||||||
|
/// Returns the school bitmask for the spell from Spell.dbc
|
||||||
|
/// (0x01=Physical, 0x02=Holy, 0x04=Fire, 0x08=Nature, 0x10=Frost, 0x20=Shadow, 0x40=Arcane).
|
||||||
|
/// Returns 0 if unknown.
|
||||||
|
uint32_t getSpellSchoolMask(uint32_t spellId) const;
|
||||||
|
|
||||||
struct TrainerTab {
|
struct TrainerTab {
|
||||||
std::string name;
|
std::string name;
|
||||||
|
|
@ -2717,6 +2757,7 @@ private:
|
||||||
uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots)
|
uint32_t petActionSlots_[10] = {}; // SMSG_PET_SPELLS action bar (10 slots)
|
||||||
uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss
|
uint8_t petCommand_ = 1; // 0=stay,1=follow,2=attack,3=dismiss
|
||||||
uint8_t petReact_ = 1; // 0=passive,1=defensive,2=aggressive
|
uint8_t petReact_ = 1; // 0=passive,1=defensive,2=aggressive
|
||||||
|
bool petRenameablePending_ = false; // set by SMSG_PET_RENAMEABLE, consumed by UI
|
||||||
std::vector<uint32_t> petSpellList_; // known pet spells
|
std::vector<uint32_t> petSpellList_; // known pet spells
|
||||||
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
std::unordered_set<uint32_t> petAutocastSpells_; // spells with autocast on
|
||||||
|
|
||||||
|
|
@ -2759,6 +2800,9 @@ private:
|
||||||
// BG scoreboard (MSG_PVP_LOG_DATA)
|
// BG scoreboard (MSG_PVP_LOG_DATA)
|
||||||
BgScoreboardData bgScoreboard_;
|
BgScoreboardData bgScoreboard_;
|
||||||
|
|
||||||
|
// BG flag carrier / player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
|
||||||
|
std::vector<BgPlayerPosition> bgPlayerPositions_;
|
||||||
|
|
||||||
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
// Instance encounter boss units (slots 0-4 from SMSG_UPDATE_INSTANCE_ENCOUNTER_UNIT)
|
||||||
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
std::array<uint64_t, kMaxEncounterSlots> encounterUnitGuids_ = {}; // 0 = empty slot
|
||||||
|
|
||||||
|
|
@ -2862,6 +2906,8 @@ private:
|
||||||
// ---- Phase 5: Loot ----
|
// ---- Phase 5: Loot ----
|
||||||
bool lootWindowOpen = false;
|
bool lootWindowOpen = false;
|
||||||
bool autoLoot_ = false;
|
bool autoLoot_ = false;
|
||||||
|
bool autoSellGrey_ = false;
|
||||||
|
bool autoRepair_ = false;
|
||||||
LootResponseData currentLoot;
|
LootResponseData currentLoot;
|
||||||
std::vector<uint64_t> masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST
|
std::vector<uint64_t> masterLootCandidates_; // from SMSG_LOOT_MASTER_LIST
|
||||||
|
|
||||||
|
|
@ -3054,7 +3100,7 @@ private:
|
||||||
// Trainer
|
// Trainer
|
||||||
bool trainerWindowOpen_ = false;
|
bool trainerWindowOpen_ = false;
|
||||||
TrainerListData currentTrainerList_;
|
TrainerListData currentTrainerList_;
|
||||||
struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; };
|
struct SpellNameEntry { std::string name; std::string rank; std::string description; uint32_t schoolMask = 0; uint8_t dispelType = 0; uint32_t attrEx = 0; };
|
||||||
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
std::unordered_map<uint32_t, SpellNameEntry> spellNameCache_;
|
||||||
bool spellNameCacheLoaded_ = false;
|
bool spellNameCacheLoaded_ = false;
|
||||||
|
|
||||||
|
|
@ -3267,6 +3313,10 @@ private:
|
||||||
bool talentWipePending_ = false;
|
bool talentWipePending_ = false;
|
||||||
uint64_t talentWipeNpcGuid_ = 0;
|
uint64_t talentWipeNpcGuid_ = 0;
|
||||||
uint32_t talentWipeCost_ = 0;
|
uint32_t talentWipeCost_ = 0;
|
||||||
|
// ---- Pet talent respec confirm dialog ----
|
||||||
|
bool petUnlearnPending_ = false;
|
||||||
|
uint64_t petUnlearnGuid_ = 0;
|
||||||
|
uint32_t petUnlearnCost_ = 0;
|
||||||
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
bool resurrectIsSpiritHealer_ = false; // true = SMSG_SPIRIT_HEALER_CONFIRM, false = SMSG_RESURRECT_REQUEST
|
||||||
uint64_t resurrectCasterGuid_ = 0;
|
uint64_t resurrectCasterGuid_ = 0;
|
||||||
std::string resurrectCasterName_;
|
std::string resurrectCasterName_;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ struct CombatTextEntry {
|
||||||
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK,
|
||||||
EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL,
|
||||||
ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER,
|
ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER,
|
||||||
DISPEL, STEAL, INTERRUPT, INSTAKILL
|
DISPEL, STEAL, INTERRUPT, INSTAKILL, HONOR_GAIN, GLANCING, CRUSHING
|
||||||
};
|
};
|
||||||
Type type;
|
Type type;
|
||||||
int32_t amount = 0;
|
int32_t amount = 0;
|
||||||
|
|
|
||||||
|
|
@ -147,9 +147,18 @@ private:
|
||||||
uint32_t heapSize_; // Heap size
|
uint32_t heapSize_; // Heap size
|
||||||
uint32_t apiStubBase_; // API stub base address
|
uint32_t apiStubBase_; // API stub base address
|
||||||
|
|
||||||
// API hooks: DLL name -> Function name -> Handler
|
// API hooks: DLL name -> Function name -> stub address
|
||||||
std::map<std::string, std::map<std::string, uint32_t>> apiAddresses_;
|
std::map<std::string, std::map<std::string, uint32_t>> apiAddresses_;
|
||||||
|
|
||||||
|
// API stub dispatch: stub address -> {argCount, handler}
|
||||||
|
struct ApiHookEntry {
|
||||||
|
int argCount;
|
||||||
|
std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler;
|
||||||
|
};
|
||||||
|
std::map<uint32_t, ApiHookEntry> apiHandlers_;
|
||||||
|
uint32_t nextApiStubAddr_; // tracks next free stub slot (replaces static local)
|
||||||
|
bool apiCodeHookRegistered_; // true once UC_HOOK_CODE for stub range is added
|
||||||
|
|
||||||
// Memory allocation tracking
|
// Memory allocation tracking
|
||||||
std::map<uint32_t, size_t> allocations_;
|
std::map<uint32_t, size_t> allocations_;
|
||||||
std::map<uint32_t, size_t> freeBlocks_; // free-list keyed by base address
|
std::map<uint32_t, size_t> freeBlocks_; // free-list keyed by base address
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,7 @@ private:
|
||||||
size_t relocDataOffset_ = 0; // Offset into decompressedData_ where relocation data starts
|
size_t relocDataOffset_ = 0; // Offset into decompressedData_ where relocation data starts
|
||||||
WardenFuncList funcList_; // Callback functions
|
WardenFuncList funcList_; // Callback functions
|
||||||
std::unique_ptr<WardenEmulator> emulator_; // Cross-platform x86 emulator
|
std::unique_ptr<WardenEmulator> emulator_; // Cross-platform x86 emulator
|
||||||
|
uint32_t emulatedPacketHandlerAddr_ = 0; // Raw emulated VA for 4-arg PacketHandler call
|
||||||
|
|
||||||
// Validation and loading steps
|
// Validation and loading steps
|
||||||
bool verifyMD5(const std::vector<uint8_t>& data,
|
bool verifyMD5(const std::vector<uint8_t>& data,
|
||||||
|
|
|
||||||
|
|
@ -1719,8 +1719,10 @@ struct AttackerStateUpdateData {
|
||||||
uint32_t blocked = 0;
|
uint32_t blocked = 0;
|
||||||
|
|
||||||
bool isValid() const { return attackerGuid != 0; }
|
bool isValid() const { return attackerGuid != 0; }
|
||||||
bool isCrit() const { return (hitInfo & 0x200) != 0; }
|
bool isCrit() const { return (hitInfo & 0x0200) != 0; }
|
||||||
bool isMiss() const { return (hitInfo & 0x10) != 0; }
|
bool isMiss() const { return (hitInfo & 0x0010) != 0; }
|
||||||
|
bool isGlancing() const { return (hitInfo & 0x0800) != 0; }
|
||||||
|
bool isCrushing() const { return (hitInfo & 0x1000) != 0; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class AttackerStateUpdateParser {
|
class AttackerStateUpdateParser {
|
||||||
|
|
@ -1873,6 +1875,7 @@ struct SpellGoData {
|
||||||
std::vector<uint64_t> hitTargets;
|
std::vector<uint64_t> hitTargets;
|
||||||
uint8_t missCount = 0;
|
uint8_t missCount = 0;
|
||||||
std::vector<SpellGoMissEntry> missTargets;
|
std::vector<SpellGoMissEntry> missTargets;
|
||||||
|
uint64_t targetGuid = 0; ///< Primary target GUID from SpellCastTargets (0 = none/AoE)
|
||||||
|
|
||||||
bool isValid() const { return spellId != 0; }
|
bool isValid() const { return spellId != 0; }
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset();
|
void reset();
|
||||||
|
void resetAngles();
|
||||||
void teleportTo(const glm::vec3& pos);
|
void teleportTo(const glm::vec3& pos);
|
||||||
void setOnlineMode(bool online) { onlineMode = online; }
|
void setOnlineMode(bool online) { onlineMode = online; }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <future>
|
#include <future>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
#include <unordered_map>
|
||||||
#include <glm/glm.hpp>
|
#include <glm/glm.hpp>
|
||||||
#include <vulkan/vulkan.h>
|
#include <vulkan/vulkan.h>
|
||||||
#include <vk_mem_alloc.h>
|
#include <vk_mem_alloc.h>
|
||||||
|
|
@ -152,6 +153,11 @@ public:
|
||||||
void playEmote(const std::string& emoteName);
|
void playEmote(const std::string& emoteName);
|
||||||
void triggerLevelUpEffect(const glm::vec3& position);
|
void triggerLevelUpEffect(const glm::vec3& position);
|
||||||
void cancelEmote();
|
void cancelEmote();
|
||||||
|
|
||||||
|
// Spell visual effects (SMSG_PLAY_SPELL_VISUAL / SMSG_PLAY_SPELL_IMPACT)
|
||||||
|
// useImpactKit=false → CastKit path; useImpactKit=true → ImpactKit path
|
||||||
|
void playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||||
|
bool useImpactKit = false);
|
||||||
bool isEmoteActive() const { return emoteActive; }
|
bool isEmoteActive() const { return emoteActive; }
|
||||||
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
|
static std::string getEmoteText(const std::string& emoteName, const std::string* targetName = nullptr);
|
||||||
static uint32_t getEmoteDbcId(const std::string& emoteName);
|
static uint32_t getEmoteDbcId(const std::string& emoteName);
|
||||||
|
|
@ -323,6 +329,19 @@ private:
|
||||||
glm::mat4 computeLightSpaceMatrix();
|
glm::mat4 computeLightSpaceMatrix();
|
||||||
|
|
||||||
pipeline::AssetManager* cachedAssetManager = nullptr;
|
pipeline::AssetManager* cachedAssetManager = nullptr;
|
||||||
|
|
||||||
|
// Spell visual effects — transient M2 instances spawned by SMSG_PLAY_SPELL_VISUAL/IMPACT
|
||||||
|
struct SpellVisualInstance { uint32_t instanceId; float elapsed; };
|
||||||
|
std::vector<SpellVisualInstance> activeSpellVisuals_;
|
||||||
|
std::unordered_map<uint32_t, std::string> spellVisualCastPath_; // visualId → cast M2 path
|
||||||
|
std::unordered_map<uint32_t, std::string> spellVisualImpactPath_; // visualId → impact M2 path
|
||||||
|
std::unordered_map<std::string, uint32_t> spellVisualModelIds_; // M2 path → M2Renderer modelId
|
||||||
|
uint32_t nextSpellVisualModelId_ = 999000; // Reserved range 999000-999799
|
||||||
|
bool spellVisualDbcLoaded_ = false;
|
||||||
|
void loadSpellVisualDbc();
|
||||||
|
void updateSpellVisuals(float deltaTime);
|
||||||
|
static constexpr float SPELL_VISUAL_DURATION = 3.5f;
|
||||||
|
|
||||||
uint32_t currentZoneId = 0;
|
uint32_t currentZoneId = 0;
|
||||||
std::string currentZoneName;
|
std::string currentZoneName;
|
||||||
bool inTavern_ = false;
|
bool inTavern_ = false;
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,15 @@ struct WorldMapPartyDot {
|
||||||
std::string name; ///< Member name (shown as tooltip on hover)
|
std::string name; ///< Member name (shown as tooltip on hover)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Taxi (flight master) node passed from the UI layer for world map overlay.
|
||||||
|
struct WorldMapTaxiNode {
|
||||||
|
uint32_t id = 0; ///< TaxiNodes.dbc ID
|
||||||
|
uint32_t mapId = 0; ///< WoW internal map ID (0=EK,1=Kal,530=Outland,571=Northrend)
|
||||||
|
float wowX = 0, wowY = 0, wowZ = 0; ///< Canonical WoW coordinates
|
||||||
|
std::string name; ///< Node name (shown as tooltip)
|
||||||
|
bool known = false; ///< Player has discovered this node
|
||||||
|
};
|
||||||
|
|
||||||
struct WorldMapZone {
|
struct WorldMapZone {
|
||||||
uint32_t wmaID = 0;
|
uint32_t wmaID = 0;
|
||||||
uint32_t areaID = 0; // 0 = continent level
|
uint32_t areaID = 0; // 0 = continent level
|
||||||
|
|
@ -57,6 +66,14 @@ public:
|
||||||
void setMapName(const std::string& name);
|
void setMapName(const std::string& name);
|
||||||
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
void setServerExplorationMask(const std::vector<uint32_t>& masks, bool hasData);
|
||||||
void setPartyDots(std::vector<WorldMapPartyDot> dots) { partyDots_ = std::move(dots); }
|
void setPartyDots(std::vector<WorldMapPartyDot> dots) { partyDots_ = std::move(dots); }
|
||||||
|
void setTaxiNodes(std::vector<WorldMapTaxiNode> nodes) { taxiNodes_ = std::move(nodes); }
|
||||||
|
/// Set the player's corpse position for overlay rendering.
|
||||||
|
/// @param hasCorpse True when the player is a ghost with an unclaimed corpse on this map.
|
||||||
|
/// @param renderPos Corpse position in render-space coordinates.
|
||||||
|
void setCorpsePos(bool hasCorpse, glm::vec3 renderPos) {
|
||||||
|
hasCorpse_ = hasCorpse;
|
||||||
|
corpseRenderPos_ = renderPos;
|
||||||
|
}
|
||||||
bool isOpen() const { return open; }
|
bool isOpen() const { return open; }
|
||||||
void close() { open = false; }
|
void close() { open = false; }
|
||||||
|
|
||||||
|
|
@ -127,6 +144,14 @@ private:
|
||||||
// Party member dots (set each frame from the UI layer)
|
// Party member dots (set each frame from the UI layer)
|
||||||
std::vector<WorldMapPartyDot> partyDots_;
|
std::vector<WorldMapPartyDot> partyDots_;
|
||||||
|
|
||||||
|
// Taxi node markers (set each frame from the UI layer)
|
||||||
|
std::vector<WorldMapTaxiNode> taxiNodes_;
|
||||||
|
int currentMapId_ = -1; ///< WoW map ID currently loaded (set in loadZonesFromDBC)
|
||||||
|
|
||||||
|
// Corpse marker (ghost state — set each frame from the UI layer)
|
||||||
|
bool hasCorpse_ = false;
|
||||||
|
glm::vec3 corpseRenderPos_ = {};
|
||||||
|
|
||||||
// Exploration / fog of war
|
// Exploration / fog of war
|
||||||
std::vector<uint32_t> serverExplorationMask;
|
std::vector<uint32_t> serverExplorationMask;
|
||||||
bool hasServerExplorationMask = false;
|
bool hasServerExplorationMask = false;
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,8 @@ private:
|
||||||
bool pendingSeparateBags = true;
|
bool pendingSeparateBags = true;
|
||||||
bool pendingShowKeyring = true;
|
bool pendingShowKeyring = true;
|
||||||
bool pendingAutoLoot = false;
|
bool pendingAutoLoot = false;
|
||||||
|
bool pendingAutoSellGrey = false;
|
||||||
|
bool pendingAutoRepair = false;
|
||||||
|
|
||||||
// Keybinding customization
|
// Keybinding customization
|
||||||
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
|
int pendingRebindAction = -1; // -1 = not rebinding, otherwise action index
|
||||||
|
|
@ -317,6 +319,7 @@ private:
|
||||||
|
|
||||||
// ---- New UI renders ----
|
// ---- New UI renders ----
|
||||||
void renderActionBar(game::GameHandler& gameHandler);
|
void renderActionBar(game::GameHandler& gameHandler);
|
||||||
|
void renderStanceBar(game::GameHandler& gameHandler);
|
||||||
void renderBagBar(game::GameHandler& gameHandler);
|
void renderBagBar(game::GameHandler& gameHandler);
|
||||||
void renderXpBar(game::GameHandler& gameHandler);
|
void renderXpBar(game::GameHandler& gameHandler);
|
||||||
void renderRepBar(game::GameHandler& gameHandler);
|
void renderRepBar(game::GameHandler& gameHandler);
|
||||||
|
|
@ -355,6 +358,7 @@ private:
|
||||||
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
void renderReclaimCorpseButton(game::GameHandler& gameHandler);
|
||||||
void renderResurrectDialog(game::GameHandler& gameHandler);
|
void renderResurrectDialog(game::GameHandler& gameHandler);
|
||||||
void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler);
|
void renderTalentWipeConfirmDialog(game::GameHandler& gameHandler);
|
||||||
|
void renderPetUnlearnConfirmDialog(game::GameHandler& gameHandler);
|
||||||
void renderEscapeMenu();
|
void renderEscapeMenu();
|
||||||
void renderSettingsWindow();
|
void renderSettingsWindow();
|
||||||
void applyGraphicsPreset(GraphicsPreset preset);
|
void applyGraphicsPreset(GraphicsPreset preset);
|
||||||
|
|
@ -375,7 +379,6 @@ private:
|
||||||
void renderGuildBankWindow(game::GameHandler& gameHandler);
|
void renderGuildBankWindow(game::GameHandler& gameHandler);
|
||||||
void renderAuctionHouseWindow(game::GameHandler& gameHandler);
|
void renderAuctionHouseWindow(game::GameHandler& gameHandler);
|
||||||
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
|
void renderDungeonFinderWindow(game::GameHandler& gameHandler);
|
||||||
void renderObjectiveTracker(game::GameHandler& gameHandler);
|
|
||||||
void renderInstanceLockouts(game::GameHandler& gameHandler);
|
void renderInstanceLockouts(game::GameHandler& gameHandler);
|
||||||
void renderNameplates(game::GameHandler& gameHandler);
|
void renderNameplates(game::GameHandler& gameHandler);
|
||||||
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
void renderBattlegroundScore(game::GameHandler& gameHandler);
|
||||||
|
|
@ -435,6 +438,10 @@ private:
|
||||||
char achievementSearchBuf_[128] = {};
|
char achievementSearchBuf_[128] = {};
|
||||||
void renderAchievementWindow(game::GameHandler& gameHandler);
|
void renderAchievementWindow(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
|
// Skills / Professions window (K key)
|
||||||
|
bool showSkillsWindow_ = false;
|
||||||
|
void renderSkillsWindow(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
// Titles window
|
// Titles window
|
||||||
bool showTitlesWindow_ = false;
|
bool showTitlesWindow_ = false;
|
||||||
void renderTitlesWindow(game::GameHandler& gameHandler);
|
void renderTitlesWindow(game::GameHandler& gameHandler);
|
||||||
|
|
@ -633,7 +640,9 @@ private:
|
||||||
float zoneTextTimer_ = 0.0f;
|
float zoneTextTimer_ = 0.0f;
|
||||||
std::string zoneTextName_;
|
std::string zoneTextName_;
|
||||||
std::string lastKnownZoneName_;
|
std::string lastKnownZoneName_;
|
||||||
void renderZoneText();
|
uint32_t lastKnownWorldStateZoneId_ = 0;
|
||||||
|
void renderZoneText(game::GameHandler& gameHandler);
|
||||||
|
void renderWeatherOverlay(game::GameHandler& gameHandler);
|
||||||
|
|
||||||
// Cooldown tracker
|
// Cooldown tracker
|
||||||
bool showCooldownTracker_ = false;
|
bool showCooldownTracker_ = false;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public:
|
||||||
TOGGLE_NAMEPLATES,
|
TOGGLE_NAMEPLATES,
|
||||||
TOGGLE_RAID_FRAMES,
|
TOGGLE_RAID_FRAMES,
|
||||||
TOGGLE_ACHIEVEMENTS,
|
TOGGLE_ACHIEVEMENTS,
|
||||||
|
TOGGLE_SKILLS,
|
||||||
ACTION_COUNT
|
ACTION_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1849,12 +1849,15 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_CHAT_WRONG_FACTION:
|
case Opcode::SMSG_CHAT_WRONG_FACTION:
|
||||||
|
addUIError("You cannot send messages to members of that faction.");
|
||||||
addSystemChatMessage("You cannot send messages to members of that faction.");
|
addSystemChatMessage("You cannot send messages to members of that faction.");
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_CHAT_NOT_IN_PARTY:
|
case Opcode::SMSG_CHAT_NOT_IN_PARTY:
|
||||||
|
addUIError("You are not in a party.");
|
||||||
addSystemChatMessage("You are not in a party.");
|
addSystemChatMessage("You are not in a party.");
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_CHAT_RESTRICTED:
|
case Opcode::SMSG_CHAT_RESTRICTED:
|
||||||
|
addUIError("You cannot send chat messages in this area.");
|
||||||
addSystemChatMessage("You cannot send chat messages in this area.");
|
addSystemChatMessage("You cannot send chat messages in this area.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -2049,6 +2052,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint8_t reason = packet.readUInt8();
|
uint8_t reason = packet.readUInt8();
|
||||||
const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason";
|
const char* msg = (reason < 8) ? reasons[reason] : "Unknown reason";
|
||||||
std::string s = std::string("Failed to tame: ") + msg;
|
std::string s = std::string("Failed to tame: ") + msg;
|
||||||
|
addUIError(s);
|
||||||
addSystemChatMessage(s);
|
addSystemChatMessage(s);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -2168,6 +2172,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
std::dec, " rank=", rank);
|
std::dec, " rank=", rank);
|
||||||
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
|
std::string msg = "You gain " + std::to_string(honor) + " honor points.";
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
|
if (honor > 0)
|
||||||
|
addCombatText(CombatTextEntry::HONOR_GAIN, static_cast<int32_t>(honor), 0, true);
|
||||||
if (pvpHonorCallback_) {
|
if (pvpHonorCallback_) {
|
||||||
pvpHonorCallback_(honor, victimGuid, rank);
|
pvpHonorCallback_(honor, victimGuid, rank);
|
||||||
}
|
}
|
||||||
|
|
@ -2369,7 +2375,8 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case 0x06: msg = "Pet retrieved from stable."; break;
|
case 0x06: msg = "Pet retrieved from stable."; break;
|
||||||
case 0x07: msg = "Stable slot purchased."; break;
|
case 0x07: msg = "Stable slot purchased."; break;
|
||||||
case 0x08: msg = "Stable list updated."; break;
|
case 0x08: msg = "Stable list updated."; break;
|
||||||
case 0x09: msg = "Stable failed: not enough money or other error."; break;
|
case 0x09: msg = "Stable failed: not enough money or other error.";
|
||||||
|
addUIError(msg); break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
if (msg) addSystemChatMessage(msg);
|
if (msg) addSystemChatMessage(msg);
|
||||||
|
|
@ -2525,8 +2532,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
"Character name does not meet requirements.", // 7
|
"Character name does not meet requirements.", // 7
|
||||||
};
|
};
|
||||||
const char* errMsg = (result < 8) ? kRenameErrors[result] : nullptr;
|
const char* errMsg = (result < 8) ? kRenameErrors[result] : nullptr;
|
||||||
addSystemChatMessage(errMsg ? std::string("Rename failed: ") + errMsg
|
std::string renameErr = errMsg ? std::string("Rename failed: ") + errMsg
|
||||||
: "Character rename failed.");
|
: "Character rename failed.";
|
||||||
|
addUIError(renameErr);
|
||||||
|
addSystemChatMessage(renameErr);
|
||||||
}
|
}
|
||||||
LOG_INFO("SMSG_CHAR_RENAME: result=", result, " newName=", newName);
|
LOG_INFO("SMSG_CHAR_RENAME: result=", result, " newName=", newName);
|
||||||
}
|
}
|
||||||
|
|
@ -2539,6 +2548,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
addSystemChatMessage("Your home is now set to this location.");
|
addSystemChatMessage("Your home is now set to this location.");
|
||||||
} else {
|
} else {
|
||||||
|
addUIError("You are too far from the innkeeper.");
|
||||||
addSystemChatMessage("You are too far from the innkeeper.");
|
addSystemChatMessage("You are too far from the innkeeper.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2557,12 +2567,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
"You must be in a raid group", "Player not in group"
|
"You must be in a raid group", "Player not in group"
|
||||||
};
|
};
|
||||||
const char* msg = (result < 8) ? reasons[result] : "Difficulty change failed.";
|
const char* msg = (result < 8) ? reasons[result] : "Difficulty change failed.";
|
||||||
|
addUIError(std::string("Cannot change difficulty: ") + msg);
|
||||||
addSystemChatMessage(std::string("Cannot change difficulty: ") + msg);
|
addSystemChatMessage(std::string("Cannot change difficulty: ") + msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_CORPSE_NOT_IN_INSTANCE:
|
case Opcode::SMSG_CORPSE_NOT_IN_INSTANCE:
|
||||||
|
addUIError("Your corpse is outside this instance.");
|
||||||
addSystemChatMessage("Your corpse is outside this instance. Release spirit to retrieve it.");
|
addSystemChatMessage("Your corpse is outside this instance. Release spirit to retrieve it.");
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_CROSSED_INEBRIATION_THRESHOLD: {
|
case Opcode::SMSG_CROSSED_INEBRIATION_THRESHOLD: {
|
||||||
|
|
@ -2815,7 +2827,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint32_t result = packet.readUInt32();
|
uint32_t result = packet.readUInt32();
|
||||||
if (result != 4) {
|
if (result != 4) {
|
||||||
const char* msgs[] = { "Cannot mount here.", "Invalid mount spell.", "Too far away to mount.", "Already mounted." };
|
const char* msgs[] = { "Cannot mount here.", "Invalid mount spell.", "Too far away to mount.", "Already mounted." };
|
||||||
addSystemChatMessage(result < 4 ? msgs[result] : "Cannot mount.");
|
std::string mountErr = result < 4 ? msgs[result] : "Cannot mount.";
|
||||||
|
addUIError(mountErr);
|
||||||
|
addSystemChatMessage(mountErr);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -2823,7 +2837,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// uint32 result: 0=ok, others=error
|
// uint32 result: 0=ok, others=error
|
||||||
if (packet.getSize() - packet.getReadPos() < 4) break;
|
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||||
uint32_t result = packet.readUInt32();
|
uint32_t result = packet.readUInt32();
|
||||||
if (result != 0) addSystemChatMessage("Cannot dismount here.");
|
if (result != 0) { addUIError("Cannot dismount here."); addSystemChatMessage("Cannot dismount here."); }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3251,10 +3265,24 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleSpellDamageLog(packet);
|
handleSpellDamageLog(packet);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_PLAY_SPELL_VISUAL: {
|
case Opcode::SMSG_PLAY_SPELL_VISUAL: {
|
||||||
// Minimal parse: uint64 casterGuid, uint32 visualId
|
// uint64 casterGuid + uint32 visualId
|
||||||
if (packet.getSize() - packet.getReadPos() < 12) break;
|
if (packet.getSize() - packet.getReadPos() < 12) break;
|
||||||
packet.readUInt64();
|
uint64_t casterGuid = packet.readUInt64();
|
||||||
packet.readUInt32();
|
uint32_t visualId = packet.readUInt32();
|
||||||
|
if (visualId == 0) break;
|
||||||
|
// Resolve caster world position and spawn the effect
|
||||||
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
|
if (!renderer) break;
|
||||||
|
glm::vec3 spawnPos;
|
||||||
|
if (casterGuid == playerGuid) {
|
||||||
|
spawnPos = renderer->getCharacterPosition();
|
||||||
|
} else {
|
||||||
|
auto entity = entityManager.getEntity(casterGuid);
|
||||||
|
if (!entity) break;
|
||||||
|
glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||||
|
spawnPos = core::coords::canonicalToRender(canonical);
|
||||||
|
}
|
||||||
|
renderer->playSpellVisual(visualId, spawnPos);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_SPELLHEALLOG:
|
case Opcode::SMSG_SPELLHEALLOG:
|
||||||
|
|
@ -3455,6 +3483,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
std::string msg = areaName.empty()
|
std::string msg = areaName.empty()
|
||||||
? std::string("A zone is under attack!")
|
? std::string("A zone is under attack!")
|
||||||
: (areaName + " is under attack!");
|
: (areaName + " is under attack!");
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -3549,6 +3578,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
char buf[80];
|
char buf[80];
|
||||||
std::snprintf(buf, sizeof(buf),
|
std::snprintf(buf, sizeof(buf),
|
||||||
"You have lost %u%% of your gear's durability due to death.", pct);
|
"You have lost %u%% of your gear's durability due to death.", pct);
|
||||||
|
addUIError(buf);
|
||||||
addSystemChatMessage(buf);
|
addSystemChatMessage(buf);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -3586,6 +3616,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
partyData.members.clear();
|
partyData.members.clear();
|
||||||
partyData.memberCount = 0;
|
partyData.memberCount = 0;
|
||||||
partyData.leaderGuid = 0;
|
partyData.leaderGuid = 0;
|
||||||
|
addUIError("Your party has been disbanded.");
|
||||||
addSystemChatMessage("Your party has been disbanded.");
|
addSystemChatMessage("Your party has been disbanded.");
|
||||||
LOG_INFO("SMSG_GROUP_DESTROYED: party cleared");
|
LOG_INFO("SMSG_GROUP_DESTROYED: party cleared");
|
||||||
break;
|
break;
|
||||||
|
|
@ -3955,6 +3986,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
else if (errorCode == 2) msg += " (already known)";
|
else if (errorCode == 2) msg += " (already known)";
|
||||||
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
|
else if (errorCode != 0) msg += " (error " + std::to_string(errorCode) + ")";
|
||||||
|
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
// Play error sound so the player notices the failure
|
// Play error sound so the player notices the failure
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
|
|
@ -4473,6 +4505,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
/*uint32_t len =*/ packet.readUInt32();
|
/*uint32_t len =*/ packet.readUInt32();
|
||||||
std::string msg = packet.readString();
|
std::string msg = packet.readString();
|
||||||
if (!msg.empty()) {
|
if (!msg.empty()) {
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
areaTriggerMsgs_.push_back(msg);
|
areaTriggerMsgs_.push_back(msg);
|
||||||
}
|
}
|
||||||
|
|
@ -4578,6 +4611,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
"Unknown error", "Only empty bag"
|
"Unknown error", "Only empty bag"
|
||||||
};
|
};
|
||||||
const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error";
|
const char* msg = (result < 7) ? sellErrors[result] : "Unknown sell error";
|
||||||
|
addUIError(std::string("Sell failed: ") + msg);
|
||||||
addSystemChatMessage(std::string("Sell failed: ") + msg);
|
addSystemChatMessage(std::string("Sell failed: ") + msg);
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
if (auto* sfx = renderer->getUiSoundManager())
|
if (auto* sfx = renderer->getUiSoundManager())
|
||||||
|
|
@ -4611,8 +4645,10 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
if (requiredLevel > 0) {
|
if (requiredLevel > 0) {
|
||||||
std::snprintf(levelBuf, sizeof(levelBuf),
|
std::snprintf(levelBuf, sizeof(levelBuf),
|
||||||
"You must reach level %u to use that item.", requiredLevel);
|
"You must reach level %u to use that item.", requiredLevel);
|
||||||
|
addUIError(levelBuf);
|
||||||
addSystemChatMessage(levelBuf);
|
addSystemChatMessage(levelBuf);
|
||||||
} else {
|
} else {
|
||||||
|
addUIError("You must reach a higher level to use that item.");
|
||||||
addSystemChatMessage("You must reach a higher level to use that item.");
|
addSystemChatMessage("You must reach a higher level to use that item.");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -4675,6 +4711,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ").";
|
std::string msg = errMsg ? errMsg : "Inventory error (" + std::to_string(error) + ").";
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
if (auto* sfx = renderer->getUiSoundManager())
|
if (auto* sfx = renderer->getUiSoundManager())
|
||||||
|
|
@ -4739,6 +4776,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case 6: msg = "You can't carry any more items."; break;
|
case 6: msg = "You can't carry any more items."; break;
|
||||||
default: break;
|
default: break;
|
||||||
}
|
}
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
if (auto* sfx = renderer->getUiSoundManager())
|
if (auto* sfx = renderer->getUiSoundManager())
|
||||||
|
|
@ -4827,6 +4865,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
: (result == 2) ? "You are not at a barber shop."
|
: (result == 2) ? "You are not at a barber shop."
|
||||||
: (result == 3) ? "You must stand up to use the barber shop."
|
: (result == 3) ? "You must stand up to use the barber shop."
|
||||||
: "Barber shop unavailable.";
|
: "Barber shop unavailable.";
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
LOG_DEBUG("SMSG_BARBER_SHOP_RESULT: result=", result);
|
LOG_DEBUG("SMSG_BARBER_SHOP_RESULT: result=", result);
|
||||||
|
|
@ -4911,6 +4950,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
addSystemChatMessage("Gems socketed successfully.");
|
addSystemChatMessage("Gems socketed successfully.");
|
||||||
} else {
|
} else {
|
||||||
|
addUIError("Failed to socket gems.");
|
||||||
addSystemChatMessage("Failed to socket gems.");
|
addSystemChatMessage("Failed to socket gems.");
|
||||||
}
|
}
|
||||||
LOG_DEBUG("SMSG_SOCKET_GEMS_RESULT: result=", result);
|
LOG_DEBUG("SMSG_SOCKET_GEMS_RESULT: result=", result);
|
||||||
|
|
@ -4947,6 +4987,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
const char* msg = (reason == 1) ? "The target cannot be resurrected right now."
|
const char* msg = (reason == 1) ? "The target cannot be resurrected right now."
|
||||||
: (reason == 2) ? "Cannot resurrect in this area."
|
: (reason == 2) ? "Cannot resurrect in this area."
|
||||||
: "Resurrection failed.";
|
: "Resurrection failed.";
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
LOG_DEBUG("SMSG_RESURRECT_FAILED: reason=", reason);
|
LOG_DEBUG("SMSG_RESURRECT_FAILED: reason=", reason);
|
||||||
}
|
}
|
||||||
|
|
@ -4974,6 +5015,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
auto go = std::static_pointer_cast<GameObject>(goEnt);
|
auto go = std::static_pointer_cast<GameObject>(goEnt);
|
||||||
auto* info = getCachedGameObjectInfo(go->getEntry());
|
auto* info = getCachedGameObjectInfo(go->getEntry());
|
||||||
if (info && info->type == 17) { // GO_TYPE_FISHINGNODE
|
if (info && info->type == 17) { // GO_TYPE_FISHINGNODE
|
||||||
|
addUIError("A fish is on your line!");
|
||||||
addSystemChatMessage("A fish is on your line!");
|
addSystemChatMessage("A fish is on your line!");
|
||||||
// Play a distinctive UI sound to alert the player
|
// Play a distinctive UI sound to alert the player
|
||||||
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
||||||
|
|
@ -5438,6 +5480,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_QUESTLOG_FULL:
|
case Opcode::SMSG_QUESTLOG_FULL:
|
||||||
// Zero-payload notification: the player's quest log is full (25 quests).
|
// Zero-payload notification: the player's quest log is full (25 quests).
|
||||||
|
addUIError("Your quest log is full.");
|
||||||
addSystemChatMessage("Your quest log is full.");
|
addSystemChatMessage("Your quest log is full.");
|
||||||
LOG_INFO("SMSG_QUESTLOG_FULL: quest log is at capacity");
|
LOG_INFO("SMSG_QUESTLOG_FULL: quest log is at capacity");
|
||||||
break;
|
break;
|
||||||
|
|
@ -5503,6 +5546,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case 0x0C: abortMsg = "Transfer aborted."; break;
|
case 0x0C: abortMsg = "Transfer aborted."; break;
|
||||||
default: abortMsg = "Transfer aborted."; break;
|
default: abortMsg = "Transfer aborted."; break;
|
||||||
}
|
}
|
||||||
|
addUIError(abortMsg);
|
||||||
addSystemChatMessage(abortMsg);
|
addSystemChatMessage(abortMsg);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -5540,12 +5584,25 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
handleBattlefieldList(packet);
|
handleBattlefieldList(packet);
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_BATTLEFIELD_PORT_DENIED:
|
case Opcode::SMSG_BATTLEFIELD_PORT_DENIED:
|
||||||
|
addUIError("Battlefield port denied.");
|
||||||
addSystemChatMessage("Battlefield port denied.");
|
addSystemChatMessage("Battlefield port denied.");
|
||||||
break;
|
break;
|
||||||
case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS:
|
case Opcode::MSG_BATTLEGROUND_PLAYER_POSITIONS: {
|
||||||
// Optional map position updates for BG objectives/players.
|
bgPlayerPositions_.clear();
|
||||||
packet.setReadPos(packet.getSize());
|
for (int grp = 0; grp < 2; ++grp) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 4) break;
|
||||||
|
uint32_t count = packet.readUInt32();
|
||||||
|
for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= 16; ++i) {
|
||||||
|
BgPlayerPosition pos;
|
||||||
|
pos.guid = packet.readUInt64();
|
||||||
|
pos.wowX = packet.readFloat();
|
||||||
|
pos.wowY = packet.readFloat();
|
||||||
|
pos.group = grp;
|
||||||
|
bgPlayerPositions_.push_back(pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case Opcode::SMSG_REMOVED_FROM_PVP_QUEUE:
|
case Opcode::SMSG_REMOVED_FROM_PVP_QUEUE:
|
||||||
addSystemChatMessage("You have been removed from the PvP queue.");
|
addSystemChatMessage("You have been removed from the PvP queue.");
|
||||||
break;
|
break;
|
||||||
|
|
@ -5635,6 +5692,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
const char* reasonMsg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason.";
|
const char* reasonMsg = (reason < 5) ? resetFailReasons[reason] : "Unknown reason.";
|
||||||
std::string mapLabel = getMapName(mapId);
|
std::string mapLabel = getMapName(mapId);
|
||||||
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
|
if (mapLabel.empty()) mapLabel = "instance #" + std::to_string(mapId);
|
||||||
|
addUIError("Cannot reset " + mapLabel + ": " + reasonMsg);
|
||||||
addSystemChatMessage("Cannot reset " + mapLabel + ": " + reasonMsg);
|
addSystemChatMessage("Cannot reset " + mapLabel + ": " + reasonMsg);
|
||||||
LOG_INFO("SMSG_INSTANCE_RESET_FAILED: mapId=", mapId, " reason=", reason);
|
LOG_INFO("SMSG_INSTANCE_RESET_FAILED: mapId=", mapId, " reason=", reason);
|
||||||
}
|
}
|
||||||
|
|
@ -6089,6 +6147,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
// Clear cached talent data so the talent screen reflects the reset.
|
// Clear cached talent data so the talent screen reflects the reset.
|
||||||
learnedTalents_[0].clear();
|
learnedTalents_[0].clear();
|
||||||
learnedTalents_[1].clear();
|
learnedTalents_[1].clear();
|
||||||
|
addUIError("Your talents have been reset by the server.");
|
||||||
addSystemChatMessage("Your talents have been reset by the server.");
|
addSystemChatMessage("Your talents have been reset by the server.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -6183,7 +6242,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_EQUIPMENT_SET_USE_RESULT: {
|
case Opcode::SMSG_EQUIPMENT_SET_USE_RESULT: {
|
||||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||||
uint8_t result = packet.readUInt8();
|
uint8_t result = packet.readUInt8();
|
||||||
if (result != 0) addSystemChatMessage("Failed to equip item set.");
|
if (result != 0) { addUIError("Failed to equip item set."); addSystemChatMessage("Failed to equip item set."); }
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -6218,12 +6277,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
uint32_t result = packet.readUInt32();
|
uint32_t result = packet.readUInt32();
|
||||||
(void)result;
|
(void)result;
|
||||||
}
|
}
|
||||||
|
addUIError("Dungeon Finder: Auto-join failed.");
|
||||||
addSystemChatMessage("Dungeon Finder: Auto-join failed.");
|
addSystemChatMessage("Dungeon Finder: Auto-join failed.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER:
|
case Opcode::SMSG_LFG_AUTOJOIN_FAILED_NO_PLAYER:
|
||||||
// No eligible players found for auto-join
|
// No eligible players found for auto-join
|
||||||
|
addUIError("Dungeon Finder: No players available for auto-join.");
|
||||||
addSystemChatMessage("Dungeon Finder: No players available for auto-join.");
|
addSystemChatMessage("Dungeon Finder: No players available for auto-join.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -6737,6 +6798,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim);
|
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim);
|
||||||
} else if (ikVictim == playerGuid) {
|
} else if (ikVictim == playerGuid) {
|
||||||
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, false, 0, ikCaster, ikVictim);
|
addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, false, 0, ikCaster, ikVictim);
|
||||||
|
addUIError("You were killed by an instant-kill effect.");
|
||||||
addSystemChatMessage("You were killed by an instant-kill effect.");
|
addSystemChatMessage("You were killed by an instant-kill effect.");
|
||||||
}
|
}
|
||||||
LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster,
|
LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster,
|
||||||
|
|
@ -7066,10 +7128,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
castTimeRemaining = castTimeTotal;
|
castTimeRemaining = castTimeTotal;
|
||||||
} else {
|
} else {
|
||||||
auto& s = unitCastStates_[chanCaster];
|
auto& s = unitCastStates_[chanCaster];
|
||||||
s.casting = true;
|
s.casting = true;
|
||||||
s.spellId = chanSpellId;
|
s.spellId = chanSpellId;
|
||||||
s.timeTotal = chanTotalMs / 1000.0f;
|
s.timeTotal = chanTotalMs / 1000.0f;
|
||||||
s.timeRemaining = s.timeTotal;
|
s.timeRemaining = s.timeTotal;
|
||||||
|
s.interruptible = isSpellInterruptible(chanSpellId);
|
||||||
}
|
}
|
||||||
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
|
LOG_DEBUG("MSG_CHANNEL_START: caster=0x", std::hex, chanCaster, std::dec,
|
||||||
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
" spell=", chanSpellId, " total=", chanTotalMs, "ms");
|
||||||
|
|
@ -7256,16 +7319,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_PLAYERBINDERROR: {
|
case Opcode::SMSG_PLAYERBINDERROR: {
|
||||||
if (packet.getSize() - packet.getReadPos() >= 4) {
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||||
uint32_t error = packet.readUInt32();
|
uint32_t error = packet.readUInt32();
|
||||||
if (error == 0)
|
if (error == 0) {
|
||||||
|
addUIError("Your hearthstone is not bound.");
|
||||||
addSystemChatMessage("Your hearthstone is not bound.");
|
addSystemChatMessage("Your hearthstone is not bound.");
|
||||||
else
|
} else {
|
||||||
|
addUIError("Hearthstone bind failed.");
|
||||||
addSystemChatMessage("Hearthstone bind failed.");
|
addSystemChatMessage("Hearthstone bind failed.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Instance/raid errors ----
|
// ---- Instance/raid errors ----
|
||||||
case Opcode::SMSG_RAID_GROUP_ONLY: {
|
case Opcode::SMSG_RAID_GROUP_ONLY: {
|
||||||
|
addUIError("You must be in a raid group to enter this instance.");
|
||||||
addSystemChatMessage("You must be in a raid group to enter this instance.");
|
addSystemChatMessage("You must be in a raid group to enter this instance.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -7273,13 +7340,14 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_RAID_READY_CHECK_ERROR: {
|
case Opcode::SMSG_RAID_READY_CHECK_ERROR: {
|
||||||
if (packet.getSize() - packet.getReadPos() >= 1) {
|
if (packet.getSize() - packet.getReadPos() >= 1) {
|
||||||
uint8_t err = packet.readUInt8();
|
uint8_t err = packet.readUInt8();
|
||||||
if (err == 0) addSystemChatMessage("Ready check failed: not in a group.");
|
if (err == 0) { addUIError("Ready check failed: not in a group."); addSystemChatMessage("Ready check failed: not in a group."); }
|
||||||
else if (err == 1) addSystemChatMessage("Ready check failed: in instance.");
|
else if (err == 1) { addUIError("Ready check failed: in instance."); addSystemChatMessage("Ready check failed: in instance."); }
|
||||||
else addSystemChatMessage("Ready check failed.");
|
else { addUIError("Ready check failed."); addSystemChatMessage("Ready check failed."); }
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Opcode::SMSG_RESET_FAILED_NOTIFY: {
|
case Opcode::SMSG_RESET_FAILED_NOTIFY: {
|
||||||
|
addUIError("Cannot reset instance: another player is still inside.");
|
||||||
addSystemChatMessage("Cannot reset instance: another player is still inside.");
|
addSystemChatMessage("Cannot reset instance: another player is still inside.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -7344,12 +7412,11 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// ---- Play object/spell sounds ----
|
// ---- Play object/spell sounds ----
|
||||||
case Opcode::SMSG_PLAY_OBJECT_SOUND:
|
case Opcode::SMSG_PLAY_OBJECT_SOUND:
|
||||||
case Opcode::SMSG_PLAY_SPELL_IMPACT:
|
|
||||||
if (packet.getSize() - packet.getReadPos() >= 12) {
|
if (packet.getSize() - packet.getReadPos() >= 12) {
|
||||||
// uint32 soundId + uint64 sourceGuid
|
// uint32 soundId + uint64 sourceGuid
|
||||||
uint32_t soundId = packet.readUInt32();
|
uint32_t soundId = packet.readUInt32();
|
||||||
uint64_t srcGuid = packet.readUInt64();
|
uint64_t srcGuid = packet.readUInt64();
|
||||||
LOG_DEBUG("SMSG_PLAY_OBJECT_SOUND/SPELL_IMPACT id=", soundId, " src=0x", std::hex, srcGuid, std::dec);
|
LOG_DEBUG("SMSG_PLAY_OBJECT_SOUND: id=", soundId, " src=0x", std::hex, srcGuid, std::dec);
|
||||||
if (playPositionalSoundCallback_) playPositionalSoundCallback_(soundId, srcGuid);
|
if (playPositionalSoundCallback_) playPositionalSoundCallback_(soundId, srcGuid);
|
||||||
else if (playSoundCallback_) playSoundCallback_(soundId);
|
else if (playSoundCallback_) playSoundCallback_(soundId);
|
||||||
} else if (packet.getSize() - packet.getReadPos() >= 4) {
|
} else if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||||
|
|
@ -7358,6 +7425,28 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_PLAY_SPELL_IMPACT: {
|
||||||
|
// uint64 targetGuid + uint32 visualId (same structure as SMSG_PLAY_SPELL_VISUAL)
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 12) {
|
||||||
|
packet.setReadPos(packet.getSize()); break;
|
||||||
|
}
|
||||||
|
uint64_t impTargetGuid = packet.readUInt64();
|
||||||
|
uint32_t impVisualId = packet.readUInt32();
|
||||||
|
if (impVisualId == 0) break;
|
||||||
|
auto* renderer = core::Application::getInstance().getRenderer();
|
||||||
|
if (!renderer) break;
|
||||||
|
glm::vec3 spawnPos;
|
||||||
|
if (impTargetGuid == playerGuid) {
|
||||||
|
spawnPos = renderer->getCharacterPosition();
|
||||||
|
} else {
|
||||||
|
auto entity = entityManager.getEntity(impTargetGuid);
|
||||||
|
if (!entity) break;
|
||||||
|
glm::vec3 canonical(entity->getLatestX(), entity->getLatestY(), entity->getLatestZ());
|
||||||
|
spawnPos = core::coords::canonicalToRender(canonical);
|
||||||
|
}
|
||||||
|
renderer->playSpellVisual(impVisualId, spawnPos, /*useImpactKit=*/true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Resistance/combat log ----
|
// ---- Resistance/combat log ----
|
||||||
case Opcode::SMSG_RESISTLOG: {
|
case Opcode::SMSG_RESISTLOG: {
|
||||||
|
|
@ -7407,6 +7496,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
case Opcode::SMSG_READ_ITEM_FAILED:
|
case Opcode::SMSG_READ_ITEM_FAILED:
|
||||||
|
addUIError("You cannot read this item.");
|
||||||
addSystemChatMessage("You cannot read this item.");
|
addSystemChatMessage("You cannot read this item.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -7474,6 +7564,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
|
|
||||||
// ---- NPC not responding ----
|
// ---- NPC not responding ----
|
||||||
case Opcode::SMSG_NPC_WONT_TALK:
|
case Opcode::SMSG_NPC_WONT_TALK:
|
||||||
|
addUIError("That creature can't talk to you right now.");
|
||||||
addSystemChatMessage("That creature can't talk to you right now.");
|
addSystemChatMessage("That creature can't talk to you right now.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -7569,12 +7660,26 @@ void GameHandler::handlePacket(network::Packet& packet) {
|
||||||
case Opcode::SMSG_PET_GUIDS:
|
case Opcode::SMSG_PET_GUIDS:
|
||||||
case Opcode::SMSG_PET_DISMISS_SOUND:
|
case Opcode::SMSG_PET_DISMISS_SOUND:
|
||||||
case Opcode::SMSG_PET_ACTION_SOUND:
|
case Opcode::SMSG_PET_ACTION_SOUND:
|
||||||
case Opcode::SMSG_PET_UNLEARN_CONFIRM:
|
case Opcode::SMSG_PET_UNLEARN_CONFIRM: {
|
||||||
case Opcode::SMSG_PET_RENAMEABLE:
|
// uint64 petGuid + uint32 cost (copper)
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 12) {
|
||||||
|
petUnlearnGuid_ = packet.readUInt64();
|
||||||
|
petUnlearnCost_ = packet.readUInt32();
|
||||||
|
petUnlearnPending_ = true;
|
||||||
|
}
|
||||||
|
packet.setReadPos(packet.getSize());
|
||||||
|
break;
|
||||||
|
}
|
||||||
case Opcode::SMSG_PET_UPDATE_COMBO_POINTS:
|
case Opcode::SMSG_PET_UPDATE_COMBO_POINTS:
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
case Opcode::SMSG_PET_RENAMEABLE:
|
||||||
|
// Server signals that the pet can now be named (first tame)
|
||||||
|
petRenameablePending_ = true;
|
||||||
|
packet.setReadPos(packet.getSize());
|
||||||
|
break;
|
||||||
case Opcode::SMSG_PET_NAME_INVALID:
|
case Opcode::SMSG_PET_NAME_INVALID:
|
||||||
|
addUIError("That pet name is invalid. Please choose a different name.");
|
||||||
addSystemChatMessage("That pet name is invalid. Please choose a different name.");
|
addSystemChatMessage("That pet name is invalid. Please choose a different name.");
|
||||||
packet.setReadPos(packet.getSize());
|
packet.setReadPos(packet.getSize());
|
||||||
break;
|
break;
|
||||||
|
|
@ -16179,6 +16284,7 @@ void GameHandler::handleLfgJoinResult(network::Packet& packet) {
|
||||||
} else {
|
} else {
|
||||||
const char* msg = lfgJoinResultString(result);
|
const char* msg = lfgJoinResultString(result);
|
||||||
std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed.");
|
std::string errMsg = std::string("Dungeon Finder: ") + (msg ? msg : "Join failed.");
|
||||||
|
addUIError(errMsg);
|
||||||
addSystemChatMessage(errMsg);
|
addSystemChatMessage(errMsg);
|
||||||
LOG_INFO("SMSG_LFG_JOIN_RESULT: result=", static_cast<int>(result),
|
LOG_INFO("SMSG_LFG_JOIN_RESULT: result=", static_cast<int>(result),
|
||||||
" state=", static_cast<int>(state));
|
" state=", static_cast<int>(state));
|
||||||
|
|
@ -16224,6 +16330,7 @@ void GameHandler::handleLfgProposalUpdate(network::Packet& packet) {
|
||||||
case 0:
|
case 0:
|
||||||
lfgState_ = LfgState::Queued;
|
lfgState_ = LfgState::Queued;
|
||||||
lfgProposalId_ = 0;
|
lfgProposalId_ = 0;
|
||||||
|
addUIError("Dungeon Finder: Group proposal failed.");
|
||||||
addSystemChatMessage("Dungeon Finder: Group proposal failed.");
|
addSystemChatMessage("Dungeon Finder: Group proposal failed.");
|
||||||
break;
|
break;
|
||||||
case 1: {
|
case 1: {
|
||||||
|
|
@ -16267,6 +16374,7 @@ void GameHandler::handleLfgRoleCheckUpdate(network::Packet& packet) {
|
||||||
LOG_INFO("LFG role check finished");
|
LOG_INFO("LFG role check finished");
|
||||||
} else if (roleCheckState == 3) {
|
} else if (roleCheckState == 3) {
|
||||||
lfgState_ = LfgState::None;
|
lfgState_ = LfgState::None;
|
||||||
|
addUIError("Dungeon Finder: Role check failed — missing required role.");
|
||||||
addSystemChatMessage("Dungeon Finder: Role check failed — missing required role.");
|
addSystemChatMessage("Dungeon Finder: Role check failed — missing required role.");
|
||||||
} else if (roleCheckState == 2) {
|
} else if (roleCheckState == 2) {
|
||||||
lfgState_ = LfgState::RoleCheck;
|
lfgState_ = LfgState::RoleCheck;
|
||||||
|
|
@ -17555,6 +17663,12 @@ void GameHandler::handleMonsterMoveTransport(network::Packet& packet) {
|
||||||
|
|
||||||
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
if (packet.getReadPos() + 4 > packet.getSize()) return;
|
||||||
uint32_t pointCount = packet.readUInt32();
|
uint32_t pointCount = packet.readUInt32();
|
||||||
|
constexpr uint32_t kMaxTransportSplinePoints = 1000;
|
||||||
|
if (pointCount > kMaxTransportSplinePoints) {
|
||||||
|
LOG_WARNING("SMSG_MONSTER_MOVE_TRANSPORT: pointCount=", pointCount,
|
||||||
|
" clamped to ", kMaxTransportSplinePoints);
|
||||||
|
pointCount = kMaxTransportSplinePoints;
|
||||||
|
}
|
||||||
|
|
||||||
// Read destination point (transport-local server coords)
|
// Read destination point (transport-local server coords)
|
||||||
float destLocalX = localX, destLocalY = localY, destLocalZ = localZ;
|
float destLocalX = localX, destLocalY = localY, destLocalZ = localZ;
|
||||||
|
|
@ -17690,7 +17804,15 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) {
|
||||||
// VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect).
|
// VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect).
|
||||||
addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
||||||
} else {
|
} else {
|
||||||
auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE;
|
CombatTextEntry::Type type;
|
||||||
|
if (data.isCrit())
|
||||||
|
type = CombatTextEntry::CRIT_DAMAGE;
|
||||||
|
else if (data.isCrushing())
|
||||||
|
type = CombatTextEntry::CRUSHING;
|
||||||
|
else if (data.isGlancing())
|
||||||
|
type = CombatTextEntry::GLANCING;
|
||||||
|
else
|
||||||
|
type = CombatTextEntry::MELEE_DAMAGE;
|
||||||
addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid);
|
||||||
// Show partial absorb/resist from sub-damage entries
|
// Show partial absorb/resist from sub-damage entries
|
||||||
uint32_t totalAbsorbed = 0, totalResisted = 0;
|
uint32_t totalAbsorbed = 0, totalResisted = 0;
|
||||||
|
|
@ -18155,21 +18277,20 @@ void GameHandler::handleCastFailed(network::Packet& packet) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add system message about failed cast with readable reason
|
// Show failure reason in the UIError overlay and in chat
|
||||||
int powerType = -1;
|
int powerType = -1;
|
||||||
auto playerEntity = entityManager.getEntity(playerGuid);
|
auto playerEntity = entityManager.getEntity(playerGuid);
|
||||||
if (auto playerUnit = std::dynamic_pointer_cast<Unit>(playerEntity)) {
|
if (auto playerUnit = std::dynamic_pointer_cast<Unit>(playerEntity)) {
|
||||||
powerType = playerUnit->getPowerType();
|
powerType = playerUnit->getPowerType();
|
||||||
}
|
}
|
||||||
const char* reason = getSpellCastResultString(data.result, powerType);
|
const char* reason = getSpellCastResultString(data.result, powerType);
|
||||||
|
std::string errMsg = reason ? reason
|
||||||
|
: ("Spell cast failed (error " + std::to_string(data.result) + ")");
|
||||||
|
addUIError(errMsg);
|
||||||
MessageChatData msg;
|
MessageChatData msg;
|
||||||
msg.type = ChatType::SYSTEM;
|
msg.type = ChatType::SYSTEM;
|
||||||
msg.language = ChatLanguage::UNIVERSAL;
|
msg.language = ChatLanguage::UNIVERSAL;
|
||||||
if (reason) {
|
msg.message = errMsg;
|
||||||
msg.message = reason;
|
|
||||||
} else {
|
|
||||||
msg.message = "Spell cast failed (error " + std::to_string(data.result) + ")";
|
|
||||||
}
|
|
||||||
addLocalChatMessage(msg);
|
addLocalChatMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18190,10 +18311,11 @@ void GameHandler::handleSpellStart(network::Packet& packet) {
|
||||||
// Track cast bar for any non-player caster (target frame + boss frames)
|
// Track cast bar for any non-player caster (target frame + boss frames)
|
||||||
if (data.casterUnit != playerGuid && data.castTime > 0) {
|
if (data.casterUnit != playerGuid && data.castTime > 0) {
|
||||||
auto& s = unitCastStates_[data.casterUnit];
|
auto& s = unitCastStates_[data.casterUnit];
|
||||||
s.casting = true;
|
s.casting = true;
|
||||||
s.spellId = data.spellId;
|
s.spellId = data.spellId;
|
||||||
s.timeTotal = data.castTime / 1000.0f;
|
s.timeTotal = data.castTime / 1000.0f;
|
||||||
s.timeRemaining = s.timeTotal;
|
s.timeRemaining = s.timeTotal;
|
||||||
|
s.interruptible = isSpellInterruptible(data.spellId);
|
||||||
// Trigger cast animation on the casting unit
|
// Trigger cast animation on the casting unit
|
||||||
if (spellCastAnimCallback_) {
|
if (spellCastAnimCallback_) {
|
||||||
spellCastAnimCallback_(data.casterUnit, true, false);
|
spellCastAnimCallback_(data.casterUnit, true, false);
|
||||||
|
|
@ -18760,6 +18882,20 @@ void GameHandler::switchTalentSpec(uint8_t newSpec) {
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::confirmPetUnlearn() {
|
||||||
|
if (!petUnlearnPending_) return;
|
||||||
|
petUnlearnPending_ = false;
|
||||||
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
|
||||||
|
// Respond with CMSG_PET_UNLEARN_TALENTS (no payload in 3.3.5a)
|
||||||
|
network::Packet pkt(wireOpcode(Opcode::CMSG_PET_UNLEARN_TALENTS));
|
||||||
|
socket->send(pkt);
|
||||||
|
LOG_INFO("confirmPetUnlearn: sent CMSG_PET_UNLEARN_TALENTS");
|
||||||
|
addSystemChatMessage("Pet talent reset confirmed.");
|
||||||
|
petUnlearnGuid_ = 0;
|
||||||
|
petUnlearnCost_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::confirmTalentWipe() {
|
void GameHandler::confirmTalentWipe() {
|
||||||
if (!talentWipePending_) return;
|
if (!talentWipePending_) return;
|
||||||
talentWipePending_ = false;
|
talentWipePending_ = false;
|
||||||
|
|
@ -18873,6 +19009,7 @@ void GameHandler::handleGroupUninvite(network::Packet& packet) {
|
||||||
msg.type = ChatType::SYSTEM;
|
msg.type = ChatType::SYSTEM;
|
||||||
msg.language = ChatLanguage::UNIVERSAL;
|
msg.language = ChatLanguage::UNIVERSAL;
|
||||||
msg.message = "You have been removed from the group.";
|
msg.message = "You have been removed from the group.";
|
||||||
|
addUIError("You have been removed from the group.");
|
||||||
addLocalChatMessage(msg);
|
addLocalChatMessage(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -18907,6 +19044,7 @@ void GameHandler::handlePartyCommandResult(network::Packet& packet) {
|
||||||
static_cast<uint32_t>(data.result));
|
static_cast<uint32_t>(data.result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addUIError(buf);
|
||||||
MessageChatData msg;
|
MessageChatData msg;
|
||||||
msg.type = ChatType::SYSTEM;
|
msg.type = ChatType::SYSTEM;
|
||||||
msg.language = ChatLanguage::UNIVERSAL;
|
msg.language = ChatLanguage::UNIVERSAL;
|
||||||
|
|
@ -20420,6 +20558,34 @@ void GameHandler::closeGossip() {
|
||||||
currentGossip = GossipMessageData{};
|
currentGossip = GossipMessageData{};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void GameHandler::offerQuestFromItem(uint64_t itemGuid, uint32_t questId) {
|
||||||
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
|
if (itemGuid == 0 || questId == 0) {
|
||||||
|
addSystemChatMessage("Cannot start quest right now.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Send CMSG_QUESTGIVER_QUERY_QUEST with the item GUID as the "questgiver."
|
||||||
|
// The server responds with SMSG_QUESTGIVER_QUEST_DETAILS which handleQuestDetails()
|
||||||
|
// picks up and opens the Accept/Decline dialog.
|
||||||
|
auto queryPkt = packetParsers_
|
||||||
|
? packetParsers_->buildQueryQuestPacket(itemGuid, questId)
|
||||||
|
: QuestgiverQueryQuestPacket::build(itemGuid, questId);
|
||||||
|
socket->send(queryPkt);
|
||||||
|
LOG_INFO("offerQuestFromItem: itemGuid=0x", std::hex, itemGuid, std::dec,
|
||||||
|
" questId=", questId);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t GameHandler::getBagItemGuid(int bagIndex, int slotIndex) const {
|
||||||
|
if (bagIndex < 0 || bagIndex >= inventory.NUM_BAG_SLOTS) return 0;
|
||||||
|
if (slotIndex < 0) return 0;
|
||||||
|
uint64_t bagGuid = equipSlotGuids_[19 + bagIndex];
|
||||||
|
if (bagGuid == 0) return 0;
|
||||||
|
auto it = containerContents_.find(bagGuid);
|
||||||
|
if (it == containerContents_.end()) return 0;
|
||||||
|
if (slotIndex >= static_cast<int>(it->second.numSlots)) return 0;
|
||||||
|
return it->second.slotGuids[slotIndex];
|
||||||
|
}
|
||||||
|
|
||||||
void GameHandler::openVendor(uint64_t npcGuid) {
|
void GameHandler::openVendor(uint64_t npcGuid) {
|
||||||
if (state != WorldState::IN_WORLD || !socket) return;
|
if (state != WorldState::IN_WORLD || !socket) return;
|
||||||
buybackItems_.clear();
|
buybackItems_.clear();
|
||||||
|
|
@ -21104,6 +21270,86 @@ void GameHandler::handleListInventory(network::Packet& packet) {
|
||||||
vendorWindowOpen = true;
|
vendorWindowOpen = true;
|
||||||
gossipWindowOpen = false; // Close gossip if vendor opens
|
gossipWindowOpen = false; // Close gossip if vendor opens
|
||||||
|
|
||||||
|
// Auto-sell grey items if enabled
|
||||||
|
if (autoSellGrey_ && currentVendorItems.vendorGuid != 0) {
|
||||||
|
uint32_t totalSellPrice = 0;
|
||||||
|
int itemsSold = 0;
|
||||||
|
|
||||||
|
// Helper lambda to attempt selling a poor-quality slot
|
||||||
|
auto tryAutoSell = [&](const ItemSlot& slot, uint64_t itemGuid) {
|
||||||
|
if (slot.empty()) return;
|
||||||
|
if (slot.item.quality != ItemQuality::POOR) return;
|
||||||
|
// Determine sell price (slot cache first, then item info fallback)
|
||||||
|
uint32_t sp = slot.item.sellPrice;
|
||||||
|
if (sp == 0) {
|
||||||
|
if (auto* info = getItemInfo(slot.item.itemId); info && info->valid)
|
||||||
|
sp = info->sellPrice;
|
||||||
|
}
|
||||||
|
if (sp == 0 || itemGuid == 0) return;
|
||||||
|
BuybackItem sold;
|
||||||
|
sold.itemGuid = itemGuid;
|
||||||
|
sold.item = slot.item;
|
||||||
|
sold.count = 1;
|
||||||
|
buybackItems_.push_front(sold);
|
||||||
|
if (buybackItems_.size() > 12) buybackItems_.pop_back();
|
||||||
|
pendingSellToBuyback_[itemGuid] = sold;
|
||||||
|
sellItem(currentVendorItems.vendorGuid, itemGuid, 1);
|
||||||
|
totalSellPrice += sp;
|
||||||
|
++itemsSold;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Backpack slots
|
||||||
|
for (int i = 0; i < inventory.getBackpackSize(); ++i) {
|
||||||
|
uint64_t guid = backpackSlotGuids_[i];
|
||||||
|
if (guid == 0) guid = resolveOnlineItemGuid(inventory.getBackpackSlot(i).item.itemId);
|
||||||
|
tryAutoSell(inventory.getBackpackSlot(i), guid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra bag slots
|
||||||
|
for (int b = 0; b < inventory.NUM_BAG_SLOTS; ++b) {
|
||||||
|
uint64_t bagGuid = equipSlotGuids_[19 + b];
|
||||||
|
for (int s = 0; s < inventory.getBagSize(b); ++s) {
|
||||||
|
uint64_t guid = 0;
|
||||||
|
if (bagGuid != 0) {
|
||||||
|
auto it = containerContents_.find(bagGuid);
|
||||||
|
if (it != containerContents_.end() && s < static_cast<int>(it->second.numSlots))
|
||||||
|
guid = it->second.slotGuids[s];
|
||||||
|
}
|
||||||
|
if (guid == 0) guid = resolveOnlineItemGuid(inventory.getBagSlot(b, s).item.itemId);
|
||||||
|
tryAutoSell(inventory.getBagSlot(b, s), guid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsSold > 0) {
|
||||||
|
uint32_t gold = totalSellPrice / 10000;
|
||||||
|
uint32_t silver = (totalSellPrice % 10000) / 100;
|
||||||
|
uint32_t copper = totalSellPrice % 100;
|
||||||
|
char buf[128];
|
||||||
|
std::snprintf(buf, sizeof(buf),
|
||||||
|
"|cffaaaaaaAuto-sold %d grey item%s for %ug %us %uc.|r",
|
||||||
|
itemsSold, itemsSold == 1 ? "" : "s", gold, silver, copper);
|
||||||
|
addSystemChatMessage(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-repair all items if enabled and vendor can repair
|
||||||
|
if (autoRepair_ && currentVendorItems.canRepair && currentVendorItems.vendorGuid != 0) {
|
||||||
|
// Check that at least one equipped item is actually damaged to avoid no-op
|
||||||
|
bool anyDamaged = false;
|
||||||
|
for (int i = 0; i < Inventory::NUM_EQUIP_SLOTS; ++i) {
|
||||||
|
const auto& slot = inventory.getEquipSlot(static_cast<EquipSlot>(i));
|
||||||
|
if (!slot.empty() && slot.item.maxDurability > 0
|
||||||
|
&& slot.item.curDurability < slot.item.maxDurability) {
|
||||||
|
anyDamaged = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (anyDamaged) {
|
||||||
|
repairAll(currentVendorItems.vendorGuid, false);
|
||||||
|
addSystemChatMessage("|cffaaaaaaAuto-repair triggered.|r");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Play vendor sound
|
// Play vendor sound
|
||||||
if (npcVendorCallback_ && currentVendorItems.vendorGuid != 0) {
|
if (npcVendorCallback_ && currentVendorItems.vendorGuid != 0) {
|
||||||
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
auto entity = entityManager.getEntity(currentVendorItems.vendorGuid);
|
||||||
|
|
@ -21229,6 +21475,14 @@ void GameHandler::loadSpellNameCache() {
|
||||||
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; }
|
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { dispelField = f; hasDispelField = true; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AttributesEx field (bit 4 = SPELL_ATTR_EX_NOT_INTERRUPTIBLE)
|
||||||
|
uint32_t attrExField = 0xFFFFFFFF;
|
||||||
|
bool hasAttrExField = false;
|
||||||
|
if (spellL) {
|
||||||
|
uint32_t f = spellL->field("AttributesEx");
|
||||||
|
if (f != 0xFFFFFFFF && f < dbc->getFieldCount()) { attrExField = f; hasAttrExField = true; }
|
||||||
|
}
|
||||||
|
|
||||||
// Tooltip/description field
|
// Tooltip/description field
|
||||||
uint32_t tooltipField = 0xFFFFFFFF;
|
uint32_t tooltipField = 0xFFFFFFFF;
|
||||||
if (spellL) {
|
if (spellL) {
|
||||||
|
|
@ -21243,7 +21497,7 @@ void GameHandler::loadSpellNameCache() {
|
||||||
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
std::string name = dbc->getString(i, spellL ? (*spellL)["Name"] : 136);
|
||||||
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
std::string rank = dbc->getString(i, spellL ? (*spellL)["Rank"] : 153);
|
||||||
if (!name.empty()) {
|
if (!name.empty()) {
|
||||||
SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0};
|
SpellNameEntry entry{std::move(name), std::move(rank), {}, 0, 0, 0};
|
||||||
if (tooltipField != 0xFFFFFFFF) {
|
if (tooltipField != 0xFFFFFFFF) {
|
||||||
entry.description = dbc->getString(i, tooltipField);
|
entry.description = dbc->getString(i, tooltipField);
|
||||||
}
|
}
|
||||||
|
|
@ -21258,6 +21512,9 @@ void GameHandler::loadSpellNameCache() {
|
||||||
if (hasDispelField) {
|
if (hasDispelField) {
|
||||||
entry.dispelType = static_cast<uint8_t>(dbc->getUInt32(i, dispelField));
|
entry.dispelType = static_cast<uint8_t>(dbc->getUInt32(i, dispelField));
|
||||||
}
|
}
|
||||||
|
if (hasAttrExField) {
|
||||||
|
entry.attrEx = dbc->getUInt32(i, attrExField);
|
||||||
|
}
|
||||||
spellNameCache_[id] = std::move(entry);
|
spellNameCache_[id] = std::move(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -21463,6 +21720,22 @@ uint8_t GameHandler::getSpellDispelType(uint32_t spellId) const {
|
||||||
return (it != spellNameCache_.end()) ? it->second.dispelType : 0;
|
return (it != spellNameCache_.end()) ? it->second.dispelType : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool GameHandler::isSpellInterruptible(uint32_t spellId) const {
|
||||||
|
if (spellId == 0) return true;
|
||||||
|
const_cast<GameHandler*>(this)->loadSpellNameCache();
|
||||||
|
auto it = spellNameCache_.find(spellId);
|
||||||
|
if (it == spellNameCache_.end()) return true; // assume interruptible if unknown
|
||||||
|
// SPELL_ATTR_EX_NOT_INTERRUPTIBLE = bit 4 of AttributesEx (0x00000010)
|
||||||
|
return (it->second.attrEx & 0x00000010u) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t GameHandler::getSpellSchoolMask(uint32_t spellId) const {
|
||||||
|
if (spellId == 0) return 0;
|
||||||
|
const_cast<GameHandler*>(this)->loadSpellNameCache();
|
||||||
|
auto it = spellNameCache_.find(spellId);
|
||||||
|
return (it != spellNameCache_.end()) ? it->second.schoolMask : 0;
|
||||||
|
}
|
||||||
|
|
||||||
const std::string& GameHandler::getSkillLineName(uint32_t spellId) const {
|
const std::string& GameHandler::getSkillLineName(uint32_t spellId) const {
|
||||||
auto slIt = spellToSkillLine_.find(spellId);
|
auto slIt = spellToSkillLine_.find(spellId);
|
||||||
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;
|
if (slIt == spellToSkillLine_.end()) return EMPTY_STRING;
|
||||||
|
|
@ -23936,6 +24209,7 @@ void GameHandler::handleAuctionCommandResult(network::Packet& packet) {
|
||||||
"DB error", "Restricted account"};
|
"DB error", "Restricted account"};
|
||||||
const char* errName = (result.errorCode < 9) ? errors[result.errorCode] : "Unknown";
|
const char* errName = (result.errorCode < 9) ? errors[result.errorCode] : "Unknown";
|
||||||
std::string msg = std::string("Auction ") + actionName + " failed: " + errName;
|
std::string msg = std::string("Auction ") + actionName + " failed: " + errName;
|
||||||
|
addUIError(msg);
|
||||||
addSystemChatMessage(msg);
|
addSystemChatMessage(msg);
|
||||||
}
|
}
|
||||||
LOG_INFO("SMSG_AUCTION_COMMAND_RESULT: action=", actionName,
|
LOG_INFO("SMSG_AUCTION_COMMAND_RESULT: action=", actionName,
|
||||||
|
|
|
||||||
|
|
@ -520,23 +520,20 @@ bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartDa
|
||||||
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
||||||
data.castTime = packet.readUInt32();
|
data.castTime = packet.readUInt32();
|
||||||
|
|
||||||
// SpellCastTargets: uint16 targetFlags in Vanilla (uint32 in TBC/WotLK)
|
// SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned.
|
||||||
if (rem() < 2) {
|
// Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40),
|
||||||
LOG_WARNING("[Classic] Spell start: missing targetFlags");
|
// SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting
|
||||||
packet.setReadPos(startPos);
|
// castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.).
|
||||||
return false;
|
{
|
||||||
}
|
uint64_t targetGuid = 0;
|
||||||
uint16_t targetFlags = packet.readUInt16();
|
// skipClassicSpellCastTargets reads uint16 targetFlags and all payloads.
|
||||||
// TARGET_FLAG_UNIT (0x02) or TARGET_FLAG_OBJECT (0x800) carry a packed GUID
|
// Non-fatal on truncation: self-cast spells have zero-byte targets.
|
||||||
if ((targetFlags & 0x02) || (targetFlags & 0x800)) {
|
skipClassicSpellCastTargets(packet, &targetGuid);
|
||||||
if (!hasFullPackedGuid(packet)) {
|
data.targetGuid = targetGuid;
|
||||||
packet.setReadPos(startPos);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms",
|
||||||
|
" targetGuid=0x", std::hex, data.targetGuid, std::dec);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -765,6 +762,10 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SpellCastTargets follows the miss list — consume all target bytes so that
|
||||||
|
// any subsequent fields (e.g. castFlags extras) are not misaligned.
|
||||||
|
skipClassicSpellCastTargets(packet, &data.targetGuid);
|
||||||
|
|
||||||
LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
" misses=", (int)data.missCount);
|
" misses=", (int)data.missCount);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1232,6 +1232,66 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// skipTbcSpellCastTargets — consume all SpellCastTargets payload bytes for TBC.
|
||||||
|
//
|
||||||
|
// TBC uses uint32 targetFlags (Classic: uint16). Unit/item/object/corpse targets
|
||||||
|
// are PackedGuid (same as Classic). Source/dest location is 3 floats (12 bytes)
|
||||||
|
// with no transport guid (Classic: same; WotLK adds a transport PackedGuid).
|
||||||
|
//
|
||||||
|
// This helper is used by parseSpellStart to ensure the read position advances
|
||||||
|
// past ALL target payload fields so subsequent fields (e.g. those parsed by the
|
||||||
|
// caller after spell targets) are not corrupted.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
static bool skipTbcSpellCastTargets(network::Packet& packet, uint64_t* primaryTargetGuid = nullptr) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 4) return false;
|
||||||
|
|
||||||
|
const uint32_t targetFlags = packet.readUInt32();
|
||||||
|
|
||||||
|
// Returns false if the packed guid can't be read, otherwise reads and optionally captures it.
|
||||||
|
auto readPackedGuidCond = [&](uint32_t flag, bool capture) -> bool {
|
||||||
|
if (!(targetFlags & flag)) return true;
|
||||||
|
// Packed GUID: 1-byte mask + up to 8 data bytes
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
uint8_t mask = packet.getData()[packet.getReadPos()];
|
||||||
|
size_t needed = 1;
|
||||||
|
for (int b = 0; b < 8; ++b) if (mask & (1u << b)) ++needed;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < needed) return false;
|
||||||
|
uint64_t g = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (capture && primaryTargetGuid && *primaryTargetGuid == 0) *primaryTargetGuid = g;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto skipFloats3 = [&](uint32_t flag) -> bool {
|
||||||
|
if (!(targetFlags & flag)) return true;
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 12) return false;
|
||||||
|
(void)packet.readFloat(); (void)packet.readFloat(); (void)packet.readFloat();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Process in wire order matching cmangos-tbc SpellCastTargets::write()
|
||||||
|
if (!readPackedGuidCond(0x0002, true)) return false; // UNIT
|
||||||
|
if (!readPackedGuidCond(0x0004, false)) return false; // UNIT_MINIPET
|
||||||
|
if (!readPackedGuidCond(0x0010, false)) return false; // ITEM
|
||||||
|
if (!skipFloats3(0x0020)) return false; // SOURCE_LOCATION
|
||||||
|
if (!skipFloats3(0x0040)) return false; // DEST_LOCATION
|
||||||
|
|
||||||
|
if (targetFlags & 0x1000) { // TRADE_ITEM: uint8
|
||||||
|
if (packet.getReadPos() >= packet.getSize()) return false;
|
||||||
|
(void)packet.readUInt8();
|
||||||
|
}
|
||||||
|
if (targetFlags & 0x2000) { // STRING: null-terminated
|
||||||
|
const auto& raw = packet.getData();
|
||||||
|
size_t pos = packet.getReadPos();
|
||||||
|
while (pos < raw.size() && raw[pos] != 0) ++pos;
|
||||||
|
if (pos >= raw.size()) return false;
|
||||||
|
packet.setReadPos(pos + 1);
|
||||||
|
}
|
||||||
|
if (!readPackedGuidCond(0x8200, false)) return false; // CORPSE / PVP_CORPSE
|
||||||
|
if (!readPackedGuidCond(0x0800, true)) return false; // OBJECT
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START
|
// TbcPacketParsers::parseSpellStart — TBC 2.4.3 SMSG_SPELL_START
|
||||||
//
|
//
|
||||||
// TBC uses full uint64 GUIDs for casterGuid and casterUnit.
|
// TBC uses full uint64 GUIDs for casterGuid and casterUnit.
|
||||||
|
|
@ -1243,7 +1303,6 @@ bool TbcPacketParsers::parseMailList(network::Packet& packet, std::vector<MailMe
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
||||||
data = SpellStartData{};
|
data = SpellStartData{};
|
||||||
const size_t startPos = packet.getReadPos();
|
|
||||||
if (packet.getSize() - packet.getReadPos() < 22) return false;
|
if (packet.getSize() - packet.getReadPos() < 22) return false;
|
||||||
|
|
||||||
data.casterGuid = packet.readUInt64(); // full GUID (object)
|
data.casterGuid = packet.readUInt64(); // full GUID (object)
|
||||||
|
|
@ -1253,23 +1312,19 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData&
|
||||||
data.castFlags = packet.readUInt32();
|
data.castFlags = packet.readUInt32();
|
||||||
data.castTime = packet.readUInt32();
|
data.castTime = packet.readUInt32();
|
||||||
|
|
||||||
if (packet.getReadPos() + 4 > packet.getSize()) {
|
// SpellCastTargets: consume ALL target payload types to keep the read position
|
||||||
LOG_WARNING("[TBC] Spell start: missing targetFlags");
|
// aligned for any bytes the caller may parse after this (ammo, etc.).
|
||||||
packet.setReadPos(startPos);
|
// The previous code only read UNIT(0x02)/OBJECT(0x800) target GUIDs and left
|
||||||
return false;
|
// DEST_LOCATION(0x40)/SOURCE_LOCATION(0x20)/ITEM(0x10) bytes unconsumed,
|
||||||
|
// corrupting subsequent reads for every AOE/ground-targeted spell cast.
|
||||||
|
{
|
||||||
|
uint64_t targetGuid = 0;
|
||||||
|
skipTbcSpellCastTargets(packet, &targetGuid); // non-fatal on truncation
|
||||||
|
data.targetGuid = targetGuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t targetFlags = packet.readUInt32();
|
LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms",
|
||||||
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
|
" targetGuid=0x", std::hex, data.targetGuid, std::dec);
|
||||||
if (needsTargetGuid) {
|
|
||||||
if (packet.getReadPos() + 8 > packet.getSize()) {
|
|
||||||
packet.setReadPos(startPos);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
data.targetGuid = packet.readUInt64(); // full GUID in TBC
|
|
||||||
}
|
|
||||||
|
|
||||||
LOG_DEBUG("[TBC] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1368,6 +1423,10 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data)
|
||||||
}
|
}
|
||||||
data.missCount = static_cast<uint8_t>(data.missTargets.size());
|
data.missCount = static_cast<uint8_t>(data.missTargets.size());
|
||||||
|
|
||||||
|
// SpellCastTargets follows the miss list — consume all target bytes so that
|
||||||
|
// any subsequent fields are not misaligned for ground-targeted AoE spells.
|
||||||
|
skipTbcSpellCastTargets(packet, &data.targetGuid);
|
||||||
|
|
||||||
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
LOG_DEBUG("[TBC] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
" misses=", (int)data.missCount);
|
" misses=", (int)data.missCount);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ WardenEmulator::WardenEmulator()
|
||||||
, heapBase_(HEAP_BASE)
|
, heapBase_(HEAP_BASE)
|
||||||
, heapSize_(HEAP_SIZE)
|
, heapSize_(HEAP_SIZE)
|
||||||
, apiStubBase_(API_STUB_BASE)
|
, apiStubBase_(API_STUB_BASE)
|
||||||
|
, nextApiStubAddr_(API_STUB_BASE)
|
||||||
|
, apiCodeHookRegistered_(false)
|
||||||
, nextHeapAddr_(HEAP_BASE)
|
, nextHeapAddr_(HEAP_BASE)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
@ -51,8 +53,11 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
||||||
allocations_.clear();
|
allocations_.clear();
|
||||||
freeBlocks_.clear();
|
freeBlocks_.clear();
|
||||||
apiAddresses_.clear();
|
apiAddresses_.clear();
|
||||||
|
apiHandlers_.clear();
|
||||||
hooks_.clear();
|
hooks_.clear();
|
||||||
nextHeapAddr_ = heapBase_;
|
nextHeapAddr_ = heapBase_;
|
||||||
|
nextApiStubAddr_ = apiStubBase_;
|
||||||
|
apiCodeHookRegistered_ = false;
|
||||||
|
|
||||||
{
|
{
|
||||||
char addrBuf[32];
|
char addrBuf[32];
|
||||||
|
|
@ -149,6 +154,13 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
||||||
uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0);
|
uc_hook_add(uc_, &hh, UC_HOOK_MEM_INVALID, (void*)hookMemInvalid, this, 1, 0);
|
||||||
hooks_.push_back(hh);
|
hooks_.push_back(hh);
|
||||||
|
|
||||||
|
// Add code hook over the API stub area so Windows API calls are intercepted
|
||||||
|
uc_hook apiHook;
|
||||||
|
uc_hook_add(uc_, &apiHook, UC_HOOK_CODE, (void*)hookCode, this,
|
||||||
|
API_STUB_BASE, API_STUB_BASE + 0x10000 - 1);
|
||||||
|
hooks_.push_back(apiHook);
|
||||||
|
apiCodeHookRegistered_ = true;
|
||||||
|
|
||||||
{
|
{
|
||||||
char sBuf[128];
|
char sBuf[128];
|
||||||
std::snprintf(sBuf, sizeof(sBuf), "WardenEmulator: Emulator initialized Stack: 0x%X-0x%X Heap: 0x%X-0x%X",
|
std::snprintf(sBuf, sizeof(sBuf), "WardenEmulator: Emulator initialized Stack: 0x%X-0x%X Heap: 0x%X-0x%X",
|
||||||
|
|
@ -161,23 +173,45 @@ bool WardenEmulator::initialize(const void* moduleCode, size_t moduleSize, uint3
|
||||||
|
|
||||||
uint32_t WardenEmulator::hookAPI(const std::string& dllName,
|
uint32_t WardenEmulator::hookAPI(const std::string& dllName,
|
||||||
const std::string& functionName,
|
const std::string& functionName,
|
||||||
[[maybe_unused]] std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
|
std::function<uint32_t(WardenEmulator&, const std::vector<uint32_t>&)> handler) {
|
||||||
// Allocate address for this API stub
|
// Allocate address for this API stub (16 bytes each)
|
||||||
static uint32_t nextStubAddr = API_STUB_BASE;
|
uint32_t stubAddr = nextApiStubAddr_;
|
||||||
uint32_t stubAddr = nextStubAddr;
|
nextApiStubAddr_ += 16;
|
||||||
nextStubAddr += 16; // Space for stub code
|
|
||||||
|
|
||||||
// Store mapping
|
// Store address mapping for IAT patching
|
||||||
apiAddresses_[dllName][functionName] = stubAddr;
|
apiAddresses_[dllName][functionName] = stubAddr;
|
||||||
|
|
||||||
{
|
// Determine stdcall arg count from known Windows APIs so the hook can
|
||||||
char hBuf[32];
|
// clean up the stack correctly (RETN N convention).
|
||||||
std::snprintf(hBuf, sizeof(hBuf), "0x%X", stubAddr);
|
static const std::pair<const char*, int> knownArgCounts[] = {
|
||||||
LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf);
|
{"VirtualAlloc", 4},
|
||||||
|
{"VirtualFree", 3},
|
||||||
|
{"GetTickCount", 0},
|
||||||
|
{"Sleep", 1},
|
||||||
|
{"GetCurrentThreadId", 0},
|
||||||
|
{"GetCurrentProcessId", 0},
|
||||||
|
{"ReadProcessMemory", 5},
|
||||||
|
};
|
||||||
|
int argCount = 0;
|
||||||
|
for (const auto& [name, cnt] : knownArgCounts) {
|
||||||
|
if (functionName == name) { argCount = cnt; break; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Write stub code that triggers a hook callback
|
// Store the handler so hookCode() can dispatch to it
|
||||||
// For now, just return the address for IAT patching
|
apiHandlers_[stubAddr] = { argCount, std::move(handler) };
|
||||||
|
|
||||||
|
// Write a RET (0xC3) at the stub address as a safe fallback in case
|
||||||
|
// the code hook fires after EIP has already advanced past our intercept.
|
||||||
|
if (uc_) {
|
||||||
|
static const uint8_t retInstr = 0xC3;
|
||||||
|
uc_mem_write(uc_, stubAddr, &retInstr, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
char hBuf[64];
|
||||||
|
std::snprintf(hBuf, sizeof(hBuf), "0x%X (argCount=%d)", stubAddr, argCount);
|
||||||
|
LOG_DEBUG("WardenEmulator: Hooked ", dllName, "!", functionName, " at ", hBuf);
|
||||||
|
}
|
||||||
|
|
||||||
return stubAddr;
|
return stubAddr;
|
||||||
}
|
}
|
||||||
|
|
@ -503,8 +537,40 @@ uint32_t WardenEmulator::apiReadProcessMemory(WardenEmulator& emu, const std::ve
|
||||||
// Unicorn Callbacks
|
// Unicorn Callbacks
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
void WardenEmulator::hookCode([[maybe_unused]] uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, [[maybe_unused]] void* userData) {
|
void WardenEmulator::hookCode(uc_engine* uc, uint64_t address, [[maybe_unused]] uint32_t size, void* userData) {
|
||||||
(void)address; // Trace disabled by default to avoid log spam
|
auto* self = static_cast<WardenEmulator*>(userData);
|
||||||
|
if (!self) return;
|
||||||
|
|
||||||
|
auto it = self->apiHandlers_.find(static_cast<uint32_t>(address));
|
||||||
|
if (it == self->apiHandlers_.end()) return; // not an API stub — trace disabled to avoid spam
|
||||||
|
|
||||||
|
const ApiHookEntry& entry = it->second;
|
||||||
|
|
||||||
|
// Read stack: [ESP+0] = return address, [ESP+4..] = stdcall args
|
||||||
|
uint32_t esp = 0;
|
||||||
|
uc_reg_read(uc, UC_X86_REG_ESP, &esp);
|
||||||
|
|
||||||
|
uint32_t retAddr = 0;
|
||||||
|
uc_mem_read(uc, esp, &retAddr, 4);
|
||||||
|
|
||||||
|
std::vector<uint32_t> args(static_cast<size_t>(entry.argCount));
|
||||||
|
for (int i = 0; i < entry.argCount; ++i) {
|
||||||
|
uint32_t val = 0;
|
||||||
|
uc_mem_read(uc, esp + 4 + static_cast<uint32_t>(i) * 4, &val, 4);
|
||||||
|
args[static_cast<size_t>(i)] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch to the C++ handler
|
||||||
|
uint32_t retVal = 0;
|
||||||
|
if (entry.handler) {
|
||||||
|
retVal = entry.handler(*self, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate stdcall epilogue: pop return address + args
|
||||||
|
uint32_t newEsp = esp + 4 + static_cast<uint32_t>(entry.argCount) * 4;
|
||||||
|
uc_reg_write(uc, UC_X86_REG_EAX, &retVal);
|
||||||
|
uc_reg_write(uc, UC_X86_REG_ESP, &newEsp);
|
||||||
|
uc_reg_write(uc, UC_X86_REG_EIP, &retAddr);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) {
|
void WardenEmulator::hookMemInvalid([[maybe_unused]] uc_engine* uc, int type, uint64_t address, int size, [[maybe_unused]] int64_t value, [[maybe_unused]] void* userData) {
|
||||||
|
|
@ -533,7 +599,8 @@ WardenEmulator::WardenEmulator()
|
||||||
: uc_(nullptr), moduleBase_(0), moduleSize_(0)
|
: uc_(nullptr), moduleBase_(0), moduleSize_(0)
|
||||||
, stackBase_(0), stackSize_(0)
|
, stackBase_(0), stackSize_(0)
|
||||||
, heapBase_(0), heapSize_(0)
|
, heapBase_(0), heapSize_(0)
|
||||||
, apiStubBase_(0), nextHeapAddr_(0) {}
|
, apiStubBase_(0), nextApiStubAddr_(0), apiCodeHookRegistered_(false)
|
||||||
|
, nextHeapAddr_(0) {}
|
||||||
WardenEmulator::~WardenEmulator() {}
|
WardenEmulator::~WardenEmulator() {}
|
||||||
bool WardenEmulator::initialize(const void*, size_t, uint32_t) { return false; }
|
bool WardenEmulator::initialize(const void*, size_t, uint32_t) { return false; }
|
||||||
uint32_t WardenEmulator::hookAPI(const std::string&, const std::string&,
|
uint32_t WardenEmulator::hookAPI(const std::string&, const std::string&,
|
||||||
|
|
|
||||||
|
|
@ -161,24 +161,53 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Call module's PacketHandler
|
if (emulatedPacketHandlerAddr_ == 0) {
|
||||||
// void PacketHandler(uint8_t* checkData, size_t checkSize,
|
LOG_ERROR("WardenModule: PacketHandler address not set (module not fully initialized)");
|
||||||
// uint8_t* responseOut, size_t* responseSizeOut)
|
emulator_->freeMemory(checkDataAddr);
|
||||||
LOG_INFO("WardenModule: Calling PacketHandler...");
|
emulator_->freeMemory(responseAddr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// For now, this is a placeholder - actual calling would depend on
|
// Allocate uint32_t for responseSizeOut in emulated memory
|
||||||
// the module's exact function signature
|
uint32_t initialSize = 1024;
|
||||||
LOG_WARNING("WardenModule: PacketHandler execution stubbed");
|
uint32_t responseSizeAddr = emulator_->writeData(&initialSize, sizeof(uint32_t));
|
||||||
LOG_INFO("WardenModule: Would call emulated function to process checks");
|
if (responseSizeAddr == 0) {
|
||||||
LOG_INFO("WardenModule: This would generate REAL responses (not fakes!)");
|
LOG_ERROR("WardenModule: Failed to allocate responseSizeAddr");
|
||||||
|
emulator_->freeMemory(checkDataAddr);
|
||||||
|
emulator_->freeMemory(responseAddr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call: void PacketHandler(uint8_t* data, uint32_t size,
|
||||||
|
// uint8_t* responseOut, uint32_t* responseSizeOut)
|
||||||
|
LOG_INFO("WardenModule: Calling emulated PacketHandler...");
|
||||||
|
emulator_->callFunction(emulatedPacketHandlerAddr_, {
|
||||||
|
checkDataAddr,
|
||||||
|
static_cast<uint32_t>(checkData.size()),
|
||||||
|
responseAddr,
|
||||||
|
responseSizeAddr
|
||||||
|
});
|
||||||
|
|
||||||
|
// Read back response size and data
|
||||||
|
uint32_t responseSize = 0;
|
||||||
|
emulator_->readMemory(responseSizeAddr, &responseSize, sizeof(uint32_t));
|
||||||
|
emulator_->freeMemory(responseSizeAddr);
|
||||||
|
|
||||||
|
if (responseSize > 0 && responseSize <= 1024) {
|
||||||
|
responseOut.resize(responseSize);
|
||||||
|
if (!emulator_->readMemory(responseAddr, responseOut.data(), responseSize)) {
|
||||||
|
LOG_ERROR("WardenModule: Failed to read response data");
|
||||||
|
responseOut.clear();
|
||||||
|
} else {
|
||||||
|
LOG_INFO("WardenModule: PacketHandler wrote ", responseSize, " byte response");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARNING("WardenModule: PacketHandler returned invalid responseSize=", responseSize);
|
||||||
|
}
|
||||||
|
|
||||||
// Clean up
|
|
||||||
emulator_->freeMemory(checkDataAddr);
|
emulator_->freeMemory(checkDataAddr);
|
||||||
emulator_->freeMemory(responseAddr);
|
emulator_->freeMemory(responseAddr);
|
||||||
|
return !responseOut.empty();
|
||||||
// For now, return false to use fake responses
|
|
||||||
// Once we have a real module, we'd read the response from responseAddr
|
|
||||||
return false;
|
|
||||||
|
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what());
|
LOG_ERROR("WardenModule: Exception during PacketHandler: ", e.what());
|
||||||
|
|
@ -196,25 +225,18 @@ bool WardenModule::processCheckRequest(const std::vector<uint8_t>& checkData,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t WardenModule::tick([[maybe_unused]] uint32_t deltaMs) {
|
uint32_t WardenModule::tick(uint32_t deltaMs) {
|
||||||
if (!loaded_ || !funcList_.tick) {
|
if (!loaded_ || !funcList_.tick) {
|
||||||
return 0; // No tick needed
|
return 0;
|
||||||
}
|
}
|
||||||
|
return funcList_.tick(deltaMs);
|
||||||
// TODO: Call module's Tick function
|
|
||||||
// return funcList_.tick(deltaMs);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WardenModule::generateRC4Keys([[maybe_unused]] uint8_t* packet) {
|
void WardenModule::generateRC4Keys(uint8_t* packet) {
|
||||||
if (!loaded_ || !funcList_.generateRC4Keys) {
|
if (!loaded_ || !funcList_.generateRC4Keys) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
funcList_.generateRC4Keys(packet);
|
||||||
// TODO: Call module's GenerateRC4Keys function
|
|
||||||
// This re-keys the Warden crypto stream
|
|
||||||
// funcList_.generateRC4Keys(packet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WardenModule::unload() {
|
void WardenModule::unload() {
|
||||||
|
|
@ -222,8 +244,7 @@ void WardenModule::unload() {
|
||||||
// Call module's Unload() function if loaded
|
// Call module's Unload() function if loaded
|
||||||
if (loaded_ && funcList_.unload) {
|
if (loaded_ && funcList_.unload) {
|
||||||
LOG_INFO("WardenModule: Calling module unload callback...");
|
LOG_INFO("WardenModule: Calling module unload callback...");
|
||||||
// TODO: Implement callback when execution layer is complete
|
funcList_.unload(nullptr);
|
||||||
// funcList_.unload(nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free executable memory region
|
// Free executable memory region
|
||||||
|
|
@ -240,6 +261,7 @@ void WardenModule::unload() {
|
||||||
|
|
||||||
// Clear function pointers
|
// Clear function pointers
|
||||||
funcList_ = {};
|
funcList_ = {};
|
||||||
|
emulatedPacketHandlerAddr_ = 0;
|
||||||
|
|
||||||
loaded_ = false;
|
loaded_ = false;
|
||||||
moduleData_.clear();
|
moduleData_.clear();
|
||||||
|
|
@ -961,7 +983,12 @@ bool WardenModule::initializeModule() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read WardenFuncList structure from emulated memory
|
// Read WardenFuncList structure from emulated memory
|
||||||
// Structure has 4 function pointers (16 bytes)
|
// Structure has 4 function pointers (16 bytes):
|
||||||
|
// [0] generateRC4Keys(uint8_t* seed)
|
||||||
|
// [1] unload(uint8_t* rc4Keys)
|
||||||
|
// [2] packetHandler(uint8_t* data, uint32_t size,
|
||||||
|
// uint8_t* responseOut, uint32_t* responseSizeOut)
|
||||||
|
// [3] tick(uint32_t deltaMs) -> uint32_t
|
||||||
uint32_t funcAddrs[4] = {};
|
uint32_t funcAddrs[4] = {};
|
||||||
if (emulator_->readMemory(result, funcAddrs, 16)) {
|
if (emulator_->readMemory(result, funcAddrs, 16)) {
|
||||||
char fb[4][32];
|
char fb[4][32];
|
||||||
|
|
@ -973,11 +1000,48 @@ bool WardenModule::initializeModule() {
|
||||||
LOG_INFO("WardenModule: packetHandler: ", fb[2]);
|
LOG_INFO("WardenModule: packetHandler: ", fb[2]);
|
||||||
LOG_INFO("WardenModule: tick: ", fb[3]);
|
LOG_INFO("WardenModule: tick: ", fb[3]);
|
||||||
|
|
||||||
// Store function addresses for later use
|
// Wrap emulated function addresses into std::function dispatchers
|
||||||
// funcList_.generateRC4Keys = ... (would wrap emulator calls)
|
WardenEmulator* emu = emulator_.get();
|
||||||
// funcList_.unload = ...
|
|
||||||
// funcList_.packetHandler = ...
|
if (funcAddrs[0]) {
|
||||||
// funcList_.tick = ...
|
uint32_t addr = funcAddrs[0];
|
||||||
|
funcList_.generateRC4Keys = [emu, addr](uint8_t* seed) {
|
||||||
|
// Warden RC4 seed is a fixed 4-byte value
|
||||||
|
uint32_t seedAddr = emu->writeData(seed, 4);
|
||||||
|
if (seedAddr) {
|
||||||
|
emu->callFunction(addr, {seedAddr});
|
||||||
|
emu->freeMemory(seedAddr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funcAddrs[1]) {
|
||||||
|
uint32_t addr = funcAddrs[1];
|
||||||
|
funcList_.unload = [emu, addr]([[maybe_unused]] uint8_t* rc4Keys) {
|
||||||
|
emu->callFunction(addr, {0u}); // pass NULL; module saves its own state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funcAddrs[2]) {
|
||||||
|
// Store raw address for the 4-arg call in processCheckRequest
|
||||||
|
emulatedPacketHandlerAddr_ = funcAddrs[2];
|
||||||
|
uint32_t addr = funcAddrs[2];
|
||||||
|
// Simple 2-arg variant for generic callers (no response extraction)
|
||||||
|
funcList_.packetHandler = [emu, addr](uint8_t* data, size_t length) {
|
||||||
|
uint32_t dataAddr = emu->writeData(data, length);
|
||||||
|
if (dataAddr) {
|
||||||
|
emu->callFunction(addr, {dataAddr, static_cast<uint32_t>(length)});
|
||||||
|
emu->freeMemory(dataAddr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (funcAddrs[3]) {
|
||||||
|
uint32_t addr = funcAddrs[3];
|
||||||
|
funcList_.tick = [emu, addr](uint32_t deltaMs) -> uint32_t {
|
||||||
|
return emu->callFunction(addr, {deltaMs});
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("WardenModule: Module fully initialized and ready!");
|
LOG_INFO("WardenModule: Module fully initialized and ready!");
|
||||||
|
|
|
||||||
|
|
@ -3780,14 +3780,44 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that
|
||||||
|
// subsequent fields (e.g. school mask, cast flags 0x20 extra data) are not
|
||||||
|
// misaligned for ground-targeted or AoE spells.
|
||||||
uint32_t targetFlags = packet.readUInt32();
|
uint32_t targetFlags = packet.readUInt32();
|
||||||
const bool needsTargetGuid = (targetFlags & 0x02) || (targetFlags & 0x800); // UNIT/OBJECT
|
|
||||||
if (needsTargetGuid) {
|
auto readPackedTarget = [&](uint64_t* out) -> bool {
|
||||||
if (!hasFullPackedGuid(packet)) {
|
if (!hasFullPackedGuid(packet)) return false;
|
||||||
packet.setReadPos(startPos);
|
uint64_t g = UpdateObjectParser::readPackedGuid(packet);
|
||||||
return false;
|
if (out) *out = g;
|
||||||
}
|
return true;
|
||||||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
};
|
||||||
|
auto skipPackedAndFloats3 = [&]() -> bool {
|
||||||
|
if (!hasFullPackedGuid(packet)) return false;
|
||||||
|
UpdateObjectParser::readPackedGuid(packet); // transport GUID (may be zero)
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 12) return false;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share a single object target GUID
|
||||||
|
if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) {
|
||||||
|
readPackedTarget(&data.targetGuid); // best-effort; ignore failure
|
||||||
|
}
|
||||||
|
// ITEM/TRADE_ITEM share a single item target GUID
|
||||||
|
if (targetFlags & (0x0010u | 0x0100u)) {
|
||||||
|
readPackedTarget(nullptr);
|
||||||
|
}
|
||||||
|
// SOURCE_LOCATION: PackedGuid (transport) + float x,y,z
|
||||||
|
if (targetFlags & 0x0020u) {
|
||||||
|
skipPackedAndFloats3();
|
||||||
|
}
|
||||||
|
// DEST_LOCATION: PackedGuid (transport) + float x,y,z
|
||||||
|
if (targetFlags & 0x0040u) {
|
||||||
|
skipPackedAndFloats3();
|
||||||
|
}
|
||||||
|
// STRING: null-terminated
|
||||||
|
if (targetFlags & 0x0200u) {
|
||||||
|
while (packet.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
LOG_DEBUG("Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms");
|
||||||
|
|
@ -3901,6 +3931,50 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) {
|
||||||
}
|
}
|
||||||
data.missCount = static_cast<uint8_t>(data.missTargets.size());
|
data.missCount = static_cast<uint8_t>(data.missTargets.size());
|
||||||
|
|
||||||
|
// WotLK 3.3.5a SpellCastTargets — consume ALL target payload bytes so that
|
||||||
|
// any trailing fields after the target section are not misaligned for
|
||||||
|
// ground-targeted or AoE spells. Same layout as SpellStartParser.
|
||||||
|
if (packet.getReadPos() < packet.getSize()) {
|
||||||
|
if (packet.getSize() - packet.getReadPos() >= 4) {
|
||||||
|
uint32_t targetFlags = packet.readUInt32();
|
||||||
|
|
||||||
|
auto readPackedTarget = [&](uint64_t* out) -> bool {
|
||||||
|
if (!hasFullPackedGuid(packet)) return false;
|
||||||
|
uint64_t g = UpdateObjectParser::readPackedGuid(packet);
|
||||||
|
if (out) *out = g;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
auto skipPackedAndFloats3 = [&]() -> bool {
|
||||||
|
if (!hasFullPackedGuid(packet)) return false;
|
||||||
|
UpdateObjectParser::readPackedGuid(packet); // transport GUID
|
||||||
|
if (packet.getSize() - packet.getReadPos() < 12) return false;
|
||||||
|
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
// UNIT/UNIT_MINIPET/CORPSE_ALLY/GAMEOBJECT share one object target GUID
|
||||||
|
if (targetFlags & (0x0002u | 0x0004u | 0x0400u | 0x0800u)) {
|
||||||
|
readPackedTarget(&data.targetGuid);
|
||||||
|
}
|
||||||
|
// ITEM/TRADE_ITEM share one item target GUID
|
||||||
|
if (targetFlags & (0x0010u | 0x0100u)) {
|
||||||
|
readPackedTarget(nullptr);
|
||||||
|
}
|
||||||
|
// SOURCE_LOCATION: PackedGuid (transport) + float x,y,z
|
||||||
|
if (targetFlags & 0x0020u) {
|
||||||
|
skipPackedAndFloats3();
|
||||||
|
}
|
||||||
|
// DEST_LOCATION: PackedGuid (transport) + float x,y,z
|
||||||
|
if (targetFlags & 0x0040u) {
|
||||||
|
skipPackedAndFloats3();
|
||||||
|
}
|
||||||
|
// STRING: null-terminated
|
||||||
|
if (targetFlags & 0x0200u) {
|
||||||
|
while (packet.getReadPos() < packet.getSize() && packet.readUInt8() != 0) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
LOG_DEBUG("Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||||||
" misses=", (int)data.missCount);
|
" misses=", (int)data.missCount);
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -388,10 +388,11 @@ void CameraController::update(float deltaTime) {
|
||||||
if (mounted_) sitting = false;
|
if (mounted_) sitting = false;
|
||||||
xKeyWasDown = xDown;
|
xKeyWasDown = xDown;
|
||||||
|
|
||||||
// Reset camera with R key (edge-triggered) — only when UI doesn't want keyboard
|
// Reset camera angles with R key (edge-triggered) — only when UI doesn't want keyboard
|
||||||
|
// Does NOT move the player; full reset() is reserved for world-entry/respawn.
|
||||||
bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
|
bool rDown = !uiWantsKeyboard && input.isKeyPressed(SDL_SCANCODE_R);
|
||||||
if (rDown && !rKeyWasDown) {
|
if (rDown && !rKeyWasDown) {
|
||||||
reset();
|
resetAngles();
|
||||||
}
|
}
|
||||||
rKeyWasDown = rDown;
|
rKeyWasDown = rDown;
|
||||||
|
|
||||||
|
|
@ -1941,6 +1942,14 @@ void CameraController::processMouseButton(const SDL_MouseButtonEvent& event) {
|
||||||
mouseButtonDown = anyDown;
|
mouseButtonDown = anyDown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CameraController::resetAngles() {
|
||||||
|
if (!camera) return;
|
||||||
|
yaw = defaultYaw;
|
||||||
|
facingYaw = defaultYaw;
|
||||||
|
pitch = defaultPitch;
|
||||||
|
camera->setRotation(yaw, pitch);
|
||||||
|
}
|
||||||
|
|
||||||
void CameraController::reset() {
|
void CameraController::reset() {
|
||||||
if (!camera) {
|
if (!camera) {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -2627,6 +2627,190 @@ void Renderer::stopChargeEffect() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Spell Visual Effects ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
void Renderer::loadSpellVisualDbc() {
|
||||||
|
if (spellVisualDbcLoaded_) return;
|
||||||
|
spellVisualDbcLoaded_ = true; // Set early to prevent re-entry on failure
|
||||||
|
|
||||||
|
if (!cachedAssetManager) {
|
||||||
|
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
||||||
|
}
|
||||||
|
if (!cachedAssetManager) return;
|
||||||
|
|
||||||
|
auto* layout = pipeline::getActiveDBCLayout();
|
||||||
|
const pipeline::DBCFieldMap* svLayout = layout ? layout->getLayout("SpellVisual") : nullptr;
|
||||||
|
const pipeline::DBCFieldMap* kitLayout = layout ? layout->getLayout("SpellVisualKit") : nullptr;
|
||||||
|
const pipeline::DBCFieldMap* fxLayout = layout ? layout->getLayout("SpellVisualEffectName") : nullptr;
|
||||||
|
|
||||||
|
uint32_t svCastKitField = svLayout ? (*svLayout)["CastKit"] : 2;
|
||||||
|
uint32_t svImpactKitField = svLayout ? (*svLayout)["ImpactKit"] : 3;
|
||||||
|
uint32_t svMissileField = svLayout ? (*svLayout)["MissileModel"] : 8;
|
||||||
|
uint32_t kitSpecial0Field = kitLayout ? (*kitLayout)["SpecialEffect0"] : 11;
|
||||||
|
uint32_t kitBaseField = kitLayout ? (*kitLayout)["BaseEffect"] : 5;
|
||||||
|
uint32_t fxFilePathField = fxLayout ? (*fxLayout)["FilePath"] : 2;
|
||||||
|
|
||||||
|
// Helper to look up effectName path from a kit ID
|
||||||
|
// Load SpellVisualEffectName.dbc — ID → M2 path
|
||||||
|
auto fxDbc = cachedAssetManager->loadDBC("SpellVisualEffectName.dbc");
|
||||||
|
if (!fxDbc || !fxDbc->isLoaded() || fxDbc->getFieldCount() <= fxFilePathField) {
|
||||||
|
LOG_DEBUG("SpellVisual: SpellVisualEffectName.dbc unavailable (fc=",
|
||||||
|
fxDbc ? fxDbc->getFieldCount() : 0, ")");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::unordered_map<uint32_t, std::string> effectPaths; // effectNameId → path
|
||||||
|
for (uint32_t i = 0; i < fxDbc->getRecordCount(); ++i) {
|
||||||
|
uint32_t id = fxDbc->getUInt32(i, 0);
|
||||||
|
std::string p = fxDbc->getString(i, fxFilePathField);
|
||||||
|
if (id && !p.empty()) effectPaths[id] = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load SpellVisualKit.dbc — kitId → best SpellVisualEffectName ID
|
||||||
|
auto kitDbc = cachedAssetManager->loadDBC("SpellVisualKit.dbc");
|
||||||
|
std::unordered_map<uint32_t, uint32_t> kitToEffectName; // kitId → effectNameId
|
||||||
|
if (kitDbc && kitDbc->isLoaded()) {
|
||||||
|
uint32_t fc = kitDbc->getFieldCount();
|
||||||
|
for (uint32_t i = 0; i < kitDbc->getRecordCount(); ++i) {
|
||||||
|
uint32_t kitId = kitDbc->getUInt32(i, 0);
|
||||||
|
if (!kitId) continue;
|
||||||
|
// Prefer SpecialEffect0, fall back to BaseEffect
|
||||||
|
uint32_t eff = 0;
|
||||||
|
if (kitSpecial0Field < fc) eff = kitDbc->getUInt32(i, kitSpecial0Field);
|
||||||
|
if (!eff && kitBaseField < fc) eff = kitDbc->getUInt32(i, kitBaseField);
|
||||||
|
if (eff) kitToEffectName[kitId] = eff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper: resolve path for a given kit ID
|
||||||
|
auto kitPath = [&](uint32_t kitId) -> std::string {
|
||||||
|
if (!kitId) return {};
|
||||||
|
auto kitIt = kitToEffectName.find(kitId);
|
||||||
|
if (kitIt == kitToEffectName.end()) return {};
|
||||||
|
auto fxIt = effectPaths.find(kitIt->second);
|
||||||
|
return (fxIt != effectPaths.end()) ? fxIt->second : std::string{};
|
||||||
|
};
|
||||||
|
auto missilePath = [&](uint32_t effId) -> std::string {
|
||||||
|
if (!effId) return {};
|
||||||
|
auto fxIt = effectPaths.find(effId);
|
||||||
|
return (fxIt != effectPaths.end()) ? fxIt->second : std::string{};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load SpellVisual.dbc — visualId → cast/impact M2 paths via kit chain
|
||||||
|
auto svDbc = cachedAssetManager->loadDBC("SpellVisual.dbc");
|
||||||
|
if (!svDbc || !svDbc->isLoaded()) {
|
||||||
|
LOG_DEBUG("SpellVisual: SpellVisual.dbc unavailable");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t svFc = svDbc->getFieldCount();
|
||||||
|
uint32_t loadedCast = 0, loadedImpact = 0;
|
||||||
|
for (uint32_t i = 0; i < svDbc->getRecordCount(); ++i) {
|
||||||
|
uint32_t vid = svDbc->getUInt32(i, 0);
|
||||||
|
if (!vid) continue;
|
||||||
|
|
||||||
|
// Cast path: CastKit → SpecialEffect0/BaseEffect, fallback to MissileModel
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
if (svCastKitField < svFc)
|
||||||
|
path = kitPath(svDbc->getUInt32(i, svCastKitField));
|
||||||
|
if (path.empty() && svMissileField < svFc)
|
||||||
|
path = missilePath(svDbc->getUInt32(i, svMissileField));
|
||||||
|
if (!path.empty()) { spellVisualCastPath_[vid] = path; ++loadedCast; }
|
||||||
|
}
|
||||||
|
// Impact path: ImpactKit → SpecialEffect0/BaseEffect, fallback to MissileModel
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
if (svImpactKitField < svFc)
|
||||||
|
path = kitPath(svDbc->getUInt32(i, svImpactKitField));
|
||||||
|
if (path.empty() && svMissileField < svFc)
|
||||||
|
path = missilePath(svDbc->getUInt32(i, svMissileField));
|
||||||
|
if (!path.empty()) { spellVisualImpactPath_[vid] = path; ++loadedImpact; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LOG_INFO("SpellVisual: loaded cast=", loadedCast, " impact=", loadedImpact,
|
||||||
|
" visual→M2 mappings (of ", svDbc->getRecordCount(), " records)");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::playSpellVisual(uint32_t visualId, const glm::vec3& worldPosition,
|
||||||
|
bool useImpactKit) {
|
||||||
|
if (!m2Renderer || visualId == 0) return;
|
||||||
|
|
||||||
|
if (!cachedAssetManager)
|
||||||
|
cachedAssetManager = core::Application::getInstance().getAssetManager();
|
||||||
|
if (!cachedAssetManager) return;
|
||||||
|
|
||||||
|
if (!spellVisualDbcLoaded_) loadSpellVisualDbc();
|
||||||
|
|
||||||
|
// Select cast or impact path map
|
||||||
|
auto& pathMap = useImpactKit ? spellVisualImpactPath_ : spellVisualCastPath_;
|
||||||
|
auto pathIt = pathMap.find(visualId);
|
||||||
|
if (pathIt == pathMap.end()) return; // No model for this visual
|
||||||
|
|
||||||
|
const std::string& modelPath = pathIt->second;
|
||||||
|
|
||||||
|
// Get or assign a model ID for this path
|
||||||
|
auto midIt = spellVisualModelIds_.find(modelPath);
|
||||||
|
uint32_t modelId = 0;
|
||||||
|
if (midIt != spellVisualModelIds_.end()) {
|
||||||
|
modelId = midIt->second;
|
||||||
|
} else {
|
||||||
|
if (nextSpellVisualModelId_ >= 999800) {
|
||||||
|
LOG_WARNING("SpellVisual: model ID pool exhausted");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modelId = nextSpellVisualModelId_++;
|
||||||
|
spellVisualModelIds_[modelPath] = modelId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the M2 model if not already loaded
|
||||||
|
if (!m2Renderer->hasModel(modelId)) {
|
||||||
|
auto m2Data = cachedAssetManager->readFile(modelPath);
|
||||||
|
if (m2Data.empty()) {
|
||||||
|
LOG_DEBUG("SpellVisual: could not read model: ", modelPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pipeline::M2Model model = pipeline::M2Loader::load(m2Data);
|
||||||
|
if (model.vertices.empty() && model.particleEmitters.empty()) {
|
||||||
|
LOG_DEBUG("SpellVisual: empty model: ", modelPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Load skin file for WotLK-format M2s
|
||||||
|
if (model.version >= 264) {
|
||||||
|
std::string skinPath = modelPath.substr(0, modelPath.rfind('.')) + "00.skin";
|
||||||
|
auto skinData = cachedAssetManager->readFile(skinPath);
|
||||||
|
if (!skinData.empty()) pipeline::M2Loader::loadSkin(skinData, model);
|
||||||
|
}
|
||||||
|
if (!m2Renderer->loadModel(model, modelId)) {
|
||||||
|
LOG_WARNING("SpellVisual: failed to load model to GPU: ", modelPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
LOG_DEBUG("SpellVisual: loaded model id=", modelId, " path=", modelPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spawn instance at world position
|
||||||
|
uint32_t instanceId = m2Renderer->createInstance(modelId, worldPosition,
|
||||||
|
glm::vec3(0.0f), 1.0f);
|
||||||
|
if (instanceId == 0) {
|
||||||
|
LOG_WARNING("SpellVisual: failed to create instance for visualId=", visualId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
activeSpellVisuals_.push_back({instanceId, 0.0f});
|
||||||
|
LOG_DEBUG("SpellVisual: spawned visualId=", visualId, " instanceId=", instanceId,
|
||||||
|
" model=", modelPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Renderer::updateSpellVisuals(float deltaTime) {
|
||||||
|
if (activeSpellVisuals_.empty() || !m2Renderer) return;
|
||||||
|
for (auto it = activeSpellVisuals_.begin(); it != activeSpellVisuals_.end(); ) {
|
||||||
|
it->elapsed += deltaTime;
|
||||||
|
if (it->elapsed >= SPELL_VISUAL_DURATION) {
|
||||||
|
m2Renderer->removeInstance(it->instanceId);
|
||||||
|
it = activeSpellVisuals_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Renderer::triggerMeleeSwing() {
|
void Renderer::triggerMeleeSwing() {
|
||||||
if (!characterRenderer || characterInstanceId == 0) return;
|
if (!characterRenderer || characterInstanceId == 0) return;
|
||||||
if (meleeSwingCooldown > 0.0f) return;
|
if (meleeSwingCooldown > 0.0f) return;
|
||||||
|
|
@ -3012,6 +3196,8 @@ void Renderer::update(float deltaTime) {
|
||||||
if (chargeEffect) {
|
if (chargeEffect) {
|
||||||
chargeEffect->update(deltaTime);
|
chargeEffect->update(deltaTime);
|
||||||
}
|
}
|
||||||
|
// Update transient spell visual instances
|
||||||
|
updateSpellVisuals(deltaTime);
|
||||||
|
|
||||||
|
|
||||||
// Launch M2 doodad animation on background thread (overlaps with character animation + audio)
|
// Launch M2 doodad animation on background thread (overlaps with character animation + audio)
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@ void WorldMap::loadZonesFromDBC() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
currentMapId_ = mapID;
|
||||||
LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID,
|
LOG_INFO("WorldMap: loaded ", zones.size(), " zones for mapID=", mapID,
|
||||||
", continentIdx=", continentIdx);
|
", continentIdx=", continentIdx);
|
||||||
}
|
}
|
||||||
|
|
@ -1059,6 +1060,69 @@ void WorldMap::renderImGuiOverlay(const glm::vec3& playerRenderPos, int screenWi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Taxi node markers — flight master icons on the map
|
||||||
|
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD && !taxiNodes_.empty()) {
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
for (const auto& node : taxiNodes_) {
|
||||||
|
if (!node.known) continue;
|
||||||
|
if (static_cast<int>(node.mapId) != currentMapId_) continue;
|
||||||
|
|
||||||
|
glm::vec3 rPos = core::coords::canonicalToRender(
|
||||||
|
glm::vec3(node.wowX, node.wowY, node.wowZ));
|
||||||
|
glm::vec2 uv = renderPosToMapUV(rPos, currentIdx);
|
||||||
|
if (uv.x < 0.0f || uv.x > 1.0f || uv.y < 0.0f || uv.y > 1.0f) continue;
|
||||||
|
|
||||||
|
float px = imgMin.x + uv.x * displayW;
|
||||||
|
float py = imgMin.y + uv.y * displayH;
|
||||||
|
|
||||||
|
// Flight-master icon: yellow diamond with dark border
|
||||||
|
constexpr float H = 5.0f; // half-size of diamond
|
||||||
|
ImVec2 top2(px, py - H);
|
||||||
|
ImVec2 right2(px + H, py );
|
||||||
|
ImVec2 bot2(px, py + H);
|
||||||
|
ImVec2 left2(px - H, py );
|
||||||
|
drawList->AddQuadFilled(top2, right2, bot2, left2,
|
||||||
|
IM_COL32(255, 215, 0, 230));
|
||||||
|
drawList->AddQuad(top2, right2, bot2, left2,
|
||||||
|
IM_COL32(80, 50, 0, 200), 1.2f);
|
||||||
|
|
||||||
|
// Tooltip on hover
|
||||||
|
if (!node.name.empty()) {
|
||||||
|
float mdx = mp.x - px, mdy = mp.y - py;
|
||||||
|
if (mdx * mdx + mdy * mdy < 49.0f) {
|
||||||
|
ImGui::SetTooltip("%s\n(Flight Master)", node.name.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corpse marker — skull X shown when player is a ghost with unclaimed corpse
|
||||||
|
if (hasCorpse_ && currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||||
|
glm::vec2 uv = renderPosToMapUV(corpseRenderPos_, currentIdx);
|
||||||
|
if (uv.x >= 0.0f && uv.x <= 1.0f && uv.y >= 0.0f && uv.y <= 1.0f) {
|
||||||
|
float cx = imgMin.x + uv.x * displayW;
|
||||||
|
float cy = imgMin.y + uv.y * displayH;
|
||||||
|
constexpr float R = 5.0f; // cross arm half-length
|
||||||
|
constexpr float T = 1.8f; // line thickness
|
||||||
|
// Dark outline
|
||||||
|
drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||||
|
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||||
|
drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||||
|
IM_COL32(0, 0, 0, 220), T + 1.5f);
|
||||||
|
// Bone-white X
|
||||||
|
drawList->AddLine(ImVec2(cx - R, cy - R), ImVec2(cx + R, cy + R),
|
||||||
|
IM_COL32(230, 220, 200, 240), T);
|
||||||
|
drawList->AddLine(ImVec2(cx + R, cy - R), ImVec2(cx - R, cy + R),
|
||||||
|
IM_COL32(230, 220, 200, 240), T);
|
||||||
|
// Tooltip on hover
|
||||||
|
ImVec2 mp = ImGui::GetMousePos();
|
||||||
|
float dx = mp.x - cx, dy = mp.y - cy;
|
||||||
|
if (dx * dx + dy * dy < 64.0f) {
|
||||||
|
ImGui::SetTooltip("Your corpse");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Hover coordinate display — show WoW coordinates under cursor
|
// Hover coordinate display — show WoW coordinates under cursor
|
||||||
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
if (currentIdx >= 0 && viewLevel != ViewLevel::WORLD) {
|
||||||
auto& io = ImGui::GetIO();
|
auto& io = ImGui::GetIO();
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1738,12 +1738,27 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
||||||
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
ImVec4 gold(1.0f, 0.84f, 0.0f, 1.0f);
|
||||||
ImVec4 gray(0.6f, 0.6f, 0.6f, 1.0f);
|
ImVec4 gray(0.6f, 0.6f, 0.6f, 1.0f);
|
||||||
|
|
||||||
|
static const char* kStatTooltips[5] = {
|
||||||
|
"Increases your melee attack power by 2.\nIncreases your block value.",
|
||||||
|
"Increases your Armor.\nIncreases ranged attack power by 2.\nIncreases your chance to dodge attacks and score critical strikes.",
|
||||||
|
"Increases Health by 10 per point.",
|
||||||
|
"Increases your Mana pool.\nIncreases your chance to score a critical strike with spells.",
|
||||||
|
"Increases Health and Mana regeneration."
|
||||||
|
};
|
||||||
|
|
||||||
// Armor (no base)
|
// Armor (no base)
|
||||||
|
ImGui::BeginGroup();
|
||||||
if (totalArmor > 0) {
|
if (totalArmor > 0) {
|
||||||
ImGui::TextColored(gold, "Armor: %d", totalArmor);
|
ImGui::TextColored(gold, "Armor: %d", totalArmor);
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(gray, "Armor: 0");
|
ImGui::TextColored(gray, "Armor: 0");
|
||||||
}
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::TextWrapped("Reduces damage taken from physical attacks.");
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
if (serverStats) {
|
if (serverStats) {
|
||||||
// Server-authoritative stats from UNIT_FIELD_STAT0-4: show total and item bonus.
|
// Server-authoritative stats from UNIT_FIELD_STAT0-4: show total and item bonus.
|
||||||
|
|
@ -1753,6 +1768,7 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 5; ++i) {
|
||||||
int32_t total = serverStats[i];
|
int32_t total = serverStats[i];
|
||||||
int32_t bonus = itemBonuses[i];
|
int32_t bonus = itemBonuses[i];
|
||||||
|
ImGui::BeginGroup();
|
||||||
if (bonus > 0) {
|
if (bonus > 0) {
|
||||||
ImGui::TextColored(white, "%s: %d", statNames[i], total);
|
ImGui::TextColored(white, "%s: %d", statNames[i], total);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
@ -1760,12 +1776,19 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(gray, "%s: %d", statNames[i], total);
|
ImGui::TextColored(gray, "%s: %d", statNames[i], total);
|
||||||
}
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::TextWrapped("%s", kStatTooltips[i]);
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback: estimated base (20 + level) plus item query bonuses.
|
// Fallback: estimated base (20 + level) plus item query bonuses.
|
||||||
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
int32_t baseStat = 20 + static_cast<int32_t>(playerLevel);
|
||||||
auto renderStat = [&](const char* name, int32_t equipBonus) {
|
auto renderStat = [&](const char* name, int32_t equipBonus, const char* tooltip) {
|
||||||
int32_t total = baseStat + equipBonus;
|
int32_t total = baseStat + equipBonus;
|
||||||
|
ImGui::BeginGroup();
|
||||||
if (equipBonus > 0) {
|
if (equipBonus > 0) {
|
||||||
ImGui::TextColored(white, "%s: %d", name, total);
|
ImGui::TextColored(white, "%s: %d", name, total);
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
@ -1773,12 +1796,18 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
||||||
} else {
|
} else {
|
||||||
ImGui::TextColored(gray, "%s: %d", name, total);
|
ImGui::TextColored(gray, "%s: %d", name, total);
|
||||||
}
|
}
|
||||||
|
ImGui::EndGroup();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::TextWrapped("%s", tooltip);
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
renderStat("Strength", itemStr);
|
renderStat("Strength", itemStr, kStatTooltips[0]);
|
||||||
renderStat("Agility", itemAgi);
|
renderStat("Agility", itemAgi, kStatTooltips[1]);
|
||||||
renderStat("Stamina", itemSta);
|
renderStat("Stamina", itemSta, kStatTooltips[2]);
|
||||||
renderStat("Intellect", itemInt);
|
renderStat("Intellect", itemInt, kStatTooltips[3]);
|
||||||
renderStat("Spirit", itemSpi);
|
renderStat("Spirit", itemSpi, kStatTooltips[4]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Secondary stats from equipped items
|
// Secondary stats from equipped items
|
||||||
|
|
@ -1789,27 +1818,34 @@ void InventoryScreen::renderStatsPanel(game::Inventory& inventory, uint32_t play
|
||||||
if (hasSecondary) {
|
if (hasSecondary) {
|
||||||
ImGui::Spacing();
|
ImGui::Spacing();
|
||||||
ImGui::Separator();
|
ImGui::Separator();
|
||||||
auto renderSecondary = [&](const char* name, int32_t val) {
|
auto renderSecondary = [&](const char* name, int32_t val, const char* tooltip) {
|
||||||
if (val > 0) {
|
if (val > 0) {
|
||||||
|
ImGui::BeginGroup();
|
||||||
ImGui::TextColored(green, "+%d %s", val, name);
|
ImGui::TextColored(green, "+%d %s", val, name);
|
||||||
|
ImGui::EndGroup();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::TextWrapped("%s", tooltip);
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
renderSecondary("Attack Power", itemAP);
|
renderSecondary("Attack Power", itemAP, "Increases the damage of your melee and ranged attacks.");
|
||||||
renderSecondary("Spell Power", itemSP);
|
renderSecondary("Spell Power", itemSP, "Increases the damage and healing of your spells.");
|
||||||
renderSecondary("Hit Rating", itemHit);
|
renderSecondary("Hit Rating", itemHit, "Reduces the chance your attacks will miss.");
|
||||||
renderSecondary("Crit Rating", itemCrit);
|
renderSecondary("Crit Rating", itemCrit, "Increases your critical strike chance.");
|
||||||
renderSecondary("Haste Rating", itemHaste);
|
renderSecondary("Haste Rating", itemHaste, "Increases attack speed and spell casting speed.");
|
||||||
renderSecondary("Resilience", itemResil);
|
renderSecondary("Resilience", itemResil, "Reduces the chance you will be critically hit.\nReduces damage taken from critical hits.");
|
||||||
renderSecondary("Expertise", itemExpertise);
|
renderSecondary("Expertise", itemExpertise,"Reduces the chance your attacks will be dodged or parried.");
|
||||||
renderSecondary("Defense Rating", itemDefense);
|
renderSecondary("Defense Rating", itemDefense, "Reduces the chance enemies will critically hit you.");
|
||||||
renderSecondary("Dodge Rating", itemDodge);
|
renderSecondary("Dodge Rating", itemDodge, "Increases your chance to dodge attacks.");
|
||||||
renderSecondary("Parry Rating", itemParry);
|
renderSecondary("Parry Rating", itemParry, "Increases your chance to parry attacks.");
|
||||||
renderSecondary("Block Rating", itemBlock);
|
renderSecondary("Block Rating", itemBlock, "Increases your chance to block attacks with your shield.");
|
||||||
renderSecondary("Block Value", itemBlockVal);
|
renderSecondary("Block Value", itemBlockVal, "Increases the amount of damage your shield blocks.");
|
||||||
renderSecondary("Armor Penetration",itemArmorPen);
|
renderSecondary("Armor Penetration",itemArmorPen, "Reduces the armor of your target.");
|
||||||
renderSecondary("Spell Penetration",itemSpellPen);
|
renderSecondary("Spell Penetration",itemSpellPen, "Reduces your target's resistance to your spells.");
|
||||||
renderSecondary("Mana per 5 sec", itemMp5);
|
renderSecondary("Mana per 5 sec", itemMp5, "Restores mana every 5 seconds, even while casting.");
|
||||||
renderSecondary("Health per 5 sec", itemHp5);
|
renderSecondary("Health per 5 sec", itemHp5, "Restores health every 5 seconds.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Elemental resistances from server update fields
|
// Elemental resistances from server update fields
|
||||||
|
|
@ -2299,8 +2335,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
} else if (kind == SlotKind::BACKPACK && backpackIndex >= 0) {
|
||||||
LOG_INFO("Right-click backpack item: name='", item.name,
|
LOG_INFO("Right-click backpack item: name='", item.name,
|
||||||
"' inventoryType=", (int)item.inventoryType,
|
"' inventoryType=", (int)item.inventoryType,
|
||||||
" itemId=", item.itemId);
|
" itemId=", item.itemId,
|
||||||
if (item.inventoryType > 0) {
|
" startQuestId=", item.startQuestId);
|
||||||
|
if (item.startQuestId != 0) {
|
||||||
|
uint64_t iGuid = gameHandler_->getBackpackItemGuid(backpackIndex);
|
||||||
|
gameHandler_->offerQuestFromItem(iGuid, item.startQuestId);
|
||||||
|
} else if (item.inventoryType > 0) {
|
||||||
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
gameHandler_->autoEquipItemBySlot(backpackIndex);
|
||||||
} else {
|
} else {
|
||||||
gameHandler_->useItemBySlot(backpackIndex);
|
gameHandler_->useItemBySlot(backpackIndex);
|
||||||
|
|
@ -2308,8 +2348,12 @@ void InventoryScreen::renderItemSlot(game::Inventory& inventory, const game::Ite
|
||||||
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
} else if (kind == SlotKind::BACKPACK && isBagSlot) {
|
||||||
LOG_INFO("Right-click bag item: name='", item.name,
|
LOG_INFO("Right-click bag item: name='", item.name,
|
||||||
"' inventoryType=", (int)item.inventoryType,
|
"' inventoryType=", (int)item.inventoryType,
|
||||||
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex);
|
" bagIndex=", bagIndex, " slotIndex=", bagSlotIndex,
|
||||||
if (item.inventoryType > 0) {
|
" startQuestId=", item.startQuestId);
|
||||||
|
if (item.startQuestId != 0) {
|
||||||
|
uint64_t iGuid = gameHandler_->getBagItemGuid(bagIndex, bagSlotIndex);
|
||||||
|
gameHandler_->offerQuestFromItem(iGuid, item.startQuestId);
|
||||||
|
} else if (item.inventoryType > 0) {
|
||||||
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
|
gameHandler_->autoEquipItemInBag(bagIndex, bagSlotIndex);
|
||||||
} else {
|
} else {
|
||||||
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
|
gameHandler_->useItemInBag(bagIndex, bagSlotIndex);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ void KeybindingManager::initializeDefaults() {
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V;
|
bindings_[static_cast<int>(Action::TOGGLE_NAMEPLATES)] = ImGuiKey_V;
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset)
|
bindings_[static_cast<int>(Action::TOGGLE_RAID_FRAMES)] = ImGuiKey_F; // Reassigned from R (now camera reset)
|
||||||
bindings_[static_cast<int>(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail)
|
bindings_[static_cast<int>(Action::TOGGLE_ACHIEVEMENTS)] = ImGuiKey_Y; // WoW standard key (Shift+Y in retail)
|
||||||
|
bindings_[static_cast<int>(Action::TOGGLE_SKILLS)] = ImGuiKey_K; // WoW standard: K opens Skills/Professions
|
||||||
}
|
}
|
||||||
|
|
||||||
bool KeybindingManager::isActionPressed(Action action, bool repeat) {
|
bool KeybindingManager::isActionPressed(Action action, bool repeat) {
|
||||||
|
|
@ -93,6 +94,7 @@ const char* KeybindingManager::getActionName(Action action) {
|
||||||
case Action::TOGGLE_NAMEPLATES: return "Nameplates";
|
case Action::TOGGLE_NAMEPLATES: return "Nameplates";
|
||||||
case Action::TOGGLE_RAID_FRAMES: return "Raid Frames";
|
case Action::TOGGLE_RAID_FRAMES: return "Raid Frames";
|
||||||
case Action::TOGGLE_ACHIEVEMENTS: return "Achievements";
|
case Action::TOGGLE_ACHIEVEMENTS: return "Achievements";
|
||||||
|
case Action::TOGGLE_SKILLS: return "Skills / Professions";
|
||||||
case Action::ACTION_COUNT: break;
|
case Action::ACTION_COUNT: break;
|
||||||
}
|
}
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
|
|
@ -158,6 +160,7 @@ void KeybindingManager::loadFromConfigFile(const std::string& filePath) {
|
||||||
else if (action == "toggle_raid_frames") actionIdx = static_cast<int>(Action::TOGGLE_RAID_FRAMES);
|
else if (action == "toggle_raid_frames") actionIdx = static_cast<int>(Action::TOGGLE_RAID_FRAMES);
|
||||||
else if (action == "toggle_quest_log") actionIdx = static_cast<int>(Action::TOGGLE_QUESTS); // legacy alias
|
else if (action == "toggle_quest_log") actionIdx = static_cast<int>(Action::TOGGLE_QUESTS); // legacy alias
|
||||||
else if (action == "toggle_achievements") actionIdx = static_cast<int>(Action::TOGGLE_ACHIEVEMENTS);
|
else if (action == "toggle_achievements") actionIdx = static_cast<int>(Action::TOGGLE_ACHIEVEMENTS);
|
||||||
|
else if (action == "toggle_skills") actionIdx = static_cast<int>(Action::TOGGLE_SKILLS);
|
||||||
|
|
||||||
if (actionIdx < 0) continue;
|
if (actionIdx < 0) continue;
|
||||||
|
|
||||||
|
|
@ -254,6 +257,7 @@ void KeybindingManager::saveToConfigFile(const std::string& filePath) const {
|
||||||
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
{Action::TOGGLE_NAMEPLATES, "toggle_nameplates"},
|
||||||
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
{Action::TOGGLE_RAID_FRAMES, "toggle_raid_frames"},
|
||||||
{Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"},
|
{Action::TOGGLE_ACHIEVEMENTS, "toggle_achievements"},
|
||||||
|
{Action::TOGGLE_SKILLS, "toggle_skills"},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const auto& [action, nameStr] : actionMap) {
|
for (const auto& [action, nameStr] : actionMap) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue